@coderifts/agent-guard 9.5.0 → 9.6.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.
Files changed (44) hide show
  1. package/README.md +35 -0
  2. package/dist/cjs/execution-grant.d.ts +41 -0
  3. package/dist/cjs/execution-grant.d.ts.map +1 -0
  4. package/dist/cjs/execution-grant.js +66 -0
  5. package/dist/cjs/execution-grant.js.map +1 -0
  6. package/dist/cjs/guard.d.ts.map +1 -1
  7. package/dist/cjs/guard.js +91 -37
  8. package/dist/cjs/guard.js.map +1 -1
  9. package/dist/cjs/index.d.ts +2 -0
  10. package/dist/cjs/index.d.ts.map +1 -1
  11. package/dist/cjs/index.js +5 -2
  12. package/dist/cjs/index.js.map +1 -1
  13. package/dist/cjs/tool-registry.d.ts +1 -1
  14. package/dist/cjs/tool-registry.d.ts.map +1 -1
  15. package/dist/cjs/tool-registry.js +2 -2
  16. package/dist/cjs/tool-registry.js.map +1 -1
  17. package/dist/cjs/types.d.ts +22 -2
  18. package/dist/cjs/types.d.ts.map +1 -1
  19. package/dist/cjs/with-coderifts.d.ts +6 -0
  20. package/dist/cjs/with-coderifts.d.ts.map +1 -1
  21. package/dist/cjs/with-coderifts.js +3 -0
  22. package/dist/cjs/with-coderifts.js.map +1 -1
  23. package/dist/esm/execution-grant.d.ts +41 -0
  24. package/dist/esm/execution-grant.d.ts.map +1 -0
  25. package/dist/esm/execution-grant.js +59 -0
  26. package/dist/esm/execution-grant.js.map +1 -0
  27. package/dist/esm/guard.d.ts.map +1 -1
  28. package/dist/esm/guard.js +91 -37
  29. package/dist/esm/guard.js.map +1 -1
  30. package/dist/esm/index.d.ts +2 -0
  31. package/dist/esm/index.d.ts.map +1 -1
  32. package/dist/esm/index.js +1 -0
  33. package/dist/esm/index.js.map +1 -1
  34. package/dist/esm/tool-registry.d.ts +1 -1
  35. package/dist/esm/tool-registry.d.ts.map +1 -1
  36. package/dist/esm/tool-registry.js +2 -2
  37. package/dist/esm/tool-registry.js.map +1 -1
  38. package/dist/esm/types.d.ts +22 -2
  39. package/dist/esm/types.d.ts.map +1 -1
  40. package/dist/esm/with-coderifts.d.ts +6 -0
  41. package/dist/esm/with-coderifts.d.ts.map +1 -1
  42. package/dist/esm/with-coderifts.js +3 -0
  43. package/dist/esm/with-coderifts.js.map +1 -1
  44. package/package.json +1 -1
package/README.md CHANGED
@@ -250,6 +250,41 @@ const { tools, registry_report, composition_assurance, receipt_thread } = withCo
250
250
  // register ONLY `tools`. Receipt carry-forward is default-on (`receipt_thread`).
