@guilz-dev/belay 0.3.0 → 0.4.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 (140) hide show
  1. package/dist/adapters/cursor/runtime-entry.js +1 -0
  2. package/dist/adapters/shared/gate-runtime.d.ts +11 -14
  3. package/dist/adapters/shared/gate-runtime.js +277 -19
  4. package/dist/bundle/claude-runtime.mjs +3138 -699
  5. package/dist/bundle/codex-runtime.mjs +3162 -711
  6. package/dist/bundle/cursor-runtime.mjs +3140 -701
  7. package/dist/cli.js +137 -5
  8. package/dist/commands/approve.d.ts +1 -0
  9. package/dist/commands/approve.js +45 -26
  10. package/dist/commands/config.js +135 -52
  11. package/dist/commands/doctor.js +11 -1
  12. package/dist/commands/explain.js +6 -1
  13. package/dist/commands/harvest.d.ts +29 -0
  14. package/dist/commands/harvest.js +72 -0
  15. package/dist/commands/judge.d.ts +11 -1
  16. package/dist/commands/judge.js +21 -2
  17. package/dist/commands/metrics.js +34 -2
  18. package/dist/commands/quality.d.ts +38 -0
  19. package/dist/commands/quality.js +105 -0
  20. package/dist/commands/simulate.d.ts +1 -0
  21. package/dist/commands/simulate.js +7 -2
  22. package/dist/commands/standing-allow.d.ts +10 -0
  23. package/dist/commands/standing-allow.js +26 -0
  24. package/dist/commands/tui.d.ts +48 -0
  25. package/dist/commands/tui.js +150 -0
  26. package/dist/config-io.js +10 -5
  27. package/dist/core/approval-replay-cli.d.ts +7 -0
  28. package/dist/core/approval-replay-cli.js +36 -0
  29. package/dist/core/approval-replay.d.ts +45 -0
  30. package/dist/core/approval-replay.js +141 -0
  31. package/dist/core/approval-service.d.ts +15 -0
  32. package/dist/core/approval-service.js +47 -2
  33. package/dist/core/approval.d.ts +24 -3
  34. package/dist/core/approval.js +47 -4
  35. package/dist/core/audit-analysis.d.ts +7 -1
  36. package/dist/core/audit-analysis.js +111 -0
  37. package/dist/core/audit-metrics.d.ts +7 -0
  38. package/dist/core/audit-metrics.js +24 -4
  39. package/dist/core/audit-query.d.ts +1 -0
  40. package/dist/core/audit-query.js +3 -0
  41. package/dist/core/audit-replay-context.d.ts +35 -0
  42. package/dist/core/audit-replay-context.js +88 -0
  43. package/dist/core/audit-types.d.ts +24 -1
  44. package/dist/core/audit-types.js +1 -1
  45. package/dist/core/capability/index.d.ts +2 -1
  46. package/dist/core/capability/index.js +1 -0
  47. package/dist/core/capability/paths.d.ts +2 -2
  48. package/dist/core/capability/paths.js +9 -9
  49. package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
  50. package/dist/core/capability/trusted-workspace-roots.js +149 -0
  51. package/dist/core/capability/types.d.ts +11 -1
  52. package/dist/core/capability-approval.d.ts +2 -1
  53. package/dist/core/capability-approval.js +100 -2
  54. package/dist/core/classify-subagent.js +2 -20
  55. package/dist/core/classify-tool.js +32 -3
  56. package/dist/core/config.d.ts +18 -0
  57. package/dist/core/config.js +49 -0
  58. package/dist/core/gate-engine.js +6 -6
  59. package/dist/core/harvest.d.ts +53 -0
  60. package/dist/core/harvest.js +276 -0
  61. package/dist/core/index.d.ts +9 -3
  62. package/dist/core/index.js +6 -2
  63. package/dist/core/judge-doctor.d.ts +3 -0
  64. package/dist/core/judge-doctor.js +54 -0
  65. package/dist/core/path-utils.d.ts +11 -0
  66. package/dist/core/path-utils.js +56 -6
  67. package/dist/core/reclassify.d.ts +3 -0
  68. package/dist/core/reclassify.js +44 -14
  69. package/dist/core/replay-scrub.d.ts +9 -0
  70. package/dist/core/replay-scrub.js +43 -0
  71. package/dist/core/shell-tokenizer.d.ts +2 -0
  72. package/dist/core/shell-tokenizer.js +82 -23
  73. package/dist/core/standing-allow.d.ts +50 -0
  74. package/dist/core/standing-allow.js +175 -0
  75. package/dist/core/types.d.ts +15 -0
  76. package/dist/core/verdict/adapter.js +10 -1
  77. package/dist/core/verdict/containment.d.ts +4 -3
  78. package/dist/core/verdict/containment.js +17 -12
  79. package/dist/core/verdict/judge-audit.d.ts +1 -0
  80. package/dist/core/verdict/judge-audit.js +32 -1
  81. package/dist/core/verdict/judge-baseline.d.ts +20 -0
  82. package/dist/core/verdict/judge-baseline.js +48 -0
  83. package/dist/core/verdict/judge-broker-service.d.ts +42 -0
  84. package/dist/core/verdict/judge-broker-service.js +279 -0
  85. package/dist/core/verdict/judge-cli-fingerprint.d.ts +5 -0
  86. package/dist/core/verdict/judge-cli-fingerprint.js +61 -0
  87. package/dist/core/verdict/judge-cli.d.ts +23 -0
  88. package/dist/core/verdict/judge-cli.js +137 -16
  89. package/dist/core/verdict/judge-factory.js +13 -4
  90. package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
  91. package/dist/core/verdict/judge-provider-matrix.js +66 -0
  92. package/dist/core/verdict/judge-runtime-config.d.ts +42 -0
  93. package/dist/core/verdict/judge-runtime-config.js +92 -0
  94. package/dist/core/verdict/judge-session-broker.d.ts +48 -0
  95. package/dist/core/verdict/judge-session-broker.js +195 -0
  96. package/dist/core/verdict/judge-session-guard.d.ts +27 -0
  97. package/dist/core/verdict/judge-session-guard.js +91 -0
  98. package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
  99. package/dist/core/verdict/judge-session-kill-switch.js +43 -0
  100. package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
  101. package/dist/core/verdict/judge-session-mutex.js +23 -0
  102. package/dist/core/verdict/judge-shadow.d.ts +24 -0
  103. package/dist/core/verdict/judge-shadow.js +116 -0
  104. package/dist/core/verdict/judge-transport.d.ts +40 -0
  105. package/dist/core/verdict/judge-transport.js +258 -0
  106. package/dist/core/verdict/judge.d.ts +14 -1
  107. package/dist/core/verdict/judge.js +6 -5
  108. package/dist/core/verdict/parser.js +6 -1
  109. package/dist/core/verdict/types.d.ts +13 -0
  110. package/dist/core/verdict/verdict.js +7 -4
  111. package/dist/corpus/adversarial-probe.d.ts +86 -0
  112. package/dist/corpus/adversarial-probe.js +220 -0
  113. package/dist/corpus/evaluate.d.ts +45 -12
  114. package/dist/corpus/evaluate.js +62 -5
  115. package/dist/corpus/gates.d.ts +35 -0
  116. package/dist/corpus/gates.js +81 -0
  117. package/dist/corpus/judge-accuracy.d.ts +8 -0
  118. package/dist/corpus/judge-accuracy.js +39 -0
  119. package/dist/corpus/must-allow-commands.d.ts +5 -0
  120. package/dist/corpus/must-allow-commands.js +13 -0
  121. package/dist/corpus/mutators.d.ts +28 -0
  122. package/dist/corpus/mutators.js +151 -0
  123. package/dist/corpus/ratchet.d.ts +42 -0
  124. package/dist/corpus/ratchet.js +116 -0
  125. package/dist/corpus/runtime-match.d.ts +19 -0
  126. package/dist/corpus/runtime-match.js +45 -0
  127. package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
  128. package/dist/corpus/standing-allow-catalog.generated.js +79 -0
  129. package/dist/corpus/types.d.ts +47 -0
  130. package/dist/corpus/types.js +157 -0
  131. package/dist/installer.js +4 -1
  132. package/dist/judge-broker-daemon.d.ts +1 -0
  133. package/dist/judge-broker-daemon.js +123 -0
  134. package/dist/services/sandbox-service.d.ts +1 -0
  135. package/dist/services/sandbox-service.js +2 -0
  136. package/dist/version.d.ts +1 -1
  137. package/dist/version.js +1 -1
  138. package/package.json +7 -3
  139. package/skills/belay/SKILL.md +7 -1
  140. package/skills/belay/belay-approve.md +17 -0
