@coderifts/agent-guard 9.5.0 → 9.7.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 (60) hide show
  1. package/README.md +54 -5
  2. package/dist/cjs/coverage-attestation.d.ts +27 -0
  3. package/dist/cjs/coverage-attestation.d.ts.map +1 -0
  4. package/dist/cjs/coverage-attestation.js +146 -0
  5. package/dist/cjs/coverage-attestation.js.map +1 -0
  6. package/dist/cjs/execution-grant.d.ts +41 -0
  7. package/dist/cjs/execution-grant.d.ts.map +1 -0
  8. package/dist/cjs/execution-grant.js +66 -0
  9. package/dist/cjs/execution-grant.js.map +1 -0
  10. package/dist/cjs/guard.d.ts.map +1 -1
  11. package/dist/cjs/guard.js +91 -37
  12. package/dist/cjs/guard.js.map +1 -1
  13. package/dist/cjs/index.d.ts +4 -0
  14. package/dist/cjs/index.d.ts.map +1 -1
  15. package/dist/cjs/index.js +13 -3
  16. package/dist/cjs/index.js.map +1 -1
  17. package/dist/cjs/policy.d.ts +1 -1
  18. package/dist/cjs/policy.d.ts.map +1 -1
  19. package/dist/cjs/policy.js +2 -2
  20. package/dist/cjs/policy.js.map +1 -1
  21. package/dist/cjs/tool-registry.d.ts +6 -5
  22. package/dist/cjs/tool-registry.d.ts.map +1 -1
  23. package/dist/cjs/tool-registry.js +7 -6
  24. package/dist/cjs/tool-registry.js.map +1 -1
  25. package/dist/cjs/types.d.ts +22 -2
  26. package/dist/cjs/types.d.ts.map +1 -1
  27. package/dist/cjs/with-coderifts.d.ts +6 -0
  28. package/dist/cjs/with-coderifts.d.ts.map +1 -1
  29. package/dist/cjs/with-coderifts.js +3 -0
  30. package/dist/cjs/with-coderifts.js.map +1 -1
  31. package/dist/esm/coverage-attestation.d.ts +27 -0
  32. package/dist/esm/coverage-attestation.d.ts.map +1 -0
  33. package/dist/esm/coverage-attestation.js +141 -0
  34. package/dist/esm/coverage-attestation.js.map +1 -0
  35. package/dist/esm/execution-grant.d.ts +41 -0
  36. package/dist/esm/execution-grant.d.ts.map +1 -0
  37. package/dist/esm/execution-grant.js +59 -0
  38. package/dist/esm/execution-grant.js.map +1 -0
  39. package/dist/esm/guard.d.ts.map +1 -1
  40. package/dist/esm/guard.js +91 -37
  41. package/dist/esm/guard.js.map +1 -1
  42. package/dist/esm/index.d.ts +4 -0
  43. package/dist/esm/index.d.ts.map +1 -1
  44. package/dist/esm/index.js +2 -0
  45. package/dist/esm/index.js.map +1 -1
  46. package/dist/esm/policy.d.ts +1 -1
  47. package/dist/esm/policy.d.ts.map +1 -1
  48. package/dist/esm/policy.js +2 -2
  49. package/dist/esm/policy.js.map +1 -1
  50. package/dist/esm/tool-registry.d.ts +6 -5
  51. package/dist/esm/tool-registry.d.ts.map +1 -1
  52. package/dist/esm/tool-registry.js +7 -6
  53. package/dist/esm/tool-registry.js.map +1 -1
  54. package/dist/esm/types.d.ts +22 -2
  55. package/dist/esm/types.d.ts.map +1 -1
  56. package/dist/esm/with-coderifts.d.ts +6 -0
  57. package/dist/esm/with-coderifts.d.ts.map +1 -1
  58. package/dist/esm/with-coderifts.js +3 -0
  59. package/dist/esm/with-coderifts.js.map +1 -1
  60. package/package.json +1 -1
