attenu-guard 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,5 +6,5 @@
6
6
  * itself imported BY index.ts, so index.ts cannot be the source: `guard.ts -> index.ts ->
7
7
  * guard.ts` would be circular. A one-constant leaf module has nothing to be circular with.
8
8
  */
9
- export declare const VERSION = "0.3.0";
9
+ export declare const VERSION = "0.5.0";
10
10
  //# sourceMappingURL=version.d.ts.map
@@ -9,5 +9,5 @@ exports.VERSION = void 0;
9
9
  * itself imported BY index.ts, so index.ts cannot be the source: `guard.ts -> index.ts ->
10
10
  * guard.ts` would be circular. A one-constant leaf module has nothing to be circular with.
11
11
  */
12
- exports.VERSION = "0.3.0";
12
+ exports.VERSION = "0.5.0";
13
13
  //# sourceMappingURL=version.js.map
@@ -51,6 +51,69 @@
51
51
  * adapter behaves exactly as it did before 0.9.0: no `capture`/`authorizedParams`, no
52
52
  * `recordOutcome` call. Every other framework adapter is unchanged in this release.
53
53
  *
54
+ * ## Adversarial review: the Python batch-1/batch-2 defect classes, checked against this
55
+ * adapter specifically (not assumed absent by analogy)
56
+ *
57
+ * The Python `attenu-guard` adapters went through two rounds of adversarial review that found
58
+ * several defect classes across their (many) framework adapters. This TS package ships exactly
59
+ * ONE adapter surface (this file — verified against `package.json`'s own `exports` map, which
60
+ * declares nothing besides `.` and `./adapters/langgraph`), so each class was checked against
61
+ * THIS adapter specifically, not inherited by assumption:
62
+ *
63
+ * - **Composable middleware / sibling short-circuit or retry** (a sibling wrapper positioned
64
+ * closer to the tool body than this one, able to fabricate or repeat what it observes) — NOT
65
+ * APPLICABLE. Verified directly against pinned `@langchain/core@1.2.9` and
66
+ * `@langchain/langgraph@1.4.13` (installed and grepped, not read off documentation): zero
67
+ * `"middleware"` hits anywhere near tool invocation in either package, and
68
+ * `ToolNode.prototype.runTool` (`dist/prebuilt/tool_node.js`) calls `tool.invoke(toolCall,
69
+ * runtime)` directly — `guardTool`'s `Proxy` IS what gets called, nothing sits between it and
70
+ * `ToolNode`. Neither framework has a composable per-call hook chain the way LangChain-Python's
71
+ * `create_agent(middleware=[...])` or AG2's `FunctionTool.register()` do.
72
+ * - **Double authorization via a second, independent gate** (e.g. Python's `claude_sdk`
73
+ * adapter's `can_use_tool` calling `authorize()` a second time for the same call) — NOT
74
+ * APPLICABLE. There is no second entry point here: `guardNode`/`guardTool` are each the ONLY
75
+ * caller-facing wrapper for their call, and there is nothing in either framework analogous to
76
+ * a second permission callback for a call this adapter already gated.
77
+ * - **Snapshot double-evaluation / narrow-projection commitment** — NOT APPLICABLE in the
78
+ * double-evaluation shape (`snapshotParams`/`snapshotToolParams` already compute ONE snapshot,
79
+ * reused unchanged for both `authorizedParams` and `invokedParams`), but a DIFFERENT,
80
+ * TS-specific gap in the same family was found and fixed — see `freeze()`'s own doc comment
81
+ * above and the CHANGELOG.
82
+ * - **Correlation-key collision across hooks** (e.g. Python's `claude_sdk` `tool_use_id`
83
+ * collision) — NOT APPLICABLE. `guard.recordOutcome` is called synchronously inside the same
84
+ * closure that owns the whole call, from `authorize()`'s own returned `Decision.callId` — no
85
+ * external pending-map keyed by a framework-supplied correlation id exists to collide on.
86
+ * - **Lazy-result detection gaps** (e.g. Python's `smolagents` adapter missing a coroutine or a
87
+ * `concurrent.futures.Future`) — `isDeferredResult` catches generators (an object with its own
88
+ * `.next` AND `[Symbol.iterator]` — "self-iterating", the shape a native generator has, but
89
+ * deliberately NOT the shape a plain `Array`/`Set`/`Map` has, since those implement
90
+ * `[Symbol.iterator]` too without an own `.next`, and their contents are already fully
91
+ * computed), a genuine ASYNC ITERABLE (anything implementing a callable
92
+ * `[Symbol.asyncIterator]`, self-iterating async generators included — see the RELEASE-GATE
93
+ * CORRECTION on this function's own body for why this does NOT require an own `.next` the way
94
+ * the sync branch does), and anything thenable. A plain (non-`async`) function that manually
95
+ * returns a bare `Promise` is also caught correctly, via the thenable check, in the sync
96
+ * branch of both wrappers. This is NOT a claim of covering "the whole lazy-result landscape" —
97
+ * only what this function's own checks actually implement, listed above; a class implementing
98
+ * some OTHER deferred-consumption protocol this function does not check for would not be
99
+ * caught.
100
+ * - **Lost-terminal-event / "fires unconditionally" false claims** (e.g. Python's `strands`
101
+ * adapter's before-hook interrupt paths) — NOT APPLICABLE. There is no external, multi-phase
102
+ * hook-dispatch loop for an event to be lost across; one wrapper function's own `try`/`catch`
103
+ * (or `await`ed async path) owns authorize-through-`recordOutcome` for the whole call
104
+ * synchronously. Structurally this adapter was already closest to Python's own `langgraph.py`
105
+ * reference wiring, not any of the adapters that needed this class of fix.
106
+ * - **Unbounded correlation cache** (Python's `claude_sdk` `_recentVerdicts`) — NOT APPLICABLE,
107
+ * for the same reason as the correlation-collision point above: no cache or pending-map exists
108
+ * in this adapter to bound.
109
+ * - **Wrong dependency declaration** (Python's `semantic-kernel` `protobuf` lesson: check what
110
+ * the RESOLVED version actually requires, not what is assumed) — checked: `src/` imports only
111
+ * `@langchain/langgraph` (lazily, in `isLangGraphAvailable()`); it never imports
112
+ * `@langchain/core` at all (only this file's own tests do, to build fixtures). `package.json`
113
+ * declares zero `dependencies` and no `peerDependencies` — matching the README's own "zero
114
+ * runtime dependencies" claim — and both `@langchain/core`/`@langchain/langgraph` are correctly
115
+ * `devDependencies`-only. Nothing this package's `src/` needs at runtime is undeclared.
116
+ *
54
117
  * ## Delegation
55
118
  *
56
119
  * Handing work to a sub-agent is the delegation moment. `delegateTo` mints the
@@ -67,6 +130,7 @@
67
130
  */
68
131
  import { type Guard } from "../guard.js";
69
132
  import type { Authority } from "../authority.js";
133
+ import type { Json } from "../canonical.js";
70
134
  import type { Context } from "../ceilings.js";
71
135
  import { type Decision } from "../reasons.js";
72
136
  /** Options shared by every guarded wrapper. */
@@ -90,6 +154,129 @@ export type GuardedNode<F extends (...args: any[]) => any> = F & {
90
154
  readonly toolScope: string;
91
155
  readonly unwrapped: F;
92
156
  };
157
+ /**
158
+ * A private, freeze()-only sentinel for "could not be represented as a JSON leaf" — a Proxy, an
159
+ * accessor property, a genuine cycle, a boxed primitive, a TypedArray, a function, or anything
160
+ * else this module does not know how to rebuild as plain JSON. NEVER a JSON-representable
161
+ * value (a string, `null`, …): a second release-gate finding showed a literal string sentinel
162
+ * (`"<accessor>"`) genuinely COLLIDES — a real getter-bearing object and a plain object holding
163
+ * the literal string `"<accessor>"` produced the IDENTICAL `authorizedParamsHash`, an
164
+ * evidence-integrity ambiguity in a supposedly cryptographic commitment (two materially
165
+ * different inputs, one commitment). A fresh, private `Symbol` cannot equal any real call
166
+ * argument, so it cannot collide with one — and it makes the SAME degradation apply uniformly
167
+ * everywhere this function cannot represent something, rather than inventing a new
168
+ * JSON-shaped sentinel (with its own collision risk) per case.
169
+ *
170
+ * Declared as `Json` even though a `Symbol` is not one — a deliberate escape from that type's
171
+ * nominal domain, not an oversight: `canonical.ts`'s own JCS `serialize()` already runtime-checks
172
+ * `typeof` for exactly this reason (its switch handles `"undefined"`/`"bigint"`/`"symbol"`/
173
+ * `"function"` despite `CJson`'s declared type not admitting any of them either), because the
174
+ * type system cannot fully describe this module's actual runtime domain. Once this sentinel
175
+ * reaches `params.ts`'s `commit()` — inside a plain object or array, same as any other frozen
176
+ * leaf — `canonicalBytes` hits that `"symbol"` case, throws `UnsupportedTypeError`, and `commit()`
177
+ * turns that into `paramsHashReason: "unsupported"` for the WHOLE params value, never a partial
178
+ * or per-field one: there is no such thing as "this one nested field is unsupported," only
179
+ * "this whole call's arguments are, or are not, representable."
180
+ *
181
+ * Exported for the same reason `freeze` itself is: not part of this adapter's semantic
182
+ * contract, but its own tests need to assert directly that a given leaf became this exact
183
+ * sentinel (by identity — nothing else can equal it) rather than inferring it indirectly.
184
+ */
185
+ export declare const FREEZE_UNSUPPORTED: Json;
186
+ /**
187
+ * A genuinely immutable, fully decoupled rebuild of `value` — the ONE, UNCONDITIONAL sanitizer
188
+ * every snapshot in this adapter goes through. Safe JSON-primitive leaves
189
+ * (`string`/`number`/`boolean`/`null`) pass through verbatim; plain objects and arrays are
190
+ * rebuilt fresh, recursively, by inspecting their REAL own property descriptors directly
191
+ * (`Object.getOwnPropertyDescriptor`), never by invoking anything the value itself controls (a
192
+ * getter, an iterator, a copy protocol, a Proxy trap, a `toString`/`valueOf`/`Symbol.toPrimitive`
193
+ * override); anything this function cannot represent becomes `UNSUPPORTED` (above) — never a
194
+ * string, never the live object.
195
+ *
196
+ * RELEASE-GATE CORRECTION (CRITICAL): this used to run ONLY as a fallback, after
197
+ * `structuredClone` had already been tried and had THROWN — the previous revision of this
198
+ * comment documented that carefully, but never asked whether `structuredClone` SUCCEEDING was
199
+ * itself a sufficient guarantee. It is not, on three counts, each reproduced directly before
200
+ * that fix: (1) a circular object clones successfully — `structuredClone` handles cycles
201
+ * natively — so this function never ran on it at all, and the circularity later reached
202
+ * `params.ts`'s own cycle-guard-less hash walk and crashed with `RangeError`; (2) a sparse
203
+ * array clones successfully too, bypassing this function's own densification; (3) a
204
+ * `SharedArrayBuffer` clones to a DISTINCT wrapper object sharing the SAME underlying memory —
205
+ * a "successful" clone that is not independent at all. Fixed by making this function the ONLY
206
+ * snapshot path, unconditionally — `structuredClone` is not called anywhere in this adapter.
207
+ *
208
+ * A SECOND release-gate pass then found that "pure introspection" was not fully true either —
209
+ * three more code-execution paths, all reproduced directly before this fix:
210
+ *
211
+ * 1. A `Proxy` is not inert under reflection. `Object.getPrototypeOf`, `Object.keys`
212
+ * (`[[OwnPropertyKeys]]` + a `[[GetOwnProperty]]` per key to check enumerability), and
213
+ * `Object.getOwnPropertyDescriptor` are each real, user-definable traps — reproduced
214
+ * directly: walking an ordinary handler-tracked Proxy through the OLD version of this
215
+ * function fired four separate traps before authorization was ever decided. `Array.isArray`
216
+ * is worse: called on a REVOKED Proxy, it throws `TypeError` outright (its spec algorithm,
217
+ * `IsArray`, unwraps `[[ProxyTarget]]`, which does not exist on a revoked handle) —
218
+ * reproduced directly. Fixed: `require("node:util").types.isProxy(value)` recognizes a
219
+ * Proxy — live OR revoked — via an internal engine slot, invoking NOTHING (verified
220
+ * directly: zero trap calls, no throw on a revoked handle either) — checked FIRST, before
221
+ * `Array.isArray` or any other reflection, and routed straight to `UNSUPPORTED`.
222
+ * 2. The bottom fallback used `String(value)` for anything not a plain object/array — a boxed
223
+ * primitive (`new Number(...)`) with a hostile `Symbol.toPrimitive`, or a `TypedArray` with
224
+ * a hostile own `toString`, each ran attacker code exactly once per snapshot, reproduced
225
+ * directly both ways — BEFORE `Guard.check` had decided allow or deny. The same is true, in
226
+ * principle, of ANY object-typed exotic value (a function's own `.toString` is just as
227
+ * overridable) — there is no way to distinguish "safe to stringify" from "hostile" by
228
+ * inspection alone, so none of them are stringified any more. Fixed: every value that is
229
+ * not a safe JSON primitive and not a plain object/array — a Proxy, a boxed primitive, a
230
+ * TypedArray/`ArrayBuffer`/`SharedArrayBuffer`/`DataView`, a `Map`/`Set`/`Date`/`RegExp`, a
231
+ * function, a `Symbol`, a `BigInt`, anything else — becomes `UNSUPPORTED` (never `String()`,
232
+ * never any other protocol) — see the confirmed-good note above: stringification itself
233
+ * was never unsafe as a RESULT (a `SharedArrayBuffer` never retained live memory as a
234
+ * string), the defect was invoking attacker-controlled code to PRODUCE that string before
235
+ * authorization ran, and `UNSUPPORTED` avoids that entirely rather than picking a "safer"
236
+ * string.
237
+ * 3. Any OTHER reflection failure — an exotic value this pass did not specifically anticipate,
238
+ * still throwing from `Object.getPrototypeOf`/`Object.keys`/`Object.getOwnPropertyDescriptor`
239
+ * despite the Proxy check above — must degrade the same way, not propagate an exception out
240
+ * of a snapshot taken before authorization. The whole reflective walk (everything past the
241
+ * Proxy/primitive fast paths) runs inside one `try`/`catch`; any throw there becomes
242
+ * `UNSUPPORTED` too.
243
+ *
244
+ * `active` is the PATH-ACTIVE cycle guard: the set of containers on the CURRENT recursion path,
245
+ * passed as a NEW `Set` at each recursive call rather than mutated in place and shared across
246
+ * sibling branches (an earlier revision DID share one mutable `WeakSet` across the whole call,
247
+ * which meant a DAG's repeated reference — the SAME object appearing twice as sibling values,
248
+ * never as its own ancestor — was wrongly flagged on its second occurrence; reproduced directly
249
+ * before that fix too). A genuine cycle's own leaf value is `UNSUPPORTED`, not a literal string
250
+ * `"<circular>"` — audited for the same collision class as `"<accessor>"` below, and it has the
251
+ * identical problem: a self-referential object and a plain object holding the literal string
252
+ * `"<circular>"` would otherwise produce the same commitment. There is no position-based reason
253
+ * a cycle's collision is any less real than an accessor's, so it gets the same fix.
254
+ *
255
+ * The property-descriptor walk ALSO closes a separate, protocol-driven gap: the previous
256
+ * revision used `Array.from`/`.map()` (which invoke `[Symbol.iterator]()` — a hostile array's
257
+ * own override can yield ANYTHING regardless of its real indexed properties; reproduced
258
+ * directly: `[1, , 3]` with a hostile iterator froze as `[999]`) and `Object.entries()` (which
259
+ * reads each property's VALUE directly, invoking a getter if one is defined there — reproduced
260
+ * directly: a getter with a side effect was observed three times across the old clone-attempt/
261
+ * freeze/body sequence, and the committed snapshot was the SECOND of three observations, not the
262
+ * first). `Object.getOwnPropertyDescriptor` and a `.length`-bounded index loop are pure
263
+ * introspection — they never invoke user code — and an accessor property (`.get`/`.set` present)
264
+ * becomes `UNSUPPORTED` rather than read at all: a getter can have arbitrary side effects, throw,
265
+ * or return something different on every call, so there is no single "correct" observation of it
266
+ * to commit, and (release-gate correction) the earlier `"<accessor>"` string sentinel this
267
+ * function used instead genuinely collided — reproduced directly: a real getter-bearing object
268
+ * and a plain object holding the literal string `"<accessor>"` produced the identical
269
+ * `authorizedParamsHash`. Both now degrade the same commitment to `unsupported` via `UNSUPPORTED`
270
+ * (see its own doc comment above), never a JSON-representable stand-in.
271
+ *
272
+ * Exported — not part of this adapter's semantic contract (it is an internal sanitizer, not a
273
+ * feature callers configure), but its own aliasing-safety invariant is worth a direct unit
274
+ * test in isolation, the same way every Python adapter's `_freeze()` is imported directly by
275
+ * its own tests: the audit log never exposes the raw snapshot value it produces (only its
276
+ * hash — see `params.ts`'s own doc comment), so "does this alias a live mutable object" is
277
+ * not otherwise observable from outside this module.
278
+ */
279
+ export declare function freeze(value: unknown, active?: ReadonlySet<unknown>): Json;
93
280
  /**
94
281
  * Wrap a callable so every call is authorized through `guard` first.
95
282
  *
@@ -1 +1 @@
1
- {"version":3,"file":"langgraph.d.ts","sourceRoot":"","sources":["../../../src/adapters/langgraph.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"langgraph.d.ts","sourceRoot":"","sources":["../../../src/adapters/langgraph.ts"],"names":[],"mappings":"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"}
@@ -51,6 +51,69 @@
51
51
  * adapter behaves exactly as it did before 0.9.0: no `capture`/`authorizedParams`, no
52
52
  * `recordOutcome` call. Every other framework adapter is unchanged in this release.
53
53
  *
54
+ * ## Adversarial review: the Python batch-1/batch-2 defect classes, checked against this
55
+ * adapter specifically (not assumed absent by analogy)
56
+ *
57
+ * The Python `attenu-guard` adapters went through two rounds of adversarial review that found
58
+ * several defect classes across their (many) framework adapters. This TS package ships exactly
59
+ * ONE adapter surface (this file — verified against `package.json`'s own `exports` map, which
60
+ * declares nothing besides `.` and `./adapters/langgraph`), so each class was checked against
61
+ * THIS adapter specifically, not inherited by assumption:
62
+ *
63
+ * - **Composable middleware / sibling short-circuit or retry** (a sibling wrapper positioned
64
+ * closer to the tool body than this one, able to fabricate or repeat what it observes) — NOT
65
+ * APPLICABLE. Verified directly against pinned `@langchain/core@1.2.9` and
66
+ * `@langchain/langgraph@1.4.13` (installed and grepped, not read off documentation): zero
67
+ * `"middleware"` hits anywhere near tool invocation in either package, and
68
+ * `ToolNode.prototype.runTool` (`dist/prebuilt/tool_node.js`) calls `tool.invoke(toolCall,
69
+ * runtime)` directly — `guardTool`'s `Proxy` IS what gets called, nothing sits between it and
70
+ * `ToolNode`. Neither framework has a composable per-call hook chain the way LangChain-Python's
71
+ * `create_agent(middleware=[...])` or AG2's `FunctionTool.register()` do.
72
+ * - **Double authorization via a second, independent gate** (e.g. Python's `claude_sdk`
73
+ * adapter's `can_use_tool` calling `authorize()` a second time for the same call) — NOT
74
+ * APPLICABLE. There is no second entry point here: `guardNode`/`guardTool` are each the ONLY
75
+ * caller-facing wrapper for their call, and there is nothing in either framework analogous to
76
+ * a second permission callback for a call this adapter already gated.
77
+ * - **Snapshot double-evaluation / narrow-projection commitment** — NOT APPLICABLE in the
78
+ * double-evaluation shape (`snapshotParams`/`snapshotToolParams` already compute ONE snapshot,
79
+ * reused unchanged for both `authorizedParams` and `invokedParams`), but a DIFFERENT,
80
+ * TS-specific gap in the same family was found and fixed — see `freeze()`'s own doc comment
81
+ * above and the CHANGELOG.
82
+ * - **Correlation-key collision across hooks** (e.g. Python's `claude_sdk` `tool_use_id`
83
+ * collision) — NOT APPLICABLE. `guard.recordOutcome` is called synchronously inside the same
84
+ * closure that owns the whole call, from `authorize()`'s own returned `Decision.callId` — no
85
+ * external pending-map keyed by a framework-supplied correlation id exists to collide on.
86
+ * - **Lazy-result detection gaps** (e.g. Python's `smolagents` adapter missing a coroutine or a
87
+ * `concurrent.futures.Future`) — `isDeferredResult` catches generators (an object with its own
88
+ * `.next` AND `[Symbol.iterator]` — "self-iterating", the shape a native generator has, but
89
+ * deliberately NOT the shape a plain `Array`/`Set`/`Map` has, since those implement
90
+ * `[Symbol.iterator]` too without an own `.next`, and their contents are already fully
91
+ * computed), a genuine ASYNC ITERABLE (anything implementing a callable
92
+ * `[Symbol.asyncIterator]`, self-iterating async generators included — see the RELEASE-GATE
93
+ * CORRECTION on this function's own body for why this does NOT require an own `.next` the way
94
+ * the sync branch does), and anything thenable. A plain (non-`async`) function that manually
95
+ * returns a bare `Promise` is also caught correctly, via the thenable check, in the sync
96
+ * branch of both wrappers. This is NOT a claim of covering "the whole lazy-result landscape" —
97
+ * only what this function's own checks actually implement, listed above; a class implementing
98
+ * some OTHER deferred-consumption protocol this function does not check for would not be
99
+ * caught.
100
+ * - **Lost-terminal-event / "fires unconditionally" false claims** (e.g. Python's `strands`
101
+ * adapter's before-hook interrupt paths) — NOT APPLICABLE. There is no external, multi-phase
102
+ * hook-dispatch loop for an event to be lost across; one wrapper function's own `try`/`catch`
103
+ * (or `await`ed async path) owns authorize-through-`recordOutcome` for the whole call
104
+ * synchronously. Structurally this adapter was already closest to Python's own `langgraph.py`
105
+ * reference wiring, not any of the adapters that needed this class of fix.
106
+ * - **Unbounded correlation cache** (Python's `claude_sdk` `_recentVerdicts`) — NOT APPLICABLE,
107
+ * for the same reason as the correlation-collision point above: no cache or pending-map exists
108
+ * in this adapter to bound.
109
+ * - **Wrong dependency declaration** (Python's `semantic-kernel` `protobuf` lesson: check what
110
+ * the RESOLVED version actually requires, not what is assumed) — checked: `src/` imports only
111
+ * `@langchain/langgraph` (lazily, in `isLangGraphAvailable()`); it never imports
112
+ * `@langchain/core` at all (only this file's own tests do, to build fixtures). `package.json`
113
+ * declares zero `dependencies` and no `peerDependencies` — matching the README's own "zero
114
+ * runtime dependencies" claim — and both `@langchain/core`/`@langchain/langgraph` are correctly
115
+ * `devDependencies`-only. Nothing this package's `src/` needs at runtime is undeclared.
116
+ *
54
117
  * ## Delegation
55
118
  *
56
119
  * Handing work to a sub-agent is the delegation moment. `delegateTo` mints the
@@ -65,9 +128,236 @@
65
128
  * });
66
129
  * const tools = guardTools(researcher, [crmQuery], { scopes: { crm_query: "crm.read" } });
67
130
  */
131
+ import { types as nodeUtilTypes } from "node:util";
68
132
  import { AuthorityDenied } from "../guard.js";
69
133
  import { BodyState, Capture } from "../reasons.js";
70
134
  import { VERSION } from "../version.js";
135
+ // `node:util`'s own Proxy check -- an internal engine-slot test, not a trapped operation (see
136
+ // `freeze()`'s doc comment). Aliased for a short, self-explanatory call site.
137
+ const isProxy = nodeUtilTypes.isProxy;
138
+ /**
139
+ * A private, freeze()-only sentinel for "could not be represented as a JSON leaf" — a Proxy, an
140
+ * accessor property, a genuine cycle, a boxed primitive, a TypedArray, a function, or anything
141
+ * else this module does not know how to rebuild as plain JSON. NEVER a JSON-representable
142
+ * value (a string, `null`, …): a second release-gate finding showed a literal string sentinel
143
+ * (`"<accessor>"`) genuinely COLLIDES — a real getter-bearing object and a plain object holding
144
+ * the literal string `"<accessor>"` produced the IDENTICAL `authorizedParamsHash`, an
145
+ * evidence-integrity ambiguity in a supposedly cryptographic commitment (two materially
146
+ * different inputs, one commitment). A fresh, private `Symbol` cannot equal any real call
147
+ * argument, so it cannot collide with one — and it makes the SAME degradation apply uniformly
148
+ * everywhere this function cannot represent something, rather than inventing a new
149
+ * JSON-shaped sentinel (with its own collision risk) per case.
150
+ *
151
+ * Declared as `Json` even though a `Symbol` is not one — a deliberate escape from that type's
152
+ * nominal domain, not an oversight: `canonical.ts`'s own JCS `serialize()` already runtime-checks
153
+ * `typeof` for exactly this reason (its switch handles `"undefined"`/`"bigint"`/`"symbol"`/
154
+ * `"function"` despite `CJson`'s declared type not admitting any of them either), because the
155
+ * type system cannot fully describe this module's actual runtime domain. Once this sentinel
156
+ * reaches `params.ts`'s `commit()` — inside a plain object or array, same as any other frozen
157
+ * leaf — `canonicalBytes` hits that `"symbol"` case, throws `UnsupportedTypeError`, and `commit()`
158
+ * turns that into `paramsHashReason: "unsupported"` for the WHOLE params value, never a partial
159
+ * or per-field one: there is no such thing as "this one nested field is unsupported," only
160
+ * "this whole call's arguments are, or are not, representable."
161
+ *
162
+ * Exported for the same reason `freeze` itself is: not part of this adapter's semantic
163
+ * contract, but its own tests need to assert directly that a given leaf became this exact
164
+ * sentinel (by identity — nothing else can equal it) rather than inferring it indirectly.
165
+ */
166
+ export const FREEZE_UNSUPPORTED = Symbol("attenu-guard:freeze-unsupported");
167
+ const UNSUPPORTED = FREEZE_UNSUPPORTED;
168
+ /**
169
+ * A genuinely immutable, fully decoupled rebuild of `value` — the ONE, UNCONDITIONAL sanitizer
170
+ * every snapshot in this adapter goes through. Safe JSON-primitive leaves
171
+ * (`string`/`number`/`boolean`/`null`) pass through verbatim; plain objects and arrays are
172
+ * rebuilt fresh, recursively, by inspecting their REAL own property descriptors directly
173
+ * (`Object.getOwnPropertyDescriptor`), never by invoking anything the value itself controls (a
174
+ * getter, an iterator, a copy protocol, a Proxy trap, a `toString`/`valueOf`/`Symbol.toPrimitive`
175
+ * override); anything this function cannot represent becomes `UNSUPPORTED` (above) — never a
176
+ * string, never the live object.
177
+ *
178
+ * RELEASE-GATE CORRECTION (CRITICAL): this used to run ONLY as a fallback, after
179
+ * `structuredClone` had already been tried and had THROWN — the previous revision of this
180
+ * comment documented that carefully, but never asked whether `structuredClone` SUCCEEDING was
181
+ * itself a sufficient guarantee. It is not, on three counts, each reproduced directly before
182
+ * that fix: (1) a circular object clones successfully — `structuredClone` handles cycles
183
+ * natively — so this function never ran on it at all, and the circularity later reached
184
+ * `params.ts`'s own cycle-guard-less hash walk and crashed with `RangeError`; (2) a sparse
185
+ * array clones successfully too, bypassing this function's own densification; (3) a
186
+ * `SharedArrayBuffer` clones to a DISTINCT wrapper object sharing the SAME underlying memory —
187
+ * a "successful" clone that is not independent at all. Fixed by making this function the ONLY
188
+ * snapshot path, unconditionally — `structuredClone` is not called anywhere in this adapter.
189
+ *
190
+ * A SECOND release-gate pass then found that "pure introspection" was not fully true either —
191
+ * three more code-execution paths, all reproduced directly before this fix:
192
+ *
193
+ * 1. A `Proxy` is not inert under reflection. `Object.getPrototypeOf`, `Object.keys`
194
+ * (`[[OwnPropertyKeys]]` + a `[[GetOwnProperty]]` per key to check enumerability), and
195
+ * `Object.getOwnPropertyDescriptor` are each real, user-definable traps — reproduced
196
+ * directly: walking an ordinary handler-tracked Proxy through the OLD version of this
197
+ * function fired four separate traps before authorization was ever decided. `Array.isArray`
198
+ * is worse: called on a REVOKED Proxy, it throws `TypeError` outright (its spec algorithm,
199
+ * `IsArray`, unwraps `[[ProxyTarget]]`, which does not exist on a revoked handle) —
200
+ * reproduced directly. Fixed: `require("node:util").types.isProxy(value)` recognizes a
201
+ * Proxy — live OR revoked — via an internal engine slot, invoking NOTHING (verified
202
+ * directly: zero trap calls, no throw on a revoked handle either) — checked FIRST, before
203
+ * `Array.isArray` or any other reflection, and routed straight to `UNSUPPORTED`.
204
+ * 2. The bottom fallback used `String(value)` for anything not a plain object/array — a boxed
205
+ * primitive (`new Number(...)`) with a hostile `Symbol.toPrimitive`, or a `TypedArray` with
206
+ * a hostile own `toString`, each ran attacker code exactly once per snapshot, reproduced
207
+ * directly both ways — BEFORE `Guard.check` had decided allow or deny. The same is true, in
208
+ * principle, of ANY object-typed exotic value (a function's own `.toString` is just as
209
+ * overridable) — there is no way to distinguish "safe to stringify" from "hostile" by
210
+ * inspection alone, so none of them are stringified any more. Fixed: every value that is
211
+ * not a safe JSON primitive and not a plain object/array — a Proxy, a boxed primitive, a
212
+ * TypedArray/`ArrayBuffer`/`SharedArrayBuffer`/`DataView`, a `Map`/`Set`/`Date`/`RegExp`, a
213
+ * function, a `Symbol`, a `BigInt`, anything else — becomes `UNSUPPORTED` (never `String()`,
214
+ * never any other protocol) — see the confirmed-good note above: stringification itself
215
+ * was never unsafe as a RESULT (a `SharedArrayBuffer` never retained live memory as a
216
+ * string), the defect was invoking attacker-controlled code to PRODUCE that string before
217
+ * authorization ran, and `UNSUPPORTED` avoids that entirely rather than picking a "safer"
218
+ * string.
219
+ * 3. Any OTHER reflection failure — an exotic value this pass did not specifically anticipate,
220
+ * still throwing from `Object.getPrototypeOf`/`Object.keys`/`Object.getOwnPropertyDescriptor`
221
+ * despite the Proxy check above — must degrade the same way, not propagate an exception out
222
+ * of a snapshot taken before authorization. The whole reflective walk (everything past the
223
+ * Proxy/primitive fast paths) runs inside one `try`/`catch`; any throw there becomes
224
+ * `UNSUPPORTED` too.
225
+ *
226
+ * `active` is the PATH-ACTIVE cycle guard: the set of containers on the CURRENT recursion path,
227
+ * passed as a NEW `Set` at each recursive call rather than mutated in place and shared across
228
+ * sibling branches (an earlier revision DID share one mutable `WeakSet` across the whole call,
229
+ * which meant a DAG's repeated reference — the SAME object appearing twice as sibling values,
230
+ * never as its own ancestor — was wrongly flagged on its second occurrence; reproduced directly
231
+ * before that fix too). A genuine cycle's own leaf value is `UNSUPPORTED`, not a literal string
232
+ * `"<circular>"` — audited for the same collision class as `"<accessor>"` below, and it has the
233
+ * identical problem: a self-referential object and a plain object holding the literal string
234
+ * `"<circular>"` would otherwise produce the same commitment. There is no position-based reason
235
+ * a cycle's collision is any less real than an accessor's, so it gets the same fix.
236
+ *
237
+ * The property-descriptor walk ALSO closes a separate, protocol-driven gap: the previous
238
+ * revision used `Array.from`/`.map()` (which invoke `[Symbol.iterator]()` — a hostile array's
239
+ * own override can yield ANYTHING regardless of its real indexed properties; reproduced
240
+ * directly: `[1, , 3]` with a hostile iterator froze as `[999]`) and `Object.entries()` (which
241
+ * reads each property's VALUE directly, invoking a getter if one is defined there — reproduced
242
+ * directly: a getter with a side effect was observed three times across the old clone-attempt/
243
+ * freeze/body sequence, and the committed snapshot was the SECOND of three observations, not the
244
+ * first). `Object.getOwnPropertyDescriptor` and a `.length`-bounded index loop are pure
245
+ * introspection — they never invoke user code — and an accessor property (`.get`/`.set` present)
246
+ * becomes `UNSUPPORTED` rather than read at all: a getter can have arbitrary side effects, throw,
247
+ * or return something different on every call, so there is no single "correct" observation of it
248
+ * to commit, and (release-gate correction) the earlier `"<accessor>"` string sentinel this
249
+ * function used instead genuinely collided — reproduced directly: a real getter-bearing object
250
+ * and a plain object holding the literal string `"<accessor>"` produced the identical
251
+ * `authorizedParamsHash`. Both now degrade the same commitment to `unsupported` via `UNSUPPORTED`
252
+ * (see its own doc comment above), never a JSON-representable stand-in.
253
+ *
254
+ * Exported — not part of this adapter's semantic contract (it is an internal sanitizer, not a
255
+ * feature callers configure), but its own aliasing-safety invariant is worth a direct unit
256
+ * test in isolation, the same way every Python adapter's `_freeze()` is imported directly by
257
+ * its own tests: the audit log never exposes the raw snapshot value it produces (only its
258
+ * hash — see `params.ts`'s own doc comment), so "does this alias a live mutable object" is
259
+ * not otherwise observable from outside this module.
260
+ */
261
+ export function freeze(value, active = new Set()) {
262
+ if (value === null || value === undefined)
263
+ return null;
264
+ const t = typeof value;
265
+ if (t === "string" || t === "number" || t === "boolean")
266
+ return value;
267
+ // A Proxy is recognized FIRST, before Array.isArray or ANY reflection -- see this function's
268
+ // own doc comment, point 1. `util.types.isProxy` reads an internal engine slot; it invokes no
269
+ // trap and does not throw even on a revoked Proxy (verified directly), unlike everything below
270
+ // it, which would either fire real traps (a live Proxy) or throw outright (a revoked one).
271
+ if (isProxy(value))
272
+ return UNSUPPORTED;
273
+ try {
274
+ if (Array.isArray(value)) {
275
+ if (active.has(value))
276
+ return UNSUPPORTED; // a genuine cycle -- see doc comment above
277
+ const withSelf = new Set(active).add(value);
278
+ const out = [];
279
+ // Index-by-index via getOwnPropertyDescriptor, not Array.from/.map: those invoke
280
+ // `[Symbol.iterator]()`, which a hostile array can override to yield ANYTHING regardless
281
+ // of what its real indexed properties hold (reproduced directly: `[1, , 3]` with a
282
+ // hostile iterator froze as `[999]`). `.length` and getOwnPropertyDescriptor are pure
283
+ // introspection -- they read the array's REAL own properties without ever calling user
284
+ // code. A hole (no descriptor at that index) is densified to `null`, same as any other
285
+ // absence here.
286
+ for (let i = 0; i < value.length; i++) {
287
+ out.push(freezeDescriptor(Object.getOwnPropertyDescriptor(value, i), withSelf));
288
+ }
289
+ return out;
290
+ }
291
+ if (t === "object") {
292
+ const proto = Object.getPrototypeOf(value);
293
+ if (proto === Object.prototype || proto === null) {
294
+ const obj = value;
295
+ if (active.has(obj))
296
+ return UNSUPPORTED; // a genuine cycle -- see doc comment above
297
+ const withSelf = new Set(active).add(obj);
298
+ const out = {};
299
+ // Object.keys + getOwnPropertyDescriptor, not Object.entries: Object.entries reads each
300
+ // property's VALUE directly, which invokes a getter if one is defined at that key --
301
+ // reproduced directly: with an unclonable sibling forcing the old fallback, a getter
302
+ // with a side effect was observed three times across the old clone-attempt/freeze/body
303
+ // path, and the committed snapshot was the SECOND of three observations, not the first.
304
+ // Reading the DESCRIPTOR instead never invokes anything; an accessor property
305
+ // (`.get`/`.set` present, no `.value`) becomes `UNSUPPORTED` -- explicitly marked, never
306
+ // executed -- rather than read (see `freezeDescriptor`). `Object.keys` correctly LISTS a
307
+ // literal `"__proto__"` key (a plain `JSON.parse('{"__proto__": {...}}')` result has it
308
+ // as an own, enumerable DATA property, same as any other key) -- the loop below still
309
+ // needs `Object.defineProperty`, not a bracket assignment, to actually WRITE it back
310
+ // safely.
311
+ for (const key of Object.keys(obj)) {
312
+ // Object.defineProperty, NOT `out[key] = ...`: a bracket ASSIGNMENT to the literal key
313
+ // "__proto__" does not create a data property at all -- it invokes Object.prototype's
314
+ // own `__proto__` SETTER, silently changing `out`'s prototype instead and dropping the
315
+ // key from its own enumerable keys entirely. defineProperty always performs a genuine
316
+ // [[DefineOwnProperty]], bypassing that accessor, for "__proto__" exactly like any
317
+ // other key name.
318
+ Object.defineProperty(out, key, {
319
+ value: freezeDescriptor(Object.getOwnPropertyDescriptor(obj, key), withSelf),
320
+ enumerable: true,
321
+ writable: true,
322
+ configurable: true,
323
+ });
324
+ }
325
+ return out;
326
+ }
327
+ }
328
+ }
329
+ catch {
330
+ // A reflection call above threw despite the Proxy check -- an exotic value this pass did
331
+ // not specifically anticipate. Degrade the same way as everything else this function
332
+ // cannot represent; never let a snapshot taken BEFORE authorization propagate an exception.
333
+ return UNSUPPORTED;
334
+ }
335
+ // A boxed primitive (`new Number(...)`/`new String(...)`/`new Boolean(...)`), a TypedArray,
336
+ // `ArrayBuffer`/`SharedArrayBuffer`/`DataView`, a `Map`/`Set`/`Date`/`RegExp`, a function, a
337
+ // `Symbol`, a `BigInt`, or anything else that is not a plain object or array -- never
338
+ // stringified any more (see this function's own doc comment, point 2: a hostile
339
+ // `Symbol.toPrimitive`/`toString`/`valueOf` override runs attacker code before authorization
340
+ // has been decided, reproduced directly for a boxed primitive and a TypedArray) and never the
341
+ // live reference either. `UNSUPPORTED` degrades the whole commitment cleanly instead.
342
+ return UNSUPPORTED;
343
+ }
344
+ /**
345
+ * Reads ONE property descriptor safely: a data property's `.value` is frozen recursively; an
346
+ * accessor property (`.get`/`.set` present) becomes `UNSUPPORTED` WITHOUT ever calling the
347
+ * getter (a getter can have arbitrary side effects, throw, or return something different on
348
+ * each call — there is no "correct" single observation to commit, and a JSON-representable
349
+ * sentinel string genuinely collided with a real getter-bearing object's commitment — see
350
+ * `UNSUPPORTED`'s own doc comment); a missing descriptor (an array hole, or a key that no
351
+ * longer exists) becomes `null`, the same as any other JSON-shaped absence `freeze` produces
352
+ * elsewhere.
353
+ */
354
+ function freezeDescriptor(desc, active) {
355
+ if (desc === undefined)
356
+ return null;
357
+ if (desc.get || desc.set)
358
+ return UNSUPPORTED;
359
+ return freeze(desc.value, active);
360
+ }
71
361
  /**
72
362
  * An IMMUTABLE snapshot of the call's arguments, taken BEFORE the wrapped callable runs and
73
363
  * reused for BOTH `authorizedParams` (`check`) and `invokedParams` (`recordOutcome`) — so a
@@ -76,16 +366,10 @@ import { VERSION } from "../version.js";
76
366
  * binding" section; no `kwargs` — JavaScript has no separate keyword-argument bag).
77
367
  */
78
368
  function snapshotParams(args) {
79
- const raw = { args: [...args] };
80
- try {
81
- return structuredClone(raw);
82
- }
83
- catch {
84
- // Best-effort: something in here isn't structured-cloneable (a live socket, a function, ...).
85
- // Fall back to the shallow copy — a residual risk only if THAT specific object is later
86
- // mutated in place, a rare edge case documented here rather than silently claimed away.
87
- return raw;
88
- }
369
+ // freeze() unconditionally, not structuredClone-then-fallback: see freeze()'s own doc
370
+ // comment's "RELEASE-GATE CORRECTION" for why a successful structuredClone is not itself a
371
+ // sufficient independence guarantee (circular inputs, sparse arrays, SharedArrayBuffer).
372
+ return freeze({ args: [...args] });
89
373
  }
90
374
  /**
91
375
  * `true` if `result` is a generator/async-generator/promise-like value whose consumption this
@@ -99,9 +383,26 @@ function isDeferredResult(result) {
99
383
  if (result === null || typeof result !== "object")
100
384
  return false;
101
385
  const r = result;
386
+ // A native generator object (from `function*`) has BOTH `.next` AND `[Symbol.iterator]`
387
+ // (returning itself) -- "self-iterating". Requiring both here deliberately does NOT match a
388
+ // plain Array/Set/Map: those implement `[Symbol.iterator]` too, but the object ITSELF has no
389
+ // `.next` (only the SEPARATE iterator `arr[Symbol.iterator]()` produces does) -- and an
390
+ // array's contents are already fully computed, nothing deferred about returning one.
102
391
  if (typeof r["next"] === "function" && typeof r[Symbol.iterator] === "function")
103
392
  return true;
104
- if (typeof r["next"] === "function" && typeof r[Symbol.asyncIterator] === "function")
393
+ // RELEASE-GATE CORRECTION (HIGH): the async branch used to require the SAME "has its own
394
+ // .next" shape, matching a native async generator (self-iterating, same reasoning as above)
395
+ // but missing the more general ASYNC ITERABLE protocol: per spec, `[Symbol.asyncIterator]`
396
+ // being callable is sufficient on its own -- calling it returns a SEPARATE async iterator
397
+ // object that has `.next`, so the ITERABLE itself need not. Reproduced directly before
398
+ // fixing: a plain object implementing only `[Symbol.asyncIterator]()` was recorded
399
+ // `BodyState.RETURNED`, not `DEFERRED`. Unlike the sync case, there is no common JavaScript
400
+ // built-in that implements `Symbol.asyncIterator` over ALREADY-computed values the way a
401
+ // plain Array does for `Symbol.iterator` (Node's own `Readable` streams implement it
402
+ // precisely because their data is NOT all available yet), so checking `Symbol.asyncIterator`
403
+ // alone does not risk the same false-positive class dropping the `.next` requirement here
404
+ // would raise for the sync branch.
405
+ if (typeof r[Symbol.asyncIterator] === "function")
105
406
  return true;
106
407
  if (typeof r["then"] === "function")
107
408
  return true;
@@ -295,13 +596,8 @@ export function guardTool(guard, tool, options = {}) {
295
596
  hookPath: `guardTool:${tool.name}`,
296
597
  };
297
598
  function snapshotToolParams(input) {
298
- const raw = toolArgs(input);
299
- try {
300
- return structuredClone(raw);
301
- }
302
- catch {
303
- return raw;
304
- }
599
+ // See snapshotParams' own comment above: freeze() unconditionally, never structuredClone.
600
+ return freeze(toolArgs(input));
305
601
  }
306
602
  function authorize(input, config, snapshot) {
307
603
  const context = options.contextFn ? options.contextFn(input, config) : {};