@@ -0,0 +1,276 @@
1
+ import { computeRepeatedFingerprintAsks, isAvailabilityCausedAsk } from './audit-analysis.js';
2
+ import { buildApprovalRoundTrips, filterAuditRecords, inferWouldBlock, isApprovalRecorded, isShellGateRecord, parseTimestamp, } from './audit-query.js';
3
+ import { matchesCustomCommand } from './custom-command-match.js';
4
+ export const HARVEST_REPORT_SCHEMA_VERSION = 1;
5
+ const READ_STYLE_COMMAND_PATTERN = /^\s*(git\s+status|git\s+diff|git\s+log|git\s+show|ls\b|cat\b|head\b|tail\b|find\b|rg\b|grep\b|fd\b|bat\b|less\b|more\b|pwd\b|whoami\b|env\b|printenv\b|npm\s+(test|run\s+test)|pnpm\s+(test|run\s+test)|yarn\s+test)/;
6
+ function shellRecords(records) {
7
+ return records.filter(isShellGateRecord);
8
+ }
9
+ function availabilitySignal(record) {
10
+ if (!isAvailabilityCausedAsk(record)) {
11
+ return null;
12
+ }
13
+ if (record.reason === 'missing_trusted_cwd') {
14
+ return 'missing_trusted_cwd';
15
+ }
16
+ const fallback = typeof record.judgeFallbackReason === 'string' ? record.judgeFallbackReason : '';
17
+ if (fallback.includes('timeout')) {
18
+ return 'judge_timeout';
19
+ }
20
+ return 'judge_fallback';
21
+ }
22
+ function commandFromRecord(record) {
23
+ return (record.summary ?? '').trim();
24
+ }
25
+ function hasReadStyleSignal(summary) {
26
+ return READ_STYLE_COMMAND_PATTERN.test(summary);
27
+ }
28
+ function upsertCandidate(map, params) {
29
+ const existing = map.get(params.fingerprint);
30
+ if (existing) {
31
+ if (!existing.sources.includes(params.source)) {
32
+ existing.sources.push(params.source);
33
+ }
34
+ existing.askCount = Math.max(existing.askCount, params.askCount ?? 1);
35
+ existing.approvedAfterDeny = existing.approvedAfterDeny || Boolean(params.approvedAfterDeny);
36
+ if (params.command) {
37
+ existing.command = params.command;
38
+ }
39
+ if (params.reason) {
40
+ existing.reason = params.reason;
41
+ }
42
+ return;
43
+ }
44
+ map.set(params.fingerprint, {
45
+ kind: 'shell',
46
+ command: params.command,
47
+ fingerprint: params.fingerprint,
48
+ reason: params.reason,
49
+ sources: [params.source],
50
+ askCount: params.askCount ?? 1,
51
+ approvedAfterDeny: Boolean(params.approvedAfterDeny),
52
+ });
53
+ }
54
+ export function extractAvailabilityQueue(records) {
55
+ const grouped = new Map();
56
+ for (const record of shellRecords(records)) {
57
+ if (!inferWouldBlock(record) || !record.fingerprint) {
58
+ continue;
59
+ }
60
+ const signal = availabilitySignal(record);
61
+ if (!signal) {
62
+ continue;
63
+ }
64
+ const fingerprint = record.fingerprint;
65
+ const existing = grouped.get(fingerprint);
66
+ if (existing) {
67
+ existing.askCount += 1;
68
+ existing.command = commandFromRecord(record) || existing.command;
69
+ existing.reason = record.reason ?? existing.reason;
70
+ continue;
71
+ }
72
+ grouped.set(fingerprint, {
73
+ kind: 'shell',
74
+ command: commandFromRecord(record),
75
+ fingerprint,
76
+ reason: record.reason ?? 'unknown',
77
+ availabilitySignal: signal,
78
+ ...(typeof record.judgeFallbackReason === 'string'
79
+ ? { judgeFallbackReason: record.judgeFallbackReason }
80
+ : {}),
81
+ askCount: 1,
82
+ });
83
+ }
84
+ return [...grouped.values()].sort((left, right) => right.askCount - left.askCount);
85
+ }
86
+ function recordsForShellRoundTrips(records) {
87
+ return records.filter((record) => isApprovalRecorded(record) ||
88
+ (isShellGateRecord(record) && inferWouldBlock(record) && !isAvailabilityCausedAsk(record)));
89
+ }
90
+ /** Pair deny/approval rows split by a time window so round-trip harvest still works. */
91
+ function augmentHarvestRoundTripPairs(scoped, timeFiltered) {
92
+ if (timeFiltered.length === 0) {
93
+ return timeFiltered;
94
+ }
95
+ const included = new Set(timeFiltered);
96
+ const augmented = [...timeFiltered];
97
+ const approvalById = new Map();
98
+ const denyByApprovalId = new Map();
99
+ for (const record of scoped) {
100
+ if (!record.approvalId) {
101
+ continue;
102
+ }
103
+ if (isApprovalRecorded(record)) {
104
+ approvalById.set(record.approvalId, record);
105
+ }
106
+ else if (isShellGateRecord(record) && inferWouldBlock(record)) {
107
+ denyByApprovalId.set(record.approvalId, record);
108
+ }
109
+ }
110
+ for (const record of timeFiltered) {
111
+ if (!record.approvalId) {
112
+ continue;
113
+ }
114
+ if (isApprovalRecorded(record)) {
115
+ const deny = denyByApprovalId.get(record.approvalId);
116
+ if (deny && !included.has(deny)) {
117
+ included.add(deny);
118
+ augmented.push(deny);
119
+ }
120
+ continue;
121
+ }
122
+ if (isShellGateRecord(record)) {
123
+ const approval = approvalById.get(record.approvalId);
124
+ if (approval && !included.has(approval)) {
125
+ included.add(approval);
126
+ augmented.push(approval);
127
+ }
128
+ }
129
+ }
130
+ return augmented.sort((left, right) => {
131
+ const leftMs = parseTimestamp(left.timestamp) ?? 0;
132
+ const rightMs = parseTimestamp(right.timestamp) ?? 0;
133
+ return leftMs - rightMs;
134
+ });
135
+ }
136
+ /** Shell gate rows plus approval events — matches what `belay harvest list` needs. */
137
+ export function filterRecordsForHarvest(records, options = {}) {
138
+ const scoped = records.filter((record) => isShellGateRecord(record) || isApprovalRecorded(record));
139
+ if (!options.since && !options.until) {
140
+ return scoped;
141
+ }
142
+ const timeFiltered = filterAuditRecords(scoped, {
143
+ since: options.since,
144
+ until: options.until,
145
+ });
146
+ return augmentHarvestRoundTripPairs(scoped, timeFiltered);
147
+ }
148
+ export function extractHarvestCandidates(records, options = {}) {
149
+ const shellOnly = shellRecords(records);
150
+ const classifierAsks = shellOnly.filter((record) => inferWouldBlock(record) && !isAvailabilityCausedAsk(record));
151
+ const map = new Map();
152
+ // `unknown` covers legacy shell rows missing `kind`.
153
+ const roundTrips = buildApprovalRoundTrips(recordsForShellRoundTrips(records)).filter((trip) => trip.kind === 'shell' || trip.kind === 'unknown');
154
+ for (const trip of roundTrips) {
155
+ if (!trip.fingerprint || !trip.approvalTimestamp) {
156
+ continue;
157
+ }
158
+ upsertCandidate(map, {
159
+ fingerprint: trip.fingerprint,
160
+ command: trip.summary,
161
+ reason: trip.reason,
162
+ source: 'deny_then_approve',
163
+ approvedAfterDeny: true,
164
+ });
165
+ }
166
+ for (const entry of computeRepeatedFingerprintAsks(classifierAsks, 2, 50)) {
167
+ upsertCandidate(map, {
168
+ fingerprint: entry.fingerprint,
169
+ command: entry.summary,
170
+ reason: entry.reason,
171
+ source: 'repeated_ask',
172
+ askCount: entry.askCount,
173
+ });
174
+ }
175
+ for (const record of classifierAsks) {
176
+ if (!record.fingerprint) {
177
+ continue;
178
+ }
179
+ const command = commandFromRecord(record);
180
+ if (!hasReadStyleSignal(command)) {
181
+ continue;
182
+ }
183
+ upsertCandidate(map, {
184
+ fingerprint: record.fingerprint,
185
+ command,
186
+ reason: record.reason ?? 'unknown',
187
+ source: 'read_style_signal',
188
+ });
189
+ }
190
+ // Current overrides.allow match on past would-blocks — not a changelog of new additions.
191
+ const allowPatterns = options.allowPatterns ?? [];
192
+ for (const record of classifierAsks) {
193
+ if (!record.fingerprint) {
194
+ continue;
195
+ }
196
+ const command = commandFromRecord(record);
197
+ if (!command) {
198
+ continue;
199
+ }
200
+ for (const pattern of allowPatterns) {
201
+ if (matchesCustomCommand(command, command, pattern)) {
202
+ upsertCandidate(map, {
203
+ fingerprint: record.fingerprint,
204
+ command,
205
+ reason: record.reason ?? 'unknown',
206
+ source: 'overrides_allow',
207
+ });
208
+ break;
209
+ }
210
+ }
211
+ }
212
+ return [...map.values()].sort((left, right) => {
213
+ if (right.askCount !== left.askCount) {
214
+ return right.askCount - left.askCount;
215
+ }
216
+ return left.command.localeCompare(right.command);
217
+ });
218
+ }
219
+ export function buildHarvestReport(records, options = {}) {
220
+ return {
221
+ schemaVersion: HARVEST_REPORT_SCHEMA_VERSION,
222
+ scope: 'shell',
223
+ candidates: extractHarvestCandidates(records, options),
224
+ availabilityQueue: extractAvailabilityQueue(records),
225
+ };
226
+ }
227
+ export function applyHarvestReview(cases, params) {
228
+ const command = params.command.trim();
229
+ if (!command) {
230
+ return { cases, applied: false, ok: false, message: 'Command must be non-empty.' };
231
+ }
232
+ if (params.outcome === 'reject') {
233
+ return {
234
+ cases,
235
+ applied: false,
236
+ ok: true,
237
+ message: `Reviewed and rejected candidate ${JSON.stringify(command)}.`,
238
+ };
239
+ }
240
+ const category = params.outcome;
241
+ const verdict = category === 'provably-benign' ? 'allow' : 'allow_flagged';
242
+ const reason = params.reason?.trim();
243
+ const duplicate = cases.find((entry) => entry.command === command);
244
+ if (duplicate) {
245
+ if (duplicate.category === category) {
246
+ return {
247
+ cases,
248
+ applied: false,
249
+ ok: false,
250
+ message: `Corpus already contains ${JSON.stringify(command)} as ${category}.`,
251
+ };
252
+ }
253
+ return {
254
+ cases,
255
+ applied: false,
256
+ ok: false,
257
+ message: `Corpus already contains ${JSON.stringify(command)} as ${duplicate.category}; resolve manually.`,
258
+ };
259
+ }
260
+ const nextCase = {
261
+ kind: 'shell',
262
+ category,
263
+ command,
264
+ verdict,
265
+ ...(reason ? { reason } : {}),
266
+ };
267
+ const followUp = category === 'provably-benign'
268
+ ? 'Next: run `pnpm corpus` and confirm hard gates pass before `pnpm build` refreshes the standing-allow catalog.'
269
+ : 'Next: run `pnpm corpus` to verify corpus evaluation (accepted-benign is soft-gated).';
270
+ return {
271
+ cases: [...cases, nextCase],
272
+ applied: true,
273
+ ok: true,
274
+ message: `Added ${JSON.stringify(command)} to corpus as ${category}. ${followUp}`,
275
+ };
276
+ }
@@ -1,9 +1,14 @@
1
- export { APPROVAL_EXECUTION_LEASE_MS, approvalCommandMatch, buildRetryInstruction, compactApprovals, createApprovalRecord, isExecutionLeaseExpired, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
1
+ export { APPROVAL_EXECUTION_LEASE_MS, approvalCommandMatch, compactApprovals, createApprovalRecord, createApprovalRecordWithEnvelope, isExecutionLeaseExpired, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
2
+ export type { ApprovalReplayHint, ReplayActionContext, ReplayAdapterId, } from './approval-replay.js';
3
+ /** @deprecated Use `buildRetryInstructionForConfig` */
4
+ export { approvalFlow, buildApprovalRecordedMessage, buildReplayEnvelopeFields, buildReplayHint, buildRetryInstructionForConfig, buildRetryInstructionForConfig as buildRetryInstruction, canAutoReplay, getExecutionLeaseMs, replayShellCommand, validateReplayEnvelope, } from './approval-replay.js';
5
+ export type { ApprovalStore } from './approval-service.js';
6
+ export { consumeApprovedAfterCliReplay, createGateApprovalStore, gateApprovalStoreFromDeps, recordApproval, } from './approval-service.js';
2
7
  export type { AuditMetricsReport } from './audit-metrics.js';
3
8
  export { computeAuditMetrics, parseAuditNdjson } from './audit-metrics.js';
4
9
  export { classifySubagent } from './classify-subagent.js';
5
10
  export { classifyToolUse } from './classify-tool.js';
6
- export { approvedApprovalsFile, type BelayConfig, type BelayConfigV1, type BelayConfigV2, type BelayConfigV3, type BelayConfigV4, type BelayControlPlaneConfig, type BelayJudgeConfig, type BelayOverridesConfig, type BelayPolicyConfig, type BelayRedactionConfig, belayStateDir, classifierOptionsFromConfig, configuredControlPlaneDir, DEFAULT_CONFIG_V2, DEFAULT_CONFIG_V3, DEFAULT_CONFIG_V4, DEFAULT_JUDGE_CURSOR_COMPOSER, DEFAULT_JUDGE_LOCAL_OLLAMA, defaultControlPlaneDir, isConfigV4, isFreshConfigInput, LEGACY_POLICY_V3, mapLegacyClassifierToOverrides, mergeConfig, migrateConfig, migrateV2ToV3, normalizeConfig, normalizeJudgeConfig, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, } from './config.js';
11
+ export { type ApprovalFlow, approvedApprovalsFile, type BelayApprovalAutoReplayScopes, type BelayApprovalConfig, type BelayConfig, type BelayConfigV1, type BelayConfigV2, type BelayConfigV3, type BelayConfigV4, type BelayControlPlaneConfig, type BelayJudgeConfig, type BelayOverridesConfig, type BelayPolicyConfig, type BelayRedactionConfig, belayStateDir, classifierOptionsFromConfig, configuredControlPlaneDir, DEFAULT_APPROVAL_CONFIG, DEFAULT_CONFIG_V2, DEFAULT_CONFIG_V3, DEFAULT_CONFIG_V4, DEFAULT_JUDGE_CURSOR_COMPOSER, DEFAULT_JUDGE_LOCAL_OLLAMA, defaultControlPlaneDir, isConfigV4, isFreshConfigInput, LEGACY_POLICY_V3, mapLegacyClassifierToOverrides, mergeConfig, migrateConfig, migrateV2ToV3, normalizeConfig, normalizeJudgeConfig, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, } from './config.js';
7
12
  export { matchesCustomCommand } from './custom-command-match.js';
8
13
  export { canonicalStringify, hashValue, shellFingerprint, subagentFingerprint, toolFingerprint, } from './fingerprint.js';
9
14
  export type { GatedAction, GatedActionKind, GatePermissionResponse, GateVerdict, } from './gate-contract.js';
@@ -11,9 +16,10 @@ export { classifyResultToGateVerdict, GATE_CONTRACT_VERSION, isGatedAction, unno
11
16
  export { classifyGatedAction, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from './gate-engine.js';
12
17
  export { matchesSensitivePath } from './glob.js';
13
18
  export { canonicalPath, hasOutsideRepoPath, normalizeToken, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, } from './path-utils.js';
19
+ export { fingerprintReplayPayload, subagentFingerprintSource } from './replay-scrub.js';
14
20
  export { scrubString, scrubValue } from './scrub.js';
15
21
  export { findCommandSubstitutions, MAX_SUBSTITUTION_DEPTH } from './shell-substitution.js';
16
22
  export type { TransactionalDiffEvaluation, TransactionalExecutionResult, } from './transactional/index.js';
17
23
  export { isTransactionalEligible, runTransactionalExecution } from './transactional/index.js';
18
- export type { ApprovalRecord, ApprovalStateFile, Assessment, ClassifierOptions, ClassifyResult, HookVerdict, Reversibility, ScrubOptions, UnknownLocalEffectPolicy, } from './types.js';
24
+ export type { ApprovalRecord, ApprovalScopeHint, ApprovalStateFile, Assessment, ClassifierOptions, ClassifyResult, HookVerdict, Reversibility, ScrubOptions, UnknownLocalEffectPolicy, } from './types.js';
19
25
  export { buildVerdictContext, classifyShell, verdict, verdictToClassifyResult, } from './verdict/index.js';
@@ -1,14 +1,18 @@
1
- export { APPROVAL_EXECUTION_LEASE_MS, approvalCommandMatch, buildRetryInstruction, compactApprovals, createApprovalRecord, isExecutionLeaseExpired, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
1
+ export { APPROVAL_EXECUTION_LEASE_MS, approvalCommandMatch, compactApprovals, createApprovalRecord, createApprovalRecordWithEnvelope, isExecutionLeaseExpired, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
2
+ /** @deprecated Use `buildRetryInstructionForConfig` */
3
+ export { approvalFlow, buildApprovalRecordedMessage, buildReplayEnvelopeFields, buildReplayHint, buildRetryInstructionForConfig, buildRetryInstructionForConfig as buildRetryInstruction, canAutoReplay, getExecutionLeaseMs, replayShellCommand, validateReplayEnvelope, } from './approval-replay.js';
4
+ export { consumeApprovedAfterCliReplay, createGateApprovalStore, gateApprovalStoreFromDeps, recordApproval, } from './approval-service.js';
2
5
  export { computeAuditMetrics, parseAuditNdjson } from './audit-metrics.js';
3
6
  export { classifySubagent } from './classify-subagent.js';
4
7
  export { classifyToolUse } from './classify-tool.js';
5
- export { approvedApprovalsFile, belayStateDir, classifierOptionsFromConfig, configuredControlPlaneDir, DEFAULT_CONFIG_V2, DEFAULT_CONFIG_V3, DEFAULT_CONFIG_V4, DEFAULT_JUDGE_CURSOR_COMPOSER, DEFAULT_JUDGE_LOCAL_OLLAMA, defaultControlPlaneDir, isConfigV4, isFreshConfigInput, LEGACY_POLICY_V3, mapLegacyClassifierToOverrides, mergeConfig, migrateConfig, migrateV2ToV3, normalizeConfig, normalizeJudgeConfig, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, } from './config.js';
8
+ export { approvedApprovalsFile, belayStateDir, classifierOptionsFromConfig, configuredControlPlaneDir, DEFAULT_APPROVAL_CONFIG, DEFAULT_CONFIG_V2, DEFAULT_CONFIG_V3, DEFAULT_CONFIG_V4, DEFAULT_JUDGE_CURSOR_COMPOSER, DEFAULT_JUDGE_LOCAL_OLLAMA, defaultControlPlaneDir, isConfigV4, isFreshConfigInput, LEGACY_POLICY_V3, mapLegacyClassifierToOverrides, mergeConfig, migrateConfig, migrateV2ToV3, normalizeConfig, normalizeJudgeConfig, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, } from './config.js';
6
9
  export { matchesCustomCommand } from './custom-command-match.js';
7
10
  export { canonicalStringify, hashValue, shellFingerprint, subagentFingerprint, toolFingerprint, } from './fingerprint.js';
8
11
  export { classifyResultToGateVerdict, GATE_CONTRACT_VERSION, isGatedAction, unnormalizedGateVerdict, } from './gate-contract.js';
9
12
  export { classifyGatedAction, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from './gate-engine.js';
10
13
  export { matchesSensitivePath } from './glob.js';
11
14
  export { canonicalPath, hasOutsideRepoPath, normalizeToken, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, } from './path-utils.js';
15
+ export { fingerprintReplayPayload, subagentFingerprintSource } from './replay-scrub.js';
12
16
  export { scrubString, scrubValue } from './scrub.js';
13
17
  export { findCommandSubstitutions, MAX_SUBSTITUTION_DEPTH } from './shell-substitution.js';
14
18
  export { isTransactionalEligible, runTransactionalExecution } from './transactional/index.js';
@@ -8,5 +8,8 @@ export interface JudgeDoctorResult {
8
8
  }
9
9
  export interface DiagnoseJudgeOptions {
10
10
  discoveryDeps?: JudgeModelDiscoveryDeps;
11
+ liveProbe?: boolean;
11
12
  }
12
13
  export declare function diagnoseJudge(config: BelayConfigV4, repoRoot?: string, options?: DiagnoseJudgeOptions): Promise<JudgeDoctorResult>;
14
+ export declare function stopJudgeSessionBrokers(repoRoot: string, stateDir: string): Promise<number>;
15
+ export declare function stopInProcessJudgeSessionBrokers(repoRoot?: string): number;
@@ -5,9 +5,13 @@ import { hasValidCloudConsent } from './judge-config.js';
5
5
  import { discoverJudgeModels, modelPresenceFromDiscovery, } from './judge-model-discovery.js';
6
6
  import { detectJudgeRuntimeCapabilities, resolveJudgeTransport } from './judge-runtime-detection.js';
7
7
  import { createOllamaJudge, createOpenAiCompatibleJudge } from './verdict/judge.js';
8
+ import { stopJudgeBrokerDaemon } from './verdict/judge-broker-service.js';
8
9
  import { getJudgeProviderCapabilities, getJudgeProviderSpec, isRemovedProviderId, normalizeLegacyProviderId, } from './verdict/judge-catalog.js';
9
10
  import { createJudgeFromConfig, resolveJudgeModel } from './verdict/judge-factory.js';
11
+ import { listRepoJudgeSessionBrokers, stopRepoJudgeSessionBroker, } from './verdict/judge-session-broker.js';
12
+ import { clearJudgeSessionKillSwitch } from './verdict/judge-session-kill-switch.js';
10
13
  export async function diagnoseJudge(config, repoRoot = process.cwd(), options = {}) {
14
+ const shouldRunLiveProbe = options.liveProbe === true && !process.env.VITEST && !process.env.VITEST_WORKER_ID;
11
15
  const issues = [];
12
16
  const warnings = [];
13
17
  const notes = [];
@@ -119,6 +123,29 @@ export async function diagnoseJudge(config, repoRoot = process.cwd(), options =
119
123
  }
120
124
  else {
121
125
  notes.push(`Native CLI transport available: ${transport}`);
126
+ if (shouldRunLiveProbe) {
127
+ const smokeConfig = {
128
+ ...config,
129
+ judge: {
130
+ ...judge,
131
+ timeoutMs: Math.min(judge.timeoutMs, 5000),
132
+ },
133
+ };
134
+ const smokeJudge = createJudgeFromConfig(smokeConfig, { repoRoot });
135
+ const smokeResult = await smokeJudge.evaluate({
136
+ text: 'git status',
137
+ context: { cwd: repoRoot, repoRoot },
138
+ });
139
+ const smokeFallback = smokeJudge.lastTrace?.judgeFallbackReason ??
140
+ smokeJudge.lastTrace?.fallbackReason ??
141
+ smokeResult.reason;
142
+ if (smokeJudge.lastTrace?.provider === 'fallback') {
143
+ issues.push(`Native CLI transport smoke probe failed (${smokeFallback}). Tier1 will fail closed to ask.`);
144
+ }
145
+ else {
146
+ notes.push('Native CLI transport smoke probe succeeded.');
147
+ }
148
+ }
122
149
  }
123
150
  return { issues, warnings, notes, modelCheck };
124
151
  }
@@ -171,5 +198,32 @@ export async function diagnoseJudge(config, repoRoot = process.cwd(), options =
171
198
  if (factoryJudge.lastTrace?.transport) {
172
199
  notes.push(`Factory transport: ${factoryJudge.lastTrace.transport}`);
173
200
  }
201
+ if (config.judge.runtime?.session.enabled) {
202
+ notes.push('Judge session transport: enabled');
203
+ const activeBrokers = listRepoJudgeSessionBrokers();
204
+ if (activeBrokers.includes(repoRoot)) {
205
+ notes.push(`Active session broker for repo: ${repoRoot}`);
206
+ }
207
+ }
208
+ else {
209
+ notes.push('Judge session transport: disabled (default)');
210
+ }
174
211
  return { issues, warnings, notes, modelCheck };
175
212
  }
213
+ export async function stopJudgeSessionBrokers(repoRoot, stateDir) {
214
+ let stopped = 0;
215
+ stopped += stopRepoJudgeSessionBroker(repoRoot, 'manual_stop');
216
+ stopped += await stopJudgeBrokerDaemon(stateDir);
217
+ await clearJudgeSessionKillSwitch(stateDir);
218
+ return stopped;
219
+ }
220
+ export function stopInProcessJudgeSessionBrokers(repoRoot) {
221
+ if (repoRoot) {
222
+ return stopRepoJudgeSessionBroker(repoRoot, 'manual_stop');
223
+ }
224
+ let stopped = 0;
225
+ for (const root of listRepoJudgeSessionBrokers()) {
226
+ stopped += stopRepoJudgeSessionBroker(root, 'manual_stop');
227
+ }
228
+ return stopped;
229
+ }
@@ -7,6 +7,17 @@
7
7
  export declare function canonicalPath(targetPath: string): string;
8
8
  export declare function pathWithinRoot(root: string, targetPath: string): boolean;
9
9
  export declare function relativeWithinRepo(repoRoot: string, targetPath: string): string | null;
10
+ export type WorkspaceRootMatch = {
11
+ kind: 'repo';
12
+ root: string;
13
+ relativePath: string;
14
+ } | {
15
+ kind: 'trusted';
16
+ root: string;
17
+ relativePath: string;
18
+ };
19
+ export declare function resolveWorkspaceRootMatch(repoRoot: string, trustedRoots: string[] | undefined, targetPath: string): WorkspaceRootMatch | null;
10
20
  export declare function normalizeToken(token: string, repoRoot: string): string;
11
21
  export declare function resolveMutationTarget(token: string, cwd: string): string | null;
12
22
  export declare function hasOutsideRepoPath(tokens: string[], cwd: string, repoRoot: string): boolean;
23
+ export declare function containingGitRoot(targetPath: string): string | null;
@@ -1,5 +1,6 @@
1
- import { existsSync, realpathSync } from 'node:fs';
1
+ import { existsSync, realpathSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { isFdDuplication, isRedirectOperator } from './shell-tokenizer.js';
3
4
  /**
4
5
  * Resolve symlinks for the longest existing prefix of `targetPath`, then append
5
6
  * any non-existent suffix without further resolution. Keeps path comparisons
@@ -44,18 +45,38 @@ export function pathWithinRoot(root, targetPath) {
44
45
  }
45
46
  return !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
46
47
  }
47
- export function relativeWithinRepo(repoRoot, targetPath) {
48
- const resolvedRoot = canonicalPath(repoRoot);
48
+ function relativeWithinRoot(root, targetPath, options = {}) {
49
+ const resolvedRoot = options.canonicalizeRoot === false ? path.resolve(root) : canonicalPath(root);
49
50
  const resolvedTarget = canonicalPath(targetPath);
50
51
  const relativePath = path.relative(resolvedRoot, resolvedTarget);
51
52
  if (relativePath === '') {
52
53
  return '.';
53
54
  }
54
- if (relativePath.startsWith('..')) {
55
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
55
56
  return null;
56
57
  }
57
58
  return relativePath;
58
59
  }
60
+ export function relativeWithinRepo(repoRoot, targetPath) {
61
+ return relativeWithinRoot(repoRoot, targetPath);
62
+ }
63
+ export function resolveWorkspaceRootMatch(repoRoot, trustedRoots = [], targetPath) {
64
+ const canonicalRepoRoot = canonicalPath(repoRoot);
65
+ const repoRelative = relativeWithinRoot(repoRoot, targetPath);
66
+ if (repoRelative !== null) {
67
+ return { kind: 'repo', root: canonicalRepoRoot, relativePath: repoRelative };
68
+ }
69
+ const normalizedTrustedRoots = [...new Set(trustedRoots.map((root) => path.resolve(root)))]
70
+ .filter((root) => root !== canonicalRepoRoot)
71
+ .sort((left, right) => right.length - left.length);
72
+ for (const root of normalizedTrustedRoots) {
73
+ const trustedRelative = relativeWithinRoot(root, targetPath, { canonicalizeRoot: false });
74
+ if (trustedRelative !== null) {
75
+ return { kind: 'trusted', root, relativePath: trustedRelative };
76
+ }
77
+ }
78
+ return null;
79
+ }
59
80
  export function normalizeToken(token, repoRoot) {
60
81
  if (!path.isAbsolute(token)) {
61
82
  return token;
@@ -67,7 +88,14 @@ export function resolveMutationTarget(token, cwd) {
67
88
  if (!token || token === '--' || token.startsWith('-')) {
68
89
  return null;
69
90
  }
70
- if (token === '2>' || token === '1>' || token === '&>' || token === '1>>' || token === '2>>') {
91
+ if (isRedirectOperator(token) ||
92
+ isFdDuplication(token) ||
93
+ token === '&&' ||
94
+ token === '||' ||
95
+ token === '|' ||
96
+ token === '|&' ||
97
+ token === ';' ||
98
+ token === '&') {
71
99
  return null;
72
100
  }
73
101
  if (path.isAbsolute(token)) {
@@ -102,6 +130,28 @@ export function hasOutsideRepoPath(tokens, cwd, repoRoot) {
102
130
  if (!resolved) {
103
131
  return false;
104
132
  }
105
- return relativeWithinRepo(repoRoot, resolved) === null;
133
+ return resolveWorkspaceRootMatch(repoRoot, [], resolved) === null;
106
134
  });
107
135
  }
136
+ export function containingGitRoot(targetPath) {
137
+ const resolved = canonicalPath(targetPath);
138
+ let current = resolved;
139
+ try {
140
+ if (!statSync(current).isDirectory()) {
141
+ current = path.dirname(current);
142
+ }
143
+ }
144
+ catch {
145
+ current = path.dirname(current);
146
+ }
147
+ while (true) {
148
+ if (existsSync(path.join(current, '.git'))) {
149
+ return current;
150
+ }
151
+ const parent = path.dirname(current);
152
+ if (parent === current) {
153
+ return null;
154
+ }
155
+ current = parent;
156
+ }
157
+ }
@@ -6,6 +6,8 @@ export interface ReclassifyDiff {
6
6
  event?: string;
7
7
  summary?: string;
8
8
  fingerprint?: string;
9
+ replayCwd?: string;
10
+ replayKind?: string;
9
11
  previousVerdict: string;
10
12
  previousReason: string;
11
13
  nextVerdict: string;
@@ -13,3 +15,4 @@ export interface ReclassifyDiff {
13
15
  }
14
16
  export declare function reclassifyAuditRecord(record: AuditRecord, config: BelayConfigV3, repoRoot: string): Promise<ClassifyResult | null>;
15
17
  export declare function diffReclassification(record: AuditRecord, config: BelayConfigV3, repoRoot: string): Promise<ReclassifyDiff | null>;
18
+ export declare function countMissingActionSnapshots(records: AuditRecord[]): number;
@@ -1,6 +1,7 @@
1
1
  import { getAdapter } from '../adapters/registry.js';
2
2
  import { repoShellClassifierOptions } from '../adapters/shared/gate-runtime.js';
3
3
  import { detectAdapterName } from '../config-io.js';
4
+ import { hashReplayPayload, parseAuditActionSnapshot, parseAuditReplayContext, } from './audit-replay-context.js';
4
5
  import { GATE_EVENTS } from './audit-types.js';
5
6
  import { classifyGatedAction, normalizeGatedAction } from './gate-engine.js';
6
7
  function shellCommandFromSummary(summary) {
@@ -11,47 +12,68 @@ function classifierOptionsForRepo(config, repoRoot) {
11
12
  const adapter = getAdapter(config.adapter ?? detectAdapterName(repoRoot));
12
13
  return repoShellClassifierOptions(config, repoRoot, adapter.layout);
13
14
  }
15
+ function trustedReplayContext(snapshot, replay) {
16
+ if (!replay) {
17
+ return null;
18
+ }
19
+ if (snapshot?.payloadHash &&
20
+ replay.payload &&
21
+ hashReplayPayload(replay.payload) !== snapshot.payloadHash) {
22
+ return { ...replay, payload: undefined };
23
+ }
24
+ return replay;
25
+ }
14
26
  export async function reclassifyAuditRecord(record, config, repoRoot) {
15
27
  if (!record.event || !GATE_EVENTS.has(record.event)) {
16
28
  return null;
17
29
  }
18
- const kind = record.kind === 'tool' || record.kind === 'subagent' ? record.kind : 'shell';
30
+ const snapshot = parseAuditActionSnapshot(record);
31
+ const replay = trustedReplayContext(snapshot, parseAuditReplayContext(record));
32
+ const kind = snapshot?.kind ??
33
+ replay?.kind ??
34
+ (record.kind === 'tool' || record.kind === 'subagent' ? record.kind : 'shell');
19
35
  const summary = record.summary ?? '';
36
+ const cwd = snapshot?.cwd ?? replay?.cwd ?? repoRoot;
20
37
  try {
21
38
  if (kind === 'shell') {
22
- const command = shellCommandFromSummary(summary);
39
+ const command = snapshot?.normalizedAction ?? replay?.command ?? shellCommandFromSummary(summary);
23
40
  if (!command) {
24
41
  return null;
25
42
  }
26
43
  const action = normalizeGatedAction({
27
44
  kind: 'shell',
28
45
  repoRoot,
29
- cwd: repoRoot,
46
+ cwd,
30
47
  command,
31
48
  });
32
49
  return await classifyGatedAction(action, config, classifierOptionsForRepo(config, repoRoot));
33
50
  }
34
51
  if (kind === 'subagent') {
52
+ const payload = replay?.payload ??
53
+ {
54
+ tool_name: 'Task',
55
+ tool_input: { description: summary },
56
+ };
35
57
  const action = normalizeGatedAction({
36
58
  kind: 'subagent',
37
59
  repoRoot,
38
- cwd: repoRoot,
39
- payload: {
40
- tool_name: 'Task',
41
- tool_input: { description: summary },
42
- },
60
+ cwd,
61
+ payload,
43
62
  });
44
63
  return await classifyGatedAction(action, config, classifierOptionsForRepo(config, repoRoot));
45
64
  }
65
+ const toolName = snapshot?.toolName ?? replay?.toolName ?? 'Shell';
66
+ const payload = replay?.payload ??
67
+ {
68
+ tool_name: toolName,
69
+ tool_input: { command: snapshot?.normalizedAction ?? replay?.command ?? summary },
70
+ };
46
71
  const action = normalizeGatedAction({
47
72
  kind: 'tool',
48
73
  repoRoot,
49
- cwd: repoRoot,
50
- toolName: 'Shell',
51
- payload: {
52
- tool_name: 'Shell',
53
- tool_input: { command: summary },
54
- },
74
+ cwd,
75
+ toolName,
76
+ payload,
55
77
  });
56
78
  return await classifyGatedAction(action, config, classifierOptionsForRepo(config, repoRoot));
57
79
  }
@@ -69,14 +91,22 @@ export async function diffReclassification(record, config, repoRoot) {
69
91
  if (previousVerdict === next.verdict && previousReason === next.reason) {
70
92
  return null;
71
93
  }
94
+ const snapshot = parseAuditActionSnapshot(record);
95
+ const replay = parseAuditReplayContext(record);
72
96
  return {
73
97
  timestamp: record.timestamp,
74
98
  event: record.event,
75
99
  summary: record.summary,
76
100
  fingerprint: record.fingerprint,
101
+ ...(snapshot || replay
102
+ ? { replayCwd: snapshot?.cwd ?? replay?.cwd, replayKind: snapshot?.kind ?? replay?.kind }
103
+ : {}),
77
104
  previousVerdict,
78
105
  previousReason,
79
106
  nextVerdict: next.verdict,
80
107
  nextReason: next.reason,
81
108
  };
82
109
  }
110
+ export function countMissingActionSnapshots(records) {
111
+ return records.filter((record) => record.event && GATE_EVENTS.has(record.event) && !parseAuditActionSnapshot(record)).length;
112
+ }
@@ -0,0 +1,9 @@
1
+ import type { GatedActionKind } from './gate-contract.js';
2
+ import type { ScrubOptions } from './types.js';
3
+ /** Subagent fingerprint input — must match classify-subagent `fingerprintSource`. */
4
+ export declare function subagentFingerprintSource(payload: Record<string, unknown>, scrubOptions: ScrubOptions): unknown;
5
+ /**
6
+ * Scrubbed payload used for replay envelope hashing — aligned with classifier fingerprints.
7
+ * Tool: scrubbed `tool_input`. Subagent: description/prompt subset. Other: full payload.
8
+ */
9
+ export declare function fingerprintReplayPayload(kind: GatedActionKind, payload: Record<string, unknown> | undefined, scrubOptions: ScrubOptions): Record<string, unknown> | undefined;