@guilz-dev/belay 0.2.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 (180) hide show
  1. package/README.md +39 -12
  2. package/dist/adapters/cursor/runtime-entry.js +1 -0
  3. package/dist/adapters/shared/gate-runtime.d.ts +11 -14
  4. package/dist/adapters/shared/gate-runtime.js +289 -23
  5. package/dist/bundle/claude-runtime.mjs +4435 -708
  6. package/dist/bundle/codex-runtime.mjs +4448 -710
  7. package/dist/bundle/cursor-runtime.mjs +4437 -710
  8. package/dist/cli.js +333 -11
  9. package/dist/commands/approve.d.ts +1 -0
  10. package/dist/commands/approve.js +56 -26
  11. package/dist/commands/config.d.ts +70 -0
  12. package/dist/commands/config.js +577 -0
  13. package/dist/commands/doctor.js +13 -2
  14. package/dist/commands/explain.js +6 -1
  15. package/dist/commands/harvest.d.ts +29 -0
  16. package/dist/commands/harvest.js +72 -0
  17. package/dist/commands/health-snapshot.d.ts +6 -0
  18. package/dist/commands/health-snapshot.js +34 -0
  19. package/dist/commands/judge.d.ts +100 -0
  20. package/dist/commands/judge.js +304 -0
  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/simulate.d.ts +1 -0
  25. package/dist/commands/simulate.js +7 -2
  26. package/dist/commands/standing-allow.d.ts +10 -0
  27. package/dist/commands/standing-allow.js +26 -0
  28. package/dist/commands/status.js +2 -2
  29. package/dist/commands/stdin-key.d.ts +1 -0
  30. package/dist/commands/stdin-key.js +8 -0
  31. package/dist/commands/tui.d.ts +48 -0
  32. package/dist/commands/tui.js +150 -0
  33. package/dist/config-io.js +10 -5
  34. package/dist/conformance/guarantee-table.js +12 -0
  35. package/dist/conformance/types.d.ts +2 -0
  36. package/dist/core/approval-replay-cli.d.ts +7 -0
  37. package/dist/core/approval-replay-cli.js +36 -0
  38. package/dist/core/approval-replay.d.ts +45 -0
  39. package/dist/core/approval-replay.js +141 -0
  40. package/dist/core/approval-service.d.ts +15 -0
  41. package/dist/core/approval-service.js +47 -2
  42. package/dist/core/approval.d.ts +24 -3
  43. package/dist/core/approval.js +47 -4
  44. package/dist/core/audit-analysis.d.ts +7 -1
  45. package/dist/core/audit-analysis.js +111 -0
  46. package/dist/core/audit-io.d.ts +2 -0
  47. package/dist/core/audit-io.js +14 -0
  48. package/dist/core/audit-metrics.d.ts +7 -0
  49. package/dist/core/audit-metrics.js +24 -4
  50. package/dist/core/audit-query.d.ts +1 -0
  51. package/dist/core/audit-query.js +3 -0
  52. package/dist/core/audit-replay-context.d.ts +35 -0
  53. package/dist/core/audit-replay-context.js +88 -0
  54. package/dist/core/audit-types.d.ts +24 -1
  55. package/dist/core/audit-types.js +1 -1
  56. package/dist/core/capability/index.d.ts +4 -3
  57. package/dist/core/capability/index.js +3 -2
  58. package/dist/core/capability/paths.d.ts +2 -1
  59. package/dist/core/capability/paths.js +76 -8
  60. package/dist/core/capability/reasons.d.ts +3 -0
  61. package/dist/core/capability/reasons.js +8 -0
  62. package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
  63. package/dist/core/capability/trusted-workspace-roots.js +149 -0
  64. package/dist/core/capability/types.d.ts +11 -1
  65. package/dist/core/capability-approval.d.ts +2 -1
  66. package/dist/core/capability-approval.js +100 -2
  67. package/dist/core/classify-subagent.js +2 -20
  68. package/dist/core/classify-tool.js +129 -30
  69. package/dist/core/config-layers.js +2 -1
  70. package/dist/core/config.d.ts +40 -2
  71. package/dist/core/config.js +159 -11
  72. package/dist/core/credential-store.d.ts +11 -0
  73. package/dist/core/credential-store.js +60 -0
  74. package/dist/core/gate-engine.js +104 -13
  75. package/dist/core/harvest.d.ts +53 -0
  76. package/dist/core/harvest.js +276 -0
  77. package/dist/core/index.d.ts +9 -3
  78. package/dist/core/index.js +6 -2
  79. package/dist/core/integrity.d.ts +2 -0
  80. package/dist/core/integrity.js +13 -0
  81. package/dist/core/judge-api-key.d.ts +19 -0
  82. package/dist/core/judge-api-key.js +74 -0
  83. package/dist/core/judge-cloud-consent.d.ts +13 -0
  84. package/dist/core/judge-cloud-consent.js +38 -0
  85. package/dist/core/judge-config.d.ts +41 -4
  86. package/dist/core/judge-config.js +263 -57
  87. package/dist/core/judge-doctor.d.ts +9 -1
  88. package/dist/core/judge-doctor.js +199 -94
  89. package/dist/core/judge-model-discovery.d.ts +24 -0
  90. package/dist/core/judge-model-discovery.js +168 -0
  91. package/dist/core/judge-model-policy.d.ts +5 -0
  92. package/dist/core/judge-model-policy.js +21 -0
  93. package/dist/core/judge-runtime-detection.d.ts +9 -0
  94. package/dist/core/judge-runtime-detection.js +68 -0
  95. package/dist/core/path-utils.d.ts +11 -0
  96. package/dist/core/path-utils.js +56 -6
  97. package/dist/core/reclassify.d.ts +3 -0
  98. package/dist/core/reclassify.js +44 -14
  99. package/dist/core/replay-scrub.d.ts +9 -0
  100. package/dist/core/replay-scrub.js +43 -0
  101. package/dist/core/shell-tokenizer.d.ts +2 -0
  102. package/dist/core/shell-tokenizer.js +82 -23
  103. package/dist/core/standing-allow.d.ts +50 -0
  104. package/dist/core/standing-allow.js +175 -0
  105. package/dist/core/transactional/diff-evaluator.js +1 -19
  106. package/dist/core/types.d.ts +17 -0
  107. package/dist/core/verdict/adapter.d.ts +1 -0
  108. package/dist/core/verdict/adapter.js +17 -2
  109. package/dist/core/verdict/containment.d.ts +8 -2
  110. package/dist/core/verdict/containment.js +45 -10
  111. package/dist/core/verdict/judge-audit.d.ts +1 -0
  112. package/dist/core/verdict/judge-audit.js +32 -1
  113. package/dist/core/verdict/judge-baseline.d.ts +20 -0
  114. package/dist/core/verdict/judge-baseline.js +48 -0
  115. package/dist/core/verdict/judge-broker-service.d.ts +42 -0
  116. package/dist/core/verdict/judge-broker-service.js +279 -0
  117. package/dist/core/verdict/judge-catalog.d.ts +40 -0
  118. package/dist/core/verdict/judge-catalog.js +148 -0
  119. package/dist/core/verdict/judge-cli-fingerprint.d.ts +5 -0
  120. package/dist/core/verdict/judge-cli-fingerprint.js +61 -0
  121. package/dist/core/verdict/judge-cli.d.ts +46 -0
  122. package/dist/core/verdict/judge-cli.js +401 -0
  123. package/dist/core/verdict/judge-factory.d.ts +15 -4
  124. package/dist/core/verdict/judge-factory.js +127 -15
  125. package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
  126. package/dist/core/verdict/judge-provider-matrix.js +66 -0
  127. package/dist/core/verdict/judge-runtime-config.d.ts +42 -0
  128. package/dist/core/verdict/judge-runtime-config.js +92 -0
  129. package/dist/core/verdict/judge-session-broker.d.ts +48 -0
  130. package/dist/core/verdict/judge-session-broker.js +195 -0
  131. package/dist/core/verdict/judge-session-guard.d.ts +27 -0
  132. package/dist/core/verdict/judge-session-guard.js +91 -0
  133. package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
  134. package/dist/core/verdict/judge-session-kill-switch.js +43 -0
  135. package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
  136. package/dist/core/verdict/judge-session-mutex.js +23 -0
  137. package/dist/core/verdict/judge-shadow.d.ts +24 -0
  138. package/dist/core/verdict/judge-shadow.js +116 -0
  139. package/dist/core/verdict/judge-transport.d.ts +40 -0
  140. package/dist/core/verdict/judge-transport.js +258 -0
  141. package/dist/core/verdict/judge.d.ts +33 -1
  142. package/dist/core/verdict/judge.js +84 -14
  143. package/dist/core/verdict/parser.js +6 -1
  144. package/dist/core/verdict/persistent-paths.d.ts +8 -0
  145. package/dist/core/verdict/persistent-paths.js +52 -0
  146. package/dist/core/verdict/types.d.ts +19 -2
  147. package/dist/core/verdict/verdict.js +166 -49
  148. package/dist/corpus/adversarial-probe.d.ts +86 -0
  149. package/dist/corpus/adversarial-probe.js +220 -0
  150. package/dist/corpus/evaluate.d.ts +45 -12
  151. package/dist/corpus/evaluate.js +62 -5
  152. package/dist/corpus/gates.d.ts +35 -0
  153. package/dist/corpus/gates.js +81 -0
  154. package/dist/corpus/judge-accuracy.d.ts +8 -0
  155. package/dist/corpus/judge-accuracy.js +39 -0
  156. package/dist/corpus/must-allow-commands.d.ts +5 -0
  157. package/dist/corpus/must-allow-commands.js +13 -0
  158. package/dist/corpus/mutators.d.ts +28 -0
  159. package/dist/corpus/mutators.js +151 -0
  160. package/dist/corpus/ratchet.d.ts +42 -0
  161. package/dist/corpus/ratchet.js +116 -0
  162. package/dist/corpus/runtime-match.d.ts +19 -0
  163. package/dist/corpus/runtime-match.js +45 -0
  164. package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
  165. package/dist/corpus/standing-allow-catalog.generated.js +79 -0
  166. package/dist/corpus/types.d.ts +47 -0
  167. package/dist/corpus/types.js +157 -0
  168. package/dist/installer.js +69 -15
  169. package/dist/judge-broker-daemon.d.ts +1 -0
  170. package/dist/judge-broker-daemon.js +123 -0
  171. package/dist/services/sandbox-service.d.ts +1 -0
  172. package/dist/services/sandbox-service.js +2 -0
  173. package/dist/types.d.ts +7 -0
  174. package/dist/version.d.ts +1 -1
  175. package/dist/version.js +1 -1
  176. package/package.json +7 -3
  177. package/skills/belay/SKILL.md +15 -8
  178. package/skills/belay/belay-approve.md +17 -0
  179. package/dist/commands/init-wizard.d.ts +0 -21
  180. package/dist/commands/init-wizard.js +0 -63
