[AAASM-5750] ✨ (gateway): Forward the hook-layer audit event to the runtime - #370
Conversation
The union had no value for a client that carries the event onward, so wiring one would have had to declare a retention the SDK cannot observe or a drop that is no longer true. Adds it and maps each value onto the ADR 0033 section 6 term it earns. Refs AAASM-5750
record / recordResult / scanPrompts constructed the event and dropped it, firing only an AA_DEBUG note. They now send on nativeClient.sendEvent — the same primitive and connected session that already carries the boot registration event — so a governed call, allowed or denied, reaches the runtime's audit pipeline. auditSink is computed from canRegister so a client built over the fallback stub still declares honestly. Refs AAASM-5750
The init warning, the context field doc and withAssembly's deny note all said both shipped clients drop the event. State what the event's fate now depends on, and name the runtime-absent case rather than dropping it. Refs AAASM-5750
Inverts both suites over the same boundary stub and the same positive control: the shipped native client must now cross it with each audit method, and a governed deny through withAssembly must too. Adds the fallback-stub input that shows the computed disposition moving, and keeps the superseded AAASM-4847 file so a revert to the no-op reddens the tests that demanded it. Refs AAASM-5750
5750 built the sink, so a claim still calling recording Planned under it describes shipped behaviour as unbuilt. Collapses the guarded-site tier into one repo-wide rule and adds a positive control over synthetic input, since a rule expecting no findings is green when the scan is broken. Refs AAASM-5750
Updates the bound sentence and its quote= in lockstep, and repoints the controls at the boundary suite plus the branch that produces no evidence, so both halves of the sentence are decided by something. Refs AAASM-5750
Four sites across the introduction and the README asserted that governed actions leave nothing behind on every path. Each now states which client forwards and which drops, and names the disposition to read. Refs AAASM-5750
…walk readdirSync followed by a separate statSync leaves a window in which an entry can be removed between the two, and the throw takes the whole scan down — turning a gate whose verdict is "no findings" into one that produced no verdict. CI hit it on pnpm's transient .tmp-postinstall directory. Reads the type from the directory entry instead, and skips a file removed before it could be read. Refs AAASM-5750
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
The payload's shape is what a reader downstream distinguishes on, and only one side of each optional spread was exercised — a spread that always fired and one that never fired look identical under that. Also covers the case where JSON.stringify returns undefined rather than throwing, which would otherwise put a recorded outcome on the wire as a missing field. Refs AAASM-5750
The first sweep enumerated by phrase and missed sites whose false clause never used the word "audit": the context field's per-framework Observed note, the guides capability table and its footnote, a negative control's scope comment, and a claim binding whose id asserted the opposite of the quote it guards. Refs AAASM-5750
|
Second sweep — the first one's method was wrong, and it missed five sites. The first pass enumerated by phrase. An independent audit against what each sentence asserts found five more, committed in
Two residuals I looked at and deliberately did not change:
Also in |
stringifyForAudit fell back to String(value) when JSON.stringify threw, but String() throws too — an object from Object.create(null) has no primitive conversion, and querystring.parse() returns exactly those. The throw escapes: AssemblyCallbackHandler.handleToolEnd awaits the sink with no try/catch and @langchain/core awaits that inside the tool invocation, so the user's tool call rejects. Guards the second conversion and falls back to a constant. Refs AAASM-5750
sendEvent returns void, is unacknowledged, and never throws, so a client whose connect never succeeds still reports "forwarded". The SDK cannot see arrival, let alone retention, and ADR 0033 section 6 Observed needs a durable event attributed to the action. States the handoff and nothing beyond it, and records that the discarded arm is unreachable through initAssembly. Refs AAASM-5750
…the regex The three positive controls drove deferralsInLines on synthetic strings, so mutating scan() to return immediately left all five tests green — detector reachability and walk reachability are different claims, and only the second makes the repo-wide "no findings" mean anything. Threads a file count out of the walk and pins a file that only exists here. Refs AAASM-5750
…true Five user-facing sites promised the record arrived. It is written to the runtime's event channel and unacknowledged from there, so the docs now say handoff and say plainly it is not an audit guarantee. Refs AAASM-5750
|
Review round 2 — regression fixed, 1. The regression, fixed first. The second conversion now has its own guard and falls back to 2. I did not substitute a different §6 term. The honest position is that this layer makes no §6 recording claim at all; the enforcement claims (Evaluated / Denied before execution) are untouched. I have not referenced the durability defect by ticket since you are filing it — say the word and I will add the reference. 3. The 4. The referent gate discriminated nothing — fixed and proven. Your mutation reproduced exactly: 5. Count corrected. The suite is 740 (738 passed, 2 skipped) at this head — the 730 in the body was stale before I added tests, and 732 was your reading of the same stale moment. Gates: |
The gap was described in seven places with no referent, which reads as an explained limitation rather than a tracked one. Points at what 5783 will change — report_event payloads reaching the live stream and the durable entry — and says the terms here are revisited when it lands, not before. The quick-start sentence gets its own binding with an unprovenReason rather than a control: the behaviour it points at lives in aa-sdk-client and aa-runtime, outside this package's test boundary. Refs AAASM-5750, AAASM-5783
|



Target
Task summary:
createNativeGatewayClient'srecord/recordResult/scanPromptsconstructed the audit event, handed it over, and dropped it — firing only a one-timeAA_DEBUGnote. They now send it onnativeClient.sendEvent, the same primitive and the same connected session that already carries the boot registration event (init-assembly.ts:831), so a governed tool call — allowed or denied — reaches the runtime's audit pipeline.Task tickets:
python-sdkBump napi-derive from 3.5.10 to 3.6.0 in /native/aa-ffi-node #319,go-sdk[AAASM-2774] 🔧 (sonar): Wire projectVersion off 0.0.0 + cover in release skills #203.Key point change (optional):
Design, and the code that decided it. The AAASM-4847 comment said hook-layer events "have nowhere to go". That was true of the gateway wire — there is still no HTTP audit route for the SDK to POST to — and false of the runtime channel, which was already open and already carrying an event.
native/aa-ffi-node/src/lib.rs:152(send_event→AssemblyClient::report_event) reaches the runtime pipeline, which enriches, re-scans and admits the frame to its audit pipeline.auditSinkis computed, not fixed.createNativeGatewayClientis also handed the fallback stubcreateNativeClientreturns when the native binding cannot be loaded, whosesendEventis a no-op. Declaring"forwarded"there would be a claim about a channel the client does not hold, so the value keys offcanRegister— which is exactly "the binding loaded", the same fact the record path depends on.§6 term earned.
napi-inprocessover a loaded binding → Observed (forwarded). The no-op client (auto/sdk-only/grpc-sidecar) holds no transport at all →discarded→ Unsupported; that gap is structural, not unwired. A native client over the fallback stub →discarded→ Degraded.Not changed: no redaction is attempted on this side — the runtime's scanner is the unconditional gate on every inbound frame, and a weaker copy here would only invite the two to disagree.
Effecting Scope
AuditSinkDispositiongains"forwarded". The union is a public export, so a consumer exhaustively switching on it must add a branch; the two existing values keep their meanings.Description
src/types/gateway-governance.ts— add"forwarded"; map each value onto the §6 term it earns.src/gateway/client.ts— send the three audit methods oversendEvent; computeauditSinkfromcanRegister; addstringifyForAuditso a circular orBigInttool result degrades to a lossy record rather than throwing out of the sink into the governed call.src/core/init-assembly.ts— the warning fires for"discarded"only, and says what to do about it.Validation
pnpm typecheck,pnpm lint(0 issues),pnpm test(730 passed, 2 skipped, 73 files),pnpm build— all green.Downstream-boundary stub with a reachability control (AC2).
tests/audit-sink-disposition.test.ts'sboundary()is theNativeClientseam, not a stand-in for the code under test. The positive control asserts aqueryPolicycrossing; the finding asserts three distinctsendEventcrossings carrying three distinct payloads. A new case drives a real deny throughwithAssembly— the shipped interceptor, no injected sink — and asserts the deny reason crosses the same boundary.Proven able to fail (AC3). Reverting the three sinks to
async () => undefined:Quote lockstep is load-bearing. Rewording
docs/02-quick-start/index.mdalone, leavingquote=untouched, reddensno sentence is unbound and unallowedand… matches exactly one whole sentence.tests/napi-audit-noop.test.tsis kept rather than deleted: it now asserts the inversion of what it used to demand, so a revert to the no-op reddens the exact file that once required it and theAA_DEBUGnote cannot quietly return.Known behaviour worth a reviewer's eye
nativeClient.sendEventnever throws — the transport stashes a failure and surfaces it on the nextqueryPolicy. That is what makes these safe to call from the hook layer, but it means a broken IPC channel can deny a later call underenforce. That is the correct fail-closed reading of an unreachable runtime rather than an audit-driven enforcement change, and it is pre-existing behaviour that registration already had.🤖 Generated with Claude Code
https://claude.ai/code/session_01XWLmA8FgULT9e6ntdCo1H2