Skip to content

feat(tools): carry trusted agent provenance sidecars [SAP-3020] - #759

Closed
ynadge wants to merge 15 commits into
yashnadge/sap-2986-infrastructure-adapt-direct-invocations-to-graph-evidencefrom
yashnadge/sap-3020-carry-trusted-callsite-and-lineage-sidecars-through-agent
Closed

feat(tools): carry trusted agent provenance sidecars [SAP-3020]#759
ynadge wants to merge 15 commits into
yashnadge/sap-2986-infrastructure-adapt-direct-invocations-to-graph-evidencefrom
yashnadge/sap-3020-carry-trusted-callsite-and-lineage-sidecars-through-agent

Conversation

@ynadge

@ynadge ynadge commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Add the private, versioned runtime-provenance bridge for @sapiom/tools agent invocations. Opaque build callsite evidence and server-signed lineage receipts stay outside AgentRunSpec, request/result JSON, public result properties, analytics, logs, and surfaced errors. Supported v1 lineage is forwarded only when a trusted build-carried callsite and an exact SDK result reference meet at the next observed agent boundary.

This is stacked on SAP-2986 / PR #755 at ee72cd56ff34328c2ebb1701e81dcc7f80aea9ee. It intentionally excludes server minting/verification, persistence, and Phase-C projection. The current bounded-repair head is bc89e2d8cc561bbb056c130af4f5cbb64ead2c08; exact-head Node 20/22/examples CI is green; the bounded delta review is complete, with its sole orange claim rejected by exact Node 18/20/24 reproduction and the approved fail-closed contract.

Changes

  • expose the minimal v1 build carrier at the unsupported implementation subpath @sapiom/tools/_internal/agent-runtime-provenance, which may change in any release
  • retain callsite/receipt state only in a shared lexical agents/provenance core, with no public extraction or rebinding helpers and no standalone store artifact
  • leave native CJS and ESM package roots and ordinary public subpaths untouched, preserving constructor identity and tree-shake-compatible ESM
  • route format-specific agents facades through the shared core while injecting each format's native defaultTransport(); the core bundles an inert transport shim and never eagerly loads the other format's real client/analytics graph
  • capture spec.input once, snapshot validated callsite scalars, consume callsite/lineage once, and expire supported handoffs after the current turn
  • reject callsites unless they are non-empty, at most 8192 characters, trim-stable, and printable ASCII; unsupported NUL/control and Unicode evidence is omitted without changing the ordinary invocation
  • return metadata-absent and provably inspectable non-reflecting errors untouched; when sanitization is required, traverse ordinary objects/arrays and same-realm Headers/Map/Set/Date through captured prototype intrinsics, preserving typed prototypes, causes, stacks, descriptors, cycles/shared references, and original graph immutability while replacing opaque/custom/accessor/symbol surfaces with inert redaction markers
  • preserve native Error stacks across Node 20 data descriptors and Node 22/24 accessors; accessor-backed clones use initialized real Errors, exact same-realm native getter/setter recognition, redacted native stack seeding, and no custom accessor evaluation
  • remove internal provenance/threat-model details from the npm-packed agents README
  • retain the all-base pull-request Test workflow trigger and beta changeset

Detectable boundary

The SDK does not recursively infer data flow through arbitrary containers or persisted objects. A timer turn, changed identity, or an uninstrumented agent invocation prevents later forwarding; exact-reference array and Map replays are covered after an uninstrumented boundary. A synchronous same-turn exact-reference array/Map round trip is indistinguishable from a direct handoff.

Testing

  • exact-head focused provenance suite: 1 suite, 36 tests passed on exact Node 20.20.2 (native data stack descriptor) and local Node 24.15.0 (native accessor stack descriptor)
  • full @sapiom/tools Jest suite: 35 suites, 645 tests passed
  • exact agent-core regressions: 2 suites, 32 tests passed
  • clean CJS/ESM tools build and runtime-provenance artifact probe on exact Node 20.20.2 and local Node 24.15.0
  • exact captured-Headers, symbol-only Headers, Map/Set, opaque custom-instance, custom-accessor, cycle/shared-reference, and original-immutability probes; poisoned instance methods/accessors remain uninvoked
  • isolated ESM-root probe uses native ESM default transport and loads no dist/cjs/_client/index.js
  • isolated CJS-root probe uses native CJS default transport and loads no ESM graph
  • root/subpath identity for Sandbox, Repository, and public HTTP error constructors; one CJS default-transport singleton
  • same-format and all four mixed-format callsite/result directions, full-result/output sidecars, CJS/ESM stubs, cache-attack reproduction, no default export, and native ESM-root shape
  • package dry-run: 354 files; required native roots/core/facades present and zero standalone callsite-store artifacts
  • @sapiom/tools typecheck
  • zero-warning @sapiom/tools lint
  • changed-file Prettier check
  • git diff --check and stacked-base diff check
  • fresh exact-head Node 20, Node 22, and examples CI passed
  • bounded delta review completed; the concrete DOMException claim does not reproduce on supported Node and returning a genuinely opaque root would violate the fail-closed confidentiality contract