@@ -1,5 +1,5 @@
1
1
  import { allPathsAllowlisted } from './capability/allowlist.js';
2
- import { collectOutsideRepoPaths } from './capability/paths.js';
2
+ import { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload, } from './capability/paths.js';
3
3
  import { classifySubagent } from './classify-subagent.js';
4
4
  import { classifyToolUse } from './classify-tool.js';
5
5
  import { classifierOptionsFromConfig } from './config.js';
@@ -92,17 +92,63 @@ export function normalizeGatedAction(params) {
92
92
  agentAssessment,
93
93
  };
94
94
  }
95
- function applyShellPeripheralPolicy(command, action, result, options) {
96
- if (options.brokerFsScope &&
97
- result.verdict === 'deny_pending_approval' &&
98
- (result.reason === 'outside_repo_mutation' ||
99
- result.reason === 'outside_repo_redirect' ||
100
- result.reason === 'repo_outside_mutation' ||
101
- result.axes?.location === 'repo_outside')) {
102
- const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
103
- if (outsideRepoPaths.length > 0 &&
104
- options.fsScopeAllowlist &&
105
- allPathsAllowlisted(outsideRepoPaths, options.fsScopeAllowlist)) {
95
+ function applySandboxFsScopeBoundary(outsideRepoPaths, result, options, hints = {}) {
96
+ if (!options.brokerFsScope) {
97
+ return result;
98
+ }
99
+ if (outsideRepoPaths.length === 0) {
100
+ return result;
101
+ }
102
+ if (options.fsScopeAllowlist && allPathsAllowlisted(outsideRepoPaths, options.fsScopeAllowlist)) {
103
+ return result;
104
+ }
105
+ if (result.verdict === 'deny_pending_approval') {
106
+ return result;
107
+ }
108
+ const redirect = hints.redirect === true ||
109
+ result.reason === 'read_only' ||
110
+ result.assessment.signals.includes('read_only') ||
111
+ result.reason === 'outside_repo_redirect';
112
+ return {
113
+ ...result,
114
+ verdict: 'deny_pending_approval',
115
+ reason: redirect ? 'outside_repo_redirect' : 'outside_repo_mutation',
116
+ assessment: {
117
+ ...result.assessment,
118
+ external: true,
119
+ reversibility: 'irreversible',
120
+ signals: [...result.assessment.signals, 'sandbox_boundary_expected'],
121
+ },
122
+ };
123
+ }
124
+ function applyFsScopePeripheralPolicy(outsideRepoPaths, result, options) {
125
+ if (!options.brokerFsScope) {
126
+ return result;
127
+ }
128
+ if (outsideRepoPaths.length === 0) {
129
+ return result;
130
+ }
131
+ if (options.fsScopeAllowlist && allPathsAllowlisted(outsideRepoPaths, options.fsScopeAllowlist)) {
132
+ if (result.verdict === 'deny_pending_approval' &&
133
+ (result.reason === 'outside_repo_mutation' ||
134
+ result.reason === 'outside_repo_redirect' ||
135
+ result.reason === 'repo_outside_mutation' ||
136
+ result.axes?.location === 'repo_outside')) {
137
+ return {
138
+ ...result,
139
+ verdict: 'allow_flagged',
140
+ reason: 'capability_fs_hint',
141
+ assessment: {
142
+ ...result.assessment,
143
+ signals: [
144
+ ...result.assessment.signals,
145
+ 'capability_fs_hint',
146
+ 'sandbox_boundary_expected',
147
+ ],
148
+ },
149
+ };
150
+ }
151
+ if (result.verdict === 'allow' || result.verdict === 'allow_flagged') {
106
152
  return {
107
153
  ...result,
108
154
  verdict: 'allow_flagged',
@@ -120,6 +166,36 @@ function applyShellPeripheralPolicy(command, action, result, options) {
120
166
  }
121
167
  return result;
122
168
  }
169
+ function applySandboxOutsideBoundary(command, action, result, options) {
170
+ const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot, options.trustedWorkspaceRoots);
171
+ return applySandboxFsScopeBoundary(outsideRepoPaths, result, options, {
172
+ redirect: command.includes('>'),
173
+ });
174
+ }
175
+ function applyShellPeripheralPolicy(command, action, result, options) {
176
+ const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot, options.trustedWorkspaceRoots);
177
+ return applyFsScopePeripheralPolicy(outsideRepoPaths, result, options);
178
+ }
179
+ function outsideRepoPathsForToolAction(action, options) {
180
+ const payload = action.payload ?? {};
181
+ const paths = new Set(collectOutsideRepoPathsFromToolPayload(payload, action.cwd, action.repoRoot, options.trustedWorkspaceRoots));
182
+ const command = shellCommandFromPayload(payload);
183
+ if (command) {
184
+ for (const resolved of collectOutsideRepoPaths(command, action.cwd, action.repoRoot, options.trustedWorkspaceRoots)) {
185
+ paths.add(resolved);
186
+ }
187
+ }
188
+ return [...paths];
189
+ }
190
+ function applyToolSandboxPolicies(action, result, options) {
191
+ const outsideRepoPaths = outsideRepoPathsForToolAction(action, options);
192
+ const command = shellCommandFromPayload(action.payload ?? {});
193
+ let next = applySandboxFsScopeBoundary(outsideRepoPaths, result, options, {
194
+ redirect: command.includes('>'),
195
+ });
196
+ next = applyFsScopePeripheralPolicy(outsideRepoPaths, next, options);
197
+ return next;
198
+ }
123
199
  export async function classifyGatedAction(action, config, extraOptions = {}) {
124
200
  const options = { ...classifierOptionsFromConfig(config), ...extraOptions };
125
201
  if (action.kind === 'shell') {
@@ -128,6 +204,7 @@ export async function classifyGatedAction(action, config, extraOptions = {}) {
128
204
  throw new GateNormalizationError('Shell gated action requires a command.');
129
205
  }
130
206
  let result = await classifyShell(command, action.cwd, action.repoRoot, config, options);
207
+ result = applySandboxOutsideBoundary(command, action, result, options);
131
208
  result = applyShellPeripheralPolicy(command, action, result, options);
132
209
  if (!action.agentAssessment) {
133
210
  return result;
@@ -146,7 +223,21 @@ export async function classifyGatedAction(action, config, extraOptions = {}) {
146
223
  if (action.kind === 'subagent') {
147
224
  return classifySubagent(action.payload ?? {}, action.repoRoot, options);
148
225
  }
149
- return classifyToolUse(action.payload ?? {}, action.repoRoot, action.cwd, config, options);
226
+ let result = await classifyToolUse(action.payload ?? {}, action.repoRoot, action.cwd, config, options);
227
+ result = applyToolSandboxPolicies(action, result, options);
228
+ if (!action.agentAssessment) {
229
+ return result;
230
+ }
231
+ const merged = mergeAgentAssessment(result.assessment, action.agentAssessment);
232
+ if (!merged.mismatch) {
233
+ return { ...result, assessment: merged.assessment };
234
+ }
235
+ return {
236
+ ...result,
237
+ verdict: 'deny_pending_approval',
238
+ reason: 'agent_assessment_mismatch',
239
+ assessment: merged.assessment,
240
+ };
150
241
  }
151
242
  export async function classifyGatedActionAsync(action, config, extraOptions = {}) {
152
243
  return classifyGatedAction(action, config, extraOptions);
@@ -0,0 +1,53 @@
1
+ import type { CorpusCase } from '../corpus/types.js';
2
+ import type { AuditRecord } from './audit-types.js';
3
+ export declare const HARVEST_REPORT_SCHEMA_VERSION = 1;
4
+ export type HarvestCandidateSource = 'deny_then_approve' | 'repeated_ask' | 'read_style_signal' | 'overrides_allow';
5
+ export type HarvestReviewOutcome = 'provably-benign' | 'accepted-benign' | 'reject';
6
+ export interface HarvestCandidate {
7
+ kind: 'shell';
8
+ command: string;
9
+ fingerprint: string;
10
+ reason: string;
11
+ sources: HarvestCandidateSource[];
12
+ askCount: number;
13
+ approvedAfterDeny: boolean;
14
+ }
15
+ export type AvailabilitySignal = 'missing_trusted_cwd' | 'judge_timeout' | 'judge_fallback';
16
+ export interface AvailabilityQueueItem {
17
+ kind: 'shell';
18
+ command: string;
19
+ fingerprint: string;
20
+ reason: string;
21
+ availabilitySignal: AvailabilitySignal;
22
+ judgeFallbackReason?: string;
23
+ askCount: number;
24
+ }
25
+ export interface HarvestReport {
26
+ schemaVersion: typeof HARVEST_REPORT_SCHEMA_VERSION;
27
+ /** Initial harvest scope — shell audit traces only. */
28
+ scope: 'shell';
29
+ candidates: HarvestCandidate[];
30
+ availabilityQueue: AvailabilityQueueItem[];
31
+ }
32
+ export declare function extractAvailabilityQueue(records: AuditRecord[]): AvailabilityQueueItem[];
33
+ /** Shell gate rows plus approval events — matches what `belay harvest list` needs. */
34
+ export declare function filterRecordsForHarvest(records: AuditRecord[], options?: {
35
+ since?: string;
36
+ until?: string;
37
+ }): AuditRecord[];
38
+ export declare function extractHarvestCandidates(records: AuditRecord[], options?: {
39
+ allowPatterns?: string[];
40
+ }): HarvestCandidate[];
41
+ export declare function buildHarvestReport(records: AuditRecord[], options?: {
42
+ allowPatterns?: string[];
43
+ }): HarvestReport;
44
+ export declare function applyHarvestReview(cases: CorpusCase[], params: {
45
+ command: string;
46
+ outcome: HarvestReviewOutcome;
47
+ reason?: string;
48
+ }): {
49
+ cases: CorpusCase[];
50
+ applied: boolean;
51
+ ok: boolean;
52
+ message: string;
53
+ };
@@ -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';
@@ -1,5 +1,6 @@
1
1
  import type { ScopedPaths } from '../adapters/layouts/scope.js';
2
2
  import type { AdapterLayout } from '../adapters/layouts/types.js';
3
+ import type { BelayConfigV4 } from './config.js';
3
4
  export interface IntegrityManifest {
4
5
  version: 1;
5
6
  generatedAt: string;
@@ -13,3 +14,4 @@ export declare function verifyIntegrityManifest(repoRoot: string, layout: Adapte
13
14
  ok: boolean;
14
15
  mismatches: string[];
15
16
  }>;
17
+ export declare function refreshIntegrityIfPinned(repoRoot: string, config: BelayConfigV4): Promise<void>;
@@ -2,6 +2,9 @@ import { createHash } from 'node:crypto';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
+ import { getAdapterLayout } from '../adapters/layouts/index.js';
6
+ import { resolveScopedPaths } from '../adapters/layouts/scope.js';
7
+ import { resolveAdapterName } from '../config-io.js';
5
8
  export async function sha256File(filePath) {
6
9
  const content = await readFile(filePath);
7
10
  return createHash('sha256').update(content).digest('hex');
@@ -66,3 +69,13 @@ export async function verifyIntegrityManifest(repoRoot, layout) {
66
69
  }
67
70
  return { ok: mismatches.length === 0, mismatches };
68
71
  }
72
+ export async function refreshIntegrityIfPinned(repoRoot, config) {
73
+ if (config.controlPlane.integrity !== 'hash-pinned') {
74
+ return;
75
+ }
76
+ const adapter = resolveAdapterName(config);
77
+ const layout = getAdapterLayout(adapter);
78
+ const installScope = config.installScope === 'global' ? 'global' : 'project';
79
+ const scoped = resolveScopedPaths(layout, installScope, repoRoot);
80
+ await writeIntegrityManifest(repoRoot, layout, runtimeIntegrityFiles(layout, scoped));
81
+ }
@@ -1,4 +1,23 @@
1
+ import type { BelayJudgeConfig } from './config.js';
2
+ import { belayStateDir } from './config.js';
3
+ import { type JudgeProviderSpec } from './verdict/judge-catalog.js';
4
+ export type JudgeCredentialSourceKind = 'env' | 'store' | 'host-session' | 'missing';
5
+ export interface ResolvedJudgeCredential {
6
+ key: string | null;
7
+ source: string | null;
8
+ sourceKind: JudgeCredentialSourceKind;
9
+ mode: 'project' | 'apiKey';
10
+ }
11
+ /** @deprecated Use resolveJudgeCredential */
1
12
  export declare function resolveJudgeApiKey(env?: NodeJS.ProcessEnv): {
2
13
  key: string | null;
3
14
  source: 'BELAY_JUDGE_API_KEY' | 'OPENAI_API_KEY' | null;
4
15
  };
16
+ export declare function resolveJudgeCredential(params: {
17
+ judge: BelayJudgeConfig;
18
+ catalogSpec?: JudgeProviderSpec;
19
+ repoRoot: string;
20
+ repoLocalStateDir: string;
21
+ config: Parameters<typeof belayStateDir>[0];
22
+ env?: NodeJS.ProcessEnv;
23
+ }): Promise<ResolvedJudgeCredential>;