@@ -0,0 +1,141 @@
1
+ /**
2
+ * cr.coverage.attest.v1 issuance (guard side).
3
+ *
4
+ * Wire/signing input MIRRORS app src/verdict-core/coverage-attestation.js. The guard SIGNS via a
5
+ * host-provided sign(bytes) callback — never a raw key in config. CodeRifts never holds the
6
+ * coverage private key. Custody model copied from tryIssueMonitoringAttestation, deliberately:
7
+ * one custody story for every host-signed artifact, not a second one per envelope.
8
+ *
9
+ * Observation-side only. Signer throw/empty → omit the token; never unsigned.
10
+ *
11
+ * ── THE PAYLOAD DECISION, and it is the point of this module ────────────────────────────────
12
+ * A signed statement must not assert more than the guard measured.
13
+ *
14
+ * The guard's own class vocabulary is WIDER than the envelope's, deliberately:
15
+ * guard UNKNOWN_OUTSIDE_SCOPE | INCOMPLETE_OBSERVED | COMPLETE_OBSERVED
16
+ * envelope UNKNOWN_OUTSIDE_SCOPE | INCOMPLETE_OBSERVED (no COMPLETE, by design)
17
+ *
18
+ * Half B absent (host never reported total dispatches): the guard knows governed_calls and
19
+ * nothing else. The token carries UNKNOWN_OUTSIDE_SCOPE and OMITS total_calls entirely. It does
20
+ * not default the total to governed_calls, and it does not send zero — absence is not zero, and a
21
+ * signed zero would be a lie with a signature on it.
22
+ *
23
+ * Guard COMPLETE_OBSERVED maps to envelope INCOMPLETE_OBSERVED. Not a downgrade for its own sake:
24
+ * the envelope's two classes encode "totals known" vs "totals unknown", and the app kernel rejects
25
+ * a COMPLETE payload as malformed rather than coercing it, because from inside the process the
26
+ * ABSENCE of an ungoverned call is unprovable. The numbers carry the finding — an empty
27
+ * ungoverned_tools tells the reader everything the host reported was governed, without a signed
28
+ * claim of completeness that we cannot back.
29
+ */
30
+ 'use strict';
31
+ export const COVERAGE_ATTEST_VERSION = 'cr.coverage.attest.v1';
32
+ export const COVERAGE_ATTEST_SIGNING_PREFIX = 'crcovattest.v1';
33
+ export const COVERAGE_ATTEST_ENVELOPE_TAG = 'cr.coverage.attest.v1';
34
+ /** Envelope classes. Intentionally two — there is no COMPLETE to emit. */
35
+ const ENVELOPE_CLASSES = ['INCOMPLETE_OBSERVED', 'UNKNOWN_OUTSIDE_SCOPE'];
36
+ /** NUL joins the tool-name list inside one pipe field, so a name can never forge a field. */
37
+ const NUL = '\x1f';
38
+ function scalar(v) {
39
+ return v == null ? '' : String(v);
40
+ }
41
+ function canonicalUngoverned(list) {
42
+ return Array.isArray(list) ? list.map((s) => String(s)).join(NUL) : '';
43
+ }
44
+ export function coverageAttestSigningInput(body) {
45
+ const parts = [
46
+ COVERAGE_ATTEST_SIGNING_PREFIX,
47
+ scalar(body.kid),
48
+ scalar(body.session_id),
49
+ scalar(body.observed_class),
50
+ body.governed_calls != null ? String(body.governed_calls) : '',
51
+ body.total_calls != null ? String(body.total_calls) : '',
52
+ canonicalUngoverned(body.ungoverned_tools),
53
+ scalar(body.decision_id),
54
+ scalar(body.receipt_digest),
55
+ scalar(body.observed_at),
56
+ ];
57
+ return parts.join('|');
58
+ }
59
+ function b64url(buf) {
60
+ return buf.toString('base64url');
61
+ }
62
+ /** True when the host reported Half B (total dispatches), so a total may be asserted. */
63
+ function hasHalfB(cov) {
64
+ return cov.class !== 'UNKNOWN_OUTSIDE_SCOPE'
65
+ && typeof cov.total_calls === 'number';
66
+ }
67
+ /**
68
+ * Issue a cr.coverage.attest.v1 token from a coverage snapshot. Returns undefined when config is
69
+ * absent, the snapshot is unusable, or the host signer throws — never an unsigned token.
70
+ */
71
+ export async function tryIssueCoverageAttestation(args) {
72
+ const cfg = args.config;
73
+ if (!cfg || typeof cfg.kid !== 'string' || !cfg.kid || typeof cfg.signer !== 'function') {
74
+ return undefined;
75
+ }
76
+ if (typeof cfg.sessionId !== 'string' || cfg.sessionId.length === 0)
77
+ return undefined;
78
+ const cov = args.coverage;
79
+ if (!cov || typeof cov !== 'object')
80
+ return undefined;
81
+ if (typeof cov.governed_calls !== 'number' || !Number.isInteger(cov.governed_calls) || cov.governed_calls < 0) {
82
+ return undefined;
83
+ }
84
+ const halfB = hasHalfB(cov);
85
+ const observed_class = halfB ? 'INCOMPLETE_OBSERVED' : 'UNKNOWN_OUTSIDE_SCOPE';
86
+ if (!ENVELOPE_CLASSES.includes(observed_class))
87
+ return undefined;
88
+ const body = {
89
+ v: COVERAGE_ATTEST_VERSION,
90
+ kid: cfg.kid,
91
+ session_id: cfg.sessionId,
92
+ observed_class,
93
+ governed_calls: cov.governed_calls,
94
+ observed_at: args.now || new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),
95
+ };
96
+ if (halfB) {
97
+ const withTotals = cov;
98
+ // A statement claiming more governed than dispatched is not a coverage statement — the app
99
+ // kernel rejects it as malformed, so refuse to mint it rather than emit a token that fails.
100
+ if (!Number.isInteger(withTotals.total_calls) || withTotals.total_calls < cov.governed_calls) {
101
+ return undefined;
102
+ }
103
+ body.total_calls = withTotals.total_calls;
104
+ body.ungoverned_tools = Array.isArray(withTotals.ungoverned_tools)
105
+ ? withTotals.ungoverned_tools.map((s) => String(s))
106
+ : [];
107
+ }
108
+ // else: total_calls and ungoverned_tools are OMITTED. Not zero. Not defaulted.
109
+ if (typeof args.decisionId === 'string' && args.decisionId)
110
+ body.decision_id = args.decisionId;
111
+ if (typeof args.receiptDigest === 'string' && args.receiptDigest)
112
+ body.receipt_digest = args.receiptDigest;
113
+ const input = Buffer.from(coverageAttestSigningInput(body), 'utf8');
114
+ let sig;
115
+ try {
116
+ sig = await cfg.signer(input);
117
+ }
118
+ catch {
119
+ return undefined;
120
+ }
121
+ if (sig == null)
122
+ return undefined;
123
+ const sigBuf = Buffer.isBuffer(sig) ? sig : Buffer.from(sig);
124
+ if (sigBuf.length === 0)
125
+ return undefined;
126
+ return [
127
+ COVERAGE_ATTEST_ENVELOPE_TAG,
128
+ cfg.kid,
129
+ b64url(Buffer.from(JSON.stringify(body), 'utf8')),
130
+ b64url(sigBuf),
131
+ ].join('|');
132
+ }
133
+ export function kidFromCoverageAttestation(token) {
134
+ if (typeof token !== 'string' || token.length === 0)
135
+ return null;
136
+ const parts = token.split('|');
137
+ if (parts.length !== 4 || parts[0] !== COVERAGE_ATTEST_ENVELOPE_TAG)
138
+ return null;
139
+ return parts[1] || null;
140
+ }
141
+ //# sourceMappingURL=coverage-attestation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage-attestation.js","sourceRoot":"","sources":["../../src/coverage-attestation.ts"],"names":[],"mappings":"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"}
@@ -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,59 @@
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 function isExecutionGrantEnabled(config) {
8
+ return !!(config && config.executionGrant && config.executionGrant.enabled === true);
9
+ }
10
+ export function readExecutionGrantToken(response) {
11
+ if (!response || typeof response !== 'object')
12
+ return null;
13
+ const g = response.execution_grant;
14
+ return typeof g === 'string' && g.length > 0 ? g : null;
15
+ }
16
+ export function firstArtifactId(artifacts) {
17
+ if (!Array.isArray(artifacts))
18
+ return null;
19
+ for (const a of artifacts) {
20
+ if (a && typeof a === 'object' && typeof a.id === 'string') {
21
+ const id = String(a.id).trim();
22
+ if (id)
23
+ return id;
24
+ }
25
+ }
26
+ return null;
27
+ }
28
+ /** True when the thrown authorize error carries the named SIGNER_UNAVAILABLE code. */
29
+ export function isSignerUnavailableError(err) {
30
+ if (!err || typeof err !== 'object')
31
+ return false;
32
+ const e = err;
33
+ if (e.code === 'SIGNER_UNAVAILABLE')
34
+ return true;
35
+ if (e.body && e.body.code === 'SIGNER_UNAVAILABLE')
36
+ return true;
37
+ return false;
38
+ }
39
+ export async function resolveStateNonceForCall(config, call, artifacts) {
40
+ const resolver = config.executionGrant && config.executionGrant.resolveStateNonce;
41
+ if (typeof resolver !== 'function')
42
+ return { ok: true };
43
+ try {
44
+ const raw = await resolver({
45
+ artifactId: firstArtifactId(artifacts) || firstArtifactId(call.artifacts),
46
+ toolName: call.toolName,
47
+ args: call.arguments,
48
+ });
49
+ if (raw == null || raw === '')
50
+ return { ok: true };
51
+ if (typeof raw !== 'string')
52
+ return { ok: false };
53
+ return { ok: true, nonce: raw };
54
+ }
55
+ catch {
56
+ return { ok: false };
57
+ }
58
+ }
59
+ //# 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,MAAM,UAAU,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,MAAM,UAAU,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,MAAM,UAAU,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,MAAM,UAAU,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;AAED,MAAM,CAAC,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/esm/guard.js CHANGED
@@ -22,6 +22,7 @@ import { buildCasAttestation, evaluateCasEvidence, isExecuteIfUnchangedOutcome,
22
22
  import { deliverMonitoring, monitoringDeliveryFailClosed, } from './monitoring-delivery.js';
23
23
  import { tryIssueMonitoringAttestation } from './monitoring-attestation.js';
24
24
  import { observePolicyPresence } from './policy.js';
25
+ import { isExecutionGrantEnabled, isSignerUnavailableError, readExecutionGrantToken, resolveStateNonceForCall, } from './execution-grant.js';
25
26
  // Per-config breaker state (time-window; not consecutive).
26
27
  const breakers = new WeakMap();
27
28
  const nowMs = () => Date.now();
@@ -80,7 +81,12 @@ function breakerTripped(config) {
80
81
  s.fails = s.fails.filter((x) => t - x < win);
81
82
  return s.fails.length >= (config.maxUnavailablePerWindow ?? 3);
82
83
  }
83
- function classifyError(err, config) {
84
+ function requestAsksForGrant(request) {
85
+ return !!(request
86
+ && typeof request === 'object'
87
+ && request.include_execution_grant === true);
88
+ }
89
+ function classifyError(err, config, grantRequested = false) {
84
90
  const e = err;
85
91
  const name = e?.name;
86
92
  const status = e?.status ?? e?.body?.status;
@@ -94,6 +100,11 @@ function classifyError(err, config) {
94
100
  return { cause: 'REQUEST_REJECTED', integrity: true };
95
101
  if (status === 400 || status === 401 || status === 409)
96
102
  return { cause: 'REQUEST_REJECTED', integrity: true };
103
+ // SIGNER_UNAVAILABLE is grant-path only. Gating keeps grant-OFF 5xx as SERVER_ERROR (9.5.0).
104
+ // Naked 503 while THIS request asked for a grant is the signer-off class, not availability.
105
+ if (grantRequested && (isSignerUnavailableError(err) || status === 503)) {
106
+ return { cause: 'SIGNER_UNAVAILABLE', integrity: true };
107
+ }
97
108
  if (typeof status === 'number' && status >= 500)
98
109
  return { cause: 'SERVER_ERROR', integrity: false };
99
110
  if (name === 'TypeError' || /fetch failed|network|ENOTFOUND|ECONNREFUSED|EAI_AGAIN/i.test(String(e?.message)))
@@ -127,7 +138,7 @@ async function preflightWithRetry(config, request) {
127
138
  return { ok: true, response };
128
139
  }
129
140
  catch (err) {
130
- last = classifyError(err, config);
141
+ last = classifyError(err, config, requestAsksForGrant(request));
131
142
  if (last.integrity)
132
143
  return { ok: false, ...last }; // integrity: never retry
133
144
  }
@@ -164,30 +175,34 @@ async function verifyEnvelope(config, envelope) {
164
175
  }
165
176
  // ── Outcome builders (keep the discriminated union satisfied) ────────────────────
166
177
  // Every path attaches proof + freshness + conditional_write bases (guard-produced only).
167
- async function runEnforced(config, factory, approved, redacted, freshness, conditional_write, monitoring_delivery, monitoring_attestation) {
178
+ async function runEnforced(config, factory, approved, redacted, freshness, conditional_write, monitoring_delivery, monitoring_attestation, grantCtx, grantObs) {
168
179
  emit(config, { type: 'execution_started', at: iso(), action: approved.action, decisionId: approved.envelope.decision_id });
169
180
  try {
170
- const result = await factory(approved.envelope, redacted);
181
+ const result = grantCtx
182
+ ? await factory(approved.envelope, redacted, grantCtx)
183
+ : await factory(approved.envelope, redacted);
171
184
  const base = { executionAttempted: true, executed: true, enforced: true, result, verdict: approved, preflighted: true };
172
- return finishExecuted(config, base, freshness, conditional_write, redacted, result, monitoring_delivery, monitoring_attestation);
185
+ return finishExecuted(config, base, freshness, conditional_write, redacted, result, monitoring_delivery, monitoring_attestation, grantObs);
173
186
  }
174
187
  catch (error) {
175
188
  emit(config, { type: 'factory_error', at: iso(), action: approved.action });
176
189
  const base = { executionAttempted: true, executed: false, enforced: true, error, verdict: approved, preflighted: true };
177
- return finishExecuted(config, base, freshness, conditional_write, redacted, undefined, monitoring_delivery, monitoring_attestation);
190
+ return finishExecuted(config, base, freshness, conditional_write, redacted, undefined, monitoring_delivery, monitoring_attestation, grantObs);
178
191
  }
179
192
  }
180
- async function runUnenforced(config, factory, envelope, verdict, preflighted, redacted, freshness, conditional_write, monitoring_delivery, monitoring_attestation) {
193
+ async function runUnenforced(config, factory, envelope, verdict, preflighted, redacted, freshness, conditional_write, monitoring_delivery, monitoring_attestation, grantCtx, grantObs) {
181
194
  emit(config, { type: 'execution_started', at: iso() });
182
195
  try {
183
- const result = await factory(envelope, redacted);
196
+ const result = grantCtx
197
+ ? await factory(envelope, redacted, grantCtx)
198
+ : await factory(envelope, redacted);
184
199
  const base = { executionAttempted: true, executed: true, enforced: false, result, verdict, preflighted };
185
- return finishExecuted(config, base, freshness, conditional_write, redacted, result, monitoring_delivery, monitoring_attestation);
200
+ return finishExecuted(config, base, freshness, conditional_write, redacted, result, monitoring_delivery, monitoring_attestation, grantObs);
186
201
  }
187
202
  catch (error) {
188
203
  emit(config, { type: 'factory_error', at: iso() });
189
204
  const base = { executionAttempted: true, executed: false, enforced: false, error, verdict, preflighted };
190
- return finishExecuted(config, base, freshness, conditional_write, redacted, undefined, monitoring_delivery, monitoring_attestation);
205
+ return finishExecuted(config, base, freshness, conditional_write, redacted, undefined, monitoring_delivery, monitoring_attestation, grantObs);
191
206
  }
192
207
  }
193
208
  function attachPolicyPresence(outcome, config) {
@@ -204,7 +219,7 @@ function coverageSnap(config) {
204
219
  function coverageOutcomeFieldsFrom(s) {
205
220
  return s.coverageObserved ? { coverage_observed: s.coverageObserved } : {};
206
221
  }
207
- function blocked(config, verdict, preflighted, freshness, conditional_write, monitoring_delivery, monitoring_attestation) {
222
+ function blocked(config, verdict, preflighted, freshness, conditional_write, monitoring_delivery, monitoring_attestation, grantObs) {
208
223
  const commit_observation = {
209
224
  status: 'not_observed', observed_at: iso(), host_attestation: 'absent',
210
225
  };
@@ -226,10 +241,11 @@ function blocked(config, verdict, preflighted, freshness, conditional_write, mon
226
241
  ...(monitoring_delivery ? { monitoring_delivery } : {}),
227
242
  ...(monitoring_attestation ? { monitoring_attestation } : {}),
228
243
  ...coverageOutcomeFieldsFrom(cov),
244
+ ...(grantObs ? { execution_grant: grantObs } : {}),
229
245
  };
230
246
  return attachPolicyPresence(out, config);
231
247
  }
232
- async function finishExecuted(config, base, freshness, conditional_write, redacted, result, monitoring_delivery, monitoring_attestation) {
248
+ async function finishExecuted(config, base, freshness, conditional_write, redacted, result, monitoring_delivery, monitoring_attestation, grantObs) {
233
249
  const enabled = config.requireCommitObservation !== false;
234
250
  const commit_observation = await observeCommit({
235
251
  enabled,
@@ -304,6 +320,7 @@ async function finishExecuted(config, base, freshness, conditional_write, redact
304
320
  : {}),
305
321
  } : {}),
306
322
  ...coverageOutcomeFieldsFrom(cov),
323
+ ...(grantObs ? { execution_grant: grantObs } : {}),
307
324
  };
308
325
  return attachPolicyPresence(out, config);
309
326
  }
@@ -456,17 +473,33 @@ export async function guardToolCall(call, executeFactory, config, callContext) {
456
473
  // previous_receipt: host-supplied only (GuardConfig.previousReceipt). Never hardcoded-undefined
457
474
  // forever — when the host provides a prior token (string or getter), thread it so the issuer can
458
475
  // hash it into the signed `prev` slot. The guard does not retain or advance the value.
476
+ //
477
+ // executionGrant (9.6.0): per-invocation locals — never stored on GuardConfig (shared across
478
+ // overlapping calls). Default OFF is this object without include_execution_grant (9.5.0 shape).
459
479
  const request = {
460
480
  artifacts: detection.artifacts,
461
481
  context: { operation: config.operation ?? 'tool_call', environment: config.environment, audience: config.audience },
462
482
  previous_receipt: resolvePreviousReceipt(config),
463
483
  idempotency_key: undefined,
464
484
  };
485
+ let grantObs;
486
+ let grantForCall = null;
487
+ if (isExecutionGrantEnabled(config)) {
488
+ grantObs = { requested: true, arrived: false };
489
+ const nonceRes = await resolveStateNonceForCall(config, redacted, detection.artifacts);
490
+ if (!nonceRes.ok) {
491
+ breakerRecord(config);
492
+ return closedIntegrity(config, 'EXECUTION_GRANT_NONCE_UNRESOLVABLE', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
493
+ }
494
+ request.include_execution_grant = true;
495
+ if (nonceRes.nonce)
496
+ request.state_nonce = nonceRes.nonce;
497
+ }
465
498
  // request-attributable payload cap => PAYLOAD_TOO_LARGE (integrity) => closed.
466
499
  const cap = config.maxPayloadBytes ?? 1_000_000;
467
500
  if (Buffer.byteLength(JSON.stringify(request), 'utf8') > cap) {
468
501
  breakerRecord(config);
469
- return closedIntegrity(config, 'PAYLOAD_TOO_LARGE', failPolicy, fctx, cwctx, redacted, detection.artifacts);
502
+ return closedIntegrity(config, 'PAYLOAD_TOO_LARGE', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
470
503
  }
471
504
  emit(config, { type: 'preflight_start', at: iso() });
472
505
  const pf = await preflightWithRetry(config, request);
@@ -479,14 +512,22 @@ export async function guardToolCall(call, executeFactory, config, callContext) {
479
512
  if (pf.integrity) {
480
513
  emit(config, { type: 'breaker_tripped', at: iso(), cause: pf.cause });
481
514
  const v = unavailableVerdict({ cause: pf.cause, failPolicy, resolution: 'CLOSED', action: 'STOP' }, count);
482
- return blocked(config, v, false, basis, cw);
515
+ return blocked(config, v, false, basis, cw, undefined, undefined, grantObs);
483
516
  }
484
517
  // availability
485
518
  const availCause = pf.cause;
519
+ // A grant was requested for THIS call: never OPEN_PASSTHROUGH / LKG-execute without a token.
520
+ // failPolicy 'open' remains the 9.5.0 opt-in only when the grant path is off.
521
+ if (grantObs && grantObs.requested) {
522
+ if (breakerTripped(config))
523
+ emit(config, { type: 'breaker_tripped', at: iso(), cause: availCause });
524
+ const v = unavailableVerdict({ cause: availCause, failPolicy, resolution: 'CLOSED', action: 'STOP' }, count);
525
+ return blocked(config, v, false, basis, cw, undefined, undefined, grantObs);
526
+ }
486
527
  if (failPolicy === 'open' && !breakerTripped(config)) {
487
528
  emit(config, { type: 'preflight_unavailable', at: iso(), cause: availCause, action: 'CONTINUE' });
488
529
  const v = unavailableVerdict({ cause: availCause, failPolicy: 'open', resolution: 'OPEN_PASSTHROUGH', action: 'CONTINUE' }, count);
489
- return runUnenforced(config, executeFactory, null, v, false, redacted, basis, cw);
530
+ return runUnenforced(config, executeFactory, null, v, false, redacted, basis, cw, undefined, undefined, undefined, grantObs);
490
531
  }
491
532
  if (failPolicy === 'lkg') {
492
533
  const lkg = await tryLkg(config, inputFp);
@@ -500,31 +541,31 @@ export async function guardToolCall(call, executeFactory, config, callContext) {
500
541
  if (breakerTripped(config))
501
542
  emit(config, { type: 'breaker_tripped', at: iso(), cause: availCause });
502
543
  const v = unavailableVerdict({ cause: availCause, failPolicy, resolution: 'CLOSED', action: 'STOP' }, count);
503
- return blocked(config, v, false, basis, cw);
544
+ return blocked(config, v, false, basis, cw, undefined, undefined, grantObs);
504
545
  }
505
546
  // We have a response. Read the decision (envelope-first) and verify the receipt.
506
547
  const rd = readDecision(pf.response);
507
548
  // PRESENT but unrecognised action — halt with its own code before any decision map or reconcile.
508
549
  if (rd.reason === 'EXECUTION_ACTION_UNRECOGNISED') {
509
550
  breakerRecord(config);
510
- return closedIntegrity(config, 'EXECUTION_ACTION_UNRECOGNISED', failPolicy, fctx, cwctx, redacted, detection.artifacts);
551
+ return closedIntegrity(config, 'EXECUTION_ACTION_UNRECOGNISED', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
511
552
  }
512
553
  // Missing EA on v2 / non-legacy-1.0 — reuse closedIntegrity halt arm, cause UNREADABLE_DECISION.
513
554
  // executed:false, enforced:false (blocked()). Never remap to CONTINUE.
514
555
  if (rd.reason === 'UNREADABLE_DECISION') {
515
556
  breakerRecord(config);
516
- return closedIntegrity(config, 'UNREADABLE_DECISION', failPolicy, fctx, cwctx, redacted, detection.artifacts);
557
+ return closedIntegrity(config, 'UNREADABLE_DECISION', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
517
558
  }
518
559
  if (!rd.envelope) {
519
560
  // Known action but no envelope => integrity => closed.
520
561
  breakerRecord(config);
521
- return closedIntegrity(config, 'SCHEMA_INVALID', failPolicy, fctx, cwctx, redacted, detection.artifacts);
562
+ return closedIntegrity(config, 'SCHEMA_INVALID', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
522
563
  }
523
564
  const envelope = rd.envelope;
524
565
  // Past unrecognised early-return: action is a closed ExecutionAction (or we treat non-closed as integrity).
525
566
  if (!isClosedAction(rd.executionAction)) {
526
567
  breakerRecord(config);
527
- return closedIntegrity(config, 'EXECUTION_ACTION_UNRECOGNISED', failPolicy, fctx, cwctx, redacted, detection.artifacts);
568
+ return closedIntegrity(config, 'EXECUTION_ACTION_UNRECOGNISED', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
528
569
  }
529
570
  const closedAction = rd.executionAction;
530
571
  // 5. expiry check before honoring any decision.
@@ -541,7 +582,7 @@ export async function guardToolCall(call, executeFactory, config, callContext) {
541
582
  // signature. Both trip the breaker and STOP — the guard NEVER executes off an unbound receipt.
542
583
  if (config.verifyReceipts !== false && !receiptVerified && envelope.receipt?.token) {
543
584
  breakerRecord(config);
544
- return closedIntegrity(config, bindResult.cause ?? 'RECEIPT_UNVERIFIED', failPolicy, fctx, cwctx, redacted, detection.artifacts);
585
+ return closedIntegrity(config, bindResult.cause ?? 'RECEIPT_UNVERIFIED', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
545
586
  }
546
587
  // ── Client-side authorization gate (P0-b/c): mirror §106/§111/§115 before honoring any action. ──
547
588
  // Threat model (bounded): without a receipt, fields including execution_action are not
@@ -554,21 +595,34 @@ export async function guardToolCall(call, executeFactory, config, callContext) {
554
595
  const gate = evaluateEnvelope(pf.response, envelope, closedAction, detection.artifacts);
555
596
  if (gate.verdict === 'fail-closed') {
556
597
  breakerRecord(config);
557
- return closedIntegrity(config, gate.cause, failPolicy, fctx, cwctx, redacted, detection.artifacts);
598
+ return closedIntegrity(config, gate.cause, failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
558
599
  }
559
600
  if (gate.verdict === 'block-strict') {
560
601
  // A real (or stricter-reconciled) BLOCK / REQUIRE_APPROVAL — clean block; the factory never runs.
561
602
  const { basis } = freshnessFor(config, redacted, fctx, detection.artifacts);
562
603
  const { basis: cw } = conditionalWriteFor(config, redacted, cwctx);
563
604
  return gate.decision === 'BLOCK'
564
- ? blocked(config, { kind: 'BLOCK', action: 'STOP', envelope, receiptVerified }, true, basis, cw)
565
- : blocked(config, { kind: 'APPROVAL', action: 'REQUEST_APPROVAL', envelope, receiptVerified }, true, basis, cw);
605
+ ? blocked(config, { kind: 'BLOCK', action: 'STOP', envelope, receiptVerified }, true, basis, cw, undefined, undefined, grantObs)
606
+ : blocked(config, { kind: 'APPROVAL', action: 'REQUEST_APPROVAL', envelope, receiptVerified }, true, basis, cw, undefined, undefined, grantObs);
566
607
  }
567
608
  const kind = gate.kind; // 'ALLOW' | 'MONITOR' — allow-class, safe, non-degraded, artifact-bound.
609
+ // Native grant: allow-class authorize that requested a grant must RECEIVE one.
610
+ // BLOCK/RA never mint a grant — missing token there is not EXECUTION_GRANT_MISSING.
611
+ if (grantObs && grantObs.requested) {
612
+ grantForCall = readExecutionGrantToken(pf.response);
613
+ grantObs = { requested: true, arrived: !!grantForCall };
614
+ if (!grantForCall) {
615
+ breakerRecord(config);
616
+ return closedIntegrity(config, 'EXECUTION_GRANT_MISSING', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
617
+ }
618
+ }
619
+ const grantCtx = grantObs
620
+ ? { execution_grant: grantForCall }
621
+ : undefined;
568
622
  // An expired decision cannot be honored fresh => closed (integrity: wrong-time state).
569
623
  if (expired) {
570
624
  breakerRecord(config);
571
- return closedIntegrity(config, 'SCHEMA_INVALID', failPolicy, fctx, cwctx, redacted, detection.artifacts);
625
+ return closedIntegrity(config, 'SCHEMA_INVALID', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
572
626
  }
573
627
  // MONITOR gate: host must ASSERT monitoring is wired (monitoringSinkWired === true) AND supply
574
628
  // onEvent. Presence of a function alone is not enough — () => {} used to unlock this gate while
@@ -622,20 +676,20 @@ export async function guardToolCall(call, executeFactory, config, callContext) {
622
676
  });
623
677
  if (sinkWired && monitoringDelivery.status === 'not_delivered' && monitoringDeliveryFailClosed(config)) {
624
678
  breakerRecord(config);
625
- return closedIntegrity(config, 'MONITORING_UNWIRED', failPolicy, fctx, cwctx, redacted, detection.artifacts, monitoringDelivery, monitoringAttestation);
679
+ return closedIntegrity(config, 'MONITORING_UNWIRED', failPolicy, fctx, cwctx, redacted, detection.artifacts, monitoringDelivery, monitoringAttestation, grantObs);
626
680
  }
627
681
  }
628
682
  // Freshness re-check immediately before any execution (ACTIVE assessment uses preflight befores).
629
683
  const { basis: freshBasis, blockCause: freshBlock } = freshnessFor(config, redacted, fctx, detection.artifacts);
630
684
  if (freshBlock === 'FRESHNESS_REQUIRED' || freshBlock === 'FRESHNESS_FAILED') {
631
685
  breakerRecord(config);
632
- return closedIntegrity(config, freshBlock, failPolicy, fctx, cwctx, redacted, detection.artifacts);
686
+ return closedIntegrity(config, freshBlock, failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
633
687
  }
634
688
  // Conditional-write re-check immediately before enforced execution (same conjunct class as freshness).
635
689
  const { basis: cwBasis, blockCause: cwBlock } = conditionalWriteFor(config, redacted, cwctx);
636
690
  if (cwBlock === 'CONDITIONAL_WRITE_REQUIRED') {
637
691
  breakerRecord(config);
638
- return closedIntegrity(config, cwBlock, failPolicy, fctx, cwctx, redacted, detection.artifacts);
692
+ return closedIntegrity(config, cwBlock, failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
639
693
  }
640
694
  // observeOnly is an EXPLICIT report-only opt-in: execute but never enforce (the one sanctioned
641
695
  // unenforced-execution mode alongside failPolicy=open; both are documented opt-ins, not the default).
@@ -644,14 +698,14 @@ export async function guardToolCall(call, executeFactory, config, callContext) {
644
698
  const verdict = kind === 'ALLOW'
645
699
  ? { kind: 'ALLOW', action: 'CONTINUE', envelope, receiptVerified }
646
700
  : { kind: 'MONITOR', action: 'CONTINUE_WITH_MONITORING', envelope, receiptVerified };
647
- return runUnenforced(config, executeFactory, envelope, verdict, true, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation);
701
+ return runUnenforced(config, executeFactory, envelope, verdict, true, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation, grantCtx, grantObs);
648
702
  }
649
703
  // Advisory CWM (sink WAS wired): delivery failed but failPolicy/observeOnly said not to block.
650
704
  // Proceed unenforced with the reason visible. Unwired MONITOR still falls through to
651
705
  // MONITORING_UNWIRED (CE-CC-04). ENFORCING not_delivered already returned closedIntegrity.
652
706
  if (kind === 'MONITOR' && sinkWired && monitoringDelivery && monitoringDelivery.status === 'not_delivered') {
653
707
  const degraded = { kind: 'MONITOR', action: 'CONTINUE_WITH_MONITORING', envelope, receiptVerified };
654
- return runUnenforced(config, executeFactory, envelope, degraded, true, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation);
708
+ return runUnenforced(config, executeFactory, envelope, degraded, true, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation, grantCtx, grantObs);
655
709
  }
656
710
  // ── enforced ⟺ executed INVARIANT (contract-triggering path): execute ONLY when we can ENFORCE. ──
657
711
  // enforceable = a bound-verified receipt AND (MONITOR) a wired sink. Anything else FAILS CLOSED —
@@ -680,7 +734,7 @@ export async function guardToolCall(call, executeFactory, config, callContext) {
680
734
  const offVerdict = kind === 'ALLOW'
681
735
  ? { kind: 'ALLOW', action: 'CONTINUE', envelope, receiptVerified }
682
736
  : { kind: 'MONITOR', action: 'CONTINUE_WITH_MONITORING', envelope, receiptVerified };
683
- return runUnenforced(config, executeFactory, envelope, offVerdict, true, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation);
737
+ return runUnenforced(config, executeFactory, envelope, offVerdict, true, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation, grantCtx, grantObs);
684
738
  }
685
739
  const et = checkExecutionTimeFingerprint({
686
740
  artifacts: detection.artifacts,
@@ -694,7 +748,7 @@ export async function guardToolCall(call, executeFactory, config, callContext) {
694
748
  const unmeasurable = isUnmeasurableExecutionStateReason(et.reason);
695
749
  if (execStateMode === true) {
696
750
  breakerRecord(config);
697
- return closedIntegrity(config, unmeasurable ? 'EXECUTION_STATE_UNMEASURABLE' : 'EXECUTION_STATE_DRIFT', failPolicy, fctx, cwctx, redacted, detection.artifacts);
751
+ return closedIntegrity(config, unmeasurable ? 'EXECUTION_STATE_UNMEASURABLE' : 'EXECUTION_STATE_DRIFT', failPolicy, fctx, cwctx, redacted, detection.artifacts, undefined, undefined, grantObs);
698
752
  }
699
753
  // warn opt-down: emit, then run unenforced — a measured mismatch is not an enforced run.
700
754
  if (unmeasurable) {
@@ -721,23 +775,23 @@ export async function guardToolCall(call, executeFactory, config, callContext) {
721
775
  const warnVerdict = kind === 'ALLOW'
722
776
  ? { kind: 'ALLOW', action: 'CONTINUE', envelope, receiptVerified }
723
777
  : { kind: 'MONITOR', action: 'CONTINUE_WITH_MONITORING', envelope, receiptVerified };
724
- return runUnenforced(config, executeFactory, envelope, warnVerdict, true, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation);
778
+ return runUnenforced(config, executeFactory, envelope, warnVerdict, true, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation, grantCtx, grantObs);
725
779
  }
726
780
  const approved = kind === 'ALLOW'
727
781
  ? { kind: 'ALLOW', action: 'CONTINUE', envelope, receiptVerified: true }
728
782
  : { kind: 'MONITOR', action: 'CONTINUE_WITH_MONITORING', envelope, receiptVerified: true };
729
- return runEnforced(config, executeFactory, approved, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation);
783
+ return runEnforced(config, executeFactory, approved, redacted, freshBasis, cwBasis, monitoringDelivery, monitoringAttestation, grantCtx, grantObs);
730
784
  }
731
785
  breakerRecord(config);
732
- return closedIntegrity(config, receiptVerified ? 'MONITORING_UNWIRED' : 'RECEIPT_MISSING', failPolicy, fctx, cwctx, redacted, detection.artifacts, monitoringDelivery, monitoringAttestation);
786
+ return closedIntegrity(config, receiptVerified ? 'MONITORING_UNWIRED' : 'RECEIPT_MISSING', failPolicy, fctx, cwctx, redacted, detection.artifacts, monitoringDelivery, monitoringAttestation, grantObs);
733
787
  }
734
- function closedIntegrity(config, cause, failPolicy, fctx, cwctx, redacted, arts, monitoring_delivery, monitoring_attestation) {
788
+ function closedIntegrity(config, cause, failPolicy, fctx, cwctx, redacted, arts, monitoring_delivery, monitoring_attestation, grantObs) {
735
789
  const count = (breakers.get(config)?.fails.length) ?? 1;
736
790
  emit(config, { type: 'breaker_tripped', at: iso(), cause });
737
791
  const v = unavailableVerdict({ cause, failPolicy, resolution: 'CLOSED', action: 'STOP' }, count);
738
792
  const { basis } = freshnessFor(config, redacted, fctx, arts);
739
793
  const { basis: cw } = conditionalWriteFor(config, redacted, cwctx);
740
- return blocked(config, v, false, basis, cw, monitoring_delivery, monitoring_attestation);
794
+ return blocked(config, v, false, basis, cw, monitoring_delivery, monitoring_attestation, grantObs);
741
795
  }
742
796
  function isExpired(envelope) {
743
797
  const exp = envelope.expires_at;