Related

  • Refs: SAP-3020

Checklist

  • No graph identities added to the public API
  • Request and result JSON remain compatible when metadata is absent
  • No provenance in public payload properties, analytics, logs, or surfaced errors
  • No process-global mutable store, public global symbol, or callable private store helper
  • Native package roots and format-native client/analytics graphs are preserved
  • Self-reviewed against the ticket contract and active automated-review findings
  • Bounded delta review completed and its sole orange claim adjudicated with exact-runtime evidence

ynadge added 8 commits August 31, 2026 22:44
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
Exercise direct output identity, delayed-dispatch exclusion, and error privacy for the v1 carrier.\n\nRefs: SAP-3020
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
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
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
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
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
@ynadge ynadge changed the title feat(tools): carry trusted agent provenance sidecars feat(tools): carry trusted agent provenance sidecars [SAP-3020] Sep 1, 2026
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
@ynadge
ynadge marked this pull request as ready for review September 1, 2026 01:34
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Review — PR #759 (round 1)

1. 🔴 Build canonicalization ships a second copy of the whole package and splits class identity

scripts/canonicalize-runtime-provenance-exports.mjs bundles scripts/agent-runtime-provenance-entry.tsexport * from "../src/index.js" with esbuild (packages: "external" externalizes only bare specifiers, so every relative import is inlined) and then overwrites dist/{cjs,esm}/index.js, dist/{cjs,esm}/agents/index.js and the carrier with facades onto that bundle. The other 15 export subpaths keep their original tsc output. That leaves two live copies of the module graph in the tarball, with three consequences:

  • instanceof breaks across root vs. subpath. require("@sapiom/tools").Sandbox !== require("@sapiom/tools/sandboxes").Sandbox. Same for Repository, MemoryHttpError, FileStorageHttpError, DatabaseHttpError, SearchHttpError, VaultHttpError, … — all exported from both surfaces. Failure: a consumer does import { MemoryHttpError } from "@sapiom/tools/memory" and catch (e) { if (e instanceof MemoryHttpError) } around a call made through the root client → the branch silently never fires, error escapes as unhandled. Same shape for createStubClient() (stub is not facaded, so it returns original-graph Sandbox/Repository instances) checked against the root Sandbox.
  • Module-level singletons duplicate. _client/index.ts's _default transport and its analytics holder now exist once in the bundle and once in dist/cjs/_client/index.js → two ambient transports, two analytics emitters, two beforeExit hooks; client.shutdown() flushes only one.
  • The ESM entry is now a CJS re-export. dist/esm/index.js becomes import canonical from "../cjs/index.js". Tree-shaking is gone ("sideEffects": false and the module field are now misleading — every ESM consumer pulls the entire bundled package), and runtimes that cannot require CJS (Workers, Deno, strict ESM bundlers) lose an entry point that worked before. Separately, the primary shipped runtime is now esbuild output (target: "node18", no type-check) while types still points at tsc's declarations — two compilers for one artifact.

The changeset is minor with no breaking note. At minimum this needs to be Breaking with a migration note; better, keep the shared store in one module (e.g. have agents/index.ts and the carrier both import the store from a single file and stop bundling the root) rather than duplicating the graph.

2. 🟠 All agents errors are rewrapped into a bare Error, dropping cause, subclass, and stack

packages/tools/src/agents/index.ts — the POST in launch() and every fetchDoc() failure path go through redactedAgentRuntimeError, which does new Error(message) and copies only .name. This runs unconditionally, including when privateProvenanceValues is empty (i.e. for every uninstrumented caller).

Failure: agents.run() against an unreachable host. Undici throws TypeError: fetch failed with cause: Error: connect ECONNREFUSED 127.0.0.1:443. The consumer now receives Error: fetch failed with no cause, no code, and a stack pointing at the redaction helper — the only diagnostic content is destroyed. Same for any typed error a future transport throws.

