@guilz-dev/belay 0.3.0 → 0.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 (242) hide show
  1. package/README.md +7 -1
  2. package/dist/adapters/cursor/runtime-entry.js +1 -0
  3. package/dist/adapters/layouts/scope.js +3 -2
  4. package/dist/adapters/shared/gate-runtime.d.ts +12 -16
  5. package/dist/adapters/shared/gate-runtime.js +499 -43
  6. package/dist/bundle/claude-runtime.mjs +8004 -2676
  7. package/dist/bundle/codex-runtime.mjs +8021 -2683
  8. package/dist/bundle/cursor-runtime.mjs +7993 -2667
  9. package/dist/cli.js +180 -6
  10. package/dist/commands/approve.d.ts +1 -0
  11. package/dist/commands/approve.js +45 -26
  12. package/dist/commands/classify-for-report.js +1 -1
  13. package/dist/commands/config.d.ts +8 -0
  14. package/dist/commands/config.js +264 -58
  15. package/dist/commands/doctor.js +47 -2
  16. package/dist/commands/explain.js +17 -2
  17. package/dist/commands/harvest.d.ts +29 -0
  18. package/dist/commands/harvest.js +72 -0
  19. package/dist/commands/judge.d.ts +11 -1
  20. package/dist/commands/judge.js +24 -4
  21. package/dist/commands/metrics.js +34 -2
  22. package/dist/commands/quality.d.ts +38 -0
  23. package/dist/commands/quality.js +105 -0
  24. package/dist/commands/session.d.ts +20 -0
  25. package/dist/commands/session.js +51 -0
  26. package/dist/commands/simulate.d.ts +1 -0
  27. package/dist/commands/simulate.js +7 -2
  28. package/dist/commands/standing-allow.d.ts +10 -0
  29. package/dist/commands/standing-allow.js +26 -0
  30. package/dist/commands/tui.d.ts +48 -0
  31. package/dist/commands/tui.js +150 -0
  32. package/dist/config-io.js +59 -17
  33. package/dist/conformance/guarantee-posture.d.ts +21 -0
  34. package/dist/conformance/guarantee-posture.js +46 -0
  35. package/dist/conformance/guarantee-table.d.ts +1 -0
  36. package/dist/conformance/guarantee-table.js +14 -9
  37. package/dist/core/approval-replay-cli.d.ts +7 -0
  38. package/dist/core/approval-replay-cli.js +36 -0
  39. package/dist/core/approval-replay.d.ts +45 -0
  40. package/dist/core/approval-replay.js +141 -0
  41. package/dist/core/approval-service.d.ts +15 -0
  42. package/dist/core/approval-service.js +53 -4
  43. package/dist/core/approval.d.ts +26 -3
  44. package/dist/core/approval.js +61 -7
  45. package/dist/core/audit-analysis.d.ts +7 -1
  46. package/dist/core/audit-analysis.js +111 -0
  47. package/dist/core/audit-metrics.d.ts +7 -0
  48. package/dist/core/audit-metrics.js +24 -4
  49. package/dist/core/audit-query.d.ts +1 -0
  50. package/dist/core/audit-query.js +3 -0
  51. package/dist/core/audit-replay-context.d.ts +35 -0
  52. package/dist/core/audit-replay-context.js +88 -0
  53. package/dist/core/audit-types.d.ts +24 -1
  54. package/dist/core/audit-types.js +1 -1
  55. package/dist/core/capability/approval-state-mutation.d.ts +13 -0
  56. package/dist/core/capability/approval-state-mutation.js +92 -0
  57. package/dist/core/capability/approval-v3.d.ts +15 -0
  58. package/dist/core/capability/approval-v3.js +74 -0
  59. package/dist/core/capability/attestation.d.ts +13 -0
  60. package/dist/core/capability/attestation.js +55 -0
  61. package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
  62. package/dist/core/capability/boundary-attestation-sign.js +84 -0
  63. package/dist/core/capability/boundary-driver-container.d.ts +7 -0
  64. package/dist/core/capability/boundary-driver-container.js +122 -0
  65. package/dist/core/capability/boundary-driver.d.ts +29 -0
  66. package/dist/core/capability/boundary-driver.js +43 -0
  67. package/dist/core/capability/boundary-egress.d.ts +21 -0
  68. package/dist/core/capability/boundary-egress.js +109 -0
  69. package/dist/core/capability/boundary-grant-materialize.d.ts +13 -0
  70. package/dist/core/capability/boundary-grant-materialize.js +78 -0
  71. package/dist/core/capability/boundary-profile.d.ts +15 -0
  72. package/dist/core/capability/boundary-profile.js +25 -0
  73. package/dist/core/capability/boundary-run.d.ts +23 -0
  74. package/dist/core/capability/boundary-run.js +23 -0
  75. package/dist/core/capability/boundary-session.d.ts +57 -0
  76. package/dist/core/capability/boundary-session.js +129 -0
  77. package/dist/core/capability/capability-request-hash.d.ts +2 -0
  78. package/dist/core/capability/capability-request-hash.js +8 -0
  79. package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
  80. package/dist/core/capability/gate-latency-ratchet.js +18 -0
  81. package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
  82. package/dist/core/capability/gate-policy-shadow.js +108 -0
  83. package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
  84. package/dist/core/capability/gate-shadow-audit.js +1 -0
  85. package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
  86. package/dist/core/capability/gate-shadow-ratchet.js +81 -0
  87. package/dist/core/capability/grant-lease.d.ts +9 -0
  88. package/dist/core/capability/grant-lease.js +46 -0
  89. package/dist/core/capability/grant-loader.d.ts +12 -0
  90. package/dist/core/capability/grant-loader.js +23 -0
  91. package/dist/core/capability/grant-match.d.ts +6 -0
  92. package/dist/core/capability/grant-match.js +93 -0
  93. package/dist/core/capability/grant.d.ts +16 -0
  94. package/dist/core/capability/grant.js +16 -0
  95. package/dist/core/capability/index.d.ts +10 -1
  96. package/dist/core/capability/index.js +8 -0
  97. package/dist/core/capability/limits.d.ts +7 -0
  98. package/dist/core/capability/limits.js +55 -0
  99. package/dist/core/capability/paths.d.ts +2 -2
  100. package/dist/core/capability/paths.js +9 -9
  101. package/dist/core/capability/policy-bridge.d.ts +18 -0
  102. package/dist/core/capability/policy-bridge.js +51 -0
  103. package/dist/core/capability/policy-engine.d.ts +70 -0
  104. package/dist/core/capability/policy-engine.js +476 -0
  105. package/dist/core/capability/policy-types.d.ts +20 -0
  106. package/dist/core/capability/policy-types.js +1 -0
  107. package/dist/core/capability/request.d.ts +45 -0
  108. package/dist/core/capability/request.js +1 -0
  109. package/dist/core/capability/resolver.d.ts +4 -0
  110. package/dist/core/capability/resolver.js +4 -0
  111. package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
  112. package/dist/core/capability/trusted-workspace-roots.js +149 -0
  113. package/dist/core/capability/types.d.ts +11 -1
  114. package/dist/core/capability-approval.d.ts +2 -1
  115. package/dist/core/capability-approval.js +100 -2
  116. package/dist/core/classify-subagent.d.ts +2 -1
  117. package/dist/core/classify-subagent.js +40 -22
  118. package/dist/core/classify-tool.js +157 -64
  119. package/dist/core/config.d.ts +32 -3
  120. package/dist/core/config.js +105 -3
  121. package/dist/core/gate-contract.d.ts +3 -0
  122. package/dist/core/gate-contract.js +3 -0
  123. package/dist/core/gate-engine.js +12 -7
  124. package/dist/core/harvest.d.ts +53 -0
  125. package/dist/core/harvest.js +276 -0
  126. package/dist/core/index.d.ts +9 -3
  127. package/dist/core/index.js +6 -2
  128. package/dist/core/judge-doctor.d.ts +3 -0
  129. package/dist/core/judge-doctor.js +90 -11
  130. package/dist/core/judge-fallback-hints.d.ts +13 -0
  131. package/dist/core/judge-fallback-hints.js +108 -0
  132. package/dist/core/path-utils.d.ts +12 -0
  133. package/dist/core/path-utils.js +60 -7
  134. package/dist/core/reclassify.d.ts +3 -0
  135. package/dist/core/reclassify.js +44 -14
  136. package/dist/core/recovery/capability.d.ts +3 -0
  137. package/dist/core/recovery/capability.js +22 -0
  138. package/dist/core/recovery/fail-closed.d.ts +6 -0
  139. package/dist/core/recovery/fail-closed.js +25 -0
  140. package/dist/core/recovery/index.d.ts +3 -0
  141. package/dist/core/recovery/index.js +2 -0
  142. package/dist/core/recovery/types.d.ts +20 -0
  143. package/dist/core/recovery/types.js +1 -0
  144. package/dist/core/replay-scrub.d.ts +9 -0
  145. package/dist/core/replay-scrub.js +43 -0
  146. package/dist/core/shell-tokenizer.d.ts +2 -0
  147. package/dist/core/shell-tokenizer.js +82 -23
  148. package/dist/core/standing-allow.d.ts +50 -0
  149. package/dist/core/standing-allow.js +178 -0
  150. package/dist/core/transactional/diff-evaluator.js +4 -1
  151. package/dist/core/transactional/eligibility.js +4 -0
  152. package/dist/core/transactional/git-worktree.d.ts +10 -3
  153. package/dist/core/transactional/git-worktree.js +81 -11
  154. package/dist/core/transactional/reasons.d.ts +1 -0
  155. package/dist/core/transactional/reasons.js +4 -0
  156. package/dist/core/transactional/runner.js +22 -6
  157. package/dist/core/transactional/types.d.ts +4 -0
  158. package/dist/core/types.d.ts +36 -3
  159. package/dist/core/verdict/adapter.d.ts +3 -4
  160. package/dist/core/verdict/adapter.js +19 -6
  161. package/dist/core/verdict/containment.d.ts +4 -3
  162. package/dist/core/verdict/containment.js +17 -12
  163. package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
  164. package/dist/core/verdict/cursor-acp-client.js +345 -0
  165. package/dist/core/verdict/egress-classify.d.ts +2 -2
  166. package/dist/core/verdict/egress-classify.js +2 -2
  167. package/dist/core/verdict/git-classifier.d.ts +26 -0
  168. package/dist/core/verdict/git-classifier.js +533 -0
  169. package/dist/core/verdict/judge-audit.d.ts +1 -0
  170. package/dist/core/verdict/judge-audit.js +33 -1
  171. package/dist/core/verdict/judge-baseline.d.ts +20 -0
  172. package/dist/core/verdict/judge-baseline.js +49 -0
  173. package/dist/core/verdict/judge-broker-service.d.ts +42 -0
  174. package/dist/core/verdict/judge-broker-service.js +291 -0
  175. package/dist/core/verdict/judge-catalog.js +1 -0
  176. package/dist/core/verdict/judge-cli-fingerprint.d.ts +6 -0
  177. package/dist/core/verdict/judge-cli-fingerprint.js +67 -0
  178. package/dist/core/verdict/judge-cli.d.ts +23 -0
  179. package/dist/core/verdict/judge-cli.js +139 -16
  180. package/dist/core/verdict/judge-factory.js +13 -4
  181. package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
  182. package/dist/core/verdict/judge-provider-matrix.js +66 -0
  183. package/dist/core/verdict/judge-runtime-config.d.ts +44 -0
  184. package/dist/core/verdict/judge-runtime-config.js +96 -0
  185. package/dist/core/verdict/judge-session-broker.d.ts +48 -0
  186. package/dist/core/verdict/judge-session-broker.js +200 -0
  187. package/dist/core/verdict/judge-session-guard.d.ts +27 -0
  188. package/dist/core/verdict/judge-session-guard.js +91 -0
  189. package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
  190. package/dist/core/verdict/judge-session-kill-switch.js +43 -0
  191. package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
  192. package/dist/core/verdict/judge-session-mutex.js +23 -0
  193. package/dist/core/verdict/judge-shadow.d.ts +24 -0
  194. package/dist/core/verdict/judge-shadow.js +116 -0
  195. package/dist/core/verdict/judge-transport.d.ts +44 -0
  196. package/dist/core/verdict/judge-transport.js +268 -0
  197. package/dist/core/verdict/judge.d.ts +14 -14
  198. package/dist/core/verdict/judge.js +5 -67
  199. package/dist/core/verdict/launcher-resolve.d.ts +2 -0
  200. package/dist/core/verdict/launcher-resolve.js +16 -0
  201. package/dist/core/verdict/parser.js +6 -1
  202. package/dist/core/verdict/prescan.d.ts +15 -0
  203. package/dist/core/verdict/prescan.js +68 -0
  204. package/dist/core/verdict/shell-policy.d.ts +25 -0
  205. package/dist/core/verdict/shell-policy.js +40 -0
  206. package/dist/core/verdict/shell-semantics.d.ts +24 -0
  207. package/dist/core/verdict/shell-semantics.js +225 -0
  208. package/dist/core/verdict/types.d.ts +26 -1
  209. package/dist/core/verdict/verdict.js +300 -179
  210. package/dist/corpus/adversarial-probe.d.ts +86 -0
  211. package/dist/corpus/adversarial-probe.js +220 -0
  212. package/dist/corpus/evaluate.d.ts +45 -12
  213. package/dist/corpus/evaluate.js +61 -8
  214. package/dist/corpus/gate-latency-budget.d.ts +39 -0
  215. package/dist/corpus/gate-latency-budget.js +33 -0
  216. package/dist/corpus/gates.d.ts +35 -0
  217. package/dist/corpus/gates.js +81 -0
  218. package/dist/corpus/judge-accuracy.d.ts +8 -0
  219. package/dist/corpus/judge-accuracy.js +39 -0
  220. package/dist/corpus/must-allow-commands.d.ts +5 -0
  221. package/dist/corpus/must-allow-commands.js +13 -0
  222. package/dist/corpus/mutators.d.ts +28 -0
  223. package/dist/corpus/mutators.js +162 -0
  224. package/dist/corpus/ratchet.d.ts +42 -0
  225. package/dist/corpus/ratchet.js +116 -0
  226. package/dist/corpus/runtime-match.d.ts +19 -0
  227. package/dist/corpus/runtime-match.js +43 -0
  228. package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
  229. package/dist/corpus/standing-allow-catalog.generated.js +99 -0
  230. package/dist/corpus/types.d.ts +47 -0
  231. package/dist/corpus/types.js +157 -0
  232. package/dist/installer.js +4 -1
  233. package/dist/judge-broker-daemon.d.ts +1 -0
  234. package/dist/judge-broker-daemon.js +137 -0
  235. package/dist/services/sandbox-service.d.ts +5 -0
  236. package/dist/services/sandbox-service.js +30 -2
  237. package/dist/types.d.ts +2 -2
  238. package/dist/version.d.ts +1 -1
  239. package/dist/version.js +1 -1
  240. package/package.json +7 -3
  241. package/skills/belay/SKILL.md +7 -1
  242. package/skills/belay/belay-approve.md +17 -0
