From 4a78ed8ec7e579347d7fa0c4f3e32eea59fa179e Mon Sep 17 00:00:00 2001 From: Yash Date: Mon, 31 Aug 2026 22:44:31 +0000 Subject: [PATCH 01/15] feat(tools): carry agent runtime provenance Keep opaque callsite evidence and signed lineage receipts out of request and result JSON, forwarding receipts only for exact direct SDK result handoffs.\n\nRefs: SAP-3020 --- .changeset/calm-agents-carry.md | 8 + packages/tools/package.json | 5 + .../src/_internal/agent-runtime-provenance.ts | 95 +++++++++ packages/tools/src/agents/README.md | 13 ++ packages/tools/src/agents/index.ts | 72 +++++-- .../src/agents/runtime-provenance.spec.ts | 199 ++++++++++++++++++ 6 files changed, 374 insertions(+), 18 deletions(-) create mode 100644 .changeset/calm-agents-carry.md create mode 100644 packages/tools/src/_internal/agent-runtime-provenance.ts create mode 100644 packages/tools/src/agents/runtime-provenance.spec.ts diff --git a/.changeset/calm-agents-carry.md b/.changeset/calm-agents-carry.md new file mode 100644 index 000000000..e65207c47 --- /dev/null +++ b/.changeset/calm-agents-carry.md @@ -0,0 +1,8 @@ +--- +"@sapiom/tools": minor +--- + +Add a private v1 runtime-provenance carrier for agent invocations. Instrumented +calls send opaque callsite evidence out of band, terminal results retain an +SDK-only server receipt, and only exact direct result-to-input handoffs forward +that receipt. Request/result JSON and calls without metadata remain unchanged. diff --git a/packages/tools/package.json b/packages/tools/package.json index 3a9a0228b..8d98396dd 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -87,6 +87,11 @@ "types": "./dist/cjs/stub/index.d.ts", "import": "./dist/esm/stub/index.js", "require": "./dist/cjs/stub/index.js" + }, + "./_internal/agent-runtime-provenance": { + "types": "./dist/cjs/_internal/agent-runtime-provenance.d.ts", + "import": "./dist/esm/_internal/agent-runtime-provenance.js", + "require": "./dist/cjs/_internal/agent-runtime-provenance.js" } }, "files": [ diff --git a/packages/tools/src/_internal/agent-runtime-provenance.ts b/packages/tools/src/_internal/agent-runtime-provenance.ts new file mode 100644 index 000000000..1f564f751 --- /dev/null +++ b/packages/tools/src/_internal/agent-runtime-provenance.ts @@ -0,0 +1,95 @@ +/** + * Private runtime-provenance bridge for instrumented agent bundles. + * + * This is deliberately an object-identity side channel: provenance never becomes + * part of AgentRunSpec, an agent input, or an AgentRunResult. The build adapter may + * associate an opaque callsite token with the exact spec it emits. Likewise, only + * an exact SDK result object can carry a server-signed receipt into the next agent + * boundary; copies and nested/transformed values intentionally lose the sidecar. + * + * @internal This is a versioned integration contract, not an author-facing API. + */ + +export const AGENT_RUNTIME_PROVENANCE_VERSION = 1 as const; + +export const AGENT_RUNTIME_PROVENANCE_VERSION_HEADER = + "x-sapiom-runtime-provenance-version"; +export const AGENT_RUNTIME_CALLSITE_HEADER = + "x-sapiom-runtime-callsite-evidence"; +export const AGENT_RUNTIME_LINEAGE_HEADER = "x-sapiom-runtime-lineage-receipt"; + +export interface AgentRuntimeProvenanceV1 { + readonly version: typeof AGENT_RUNTIME_PROVENANCE_VERSION; + /** Opaque build-owned reference. It contains no graph identity. */ + readonly callsite: string; +} + +interface AgentRuntimeLineageV1 { + readonly version: typeof AGENT_RUNTIME_PROVENANCE_VERSION; + /** Opaque server-signed receipt. Its contents are never decoded by the SDK. */ + readonly receipt: string; +} + +const MAX_OPAQUE_TOKEN_LENGTH = 8_192; +const invocationProvenance = new WeakMap(); +const resultLineage = new WeakMap(); + +function supportedOpaqueToken(value: unknown): value is string { + return ( + typeof value === "string" && + value.length > 0 && + value.length <= MAX_OPAQUE_TOKEN_LENGTH && + !/[\r\n]/.test(value) + ); +} + +/** Associate build-derived v1 evidence without mutating or wrapping the spec. */ +export function carryAgentRuntimeProvenance( + spec: T, + provenance: AgentRuntimeProvenanceV1, +): T { + if ( + provenance.version === AGENT_RUNTIME_PROVENANCE_VERSION && + supportedOpaqueToken(provenance.callsite) + ) { + invocationProvenance.set(spec, provenance); + } + return spec; +} + +/** @internal Headers for one immediate invocation; never recursively inspects input. */ +export function agentRuntimeProvenanceHeaders( + spec: object, + directInput: unknown, +): Record { + const callsite = invocationProvenance.get(spec); + const lineage = + directInput !== null && typeof directInput === "object" + ? resultLineage.get(directInput) + : undefined; + if (!callsite && !lineage) return {}; + return { + [AGENT_RUNTIME_PROVENANCE_VERSION_HEADER]: String( + AGENT_RUNTIME_PROVENANCE_VERSION, + ), + ...(callsite ? { [AGENT_RUNTIME_CALLSITE_HEADER]: callsite.callsite } : {}), + ...(lineage ? { [AGENT_RUNTIME_LINEAGE_HEADER]: lineage.receipt } : {}), + }; +} + +/** @internal Retain only a supported response receipt on the exact SDK result. */ +export function retainAgentRuntimeLineage( + result: object, + version: string | null, + receipt: string | null, +): void { + if ( + version === String(AGENT_RUNTIME_PROVENANCE_VERSION) && + supportedOpaqueToken(receipt) + ) { + resultLineage.set(result, { + version: AGENT_RUNTIME_PROVENANCE_VERSION, + receipt, + }); + } +} diff --git a/packages/tools/src/agents/README.md b/packages/tools/src/agents/README.md index 16ab6ffd8..c29ef7a44 100644 --- a/packages/tools/src/agents/README.md +++ b/packages/tools/src/agents/README.md @@ -45,6 +45,19 @@ const useResult = defineStep({ - **Failure is data, not an exception.** The result is discriminated on `status` (`"completed" | "failed"`). A failed run resumes your step with `status: "failed"` and an `error` to branch on — it does not throw. Validate an incoming payload with `agents.agentResultSchema.parse(value)` if you want a runtime check. +### Runtime provenance (internal) + +Instrumented bundles may associate an opaque v1 callsite token with an agent +invocation through `@sapiom/tools/_internal/agent-runtime-provenance`. The token +travels in dedicated request headers, never in `AgentRunSpec` or its JSON body. +When a terminal response carries a supported server-signed lineage receipt, the +SDK retains it in an object-identity sidecar on the returned result. The receipt +is forwarded only when that exact result object is passed directly as the next +agent's `input`; copies, nested values, transformed primitives, delayed queues, +storage, and arbitrary objects do not inherit it. The SDK treats both values as +opaque and exposes no caller, callee, bundle, or execution identity through this +contract. Missing or unsupported metadata preserves the legacy behavior. + - **Addressed by slug.** `definition` is the deployed agent's slug — its stable handle. `input` is passed to its entry step. - **`idempotencyKey` deduplicates.** Repeating a launch with the same key returns the existing run instead of starting a new one. diff --git a/packages/tools/src/agents/index.ts b/packages/tools/src/agents/index.ts index 09225b78c..252f19b32 100644 --- a/packages/tools/src/agents/index.ts +++ b/packages/tools/src/agents/index.ts @@ -15,6 +15,12 @@ * a pausable handle). An orchestration is addressed by its **slug** (its stable handle). */ import { Transport, defaultTransport } from "../_client/index.js"; +import { + AGENT_RUNTIME_LINEAGE_HEADER, + AGENT_RUNTIME_PROVENANCE_VERSION_HEADER, + agentRuntimeProvenanceHeaders, + retainAgentRuntimeLineage, +} from "../_internal/agent-runtime-provenance.js"; import type { DispatchHandle } from "../dispatch.js"; const DEFAULT_BASE_URL = @@ -107,9 +113,7 @@ export class AgentResultSchemaError extends Error {} * `output` itself is the child orchestration's contract, not validated here. */ export const agentResultSchema = { - parse( - value: unknown, - ): AgentRunResultPayload { + parse(value: unknown): AgentRunResultPayload { const fail = (msg: string): never => { throw new AgentResultSchemaError( `invalid orchestration result payload: ${msg}`, @@ -144,10 +148,7 @@ export interface RunHandle extends DispatchHandle { /** Fetch the current status without blocking. */ status(): Promise; /** Poll to a terminal state and resolve the run result. */ - wait(opts?: { - timeoutMs?: number; - pollMs?: number; - }): Promise; + wait(opts?: { timeoutMs?: number; pollMs?: number }): Promise; } /** @@ -181,12 +182,20 @@ interface ExecutionDoc { * engine stamps on the eventually-fired child, so the resume lands. Pause-only: there is no child * to poll until the scheduled time, so `status`/`wait` throw. */ -async function launchScheduled(spec: AgentRunSpec, transport: Transport, baseUrl: string): Promise { +async function launchScheduled( + spec: AgentRunSpec, + transport: Transport, + baseUrl: string, +): Promise { const res = await transport.request<{ id: string }>( `${baseUrl}/agents/v1/definitions/${encodeURIComponent(spec.definition)}/triggers`, { method: "POST", - body: JSON.stringify({ kind: "schedule_once", at: spec.at, input: spec.input ?? {} }), + body: JSON.stringify({ + kind: "schedule_once", + at: spec.at, + input: spec.input ?? {}, + }), headers: workflowResumeHeaders(transport.resumeToken), }, ); @@ -197,7 +206,10 @@ async function launchScheduled(spec: AgentRunSpec, transport: Transport, baseUrl }; return { executionId: "", // no child execution exists until the schedule fires - dispatch: { correlationId: `trigger-${res.id}`, resultSignal: AGENTS_RESULT_SIGNAL }, + dispatch: { + correlationId: `trigger-${res.id}`, + resultSignal: AGENTS_RESULT_SIGNAL, + }, status: notAvailable, wait: notAvailable, }; @@ -219,15 +231,37 @@ export async function launch( input: spec.input ?? {}, idempotencyKey: spec.idempotencyKey, }), - headers: workflowResumeHeaders(transport.resumeToken), + headers: { + ...workflowResumeHeaders(transport.resumeToken), + ...agentRuntimeProvenanceHeaders(spec, spec.input), + }, }, ); const executionId = res.executionId; - const fetchDoc = () => - transport.request( - `${baseUrl}/agents/v1/executions/${encodeURIComponent(executionId)}`, - ); + const fetchDoc = async (): Promise<{ + doc: ExecutionDoc; + provenanceVersion: string | null; + lineageReceipt: string | null; + }> => { + const url = `${baseUrl}/agents/v1/executions/${encodeURIComponent(executionId)}`; + const response = await transport.fetch(url, { + headers: { "content-type": "application/json" }, + }); + if (!response.ok) { + throw new Error( + `GET ${url} → ${response.status} ${await response.text()}`, + ); + } + return { + doc: (await response.json()) as ExecutionDoc, + provenanceVersion: + response.headers?.get?.(AGENT_RUNTIME_PROVENANCE_VERSION_HEADER) ?? + null, + lineageReceipt: + response.headers?.get?.(AGENT_RUNTIME_LINEAGE_HEADER) ?? null, + }; + }; return { executionId, @@ -238,20 +272,22 @@ export async function launch( resultSignal: AGENTS_RESULT_SIGNAL, }, async status() { - return (await fetchDoc()).status; + return (await fetchDoc()).doc.status; }, async wait({ timeoutMs = 60 * 60_000, pollMs = 3_000 } = {}) { const deadline = Date.now() + timeoutMs; // eslint-disable-next-line no-constant-condition while (true) { - const d = await fetchDoc(); + const { doc: d, provenanceVersion, lineageReceipt } = await fetchDoc(); if (TERMINAL.has(d.status)) { - return { + const result: AgentRunResult = { executionId, status: d.status, output: d.output ?? null, error: d.error ?? null, }; + retainAgentRuntimeLineage(result, provenanceVersion, lineageReceipt); + return result; } if (Date.now() > deadline) { throw new Error( diff --git a/packages/tools/src/agents/runtime-provenance.spec.ts b/packages/tools/src/agents/runtime-provenance.spec.ts new file mode 100644 index 000000000..4949ff3f4 --- /dev/null +++ b/packages/tools/src/agents/runtime-provenance.spec.ts @@ -0,0 +1,199 @@ +import { createClient } from "../index.js"; +import { + AGENT_RUNTIME_CALLSITE_HEADER, + AGENT_RUNTIME_LINEAGE_HEADER, + AGENT_RUNTIME_PROVENANCE_VERSION_HEADER, + carryAgentRuntimeProvenance, +} from "../_internal/agent-runtime-provenance.js"; + +interface CapturedCall { + url: string; + init: RequestInit; +} + +function response( + value: unknown, + status = 200, + headers: Record = {}, +): Response { + return { + ok: status >= 200 && status < 300, + status, + headers: new Headers(headers), + json: async () => value, + text: async () => JSON.stringify(value), + } as Response; +} + +function agentServer( + opts: { + receiptVersion?: string; + receipt?: string; + } = {}, +): { fetch: typeof globalThis.fetch; calls: CapturedCall[] } { + const calls: CapturedCall[] = []; + let nextExecution = 0; + const fetch = (async ( + input: string | URL | Request, + init: RequestInit = {}, + ) => { + const url = String(input); + calls.push({ url, init }); + if (init.method === "POST") { + nextExecution += 1; + return response( + { status: "enqueued", executionId: `exec-${nextExecution}` }, + 201, + ); + } + return response( + { status: "completed", output: { ok: true }, error: null }, + 200, + { + ...(opts.receiptVersion + ? { + [AGENT_RUNTIME_PROVENANCE_VERSION_HEADER]: opts.receiptVersion, + } + : {}), + ...(opts.receipt + ? { [AGENT_RUNTIME_LINEAGE_HEADER]: opts.receipt } + : {}), + }, + ); + }) as typeof globalThis.fetch; + return { fetch, calls }; +} + +function header(call: CapturedCall, name: string): string | undefined { + const headers = call.init.headers as Record; + return headers?.[name]; +} + +function posts(calls: CapturedCall[]): CapturedCall[] { + return calls.filter((call) => call.init.method === "POST"); +} + +describe("agents runtime provenance v1", () => { + it("launch carries opaque callsite evidence out of band and wait retains a private receipt", async () => { + const server = agentServer({ + receiptVersion: "1", + receipt: "signed.receipt", + }); + const client = createClient({ apiKey: "k", fetch: server.fetch }); + const spec = carryAgentRuntimeProvenance( + { definition: "child", input: { public: true } }, + { version: 1, callsite: "callsite.opaque" }, + ); + + const handle = await client.agents.launch(spec); + const result = await handle.wait({ pollMs: 1 }); + const launch = posts(server.calls)[0]!; + + expect(header(launch, AGENT_RUNTIME_PROVENANCE_VERSION_HEADER)).toBe("1"); + expect(header(launch, AGENT_RUNTIME_CALLSITE_HEADER)).toBe( + "callsite.opaque", + ); + expect(JSON.parse(String(launch.init.body))).toEqual({ + input: { public: true }, + }); + expect(result).toEqual({ + executionId: "exec-1", + status: "completed", + output: { ok: true }, + error: null, + }); + expect(Object.keys(result)).toEqual([ + "executionId", + "status", + "output", + "error", + ]); + expect(JSON.stringify(result)).not.toContain("signed.receipt"); + }); + + it("run forwards a receipt only when the exact SDK result is the next input", async () => { + const server = agentServer({ + receiptVersion: "1", + receipt: "signed.direct", + }); + const client = createClient({ apiKey: "k", fetch: server.fetch }); + const result = await client.agents.run({ definition: "producer" }); + + await client.agents.run({ + definition: "consumer", + input: result as unknown as Record, + }); + + const secondLaunch = posts(server.calls)[1]!; + expect(header(secondLaunch, AGENT_RUNTIME_PROVENANCE_VERSION_HEADER)).toBe( + "1", + ); + expect(header(secondLaunch, AGENT_RUNTIME_LINEAGE_HEADER)).toBe( + "signed.direct", + ); + expect(JSON.parse(String(secondLaunch.init.body)).input).toEqual(result); + }); + + it.each([ + ["a copied result", (result: object) => ({ ...result })], + ["a nested result", (result: object) => ({ result })], + ["a transformed primitive", () => ({ value: "ok" })], + ])("does not infer lineage through %s", async (_label, toInput) => { + const server = agentServer({ + receiptVersion: "1", + receipt: "signed.private", + }); + const client = createClient({ apiKey: "k", fetch: server.fetch }); + const result = await client.agents.run({ definition: "producer" }); + + await client.agents.run({ + definition: "consumer", + input: toInput(result), + }); + + const secondLaunch = posts(server.calls)[1]!; + expect(header(secondLaunch, AGENT_RUNTIME_LINEAGE_HEADER)).toBeUndefined(); + expect( + header(secondLaunch, AGENT_RUNTIME_PROVENANCE_VERSION_HEADER), + ).toBeUndefined(); + }); + + it("ignores an unsupported receipt version", async () => { + const server = agentServer({ + receiptVersion: "2", + receipt: "signed.future", + }); + const client = createClient({ apiKey: "k", fetch: server.fetch }); + const result = await client.agents.run({ definition: "producer" }); + await client.agents.run({ + definition: "consumer", + input: result as unknown as Record, + }); + + expect( + header(posts(server.calls)[1]!, AGENT_RUNTIME_LINEAGE_HEADER), + ).toBeUndefined(); + }); + + it("preserves legacy request and result behavior when provenance is absent", async () => { + const server = agentServer(); + const client = createClient({ apiKey: "k", fetch: server.fetch }); + const result = await client.agents.run({ + definition: "legacy", + input: { value: 1 }, + idempotencyKey: "same", + }); + const launch = posts(server.calls)[0]!; + + expect(header(launch, AGENT_RUNTIME_CALLSITE_HEADER)).toBeUndefined(); + expect(header(launch, AGENT_RUNTIME_LINEAGE_HEADER)).toBeUndefined(); + expect( + header(launch, AGENT_RUNTIME_PROVENANCE_VERSION_HEADER), + ).toBeUndefined(); + expect(JSON.parse(String(launch.init.body))).toEqual({ + input: { value: 1 }, + idempotencyKey: "same", + }); + expect(result.output).toEqual({ ok: true }); + }); +}); From b3d6a56ca192266703fdaf15a0869436c2ffb061 Mon Sep 17 00:00:00 2001 From: Yash Date: Mon, 31 Aug 2026 22:48:17 +0000 Subject: [PATCH 02/15] test(tools): harden provenance handoff boundaries Exercise direct output identity, delayed-dispatch exclusion, and error privacy for the v1 carrier.\n\nRefs: SAP-3020 --- packages/tools/src/agents/index.ts | 10 +++ .../src/agents/runtime-provenance.spec.ts | 76 +++++++++++++++++-- 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/packages/tools/src/agents/index.ts b/packages/tools/src/agents/index.ts index 252f19b32..5a6b90051 100644 --- a/packages/tools/src/agents/index.ts +++ b/packages/tools/src/agents/index.ts @@ -287,6 +287,16 @@ export async function launch( error: d.error ?? null, }; retainAgentRuntimeLineage(result, provenanceVersion, lineageReceipt); + // This exact output object is the author-facing value commonly handed + // to the next agent. Copies, nested values, and primitives remain + // deliberately unassociated. + if (d.output !== null && typeof d.output === "object") { + retainAgentRuntimeLineage( + d.output, + provenanceVersion, + lineageReceipt, + ); + } return result; } if (Date.now() > deadline) { diff --git a/packages/tools/src/agents/runtime-provenance.spec.ts b/packages/tools/src/agents/runtime-provenance.spec.ts index 4949ff3f4..8adbb785e 100644 --- a/packages/tools/src/agents/runtime-provenance.spec.ts +++ b/packages/tools/src/agents/runtime-provenance.spec.ts @@ -111,7 +111,7 @@ describe("agents runtime provenance v1", () => { expect(JSON.stringify(result)).not.toContain("signed.receipt"); }); - it("run forwards a receipt only when the exact SDK result is the next input", async () => { + it("run forwards a receipt when the exact SDK output is the next input", async () => { const server = agentServer({ receiptVersion: "1", receipt: "signed.direct", @@ -121,7 +121,7 @@ describe("agents runtime provenance v1", () => { await client.agents.run({ definition: "consumer", - input: result as unknown as Record, + input: result.output as Record, }); const secondLaunch = posts(server.calls)[1]!; @@ -131,12 +131,14 @@ describe("agents runtime provenance v1", () => { expect(header(secondLaunch, AGENT_RUNTIME_LINEAGE_HEADER)).toBe( "signed.direct", ); - expect(JSON.parse(String(secondLaunch.init.body)).input).toEqual(result); + expect(JSON.parse(String(secondLaunch.init.body)).input).toEqual( + result.output, + ); }); it.each([ - ["a copied result", (result: object) => ({ ...result })], - ["a nested result", (result: object) => ({ result })], + ["a copied output", (result: object) => ({ ...result })], + ["a nested output", (result: object) => ({ result })], ["a transformed primitive", () => ({ value: "ok" })], ])("does not infer lineage through %s", async (_label, toInput) => { const server = agentServer({ @@ -148,7 +150,7 @@ describe("agents runtime provenance v1", () => { await client.agents.run({ definition: "consumer", - input: toInput(result), + input: toInput(result.output as object), }); const secondLaunch = posts(server.calls)[1]!; @@ -167,7 +169,7 @@ describe("agents runtime provenance v1", () => { const result = await client.agents.run({ definition: "producer" }); await client.agents.run({ definition: "consumer", - input: result as unknown as Record, + input: result.output as Record, }); expect( @@ -175,6 +177,66 @@ describe("agents runtime provenance v1", () => { ).toBeUndefined(); }); + it("does not carry provenance through delayed dispatch", async () => { + const server = agentServer({ + receiptVersion: "1", + receipt: "signed.queue", + }); + const client = createClient({ apiKey: "k", fetch: server.fetch }); + const result = await client.agents.run({ definition: "producer" }); + const scheduled = carryAgentRuntimeProvenance( + { + definition: "consumer", + input: result.output as Record, + at: "2026-09-01T00:00:00.000Z", + }, + { version: 1, callsite: "callsite.delayed" }, + ); + + await client.agents.launch(scheduled); + + const delayedLaunch = posts(server.calls)[1]!; + expect( + header(delayedLaunch, AGENT_RUNTIME_CALLSITE_HEADER), + ).toBeUndefined(); + expect(header(delayedLaunch, AGENT_RUNTIME_LINEAGE_HEADER)).toBeUndefined(); + expect( + header(delayedLaunch, AGENT_RUNTIME_PROVENANCE_VERSION_HEADER), + ).toBeUndefined(); + }); + + it("does not include private evidence in surfaced HTTP errors", async () => { + const calls: CapturedCall[] = []; + const fetch = (async ( + input: string | URL | Request, + init: RequestInit = {}, + ) => { + calls.push({ url: String(input), init }); + return response({ message: "rejected" }, 400); + }) as typeof globalThis.fetch; + const client = createClient({ apiKey: "k", fetch }); + const spec = carryAgentRuntimeProvenance( + { definition: "child" }, + { version: 1, callsite: "callsite.must-stay-private" }, + ); + + let error: unknown; + try { + await client.agents.launch(spec); + } catch (value) { + error = value; + } + + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("rejected"); + expect((error as Error).message).not.toContain( + "callsite.must-stay-private", + ); + expect(String(calls[0]!.init.body)).not.toContain( + "callsite.must-stay-private", + ); + }); + it("preserves legacy request and result behavior when provenance is absent", async () => { const server = agentServer(); const client = createClient({ apiKey: "k", fetch: server.fetch }); From ca12dfd9291ca7ad3e4b08d13ab4b3558f99b0a1 Mon Sep 17 00:00:00 2001 From: Yash Date: Mon, 31 Aug 2026 22:49:25 +0000 Subject: [PATCH 03/15] docs(tools): clarify direct provenance handoffs Refs: SAP-3020 --- packages/tools/src/agents/README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/tools/src/agents/README.md b/packages/tools/src/agents/README.md index c29ef7a44..7ff624c66 100644 --- a/packages/tools/src/agents/README.md +++ b/packages/tools/src/agents/README.md @@ -51,12 +51,13 @@ Instrumented bundles may associate an opaque v1 callsite token with an agent invocation through `@sapiom/tools/_internal/agent-runtime-provenance`. The token travels in dedicated request headers, never in `AgentRunSpec` or its JSON body. When a terminal response carries a supported server-signed lineage receipt, the -SDK retains it in an object-identity sidecar on the returned result. The receipt -is forwarded only when that exact result object is passed directly as the next -agent's `input`; copies, nested values, transformed primitives, delayed queues, -storage, and arbitrary objects do not inherit it. The SDK treats both values as -opaque and exposes no caller, callee, bundle, or execution identity through this -contract. Missing or unsupported metadata preserves the legacy behavior. +SDK retains it in object-identity sidecars on the returned result and its exact +object-valued `output`. The receipt is forwarded only when one of those exact +objects is passed directly as the next agent's `input`; copies, nested values, +transformed primitives, delayed queues, storage, and arbitrary objects do not +inherit it. The SDK treats both values as opaque and exposes no caller, callee, +bundle, or execution identity through this contract. Missing or unsupported +metadata preserves the legacy behavior. - **Addressed by slug.** `definition` is the deployed agent's slug — its stable handle. `input` is passed to its entry step. From aea6186c14f63cc024ae9982b35b69664e57a20a Mon Sep 17 00:00:00 2001 From: Yash Date: Mon, 31 Aug 2026 23:21:45 +0000 Subject: [PATCH 04/15] fix(tools): harden agent provenance boundaries Make callsite and lineage carriers one-shot, require build evidence for forwarding, keep receipt state lexical to the agent runtime, and redact reflected private values from invocation and status errors.\n\nRefs: SAP-3020 --- .../src/_internal/agent-runtime-provenance.ts | 84 +--- packages/tools/src/agents/index.ts | 212 ++++++-- .../src/agents/runtime-callsite-store.ts | 55 +++ .../src/agents/runtime-provenance.spec.ts | 458 +++++++++++++++++- 4 files changed, 669 insertions(+), 140 deletions(-) create mode 100644 packages/tools/src/agents/runtime-callsite-store.ts diff --git a/packages/tools/src/_internal/agent-runtime-provenance.ts b/packages/tools/src/_internal/agent-runtime-provenance.ts index 1f564f751..970565137 100644 --- a/packages/tools/src/_internal/agent-runtime-provenance.ts +++ b/packages/tools/src/_internal/agent-runtime-provenance.ts @@ -1,95 +1,27 @@ /** - * Private runtime-provenance bridge for instrumented agent bundles. + * Build-facing runtime-provenance carrier for instrumented agent bundles. * - * This is deliberately an object-identity side channel: provenance never becomes - * part of AgentRunSpec, an agent input, or an AgentRunResult. The build adapter may - * associate an opaque callsite token with the exact spec it emits. Likewise, only - * an exact SDK result object can carry a server-signed receipt into the next agent - * boundary; copies and nested/transformed values intentionally lose the sidecar. + * This published internal subpath intentionally exposes only the opaque v1 + * callsite carrier. Receipt retention, extraction, header assembly, and + * redaction remain package-private in the agents implementation. * - * @internal This is a versioned integration contract, not an author-facing API. + * @internal Versioned build integration contract; not an author-facing API. */ +import { registerAgentRuntimeCallsite } from "../agents/runtime-callsite-store.js"; export const AGENT_RUNTIME_PROVENANCE_VERSION = 1 as const; -export const AGENT_RUNTIME_PROVENANCE_VERSION_HEADER = - "x-sapiom-runtime-provenance-version"; -export const AGENT_RUNTIME_CALLSITE_HEADER = - "x-sapiom-runtime-callsite-evidence"; -export const AGENT_RUNTIME_LINEAGE_HEADER = "x-sapiom-runtime-lineage-receipt"; - export interface AgentRuntimeProvenanceV1 { readonly version: typeof AGENT_RUNTIME_PROVENANCE_VERSION; /** Opaque build-owned reference. It contains no graph identity. */ readonly callsite: string; } -interface AgentRuntimeLineageV1 { - readonly version: typeof AGENT_RUNTIME_PROVENANCE_VERSION; - /** Opaque server-signed receipt. Its contents are never decoded by the SDK. */ - readonly receipt: string; -} - -const MAX_OPAQUE_TOKEN_LENGTH = 8_192; -const invocationProvenance = new WeakMap(); -const resultLineage = new WeakMap(); - -function supportedOpaqueToken(value: unknown): value is string { - return ( - typeof value === "string" && - value.length > 0 && - value.length <= MAX_OPAQUE_TOKEN_LENGTH && - !/[\r\n]/.test(value) - ); -} - -/** Associate build-derived v1 evidence without mutating or wrapping the spec. */ +/** Associate validated build evidence without mutating or wrapping the spec. */ export function carryAgentRuntimeProvenance( spec: T, provenance: AgentRuntimeProvenanceV1, ): T { - if ( - provenance.version === AGENT_RUNTIME_PROVENANCE_VERSION && - supportedOpaqueToken(provenance.callsite) - ) { - invocationProvenance.set(spec, provenance); - } + registerAgentRuntimeCallsite(spec, provenance.version, provenance.callsite); return spec; } - -/** @internal Headers for one immediate invocation; never recursively inspects input. */ -export function agentRuntimeProvenanceHeaders( - spec: object, - directInput: unknown, -): Record { - const callsite = invocationProvenance.get(spec); - const lineage = - directInput !== null && typeof directInput === "object" - ? resultLineage.get(directInput) - : undefined; - if (!callsite && !lineage) return {}; - return { - [AGENT_RUNTIME_PROVENANCE_VERSION_HEADER]: String( - AGENT_RUNTIME_PROVENANCE_VERSION, - ), - ...(callsite ? { [AGENT_RUNTIME_CALLSITE_HEADER]: callsite.callsite } : {}), - ...(lineage ? { [AGENT_RUNTIME_LINEAGE_HEADER]: lineage.receipt } : {}), - }; -} - -/** @internal Retain only a supported response receipt on the exact SDK result. */ -export function retainAgentRuntimeLineage( - result: object, - version: string | null, - receipt: string | null, -): void { - if ( - version === String(AGENT_RUNTIME_PROVENANCE_VERSION) && - supportedOpaqueToken(receipt) - ) { - resultLineage.set(result, { - version: AGENT_RUNTIME_PROVENANCE_VERSION, - receipt, - }); - } -} diff --git a/packages/tools/src/agents/index.ts b/packages/tools/src/agents/index.ts index 5a6b90051..4f9bc7284 100644 --- a/packages/tools/src/agents/index.ts +++ b/packages/tools/src/agents/index.ts @@ -15,12 +15,7 @@ * a pausable handle). An orchestration is addressed by its **slug** (its stable handle). */ import { Transport, defaultTransport } from "../_client/index.js"; -import { - AGENT_RUNTIME_LINEAGE_HEADER, - AGENT_RUNTIME_PROVENANCE_VERSION_HEADER, - agentRuntimeProvenanceHeaders, - retainAgentRuntimeLineage, -} from "../_internal/agent-runtime-provenance.js"; +import { takeAgentRuntimeCallsite } from "./runtime-callsite-store.js"; import type { DispatchHandle } from "../dispatch.js"; const DEFAULT_BASE_URL = @@ -45,6 +40,92 @@ export type ExecutionStatus = | "cancelled"; const TERMINAL = new Set(["completed", "failed", "cancelled"]); +const AGENT_RUNTIME_PROVENANCE_VERSION = 1 as const; +const AGENT_RUNTIME_PROVENANCE_VERSION_HEADER = + "x-sapiom-runtime-provenance-version"; +const AGENT_RUNTIME_CALLSITE_HEADER = "x-sapiom-runtime-callsite-evidence"; +const AGENT_RUNTIME_LINEAGE_HEADER = "x-sapiom-runtime-lineage-receipt"; +const MAX_OPAQUE_TOKEN_LENGTH = 8_192; +const RUNTIME_PROVENANCE_REDACTION = "[REDACTED runtime provenance]"; + +interface LineageRecord { + readonly receipt: string; + active: boolean; + consumed: boolean; +} + +const resultLineage = new WeakMap(); + +function supportedOpaqueToken(value: unknown): value is string { + return ( + typeof value === "string" && + value.length > 0 && + value.length <= MAX_OPAQUE_TOKEN_LENGTH && + !/[\r\n]/.test(value) + ); +} + +function takeAgentRuntimeLineage(directInput: unknown): string | undefined { + if (directInput === null || typeof directInput !== "object") return undefined; + const record = resultLineage.get(directInput); + resultLineage.delete(directInput); + const receipt = + record?.active && !record.consumed ? record.receipt : undefined; + if (record) record.consumed = true; + return receipt; +} + +function retainAgentRuntimeLineage( + targets: readonly object[], + version: string | null, + receipt: string | null, +): void { + if ( + version !== String(AGENT_RUNTIME_PROVENANCE_VERSION) || + !supportedOpaqueToken(receipt) + ) { + return; + } + const record: LineageRecord = { + receipt: `${receipt}`, + active: true, + consumed: false, + }; + for (const target of targets) resultLineage.set(target, record); + const timer = setTimeout(() => { + record.active = false; + }, 0); + timer.unref?.(); +} + +function redactAgentRuntimeProvenance( + text: string, + privateValues: readonly (string | null | undefined)[], +): string { + let redacted = text; + const values = [...new Set(privateValues.filter(supportedOpaqueToken))].sort( + (left, right) => right.length - left.length, + ); + for (const value of values) { + redacted = redacted.split(value).join(RUNTIME_PROVENANCE_REDACTION); + } + return redacted; +} + +function redactedAgentRuntimeError( + error: unknown, + privateValues: readonly (string | null | undefined)[], +): Error { + const redacted = new Error( + redactAgentRuntimeProvenance( + error instanceof Error ? error.message : String(error), + privateValues, + ), + ); + if (error instanceof Error) redacted.name = error.name; + return redacted; +} + export interface AgentRunSpec { /** Slug of the deployed orchestration to run (its stable handle). */ definition: string; @@ -184,6 +265,7 @@ interface ExecutionDoc { */ async function launchScheduled( spec: AgentRunSpec, + input: Record, transport: Transport, baseUrl: string, ): Promise { @@ -194,7 +276,7 @@ async function launchScheduled( body: JSON.stringify({ kind: "schedule_once", at: spec.at, - input: spec.input ?? {}, + input, }), headers: workflowResumeHeaders(transport.resumeToken), }, @@ -220,23 +302,46 @@ export async function launch( transport: Transport = defaultTransport(), baseUrl = DEFAULT_BASE_URL, ): Promise { + const input = spec.input ?? {}; + const callsite = takeAgentRuntimeCallsite(spec); + // Always consume an exact input receipt at an observed agent boundary. It is + // forwarded only when this same invocation has trusted v1 build evidence. + const inputLineageReceipt = takeAgentRuntimeLineage(input); if (spec.at) { - return launchScheduled(spec, transport, baseUrl); + return launchScheduled(spec, input, transport, baseUrl); } - const res = await transport.request( - `${baseUrl}/agents/v1/definitions/${encodeURIComponent(spec.definition)}/executions`, - { - method: "POST", - body: JSON.stringify({ - input: spec.input ?? {}, - idempotencyKey: spec.idempotencyKey, - }), - headers: { - ...workflowResumeHeaders(transport.resumeToken), - ...agentRuntimeProvenanceHeaders(spec, spec.input), + const provenanceHeaders: Record = {}; + const privateProvenanceValues: string[] = []; + if (callsite) { + provenanceHeaders[AGENT_RUNTIME_PROVENANCE_VERSION_HEADER] = String( + AGENT_RUNTIME_PROVENANCE_VERSION, + ); + provenanceHeaders[AGENT_RUNTIME_CALLSITE_HEADER] = callsite; + privateProvenanceValues.push(callsite); + if (inputLineageReceipt) { + provenanceHeaders[AGENT_RUNTIME_LINEAGE_HEADER] = inputLineageReceipt; + privateProvenanceValues.push(inputLineageReceipt); + } + } + let res: StartResponse; + try { + res = await transport.request( + `${baseUrl}/agents/v1/definitions/${encodeURIComponent(spec.definition)}/executions`, + { + method: "POST", + body: JSON.stringify({ + input, + idempotencyKey: spec.idempotencyKey, + }), + headers: { + ...workflowResumeHeaders(transport.resumeToken), + ...provenanceHeaders, + }, }, - }, - ); + ); + } catch (error) { + throw redactedAgentRuntimeError(error, privateProvenanceValues); + } const executionId = res.executionId; const fetchDoc = async (): Promise<{ @@ -245,21 +350,48 @@ export async function launch( lineageReceipt: string | null; }> => { const url = `${baseUrl}/agents/v1/executions/${encodeURIComponent(executionId)}`; - const response = await transport.fetch(url, { - headers: { "content-type": "application/json" }, - }); + let response: Response; + try { + response = await transport.fetch(url, { + headers: { "content-type": "application/json" }, + }); + } catch (error) { + throw redactedAgentRuntimeError(error, privateProvenanceValues); + } + const provenanceVersion = + response.headers?.get?.(AGENT_RUNTIME_PROVENANCE_VERSION_HEADER) ?? null; + const lineageReceipt = + response.headers?.get?.(AGENT_RUNTIME_LINEAGE_HEADER) ?? null; if (!response.ok) { + let body: string; + try { + body = await response.text(); + } catch (error) { + throw redactedAgentRuntimeError(error, [ + ...privateProvenanceValues, + lineageReceipt, + ]); + } throw new Error( - `GET ${url} → ${response.status} ${await response.text()}`, + redactAgentRuntimeProvenance( + `GET ${url} → ${response.status} ${body}`, + [...privateProvenanceValues, lineageReceipt], + ), ); } + let doc: ExecutionDoc; + try { + doc = (await response.json()) as ExecutionDoc; + } catch (error) { + throw redactedAgentRuntimeError(error, [ + ...privateProvenanceValues, + lineageReceipt, + ]); + } return { - doc: (await response.json()) as ExecutionDoc, - provenanceVersion: - response.headers?.get?.(AGENT_RUNTIME_PROVENANCE_VERSION_HEADER) ?? - null, - lineageReceipt: - response.headers?.get?.(AGENT_RUNTIME_LINEAGE_HEADER) ?? null, + doc, + provenanceVersion, + lineageReceipt, }; }; @@ -286,22 +418,26 @@ export async function launch( output: d.output ?? null, error: d.error ?? null, }; - retainAgentRuntimeLineage(result, provenanceVersion, lineageReceipt); + const lineageTargets: object[] = [result]; // This exact output object is the author-facing value commonly handed // to the next agent. Copies, nested values, and primitives remain // deliberately unassociated. if (d.output !== null && typeof d.output === "object") { - retainAgentRuntimeLineage( - d.output, - provenanceVersion, - lineageReceipt, - ); + lineageTargets.push(d.output); } + retainAgentRuntimeLineage( + lineageTargets, + provenanceVersion, + lineageReceipt, + ); return result; } if (Date.now() > deadline) { throw new Error( - `orchestration ${executionId} timed out after ${timeoutMs}ms (last status: ${d.status})`, + redactAgentRuntimeProvenance( + `orchestration ${executionId} timed out after ${timeoutMs}ms (last status: ${d.status})`, + [...privateProvenanceValues, lineageReceipt], + ), ); } await new Promise((r) => setTimeout(r, pollMs)); diff --git a/packages/tools/src/agents/runtime-callsite-store.ts b/packages/tools/src/agents/runtime-callsite-store.ts new file mode 100644 index 000000000..7e86477c8 --- /dev/null +++ b/packages/tools/src/agents/runtime-callsite-store.ts @@ -0,0 +1,55 @@ +/** Package-private bridge between the published build carrier and agent calls. */ + +const AGENT_RUNTIME_PROVENANCE_VERSION = 1 as const; +const MAX_OPAQUE_TOKEN_LENGTH = 8_192; + +interface CallsiteRecord { + readonly callsite: string; + active: boolean; +} + +const invocationCallsites = new WeakMap(); + +function supportedOpaqueToken(value: unknown): value is string { + return ( + typeof value === "string" && + value.length > 0 && + value.length <= MAX_OPAQUE_TOKEN_LENGTH && + !/[\r\n]/.test(value) + ); +} + +/** Called only by the published build carrier. Snapshots scalars after validation. */ +export function registerAgentRuntimeCallsite( + spec: object, + version: unknown, + callsite: unknown, +): void { + if ( + version !== AGENT_RUNTIME_PROVENANCE_VERSION || + !supportedOpaqueToken(callsite) + ) { + return; + } + const record: CallsiteRecord = { + callsite: `${callsite}`, + active: true, + }; + invocationCallsites.set(spec, record); + const timer = setTimeout(() => { + record.active = false; + }, 0); + timer.unref?.(); +} + +/** Consume one validated build callsite. Receipt state never enters this module. */ +export function takeAgentRuntimeCallsite(spec: object): string | undefined { + const record = invocationCallsites.get(spec); + invocationCallsites.delete(spec); + const callsite = + record?.active && supportedOpaqueToken(record.callsite) + ? record.callsite + : undefined; + if (record) record.active = false; + return callsite; +} diff --git a/packages/tools/src/agents/runtime-provenance.spec.ts b/packages/tools/src/agents/runtime-provenance.spec.ts index 8adbb785e..db1eead6b 100644 --- a/packages/tools/src/agents/runtime-provenance.spec.ts +++ b/packages/tools/src/agents/runtime-provenance.spec.ts @@ -1,10 +1,11 @@ import { createClient } from "../index.js"; -import { - AGENT_RUNTIME_CALLSITE_HEADER, - AGENT_RUNTIME_LINEAGE_HEADER, - AGENT_RUNTIME_PROVENANCE_VERSION_HEADER, - carryAgentRuntimeProvenance, -} from "../_internal/agent-runtime-provenance.js"; +import { carryAgentRuntimeProvenance } from "../_internal/agent-runtime-provenance.js"; +import * as publicCarrier from "../_internal/agent-runtime-provenance.js"; + +const AGENT_RUNTIME_PROVENANCE_VERSION_HEADER = + "x-sapiom-runtime-provenance-version"; +const AGENT_RUNTIME_CALLSITE_HEADER = "x-sapiom-runtime-callsite-evidence"; +const AGENT_RUNTIME_LINEAGE_HEADER = "x-sapiom-runtime-lineage-receipt"; interface CapturedCall { url: string; @@ -29,6 +30,7 @@ function agentServer( opts: { receiptVersion?: string; receipt?: string; + terminalStatus?: "completed" | "failed" | "cancelled"; } = {}, ): { fetch: typeof globalThis.fetch; calls: CapturedCall[] } { const calls: CapturedCall[] = []; @@ -46,8 +48,14 @@ function agentServer( 201, ); } + const terminalStatus = opts.terminalStatus ?? "completed"; return response( - { status: "completed", output: { ok: true }, error: null }, + terminalStatus === "completed" + ? { status: terminalStatus, output: { ok: true }, error: null } + : { + status: terminalStatus, + error: { message: `${terminalStatus} privately` }, + }, 200, { ...(opts.receiptVersion @@ -74,6 +82,13 @@ function posts(calls: CapturedCall[]): CapturedCall[] { } describe("agents runtime provenance v1", () => { + it("publishes only the minimal build-facing carrier surface", () => { + expect(Object.keys(publicCarrier).sort()).toEqual([ + "AGENT_RUNTIME_PROVENANCE_VERSION", + "carryAgentRuntimeProvenance", + ]); + }); + it("launch carries opaque callsite evidence out of band and wait retains a private receipt", async () => { const server = agentServer({ receiptVersion: "1", @@ -108,6 +123,21 @@ describe("agents runtime provenance v1", () => { "output", "error", ]); + expect(Reflect.ownKeys(result)).toEqual([ + "executionId", + "status", + "output", + "error", + ]); + expect(Reflect.ownKeys(result.output as object)).toEqual(["ok"]); + for (const descriptor of [ + ...Object.values(Object.getOwnPropertyDescriptors(result)), + ...Object.values( + Object.getOwnPropertyDescriptors(result.output as object), + ), + ]) { + expect(descriptor.value).not.toBe("signed.receipt"); + } expect(JSON.stringify(result)).not.toContain("signed.receipt"); }); @@ -119,10 +149,15 @@ describe("agents runtime provenance v1", () => { const client = createClient({ apiKey: "k", fetch: server.fetch }); const result = await client.agents.run({ definition: "producer" }); - await client.agents.run({ - definition: "consumer", - input: result.output as Record, - }); + await client.agents.run( + carryAgentRuntimeProvenance( + { + definition: "consumer", + input: result.output as Record, + }, + { version: 1, callsite: "callsite.consumer" }, + ), + ); const secondLaunch = posts(server.calls)[1]!; expect(header(secondLaunch, AGENT_RUNTIME_PROVENANCE_VERSION_HEADER)).toBe( @@ -136,6 +171,241 @@ describe("agents runtime provenance v1", () => { ); }); + it("forwards the exact full SDK result and consumes the shared output alias", async () => { + const server = agentServer({ + receiptVersion: "1", + receipt: "signed.full-result", + }); + const client = createClient({ apiKey: "k", fetch: server.fetch }); + const result = await client.agents.run({ definition: "producer" }); + + await client.agents.run( + carryAgentRuntimeProvenance( + { + definition: "full-result-consumer", + input: result as unknown as Record, + }, + { version: 1, callsite: "callsite.full-result" }, + ), + ); + await client.agents.run( + carryAgentRuntimeProvenance( + { + definition: "output-alias-replay", + input: result.output as Record, + }, + { version: 1, callsite: "callsite.output-replay" }, + ), + ); + + const [, direct, replay] = posts(server.calls); + expect(header(direct!, AGENT_RUNTIME_LINEAGE_HEADER)).toBe( + "signed.full-result", + ); + expect(header(replay!, AGENT_RUNTIME_LINEAGE_HEADER)).toBeUndefined(); + }); + + it.each(["failed", "cancelled"] as const)( + "retains a private receipt on a %s full result", + async (terminalStatus) => { + const server = agentServer({ + receiptVersion: "1", + receipt: `signed.${terminalStatus}`, + terminalStatus, + }); + const client = createClient({ apiKey: "k", fetch: server.fetch }); + const result = await client.agents.run({ definition: "producer" }); + + await client.agents.run( + carryAgentRuntimeProvenance( + { + definition: "consumer", + input: result as unknown as Record, + }, + { version: 1, callsite: `callsite.${terminalStatus}` }, + ), + ); + + expect(result.status).toBe(terminalStatus); + expect( + header(posts(server.calls)[1]!, AGENT_RUNTIME_LINEAGE_HEADER), + ).toBe(`signed.${terminalStatus}`); + expect(Reflect.ownKeys(result)).toEqual([ + "executionId", + "status", + "output", + "error", + ]); + }, + ); + + it("captures input once for both serialization and lineage lookup", async () => { + const server = agentServer({ + receiptVersion: "1", + receipt: "signed.single-read", + }); + const client = createClient({ apiKey: "k", fetch: server.fetch }); + const result = await client.agents.run({ definition: "producer" }); + let reads = 0; + const spec = carryAgentRuntimeProvenance( + { + definition: "consumer", + get input(): Record { + reads += 1; + return reads === 1 + ? (result.output as Record) + : { swapped: true }; + }, + }, + { version: 1, callsite: "callsite.single-read" }, + ); + + await client.agents.run(spec); + + const secondLaunch = posts(server.calls)[1]!; + expect(reads).toBe(1); + expect(JSON.parse(String(secondLaunch.init.body)).input).toEqual( + result.output, + ); + expect(header(secondLaunch, AGENT_RUNTIME_LINEAGE_HEADER)).toBe( + "signed.single-read", + ); + }); + + it("snapshots validated callsite scalars", async () => { + const server = agentServer(); + const client = createClient({ apiKey: "k", fetch: server.fetch }); + const provenance = { version: 1 as const, callsite: "callsite.safe" }; + const spec = carryAgentRuntimeProvenance( + { definition: "consumer" }, + provenance, + ); + provenance.callsite = "callsite.mutated\r\nprivate"; + + await client.agents.launch(spec); + + expect(header(posts(server.calls)[0]!, AGENT_RUNTIME_CALLSITE_HEADER)).toBe( + "callsite.safe", + ); + }); + + it("requires a build-carried callsite and consumes lineage at an uninstrumented boundary", async () => { + const server = agentServer({ + receiptVersion: "1", + receipt: "signed.consume", + }); + const client = createClient({ apiKey: "k", fetch: server.fetch }); + const result = await client.agents.run({ definition: "producer" }); + + await client.agents.run({ + definition: "uninstrumented", + input: result.output as Record, + }); + await client.agents.run( + carryAgentRuntimeProvenance( + { + definition: "replay", + input: result.output as Record, + }, + { version: 1, callsite: "callsite.replay" }, + ), + ); + + const [, uninstrumented, replay] = posts(server.calls); + expect( + header(uninstrumented!, AGENT_RUNTIME_LINEAGE_HEADER), + ).toBeUndefined(); + expect(header(replay!, AGENT_RUNTIME_LINEAGE_HEADER)).toBeUndefined(); + }); + + it("consumes one carried callsite and lineage receipt once", async () => { + const server = agentServer({ + receiptVersion: "1", + receipt: "signed.once", + }); + const client = createClient({ apiKey: "k", fetch: server.fetch }); + const result = await client.agents.run({ definition: "producer" }); + const spec = carryAgentRuntimeProvenance( + { + definition: "consumer", + input: result.output as Record, + }, + { version: 1, callsite: "callsite.once" }, + ); + + await client.agents.run(spec); + await client.agents.run(spec); + + const [, first, replay] = posts(server.calls); + expect(header(first!, AGENT_RUNTIME_LINEAGE_HEADER)).toBe("signed.once"); + expect(header(replay!, AGENT_RUNTIME_CALLSITE_HEADER)).toBeUndefined(); + expect(header(replay!, AGENT_RUNTIME_LINEAGE_HEADER)).toBeUndefined(); + }); + + it("does not forward an exact reference after a timer boundary", async () => { + const server = agentServer({ + receiptVersion: "1", + receipt: "signed.timer", + }); + const client = createClient({ apiKey: "k", fetch: server.fetch }); + const result = await client.agents.run({ definition: "producer" }); + await new Promise((resolve) => setTimeout(resolve, 0)); + + await client.agents.run( + carryAgentRuntimeProvenance( + { + definition: "consumer", + input: result.output as Record, + }, + { version: 1, callsite: "callsite.after-timer" }, + ), + ); + + expect( + header(posts(server.calls)[1]!, AGENT_RUNTIME_LINEAGE_HEADER), + ).toBeUndefined(); + }); + + it.each(["array", "map"] as const)( + "does not replay an exact reference from %s after an uninstrumented boundary", + async (container) => { + const server = agentServer({ + receiptVersion: "1", + receipt: `signed.${container}`, + }); + const client = createClient({ apiKey: "k", fetch: server.fetch }); + const result = await client.agents.run({ definition: "producer" }); + let exact: unknown; + if (container === "array") { + const stored = [result.output]; + exact = stored[0]; + } else { + const stored = new Map([["result", result.output]]); + exact = stored.get("result"); + } + + await client.agents.run({ + definition: "queue-worker-uninstrumented", + input: exact as Record, + }); + await client.agents.run( + carryAgentRuntimeProvenance( + { + definition: "replay", + input: exact as Record, + }, + { version: 1, callsite: `callsite.${container}` }, + ), + ); + + const [, uninstrumented, replay] = posts(server.calls); + expect( + header(uninstrumented!, AGENT_RUNTIME_LINEAGE_HEADER), + ).toBeUndefined(); + expect(header(replay!, AGENT_RUNTIME_LINEAGE_HEADER)).toBeUndefined(); + }, + ); + it.each([ ["a copied output", (result: object) => ({ ...result })], ["a nested output", (result: object) => ({ result })], @@ -148,16 +418,21 @@ describe("agents runtime provenance v1", () => { const client = createClient({ apiKey: "k", fetch: server.fetch }); const result = await client.agents.run({ definition: "producer" }); - await client.agents.run({ - definition: "consumer", - input: toInput(result.output as object), - }); + await client.agents.run( + carryAgentRuntimeProvenance( + { + definition: "consumer", + input: toInput(result.output as object), + }, + { version: 1, callsite: `callsite.${_label.replace(/\s/g, "-")}` }, + ), + ); const secondLaunch = posts(server.calls)[1]!; expect(header(secondLaunch, AGENT_RUNTIME_LINEAGE_HEADER)).toBeUndefined(); - expect( - header(secondLaunch, AGENT_RUNTIME_PROVENANCE_VERSION_HEADER), - ).toBeUndefined(); + expect(header(secondLaunch, AGENT_RUNTIME_PROVENANCE_VERSION_HEADER)).toBe( + "1", + ); }); it("ignores an unsupported receipt version", async () => { @@ -167,10 +442,15 @@ describe("agents runtime provenance v1", () => { }); const client = createClient({ apiKey: "k", fetch: server.fetch }); const result = await client.agents.run({ definition: "producer" }); - await client.agents.run({ - definition: "consumer", - input: result.output as Record, - }); + await client.agents.run( + carryAgentRuntimeProvenance( + { + definition: "consumer", + input: result.output as Record, + }, + { version: 1, callsite: "callsite.unsupported" }, + ), + ); expect( header(posts(server.calls)[1]!, AGENT_RUNTIME_LINEAGE_HEADER), @@ -194,8 +474,18 @@ describe("agents runtime provenance v1", () => { ); await client.agents.launch(scheduled); + await client.agents.launch( + carryAgentRuntimeProvenance( + { + definition: "consumer-replay", + input: result.output as Record, + }, + { version: 1, callsite: "callsite.after-delayed" }, + ), + ); const delayedLaunch = posts(server.calls)[1]!; + const replay = posts(server.calls)[2]!; expect( header(delayedLaunch, AGENT_RUNTIME_CALLSITE_HEADER), ).toBeUndefined(); @@ -203,20 +493,52 @@ describe("agents runtime provenance v1", () => { expect( header(delayedLaunch, AGENT_RUNTIME_PROVENANCE_VERSION_HEADER), ).toBeUndefined(); + expect(header(replay, AGENT_RUNTIME_LINEAGE_HEADER)).toBeUndefined(); }); - it("does not include private evidence in surfaced HTTP errors", async () => { + it("redacts reflected request provenance from invocation errors", async () => { const calls: CapturedCall[] = []; + const receipt = "signed.invocation-private"; + let postCount = 0; const fetch = (async ( input: string | URL | Request, init: RequestInit = {}, ) => { calls.push({ url: String(input), init }); - return response({ message: "rejected" }, 400); + if (init.method !== "POST") { + return response( + { status: "completed", output: { ok: true }, error: null }, + 200, + { + [AGENT_RUNTIME_PROVENANCE_VERSION_HEADER]: "1", + [AGENT_RUNTIME_LINEAGE_HEADER]: receipt, + }, + ); + } + postCount += 1; + if (postCount === 1) { + return response( + { status: "enqueued", executionId: "exec-producer" }, + 201, + ); + } + const headers = init.headers as Record; + return response( + { + message: `rejected ${headers[AGENT_RUNTIME_CALLSITE_HEADER] ?? ""} ${ + headers[AGENT_RUNTIME_LINEAGE_HEADER] ?? "" + }`, + }, + 400, + ); }) as typeof globalThis.fetch; const client = createClient({ apiKey: "k", fetch }); + const result = await client.agents.run({ definition: "producer" }); const spec = carryAgentRuntimeProvenance( - { definition: "child" }, + { + definition: "child", + input: result.output as Record, + }, { version: 1, callsite: "callsite.must-stay-private" }, ); @@ -232,9 +554,93 @@ describe("agents runtime provenance v1", () => { expect((error as Error).message).not.toContain( "callsite.must-stay-private", ); - expect(String(calls[0]!.init.body)).not.toContain( + expect((error as Error).message).not.toContain(receipt); + expect(String(posts(calls)[1]!.init.body)).not.toContain( "callsite.must-stay-private", ); + expect(String(posts(calls)[1]!.init.body)).not.toContain(receipt); + }); + + it("redacts reflected callsite and response receipt from status errors", async () => { + const receipt = "signed.status-private"; + const callsite = "callsite.status-private"; + const fetch = (async ( + _input: string | URL | Request, + init: RequestInit = {}, + ) => { + if (init.method === "POST") { + return response( + { status: "enqueued", executionId: "exec-status" }, + 201, + ); + } + return response({ message: `reflected ${callsite} ${receipt}` }, 500, { + [AGENT_RUNTIME_PROVENANCE_VERSION_HEADER]: "1", + [AGENT_RUNTIME_LINEAGE_HEADER]: receipt, + }); + }) as typeof globalThis.fetch; + const client = createClient({ apiKey: "k", fetch }); + const handle = await client.agents.launch( + carryAgentRuntimeProvenance( + { definition: "child" }, + { version: 1, callsite }, + ), + ); + + let error: unknown; + try { + await handle.status(); + } catch (value) { + error = value; + } + + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("reflected"); + expect((error as Error).message).not.toContain(callsite); + expect((error as Error).message).not.toContain(receipt); + }); + + it("redacts known provenance from status parsing errors", async () => { + const receipt = "signed.parse-private"; + const callsite = "callsite.parse-private"; + const fetch = (async ( + _input: string | URL | Request, + init: RequestInit = {}, + ) => { + if (init.method === "POST") { + return response({ status: "enqueued", executionId: "exec-parse" }, 201); + } + return { + ok: true, + status: 200, + headers: new Headers({ + [AGENT_RUNTIME_PROVENANCE_VERSION_HEADER]: "1", + [AGENT_RUNTIME_LINEAGE_HEADER]: receipt, + }), + json: async () => { + throw new Error(`invalid response ${callsite} ${receipt}`); + }, + } as unknown as Response; + }) as typeof globalThis.fetch; + const client = createClient({ apiKey: "k", fetch }); + const handle = await client.agents.launch( + carryAgentRuntimeProvenance( + { definition: "child" }, + { version: 1, callsite }, + ), + ); + + let error: unknown; + try { + await handle.status(); + } catch (value) { + error = value; + } + + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain("invalid response"); + expect((error as Error).message).not.toContain(callsite); + expect((error as Error).message).not.toContain(receipt); }); it("preserves legacy request and result behavior when provenance is absent", async () => { From f5d547cfc9494ab65672d0ce139947ae56077998 Mon Sep 17 00:00:00 2001 From: Yash Date: Mon, 31 Aug 2026 23:22:17 +0000 Subject: [PATCH 05/15] test(tools): verify provenance package surfaces Exercise clean-built CJS and ESM carriers, pin mixed-format isolation, and document the exact detectable handoff boundary without claiming recursive queue or storage inference.\n\nRefs: SAP-3020 --- .changeset/calm-agents-carry.md | 4 +- packages/tools/package.json | 1 + .../test-runtime-provenance-package.mjs | 113 ++++++++++++++++++ packages/tools/src/agents/README.md | 26 +++- 4 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 packages/tools/scripts/test-runtime-provenance-package.mjs diff --git a/.changeset/calm-agents-carry.md b/.changeset/calm-agents-carry.md index e65207c47..ee9f25f3f 100644 --- a/.changeset/calm-agents-carry.md +++ b/.changeset/calm-agents-carry.md @@ -5,4 +5,6 @@ Add a private v1 runtime-provenance carrier for agent invocations. Instrumented calls send opaque callsite evidence out of band, terminal results retain an SDK-only server receipt, and only exact direct result-to-input handoffs forward -that receipt. Request/result JSON and calls without metadata remain unchanged. +that receipt through a trusted, one-shot build callsite. Private receipt state is +not package-exported; reflected values are redacted from errors. Request/result +JSON and calls without metadata remain unchanged. diff --git a/packages/tools/package.json b/packages/tools/package.json index 8d98396dd..742189e43 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -114,6 +114,7 @@ "gen:version": "node scripts/generate-version.mjs", "test": "npm run gen:version && jest", "test:coverage": "npm run gen:version && jest --coverage", + "test:runtime-provenance-package": "node scripts/test-runtime-provenance-package.mjs", "test:watch": "npm run gen:version && jest --watch", "typecheck": "npm run gen:version && tsc --noEmit", "lint": "eslint src --ext .ts", diff --git a/packages/tools/scripts/test-runtime-provenance-package.mjs b/packages/tools/scripts/test-runtime-provenance-package.mjs new file mode 100644 index 000000000..af152c541 --- /dev/null +++ b/packages/tools/scripts/test-runtime-provenance-package.mjs @@ -0,0 +1,113 @@ +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const CARRIER_EXPORT = "@sapiom/tools/_internal/agent-runtime-provenance"; +const VERSION_HEADER = "x-sapiom-runtime-provenance-version"; +const CALLSITE_HEADER = "x-sapiom-runtime-callsite-evidence"; +const LINEAGE_HEADER = "x-sapiom-runtime-lineage-receipt"; + +function fakeAgentServer() { + const calls = []; + let execution = 0; + const fetch = async (input, init = {}) => { + calls.push({ url: String(input), init }); + if (init.method === "POST") { + execution += 1; + return new Response( + JSON.stringify({ + status: "enqueued", + executionId: `exec-${execution}`, + }), + { status: 201, headers: { "content-type": "application/json" } }, + ); + } + return new Response( + JSON.stringify({ + status: "completed", + output: { ok: true }, + error: null, + }), + { + status: 200, + headers: { + "content-type": "application/json", + [VERSION_HEADER]: "1", + [LINEAGE_HEADER]: "signed.package-surface", + }, + }, + ); + }; + return { fetch, calls }; +} + +function header(call, name) { + return new Headers(call.init.headers).get(name); +} + +async function verifySameFormat(label, tools, carrier) { + assert.deepEqual(Object.keys(carrier).sort(), [ + "AGENT_RUNTIME_PROVENANCE_VERSION", + "carryAgentRuntimeProvenance", + ]); + const server = fakeAgentServer(); + const client = tools.createClient({ apiKey: "k", fetch: server.fetch }); + const result = await client.agents.run({ definition: `${label}-producer` }); + await client.agents.run( + carrier.carryAgentRuntimeProvenance( + { definition: `${label}-consumer`, input: result.output }, + { version: 1, callsite: `callsite.${label}` }, + ), + ); + const posts = server.calls.filter((call) => call.init.method === "POST"); + assert.equal(header(posts[1], CALLSITE_HEADER), `callsite.${label}`); + assert.equal(header(posts[1], LINEAGE_HEADER), "signed.package-surface"); + assert.equal( + JSON.stringify(result).includes("signed.package-surface"), + false, + ); + await client.shutdown(); +} + +const cjsTools = require("@sapiom/tools"); +const cjsCarrier = require(CARRIER_EXPORT); +assert.throws( + () => require("@sapiom/tools/dist/cjs/agents/runtime-callsite-store.js"), + (error) => error?.code === "ERR_PACKAGE_PATH_NOT_EXPORTED", +); +await verifySameFormat("cjs", cjsTools, cjsCarrier); +const sensitiveCjsExports = Object.values(require.cache) + .filter((loaded) => loaded?.filename.includes("/packages/tools/dist/cjs/")) + .flatMap((loaded) => Object.keys(loaded?.exports ?? {})) + .filter((name) => + /Lineage|Receipt|retainAgentRuntime|redactAgentRuntime|ProvenanceHeaders/.test( + name, + ), + ); +assert.deepEqual(sensitiveCjsExports, []); + +const esmTools = await import("@sapiom/tools"); +const esmCarrier = await import(CARRIER_EXPORT); +await verifySameFormat("esm", esmTools, esmCarrier); + +// Mixed CJS/ESM loading intentionally has isolated closure state. Sharing via a +// discoverable global would make the receipt store consumer-accessible. Pin the +// documented boundary: mixed-format carrier/client pairs do not forward. +const mixedServer = fakeAgentServer(); +const mixedClient = esmTools.createClient({ + apiKey: "k", + fetch: mixedServer.fetch, +}); +await mixedClient.agents.launch( + cjsCarrier.carryAgentRuntimeProvenance( + { definition: "mixed-consumer" }, + { version: 1, callsite: "callsite.mixed" }, + ), +); +const mixedPost = mixedServer.calls.find((call) => call.init.method === "POST"); +assert.equal(header(mixedPost, CALLSITE_HEADER), null); +await mixedClient.shutdown(); + +console.log( + "runtime provenance package surfaces: CJS + ESM passed; mixed format isolated", +); diff --git a/packages/tools/src/agents/README.md b/packages/tools/src/agents/README.md index 7ff624c66..356f944a9 100644 --- a/packages/tools/src/agents/README.md +++ b/packages/tools/src/agents/README.md @@ -52,12 +52,26 @@ invocation through `@sapiom/tools/_internal/agent-runtime-provenance`. The token travels in dedicated request headers, never in `AgentRunSpec` or its JSON body. When a terminal response carries a supported server-signed lineage receipt, the SDK retains it in object-identity sidecars on the returned result and its exact -object-valued `output`. The receipt is forwarded only when one of those exact -objects is passed directly as the next agent's `input`; copies, nested values, -transformed primitives, delayed queues, storage, and arbitrary objects do not -inherit it. The SDK treats both values as opaque and exposes no caller, callee, -bundle, or execution identity through this contract. Missing or unsupported -metadata preserves the legacy behavior. +object-valued `output`. One receipt can be forwarded once, only when one of those +exact objects is the direct `input` of an immediate invocation carrying a valid +v1 build callsite. An uninstrumented agent invocation consumes the sidecar +without forwarding it, a timer turn expires it, and delayed dispatch never sends +runtime provenance. Copies, nested values, and transformed primitives do not +inherit the sidecar. + +The detectable boundary is intentionally narrower than arbitrary data-flow +tracking: the SDK does not recursively inspect values and cannot distinguish a +synchronous exact-reference round trip through an in-memory array or `Map` from +a direct handoff before the one-turn sidecar expires. Queue/storage exclusion is +therefore guaranteed only when the boundary changes object identity, crosses a +timer turn, or invokes an uninstrumented agent boundary (which consumes the +receipt). This v1 carrier and its client must also resolve through the same CJS +or ESM package format. Mixed CJS/ESM loading keeps separate closure stores and is +an explicit remaining integration limitation; using a discoverable global store +would violate receipt privacy. Both same-format published surfaces are tested. +The SDK treats all carrier values as opaque and exposes no new caller, callee, +bundle, or execution identity. Missing or unsupported metadata preserves legacy +behavior. - **Addressed by slug.** `definition` is the deployed agent's slug — its stable handle. `input` is passed to its entry step. From f80303e1383c17fd1ad97ca986fcde4f9424963d Mon Sep 17 00:00:00 2001 From: Yash Date: Mon, 31 Aug 2026 23:46:26 +0000 Subject: [PATCH 06/15] fix(tools): share provenance across module formats Route ESM root and carrier exports through the canonical CJS closure so mixed-format direct handoffs retain private one-shot provenance without global state or public store helpers. Refs: SAP-3020 --- .changeset/calm-agents-carry.md | 3 +- packages/tools/package.json | 3 +- ...anonicalize-runtime-provenance-exports.mjs | 39 ++++++++ .../test-runtime-provenance-package.mjs | 92 +++++++++++++++---- packages/tools/src/agents/README.md | 8 +- 5 files changed, 122 insertions(+), 23 deletions(-) create mode 100644 packages/tools/scripts/canonicalize-runtime-provenance-exports.mjs diff --git a/.changeset/calm-agents-carry.md b/.changeset/calm-agents-carry.md index ee9f25f3f..07a1a729c 100644 --- a/.changeset/calm-agents-carry.md +++ b/.changeset/calm-agents-carry.md @@ -7,4 +7,5 @@ calls send opaque callsite evidence out of band, terminal results retain an SDK-only server receipt, and only exact direct result-to-input handoffs forward that receipt through a trusted, one-shot build callsite. Private receipt state is not package-exported; reflected values are redacted from errors. Request/result -JSON and calls without metadata remain unchanged. +JSON and calls without metadata remain unchanged. CJS and ESM imports share one +private closure-backed store, including mixed-format direct handoffs. diff --git a/packages/tools/package.json b/packages/tools/package.json index 742189e43..0a946aa2f 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -105,8 +105,9 @@ "@sapiom/analytics-core": "workspace:^" }, "scripts": { - "build": "npm run gen:version && npm run build:cjs && npm run build:esm && npm run build:esm-pkg", + "build": "npm run gen:version && npm run build:cjs && npm run build:esm && npm run build:canonical-exports && npm run build:esm-pkg", "build:cjs": "tsc --project tsconfig.cjs.json", + "build:canonical-exports": "node scripts/canonicalize-runtime-provenance-exports.mjs", "build:esm": "tsc --project tsconfig.esm.json", "build:esm-pkg": "node -e \"require('fs').writeFileSync('dist/esm/package.json',JSON.stringify({type:'module'}))\"", "clean": "rm -rf dist *.tsbuildinfo", diff --git a/packages/tools/scripts/canonicalize-runtime-provenance-exports.mjs b/packages/tools/scripts/canonicalize-runtime-provenance-exports.mjs new file mode 100644 index 000000000..cfe9f7952 --- /dev/null +++ b/packages/tools/scripts/canonicalize-runtime-provenance-exports.mjs @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import { rm, writeFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const require = createRequire(import.meta.url); +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +async function writeCanonicalFacade({ esmPath, cjsPath, cjsSpecifier }) { + const canonical = require(resolve(packageRoot, cjsPath)); + const exportNames = Object.keys(canonical).sort(); + assert(exportNames.length > 0, `no runtime exports found in ${cjsPath}`); + for (const name of exportNames) { + assert.match(name, /^[A-Z_$][0-9A-Z_$]*$/i, `unsupported export: ${name}`); + } + + const source = [ + "// Generated by scripts/canonicalize-runtime-provenance-exports.mjs.", + "// Import and require share this canonical closure-backed implementation.", + `import canonical from ${JSON.stringify(cjsSpecifier)};`, + ...exportNames.map((name) => `export const ${name} = canonical.${name};`), + "", + ].join("\n"); + const outputPath = resolve(packageRoot, esmPath); + await writeFile(outputPath, source); + await rm(`${outputPath}.map`, { force: true }); +} + +await writeCanonicalFacade({ + esmPath: "dist/esm/index.js", + cjsPath: "dist/cjs/index.js", + cjsSpecifier: "../cjs/index.js", +}); +await writeCanonicalFacade({ + esmPath: "dist/esm/_internal/agent-runtime-provenance.js", + cjsPath: "dist/cjs/_internal/agent-runtime-provenance.js", + cjsSpecifier: "../../cjs/_internal/agent-runtime-provenance.js", +}); diff --git a/packages/tools/scripts/test-runtime-provenance-package.mjs b/packages/tools/scripts/test-runtime-provenance-package.mjs index af152c541..6bb656469 100644 --- a/packages/tools/scripts/test-runtime-provenance-package.mjs +++ b/packages/tools/scripts/test-runtime-provenance-package.mjs @@ -88,26 +88,84 @@ assert.deepEqual(sensitiveCjsExports, []); const esmTools = await import("@sapiom/tools"); const esmCarrier = await import(CARRIER_EXPORT); +await assert.rejects( + import("@sapiom/tools/dist/esm/agents/runtime-callsite-store.js"), + (error) => error?.code === "ERR_PACKAGE_PATH_NOT_EXPORTED", +); +assert.deepEqual(Object.keys(esmTools).sort(), Object.keys(cjsTools).sort()); +assert.equal(esmTools.createClient, cjsTools.createClient); +assert.equal( + esmCarrier.carryAgentRuntimeProvenance, + cjsCarrier.carryAgentRuntimeProvenance, +); await verifySameFormat("esm", esmTools, esmCarrier); -// Mixed CJS/ESM loading intentionally has isolated closure state. Sharing via a -// discoverable global would make the receipt store consumer-accessible. Pin the -// documented boundary: mixed-format carrier/client pairs do not forward. -const mixedServer = fakeAgentServer(); -const mixedClient = esmTools.createClient({ - apiKey: "k", - fetch: mixedServer.fetch, -}); -await mixedClient.agents.launch( - cjsCarrier.carryAgentRuntimeProvenance( - { definition: "mixed-consumer" }, - { version: 1, callsite: "callsite.mixed" }, - ), +async function verifyCrossFormatCallsite(label, tools, carrier) { + const server = fakeAgentServer(); + const client = tools.createClient({ apiKey: "k", fetch: server.fetch }); + await client.agents.launch( + carrier.carryAgentRuntimeProvenance( + { definition: `${label}-consumer` }, + { version: 1, callsite: `callsite.${label}` }, + ), + ); + const post = server.calls.find((call) => call.init.method === "POST"); + assert.equal(header(post, CALLSITE_HEADER), `callsite.${label}`); + await client.shutdown(); +} + +async function verifyCrossFormatResult( + label, + producerTools, + consumerTools, + consumerCarrier, +) { + const server = fakeAgentServer(); + const producer = producerTools.createClient({ + apiKey: "k", + fetch: server.fetch, + }); + const consumer = consumerTools.createClient({ + apiKey: "k", + fetch: server.fetch, + }); + for (const target of ["full-result", "output"]) { + const result = await producer.agents.run({ + definition: `${label}-${target}-producer`, + }); + await consumer.agents.run( + consumerCarrier.carryAgentRuntimeProvenance( + { + definition: `${label}-${target}-consumer`, + input: target === "full-result" ? result : result.output, + }, + { version: 1, callsite: `callsite.${label}.${target}` }, + ), + ); + const post = server.calls + .filter((call) => call.init.method === "POST") + .at(-1); + assert.equal(header(post, CALLSITE_HEADER), `callsite.${label}.${target}`); + assert.equal(header(post, LINEAGE_HEADER), "signed.package-surface"); + } + await Promise.all([producer.shutdown(), consumer.shutdown()]); +} + +await verifyCrossFormatCallsite("cjs-carrier-esm-client", esmTools, cjsCarrier); +await verifyCrossFormatCallsite("esm-carrier-cjs-client", cjsTools, esmCarrier); +await verifyCrossFormatResult( + "cjs-result-esm-client", + cjsTools, + esmTools, + esmCarrier, +); +await verifyCrossFormatResult( + "esm-result-cjs-client", + esmTools, + cjsTools, + cjsCarrier, ); -const mixedPost = mixedServer.calls.find((call) => call.init.method === "POST"); -assert.equal(header(mixedPost, CALLSITE_HEADER), null); -await mixedClient.shutdown(); console.log( - "runtime provenance package surfaces: CJS + ESM passed; mixed format isolated", + "runtime provenance package surfaces: CJS + ESM and four cross-format paths passed", ); diff --git a/packages/tools/src/agents/README.md b/packages/tools/src/agents/README.md index 356f944a9..c51deb4da 100644 --- a/packages/tools/src/agents/README.md +++ b/packages/tools/src/agents/README.md @@ -65,10 +65,10 @@ synchronous exact-reference round trip through an in-memory array or `Map` from a direct handoff before the one-turn sidecar expires. Queue/storage exclusion is therefore guaranteed only when the boundary changes object identity, crosses a timer turn, or invokes an uninstrumented agent boundary (which consumes the -receipt). This v1 carrier and its client must also resolve through the same CJS -or ESM package format. Mixed CJS/ESM loading keeps separate closure stores and is -an explicit remaining integration limitation; using a discoverable global store -would violate receipt privacy. Both same-format published surfaces are tested. +receipt). The package's CJS and ESM root/carrier exports route through one +canonical closure-backed implementation, so supported mixed-format callsites and +result handoffs share the same private state without a process-global store or +public extraction/rebinding helpers. All four cross-format directions are tested. The SDK treats all carrier values as opaque and exposes no new caller, callee, bundle, or execution identity. Missing or unsupported metadata preserves legacy behavior. From 7c75db9f05aad868843122a44e94c1c006179336 Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 1 Sep 2026 00:15:30 +0000 Subject: [PATCH 07/15] fix(tools): keep provenance store lexical Bundle root and carrier runtimes into one closure so module-cache inspection cannot extract or rebind private callsite helpers while CJS and ESM handoffs retain shared state. Refs: SAP-3020 --- .changeset/calm-agents-carry.md | 3 +- .github/workflows/test.yml | 3 + packages/tools/package.json | 1 + .../scripts/agent-runtime-provenance-entry.ts | 5 + ...anonicalize-runtime-provenance-exports.mjs | 103 +++++++++++++++--- .../test-runtime-provenance-package.mjs | 88 +++++++++++++-- packages/tools/src/agents/README.md | 8 +- pnpm-lock.yaml | 3 + 8 files changed, 182 insertions(+), 32 deletions(-) create mode 100644 packages/tools/scripts/agent-runtime-provenance-entry.ts diff --git a/.changeset/calm-agents-carry.md b/.changeset/calm-agents-carry.md index 07a1a729c..ddeeb751a 100644 --- a/.changeset/calm-agents-carry.md +++ b/.changeset/calm-agents-carry.md @@ -8,4 +8,5 @@ SDK-only server receipt, and only exact direct result-to-input handoffs forward that receipt through a trusted, one-shot build callsite. Private receipt state is not package-exported; reflected values are redacted from errors. Request/result JSON and calls without metadata remain unchanged. CJS and ESM imports share one -private closure-backed store, including mixed-format direct handoffs. +bundled lexical store, including mixed-format direct handoffs, without exposing +private extraction or rebinding helpers through the module cache. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 940bc7a12..b94fd95a6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,6 +42,9 @@ jobs: - name: Build run: pnpm build + - name: Verify tools runtime provenance package + run: pnpm --filter @sapiom/tools test:runtime-provenance-package + - name: Type check run: pnpm typecheck diff --git a/packages/tools/package.json b/packages/tools/package.json index 0a946aa2f..07bab145f 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -127,6 +127,7 @@ "@types/node": "^20.11.30", "@typescript-eslint/eslint-plugin": "^7.3.1", "@typescript-eslint/parser": "^7.3.1", + "esbuild": "^0.28.1", "eslint": "^8.57.0", "jest": "^29.7.0", "prettier": "^3.2.5", diff --git a/packages/tools/scripts/agent-runtime-provenance-entry.ts b/packages/tools/scripts/agent-runtime-provenance-entry.ts new file mode 100644 index 000000000..f771f65fa --- /dev/null +++ b/packages/tools/scripts/agent-runtime-provenance-entry.ts @@ -0,0 +1,5 @@ +export * from "../src/index.js"; +export { + AGENT_RUNTIME_PROVENANCE_VERSION, + carryAgentRuntimeProvenance, +} from "../src/_internal/agent-runtime-provenance.js"; diff --git a/packages/tools/scripts/canonicalize-runtime-provenance-exports.mjs b/packages/tools/scripts/canonicalize-runtime-provenance-exports.mjs index cfe9f7952..7047dd338 100644 --- a/packages/tools/scripts/canonicalize-runtime-provenance-exports.mjs +++ b/packages/tools/scripts/canonicalize-runtime-provenance-exports.mjs @@ -1,39 +1,106 @@ import assert from "node:assert/strict"; import { rm, writeFile } from "node:fs/promises"; import { createRequire } from "node:module"; -import { dirname, resolve } from "node:path"; +import { dirname, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { build } from "esbuild"; + const require = createRequire(import.meta.url); const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const cjsRootPath = resolve(packageRoot, "dist/cjs/index.js"); +const cjsCarrierPath = resolve( + packageRoot, + "dist/cjs/_internal/agent-runtime-provenance.js", +); +const bundlePath = resolve( + packageRoot, + "dist/runtime/agent-runtime-provenance.cjs", +); -async function writeCanonicalFacade({ esmPath, cjsPath, cjsSpecifier }) { - const canonical = require(resolve(packageRoot, cjsPath)); - const exportNames = Object.keys(canonical).sort(); - assert(exportNames.length > 0, `no runtime exports found in ${cjsPath}`); - for (const name of exportNames) { +function runtimeExportNames(modulePath) { + const names = Object.keys(require(modulePath)).sort(); + assert(names.length > 0, `no runtime exports found in ${modulePath}`); + for (const name of names) { assert.match(name, /^[A-Z_$][0-9A-Z_$]*$/i, `unsupported export: ${name}`); } + return names; +} + +const rootExportNames = runtimeExportNames(cjsRootPath); +const carrierExportNames = runtimeExportNames(cjsCarrierPath); +const supportedExportNames = [ + ...new Set([...rootExportNames, ...carrierExportNames]), +].sort(); + +await build({ + entryPoints: [ + resolve(packageRoot, "scripts/agent-runtime-provenance-entry.ts"), + ], + outfile: bundlePath, + bundle: true, + format: "cjs", + platform: "node", + target: "node18", + packages: "external", + sourcemap: false, + logLevel: "warning", +}); + +assert.deepEqual(runtimeExportNames(bundlePath), supportedExportNames); +function moduleSpecifier(fromPath, toPath) { + const path = relative(dirname(fromPath), toPath).replaceAll("\\", "/"); + return path.startsWith(".") ? path : `./${path}`; +} + +async function writeCjsFacade(outputPath, exportNames) { + const bundleSpecifier = moduleSpecifier(outputPath, bundlePath); const source = [ + '"use strict";', "// Generated by scripts/canonicalize-runtime-provenance-exports.mjs.", - "// Import and require share this canonical closure-backed implementation.", + "// Private provenance state is lexical inside the canonical bundle.", + `const canonical = require(${JSON.stringify(bundleSpecifier)});`, + 'Object.defineProperty(exports, "__esModule", { value: true });', + ...exportNames.map((name) => `exports.${name} = canonical.${name};`), + "", + ].join("\n"); + await writeFile(outputPath, source); + await rm(`${outputPath}.map`, { force: true }); +} + +async function writeEsmFacade(outputPath, cjsPath, exportNames) { + const cjsSpecifier = moduleSpecifier(outputPath, cjsPath); + const source = [ + "// Generated by scripts/canonicalize-runtime-provenance-exports.mjs.", + "// Import and require share the canonical closure-backed implementation.", `import canonical from ${JSON.stringify(cjsSpecifier)};`, ...exportNames.map((name) => `export const ${name} = canonical.${name};`), "", ].join("\n"); - const outputPath = resolve(packageRoot, esmPath); await writeFile(outputPath, source); await rm(`${outputPath}.map`, { force: true }); } -await writeCanonicalFacade({ - esmPath: "dist/esm/index.js", - cjsPath: "dist/cjs/index.js", - cjsSpecifier: "../cjs/index.js", -}); -await writeCanonicalFacade({ - esmPath: "dist/esm/_internal/agent-runtime-provenance.js", - cjsPath: "dist/cjs/_internal/agent-runtime-provenance.js", - cjsSpecifier: "../../cjs/_internal/agent-runtime-provenance.js", -}); +await writeCjsFacade(cjsRootPath, rootExportNames); +await writeCjsFacade(cjsCarrierPath, carrierExportNames); +await writeEsmFacade( + resolve(packageRoot, "dist/esm/index.js"), + cjsRootPath, + rootExportNames, +); +await writeEsmFacade( + resolve(packageRoot, "dist/esm/_internal/agent-runtime-provenance.js"), + cjsCarrierPath, + carrierExportNames, +); + +for (const format of ["cjs", "esm"]) { + const storeBase = resolve( + packageRoot, + `dist/${format}/agents/runtime-callsite-store`, + ); + for (const extension of [".js", ".js.map", ".d.ts", ".d.ts.map"]) { + await rm(`${storeBase}${extension}`, { force: true }); + } +} diff --git a/packages/tools/scripts/test-runtime-provenance-package.mjs b/packages/tools/scripts/test-runtime-provenance-package.mjs index 6bb656469..a2c3f843c 100644 --- a/packages/tools/scripts/test-runtime-provenance-package.mjs +++ b/packages/tools/scripts/test-runtime-provenance-package.mjs @@ -1,7 +1,11 @@ import assert from "node:assert/strict"; +import { existsSync } from "node:fs"; import { createRequire } from "node:module"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; const require = createRequire(import.meta.url); +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const CARRIER_EXPORT = "@sapiom/tools/_internal/agent-runtime-provenance"; const VERSION_HEADER = "x-sapiom-runtime-provenance-version"; const CALLSITE_HEADER = "x-sapiom-runtime-callsite-evidence"; @@ -71,20 +75,13 @@ async function verifySameFormat(label, tools, carrier) { const cjsTools = require("@sapiom/tools"); const cjsCarrier = require(CARRIER_EXPORT); +assert.equal("default" in cjsTools, false); +assert.equal("default" in cjsCarrier, false); assert.throws( () => require("@sapiom/tools/dist/cjs/agents/runtime-callsite-store.js"), (error) => error?.code === "ERR_PACKAGE_PATH_NOT_EXPORTED", ); await verifySameFormat("cjs", cjsTools, cjsCarrier); -const sensitiveCjsExports = Object.values(require.cache) - .filter((loaded) => loaded?.filename.includes("/packages/tools/dist/cjs/")) - .flatMap((loaded) => Object.keys(loaded?.exports ?? {})) - .filter((name) => - /Lineage|Receipt|retainAgentRuntime|redactAgentRuntime|ProvenanceHeaders/.test( - name, - ), - ); -assert.deepEqual(sensitiveCjsExports, []); const esmTools = await import("@sapiom/tools"); const esmCarrier = await import(CARRIER_EXPORT); @@ -92,6 +89,8 @@ await assert.rejects( import("@sapiom/tools/dist/esm/agents/runtime-callsite-store.js"), (error) => error?.code === "ERR_PACKAGE_PATH_NOT_EXPORTED", ); +assert.equal("default" in esmTools, false); +assert.equal("default" in esmCarrier, false); assert.deepEqual(Object.keys(esmTools).sort(), Object.keys(cjsTools).sort()); assert.equal(esmTools.createClient, cjsTools.createClient); assert.equal( @@ -166,6 +165,75 @@ await verifyCrossFormatResult( cjsCarrier, ); +for (const format of ["cjs", "esm"]) { + for (const extension of [".js", ".js.map", ".d.ts", ".d.ts.map"]) { + assert.equal( + existsSync( + resolve( + packageRoot, + `dist/${format}/agents/runtime-callsite-store${extension}`, + ), + ), + false, + ); + } +} + +const loadedToolsModules = Object.values(require.cache).filter((loaded) => + loaded?.filename.includes("/packages/tools/dist/"), +); +const loadedExportNames = loadedToolsModules.flatMap((loaded) => + Object.keys(loaded?.exports ?? {}), +); +const forbiddenHelperNames = [ + "registerAgentRuntimeCallsite", + "takeAgentRuntimeCallsite", +]; +for (const name of forbiddenHelperNames) { + assert.equal(loadedExportNames.includes(name), false); +} +const supportedCacheExports = new Set([ + ...Object.keys(cjsTools), + ...Object.keys(cjsCarrier), +]); +for (const loaded of loadedToolsModules) { + assert.deepEqual( + Object.keys(loaded.exports).filter( + (name) => !supportedCacheExports.has(name), + ), + [], + `unsupported cache exports from ${loaded.filename}`, + ); +} + +const attackSource = cjsCarrier.carryAgentRuntimeProvenance( + { definition: "cache-attack-source" }, + { version: 1, callsite: "callsite.cache-attack" }, +); +const cachedTake = loadedToolsModules + .map((loaded) => loaded.exports?.takeAgentRuntimeCallsite) + .find(Boolean); +const cachedRegister = loadedToolsModules + .map((loaded) => loaded.exports?.registerAgentRuntimeCallsite) + .find(Boolean); +assert.equal(cachedTake, undefined); +assert.equal(cachedRegister, undefined); +const reboundSpec = { definition: "cache-attack-rebound" }; +if (cachedTake && cachedRegister) { + cachedRegister(reboundSpec, 1, cachedTake(attackSource)); +} +const attackServer = fakeAgentServer(); +const attackClient = cjsTools.createClient({ + apiKey: "k", + fetch: attackServer.fetch, +}); +await attackClient.agents.launch(reboundSpec); +const attackPost = attackServer.calls.find( + (call) => call.init.method === "POST", +); +assert.equal(header(attackPost, CALLSITE_HEADER), null); +await attackClient.shutdown(); + console.log( - "runtime provenance package surfaces: CJS + ESM and four cross-format paths passed", + "runtime provenance package surfaces: cache-private CJS + ESM and four cross-format paths passed", ); diff --git a/packages/tools/src/agents/README.md b/packages/tools/src/agents/README.md index c51deb4da..7631cd3d1 100644 --- a/packages/tools/src/agents/README.md +++ b/packages/tools/src/agents/README.md @@ -66,9 +66,11 @@ a direct handoff before the one-turn sidecar expires. Queue/storage exclusion is therefore guaranteed only when the boundary changes object identity, crosses a timer turn, or invokes an uninstrumented agent boundary (which consumes the receipt). The package's CJS and ESM root/carrier exports route through one -canonical closure-backed implementation, so supported mixed-format callsites and -result handoffs share the same private state without a process-global store or -public extraction/rebinding helpers. All four cross-format directions are tested. +canonical bundled implementation. Its callsite and receipt stores remain lexical, +so supported mixed-format handoffs share private state without process globals, +public extraction/rebinding helpers, or callable private helpers in +`require.cache`. Standalone callsite-store artifacts are omitted from the package. +All four cross-format directions are tested. The SDK treats all carrier values as opaque and exposes no new caller, callee, bundle, or execution identity. Missing or unsupported metadata preserves legacy behavior. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c0a05e41..978a61571 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -767,6 +767,9 @@ importers: '@typescript-eslint/parser': specifier: ^7.3.1 version: 7.18.0(eslint@8.57.1)(typescript@5.9.3) + esbuild: + specifier: ^0.28.1 + version: 0.28.1 eslint: specifier: ^8.57.0 version: 8.57.1 From 57986cf4276ebdb00d28588976551adddb2085f9 Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 1 Sep 2026 01:04:50 +0000 Subject: [PATCH 08/15] fix(ci): run tests for stacked pull requests Allow the Test workflow to run for pull requests targeting any legitimate base branch so stacked PRs exercise the post-build runtime provenance package probe. Refs: SAP-3020 --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b94fd95a6..9a42d5350 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,6 @@ name: Test on: pull_request: - branches: [main] push: branches: [main] From e097ce5d613960da74811c8d980118fc2ada2694 Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 1 Sep 2026 01:18:18 +0000 Subject: [PATCH 09/15] fix(tools): route agent artifacts through canonical bundle Generate CJS and ESM agent facades backed by the canonical lexical provenance runtime so supported stub imports cannot reach a removed private store artifact. Extend the package probe to execute both stub formats and reject private helpers from their cache surfaces. Refs: SAP-3020 --- ...anonicalize-runtime-provenance-exports.mjs | 17 +++++++-- .../test-runtime-provenance-package.mjs | 36 ++++++++++++++++++- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/tools/scripts/canonicalize-runtime-provenance-exports.mjs b/packages/tools/scripts/canonicalize-runtime-provenance-exports.mjs index 7047dd338..ba2945227 100644 --- a/packages/tools/scripts/canonicalize-runtime-provenance-exports.mjs +++ b/packages/tools/scripts/canonicalize-runtime-provenance-exports.mjs @@ -13,6 +13,7 @@ const cjsCarrierPath = resolve( packageRoot, "dist/cjs/_internal/agent-runtime-provenance.js", ); +const cjsAgentsPath = resolve(packageRoot, "dist/cjs/agents/index.js"); const bundlePath = resolve( packageRoot, "dist/runtime/agent-runtime-provenance.cjs", @@ -48,21 +49,27 @@ await build({ }); assert.deepEqual(runtimeExportNames(bundlePath), supportedExportNames); +const agentExportNames = Object.keys(require(bundlePath).agents).sort(); +assert( + agentExportNames.length > 0, + "canonical agents namespace has no exports", +); function moduleSpecifier(fromPath, toPath) { const path = relative(dirname(fromPath), toPath).replaceAll("\\", "/"); return path.startsWith(".") ? path : `./${path}`; } -async function writeCjsFacade(outputPath, exportNames) { +async function writeCjsFacade(outputPath, exportNames, namespace) { const bundleSpecifier = moduleSpecifier(outputPath, bundlePath); + const canonical = namespace ? `canonical.${namespace}` : "canonical"; const source = [ '"use strict";', "// Generated by scripts/canonicalize-runtime-provenance-exports.mjs.", "// Private provenance state is lexical inside the canonical bundle.", `const canonical = require(${JSON.stringify(bundleSpecifier)});`, 'Object.defineProperty(exports, "__esModule", { value: true });', - ...exportNames.map((name) => `exports.${name} = canonical.${name};`), + ...exportNames.map((name) => `exports.${name} = ${canonical}.${name};`), "", ].join("\n"); await writeFile(outputPath, source); @@ -84,6 +91,7 @@ async function writeEsmFacade(outputPath, cjsPath, exportNames) { await writeCjsFacade(cjsRootPath, rootExportNames); await writeCjsFacade(cjsCarrierPath, carrierExportNames); +await writeCjsFacade(cjsAgentsPath, agentExportNames, "agents"); await writeEsmFacade( resolve(packageRoot, "dist/esm/index.js"), cjsRootPath, @@ -94,6 +102,11 @@ await writeEsmFacade( cjsCarrierPath, carrierExportNames, ); +await writeEsmFacade( + resolve(packageRoot, "dist/esm/agents/index.js"), + cjsAgentsPath, + agentExportNames, +); for (const format of ["cjs", "esm"]) { const storeBase = resolve( diff --git a/packages/tools/scripts/test-runtime-provenance-package.mjs b/packages/tools/scripts/test-runtime-provenance-package.mjs index a2c3f843c..6b93d3c4d 100644 --- a/packages/tools/scripts/test-runtime-provenance-package.mjs +++ b/packages/tools/scripts/test-runtime-provenance-package.mjs @@ -234,6 +234,40 @@ const attackPost = attackServer.calls.find( assert.equal(header(attackPost, CALLSITE_HEADER), null); await attackClient.shutdown(); +async function verifyStubSurface(label, stubModule) { + assert.equal("default" in stubModule, false); + assert.equal(typeof stubModule.createStubClient, "function"); + const stub = stubModule.createStubClient(); + const runResult = await stub.agents.run({ + definition: `${label}-stub-run`, + }); + assert.equal(runResult.status, "completed"); + const handle = await stub.agents.launch({ + definition: `${label}-stub-launch`, + }); + assert.equal(handle.dispatch.resultSignal, cjsTools.AGENTS_RESULT_SIGNAL); + assert.equal((await handle.wait()).status, "completed"); + await stub.shutdown(); +} + +const cjsStub = require("@sapiom/tools/stub"); +const esmStub = await import("@sapiom/tools/stub"); +await verifyStubSurface("cjs", cjsStub); +await verifyStubSurface("esm", esmStub); + +const loadedAfterStub = Object.values(require.cache).filter((loaded) => + loaded?.filename.includes("/packages/tools/dist/"), +); +for (const loaded of loadedAfterStub) { + for (const name of forbiddenHelperNames) { + assert.equal( + Object.prototype.hasOwnProperty.call(loaded.exports ?? {}, name), + false, + `private helper ${name} exposed by ${loaded.filename}`, + ); + } +} + console.log( - "runtime provenance package surfaces: cache-private CJS + ESM and four cross-format paths passed", + "runtime provenance package surfaces: cache-private CJS + ESM, four cross-format paths, and both stub formats passed", ); From df72e59d5620cc49e4ebe81baaf9f37fed43c125 Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 1 Sep 2026 01:59:11 +0000 Subject: [PATCH 10/15] fix(tools): preserve native package module graphs Refs: SAP-3020 --- .changeset/calm-agents-carry.md | 5 +- .../scripts/agent-runtime-provenance-entry.ts | 2 +- ...anonicalize-runtime-provenance-exports.mjs | 110 ++++++++++--- .../test-runtime-provenance-package.mjs | 151 +++++++++++++++++- packages/tools/src/agents/README.md | 30 ---- packages/tools/src/agents/index.ts | 57 +++++-- .../src/agents/runtime-callsite-store.ts | 10 +- .../src/agents/runtime-provenance.spec.ts | 141 ++++++++++++++++ 8 files changed, 437 insertions(+), 69 deletions(-) diff --git a/.changeset/calm-agents-carry.md b/.changeset/calm-agents-carry.md index ddeeb751a..4b6168655 100644 --- a/.changeset/calm-agents-carry.md +++ b/.changeset/calm-agents-carry.md @@ -9,4 +9,7 @@ that receipt through a trusted, one-shot build callsite. Private receipt state i not package-exported; reflected values are redacted from errors. Request/result JSON and calls without metadata remain unchanged. CJS and ESM imports share one bundled lexical store, including mixed-format direct handoffs, without exposing -private extraction or rebinding helpers through the module cache. +private extraction or rebinding helpers through the module cache. Build tooling +uses the unsupported implementation subpath +`@sapiom/tools/_internal/agent-runtime-provenance`; that subpath may change in +any release. diff --git a/packages/tools/scripts/agent-runtime-provenance-entry.ts b/packages/tools/scripts/agent-runtime-provenance-entry.ts index f771f65fa..9e7607377 100644 --- a/packages/tools/scripts/agent-runtime-provenance-entry.ts +++ b/packages/tools/scripts/agent-runtime-provenance-entry.ts @@ -1,4 +1,4 @@ -export * from "../src/index.js"; +export * as agents from "../src/agents/index.js"; export { AGENT_RUNTIME_PROVENANCE_VERSION, carryAgentRuntimeProvenance, diff --git a/packages/tools/scripts/canonicalize-runtime-provenance-exports.mjs b/packages/tools/scripts/canonicalize-runtime-provenance-exports.mjs index ba2945227..a6b35809e 100644 --- a/packages/tools/scripts/canonicalize-runtime-provenance-exports.mjs +++ b/packages/tools/scripts/canonicalize-runtime-provenance-exports.mjs @@ -8,7 +8,6 @@ import { build } from "esbuild"; const require = createRequire(import.meta.url); const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); -const cjsRootPath = resolve(packageRoot, "dist/cjs/index.js"); const cjsCarrierPath = resolve( packageRoot, "dist/cjs/_internal/agent-runtime-provenance.js", @@ -16,7 +15,7 @@ const cjsCarrierPath = resolve( const cjsAgentsPath = resolve(packageRoot, "dist/cjs/agents/index.js"); const bundlePath = resolve( packageRoot, - "dist/runtime/agent-runtime-provenance.cjs", + "dist/cjs/agents/runtime-provenance.cjs", ); function runtimeExportNames(modulePath) { @@ -28,13 +27,10 @@ function runtimeExportNames(modulePath) { return names; } -const rootExportNames = runtimeExportNames(cjsRootPath); const carrierExportNames = runtimeExportNames(cjsCarrierPath); -const supportedExportNames = [ - ...new Set([...rootExportNames, ...carrierExportNames]), -].sort(); +const agentExportNames = runtimeExportNames(cjsAgentsPath); -await build({ +const canonicalBuild = await build({ entryPoints: [ resolve(packageRoot, "scripts/agent-runtime-provenance-entry.ts"), ], @@ -44,12 +40,50 @@ await build({ platform: "node", target: "node18", packages: "external", + plugins: [ + { + name: "native-agent-transport-boundary", + setup(builder) { + builder.onResolve({ filter: /^\.\.\/_client\/index\.js$/ }, () => ({ + path: "agent-runtime-provenance-transport-shim", + namespace: "agent-runtime-provenance", + })); + builder.onLoad( + { + filter: /^agent-runtime-provenance-transport-shim$/, + namespace: "agent-runtime-provenance", + }, + () => ({ + contents: + 'export function defaultTransport() { throw new Error("agent transport must be supplied by the format-native facade"); }', + loader: "js", + }), + ); + }, + }, + ], + metafile: true, sourcemap: false, logLevel: "warning", }); -assert.deepEqual(runtimeExportNames(bundlePath), supportedExportNames); -const agentExportNames = Object.keys(require(bundlePath).agents).sort(); +assert.equal( + Object.keys(canonicalBuild.metafile.inputs).some((input) => + input.includes("src/_client/"), + ), + false, + "canonical provenance artifact must not bundle a real client graph", +); + +assert.deepEqual(runtimeExportNames(bundlePath), [ + "AGENT_RUNTIME_PROVENANCE_VERSION", + "agents", + "carryAgentRuntimeProvenance", +]); +assert.deepEqual( + Object.keys(require(bundlePath).agents).sort(), + agentExportNames, +); assert( agentExportNames.length > 0, "canonical agents namespace has no exports", @@ -89,22 +123,62 @@ async function writeEsmFacade(outputPath, cjsPath, exportNames) { await rm(`${outputPath}.map`, { force: true }); } -await writeCjsFacade(cjsRootPath, rootExportNames); +async function writeCjsAgentsFacade(outputPath, exportNames) { + const bundleSpecifier = moduleSpecifier(outputPath, bundlePath); + const source = [ + '"use strict";', + "// Generated by scripts/canonicalize-runtime-provenance-exports.mjs.", + "// The facade supplies this format's native default transport.", + `const canonical = require(${JSON.stringify(bundleSpecifier)}).agents;`, + 'const { defaultTransport } = require("../_client/index.js");', + 'Object.defineProperty(exports, "__esModule", { value: true });', + ...exportNames + .filter((name) => name !== "launch" && name !== "run") + .map((name) => `exports.${name} = canonical.${name};`), + "exports.launch = async function launch(spec, transport = defaultTransport(), baseUrl) {", + " return canonical.launch(spec, transport, baseUrl);", + "};", + "exports.run = async function run(spec, transport = defaultTransport(), baseUrl) {", + " return canonical.run(spec, transport, baseUrl);", + "};", + "", + ].join("\n"); + await writeFile(outputPath, source); + await rm(`${outputPath}.map`, { force: true }); +} + +async function writeEsmAgentsFacade(outputPath, exportNames) { + const bundleSpecifier = moduleSpecifier(outputPath, bundlePath); + const source = [ + "// Generated by scripts/canonicalize-runtime-provenance-exports.mjs.", + "// The facade supplies this format's native default transport.", + `import canonicalModule from ${JSON.stringify(bundleSpecifier)};`, + 'import { defaultTransport } from "../_client/index.js";', + "const canonical = canonicalModule.agents;", + ...exportNames + .filter((name) => name !== "launch" && name !== "run") + .map((name) => `export const ${name} = canonical.${name};`), + "export async function launch(spec, transport = defaultTransport(), baseUrl) {", + " return canonical.launch(spec, transport, baseUrl);", + "}", + "export async function run(spec, transport = defaultTransport(), baseUrl) {", + " return canonical.run(spec, transport, baseUrl);", + "}", + "", + ].join("\n"); + await writeFile(outputPath, source); + await rm(`${outputPath}.map`, { force: true }); +} + await writeCjsFacade(cjsCarrierPath, carrierExportNames); -await writeCjsFacade(cjsAgentsPath, agentExportNames, "agents"); -await writeEsmFacade( - resolve(packageRoot, "dist/esm/index.js"), - cjsRootPath, - rootExportNames, -); +await writeCjsAgentsFacade(cjsAgentsPath, agentExportNames); await writeEsmFacade( resolve(packageRoot, "dist/esm/_internal/agent-runtime-provenance.js"), cjsCarrierPath, carrierExportNames, ); -await writeEsmFacade( +await writeEsmAgentsFacade( resolve(packageRoot, "dist/esm/agents/index.js"), - cjsAgentsPath, agentExportNames, ); diff --git a/packages/tools/scripts/test-runtime-provenance-package.mjs b/packages/tools/scripts/test-runtime-provenance-package.mjs index 6b93d3c4d..91addfe82 100644 --- a/packages/tools/scripts/test-runtime-provenance-package.mjs +++ b/packages/tools/scripts/test-runtime-provenance-package.mjs @@ -1,7 +1,8 @@ import assert from "node:assert/strict"; -import { existsSync } from "node:fs"; +import { existsSync, readFileSync } from "node:fs"; import { createRequire } from "node:module"; import { dirname, resolve } from "node:path"; +import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; const require = createRequire(import.meta.url); @@ -11,6 +12,63 @@ const VERSION_HEADER = "x-sapiom-runtime-provenance-version"; const CALLSITE_HEADER = "x-sapiom-runtime-callsite-evidence"; const LINEAGE_HEADER = "x-sapiom-runtime-lineage-receipt"; +function verifyIsolatedFormat(label, source, inputType) { + const result = spawnSync( + process.execPath, + [...(inputType ? ["--input-type", inputType] : []), "--eval", source], + { + cwd: packageRoot, + encoding: "utf8", + env: { ...process.env, SAPIOM_API_KEY: "isolated-format-key" }, + }, + ); + assert.equal( + result.status, + 0, + `${label} isolated probe failed\n${result.stdout}\n${result.stderr}`, + ); +} + +const isolatedServerSource = ` +let execution = 0; +globalThis.fetch = async (_input, init = {}) => { + new Headers(init.headers); + if (init.method === "POST") { + execution += 1; + return new Response(JSON.stringify({ status: "enqueued", executionId: "isolated-" + execution }), { status: 201, headers: { "content-type": "application/json" } }); + } + return new Response(JSON.stringify({ status: "completed", output: { ok: true }, error: null }), { status: 200, headers: { "content-type": "application/json" } }); +};`; + +verifyIsolatedFormat( + "ESM native transport", + ` +import { createRequire } from "node:module"; +${isolatedServerSource} +const require = createRequire(import.meta.url); +const tools = await import("@sapiom/tools"); +const handle = await tools.agents.launch({ definition: "esm-native-launch" }); +if ((await handle.wait({ pollMs: 1 })).status !== "completed") process.exit(2); +if ((await tools.agents.run({ definition: "esm-native-run" })).status !== "completed") process.exit(3); +if (Object.keys(require.cache).some((path) => path.endsWith("/dist/cjs/_client/index.js"))) process.exit(4); +`, + "module", +); + +verifyIsolatedFormat( + "CJS native transport", + ` +${isolatedServerSource} +const tools = require("@sapiom/tools"); +(async () => { + const handle = await tools.agents.launch({ definition: "cjs-native-launch" }); + if ((await handle.wait({ pollMs: 1 })).status !== "completed") process.exit(2); + if ((await tools.agents.run({ definition: "cjs-native-run" })).status !== "completed") process.exit(3); + if (Object.keys(require.cache).some((path) => path.includes("/dist/esm/"))) process.exit(4); +})().catch((error) => { console.error(error); process.exit(5); }); +`, +); + function fakeAgentServer() { const calls = []; let execution = 0; @@ -75,8 +133,48 @@ async function verifySameFormat(label, tools, carrier) { const cjsTools = require("@sapiom/tools"); const cjsCarrier = require(CARRIER_EXPORT); +const cjsSandboxes = require("@sapiom/tools/sandboxes"); +const cjsRepositories = require("@sapiom/tools/repositories"); +const cjsMemory = require("@sapiom/tools/memory"); +const cjsFileStorage = require("@sapiom/tools/file-storage"); +const cjsContentGeneration = require("@sapiom/tools/content-generation"); +const cjsSearch = require("@sapiom/tools/search"); +const cjsDatabase = require("@sapiom/tools/database"); +const cjsRootSource = readFileSync( + resolve(packageRoot, "dist/cjs/index.js"), + "utf8", +); assert.equal("default" in cjsTools, false); assert.equal("default" in cjsCarrier, false); +assert.match(cjsRootSource, /require\("\.\/client\.js"\)/); +assert.doesNotMatch(cjsRootSource, /runtime-provenance\.cjs/); +assert.equal(cjsTools.Sandbox, cjsSandboxes.Sandbox); +assert.equal(cjsTools.Repository, cjsRepositories.Repository); +assert.equal(cjsTools.MemoryHttpError, cjsMemory.MemoryHttpError); +assert.equal( + cjsTools.FileStorageHttpError, + cjsFileStorage.FileStorageHttpError, +); +assert.equal( + cjsTools.ContentGenerationHttpError, + cjsContentGeneration.ContentGenerationHttpError, +); +assert.equal(cjsTools.SearchHttpError, cjsSearch.SearchHttpError); +assert.equal(cjsTools.DatabaseHttpError, cjsDatabase.DatabaseHttpError); +assert.equal(cjsTools.sandboxes.create, cjsSandboxes.create); +assert.equal(cjsTools.repositories.create, cjsRepositories.create); +assert.equal(cjsTools.memory.recall, cjsMemory.recall); +assert.equal(cjsTools.agents.launch.name, "launch"); +assert.equal(cjsTools.agents.launch.length, 1); +assert.equal(cjsTools.agents.launch.constructor.name, "AsyncFunction"); +const cjsClientModules = Object.values(require.cache).filter((loaded) => + loaded?.filename.endsWith("/dist/cjs/_client/index.js"), +); +assert.equal(cjsClientModules.length, 1); +assert.equal( + cjsClientModules[0].exports.defaultTransport(), + cjsClientModules[0].exports.defaultTransport(), +); assert.throws( () => require("@sapiom/tools/dist/cjs/agents/runtime-callsite-store.js"), (error) => error?.code === "ERR_PACKAGE_PATH_NOT_EXPORTED", @@ -85,6 +183,13 @@ await verifySameFormat("cjs", cjsTools, cjsCarrier); const esmTools = await import("@sapiom/tools"); const esmCarrier = await import(CARRIER_EXPORT); +const esmSandboxes = await import("@sapiom/tools/sandboxes"); +const esmRepositories = await import("@sapiom/tools/repositories"); +const esmMemory = await import("@sapiom/tools/memory"); +const esmFileStorage = await import("@sapiom/tools/file-storage"); +const esmContentGeneration = await import("@sapiom/tools/content-generation"); +const esmSearch = await import("@sapiom/tools/search"); +const esmDatabase = await import("@sapiom/tools/database"); await assert.rejects( import("@sapiom/tools/dist/esm/agents/runtime-callsite-store.js"), (error) => error?.code === "ERR_PACKAGE_PATH_NOT_EXPORTED", @@ -92,13 +197,40 @@ await assert.rejects( assert.equal("default" in esmTools, false); assert.equal("default" in esmCarrier, false); assert.deepEqual(Object.keys(esmTools).sort(), Object.keys(cjsTools).sort()); -assert.equal(esmTools.createClient, cjsTools.createClient); +assert.equal(esmTools.Sandbox, esmSandboxes.Sandbox); +assert.equal(esmTools.Repository, esmRepositories.Repository); +assert.equal(esmTools.MemoryHttpError, esmMemory.MemoryHttpError); +assert.equal( + esmTools.FileStorageHttpError, + esmFileStorage.FileStorageHttpError, +); +assert.equal( + esmTools.ContentGenerationHttpError, + esmContentGeneration.ContentGenerationHttpError, +); +assert.equal(esmTools.SearchHttpError, esmSearch.SearchHttpError); +assert.equal(esmTools.DatabaseHttpError, esmDatabase.DatabaseHttpError); +assert.equal(esmTools.sandboxes.create, esmSandboxes.create); +assert.equal(esmTools.repositories.create, esmRepositories.create); +assert.equal(esmTools.memory.recall, esmMemory.recall); +assert.equal(esmTools.agents.launch.name, "launch"); +assert.equal(esmTools.agents.launch.length, 1); +assert.equal(esmTools.agents.launch.constructor.name, "AsyncFunction"); assert.equal( esmCarrier.carryAgentRuntimeProvenance, cjsCarrier.carryAgentRuntimeProvenance, ); await verifySameFormat("esm", esmTools, esmCarrier); +const esmRootSource = readFileSync( + resolve(packageRoot, "dist/esm/index.js"), + "utf8", +); +assert.match(esmRootSource, /from "\.\/client\.js"/); +assert.match(esmRootSource, /export \* as agents from "\.\/agents\/index\.js"/); +assert.doesNotMatch(esmRootSource, /\.\.\/cjs\/index\.js/); +assert.doesNotMatch(esmRootSource, /runtime-provenance\.cjs/); + async function verifyCrossFormatCallsite(label, tools, carrier) { const server = fakeAgentServer(); const client = tools.createClient({ apiKey: "k", fetch: server.fetch }); @@ -192,14 +324,21 @@ const forbiddenHelperNames = [ for (const name of forbiddenHelperNames) { assert.equal(loadedExportNames.includes(name), false); } -const supportedCacheExports = new Set([ +const supportedProvenanceCacheExports = new Set([ ...Object.keys(cjsTools), + ...Object.keys(cjsTools.agents), ...Object.keys(cjsCarrier), ]); -for (const loaded of loadedToolsModules) { +const loadedProvenanceModules = loadedToolsModules.filter( + (loaded) => + loaded.filename.endsWith("/agents/index.js") || + loaded.filename.includes("agent-runtime-provenance") || + loaded.filename.includes("runtime-provenance.cjs"), +); +for (const loaded of loadedProvenanceModules) { assert.deepEqual( Object.keys(loaded.exports).filter( - (name) => !supportedCacheExports.has(name), + (name) => !supportedProvenanceCacheExports.has(name), ), [], `unsupported cache exports from ${loaded.filename}`, @@ -269,5 +408,5 @@ for (const loaded of loadedAfterStub) { } console.log( - "runtime provenance package surfaces: cache-private CJS + ESM, four cross-format paths, and both stub formats passed", + "runtime provenance package surfaces: native roots, constructor identity, cache-private CJS + ESM, four cross-format paths, and both stub formats passed", ); diff --git a/packages/tools/src/agents/README.md b/packages/tools/src/agents/README.md index 7631cd3d1..16ab6ffd8 100644 --- a/packages/tools/src/agents/README.md +++ b/packages/tools/src/agents/README.md @@ -45,36 +45,6 @@ const useResult = defineStep({ - **Failure is data, not an exception.** The result is discriminated on `status` (`"completed" | "failed"`). A failed run resumes your step with `status: "failed"` and an `error` to branch on — it does not throw. Validate an incoming payload with `agents.agentResultSchema.parse(value)` if you want a runtime check. -### Runtime provenance (internal) - -Instrumented bundles may associate an opaque v1 callsite token with an agent -invocation through `@sapiom/tools/_internal/agent-runtime-provenance`. The token -travels in dedicated request headers, never in `AgentRunSpec` or its JSON body. -When a terminal response carries a supported server-signed lineage receipt, the -SDK retains it in object-identity sidecars on the returned result and its exact -object-valued `output`. One receipt can be forwarded once, only when one of those -exact objects is the direct `input` of an immediate invocation carrying a valid -v1 build callsite. An uninstrumented agent invocation consumes the sidecar -without forwarding it, a timer turn expires it, and delayed dispatch never sends -runtime provenance. Copies, nested values, and transformed primitives do not -inherit the sidecar. - -The detectable boundary is intentionally narrower than arbitrary data-flow -tracking: the SDK does not recursively inspect values and cannot distinguish a -synchronous exact-reference round trip through an in-memory array or `Map` from -a direct handoff before the one-turn sidecar expires. Queue/storage exclusion is -therefore guaranteed only when the boundary changes object identity, crosses a -timer turn, or invokes an uninstrumented agent boundary (which consumes the -receipt). The package's CJS and ESM root/carrier exports route through one -canonical bundled implementation. Its callsite and receipt stores remain lexical, -so supported mixed-format handoffs share private state without process globals, -public extraction/rebinding helpers, or callable private helpers in -`require.cache`. Standalone callsite-store artifacts are omitted from the package. -All four cross-format directions are tested. -The SDK treats all carrier values as opaque and exposes no new caller, callee, -bundle, or execution identity. Missing or unsupported metadata preserves legacy -behavior. - - **Addressed by slug.** `definition` is the deployed agent's slug — its stable handle. `input` is passed to its entry step. - **`idempotencyKey` deduplicates.** Repeating a launch with the same key returns the existing run instead of starting a new one. diff --git a/packages/tools/src/agents/index.ts b/packages/tools/src/agents/index.ts index 4f9bc7284..26fc84f69 100644 --- a/packages/tools/src/agents/index.ts +++ b/packages/tools/src/agents/index.ts @@ -14,7 +14,8 @@ * use it for inline standalone calls, NOT to pause a step (it returns a result, not * a pausable handle). An orchestration is addressed by its **slug** (its stable handle). */ -import { Transport, defaultTransport } from "../_client/index.js"; +import { defaultTransport } from "../_client/index.js"; +import type { Transport } from "../_client/index.js"; import { takeAgentRuntimeCallsite } from "./runtime-callsite-store.js"; import type { DispatchHandle } from "../dispatch.js"; @@ -115,15 +116,53 @@ function redactAgentRuntimeProvenance( function redactedAgentRuntimeError( error: unknown, privateValues: readonly (string | null | undefined)[], -): Error { - const redacted = new Error( - redactAgentRuntimeProvenance( - error instanceof Error ? error.message : String(error), - privateValues, - ), +): unknown { + const values = [...new Set(privateValues.filter(supportedOpaqueToken))].sort( + (left, right) => right.length - left.length, ); - if (error instanceof Error) redacted.name = error.name; - return redacted; + if (values.length === 0) return error; + + const redactString = (value: string): string => + redactAgentRuntimeProvenance(value, values); + if (!(error instanceof Error)) { + const message = String(error); + const redacted = redactString(message); + return redacted === message ? error : new Error(redacted); + } + + const sanitizedErrors = new WeakMap(); + const sanitizeError = (source: Error): Error => { + const cached = sanitizedErrors.get(source); + if (cached) return cached; + + let changed = false; + const target = Object.create(Object.getPrototypeOf(source)) as Error; + sanitizedErrors.set(source, target); + + for (const key of Reflect.ownKeys(source)) { + const descriptor = Object.getOwnPropertyDescriptor(source, key)!; + if ("value" in descriptor) { + if (typeof descriptor.value === "string") { + const value = redactString(descriptor.value); + changed ||= value !== descriptor.value; + descriptor.value = value; + } else if (descriptor.value instanceof Error) { + const value = sanitizeError(descriptor.value); + changed ||= value !== descriptor.value; + descriptor.value = value; + } + } + Object.defineProperty(target, key, descriptor); + } + + if (!changed) { + sanitizedErrors.set(source, source); + return source; + } + return target; + }; + + return sanitizeError(error); } export interface AgentRunSpec { diff --git a/packages/tools/src/agents/runtime-callsite-store.ts b/packages/tools/src/agents/runtime-callsite-store.ts index 7e86477c8..767f40180 100644 --- a/packages/tools/src/agents/runtime-callsite-store.ts +++ b/packages/tools/src/agents/runtime-callsite-store.ts @@ -10,12 +10,14 @@ interface CallsiteRecord { const invocationCallsites = new WeakMap(); -function supportedOpaqueToken(value: unknown): value is string { +function supportedCallsite(value: unknown): value is string { return ( typeof value === "string" && value.length > 0 && value.length <= MAX_OPAQUE_TOKEN_LENGTH && - !/[\r\n]/.test(value) + value.trim() === value && + !/[\r\n]/.test(value) && + /^[\x20-\x7e]+$/.test(value) ); } @@ -27,7 +29,7 @@ export function registerAgentRuntimeCallsite( ): void { if ( version !== AGENT_RUNTIME_PROVENANCE_VERSION || - !supportedOpaqueToken(callsite) + !supportedCallsite(callsite) ) { return; } @@ -47,7 +49,7 @@ export function takeAgentRuntimeCallsite(spec: object): string | undefined { const record = invocationCallsites.get(spec); invocationCallsites.delete(spec); const callsite = - record?.active && supportedOpaqueToken(record.callsite) + record?.active && supportedCallsite(record.callsite) ? record.callsite : undefined; if (record) record.active = false; diff --git a/packages/tools/src/agents/runtime-provenance.spec.ts b/packages/tools/src/agents/runtime-provenance.spec.ts index db1eead6b..691f7bbec 100644 --- a/packages/tools/src/agents/runtime-provenance.spec.ts +++ b/packages/tools/src/agents/runtime-provenance.spec.ts @@ -40,6 +40,9 @@ function agentServer( init: RequestInit = {}, ) => { const url = String(input); + // Mirror Fetch's header-value conversion so optional provenance can never + // make an otherwise valid invocation fail before the request is observed. + new Headers(init.headers); calls.push({ url, init }); if (init.method === "POST") { nextExecution += 1; @@ -289,6 +292,37 @@ describe("agents runtime provenance v1", () => { ); }); + it.each([ + ["NUL/control", `opaque${String.fromCharCode(0)}token`], + [ + "non-ByteString Unicode/emoji", + `opaque${String.fromCodePoint(0x1f680)}token`, + ], + ["leading whitespace", " opaque-token"], + ["trailing whitespace", "opaque-token "], + ])( + "omits unsupported %s callsite evidence without changing launch behavior", + async (_label, callsite) => { + const server = agentServer(); + const client = createClient({ apiKey: "k", fetch: server.fetch }); + const spec = carryAgentRuntimeProvenance( + { definition: "ordinary", input: { public: true } }, + { version: 1, callsite }, + ); + + const handle = await client.agents.launch(spec); + expect(handle.executionId).toBe("exec-1"); + const launch = posts(server.calls)[0]!; + expect( + header(launch, AGENT_RUNTIME_PROVENANCE_VERSION_HEADER), + ).toBeUndefined(); + expect(header(launch, AGENT_RUNTIME_CALLSITE_HEADER)).toBeUndefined(); + expect(JSON.parse(String(launch.init.body))).toEqual({ + input: { public: true }, + }); + }, + ); + it("requires a build-carried callsite and consumes lineage at an uninstrumented boundary", async () => { const server = agentServer({ receiptVersion: "1", @@ -561,6 +595,113 @@ describe("agents runtime provenance v1", () => { expect(String(posts(calls)[1]!.init.body)).not.toContain(receipt); }); + it("rethrows an uninstrumented typed transport error untouched", async () => { + const cause = Object.assign(new Error("connect refused"), { + code: "ECONNREFUSED", + }); + const failure = new TypeError("fetch failed") as TypeError & { + cause: Error; + }; + Object.defineProperty(failure, "cause", { + configurable: true, + value: cause, + writable: true, + }); + const originalStack = failure.stack; + const fetch = (async () => { + throw failure; + }) as typeof globalThis.fetch; + const client = createClient({ apiKey: "k", fetch }); + + let error: unknown; + try { + await client.agents.launch({ definition: "uninstrumented" }); + } catch (value) { + error = value; + } + + expect(error).toBe(failure); + expect(error).toBeInstanceOf(TypeError); + expect((error as Error & { cause: Error }).cause).toBe(cause); + expect((error as Error).stack).toBe(originalStack); + expect( + ((error as Error & { cause: Error }).cause as Error & { code: string }) + .code, + ).toBe("ECONNREFUSED"); + }); + + it("preserves typed errors, causes, diagnostics, and stack frames while redacting", async () => { + const callsite = "callsite.typed-private"; + class AgentTransportError extends TypeError { + readonly diagnostic = "request transport failed"; + readonly code = "EAGENT"; + } + const cause = Object.assign(new Error(`connect failed for ${callsite}`), { + code: "ECONNREFUSED", + }); + const failure = new AgentTransportError( + `fetch failed for ${callsite}`, + ) as AgentTransportError & { cause: Error }; + Object.defineProperty(failure, "stack", { + configurable: true, + value: + `AgentTransportError: fetch failed for ${callsite}\n` + + " at runtime-provenance.spec.ts:1:1", + writable: true, + }); + Object.defineProperty(cause, "stack", { + configurable: true, + value: + `Error: connect failed for ${callsite}\n` + + " at runtime-provenance.spec.ts:2:1", + writable: true, + }); + Object.defineProperty(failure, "cause", { + configurable: true, + value: cause, + writable: true, + }); + const fetch = (async () => { + throw failure; + }) as typeof globalThis.fetch; + const client = createClient({ apiKey: "k", fetch }); + + let error: unknown; + try { + await client.agents.launch( + carryAgentRuntimeProvenance( + { definition: "instrumented" }, + { version: 1, callsite }, + ), + ); + } catch (value) { + error = value; + } + + expect(error).not.toBe(failure); + expect(error).toBeInstanceOf(AgentTransportError); + expect(Object.getPrototypeOf(error)).toBe(AgentTransportError.prototype); + expect((error as AgentTransportError).name).toBe(failure.name); + expect((error as AgentTransportError).code).toBe("EAGENT"); + expect((error as AgentTransportError).diagnostic).toBe( + "request transport failed", + ); + expect((error as AgentTransportError).message).toContain("fetch failed"); + expect((error as AgentTransportError).message).not.toContain(callsite); + expect((error as AgentTransportError).stack).toContain( + "runtime-provenance.spec.ts", + ); + expect((error as AgentTransportError).stack).not.toContain(callsite); + const redactedCause = (error as AgentTransportError & { cause: Error }) + .cause as Error & { code: string }; + expect(redactedCause).not.toBe(cause); + expect(redactedCause).toBeInstanceOf(Error); + expect(redactedCause.code).toBe("ECONNREFUSED"); + expect(redactedCause.message).toContain("connect failed"); + expect(redactedCause.message).not.toContain(callsite); + expect(redactedCause.stack).not.toContain(callsite); + }); + it("redacts reflected callsite and response receipt from status errors", async () => { const receipt = "signed.status-private"; const callsite = "callsite.status-private"; From 4e7824534f9a7fa2bc4ff74fde2f1ae617a537c7 Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 1 Sep 2026 02:16:25 +0000 Subject: [PATCH 11/15] fix(tools): redact nested provenance diagnostics Refs: SAP-3020 --- packages/tools/src/agents/index.ts | 67 +++--- .../src/agents/runtime-provenance.spec.ts | 195 ++++++++++++++++++ 2 files changed, 237 insertions(+), 25 deletions(-) diff --git a/packages/tools/src/agents/index.ts b/packages/tools/src/agents/index.ts index 26fc84f69..4480d4faa 100644 --- a/packages/tools/src/agents/index.ts +++ b/packages/tools/src/agents/index.ts @@ -124,45 +124,62 @@ function redactedAgentRuntimeError( const redactString = (value: string): string => redactAgentRuntimeProvenance(value, values); - if (!(error instanceof Error)) { + const isTraversableDiagnostic = (value: unknown): value is object => { + if (value === null || typeof value !== "object") return false; + if (value instanceof Error || Array.isArray(value)) return true; + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; + }; + + if (!isTraversableDiagnostic(error)) { const message = String(error); const redacted = redactString(message); return redacted === message ? error : new Error(redacted); } - const sanitizedErrors = new WeakMap(); - const sanitizeError = (source: Error): Error => { - const cached = sanitizedErrors.get(source); - if (cached) return cached; + const inspected = new WeakSet(); + const containsPrivateValue = (value: unknown): boolean => { + if (typeof value === "string") return redactString(value) !== value; + if (!isTraversableDiagnostic(value) || inspected.has(value)) return false; + inspected.add(value); + for (const key of Reflect.ownKeys(value)) { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if ( + descriptor && + "value" in descriptor && + containsPrivateValue(descriptor.value) + ) { + return true; + } + } + return false; + }; - let changed = false; - const target = Object.create(Object.getPrototypeOf(source)) as Error; - sanitizedErrors.set(source, target); + if (!containsPrivateValue(error)) return error; - for (const key of Reflect.ownKeys(source)) { - const descriptor = Object.getOwnPropertyDescriptor(source, key)!; + const sanitized = new WeakMap(); + const sanitizeValue = (value: unknown): unknown => { + if (typeof value === "string") return redactString(value); + if (!isTraversableDiagnostic(value)) return value; + const cached = sanitized.get(value); + if (cached) return cached; + + const target = Array.isArray(value) + ? Object.setPrototypeOf([], Object.getPrototypeOf(value)) + : Object.create(Object.getPrototypeOf(value)); + sanitized.set(value, target); + for (const key of Reflect.ownKeys(value)) { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (!descriptor) continue; if ("value" in descriptor) { - if (typeof descriptor.value === "string") { - const value = redactString(descriptor.value); - changed ||= value !== descriptor.value; - descriptor.value = value; - } else if (descriptor.value instanceof Error) { - const value = sanitizeError(descriptor.value); - changed ||= value !== descriptor.value; - descriptor.value = value; - } + descriptor.value = sanitizeValue(descriptor.value); } Object.defineProperty(target, key, descriptor); } - - if (!changed) { - sanitizedErrors.set(source, source); - return source; - } return target; }; - return sanitizeError(error); + return sanitizeValue(error); } export interface AgentRunSpec { diff --git a/packages/tools/src/agents/runtime-provenance.spec.ts b/packages/tools/src/agents/runtime-provenance.spec.ts index 691f7bbec..07148b144 100644 --- a/packages/tools/src/agents/runtime-provenance.spec.ts +++ b/packages/tools/src/agents/runtime-provenance.spec.ts @@ -630,6 +630,201 @@ describe("agents runtime provenance v1", () => { ).toBe("ECONNREFUSED"); }); + it("redacts nested ordinary diagnostics without mutating the original error graph", async () => { + const callsite = "callsite.nested-private"; + interface NestedDiagnostics { + request: { + headers: Record; + lazyDiagnostic?: string; + }; + response: { status: number; retryable: boolean }; + observedAt: Date; + } + let accessorReads = 0; + const observedAt = new Date("2026-09-01T00:00:00.000Z"); + const diagnostics: NestedDiagnostics = { + request: { + headers: { + [AGENT_RUNTIME_CALLSITE_HEADER]: callsite, + "x-request-id": "request-public", + }, + }, + response: { status: 502, retryable: true }, + observedAt, + }; + Object.defineProperty(diagnostics.request, "lazyDiagnostic", { + configurable: true, + enumerable: false, + get() { + accessorReads += 1; + return "lazy-public"; + }, + }); + class DiagnosticTransportError extends TypeError { + readonly code = "EAGENT"; + constructor(readonly diagnostics: NestedDiagnostics) { + super("fetch failed with diagnostics"); + } + } + const failure = new DiagnosticTransportError(diagnostics); + Object.defineProperty(failure, "stack", { + configurable: true, + enumerable: false, + value: + "DiagnosticTransportError: fetch failed with diagnostics\n at preserved-frame.ts:1:1", + writable: true, + }); + const originalDescriptor = Object.getOwnPropertyDescriptor( + failure, + "diagnostics", + ); + const fetch = (async () => { + throw failure; + }) as typeof globalThis.fetch; + const client = createClient({ apiKey: "k", fetch }); + + let error: unknown; + try { + await client.agents.launch( + carryAgentRuntimeProvenance( + { definition: "instrumented-nested" }, + { version: 1, callsite }, + ), + ); + } catch (value) { + error = value; + } + + expect(error).not.toBe(failure); + expect(error).toBeInstanceOf(DiagnosticTransportError); + expect((error as DiagnosticTransportError).code).toBe("EAGENT"); + expect((error as DiagnosticTransportError).message).toBe(failure.message); + expect((error as DiagnosticTransportError).stack).toBe(failure.stack); + expect( + (error as DiagnosticTransportError).diagnostics.request.headers[ + AGENT_RUNTIME_CALLSITE_HEADER + ], + ).toBe("[REDACTED runtime provenance]"); + expect( + (error as DiagnosticTransportError).diagnostics.request.headers[ + "x-request-id" + ], + ).toBe("request-public"); + expect((error as DiagnosticTransportError).diagnostics.response).toEqual({ + status: 502, + retryable: true, + }); + expect((error as DiagnosticTransportError).diagnostics.observedAt).toBe( + observedAt, + ); + expect(observedAt.toISOString()).toBe("2026-09-01T00:00:00.000Z"); + expect(accessorReads).toBe(0); + expect( + Object.getOwnPropertyDescriptor( + (error as DiagnosticTransportError).diagnostics.request, + "lazyDiagnostic", + )?.get, + ).toBe( + Object.getOwnPropertyDescriptor( + failure.diagnostics.request, + "lazyDiagnostic", + )?.get, + ); + expect(Object.getOwnPropertyDescriptor(error, "diagnostics")).toEqual( + expect.objectContaining({ + configurable: originalDescriptor?.configurable, + enumerable: originalDescriptor?.enumerable, + writable: originalDescriptor?.writable, + }), + ); + + expect(failure.diagnostics).toBe(diagnostics); + expect( + failure.diagnostics.request.headers[AGENT_RUNTIME_CALLSITE_HEADER], + ).toBe(callsite); + expect(failure.code).toBe("EAGENT"); + }); + + it("preserves exact identity when supplied provenance does not occur in nested diagnostics", async () => { + const diagnostics = { + request: { headers: { "x-request-id": "request-public" } }, + }; + const failure = Object.assign(new TypeError("fetch failed"), { + code: "EAGENT", + diagnostics, + }); + const fetch = (async () => { + throw failure; + }) as typeof globalThis.fetch; + const client = createClient({ apiKey: "k", fetch }); + + let error: unknown; + try { + await client.agents.launch( + carryAgentRuntimeProvenance( + { definition: "instrumented-no-match" }, + { version: 1, callsite: "callsite.not-reflected" }, + ), + ); + } catch (value) { + error = value; + } + + expect(error).toBe(failure); + expect((error as typeof failure).diagnostics).toBe(diagnostics); + }); + + it("redacts arrays and preserves cycles and shared ordinary diagnostics", async () => { + const callsite = "callsite.cyclic-private"; + const shared: Record = { + privateValue: callsite, + publicValue: "shared-public", + }; + const diagnostics: Record = { + entries: [shared, shared], + shared, + }; + diagnostics.self = diagnostics; + const failure = Object.assign(new TypeError("fetch failed"), { + code: "EAGENT", + diagnostics, + }); + const fetch = (async () => { + throw failure; + }) as typeof globalThis.fetch; + const client = createClient({ apiKey: "k", fetch }); + + let error: unknown; + try { + await client.agents.launch( + carryAgentRuntimeProvenance( + { definition: "instrumented-cyclic" }, + { version: 1, callsite }, + ), + ); + } catch (value) { + error = value; + } + + const sanitized = (error as typeof failure).diagnostics; + const sanitizedEntries = sanitized.entries as Record[]; + expect(error).not.toBe(failure); + expect(error).toBeInstanceOf(TypeError); + expect((error as typeof failure).code).toBe("EAGENT"); + expect(sanitized).not.toBe(diagnostics); + expect(sanitized.self).toBe(sanitized); + expect(sanitizedEntries[0]).toBe(sanitizedEntries[1]); + expect(sanitizedEntries[0]).toBe(sanitized.shared); + expect(sanitizedEntries[0]!.privateValue).toBe( + "[REDACTED runtime provenance]", + ); + expect(sanitizedEntries[0]!.publicValue).toBe("shared-public"); + + expect(diagnostics.self).toBe(diagnostics); + expect((diagnostics.entries as object[])[0]).toBe(shared); + expect(shared.privateValue).toBe(callsite); + }); + it("preserves typed errors, causes, diagnostics, and stack frames while redacting", async () => { const callsite = "callsite.typed-private"; class AgentTransportError extends TypeError { From 21ea187397e33f96bb70a538e5385aa939d7d62f Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 1 Sep 2026 02:38:58 +0000 Subject: [PATCH 12/15] fix(tools): preserve native error stacks Refs: SAP-3020 --- .../test-runtime-provenance-package.mjs | 209 +++++++++++++++++- packages/tools/src/agents/index.ts | 83 ++++++- .../src/agents/runtime-provenance.spec.ts | 89 +++++++- 3 files changed, 369 insertions(+), 12 deletions(-) diff --git a/packages/tools/scripts/test-runtime-provenance-package.mjs b/packages/tools/scripts/test-runtime-provenance-package.mjs index 91addfe82..68bea0434 100644 --- a/packages/tools/scripts/test-runtime-provenance-package.mjs +++ b/packages/tools/scripts/test-runtime-provenance-package.mjs @@ -131,6 +131,212 @@ async function verifySameFormat(label, tools, carrier) { await client.shutdown(); } +async function captureLaunchError(client, spec) { + try { + await client.agents.launch(spec); + } catch (error) { + return error; + } + assert.fail("expected agent launch to throw"); +} + +class NativeStackTransportError extends TypeError { + constructor(message, cause, diagnostics) { + super(message, { cause }); + this.name = "NativeStackTransportError"; + this.code = "EAGENT"; + this.diagnostics = diagnostics; + } +} + +function nativeStackFixture(callsite, diagnostics) { + const cause = new Error(`native cause reflected ${callsite}`); + cause.code = "ECONNREFUSED"; + return new NativeStackTransportError( + `native transport reflected ${callsite}`, + cause, + diagnostics, + ); +} + +async function verifyNativeErrorStackRedaction(tools, carrier) { + const callsite = "callsite.package-native-stack"; + let nestedGetterReads = 0; + const diagnostics = { + request: { + headers: { + [CALLSITE_HEADER]: callsite, + "x-request-id": "request-public", + }, + }, + response: { status: 502, retryable: true }, + }; + Object.defineProperty(diagnostics.request, "lazy", { + configurable: true, + enumerable: false, + get() { + nestedGetterReads += 1; + return callsite; + }, + }); + const failure = nativeStackFixture(callsite, diagnostics); + const failureStackDescriptor = Object.getOwnPropertyDescriptor( + failure, + "stack", + ); + assert.ok(failureStackDescriptor); + assert.equal("value" in failureStackDescriptor, false); + assert.equal(typeof failure.stack, "string"); + assert.match(failure.stack, /nativeStackFixture/); + assert.match(failure.stack, new RegExp(callsite)); + const client = tools.createClient({ + apiKey: "k", + fetch: async () => { + throw failure; + }, + }); + const caught = await captureLaunchError( + client, + carrier.carryAgentRuntimeProvenance( + { definition: "package-native-stack" }, + { version: 1, callsite }, + ), + ); + + assert.notEqual(caught, failure); + assert.ok(caught instanceof NativeStackTransportError); + assert.equal( + Object.getPrototypeOf(caught), + NativeStackTransportError.prototype, + ); + assert.equal(caught.code, "EAGENT"); + assert.equal(caught.diagnostics.response.status, 502); + assert.equal(caught.diagnostics.response.retryable, true); + assert.equal( + caught.diagnostics.request.headers["x-request-id"], + "request-public", + ); + assert.equal( + caught.diagnostics.request.headers[CALLSITE_HEADER], + "[REDACTED runtime provenance]", + ); + assert.equal(typeof caught.stack, "string"); + assert.match(caught.stack, /nativeStackFixture/); + assert.equal(caught.stack.includes(callsite), false); + const caughtStackDescriptor = Object.getOwnPropertyDescriptor( + caught, + "stack", + ); + assert.ok(caughtStackDescriptor); + assert.equal("value" in caughtStackDescriptor, false); + assert.equal(caughtStackDescriptor.get, failureStackDescriptor.get); + assert.equal(caughtStackDescriptor.set, failureStackDescriptor.set); + assert.equal( + caughtStackDescriptor.configurable, + failureStackDescriptor.configurable, + ); + assert.equal( + caughtStackDescriptor.enumerable, + failureStackDescriptor.enumerable, + ); + assert.equal(caught.message.includes(callsite), false); + assert.equal(caught.cause.message.includes(callsite), false); + assert.equal(typeof caught.cause.stack, "string"); + assert.match(caught.cause.stack, /nativeStackFixture/); + assert.equal(caught.cause.stack.includes(callsite), false); + assert.equal(caught.cause.code, "ECONNREFUSED"); + assert.equal(nestedGetterReads, 0); + assert.equal(failure.message.includes(callsite), true); + assert.equal(failure.stack.includes(callsite), true); + assert.equal(failure.cause.message.includes(callsite), true); + assert.equal(failure.cause.stack.includes(callsite), true); + assert.equal(failure.diagnostics.request.headers[CALLSITE_HEADER], callsite); + await client.shutdown(); + + let customStackReads = 0; + let customDiagnosticReads = 0; + const customDiagnostics = { reflected: callsite }; + Object.defineProperty(customDiagnostics, "lazy", { + configurable: true, + enumerable: false, + get() { + customDiagnosticReads += 1; + return callsite; + }, + }); + const customFailure = new NativeStackTransportError( + "custom stack transport", + new Error("public cause"), + customDiagnostics, + ); + const customStackGetter = () => { + customStackReads += 1; + return `custom stack ${callsite}`; + }; + Object.defineProperty(customFailure, "stack", { + configurable: true, + enumerable: false, + get: customStackGetter, + }); + const customClient = tools.createClient({ + apiKey: "k", + fetch: async () => { + throw customFailure; + }, + }); + const customCaught = await captureLaunchError( + customClient, + carrier.carryAgentRuntimeProvenance( + { definition: "package-custom-stack" }, + { version: 1, callsite }, + ), + ); + assert.notEqual(customCaught, customFailure); + assert.equal(customStackReads, 0); + assert.equal(customDiagnosticReads, 0); + assert.equal( + Object.getOwnPropertyDescriptor(customCaught, "stack").get, + customStackGetter, + ); + assert.equal( + customCaught.diagnostics.reflected, + "[REDACTED runtime provenance]", + ); + await customClient.shutdown(); + + const noMatchFailure = nativeStackFixture("public-value", { + request: { headers: { "x-request-id": "request-public" } }, + }); + const noMatchClient = tools.createClient({ + apiKey: "k", + fetch: async () => { + throw noMatchFailure; + }, + }); + const noMatchCaught = await captureLaunchError( + noMatchClient, + carrier.carryAgentRuntimeProvenance( + { definition: "package-no-match" }, + { version: 1, callsite: "callsite.not-reflected" }, + ), + ); + assert.equal(noMatchCaught, noMatchFailure); + await noMatchClient.shutdown(); + + const noPrivateFailure = new TypeError("uninstrumented transport"); + const noPrivateClient = tools.createClient({ + apiKey: "k", + fetch: async () => { + throw noPrivateFailure; + }, + }); + const noPrivateCaught = await captureLaunchError(noPrivateClient, { + definition: "package-no-private", + }); + assert.equal(noPrivateCaught, noPrivateFailure); + await noPrivateClient.shutdown(); +} + const cjsTools = require("@sapiom/tools"); const cjsCarrier = require(CARRIER_EXPORT); const cjsSandboxes = require("@sapiom/tools/sandboxes"); @@ -180,6 +386,7 @@ assert.throws( (error) => error?.code === "ERR_PACKAGE_PATH_NOT_EXPORTED", ); await verifySameFormat("cjs", cjsTools, cjsCarrier); +await verifyNativeErrorStackRedaction(cjsTools, cjsCarrier); const esmTools = await import("@sapiom/tools"); const esmCarrier = await import(CARRIER_EXPORT); @@ -408,5 +615,5 @@ for (const loaded of loadedAfterStub) { } console.log( - "runtime provenance package surfaces: native roots, constructor identity, cache-private CJS + ESM, four cross-format paths, and both stub formats passed", + "runtime provenance package surfaces: native Error stacks, native roots, constructor identity, cache-private CJS + ESM, four cross-format paths, and both stub formats passed", ); diff --git a/packages/tools/src/agents/index.ts b/packages/tools/src/agents/index.ts index 4480d4faa..22b3bd013 100644 --- a/packages/tools/src/agents/index.ts +++ b/packages/tools/src/agents/index.ts @@ -130,6 +130,44 @@ function redactedAgentRuntimeError( const prototype = Object.getPrototypeOf(value); return prototype === Object.prototype || prototype === null; }; + const nativeErrorStackDescriptor = Object.getOwnPropertyDescriptor( + new Error(), + "stack", + ); + const isNativeErrorStackAccessor = ( + source: object, + key: PropertyKey, + descriptor: PropertyDescriptor, + ): descriptor is PropertyDescriptor & { + get: () => unknown; + set: (value: unknown) => void; + } => + source instanceof Error && + key === "stack" && + !("value" in descriptor) && + nativeErrorStackDescriptor !== undefined && + !("value" in nativeErrorStackDescriptor) && + typeof descriptor.get === "function" && + typeof descriptor.set === "function" && + descriptor.get === nativeErrorStackDescriptor.get && + descriptor.set === nativeErrorStackDescriptor.set; + const nativeErrorStacks = new WeakMap(); + const nativeErrorStack = ( + source: object, + descriptor: PropertyDescriptor & { get: () => unknown }, + ): string | undefined => { + const cached = nativeErrorStacks.get(source); + if (cached !== undefined) return cached; + let stack: unknown; + try { + stack = descriptor.get.call(source); + } catch { + return undefined; + } + if (typeof stack !== "string") return undefined; + nativeErrorStacks.set(source, stack); + return stack; + }; if (!isTraversableDiagnostic(error)) { const message = String(error); @@ -144,6 +182,10 @@ function redactedAgentRuntimeError( inspected.add(value); for (const key of Reflect.ownKeys(value)) { const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (descriptor && isNativeErrorStackAccessor(value, key, descriptor)) { + const stack = nativeErrorStack(value, descriptor); + if (stack !== undefined && redactString(stack) !== stack) return true; + } if ( descriptor && "value" in descriptor && @@ -164,13 +206,48 @@ function redactedAgentRuntimeError( const cached = sanitized.get(value); if (cached) return cached; - const target = Array.isArray(value) - ? Object.setPrototypeOf([], Object.getPrototypeOf(value)) - : Object.create(Object.getPrototypeOf(value)); + let initializedStackDescriptor: PropertyDescriptor | undefined; + let target: object; + if (value instanceof Error) { + target = new Error(); + initializedStackDescriptor = Object.getOwnPropertyDescriptor( + target, + "stack", + ); + Object.setPrototypeOf(target, Object.getPrototypeOf(value)); + for (const key of Reflect.ownKeys(target)) { + if (!Object.prototype.hasOwnProperty.call(value, key)) { + Reflect.deleteProperty(target, key); + } + } + } else if (Array.isArray(value)) { + target = Object.setPrototypeOf([], Object.getPrototypeOf(value)); + } else { + target = Object.create(Object.getPrototypeOf(value)); + } sanitized.set(value, target); for (const key of Reflect.ownKeys(value)) { const descriptor = Object.getOwnPropertyDescriptor(value, key); if (!descriptor) continue; + if ( + initializedStackDescriptor && + !("value" in initializedStackDescriptor) && + typeof initializedStackDescriptor.get === "function" && + typeof initializedStackDescriptor.set === "function" && + isNativeErrorStackAccessor(value, key, descriptor) + ) { + const stack = nativeErrorStack(value, descriptor); + if (stack !== undefined) { + initializedStackDescriptor.set.call(target, redactString(stack)); + Object.defineProperty(target, key, { + configurable: descriptor.configurable, + enumerable: descriptor.enumerable, + get: initializedStackDescriptor.get, + set: initializedStackDescriptor.set, + }); + continue; + } + } if ("value" in descriptor) { descriptor.value = sanitizeValue(descriptor.value); } diff --git a/packages/tools/src/agents/runtime-provenance.spec.ts b/packages/tools/src/agents/runtime-provenance.spec.ts index 07148b144..919c3aaec 100644 --- a/packages/tools/src/agents/runtime-provenance.spec.ts +++ b/packages/tools/src/agents/runtime-provenance.spec.ts @@ -667,13 +667,15 @@ describe("agents runtime provenance v1", () => { } } const failure = new DiagnosticTransportError(diagnostics); - Object.defineProperty(failure, "stack", { - configurable: true, - enumerable: false, - value: - "DiagnosticTransportError: fetch failed with diagnostics\n at preserved-frame.ts:1:1", - writable: true, - }); + const originalStack = failure.stack; + const originalStackDescriptor = Object.getOwnPropertyDescriptor( + failure, + "stack", + ); + expect(originalStackDescriptor).toBeDefined(); + expect(originalStackDescriptor).not.toHaveProperty("value"); + expect(typeof originalStack).toBe("string"); + expect(originalStack).toContain("runtime-provenance.spec.ts"); const originalDescriptor = Object.getOwnPropertyDescriptor( failure, "diagnostics", @@ -699,7 +701,15 @@ describe("agents runtime provenance v1", () => { expect(error).toBeInstanceOf(DiagnosticTransportError); expect((error as DiagnosticTransportError).code).toBe("EAGENT"); expect((error as DiagnosticTransportError).message).toBe(failure.message); - expect((error as DiagnosticTransportError).stack).toBe(failure.stack); + expect((error as DiagnosticTransportError).stack).toBe(originalStack); + expect(Object.getOwnPropertyDescriptor(error, "stack")).toEqual( + expect.objectContaining({ + configurable: originalStackDescriptor?.configurable, + enumerable: originalStackDescriptor?.enumerable, + get: originalStackDescriptor?.get, + set: originalStackDescriptor?.set, + }), + ); expect( (error as DiagnosticTransportError).diagnostics.request.headers[ AGENT_RUNTIME_CALLSITE_HEADER @@ -745,6 +755,69 @@ describe("agents runtime provenance v1", () => { expect(failure.code).toBe("EAGENT"); }); + it("does not invoke custom stack or nested diagnostic accessors", async () => { + const callsite = "callsite.custom-accessor-private"; + let stackReads = 0; + let diagnosticReads = 0; + const diagnostics: Record = { + reflected: callsite, + publicValue: "diagnostic-public", + }; + Object.defineProperty(diagnostics, "lazy", { + configurable: true, + enumerable: false, + get() { + diagnosticReads += 1; + return callsite; + }, + }); + const failure = Object.assign(new TypeError("fetch failed"), { + code: "EAGENT", + diagnostics, + }); + const customStackGetter = () => { + stackReads += 1; + return `custom stack ${callsite}`; + }; + Object.defineProperty(failure, "stack", { + configurable: true, + enumerable: false, + get: customStackGetter, + }); + const fetch = (async () => { + throw failure; + }) as typeof globalThis.fetch; + const client = createClient({ apiKey: "k", fetch }); + + let error: unknown; + try { + await client.agents.launch( + carryAgentRuntimeProvenance( + { definition: "instrumented-custom-accessors" }, + { version: 1, callsite }, + ), + ); + } catch (value) { + error = value; + } + + expect(error).not.toBe(failure); + expect(error).toBeInstanceOf(TypeError); + expect(stackReads).toBe(0); + expect(diagnosticReads).toBe(0); + expect(Object.getOwnPropertyDescriptor(error, "stack")?.get).toBe( + customStackGetter, + ); + expect((error as typeof failure).diagnostics.reflected).toBe( + "[REDACTED runtime provenance]", + ); + expect((error as typeof failure).diagnostics.publicValue).toBe( + "diagnostic-public", + ); + expect(failure.diagnostics).toBe(diagnostics); + expect(failure.diagnostics.reflected).toBe(callsite); + }); + it("preserves exact identity when supplied provenance does not occur in nested diagnostics", async () => { const diagnostics = { request: { headers: { "x-request-id": "request-public" } }, From 0f03ec1f79c535f30660e049ab6600c83a486706 Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 1 Sep 2026 02:45:15 +0000 Subject: [PATCH 13/15] test(tools): support Node 20 stack descriptors Refs: SAP-3020 --- .../scripts/test-runtime-provenance-package.mjs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/tools/scripts/test-runtime-provenance-package.mjs b/packages/tools/scripts/test-runtime-provenance-package.mjs index 68bea0434..7b107f5fd 100644 --- a/packages/tools/scripts/test-runtime-provenance-package.mjs +++ b/packages/tools/scripts/test-runtime-provenance-package.mjs @@ -185,7 +185,7 @@ async function verifyNativeErrorStackRedaction(tools, carrier) { "stack", ); assert.ok(failureStackDescriptor); - assert.equal("value" in failureStackDescriptor, false); + const failureStackIsDataDescriptor = "value" in failureStackDescriptor; assert.equal(typeof failure.stack, "string"); assert.match(failure.stack, /nativeStackFixture/); assert.match(failure.stack, new RegExp(callsite)); @@ -228,9 +228,7 @@ async function verifyNativeErrorStackRedaction(tools, carrier) { "stack", ); assert.ok(caughtStackDescriptor); - assert.equal("value" in caughtStackDescriptor, false); - assert.equal(caughtStackDescriptor.get, failureStackDescriptor.get); - assert.equal(caughtStackDescriptor.set, failureStackDescriptor.set); + assert.equal("value" in caughtStackDescriptor, failureStackIsDataDescriptor); assert.equal( caughtStackDescriptor.configurable, failureStackDescriptor.configurable, @@ -239,6 +237,16 @@ async function verifyNativeErrorStackRedaction(tools, carrier) { caughtStackDescriptor.enumerable, failureStackDescriptor.enumerable, ); + if (failureStackIsDataDescriptor) { + assert.equal( + caughtStackDescriptor.writable, + failureStackDescriptor.writable, + ); + assert.equal(caughtStackDescriptor.value, caught.stack); + } else { + assert.equal(caughtStackDescriptor.get, failureStackDescriptor.get); + assert.equal(caughtStackDescriptor.set, failureStackDescriptor.set); + } assert.equal(caught.message.includes(callsite), false); assert.equal(caught.cause.message.includes(callsite), false); assert.equal(typeof caught.cause.stack, "string"); From 18e57a7c6b9988d50dc67f3600c73f2d7f496e00 Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 1 Sep 2026 02:53:25 +0000 Subject: [PATCH 14/15] test(tools): support Node 20 stack specs Refs: SAP-3020 --- .../src/agents/runtime-provenance.spec.ts | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/packages/tools/src/agents/runtime-provenance.spec.ts b/packages/tools/src/agents/runtime-provenance.spec.ts index 919c3aaec..d1f841688 100644 --- a/packages/tools/src/agents/runtime-provenance.spec.ts +++ b/packages/tools/src/agents/runtime-provenance.spec.ts @@ -673,7 +673,9 @@ describe("agents runtime provenance v1", () => { "stack", ); expect(originalStackDescriptor).toBeDefined(); - expect(originalStackDescriptor).not.toHaveProperty("value"); + const originalStackIsDataDescriptor = + originalStackDescriptor !== undefined && + "value" in originalStackDescriptor; expect(typeof originalStack).toBe("string"); expect(originalStack).toContain("runtime-provenance.spec.ts"); const originalDescriptor = Object.getOwnPropertyDescriptor( @@ -702,14 +704,29 @@ describe("agents runtime provenance v1", () => { expect((error as DiagnosticTransportError).code).toBe("EAGENT"); expect((error as DiagnosticTransportError).message).toBe(failure.message); expect((error as DiagnosticTransportError).stack).toBe(originalStack); - expect(Object.getOwnPropertyDescriptor(error, "stack")).toEqual( - expect.objectContaining({ - configurable: originalStackDescriptor?.configurable, - enumerable: originalStackDescriptor?.enumerable, - get: originalStackDescriptor?.get, - set: originalStackDescriptor?.set, - }), + const caughtStackDescriptor = Object.getOwnPropertyDescriptor( + error, + "stack", + ); + expect(caughtStackDescriptor).toBeDefined(); + expect( + caughtStackDescriptor !== undefined && "value" in caughtStackDescriptor, + ).toBe(originalStackIsDataDescriptor); + expect(caughtStackDescriptor?.configurable).toBe( + originalStackDescriptor?.configurable, + ); + expect(caughtStackDescriptor?.enumerable).toBe( + originalStackDescriptor?.enumerable, ); + if (originalStackIsDataDescriptor) { + expect(caughtStackDescriptor?.writable).toBe( + originalStackDescriptor?.writable, + ); + expect(caughtStackDescriptor?.value).toBe(originalStack); + } else { + expect(caughtStackDescriptor?.get).toBe(originalStackDescriptor?.get); + expect(caughtStackDescriptor?.set).toBe(originalStackDescriptor?.set); + } expect( (error as DiagnosticTransportError).diagnostics.request.headers[ AGENT_RUNTIME_CALLSITE_HEADER @@ -752,6 +769,7 @@ describe("agents runtime provenance v1", () => { expect( failure.diagnostics.request.headers[AGENT_RUNTIME_CALLSITE_HEADER], ).toBe(callsite); + expect(failure.stack).toBe(originalStack); expect(failure.code).toBe("EAGENT"); }); From bc89e2d8cc561bbb056c130af4f5cbb64ead2c08 Mon Sep 17 00:00:00 2001 From: Yash Date: Tue, 1 Sep 2026 03:25:55 +0000 Subject: [PATCH 15/15] fix(tools): redact opaque agent diagnostics Refs: SAP-3020 --- .../test-runtime-provenance-package.mjs | 185 ++++++++++- packages/tools/src/agents/index.ts | 301 +++++++++++++++--- .../src/agents/runtime-provenance.spec.ts | 278 +++++++++++++++- 3 files changed, 700 insertions(+), 64 deletions(-) diff --git a/packages/tools/scripts/test-runtime-provenance-package.mjs b/packages/tools/scripts/test-runtime-provenance-package.mjs index 7b107f5fd..826594507 100644 --- a/packages/tools/scripts/test-runtime-provenance-package.mjs +++ b/packages/tools/scripts/test-runtime-provenance-package.mjs @@ -302,10 +302,20 @@ async function verifyNativeErrorStackRedaction(tools, carrier) { assert.notEqual(customCaught, customFailure); assert.equal(customStackReads, 0); assert.equal(customDiagnosticReads, 0); - assert.equal( - Object.getOwnPropertyDescriptor(customCaught, "stack").get, - customStackGetter, + const customStackDescriptor = Object.getOwnPropertyDescriptor( + customCaught, + "stack", + ); + assert.equal(customStackDescriptor.value, "[REDACTED runtime provenance]"); + assert.equal("get" in customStackDescriptor, false); + assert.equal("set" in customStackDescriptor, false); + const customLazyDescriptor = Object.getOwnPropertyDescriptor( + customCaught.diagnostics, + "lazy", ); + assert.equal(customLazyDescriptor.value, "[REDACTED runtime provenance]"); + assert.equal("get" in customLazyDescriptor, false); + assert.equal("set" in customLazyDescriptor, false); assert.equal( customCaught.diagnostics.reflected, "[REDACTED runtime provenance]", @@ -345,6 +355,172 @@ async function verifyNativeErrorStackRedaction(tools, carrier) { await noPrivateClient.shutdown(); } +async function verifyContainerDiagnosticRedaction(tools, carrier) { + const callsite = "callsite.package-container-private"; + const secretSymbol = Symbol("secret diagnostic"); + const lazySymbol = Symbol("lazy diagnostic"); + let symbolAccessorReads = 0; + const symbolHeaders = new Headers({ "x-request-id": "request-public" }); + symbolHeaders[secretSymbol] = callsite; + Object.defineProperty(symbolHeaders, lazySymbol, { + configurable: true, + get() { + symbolAccessorReads += 1; + return callsite; + }, + }); + const symbolFailure = Object.assign( + new TypeError("symbol transport failed"), + { + request: { headers: symbolHeaders }, + }, + ); + const symbolClient = tools.createClient({ + apiKey: "k", + fetch: async () => { + throw symbolFailure; + }, + }); + const symbolCaught = await captureLaunchError( + symbolClient, + carrier.carryAgentRuntimeProvenance( + { definition: "package-symbol-diagnostics" }, + { version: 1, callsite }, + ), + ); + assert.notEqual(symbolCaught, symbolFailure); + assert.equal(symbolCaught.request.headers[secretSymbol], undefined); + assert.equal( + Object.getOwnPropertyDescriptor(symbolCaught.request.headers, lazySymbol), + undefined, + ); + assert.equal(symbolAccessorReads, 0); + assert.equal(symbolFailure.request.headers[secretSymbol], callsite); + assert.equal( + typeof Object.getOwnPropertyDescriptor( + symbolFailure.request.headers, + lazySymbol, + ).get, + "function", + ); + await symbolClient.shutdown(); + + let poisonedMethodReads = 0; + let customGetterReads = 0; + const poison = () => { + poisonedMethodReads += 1; + throw new Error("instance container method must not run"); + }; + const headers = new Headers({ + [CALLSITE_HEADER]: callsite, + "x-request-id": "request-public", + }); + Object.defineProperty(headers, "forEach", { + configurable: true, + value: poison, + }); + const shared = { privateValue: callsite, publicValue: "shared-public" }; + const map = new Map([["shared", shared]]); + const set = new Set([shared]); + map.set("self", map); + set.add(set); + for (const [container, methods] of [ + [map, ["entries", "forEach", "set", Symbol.iterator]], + [set, ["entries", "forEach", "add", Symbol.iterator]], + ]) { + for (const method of methods) { + Object.defineProperty(container, method, { + configurable: true, + value: poison, + }); + } + } + class OpaqueDiagnostic { + constructor() { + this.privateValue = callsite; + this.publicValue = "opaque-public"; + } + } + const opaque = new OpaqueDiagnostic(); + const diagnostics = { + request: { headers, requestId: "request-public" }, + map, + set, + opaque, + publicValue: "diagnostic-public", + }; + Object.defineProperty(diagnostics, "lazy", { + configurable: true, + get() { + customGetterReads += 1; + return callsite; + }, + }); + const failure = Object.assign(new TypeError("container transport failed"), { + code: "EAGENT", + diagnostics, + }); + const client = tools.createClient({ + apiKey: "k", + fetch: async () => { + throw failure; + }, + }); + const caught = await captureLaunchError( + client, + carrier.carryAgentRuntimeProvenance( + { definition: "package-container-diagnostics" }, + { version: 1, callsite }, + ), + ); + + assert.notEqual(caught, failure); + assert.ok(caught instanceof TypeError); + assert.equal(caught.code, "EAGENT"); + assert.equal(caught.diagnostics.request.requestId, "request-public"); + assert.equal( + Headers.prototype.get.call( + caught.diagnostics.request.headers, + CALLSITE_HEADER, + ), + "[REDACTED runtime provenance]", + ); + const caughtShared = Map.prototype.get.call(caught.diagnostics.map, "shared"); + assert.equal( + Map.prototype.get.call(caught.diagnostics.map, "self"), + caught.diagnostics.map, + ); + assert.equal( + Set.prototype.has.call(caught.diagnostics.set, caught.diagnostics.set), + true, + ); + assert.equal( + Set.prototype.has.call(caught.diagnostics.set, caughtShared), + true, + ); + assert.equal(caughtShared.privateValue, "[REDACTED runtime provenance]"); + assert.equal(caughtShared.publicValue, "shared-public"); + assert.equal(caught.diagnostics.opaque, "[REDACTED runtime provenance]"); + assert.equal(caught.diagnostics.publicValue, "diagnostic-public"); + const lazyDescriptor = Object.getOwnPropertyDescriptor( + caught.diagnostics, + "lazy", + ); + assert.equal(lazyDescriptor.value, "[REDACTED runtime provenance]"); + assert.equal("get" in lazyDescriptor, false); + assert.equal("set" in lazyDescriptor, false); + assert.equal(poisonedMethodReads, 0); + assert.equal(customGetterReads, 0); + assert.equal(Headers.prototype.get.call(headers, CALLSITE_HEADER), callsite); + assert.equal(Map.prototype.get.call(map, "shared"), shared); + assert.equal(Map.prototype.get.call(map, "self"), map); + assert.equal(Set.prototype.has.call(set, set), true); + assert.equal(shared.privateValue, callsite); + assert.equal(opaque.privateValue, callsite); + assert.equal(failure.diagnostics, diagnostics); + await client.shutdown(); +} + const cjsTools = require("@sapiom/tools"); const cjsCarrier = require(CARRIER_EXPORT); const cjsSandboxes = require("@sapiom/tools/sandboxes"); @@ -395,6 +571,7 @@ assert.throws( ); await verifySameFormat("cjs", cjsTools, cjsCarrier); await verifyNativeErrorStackRedaction(cjsTools, cjsCarrier); +await verifyContainerDiagnosticRedaction(cjsTools, cjsCarrier); const esmTools = await import("@sapiom/tools"); const esmCarrier = await import(CARRIER_EXPORT); @@ -623,5 +800,5 @@ for (const loaded of loadedAfterStub) { } console.log( - "runtime provenance package surfaces: native Error stacks, native roots, constructor identity, cache-private CJS + ESM, four cross-format paths, and both stub formats passed", + "runtime provenance package surfaces: fail-closed diagnostics, native Error stacks, native roots, constructor identity, cache-private CJS + ESM, four cross-format paths, and both stub formats passed", ); diff --git a/packages/tools/src/agents/index.ts b/packages/tools/src/agents/index.ts index 22b3bd013..62d777905 100644 --- a/packages/tools/src/agents/index.ts +++ b/packages/tools/src/agents/index.ts @@ -48,6 +48,39 @@ const AGENT_RUNTIME_CALLSITE_HEADER = "x-sapiom-runtime-callsite-evidence"; const AGENT_RUNTIME_LINEAGE_HEADER = "x-sapiom-runtime-lineage-receipt"; const MAX_OPAQUE_TOKEN_LENGTH = 8_192; const RUNTIME_PROVENANCE_REDACTION = "[REDACTED runtime provenance]"; +const NATIVE_ARRAY_IS_ARRAY = Array.isArray; +const NATIVE_ARRAY_PROTOTYPE = Array.prototype; +const NATIVE_DATE = Date; +const NATIVE_DATE_PROTOTYPE = Date.prototype; +const NATIVE_DATE_GET_TIME = Date.prototype.getTime; +const NATIVE_ERROR = Error; +const NATIVE_HEADERS = Headers; +const NATIVE_HEADERS_PROTOTYPE = Headers.prototype; +const NATIVE_HEADERS_APPEND = Headers.prototype.append; +const NATIVE_HEADERS_FOR_EACH = Headers.prototype.forEach; +const NATIVE_HEADERS_HAS = Headers.prototype.has; +const NATIVE_MAP = Map; +const NATIVE_MAP_PROTOTYPE = Map.prototype; +const NATIVE_MAP_FOR_EACH = Map.prototype.forEach; +const NATIVE_MAP_HAS = Map.prototype.has; +const NATIVE_MAP_SET = Map.prototype.set; +const NATIVE_SET = Set; +const NATIVE_SET_PROTOTYPE = Set.prototype; +const NATIVE_SET_ADD = Set.prototype.add; +const NATIVE_SET_FOR_EACH = Set.prototype.forEach; +const NATIVE_SET_HAS = Set.prototype.has; +const NATIVE_OBJECT_CREATE = Object.create; +const NATIVE_OBJECT_DEFINE_PROPERTY = Object.defineProperty; +const NATIVE_OBJECT_GET_OWN_PROPERTY_DESCRIPTOR = + Object.getOwnPropertyDescriptor; +const NATIVE_OBJECT_GET_PROTOTYPE_OF = Object.getPrototypeOf; +const NATIVE_OBJECT_PROTOTYPE = Object.prototype; +const NATIVE_OBJECT_SET_PROTOTYPE_OF = Object.setPrototypeOf; +const NATIVE_REFLECT_APPLY = Reflect.apply; +const NATIVE_REFLECT_DELETE_PROPERTY = Reflect.deleteProperty; +const NATIVE_REFLECT_OWN_KEYS = Reflect.ownKeys; +const DIAGNOSTIC_BRAND_SENTINEL = {}; +const DIAGNOSTIC_HEADERS_BRAND_SENTINEL = "x-sapiom-diagnostic-brand"; interface LineageRecord { readonly receipt: string; @@ -124,14 +157,8 @@ function redactedAgentRuntimeError( const redactString = (value: string): string => redactAgentRuntimeProvenance(value, values); - const isTraversableDiagnostic = (value: unknown): value is object => { - if (value === null || typeof value !== "object") return false; - if (value instanceof Error || Array.isArray(value)) return true; - const prototype = Object.getPrototypeOf(value); - return prototype === Object.prototype || prototype === null; - }; - const nativeErrorStackDescriptor = Object.getOwnPropertyDescriptor( - new Error(), + const nativeErrorStackDescriptor = NATIVE_OBJECT_GET_OWN_PROPERTY_DESCRIPTOR( + new NATIVE_ERROR(), "stack", ); const isNativeErrorStackAccessor = ( @@ -142,7 +169,7 @@ function redactedAgentRuntimeError( get: () => unknown; set: (value: unknown) => void; } => - source instanceof Error && + source instanceof NATIVE_ERROR && key === "stack" && !("value" in descriptor) && nativeErrorStackDescriptor !== undefined && @@ -160,7 +187,7 @@ function redactedAgentRuntimeError( if (cached !== undefined) return cached; let stack: unknown; try { - stack = descriptor.get.call(source); + stack = NATIVE_REFLECT_APPLY(descriptor.get, source, []); } catch { return undefined; } @@ -169,66 +196,237 @@ function redactedAgentRuntimeError( return stack; }; - if (!isTraversableDiagnostic(error)) { - const message = String(error); - const redacted = redactString(message); - return redacted === message ? error : new Error(redacted); - } + type DiagnosticKind = + | "error" + | "array" + | "plain" + | "map" + | "set" + | "headers" + | "date" + | "opaque"; + const hasNativeBrand = ( + intrinsic: (...args: never[]) => unknown, + value: object, + args: readonly unknown[], + ): boolean => { + try { + NATIVE_REFLECT_APPLY(intrinsic, value, args); + return true; + } catch { + return false; + } + }; + const diagnosticKind = (value: object): DiagnosticKind => { + if (value instanceof NATIVE_ERROR) return "error"; + const prototype = NATIVE_OBJECT_GET_PROTOTYPE_OF(value); + if (NATIVE_ARRAY_IS_ARRAY(value)) { + return prototype === NATIVE_ARRAY_PROTOTYPE ? "array" : "opaque"; + } + if (prototype === NATIVE_OBJECT_PROTOTYPE || prototype === null) { + return "plain"; + } + if ( + prototype === NATIVE_MAP_PROTOTYPE && + hasNativeBrand(NATIVE_MAP_HAS, value, [DIAGNOSTIC_BRAND_SENTINEL]) + ) { + return "map"; + } + if ( + prototype === NATIVE_SET_PROTOTYPE && + hasNativeBrand(NATIVE_SET_HAS, value, [DIAGNOSTIC_BRAND_SENTINEL]) + ) { + return "set"; + } + if ( + prototype === NATIVE_HEADERS_PROTOTYPE && + hasNativeBrand(NATIVE_HEADERS_HAS, value, [ + DIAGNOSTIC_HEADERS_BRAND_SENTINEL, + ]) + ) { + return "headers"; + } + if ( + prototype === NATIVE_DATE_PROTOTYPE && + hasNativeBrand(NATIVE_DATE_GET_TIME, value, []) + ) { + return "date"; + } + return "opaque"; + }; + type DescriptorSnapshot = readonly [PropertyKey, PropertyDescriptor]; const inspected = new WeakSet(); - const containsPrivateValue = (value: unknown): boolean => { + const kinds = new WeakMap(); + const descriptors = new WeakMap(); + const mapEntries = new WeakMap(); + const setEntries = new WeakMap(); + const headerEntries = new WeakMap(); + const dateValues = new WeakMap(); + + const descriptorSnapshots = (value: object): DescriptorSnapshot[] => { + const snapshots: DescriptorSnapshot[] = []; + for (const key of NATIVE_REFLECT_OWN_KEYS(value)) { + const descriptor = NATIVE_OBJECT_GET_OWN_PROPERTY_DESCRIPTOR(value, key); + if (descriptor) snapshots.push([key, descriptor]); + } + descriptors.set(value, snapshots); + return snapshots; + }; + + const requiresSanitization = (value: unknown): boolean => { if (typeof value === "string") return redactString(value) !== value; - if (!isTraversableDiagnostic(value) || inspected.has(value)) return false; + if (typeof value === "function" || typeof value === "symbol") return true; + if (value === null || typeof value !== "object") return false; + const kind = diagnosticKind(value); + kinds.set(value, kind); + if (kind === "opaque") return true; + if (inspected.has(value)) return false; inspected.add(value); - for (const key of Reflect.ownKeys(value)) { - const descriptor = Object.getOwnPropertyDescriptor(value, key); + + let required = false; + if (kind === "map") { + const entries: [unknown, unknown][] = []; + NATIVE_REFLECT_APPLY(NATIVE_MAP_FOR_EACH, value, [ + (entryValue: unknown, entryKey: unknown) => { + entries.push([entryKey, entryValue]); + }, + ]); + mapEntries.set(value, entries); + for (const [entryKey, entryValue] of entries) { + if (requiresSanitization(entryKey)) required = true; + if (requiresSanitization(entryValue)) required = true; + } + } else if (kind === "set") { + const entries: unknown[] = []; + NATIVE_REFLECT_APPLY(NATIVE_SET_FOR_EACH, value, [ + (entryValue: unknown) => { + entries.push(entryValue); + }, + ]); + setEntries.set(value, entries); + for (const entryValue of entries) { + if (requiresSanitization(entryValue)) required = true; + } + } else if (kind === "headers") { + const entries: [string, string][] = []; + NATIVE_REFLECT_APPLY(NATIVE_HEADERS_FOR_EACH, value, [ + (entryValue: string, entryKey: string) => { + entries.push([entryKey, entryValue]); + }, + ]); + headerEntries.set(value, entries); + for (const [entryKey, entryValue] of entries) { + if (redactString(entryKey) !== entryKey) required = true; + if (redactString(entryValue) !== entryValue) required = true; + } + } else if (kind === "date") { + dateValues.set( + value, + NATIVE_REFLECT_APPLY(NATIVE_DATE_GET_TIME, value, []), + ); + } + + for (const [key, descriptor] of descriptorSnapshots(value)) { + if (typeof key !== "string" || redactString(key) !== key) { + required = true; + continue; + } if (descriptor && isNativeErrorStackAccessor(value, key, descriptor)) { const stack = nativeErrorStack(value, descriptor); - if (stack !== undefined && redactString(stack) !== stack) return true; + if (stack === undefined || redactString(stack) !== stack) { + required = true; + } + continue; } - if ( - descriptor && - "value" in descriptor && - containsPrivateValue(descriptor.value) - ) { - return true; + if (!("value" in descriptor)) { + required = true; + continue; } + if (requiresSanitization(descriptor.value)) required = true; } - return false; + return required; }; - if (!containsPrivateValue(error)) return error; + if (!requiresSanitization(error)) return error; const sanitized = new WeakMap(); const sanitizeValue = (value: unknown): unknown => { if (typeof value === "string") return redactString(value); - if (!isTraversableDiagnostic(value)) return value; + if (typeof value === "function" || typeof value === "symbol") { + return RUNTIME_PROVENANCE_REDACTION; + } + if (value === null || typeof value !== "object") return value; + const kind = kinds.get(value) ?? diagnosticKind(value); + if (kind === "opaque") return RUNTIME_PROVENANCE_REDACTION; const cached = sanitized.get(value); if (cached) return cached; let initializedStackDescriptor: PropertyDescriptor | undefined; let target: object; - if (value instanceof Error) { - target = new Error(); - initializedStackDescriptor = Object.getOwnPropertyDescriptor( + if (kind === "error") { + target = new NATIVE_ERROR(); + initializedStackDescriptor = NATIVE_OBJECT_GET_OWN_PROPERTY_DESCRIPTOR( target, "stack", ); - Object.setPrototypeOf(target, Object.getPrototypeOf(value)); - for (const key of Reflect.ownKeys(target)) { - if (!Object.prototype.hasOwnProperty.call(value, key)) { - Reflect.deleteProperty(target, key); + NATIVE_OBJECT_SET_PROTOTYPE_OF( + target, + NATIVE_OBJECT_GET_PROTOTYPE_OF(value), + ); + const sourceDescriptors = descriptors.get(value) ?? []; + for (const targetKey of NATIVE_REFLECT_OWN_KEYS(target)) { + let sourceHasKey = false; + for (const [sourceKey] of sourceDescriptors) { + if (sourceKey === targetKey) sourceHasKey = true; + } + if (!sourceHasKey) { + NATIVE_REFLECT_DELETE_PROPERTY(target, targetKey); } } - } else if (Array.isArray(value)) { - target = Object.setPrototypeOf([], Object.getPrototypeOf(value)); + } else if (kind === "array") { + target = NATIVE_OBJECT_SET_PROTOTYPE_OF( + [], + NATIVE_OBJECT_GET_PROTOTYPE_OF(value), + ); + } else if (kind === "map") { + target = new NATIVE_MAP(); + } else if (kind === "set") { + target = new NATIVE_SET(); + } else if (kind === "headers") { + target = new NATIVE_HEADERS(); + } else if (kind === "date") { + target = new NATIVE_DATE(dateValues.get(value)!); } else { - target = Object.create(Object.getPrototypeOf(value)); + target = NATIVE_OBJECT_CREATE(NATIVE_OBJECT_GET_PROTOTYPE_OF(value)); } sanitized.set(value, target); - for (const key of Reflect.ownKeys(value)) { - const descriptor = Object.getOwnPropertyDescriptor(value, key); - if (!descriptor) continue; + + if (kind === "map") { + for (const [entryKey, entryValue] of mapEntries.get(value) ?? []) { + NATIVE_REFLECT_APPLY(NATIVE_MAP_SET, target, [ + sanitizeValue(entryKey), + sanitizeValue(entryValue), + ]); + } + } else if (kind === "set") { + for (const entryValue of setEntries.get(value) ?? []) { + NATIVE_REFLECT_APPLY(NATIVE_SET_ADD, target, [ + sanitizeValue(entryValue), + ]); + } + } else if (kind === "headers") { + for (const [entryKey, entryValue] of headerEntries.get(value) ?? []) { + if (redactString(entryKey) !== entryKey) continue; + NATIVE_REFLECT_APPLY(NATIVE_HEADERS_APPEND, target, [ + entryKey, + redactString(entryValue), + ]); + } + } + + for (const [key, descriptor] of descriptors.get(value) ?? []) { + if (typeof key !== "string" || redactString(key) !== key) continue; if ( initializedStackDescriptor && !("value" in initializedStackDescriptor) && @@ -238,8 +436,10 @@ function redactedAgentRuntimeError( ) { const stack = nativeErrorStack(value, descriptor); if (stack !== undefined) { - initializedStackDescriptor.set.call(target, redactString(stack)); - Object.defineProperty(target, key, { + NATIVE_REFLECT_APPLY(initializedStackDescriptor.set, target, [ + redactString(stack), + ]); + NATIVE_OBJECT_DEFINE_PROPERTY(target, key, { configurable: descriptor.configurable, enumerable: descriptor.enumerable, get: initializedStackDescriptor.get, @@ -248,10 +448,17 @@ function redactedAgentRuntimeError( continue; } } - if ("value" in descriptor) { - descriptor.value = sanitizeValue(descriptor.value); + if (!("value" in descriptor)) { + NATIVE_OBJECT_DEFINE_PROPERTY(target, key, { + configurable: descriptor.configurable, + enumerable: descriptor.enumerable, + value: RUNTIME_PROVENANCE_REDACTION, + writable: false, + }); + continue; } - Object.defineProperty(target, key, descriptor); + descriptor.value = sanitizeValue(descriptor.value); + NATIVE_OBJECT_DEFINE_PROPERTY(target, key, descriptor); } return target; }; diff --git a/packages/tools/src/agents/runtime-provenance.spec.ts b/packages/tools/src/agents/runtime-provenance.spec.ts index d1f841688..74fa637ad 100644 --- a/packages/tools/src/agents/runtime-provenance.spec.ts +++ b/packages/tools/src/agents/runtime-provenance.spec.ts @@ -630,6 +630,241 @@ describe("agents runtime provenance v1", () => { ).toBe("ECONNREFUSED"); }); + it("redacts launch headers captured by a custom transport without invoking instance methods", async () => { + const callsite = "callsite.headers-private"; + let instanceForEachReads = 0; + let failure: + | (TypeError & { + code: string; + request: { headers: Headers; requestId: string }; + }) + | undefined; + const fetch = (async ( + _input: string | URL | Request, + init: RequestInit = {}, + ) => { + const headers = new Headers(init.headers); + Object.defineProperty(headers, "forEach", { + configurable: true, + value() { + instanceForEachReads += 1; + throw new Error("instance forEach must not run"); + }, + }); + failure = Object.assign(new TypeError("fetch failed"), { + code: "EAGENT", + request: { headers, requestId: "request-public" }, + }); + throw failure; + }) as typeof globalThis.fetch; + const client = createClient({ apiKey: "k", fetch }); + + let error: unknown; + try { + await client.agents.launch( + carryAgentRuntimeProvenance( + { definition: "instrumented-headers" }, + { version: 1, callsite }, + ), + ); + } catch (value) { + error = value; + } + + const surfaced = error as typeof failure & { + request: { headers: Headers; requestId: string }; + }; + expect(failure).toBeDefined(); + expect(error).not.toBe(failure); + expect(error).toBeInstanceOf(TypeError); + expect(surfaced.code).toBe("EAGENT"); + expect(surfaced.request.requestId).toBe("request-public"); + expect(surfaced.request.headers).toBeInstanceOf(Headers); + expect( + Headers.prototype.get.call( + surfaced.request.headers, + AGENT_RUNTIME_CALLSITE_HEADER, + ), + ).toBe("[REDACTED runtime provenance]"); + expect(instanceForEachReads).toBe(0); + expect( + Headers.prototype.get.call( + failure!.request.headers, + AGENT_RUNTIME_CALLSITE_HEADER, + ), + ).toBe(callsite); + expect(failure!.request.requestId).toBe("request-public"); + }); + + it("fails closed for custom symbol surfaces on captured Headers without invoking accessors", async () => { + const callsite = "callsite.headers-symbol-private"; + const secretSymbol = Symbol("secret diagnostic"); + const lazySymbol = Symbol("lazy diagnostic"); + let symbolAccessorReads = 0; + let failure: + | (TypeError & { + request: { headers: Headers & Record }; + }) + | undefined; + const fetch = (async ( + _input: string | URL | Request, + init: RequestInit = {}, + ) => { + const headers = new Headers(init.headers) as Headers & + Record; + Headers.prototype.delete.call(headers, AGENT_RUNTIME_CALLSITE_HEADER); + headers[secretSymbol] = callsite; + Object.defineProperty(headers, lazySymbol, { + configurable: true, + get() { + symbolAccessorReads += 1; + return callsite; + }, + }); + failure = Object.assign(new TypeError("fetch failed"), { + request: { headers }, + }); + throw failure; + }) as typeof globalThis.fetch; + const client = createClient({ apiKey: "k", fetch }); + + let error: unknown; + try { + await client.agents.launch( + carryAgentRuntimeProvenance( + { definition: "instrumented-headers-symbol" }, + { version: 1, callsite }, + ), + ); + } catch (value) { + error = value; + } + + const surfaced = error as typeof failure & { + request: { headers: Headers & Record }; + }; + expect(failure).toBeDefined(); + expect(error).not.toBe(failure); + expect(error).toBeInstanceOf(TypeError); + expect(surfaced.request.headers[secretSymbol]).toBeUndefined(); + expect( + Object.getOwnPropertyDescriptor(surfaced.request.headers, lazySymbol), + ).toBeUndefined(); + expect(symbolAccessorReads).toBe(0); + expect(failure!.request.headers[secretSymbol]).toBe(callsite); + expect( + Object.getOwnPropertyDescriptor(failure!.request.headers, lazySymbol) + ?.get, + ).toBeDefined(); + }); + + it("redacts Map and Set entries while preserving cycles and shared references", async () => { + const callsite = "callsite.containers-private"; + const shared = { privateValue: callsite, publicValue: "shared-public" }; + const map = new Map(); + const set = new Set(); + map.set("shared", shared); + map.set("self", map); + set.add(shared); + set.add(set); + let instanceMethodReads = 0; + const poisonedMethod = () => { + instanceMethodReads += 1; + throw new Error("instance container method must not run"); + }; + for (const [container, methods] of [ + [map, ["entries", "forEach", "set", Symbol.iterator]], + [set, ["entries", "forEach", "add", Symbol.iterator]], + ] as const) { + for (const method of methods) { + Object.defineProperty(container, method, { + configurable: true, + value: poisonedMethod, + }); + } + } + const diagnostics = { map, set }; + const failure = Object.assign(new TypeError("fetch failed"), { + code: "EAGENT", + diagnostics, + }); + const fetch = (async () => { + throw failure; + }) as typeof globalThis.fetch; + const client = createClient({ apiKey: "k", fetch }); + + let error: unknown; + try { + await client.agents.launch( + carryAgentRuntimeProvenance( + { definition: "instrumented-containers" }, + { version: 1, callsite }, + ), + ); + } catch (value) { + error = value; + } + + const surfaced = error as typeof failure; + const surfacedMap = surfaced.diagnostics.map; + const surfacedSet = surfaced.diagnostics.set; + const surfacedShared = surfacedMap.get("shared") as typeof shared; + expect(error).not.toBe(failure); + expect(surfacedMap).toBeInstanceOf(Map); + expect(surfacedSet).toBeInstanceOf(Set); + expect(surfacedMap.get("self")).toBe(surfacedMap); + expect(surfacedSet.has(surfacedSet)).toBe(true); + expect([...surfacedSet][0]).toBe(surfacedShared); + expect(surfacedShared.privateValue).toBe("[REDACTED runtime provenance]"); + expect(surfacedShared.publicValue).toBe("shared-public"); + expect(instanceMethodReads).toBe(0); + expect(map.get("shared")).toBe(shared); + expect(map.get("self")).toBe(map); + expect(set.has(set)).toBe(true); + expect(shared.privateValue).toBe(callsite); + }); + + it("fails closed for custom diagnostic instances without creating invalid shells", async () => { + const callsite = "callsite.custom-instance-private"; + class CustomDiagnostic { + readonly privateValue = callsite; + readonly publicValue = "custom-public"; + } + const custom = new CustomDiagnostic(); + const diagnostics = { custom, publicSibling: "sibling-public" }; + const failure = Object.assign(new TypeError("fetch failed"), { + code: "EAGENT", + diagnostics, + }); + const fetch = (async () => { + throw failure; + }) as typeof globalThis.fetch; + const client = createClient({ apiKey: "k", fetch }); + + let error: unknown; + try { + await client.agents.launch( + carryAgentRuntimeProvenance( + { definition: "instrumented-custom-instance" }, + { version: 1, callsite }, + ), + ); + } catch (value) { + error = value; + } + + const surfaced = error as typeof failure; + expect(error).not.toBe(failure); + expect(error).toBeInstanceOf(TypeError); + expect(surfaced.code).toBe("EAGENT"); + expect(surfaced.diagnostics.custom).toBe("[REDACTED runtime provenance]"); + expect(surfaced.diagnostics.custom).not.toBeInstanceOf(CustomDiagnostic); + expect(surfaced.diagnostics.publicSibling).toBe("sibling-public"); + expect(failure.diagnostics).toBe(diagnostics); + expect(custom.privateValue).toBe(callsite); + expect(custom.publicValue).toBe("custom-public"); + }); + it("redacts nested ordinary diagnostics without mutating the original error graph", async () => { const callsite = "callsite.nested-private"; interface NestedDiagnostics { @@ -741,22 +976,26 @@ describe("agents runtime provenance v1", () => { status: 502, retryable: true, }); - expect((error as DiagnosticTransportError).diagnostics.observedAt).toBe( + expect((error as DiagnosticTransportError).diagnostics.observedAt).not.toBe( observedAt, ); + expect( + (error as DiagnosticTransportError).diagnostics.observedAt, + ).toBeInstanceOf(Date); expect(observedAt.toISOString()).toBe("2026-09-01T00:00:00.000Z"); - expect(accessorReads).toBe(0); expect( - Object.getOwnPropertyDescriptor( - (error as DiagnosticTransportError).diagnostics.request, - "lazyDiagnostic", - )?.get, - ).toBe( - Object.getOwnPropertyDescriptor( - failure.diagnostics.request, - "lazyDiagnostic", - )?.get, + (error as DiagnosticTransportError).diagnostics.observedAt.toISOString(), + ).toBe("2026-09-01T00:00:00.000Z"); + expect(accessorReads).toBe(0); + const lazyDiagnosticDescriptor = Object.getOwnPropertyDescriptor( + (error as DiagnosticTransportError).diagnostics.request, + "lazyDiagnostic", ); + expect(lazyDiagnosticDescriptor?.value).toBe( + "[REDACTED runtime provenance]", + ); + expect("get" in lazyDiagnosticDescriptor!).toBe(false); + expect("set" in lazyDiagnosticDescriptor!).toBe(false); expect(Object.getOwnPropertyDescriptor(error, "diagnostics")).toEqual( expect.objectContaining({ configurable: originalDescriptor?.configurable, @@ -823,9 +1062,22 @@ describe("agents runtime provenance v1", () => { expect(error).toBeInstanceOf(TypeError); expect(stackReads).toBe(0); expect(diagnosticReads).toBe(0); - expect(Object.getOwnPropertyDescriptor(error, "stack")?.get).toBe( - customStackGetter, + const surfacedStackDescriptor = Object.getOwnPropertyDescriptor( + error, + "stack", + ); + expect(surfacedStackDescriptor?.value).toBe( + "[REDACTED runtime provenance]", + ); + expect("get" in surfacedStackDescriptor!).toBe(false); + expect("set" in surfacedStackDescriptor!).toBe(false); + const surfacedLazyDescriptor = Object.getOwnPropertyDescriptor( + (error as typeof failure).diagnostics, + "lazy", ); + expect(surfacedLazyDescriptor?.value).toBe("[REDACTED runtime provenance]"); + expect("get" in surfacedLazyDescriptor!).toBe(false); + expect("set" in surfacedLazyDescriptor!).toBe(false); expect((error as typeof failure).diagnostics.reflected).toBe( "[REDACTED runtime provenance]", );