Fix: return the original error untouched when there is nothing to redact, and when there is, preserve cause and the prototype (Object.setPrototypeOf / re-throw a same-class error) instead of flattening to Error.

3. 🟠 The internal provenance section in src/agents/README.md is published to npm

files includes src/**/README.md, so this ships in the tarball. The new section documents an @internal, unsupported subpath as if it were consumer API, and then spells out the control's evasion boundary: "cannot distinguish a synchronous exact-reference round trip through an in-memory array or Map from a direct handoff", plus require.cache, "extraction/rebinding helpers", and "callable private helpers" — internal threat-model text addressed to an anonymous npm reader. For a lineage/provenance control whose server-side verification is explicitly out of scope in this PR, publishing the bypass recipe goes beyond what the feature inherently reveals; it belongs in docs/, not the packed README. The section is also inserted mid-bulleted-list, between the "Failure is data" and "Addressed by slug" bullets.

4. 🟡 The _internal/agent-runtime-provenance subpath is a real public API commitment

Adding it to exports makes carryAgentRuntimeProvenance importable and typed by anyone, regardless of the _internal path segment and the @internal JSDoc. The changeset's "Private receipt state is not package-exported" reads as though nothing new is exported. If this must ship (build tooling needs it), say plainly in the changeset that the subpath is unsupported and may change in any release; otherwise consumers will pin to it and the next shape change becomes a breaking change you didn't intend to make.


Verdict: Request changes — finding 1 is a silent cross-surface behavior break shipped under a minor changeset, and finding 2 degrades error reporting for every agents caller including uninstrumented ones. Confidentiality: changeset and PR prose are clean (no company names, business arrangements, or internal hosts); the only prose issue is finding 3.

@ynadge
ynadge marked this pull request as draft September 1, 2026 01:41
@ynadge
ynadge marked this pull request as ready for review September 1, 2026 02:59
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Review — PR #759 (round 2, delta since e097ce5d)

Fixed: round‑1 #2 (errors now pass through untouched when there is nothing to redact; prototype, cause, code and stack preserved), #3 (the internal provenance/threat‑model section is gone from src/agents/README.md), #4 (changeset now states the _internal subpath is unsupported and may change in any release). Round‑1 #1 is largely fixed — the entry is now export * as agents + carrier only, the tsc copies of exactly those modules are overwritten and the store artifact deleted, so there is one copy of each and the root entries are untouched. Nothing in round 1 was wrong. Delta prose is clean.

New findings

1. 🟠 The new traversal rethrows the original error — callsite intact — when provenance sits in a non-plain container

packages/tools/src/agents/index.tsisTraversableDiagnostic admits only Error, arrays, and Object.prototype/null‑prototype objects, and non‑native accessors are never invoked. If the reflected callsite lives only inside a Map, Set, Headers, or any class instance (or behind a custom getter), containsPrivateValue returns false and redactedAgentRuntimeError returns error by reference.

Failure: a consumer‑supplied fetch (createClient({ fetch })) throws err.request = { headers: new Headers(init.headers) }. launch() put x-sapiom-runtime-callsite-evidence: <callsite> in those headers at line 452; Headers is not traversable, so the caller reads the callsite straight off the surfaced error. Round 1's new Error(message) flatten could not leak nested values — the fix widened the leak surface. The changeset still promises, unqualified, "reflected values are redacted from errors", and the PR checklist claims "No provenance in … surfaced errors". Either traverse opaque containers or qualify both claims.

2. 🟡 The generated facades hardcode launch/run; a rename or a 4th parameter ships a broken package on a green build

scripts/canonicalize-runtime-provenance-exports.mjs:135-143,158-166 filters the two names by string and re-emits a fixed (spec, transport, baseUrl) shape. Nothing asserts that every bundle export whose default transport is the throwing shim gets wrapped. Rename launch → the facade exports the bundle's version unwrapped and every call throws agent transport must be supplied by the format-native facade; add a 4th param → the facade silently drops it while dist/cjs/agents/index.d.ts still advertises it. Add an assertion tying the wrapped set to the shim's consumers.

3. 🟡 Round‑1 #1 residual: the ESM agents entry (and the ESM root) now depends on a CJS file

dist/esm/agents/index.js statically imports ../../cjs/agents/runtime-provenance.cjs, and src/index.ts:65 (export * as agents) pulls it into the ESM root. Node and bundlers handle this; strict‑ESM‑only consumers lose an entry point that worked. The changeset is still minor with no note.