@@ -1,5 +1,8 @@
1
- /** Cursor may invoke the same shell gate more than once per retry; lease covers that window. */
2
- export const APPROVAL_EXECUTION_LEASE_MS = 60_000;
1
+ import { buildReplayEnvelopeFields } from './approval-replay.js';
2
+ import { attachCapabilityEnvelope } from './capability/approval-v3.js';
3
+ import { DEFAULT_APPROVAL_CONFIG } from './config.js';
4
+ /** @deprecated Use `DEFAULT_APPROVAL_CONFIG.executionLeaseMs` */
5
+ export const APPROVAL_EXECUTION_LEASE_MS = DEFAULT_APPROVAL_CONFIG.executionLeaseMs;
3
6
  export function nowIso() {
4
7
  return new Date().toISOString();
5
8
  }
@@ -13,10 +16,14 @@ export function isExecutionLeaseExpired(approval) {
13
16
  return Date.parse(approval.executionLeaseExpiresAt) <= Date.now();
14
17
  }
15
18
  export function compactApprovals(state) {
16
- return {
19
+ const compacted = {
17
20
  version: state.version,
18
21
  approvals: state.approvals.filter((approval) => !isExpired(approval) && !isExecutionLeaseExpired(approval)),
19
22
  };
23
+ if (state.revision !== undefined) {
24
+ compacted.revision = state.revision;
25
+ }
26
+ return compacted;
20
27
  }
21
28
  export function mergeApprovalStates(target, source) {
22
29
  const byId = new Map();
@@ -29,7 +36,12 @@ export function mergeApprovalStates(target, source) {
29
36
  }
30
37
  }
31
38
  return compactApprovals({
32
- version: target.version === 2 || source.version === 2 ? 2 : 1,
39
+ version: target.version === 3 || source.version === 3
40
+ ? 3
41
+ : target.version === 2 || source.version === 2
42
+ ? 2
43
+ : 1,
44
+ revision: Math.max(target.revision ?? 0, source.revision ?? 0),
33
45
  approvals: [...byId.values()],
34
46
  });
35
47
  }