251
251
  ```
252
252
 
253
+ ### Native execution grant (`executionGrant`, 9.6.0)
254
+
255
+ `withCodeRifts` 9.5.0 could not request a grant: its authorize sent four
256
+ fields and the factory received `decision_result`, so a top-level
257
+ `execution_grant` was dropped. The interim app-side wrap
258
+ (`withExecutionGrantClient` / `takeGrant()`) worked but was **last-authorize**
259
+ — overlapping tool calls could hand a tool the wrong grant.
260
+
261
+ 9.6.0 closes that natively. Default **OFF** (absent config is byte-identical
262
+ to 9.5.0). A host on 9.6.0 does **not** need `withExecutionGrantClient`.
263
+
264
+ ```typescript
265
+ const { tools } = withCodeRifts({
266
+ tools: rawTools, // execute(args, { execution_grant }) — 2nd arg is THIS call's grant
267
+ client,
268
+ operation: 'merge',
269
+ executionGrant: {
270
+ enabled: true,
271
+ // Optional. Return the ATOMIC nonce for THIS call (customer executor
272
+ // state-challenge). A throw fails the call closed
273
+ // (EXECUTION_GRANT_NONCE_UNRESOLVABLE) — never a grant-less proceed.
274
+ resolveStateNonce: async ({ artifactId, toolName, args }) =>
275
+ challengeNonceFor(artifactId),
276
+ },
277
+ });
278
+ ```
279
+
280
+ `guardToolCall` factory 3rd argument is the same `{ execution_grant }` object,
281
+ scoped to that invocation. Allow-class authorize that requested a grant and
282
+ does not receive one fails closed (`EXECUTION_GRANT_MISSING` /
283
+ `SIGNER_UNAVAILABLE`). After a grant was requested, `failPolicy: 'open'`
284
+ does not OPEN_PASSTHROUGH grant-less — the call fails closed with the
285
+ server's reason. The outcome records `{ requested, arrived }` only —
286
+ not the token. Not a verdict input; not a preimage field.
287
+
253
288
  Honesty (do not over-claim):
254
289
 
255
290
  - **Freshness** is ACTIVE only because STRICT sets `requireFreshness` **and** you supplied
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Native execution-grant helpers (9.6.0).
3
+ *
4
+ * Grant request is per guardToolCall invocation (local vars). The token is not
5
+ * a verdict input and is not folded into any fingerprint preimage.
6
+ */
7
+ export type ExecutionGrantConfig = {
8
+ enabled: true;
9
+ resolveStateNonce?: (ctx: {
10
+ artifactId: string | null;
11
+ toolName: string;
12
+ args: unknown;
13
+ }) => string | Promise<string>;
14
+ };
15
+ export type ExecutionGrantObservation = {
16
+ requested: boolean;
17
+ arrived: boolean;
18
+ };
19
+ export type ExecutionGrantCallContext = {
20
+ execution_grant: string | null;
21
+ };
22
+ export declare function isExecutionGrantEnabled(config: {
23
+ executionGrant?: ExecutionGrantConfig;
24
+ } | null | undefined): boolean;
25
+ export declare function readExecutionGrantToken(response: unknown): string | null;
26
+ export declare function firstArtifactId(artifacts: unknown): string | null;
27
+ /** True when the thrown authorize error carries the named SIGNER_UNAVAILABLE code. */
28
+ export declare function isSignerUnavailableError(err: unknown): boolean;
29
+ export declare function resolveStateNonceForCall(config: {
30
+ executionGrant?: ExecutionGrantConfig;
31
+ }, call: {
32
+ toolName: string;
33
+ arguments?: unknown;
34
+ artifacts?: unknown;
35
+ }, artifacts: unknown): Promise<{
36
+ ok: true;
37
+ nonce?: string;
38
+ } | {
39
+ ok: false;
40
+ }>;
41
+ //# sourceMappingURL=execution-grant.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execution-grant.d.ts","sourceRoot":"","sources":["../../src/execution-grant.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,IAAI,CAAC;IACd,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE;QACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,OAAO,CAAC;KACf,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,CAAC;AAEF,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAAE,cAAc,CAAC,EAAE,oBAAoB,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAErH;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAIxE;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CASjE;AAED,sFAAsF;AACtF,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAM9D;AAED,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE;IAAE,cAAc,CAAC,EAAE,oBAAoB,CAAA;CAAE,EACjD,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,EACpE,SAAS,EAAE,OAAO,GACjB,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,CAAC,CAevD"}
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ /**
3
+ * Native execution-grant helpers (9.6.0).
4
+ *
5
+ * Grant request is per guardToolCall invocation (local vars). The token is not
6
+ * a verdict input and is not folded into any fingerprint preimage.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.isExecutionGrantEnabled = isExecutionGrantEnabled;
10
+ exports.readExecutionGrantToken = readExecutionGrantToken;
11
+ exports.firstArtifactId = firstArtifactId;
12
+ exports.isSignerUnavailableError = isSignerUnavailableError;
13
+ exports.resolveStateNonceForCall = resolveStateNonceForCall;
14
+ function isExecutionGrantEnabled(config) {
15
+ return !!(config && config.executionGrant && config.executionGrant.enabled === true);
16
+ }
17
+ function readExecutionGrantToken(response) {
18
+ if (!response || typeof response !== 'object')
19
+ return null;
20
+ const g = response.execution_grant;
21
+ return typeof g === 'string' && g.length > 0 ? g : null;
22
+ }
23
+ function firstArtifactId(artifacts) {
24
+ if (!Array.isArray(artifacts))
25
+ return null;
26
+ for (const a of artifacts) {
27
+ if (a && typeof a === 'object' && typeof a.id === 'string') {
28
+ const id = String(a.id).trim();
29
+ if (id)
30
+ return id;
31
+ }
32
+ }
33
+ return null;
34
+ }
35
+ /** True when the thrown authorize error carries the named SIGNER_UNAVAILABLE code. */
36
+ function isSignerUnavailableError(err) {
37
+ if (!err || typeof err !== 'object')
38
+ return false;
39
+ const e = err;
40
+ if (e.code === 'SIGNER_UNAVAILABLE')
41
+ return true;
42
+ if (e.body && e.body.code === 'SIGNER_UNAVAILABLE')
43
+ return true;
44
+ return false;
45
+ }
46
+ async function resolveStateNonceForCall(config, call, artifacts) {
47
+ const resolver = config.executionGrant && config.executionGrant.resolveStateNonce;
48
+ if (typeof resolver !== 'function')
49
+ return { ok: true };
50
+ try {
51
+ const raw = await resolver({
52
+ artifactId: firstArtifactId(artifacts) || firstArtifactId(call.artifacts),
53
+ toolName: call.toolName,
54
+ args: call.arguments,
55
+ });
56
+ if (raw == null || raw === '')
57
+ return { ok: true };
58
+ if (typeof raw !== 'string')
59
+ return { ok: false };
60
+ return { ok: true, nonce: raw };
61
+ }
62
+ catch {
63
+ return { ok: false };
64
+ }
65
+ }
66
+ //# sourceMappingURL=execution-grant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execution-grant.js","sourceRoot":"","sources":["../../src/execution-grant.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;AAoBH,0DAEC;AAED,0DAIC;AAED,0CASC;AAGD,4DAMC;AAED,4DAmBC;AAjDD,SAAgB,uBAAuB,CAAC,MAAoE;IAC1G,OAAO,CAAC,CAAC,CAAC,MAAM,IAAI,MAAM,CAAC,cAAc,IAAI,MAAM,CAAC,cAAc,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC;AACvF,CAAC;AAED,SAAgB,uBAAuB,CAAC,QAAiB;IACvD,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3D,MAAM,CAAC,GAAI,QAA0C,CAAC,eAAe,CAAC;IACtE,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1D,CAAC;AAED,SAAgB,eAAe,CAAC,SAAkB;IAChD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3C,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;QAC1B,IAAI,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAQ,CAAsB,CAAC,EAAE,KAAK,QAAQ,EAAE,CAAC;YACjF,MAAM,EAAE,GAAG,MAAM,CAAE,CAAoB,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YACnD,IAAI,EAAE;gBAAE,OAAO,EAAE,CAAC;QACpB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,sFAAsF;AACtF,SAAgB,wBAAwB,CAAC,GAAY;IACnD,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAClD,MAAM,CAAC,GAAG,GAAoD,CAAC;IAC/D,IAAI,CAAC,CAAC,IAAI,KAAK,oBAAoB;QAAE,OAAO,IAAI,CAAC;IACjD,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,oBAAoB;QAAE,OAAO,IAAI,CAAC;IAChE,OAAO,KAAK,CAAC;AACf,CAAC;AAEM,KAAK,UAAU,wBAAwB,CAC5C,MAAiD,EACjD,IAAoE,EACpE,SAAkB;IAElB,MAAM,QAAQ,GAAG,MAAM,CAAC,cAAc,IAAI,MAAM,CAAC,cAAc,CAAC,iBAAiB,CAAC;IAClF,IAAI,OAAO,QAAQ,KAAK,UAAU;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACxD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC;YACzB,UAAU,EAAE,eAAe,CAAC,SAAS,CAAC,IAAI,eAAe,CAAC,IAAI,CAAC,SAAS,CAAC;YACzE,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,IAAI,EAAE,IAAI,CAAC,SAAS;SACrB,CAAC,CAAC;QACH,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,KAAK,EAAE;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;QACnD,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC;QAClD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC;IACvB,CAAC;AACH,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EACV,WAAW,EAAE,YAAY,EACT,kBAAkB,EAAE,cAAc,EAEnD,MAAM,YAAY,CAAC;AAYpB,OAAO,EAIL,KAAK,oBAAoB,EAC1B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAGL,KAAK,2BAA2B,EACjC,MAAM,wBAAwB,CAAC;AA+ZhC;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,IAAI,EAAE,kBAAkB,EACxB,cAAc,EAAE,cAAc,CAAC,CAAC,CAAC,EACjC,MAAM,EAAE,WAAW,EACnB,WAAW,CAAC,EAAE,oBAAoB,GAAG,2BAA2B,GAC/D,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CA+Y1B"}
1
+ {"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../src/guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EACV,WAAW,EAAE,YAAY,EACT,kBAAkB,EAAE,cAAc,EAEnD,MAAM,YAAY,CAAC;AAYpB,OAAO,EAIL,KAAK,oBAAoB,EAC1B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAGL,KAAK,2BAA2B,EACjC,MAAM,wBAAwB,CAAC;AAgchC;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,IAAI,EAAE,kBAAkB,EACxB,cAAc,EAAE,cAAc,CAAC,CAAC,CAAC,EACjC,MAAM,EAAE,WAAW,EACnB,WAAW,CAAC,EAAE,oBAAoB,GAAG,2BAA2B,GAC/D,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAqc1B"}
package/dist/cjs/guard.js CHANGED
@@ -25,6 +25,7 @@ const cas_attestation_js_1 = require("./cas-attestation.js");
25
25
  const monitoring_delivery_js_1 = require("./monitoring-delivery.js");
26
26
  const monitoring_attestation_js_1 = require("./monitoring-attestation.js");
27
27
  const policy_js_1 = require("./policy.js");
28
+ const execution_grant_js_1 = require("./execution-grant.js");
28
29
  // Per-config breaker state (time-window; not consecutive).
29
30
  const breakers = new WeakMap();
30
31
  const nowMs = () => Date.now();
@@ -83,7 +84,12 @@ function breakerTripped(config) {
83
84
  s.fails = s.fails.filter((x) => t - x < win);
84
85
  return s.fails.length >= (config.maxUnavailablePerWindow ?? 3);
85
86
  }
86
- function classifyError(err, config) {
87
+ function requestAsksForGrant(request) {
88
+ return !!(request
89
+ && typeof request === 'object'
90
+ && request.include_execution_grant === true);
91
+ }
92
+ function classifyError(err, config, grantRequested = false) {
87
93
  const e = err;
88
94
  const name = e?.name;
89
95
  const status = e?.status ?? e?.body?.status;
@@ -97,6 +103,11 @@ function classifyError(err, config) {
97
103
  return { cause: 'REQUEST_REJECTED', integrity: true };
98
104
  if (status === 400 || status === 401 || status === 409)
99
105
  return { cause: 'REQUEST_REJECTED', integrity: true };
106
+ // SIGNER_UNAVAILABLE is grant-path only. Gating keeps grant-OFF 5xx as SERVER_ERROR (9.5.0).
107
+ // Naked 503 while THIS request asked for a grant is the signer-off class, not availability.
108
+ if (grantRequested && ((0, execution_grant_js_1.isSignerUnavailableError)(err) || status === 503)) {
109
+ return { cause: 'SIGNER_UNAVAILABLE', integrity: true };
110
+ }
100
111
  if (typeof status === 'number' && status >= 500)
101
112
  return { cause: 'SERVER_ERROR', integrity: false };
102
113
  if (name === 'TypeError' || /fetch failed|network|ENOTFOUND|ECONNREFUSED|EAI_AGAIN/i.test(String(e?.message)))
@@ -130,7 +141,7 @@ async function preflightWithRetry(config, request) {
130
141
  return { ok: true, response };
131
142
  }
132
143
  catch (err) {
133
- last = classifyError(err, config);
144
+ last = classifyError(err, config, requestAsksForGrant(request));
134
145
  if (last.integrity)
135
146
  return { ok: false, ...last }; // integrity: never retry
136
147
  }
@@ -167,30 +178,34 @@ async function verifyEnvelope(config, envelope) {
167
178
  }
168
179
  // ── Outcome builders (keep the discriminated union satisfied) ────────────────────
169
180
  // Every path attaches proof + freshness + conditional_write bases (guard-produced only).
170
- async function runEnforced(config, factory, approved, redacted, freshness, conditional_write, monitoring_delivery, monitoring_attestation) {
181
+ async function runEnforced(config, factory, approved, redacted, freshness, conditional_write, monitoring_delivery, monitoring_attestation, grantCtx, grantObs) {
171
182
  emit(config, { type: 'execution_started', at: iso(), action: approved.action, decisionId: approved.envelope.decision_id });
172
183
  try {
173
- const result = await factory(approved.envelope, redacted);
184
+ const result = grantCtx
185
+ ? await factory(approved.envelope, redacted, grantCtx)
186
+ : await factory(approved.envelope, redacted);
174
187
  const base = { executionAttempted: true, executed: true, enforced: true, result, verdict: approved, preflighted: true };
175
- return finishExecuted(config, base, freshness, conditional_write, redacted, result, monitoring_delivery, monitoring_attestation);
188
+ return finishExecuted(config, base, freshness, conditional_write, redacted, result, monitoring_delivery, monitoring_attestation, grantObs);
176
189
  }
177
190
  catch (error) {
178
191
  emit(config, { type: 'factory_error', at: iso(), action: approved.action });
179
192
  const base = { executionAttempted: true, executed: false, enforced: true, error, verdict: approved, preflighted: true };
180
- return finishExecuted(config, base, freshness, conditional_write, redacted, undefined, monitoring_delivery, monitoring_attestation);
193
+ return finishExecuted(config, base, freshness, conditional_write, redacted, undefined, monitoring_delivery, monitoring_attestation, grantObs);
181
194
  }
182
195
  }
183
- async function runUnenforced(config, factory, envelope, verdict, preflighted, redacted, freshness, conditional_write, monitoring_delivery, monitoring_attestation) {
196
+ async function runUnenforced(config, factory, envelope, verdict, preflighted, redacted, freshness, conditional_write, monitoring_delivery, monitoring_attestation, grantCtx, grantObs) {
184
197
  emit(config, { type: 'execution_started', at: iso() });
185
198
  try {
186
- const result = await factory(envelope, redacted);
199
+ const result = grantCtx
200
+ ? await factory(envelope, redacted, grantCtx)
201
+ : await factory(envelope, redacted);
187
202
  const base = { executionAttempted: true, executed: true, enforced: false, result, verdict, preflighted };
188
- return finishExecuted(config, base, freshness, conditional_write, redacted, result, monitoring_delivery, monitoring_attestation);
203
+ return finishExecuted(config, base, freshness, conditional_write, redacted, result, monitoring_delivery, monitoring_attestation, grantObs);
189
204
  }
190
205
  catch (error) {
191
206
  emit(config, { type: 'factory_error', at: iso() });
192
207
  const base = { executionAttempted: true, executed: false, enforced: false, error, verdict, preflighted };
193
- return finishExecuted(config, base, freshness, conditional_write, redacted, undefined, monitoring_delivery, monitoring_attestation);
208
+ return finishExecuted(config, base, freshness, conditional_write, redacted, undefined, monitoring_delivery, monitoring_attestation, grantObs);
194
209
  }
195
210
  }
196
211
  function attachPolicyPresence(outcome, config) {
@@ -207,7 +222,7 @@ function coverageSnap(config) {
207
222
  function coverageOutcomeFieldsFrom(s) {
208
223
  return s.coverageObserved ? { coverage_observed: s.coverageObserved } : {};
209
224
  }
210
- function blocked(config, verdict, preflighted, freshness, conditional_write, monitoring_delivery, monitoring_attestation) {
225
+ function blocked(config, verdict, preflighted, freshness, conditional_write, monitoring_delivery, monitoring_attestation, grantObs) {
211
226
  const commit_observation = {
212
227
  status: 'not_observed', observed_at: iso(), host_attestation: 'absent',
213
228
  };
@@ -229,10 +244,11 @@ function blocked(config, verdict, preflighted, freshness, conditional_write, mon
229
244
  ...(monitoring_delivery ? { monitoring_delivery } : {}),
230
245
  ...(monitoring_attestation ? { monitoring_attestation } : {}),
231
246
  ...coverageOutcomeFieldsFrom(cov),
247
+ ...(grantObs ? { execution_grant: grantObs } : {}),
232
248
  };
233
249
  return attachPolicyPresence(out, config);
234
250
  }
235
- async function finishExecuted(config, base, freshness, conditional_write, redacted, result, monitoring_delivery, monitoring_attestation) {
251
+ async function finishExecuted(config, base, freshness, conditional_write, redacted, result, monitoring_delivery, monitoring_attestation, grantObs) {
236
252
  const enabled = config.requireCommitObservation !== false;
237
253
  const commit_observation = await (0, commit_observation_js_1.observeCommit)({
238
254
  enabled,
@@ -307,6 +323,7 @@ async function finishExecuted(config, base, freshness, conditional_write, redact
307
323
  : {}),
308
324
  } : {}),
309
325
  ...coverageOutcomeFieldsFrom(cov),
326
+ ...(grantObs ? { execution_grant: grantObs } : {}),
310
327
  };
311
328
  return attachPolicyPresence(out, config);
312
329
  }
@@ -459,17 +476,33 @@ async function guardToolCall(call, executeFactory, config, callContext) {
459
476
  // previous_receipt: host-supplied only (GuardConfig.previousReceipt). Never hardcoded-undefined
460
477
  // forever — when the host provides a prior token (string or getter), thread it so the issuer can
461
478
  // hash it into the signed `prev` slot. The guard does not retain or advance the value.
479
+ //
480
+ // executionGrant (9.6.0): per-invocation locals — never stored on GuardConfig (shared across
481
+ // overlapping calls). Default OFF is this object without include_execution_grant (9.5.0 shape).
462
482
  const request = {
463
483
  artifacts: detection.artifacts,
464
484
  context: { operation: config.operation ?? 'tool_call', environment: config.environment, audience: config.audience },
465
485
  previous_receipt: resolvePreviousReceipt(config),
466
486
  idempotency_key: undefined,
467
487
  };
488
+ let grantObs;
489
+ let grantForCall = null;
490
+ if ((0, execution_grant_js_1.isExecutionGrantEnabled)(config)) {
491
+ grantObs = { requested: true, arrived: false };
492
+ const nonceRes = await (0, execution_grant_js_1.resolveStateNonceForCall)(config, redacted, detection.artifacts);
493
+ if (!nonceRes.ok) {
494
+ breakerRecord(config);
495
+ return closedIntegrity(config, 'EXECUTION_GRANT_NONCE_UNRESOLVABLE', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
496
+ }
497
+ request.include_execution_grant = true;
498
+ if (nonceRes.nonce)
499
+ request.state_nonce = nonceRes.nonce;
500
+ }
468
501
  // request-attributable payload cap => PAYLOAD_TOO_LARGE (integrity) => closed.
469
502
  const cap = config.maxPayloadBytes ?? 1_000_000;
470
503
  if (Buffer.byteLength(JSON.stringify(request), 'utf8') > cap) {
471
504
  breakerRecord(config);
472
- return closedIntegrity(config, 'PAYLOAD_TOO_LARGE', failPolicy, fctx, cwctx, redacted, detection.artifacts);
505
+ return closedIntegrity(config, 'PAYLOAD_TOO_LARGE', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
473
506
  }
474
507
  emit(config, { type: 'preflight_start', at: iso() });
475
508
  const pf = await preflightWithRetry(config, request);
@@ -482,14 +515,22 @@ async function guardToolCall(call, executeFactory, config, callContext) {
482
515
  if (pf.integrity) {
483
516
  emit(config, { type: 'breaker_tripped', at: iso(), cause: pf.cause });
484
517
  const v = unavailableVerdict({ cause: pf.cause, failPolicy, resolution: 'CLOSED', action: 'STOP' }, count);
485
- return blocked(config, v, false, basis, cw);
518
+ return blocked(config, v, false, basis, cw, undefined, undefined, grantObs);
486
519
  }
487
520
  // availability
488
521
  const availCause = pf.cause;
522
+ // A grant was requested for THIS call: never OPEN_PASSTHROUGH / LKG-execute without a token.
523
+ // failPolicy 'open' remains the 9.5.0 opt-in only when the grant path is off.
524
+ if (grantObs && grantObs.requested) {
525
+ if (breakerTripped(config))
526
+ emit(config, { type: 'breaker_tripped', at: iso(), cause: availCause });
527
+ const v = unavailableVerdict({ cause: availCause, failPolicy, resolution: 'CLOSED', action: 'STOP' }, count);
528
+ return blocked(config, v, false, basis, cw, undefined, undefined, grantObs);
529
+ }
489
530
  if (failPolicy === 'open' && !breakerTripped(config)) {
490
531
  emit(config, { type: 'preflight_unavailable', at: iso(), cause: availCause, action: 'CONTINUE' });
491
532
  const v = unavailableVerdict({ cause: availCause, failPolicy: 'open', resolution: 'OPEN_PASSTHROUGH', action: 'CONTINUE' }, count);
492
- return runUnenforced(config, executeFactory, null, v, false, redacted, basis, cw);
533
+ return runUnenforced(config, executeFactory, null, v, false, redacted, basis, cw, undefined, undefined, undefined, grantObs);
493
534
  }
494
535
  if (failPolicy === 'lkg') {
495
536
  const lkg = await tryLkg(config, inputFp);
@@ -503,31 +544,31 @@ async function guardToolCall(call, executeFactory, config, callContext) {
503
544
  if (breakerTripped(config))
504
545
  emit(config, { type: 'breaker_tripped', at: iso(), cause: availCause });
505
546
  const v = unavailableVerdict({ cause: availCause, failPolicy, resolution: 'CLOSED', action: 'STOP' }, count);
506
- return blocked(config, v, false, basis, cw);
547
+ return blocked(config, v, false, basis, cw, undefined, undefined, grantObs);
507
548
  }
508
549
  // We have a response. Read the decision (envelope-first) and verify the receipt.
509
550
  const rd = (0, read_decision_js_1.readDecision)(pf.response);
510
551
  // PRESENT but unrecognised action — halt with its own code before any decision map or reconcile.
511
552
  if (rd.reason === 'EXECUTION_ACTION_UNRECOGNISED') {
512
553
  breakerRecord(config);
513
- return closedIntegrity(config, 'EXECUTION_ACTION_UNRECOGNISED', failPolicy, fctx, cwctx, redacted, detection.artifacts);
554
+ return closedIntegrity(config, 'EXECUTION_ACTION_UNRECOGNISED', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
514
555
  }
515
556
  // Missing EA on v2 / non-legacy-1.0 — reuse closedIntegrity halt arm, cause UNREADABLE_DECISION.
516
557
  // executed:false, enforced:false (blocked()). Never remap to CONTINUE.
517
558
  if (rd.reason === 'UNREADABLE_DECISION') {
518
559
  breakerRecord(config);
519
- return closedIntegrity(config, 'UNREADABLE_DECISION', failPolicy, fctx, cwctx, redacted, detection.artifacts);
560
+ return closedIntegrity(config, 'UNREADABLE_DECISION', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
520
561
  }
521
562
  if (!rd.envelope) {
522
563
  // Known action but no envelope => integrity => closed.
523
564
  breakerRecord(config);
524
- return closedIntegrity(config, 'SCHEMA_INVALID', failPolicy, fctx, cwctx, redacted, detection.artifacts);
565
+ return closedIntegrity(config, 'SCHEMA_INVALID', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
525
566
  }
526
567
  const envelope = rd.envelope;
527
568
  // Past unrecognised early-return: action is a closed ExecutionAction (or we treat non-closed as integrity).
528
569
  if (!(0, read_decision_js_1.isClosedAction)(rd.executionAction)) {
529
570
  breakerRecord(config);
530
- return closedIntegrity(config, 'EXECUTION_ACTION_UNRECOGNISED', failPolicy, fctx, cwctx, redacted, detection.artifacts);
571
+ return closedIntegrity(config, 'EXECUTION_ACTION_UNRECOGNISED', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
531
572
  }
532
573
  const closedAction = rd.executionAction;
533
574
  // 5. expiry check before honoring any decision.
@@ -544,7 +585,7 @@ async function guardToolCall(call, executeFactory, config, callContext) {
544
585
  // signature. Both trip the breaker and STOP — the guard NEVER executes off an unbound receipt.
545
586
  if (config.verifyReceipts !== false && !receiptVerified && envelope.receipt?.token) {
546
587
  breakerRecord(config);
547
- return closedIntegrity(config, bindResult.cause ?? 'RECEIPT_UNVERIFIED', failPolicy, fctx, cwctx, redacted, detection.artifacts);
588
+ return closedIntegrity(config, bindResult.cause ?? 'RECEIPT_UNVERIFIED', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
548
589
  }
549
590
  // ── Client-side authorization gate (P0-b/c): mirror §106/§111/§115 before honoring any action. ──
550
591
  // Threat model (bounded): without a receipt, fields including execution_action are not
@@ -557,21 +598,34 @@ async function guardToolCall(call, executeFactory, config, callContext) {
557
598
  const gate = (0, enforcement_gate_js_1.evaluateEnvelope)(pf.response, envelope, closedAction, detection.artifacts);
558
599
  if (gate.verdict === 'fail-closed') {
559
600
  breakerRecord(config);
560
- return closedIntegrity(config, gate.cause, failPolicy, fctx, cwctx, redacted, detection.artifacts);
601
+ return closedIntegrity(config, gate.cause, failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
561
602
  }
562
603
  if (gate.verdict === 'block-strict') {
563
604
  // A real (or stricter-reconciled) BLOCK / REQUIRE_APPROVAL — clean block; the factory never runs.
564
605
  const { basis } = freshnessFor(config, redacted, fctx, detection.artifacts);
565
606
  const { basis: cw } = conditionalWriteFor(config, redacted, cwctx);
566
607
  return gate.decision === 'BLOCK'
567
- ? blocked(config, { kind: 'BLOCK', action: 'STOP', envelope, receiptVerified }, true, basis, cw)
568
- : blocked(config, { kind: 'APPROVAL', action: 'REQUEST_APPROVAL', envelope, receiptVerified }, true, basis, cw);
608
+ ? blocked(config, { kind: 'BLOCK', action: 'STOP', envelope, receiptVerified }, true, basis, cw, undefined, undefined, grantObs)
609
+ : blocked(config, { kind: 'APPROVAL', action: 'REQUEST_APPROVAL', envelope, receiptVerified }, true, basis, cw, undefined, undefined, grantObs);
569
610
  }
570
611
  const kind = gate.kind; // 'ALLOW' | 'MONITOR' — allow-class, safe, non-degraded, artifact-bound.
612
+ // Native grant: allow-class authorize that requested a grant must RECEIVE one.
613
+ // BLOCK/RA never mint a grant — missing token there is not EXECUTION_GRANT_MISSING.
614
+ if (grantObs && grantObs.requested) {
615
+ grantForCall = (0, execution_grant_js_1.readExecutionGrantToken)(pf.response);
616
+ grantObs = { requested: true, arrived: !!grantForCall };
617
+ if (!grantForCall) {
618
+ breakerRecord(config);
619
+ return closedIntegrity(config, 'EXECUTION_GRANT_MISSING', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
620
+ }
621
+ }
622
+ const grantCtx = grantObs
623
+ ? { execution_grant: grantForCall }
624
+ : undefined;
571
625
  // An expired decision cannot be honored fresh => closed (integrity: wrong-time state).
572
626
  if (expired) {
573
627
  breakerRecord(config);
574
- return closedIntegrity(config, 'SCHEMA_INVALID', failPolicy, fctx, cwctx, redacted, detection.artifacts);
628
+ return closedIntegrity(config, 'SCHEMA_INVALID', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
575
629
  }
576
630
  // MONITOR gate: host must ASSERT monitoring is wired (monitoringSinkWired === true) AND supply
577
631
  // onEvent. Presence of a function alone is not enough — () => {} used to unlock this gate while
@@ -625,20 +679,20 @@ async function guardToolCall(call, executeFactory, config, callContext) {
625
679
  });
626
680
  if (sinkWired && monitoringDelivery.status === 'not_delivered' && (0, monitoring_delivery_js_1.monitoringDeliveryFailClosed)(config)) {
627
681
  breakerRecord(config);
628
- return closedIntegrity(config, 'MONITORING_UNWIRED', failPolicy, fctx, cwctx, redacted, detection.artifacts, monitoringDelivery, monitoringAttestation);
682
+ return closedIntegrity(config, 'MONITORING_UNWIRED', failPolicy, fctx, cwctx, redacted, detection.artifacts, monitoringDelivery, monitoringAttestation, grantObs);
629
683
  }
630
684
  }
631
685
  // Freshness re-check immediately before any execution (ACTIVE assessment uses preflight befores).
632
686
  const { basis: freshBasis, blockCause: freshBlock } = freshnessFor(config, redacted, fctx, detection.artifacts);
633
687
  if (freshBlock === 'FRESHNESS_REQUIRED' || freshBlock === 'FRESHNESS_FAILED') {
634
688
  breakerRecord(config);
635
- return closedIntegrity(config, freshBlock, failPolicy, fctx, cwctx, redacted, detection.artifacts);
689
+ return closedIntegrity(config, freshBlock, failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
636
690
  }
637
691
  // Conditional-write re-check immediately before enforced execution (same conjunct class as freshness).
638
692
  const { basis: cwBasis, blockCause: cwBlock } = conditionalWriteFor(config, redacted, cwctx);
639
693
  if (cwBlock === 'CONDITIONAL_WRITE_REQUIRED') {
640
694
  breakerRecord(config);
641
- return closedIntegrity(config, cwBlock, failPolicy, fctx, cwctx, redacted, detection.artifacts);
695
+ return closedIntegrity(config, cwBlock, failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
642
696
  }
643
697
  // observeOnly is an EXPLICIT report-only opt-in: execute but never enforce (the one sanctioned
644
698
  // unenforced-execution mode alongside failPolicy=open; both are documented opt-ins, not the default).
@@ -647,14 +701,14 @@ async function guardToolCall(call, executeFactory, config, callContext) {
647
701
  const verdict = kind === 'ALLOW'
648
702
  ? { kind: 'ALLOW', action: 'CONTINUE', envelope, receiptVerified }
649
703
  : { kind: 'MONITOR', action: 'CONTINUE_WITH_MONITORING', envelope, receiptVerified };
650
- return runUnenforced(config, executeFactory, envelope, verdict, true, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation);
704
+ return runUnenforced(config, executeFactory, envelope, verdict, true, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation, grantCtx, grantObs);
651
705
  }
652
706
  // Advisory CWM (sink WAS wired): delivery failed but failPolicy/observeOnly said not to block.
653
707
  // Proceed unenforced with the reason visible. Unwired MONITOR still falls through to
654
708
  // MONITORING_UNWIRED (CE-CC-04). ENFORCING not_delivered already returned closedIntegrity.
655
709
  if (kind === 'MONITOR' && sinkWired && monitoringDelivery && monitoringDelivery.status === 'not_delivered') {
656
710
  const degraded = { kind: 'MONITOR', action: 'CONTINUE_WITH_MONITORING', envelope, receiptVerified };
657
- return runUnenforced(config, executeFactory, envelope, degraded, true, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation);
711
+ return runUnenforced(config, executeFactory, envelope, degraded, true, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation, grantCtx, grantObs);
658
712
  }
659
713
  // ── enforced ⟺ executed INVARIANT (contract-triggering path): execute ONLY when we can ENFORCE. ──
660
714
  // enforceable = a bound-verified receipt AND (MONITOR) a wired sink. Anything else FAILS CLOSED —
@@ -683,7 +737,7 @@ async function guardToolCall(call, executeFactory, config, callContext) {
683
737
  const offVerdict = kind === 'ALLOW'
684
738
  ? { kind: 'ALLOW', action: 'CONTINUE', envelope, receiptVerified }
685
739
  : { kind: 'MONITOR', action: 'CONTINUE_WITH_MONITORING', envelope, receiptVerified };
686
- return runUnenforced(config, executeFactory, envelope, offVerdict, true, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation);
740
+ return runUnenforced(config, executeFactory, envelope, offVerdict, true, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation, grantCtx, grantObs);
687
741
  }
688
742
  const et = (0, execution_time_fingerprint_js_1.checkExecutionTimeFingerprint)({
689
743
  artifacts: detection.artifacts,
@@ -697,7 +751,7 @@ async function guardToolCall(call, executeFactory, config, callContext) {
697
751
  const unmeasurable = (0, execution_time_fingerprint_js_1.isUnmeasurableExecutionStateReason)(et.reason);
698
752
  if (execStateMode === true) {
699
753
  breakerRecord(config);
700
- return closedIntegrity(config, unmeasurable ? 'EXECUTION_STATE_UNMEASURABLE' : 'EXECUTION_STATE_DRIFT', failPolicy, fctx, cwctx, redacted, detection.artifacts);
754
+ return closedIntegrity(config, unmeasurable ? 'EXECUTION_STATE_UNMEASURABLE' : 'EXECUTION_STATE_DRIFT', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
701
755
  }
702
756
  // warn opt-down: emit, then run unenforced — a measured mismatch is not an enforced run.
703
757
  if (unmeasurable) {
@@ -724,23 +778,23 @@ async function guardToolCall(call, executeFactory, config, callContext) {
724
778
  const warnVerdict = kind === 'ALLOW'
725
779
  ? { kind: 'ALLOW', action: 'CONTINUE', envelope, receiptVerified }
726
780
  : { kind: 'MONITOR', action: 'CONTINUE_WITH_MONITORING', envelope, receiptVerified };
727
- return runUnenforced(config, executeFactory, envelope, warnVerdict, true, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation);
781
+ return runUnenforced(config, executeFactory, envelope, warnVerdict, true, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation, grantCtx, grantObs);
728
782
  }
729
783
  const approved = kind === 'ALLOW'
730
784
  ? { kind: 'ALLOW', action: 'CONTINUE', envelope, receiptVerified: true }
731
785
  : { kind: 'MONITOR', action: 'CONTINUE_WITH_MONITORING', envelope, receiptVerified: true };
732
- return runEnforced(config, executeFactory, approved, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation);
786
+ return runEnforced(config, executeFactory, approved, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation, grantCtx, grantObs);
733
787
  }
734
788
  breakerRecord(config);
735
- return closedIntegrity(config, receiptVerified ? 'MONITORING_UNWIRED' : 'RECEIPT_MISSING', failPolicy, fctx, cwctx, redacted, detection.artifacts, monitoringDelivery, monitoringAttestation);
789
+ return closedIntegrity(config, receiptVerified ? 'MONITORING_UNWIRED' : 'RECEIPT_MISSING', failPolicy, fctx, cwctx, redacted, detection.artifacts, monitoringDelivery, monitoringAttestation, grantObs);
736
790
  }
737
- function closedIntegrity(config, cause, failPolicy, fctx, cwctx, redacted, arts, monitoring_delivery, monitoring_attestation) {
791
+ function closedIntegrity(config, cause, failPolicy, fctx, cwctx, redacted, arts, monitoring_delivery, monitoring_attestation, grantObs) {
738
792
  const count = (breakers.get(config)?.fails.length) ?? 1;
739
793
  emit(config, { type: 'breaker_tripped', at: iso(), cause });
740
794
  const v = unavailableVerdict({ cause, failPolicy, resolution: 'CLOSED', action: 'STOP' }, count);
741
795
  const { basis } = freshnessFor(config, redacted, fctx, arts);
742
796
  const { basis: cw } = conditionalWriteFor(config, redacted, cwctx);
743
- return blocked(config, v, false, basis, cw, monitoring_delivery, monitoring_attestation);
797
+ return blocked(config, v, false, basis, cw, monitoring_delivery, monitoring_attestation, grantObs);
744
798
  }
745
799
  function isExpired(envelope) {
746
800
  const exp = envelope.expires_at;