Verdict: Approve with changes — finding 1 defeats the feature's own confidentiality guarantee for one realistic transport shape; 2 and 3 are build/packaging debt, not blockers.

@ynadge
ynadge marked this pull request as draft September 1, 2026 03:07
@ynadge
ynadge marked this pull request as ready for review September 1, 2026 03:35

ynadge commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

/review

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Review — PR #759 (round 3, delta since 18e57a7c)

One commit (bc89e2d8), three files, no prose/changeset surfaces touched — confidentiality delta is clean.

Fixed: round‑2 #1. Headers, Map, Set and Date are now traversed through captured intrinsics and redacted; opaque instances, functions, symbol keys and non-native accessors fail closed to the redaction marker without being invoked. The err.request.headers leak no longer reproduces, and the package probe covers it. Nothing in round 2 was wrong.

New finding

1. 🟠 A top-level non-Error throwable is replaced by a bare string, provenance or not

packages/tools/src/agents/index.ts:466throw sanitizeValue(error). When error itself is opaque (prototype isn't Error/Array/Object.prototype/Map/Set/Headers/Date), requiresSanitization returns true without inspecting it, and sanitizeValue returns RUNTIME_PROVENANCE_REDACTION. The consumer gets throw "[REDACTED runtime provenance]".

Failure: an instrumented agents.run() through a consumer fetch with its own AbortSignal. Node rejects with a DOMException("Aborted","AbortError"), which is not instanceof Error. The caller's catch (e) { if (e.name === "AbortError") retry() } sees a string — e.name is undefined, the branch never fires, and e.message/e.stack are gone. The DOMException contained no callsite; nothing was actually redacted. Same for any class instance thrown by a custom transport.

The nested-opaque case is deliberate and tested (fails closed for custom diagnostic instances); the root case is not covered by any test. Fix: at the root, return the original when the opaque value is not itself provenance-bearing, or wrap it in a real Error carrying the redacted String(error) rather than throwing a primitive.

Not fixed since round 2

  • 🟡 update SDKs to use payment protocol data #2scripts/canonicalize-runtime-provenance-exports.mjs:135-166 still string-matches launch/run and re-emits a fixed (spec, transport, baseUrl) shape; a rename or 4th param ships a broken package on a green build. No assertion added.
  • 🟡 langchain SDK config cleanup #3dist/esm/agents/index.js still statically imports ../../cjs/agents/runtime-provenance.cjs; strict-ESM-only consumers lose a working entry point and the changeset is still minor with no note.

Verdict: Approve with changes — finding 1 destroys a thrown value that carries no provenance; #2/#3 remain packaging debt.

@ynadge
ynadge marked this pull request as draft September 1, 2026 03:41

ynadge commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Review finding 1 adjudication (no code change): the concrete DOMException failure does not reproduce on the supported Node range. Against the exact built package at bc89e2d, Node 18.20.8, 20.20.2, and 24.15.0 all report the thrown DOMException as instanceof Error and preserve exact object identity, name=AbortError, message=Aborted, and a string stack. Metadata-absent calls also return before traversal.

For a genuinely opaque instrumented throwable, we cannot prove the private callsite is absent from private/internal state without invoking user code. Returning it would reopen the round-2 confidentiality hole; wrapping String(error) would invoke the explicitly forbidden custom toString path. The accepted repair contract says privacy takes precedence for opaque surfaces and permits a conservative inert marker. Therefore the orange finding is non-actionable under the ticket contract. The two yellow packaging notes remain explicitly out of scope, and no further automated-review loop will be triggered.

@ynadge
ynadge marked this pull request as ready for review September 1, 2026 03:44

ynadge commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Closing as superseded by the plan-first Agent Map direction.

Disposition: preserve the private-header, one-shot provenance, and fail-closed transport lessons. Defer implementation until there is a smaller end-to-end, plan-aware runtime reconciliation consumer.

Frozen head: bc89e2d8cc561bbb056c130af4f5cbb64ead2c08
Base at closure: yashnadge/sap-2986-infrastructure-adapt-direct-invocations-to-graph-evidence

Replacement direction: Agent Studio: Plan-First Agent Map
Cleanup record: SAP-3045

No branch or commit history is being deleted. Any future extraction should be proposed in a new, consumer-scoped PR.

@ynadge ynadge closed this Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant