feat: Provenance gains email + agent.onBehalfOf — cross-rung identity (F5 SLICE-2, #14)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+39
-6
@@ -25,10 +25,25 @@ export interface Anchor {
|
||||
fingerprint: Fingerprint;
|
||||
}
|
||||
|
||||
/** The reusable author field (spec §6.3). `agent` only present when kind=agent. */
|
||||
/**
|
||||
* The reusable author field (contract §3.3). `agent` only present when
|
||||
* kind=agent. F5 (spec §6.3, fork c): `email` is the cross-rung join key —
|
||||
* git's own identity model (forges map email → account); `onBehalfOf` records
|
||||
* the operator a machine acted for (rfc-app#46 §6.5 made data).
|
||||
*/
|
||||
export type Provenance =
|
||||
| { kind: "human"; id: string }
|
||||
| { kind: "agent"; id: string; agent: { sdk: string; model: string; sessionId: string } };
|
||||
| { kind: "human"; id: string; email?: string }
|
||||
| {
|
||||
kind: "agent";
|
||||
id: string;
|
||||
email?: string;
|
||||
agent: {
|
||||
sdk: string;
|
||||
model: string;
|
||||
sessionId: string;
|
||||
onBehalfOf?: { id: string; email?: string };
|
||||
};
|
||||
};
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
@@ -107,9 +122,27 @@ export function newId(prefix: string): string {
|
||||
}
|
||||
|
||||
function serializeProvenance(p: Provenance): Record<string, unknown> {
|
||||
return p.kind === "agent"
|
||||
? { kind: p.kind, id: p.id, agent: { sdk: p.agent.sdk, model: p.agent.model, sessionId: p.agent.sessionId } }
|
||||
: { kind: p.kind, id: p.id };
|
||||
const known: Record<string, unknown> = {
|
||||
kind: p.kind,
|
||||
id: p.id,
|
||||
...(p.email !== undefined ? { email: p.email } : {}),
|
||||
};
|
||||
if (p.kind === "agent") {
|
||||
known.agent = {
|
||||
sdk: p.agent.sdk,
|
||||
model: p.agent.model,
|
||||
sessionId: p.agent.sessionId,
|
||||
...(p.agent.onBehalfOf !== undefined
|
||||
? {
|
||||
onBehalfOf: {
|
||||
id: p.agent.onBehalfOf.id,
|
||||
...(p.agent.onBehalfOf.email !== undefined ? { email: p.agent.onBehalfOf.email } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
return known;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -57,6 +57,52 @@ describe("serializeArtifact", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("cross-rung identity (F5 SLICE-2)", () => {
|
||||
it("round-trips email on human provenance, omitted when absent", () => {
|
||||
const a = emptyArtifact("docs/x.md");
|
||||
a.anchors["a_1"] = { fingerprint: { text: "alpha", before: "", after: "", lineHint: 0 } };
|
||||
a.threads.push({
|
||||
id: "t_1",
|
||||
anchorId: "a_1",
|
||||
status: "open",
|
||||
messages: [
|
||||
{ id: "m_1", author: { kind: "human", id: "ben", email: "ben@wiggleverse.org" }, body: "hi", createdAt: "2026-06-10T00:00:00.000Z" },
|
||||
{ id: "m_2", author: { kind: "human", id: "anon" }, body: "yo", createdAt: "2026-06-10T00:00:01.000Z" },
|
||||
],
|
||||
});
|
||||
const out = serializeArtifact(a);
|
||||
const parsed = JSON.parse(out) as Artifact;
|
||||
expect(parsed.threads[0].messages[0].author).toEqual({ kind: "human", id: "ben", email: "ben@wiggleverse.org" });
|
||||
expect("email" in parsed.threads[0].messages[1].author).toBe(false);
|
||||
expect(serializeArtifact(parsed)).toBe(out);
|
||||
});
|
||||
|
||||
it("round-trips agent.onBehalfOf and omits it when absent", () => {
|
||||
const a = emptyArtifact("docs/x.md");
|
||||
a.attributions.push({
|
||||
id: "at_1",
|
||||
anchorId: "a_1",
|
||||
author: {
|
||||
kind: "agent",
|
||||
id: "claude",
|
||||
agent: { sdk: "@cline/sdk", model: "sonnet", sessionId: "run-1", onBehalfOf: { id: "benstull", email: "ben@wiggleverse.org" } },
|
||||
},
|
||||
createdAt: "2026-06-10T00:00:00.000Z",
|
||||
updatedAt: "2026-06-10T00:00:00.000Z",
|
||||
});
|
||||
const out = serializeArtifact(a);
|
||||
const parsed = JSON.parse(out) as Artifact;
|
||||
const author = parsed.attributions[0].author;
|
||||
expect(author.kind).toBe("agent");
|
||||
if (author.kind === "agent") {
|
||||
expect(author.agent.onBehalfOf).toEqual({ id: "benstull", email: "ben@wiggleverse.org" });
|
||||
}
|
||||
expect(out.includes("onBehalfOf")).toBe(true);
|
||||
const a2 = emptyArtifact("docs/x.md");
|
||||
expect(serializeArtifact(a2).includes("onBehalfOf")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("attributions[] (F3 SLICE-1)", () => {
|
||||
it("round-trips an attribution record through serialize → parse", () => {
|
||||
const a = emptyArtifact("docs/x.md");
|
||||
|
||||
Reference in New Issue
Block a user