@@ -49,8 +61,35 @@ export function approvalCommandMatch(prompt, tokenPrefix) {
49
61
  }
50
62
  return null;
51
63
  }
52
- export function buildRetryInstruction(tokenPrefix, approvalId) {
53
- return `To allow the next matching action once, send ${tokenPrefix} ${approvalId} and then retry the original action unchanged.`;
64
+ export function createApprovalRecordWithEnvelope(params) {
65
+ const envelope = buildReplayEnvelopeFields({
66
+ kind: params.kind,
67
+ command: params.approvalInput?.input,
68
+ input: params.approvalInput?.input,
69
+ inputKind: params.approvalInput?.inputKind,
70
+ cwd: params.approvalInput?.cwd,
71
+ toolName: params.approvalInput?.toolName,
72
+ payload: params.approvalInput?.payload,
73
+ fingerprint: params.fingerprint,
74
+ repoRoot: params.repoRoot,
75
+ });
76
+ return createApprovalRecord({
77
+ kind: params.kind,
78
+ fingerprint: params.fingerprint,
79
+ repoRoot: params.repoRoot,
80
+ reason: params.reason,
81
+ summary: params.summary,
82
+ approvalTtlMinutes: params.approvalTtlMinutes,
83
+ approvalId: params.approvalId,
84
+ input: envelope.input ?? params.approvalInput?.input,
85
+ inputKind: envelope.inputKind ?? params.approvalInput?.inputKind,
86
+ cwd: envelope.cwd,
87
+ toolName: envelope.toolName ?? params.approvalInput?.toolName,
88
+ payloadHash: envelope.payloadHash,
89
+ payloadJson: envelope.payloadJson,
90
+ scopeHint: params.scopeHint,
91
+ capabilityRequests: params.capabilityRequests,
92
+ });
54
93
  }
55
94
  export function createApprovalRecord(params) {
56
95
  const createdAt = nowIso();
@@ -69,5 +108,20 @@ export function createApprovalRecord(params) {
69
108
  record.input = params.input;
70
109
  record.inputKind = params.inputKind ?? params.kind;
71
110
  }
72
- return record;
111
+ if (params.cwd) {
112
+ record.cwd = params.cwd;
113
+ }
114
+ if (params.toolName) {
115
+ record.toolName = params.toolName;
116
+ }
117
+ if (params.payloadHash) {
118
+ record.payloadHash = params.payloadHash;
119
+ }
120
+ if (params.payloadJson) {
121
+ record.payloadJson = params.payloadJson;
122
+ }
123
+ if (params.scopeHint) {
124
+ record.scopeHint = params.scopeHint;
125
+ }
126
+ return attachCapabilityEnvelope(record, params.capabilityRequests);
73
127
  }
@@ -1,4 +1,4 @@
1
- import type { ApprovalRoundTrip, AuditRecord, BypassAttempt, NoisyRuleCandidate } from './audit-types.js';
1
+ import type { ApprovalRoundTrip, AuditRecord, AvailabilityAskCounts, BypassAttempt, NoisyRuleCandidate, ReasonApprovalRatio, RepeatedFingerprintAsk } from './audit-types.js';
2
2
  export declare function detectBypassAttempts(records: AuditRecord[], windowMs?: number): BypassAttempt[];
3
3
  export declare function detectNoisyRules(records: AuditRecord[], roundTrips: ApprovalRoundTrip[], minDenies?: number): NoisyRuleCandidate[];
4
4
  export declare function computeApprovalLatencyStats(roundTrips: ApprovalRoundTrip[]): {
@@ -8,3 +8,9 @@ export declare function computeApprovalLatencyStats(roundTrips: ApprovalRoundTri
8
8
  };
9
9
  export declare function bucketGateEventsByDay(records: AuditRecord[]): Record<string, number>;
10
10
  export declare function countVerdicts(records: AuditRecord[]): Record<string, number>;
11
+ export declare function computeWouldBlockByReason(records: AuditRecord[]): Record<string, number>;
12
+ /** Approval ratios are candidate signals from operator behavior — not corpus ground truth. */
13
+ export declare function computeApprovalRatioByReason(records: AuditRecord[], roundTrips: ApprovalRoundTrip[]): ReasonApprovalRatio[];
14
+ export declare function isAvailabilityCausedAsk(record: AuditRecord): boolean;
15
+ export declare function computeAvailabilityAskCounts(records: AuditRecord[]): AvailabilityAskCounts;
16
+ export declare function computeRepeatedFingerprintAsks(records: AuditRecord[], minCount?: number, limit?: number): RepeatedFingerprintAsk[];
@@ -145,3 +145,114 @@ export function countVerdicts(records) {
145
145
  }
146
146
  return counts;
147
147
  }
148
+ export function computeWouldBlockByReason(records) {
149
+ const counts = {};
150
+ for (const record of records) {
151
+ if (!isGateRecord(record) || !inferWouldBlock(record)) {
152
+ continue;
153
+ }
154
+ const reason = record.reason ?? 'unknown';
155
+ counts[reason] = (counts[reason] ?? 0) + 1;
156
+ }
157
+ return counts;
158
+ }
159
+ /** Approval ratios are candidate signals from operator behavior — not corpus ground truth. */
160
+ export function computeApprovalRatioByReason(records, roundTrips) {
161
+ const wouldBlockByReason = new Map();
162
+ const approvedByReason = new Map();
163
+ for (const record of records) {
164
+ if (!isGateRecord(record) || !inferWouldBlock(record)) {
165
+ continue;
166
+ }
167
+ const reason = record.reason ?? 'unknown';
168
+ wouldBlockByReason.set(reason, (wouldBlockByReason.get(reason) ?? 0) + 1);
169
+ }
170
+ for (const trip of roundTrips) {
171
+ if (!trip.approvalTimestamp) {
172
+ continue;
173
+ }
174
+ approvedByReason.set(trip.reason, (approvedByReason.get(trip.reason) ?? 0) + 1);
175
+ }
176
+ return [...wouldBlockByReason.entries()]
177
+ .map(([reason, wouldBlockCount]) => {
178
+ const approvedAfterDenyCount = approvedByReason.get(reason) ?? 0;
179
+ return {
180
+ reason,
181
+ wouldBlockCount,
182
+ approvedAfterDenyCount,
183
+ approvalRate: wouldBlockCount > 0 ? approvedAfterDenyCount / wouldBlockCount : 0,
184
+ };
185
+ })
186
+ .sort((left, right) => right.wouldBlockCount - left.wouldBlockCount);
187
+ }
188
+ function judgeFallbackReason(record) {
189
+ return typeof record.judgeFallbackReason === 'string' ? record.judgeFallbackReason : '';
190
+ }
191
+ function isJudgeTimeoutFallback(fallback) {
192
+ return fallback.includes('timeout');
193
+ }
194
+ export function isAvailabilityCausedAsk(record) {
195
+ if (!isGateRecord(record) || !inferWouldBlock(record)) {
196
+ return false;
197
+ }
198
+ if (record.reason === 'missing_trusted_cwd') {
199
+ return true;
200
+ }
201
+ return judgeFallbackReason(record).length > 0;
202
+ }
203
+ export function computeAvailabilityAskCounts(records) {
204
+ const counts = {
205
+ total: 0,
206
+ missingTrustedCwd: 0,
207
+ judgeTimeout: 0,
208
+ judgeFallback: 0,
209
+ };
210
+ for (const record of records) {
211
+ if (!isGateRecord(record) || !inferWouldBlock(record)) {
212
+ continue;
213
+ }
214
+ if (record.reason === 'missing_trusted_cwd') {
215
+ counts.missingTrustedCwd += 1;
216
+ counts.total += 1;
217
+ continue;
218
+ }
219
+ const fallback = judgeFallbackReason(record);
220
+ if (!fallback) {
221
+ continue;
222
+ }
223
+ if (isJudgeTimeoutFallback(fallback)) {
224
+ counts.judgeTimeout += 1;
225
+ }
226
+ else {
227
+ counts.judgeFallback += 1;
228
+ }
229
+ counts.total += 1;
230
+ }
231
+ return counts;
232
+ }
233
+ export function computeRepeatedFingerprintAsks(records, minCount = 2, limit = 10) {
234
+ const grouped = new Map();
235
+ for (const record of records) {
236
+ if (!isGateRecord(record) || !inferWouldBlock(record) || !record.fingerprint) {
237
+ continue;
238
+ }
239
+ const fingerprint = record.fingerprint;
240
+ const existing = grouped.get(fingerprint);
241
+ if (existing) {
242
+ existing.askCount += 1;
243
+ existing.summary = record.summary ?? existing.summary;
244
+ existing.reason = record.reason ?? existing.reason;
245
+ continue;
246
+ }
247
+ grouped.set(fingerprint, {
248
+ fingerprint,
249
+ summary: record.summary ?? '',
250
+ reason: record.reason ?? 'unknown',
251
+ askCount: 1,
252
+ });
253
+ }
254
+ return [...grouped.values()]
255
+ .filter((entry) => entry.askCount >= minCount)
256
+ .sort((left, right) => right.askCount - left.askCount)
257
+ .slice(0, limit);
258
+ }
@@ -1,4 +1,5 @@
1
1
  import { buildApprovalRoundTrips, filterAuditRecords, toAuditRecord } from './audit-query.js';
2
+ import type { AvailabilityAskCounts, ReasonApprovalRatio, RepeatedFingerprintAsk } from './audit-types.js';
2
3
  /** Minimum gate events before recommending enforce with zero would-block rate. */
3
4
  export declare const MIN_GATE_EVENTS_FOR_ENFORCE = 20;
4
5
  export interface AuditMetricsReport {
@@ -9,6 +10,12 @@ export interface AuditMetricsReport {
9
10
  gateEvents: number;
10
11
  wouldBlockCount: number;
11
12
  wouldBlockRate: number;
13
+ classifierWouldBlockCount: number;
14
+ classifierWouldBlockRate: number;
15
+ wouldBlockByReason: Record<string, number>;
16
+ approvalRatioByReason: ReasonApprovalRatio[];
17
+ availabilityAsks: AvailabilityAskCounts;
18
+ repeatedFingerprintAsks: RepeatedFingerprintAsk[];
12
19
  byReason: Record<string, number>;
13
20
  byKind: Record<string, number>;
14
21
  byVerdict: Record<string, number>;
@@ -1,4 +1,4 @@
1
- import { bucketGateEventsByDay, computeApprovalLatencyStats, countVerdicts, detectBypassAttempts, detectNoisyRules, } from './audit-analysis.js';
1
+ import { bucketGateEventsByDay, computeApprovalLatencyStats, computeApprovalRatioByReason, computeAvailabilityAskCounts, computeRepeatedFingerprintAsks, computeWouldBlockByReason, countVerdicts, detectBypassAttempts, detectNoisyRules, } from './audit-analysis.js';
2
2
  import { buildApprovalRoundTrips, filterAuditRecords, inferWouldBlock, isApprovalRecorded, toAuditRecord, } from './audit-query.js';
3
3
  import { AUDIT_METRICS_SCHEMA_VERSION, GATE_EVENTS } from './audit-types.js';
4
4
  /** Minimum gate events before recommending enforce with zero would-block rate. */
@@ -78,7 +78,13 @@ export function computeAuditMetrics(records, options = {}) {
78
78
  const approvalLatency = computeApprovalLatencyStats(roundTrips);
79
79
  const bypassAttempts = detectBypassAttempts(auditRecords);
80
80
  const noisyRuleCandidates = detectNoisyRules(auditRecords, roundTrips);
81
+ const wouldBlockByReason = computeWouldBlockByReason(auditRecords);
82
+ const approvalRatioByReason = computeApprovalRatioByReason(auditRecords, roundTrips);
83
+ const availabilityAsks = computeAvailabilityAskCounts(auditRecords);
84
+ const repeatedFingerprintAsks = computeRepeatedFingerprintAsks(auditRecords);
81
85
  const wouldBlockRate = gateEvents > 0 ? wouldBlockCount / gateEvents : 0;
86
+ const classifierWouldBlockCount = Math.max(0, wouldBlockCount - availabilityAsks.total);
87
+ const classifierWouldBlockRate = gateEvents > 0 ? classifierWouldBlockCount / gateEvents : 0;
82
88
  const topWouldBlockSummaries = [...summaryCounts.values()]
83
89
  .sort((left, right) => right.count - left.count)
84
90
  .slice(0, 10);
@@ -101,16 +107,16 @@ export function computeAuditMetrics(records, options = {}) {
101
107
  }
102
108
  }
103
109
  else {
104
- notes.push(`${wouldBlockCount} would-block event(s) (${(wouldBlockRate * 100).toFixed(1)}% of gate traffic). Review top summaries and add overrides.allow where appropriate.`);
110
+ notes.push(`${wouldBlockCount} would-block event(s) (${(wouldBlockRate * 100).toFixed(1)}% of gate traffic; classifier-quality ${(classifierWouldBlockRate * 100).toFixed(1)}%). Review top summaries and add overrides.allow where appropriate.`);
105
111
  if (approvalRecordedCount > 0) {
106
112
  notes.push(`${approvalRecordedCount} approval(s) recorded — these likely indicate actions operators wanted.`);
107
113
  }
108
114
  else {
109
115
  notes.push('Review top would-block summaries and add overrides.allow for legitimate commands before switching to enforce.');
110
116
  }
111
- if (wouldBlockRate < 0.05 && gateEvents >= 20) {
117
+ if (classifierWouldBlockRate < 0.05 && gateEvents >= 20 && availabilityAsks.total === 0) {
112
118
  readyForEnforce = true;
113
- notes.push('Would-block rate is below 5% with sufficient sample size — consider enforce mode.');
119
+ notes.push('Classifier-quality would-block rate is below 5% with sufficient sample size — consider enforce mode.');
114
120
  }
115
121
  }
116
122
  }
@@ -120,6 +126,14 @@ export function computeAuditMetrics(records, options = {}) {
120
126
  else {
121
127
  notes.push('Set policy.unknownLocalEffect to "deny" to dogfood fail-closed defaults.');
122
128
  }
129
+ if (availabilityAsks.total > 0) {
130
+ readyForEnforce = false;
131
+ notes.push(`${availabilityAsks.total} availability-caused ask(s) — tune infrastructure before corpus overrides.`);
132
+ notes.push('Ready for enforce withheld while availability-caused asks are present.');
133
+ }
134
+ if (repeatedFingerprintAsks.length > 0) {
135
+ notes.push(`${repeatedFingerprintAsks.length} repeated fingerprint ask pattern(s) — review standing-allow / cache candidates.`);
136
+ }
123
137
  if (noisyRuleCandidates.length > 0) {
124
138
  notes.push(`${noisyRuleCandidates.length} noisy rule candidate(s) — high deny-then-approve rate.`);
125
139
  }
@@ -131,6 +145,12 @@ export function computeAuditMetrics(records, options = {}) {
131
145
  gateEvents,
132
146
  wouldBlockCount,
133
147
  wouldBlockRate,
148
+ classifierWouldBlockCount,
149
+ classifierWouldBlockRate,
150
+ wouldBlockByReason,
151
+ approvalRatioByReason,
152
+ availabilityAsks,
153
+ repeatedFingerprintAsks,
134
154
  byReason,
135
155
  byKind,
136
156
  byVerdict,
@@ -2,6 +2,7 @@ import type { ApprovalRoundTrip, AuditFilter, AuditRecord } from './audit-types.
2
2
  export declare function toAuditRecord(value: Record<string, unknown>): AuditRecord;
3
3
  export declare function parseTimestamp(value?: string): number | null;
4
4
  export declare function isGateRecord(record: AuditRecord): boolean;
5
+ export declare function isShellGateRecord(record: AuditRecord): boolean;
5
6
  export declare function isApprovalRecorded(record: AuditRecord): boolean;
6
7
  export declare function inferWouldBlock(record: AuditRecord): boolean;
7
8
  export declare function recordStringField(record: AuditRecord, field: 'effect' | 'reason' | 'permission' | 'verdict' | 'mode' | 'summary' | 'fingerprint' | 'location'): string;
@@ -16,6 +16,9 @@ export function parseTimestamp(value) {
16
16
  export function isGateRecord(record) {
17
17
  return typeof record.event === 'string' && GATE_EVENTS.has(record.event);
18
18
  }
19
+ export function isShellGateRecord(record) {
20
+ return (isGateRecord(record) && (record.event === 'beforeShellExecution' || record.kind === 'shell'));
21
+ }
19
22
  export function isApprovalRecorded(record) {
20
23
  return ((record.event === 'approval' ||
21
24
  (record.event === 'beforeSubmitPrompt' && record.reason === 'approval_recorded')) &&
@@ -0,0 +1,35 @@
1
+ import type { GatedActionKind } from './gate-contract.js';
2
+ import type { ClassifyResult } from './types.js';
3
+ /** Preserved action context for simulate triage — not a safety gate. */
4
+ export interface AuditReplayContext {
5
+ cwd: string;
6
+ kind: GatedActionKind;
7
+ command?: string;
8
+ toolName?: string;
9
+ payload?: Record<string, unknown>;
10
+ }
11
+ /** Classifier-facing action snapshot for high-fidelity simulate replay. */
12
+ export interface AuditActionSnapshot {
13
+ schemaVersion: 1;
14
+ kind: GatedActionKind;
15
+ cwd: string;
16
+ normalizedAction: string;
17
+ toolName?: string;
18
+ payloadHash?: string;
19
+ }
20
+ export interface ReplayActionLike {
21
+ cwd?: string;
22
+ kind?: string;
23
+ command?: string;
24
+ toolName?: string;
25
+ payload?: Record<string, unknown>;
26
+ }
27
+ export declare function hashReplayPayload(payload: Record<string, unknown>): string;
28
+ export declare function buildAuditActionSnapshot(kind: GatedActionKind, result: Pick<ClassifyResult, 'normalizedCommand' | 'summary'>, replayAction?: ReplayActionLike): AuditActionSnapshot | undefined;
29
+ export declare function parseAuditActionSnapshot(record: {
30
+ actionSnapshot?: unknown;
31
+ }): AuditActionSnapshot | null;
32
+ export declare function buildAuditReplayContext(kind: GatedActionKind, result: Pick<ClassifyResult, 'normalizedCommand' | 'summary'>, replayAction?: ReplayActionLike): AuditReplayContext | undefined;
33
+ export declare function parseAuditReplayContext(record: {
34
+ replayContext?: unknown;
35
+ }): AuditReplayContext | null;
@@ -0,0 +1,88 @@
1
+ import { createHash } from 'node:crypto';
2
+ export function hashReplayPayload(payload) {
3
+ return createHash('sha256').update(JSON.stringify(payload)).digest('hex');
4
+ }
5
+ export function buildAuditActionSnapshot(kind, result, replayAction) {
6
+ if (!replayAction?.cwd) {
7
+ return undefined;
8
+ }
9
+ const replayKind = replayAction.kind;
10
+ const resolvedKind = replayKind === 'shell' || replayKind === 'tool' || replayKind === 'subagent' ? replayKind : kind;
11
+ const normalizedAction = result.normalizedCommand ?? replayAction.command ?? result.summary ?? '';
12
+ if (!normalizedAction.trim()) {
13
+ return undefined;
14
+ }
15
+ return {
16
+ schemaVersion: 1,
17
+ kind: resolvedKind,
18
+ cwd: replayAction.cwd,
19
+ normalizedAction,
20
+ ...(replayAction.toolName ? { toolName: replayAction.toolName } : {}),
21
+ ...(replayAction.payload ? { payloadHash: hashReplayPayload(replayAction.payload) } : {}),
22
+ };
23
+ }
24
+ export function parseAuditActionSnapshot(record) {
25
+ const raw = record.actionSnapshot;
26
+ if (!raw || typeof raw !== 'object') {
27
+ return null;
28
+ }
29
+ const snapshot = raw;
30
+ if (snapshot.schemaVersion !== 1) {
31
+ return null;
32
+ }
33
+ if (typeof snapshot.cwd !== 'string' || !snapshot.cwd.trim()) {
34
+ return null;
35
+ }
36
+ const kind = snapshot.kind;
37
+ if (kind !== 'shell' && kind !== 'tool' && kind !== 'subagent') {
38
+ return null;
39
+ }
40
+ if (typeof snapshot.normalizedAction !== 'string' || !snapshot.normalizedAction.trim()) {
41
+ return null;
42
+ }
43
+ return {
44
+ schemaVersion: 1,
45
+ kind,
46
+ cwd: snapshot.cwd,
47
+ normalizedAction: snapshot.normalizedAction,
48
+ ...(typeof snapshot.toolName === 'string' ? { toolName: snapshot.toolName } : {}),
49
+ ...(typeof snapshot.payloadHash === 'string' ? { payloadHash: snapshot.payloadHash } : {}),
50
+ };
51
+ }
52
+ export function buildAuditReplayContext(kind, result, replayAction) {
53
+ if (!replayAction?.cwd) {
54
+ return undefined;
55
+ }
56
+ const replayKind = replayAction.kind;
57
+ const resolvedKind = replayKind === 'shell' || replayKind === 'tool' || replayKind === 'subagent' ? replayKind : kind;
58
+ return {
59
+ cwd: replayAction.cwd,
60
+ kind: resolvedKind,
61
+ command: replayAction.command ?? result.normalizedCommand ?? result.summary,
62
+ ...(replayAction.toolName ? { toolName: replayAction.toolName } : {}),
63
+ ...(replayAction.payload ? { payload: replayAction.payload } : {}),
64
+ };
65
+ }
66
+ export function parseAuditReplayContext(record) {
67
+ const raw = record.replayContext;
68
+ if (!raw || typeof raw !== 'object') {
69
+ return null;
70
+ }
71
+ const ctx = raw;
72
+ if (typeof ctx.cwd !== 'string' || !ctx.cwd.trim()) {
73
+ return null;
74
+ }
75
+ const kind = ctx.kind;
76
+ if (kind !== 'shell' && kind !== 'tool' && kind !== 'subagent') {
77
+ return null;
78
+ }
79
+ return {
80
+ cwd: ctx.cwd,
81
+ kind,
82
+ ...(typeof ctx.command === 'string' ? { command: ctx.command } : {}),
83
+ ...(typeof ctx.toolName === 'string' ? { toolName: ctx.toolName } : {}),
84
+ ...(ctx.payload && typeof ctx.payload === 'object' && !Array.isArray(ctx.payload)
85
+ ? { payload: ctx.payload }
86
+ : {}),
87
+ };
88
+ }
@@ -1,5 +1,7 @@
1
+ import type { AuditActionSnapshot, AuditReplayContext } from './audit-replay-context.js';
1
2
  import type { Assessment } from './types.js';
2
- export declare const AUDIT_METRICS_SCHEMA_VERSION = 2;
3
+ export type { AuditActionSnapshot, AuditReplayContext } from './audit-replay-context.js';
4
+ export declare const AUDIT_METRICS_SCHEMA_VERSION = 3;
3
5
  export declare const GATE_EVENTS: Set<string>;
4
6
  export interface AuditRecord {
5
7
  timestamp?: string;
@@ -11,6 +13,7 @@ export interface AuditRecord {
11
13
  summary?: string;
12
14
  approvalId?: string;
13
15
  wouldBlock?: boolean;
16
+ judgeFallbackReason?: string;
14
17
  permission?: string;
15
18
  mode?: string;
16
19
  assessment?: Assessment;
@@ -21,6 +24,8 @@ export interface AuditRecord {
21
24
  transactionalCategories?: string[];
22
25
  transactionalChangeCount?: number;
23
26
  transactionalSkipReason?: string;
27
+ replayContext?: AuditReplayContext;
28
+ actionSnapshot?: AuditActionSnapshot;
24
29
  [key: string]: unknown;
25
30
  }
26
31
  export interface AuditFilter {
@@ -63,3 +68,21 @@ export interface NoisyRuleCandidate {
63
68
  approvedCount: number;
64
69
  approvalRate: number;
65
70
  }
71
+ export interface ReasonApprovalRatio {
72
+ reason: string;
73
+ wouldBlockCount: number;
74
+ approvedAfterDenyCount: number;
75
+ approvalRate: number;
76
+ }
77
+ export interface RepeatedFingerprintAsk {
78
+ fingerprint: string;
79
+ summary: string;
80
+ reason: string;
81
+ askCount: number;
82
+ }
83
+ export interface AvailabilityAskCounts {
84
+ total: number;
85
+ missingTrustedCwd: number;
86
+ judgeTimeout: number;
87
+ judgeFallback: number;
88
+ }
@@ -1,2 +1,2 @@
1
- export const AUDIT_METRICS_SCHEMA_VERSION = 2;
1
+ export const AUDIT_METRICS_SCHEMA_VERSION = 3;
2
2
  export const GATE_EVENTS = new Set(['beforeShellExecution', 'preToolUse', 'subagentGate']);
@@ -0,0 +1,13 @@
1
+ import type { ApprovalStateFile } from '../types.js';
2
+ export declare function mutateApprovalStateWithRetry<T>(params: {
3
+ load: () => Promise<{
4
+ filePath: string;
5
+ state: ApprovalStateFile;
6
+ }>;
7
+ write: (filePath: string, state: ApprovalStateFile) => Promise<void>;
8
+ mutate: (state: ApprovalStateFile) => {
9
+ state: ApprovalStateFile;
10
+ result: T;
11
+ } | null;
12
+ maxRetries?: number;
13
+ }): Promise<T | null>;
@@ -0,0 +1,92 @@
1
+ import { constants } from 'node:fs';
2
+ import { open, readFile, unlink } from 'node:fs/promises';
3
+ import { upgradeApprovalStateToV3 } from './approval-v3.js';
4
+ const DEFAULT_RETRIES = 8;
5
+ const LOCK_STALE_MS = 5 * 60_000;
6
+ async function sleep(ms) {
7
+ await new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+ async function isStaleApprovalLock(lockPath) {
10
+ try {
11
+ const raw = await readFile(lockPath, 'utf8');
12
+ const [pidText, timestampText] = raw.trim().split(':');
13
+ const timestamp = Number(timestampText);
14
+ if (Number.isFinite(timestamp) && Date.now() - timestamp > LOCK_STALE_MS) {
15
+ return true;
16
+ }
17
+ const pid = Number(pidText);
18
+ if (!Number.isFinite(pid)) {
19
+ return true;
20
+ }
21
+ try {
22
+ process.kill(pid, 0);
23
+ return false;
24
+ }
25
+ catch {
26
+ return true;
27
+ }
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ async function acquireApprovalStateLock(lockPath, maxRetries) {
34
+ for (let attempt = 0; attempt < maxRetries; attempt += 1) {
35
+ try {
36
+ const handle = await open(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
37
+ await handle.writeFile(`${process.pid}:${Date.now()}`);
38
+ await handle.close();
39
+ return async () => {
40
+ await unlink(lockPath).catch(() => { });
41
+ };
42
+ }
43
+ catch (error) {
44
+ const code = error.code;
45
+ if (code !== 'EEXIST') {
46
+ throw error;
47
+ }
48
+ if (await isStaleApprovalLock(lockPath)) {
49
+ await unlink(lockPath).catch(() => { });
50
+ }
51
+ await sleep(5 + attempt * 8);
52
+ }
53
+ }
54
+ throw new Error(`Failed to acquire approval state lock: ${lockPath}`);
55
+ }
56
+ export async function mutateApprovalStateWithRetry(params) {
57
+ const maxRetries = params.maxRetries ?? DEFAULT_RETRIES;
58
+ for (let attempt = 0; attempt < maxRetries; attempt += 1) {
59
+ const loaded = await params.load();
60
+ const lockPath = `${loaded.filePath}.lock`;
61
+ let release = null;
62
+ try {
63
+ release = await acquireApprovalStateLock(lockPath, maxRetries);
64
+ const base = upgradeApprovalStateToV3(loaded.state);
65
+ const revision = base.revision ?? 0;
66
+ const outcome = params.mutate(base);
67
+ if (!outcome) {
68
+ return null;
69
+ }
70
+ const next = {
71
+ ...outcome.state,
72
+ version: 3,
73
+ revision: revision + 1,
74
+ };
75
+ const verifyBeforeWrite = upgradeApprovalStateToV3((await params.load()).state);
76
+ if ((verifyBeforeWrite.revision ?? 0) !== revision) {
77
+ continue;
78
+ }
79
+ await params.write(loaded.filePath, next);
80
+ const verify = upgradeApprovalStateToV3((await params.load()).state);
81
+ if ((verify.revision ?? 0) === revision + 1) {
82
+ return outcome.result;
83
+ }
84
+ }
85
+ finally {
86
+ if (release) {
87
+ await release();
88
+ }
89
+ }
90
+ }
91
+ return null;
92
+ }
@@ -0,0 +1,15 @@
1
+ import type { ApprovalRecord, ApprovalStateFile } from '../types.js';
2
+ import type { CapabilityGrantV1 } from './grant.js';
3
+ import type { CapabilityRequestV1 } from './request.js';
4
+ export declare const APPROVAL_STATE_VERSION_V3: 3;
5
+ export declare function normalizeApprovalStateVersion(state: ApprovalStateFile): ApprovalStateFile;
6
+ export declare function upgradeApprovalStateToV3(state: ApprovalStateFile): ApprovalStateFile;
7
+ export declare function attachCapabilityEnvelope(approval: ApprovalRecord, capabilityRequests?: CapabilityRequestV1[]): ApprovalRecord;
8
+ export declare function mintCapabilityGrant(params: {
9
+ approval: ApprovalRecord;
10
+ capabilityRequests: CapabilityRequestV1[];
11
+ issuer?: string;
12
+ maxUses?: number;
13
+ }): CapabilityGrantV1 | null;
14
+ export declare function mintGrantForApprovedRecord(approval: ApprovalRecord): ApprovalRecord;
15
+ export declare function newGrantId(): string;