@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
@@ -1,4 +1,4 @@
1
- export type CapabilityApprovalScope = 'once' | 'path';
1
+ export type CapabilityApprovalScope = 'once' | 'path' | 'workspace-root';
2
2
  export interface FsScopeAllowlistEntry {
3
3
  path: string;
4
4
  approvedAt: string;
@@ -8,3 +8,13 @@ export interface FsScopeAllowlistFile {
8
8
  version: 1;
9
9
  paths: FsScopeAllowlistEntry[];
10
10
  }
11
+ export interface TrustedWorkspaceRootEntry {
12
+ path: string;
13
+ approvedAt: string;
14
+ approvalId: string;
15
+ source?: 'approval';
16
+ }
17
+ export interface TrustedWorkspaceRootsFile {
18
+ version: 1;
19
+ roots: TrustedWorkspaceRootEntry[];
20
+ }
@@ -1,5 +1,5 @@
1
1
  import type { CapabilityApprovalScope } from './capability/types.js';
2
- import type { BelayConfigV3 } from './config.js';
2
+ import { type BelayConfigV3 } from './config.js';
3
3
  import type { ApprovalStateFile } from './types.js';
4
4
  export interface CapabilityApprovalStore {
5
5
  loadPending: () => Promise<{
@@ -13,6 +13,7 @@ export interface CapabilityApprovalStore {
13
13
  writePending: (filePath: string, state: ApprovalStateFile) => Promise<void>;
14
14
  writeApproved: (filePath: string, state: ApprovalStateFile) => Promise<void>;
15
15
  allowlistPath: string;
16
+ trustedRootsPath: string;
16
17
  }
17
18
  export declare function recordCapabilityApproval(params: {
18
19
  approvalId: string;
@@ -1,11 +1,13 @@
1
1
  import { recordApproval } from './approval-service.js';
2
2
  import { addPathToAllowlist, loadFsScopeAllowlist, normalizeAllowlistPath, saveFsScopeAllowlist, } from './capability/allowlist.js';
3
3
  import { FS_SCOPE_REASONS } from './capability/reasons.js';
4
+ import { addTrustedWorkspaceRoot, loadTrustedWorkspaceRoots, normalizeTrustedWorkspaceRootPath, saveTrustedWorkspaceRoots, validateTrustedWorkspaceRootCandidate, } from './capability/trusted-workspace-roots.js';
5
+ import { resolveControlPlaneDir } from './config.js';
4
6
  export async function recordCapabilityApproval(params) {
5
7
  const pending = await params.store.loadPending();
6
8
  const match = pending.state.approvals.find((approval) => approval.approvalId === params.approvalId);
7
9
  if (params.scope === 'path') {
8
- if (!match || !FS_SCOPE_REASONS.has(match.reason)) {
10
+ if (!match || match.kind !== 'shell' || !FS_SCOPE_REASONS.has(match.reason)) {
9
11
  return {
10
12
  ok: false,
11
13
  message: 'Path scope approvals apply only to pending outside-repo shell actions (outside_repo_mutation / outside_repo_redirect).',
@@ -18,6 +20,85 @@ export async function recordCapabilityApproval(params) {
18
20
  };
19
21
  }
20
22
  }
23
+ if (params.scope === 'workspace-root') {
24
+ if (!match || !FS_SCOPE_REASONS.has(match.reason)) {
25
+ return {
26
+ ok: false,
27
+ message: 'workspace-root scope approvals apply only to pending outside-repo actions (outside_repo_mutation / outside_repo_redirect).',
28
+ };
29
+ }
30
+ if (!params.scopePath?.trim()) {
31
+ return {
32
+ ok: false,
33
+ message: 'approve --scope workspace-root requires --path <absolute-directory-path>.',
34
+ };
35
+ }
36
+ if (!match.scopeHint || match.scopeHint.scope !== 'workspace-root') {
37
+ return {
38
+ ok: false,
39
+ message: 'workspace-root approval requires a pending scope hint. Re-run the original blocked action to mint a scoped approval.',
40
+ };
41
+ }
42
+ const normalizedPath = normalizeTrustedWorkspaceRootPath(params.scopePath.trim());
43
+ const normalizedHint = normalizeTrustedWorkspaceRootPath(match.scopeHint.path);
44
+ if (normalizedPath !== normalizedHint) {
45
+ return {
46
+ ok: false,
47
+ message: `workspace-root approval path must exactly match the suggested path: ${normalizedHint}`,
48
+ };
49
+ }
50
+ const validation = validateTrustedWorkspaceRootCandidate({
51
+ candidatePath: normalizedPath,
52
+ repoRoot: match.repoRoot,
53
+ controlPlaneDir: params.config.controlPlane.enabled
54
+ ? resolveControlPlaneDir(params.config)
55
+ : undefined,
56
+ requireExistingDirectory: true,
57
+ requireNonGit: true,
58
+ });
59
+ if (!validation.ok) {
60
+ if (validation.reason === 'not_directory') {
61
+ return {
62
+ ok: false,
63
+ message: 'workspace-root approval requires an existing directory path.',
64
+ };
65
+ }
66
+ if (validation.reason === 'broad_root') {
67
+ return {
68
+ ok: false,
69
+ message: 'workspace-root approval rejected: broad roots (/, drive root, HOME) are not allowed.',
70
+ };
71
+ }
72
+ if (validation.reason === 'high_stakes') {
73
+ return {
74
+ ok: false,
75
+ message: 'workspace-root approval rejected: high-stakes directories cannot be trusted roots.',
76
+ };
77
+ }
78
+ if (validation.reason === 'inside_repo') {
79
+ return {
80
+ ok: false,
81
+ message: 'workspace-root approval rejected: the path is already inside the repository root.',
82
+ };
83
+ }
84
+ if (validation.reason === 'inside_git_repo') {
85
+ return {
86
+ ok: false,
87
+ message: `workspace-root approval rejected: ${normalizedPath} is under a git repository.`,
88
+ };
89
+ }
90
+ if (validation.reason === 'control_plane_overlap') {
91
+ return {
92
+ ok: false,
93
+ message: 'workspace-root approval rejected: control plane paths cannot be trusted roots.',
94
+ };
95
+ }
96
+ return {
97
+ ok: false,
98
+ message: 'workspace-root approval rejected by trusted-root policy.',
99
+ };
100
+ }
101
+ }
21
102
  const result = await recordApproval({
22
103
  approvalId: params.approvalId,
23
104
  config: params.config,
@@ -25,9 +106,26 @@ export async function recordCapabilityApproval(params) {
25
106
  requireSignedToken: params.requireSignedToken ?? false,
26
107
  store: params.store,
27
108
  });
28
- if (!result.ok || params.scope !== 'path' || !params.scopePath?.trim()) {
109
+ if (!result.ok ||
110
+ (params.scope !== 'path' && params.scope !== 'workspace-root') ||
111
+ !params.scopePath?.trim()) {
29
112
  return { ok: result.ok, message: result.message };
30
113
  }
114
+ if (params.scope === 'workspace-root') {
115
+ const trustedRoots = await loadTrustedWorkspaceRoots(params.store.trustedRootsPath);
116
+ const normalizedPath = normalizeTrustedWorkspaceRootPath(params.scopePath.trim());
117
+ const updated = addTrustedWorkspaceRoot(trustedRoots, {
118
+ path: normalizedPath,
119
+ approvedAt: new Date().toISOString(),
120
+ approvalId: params.approvalId,
121
+ source: 'approval',
122
+ });
123
+ await saveTrustedWorkspaceRoots(params.store.trustedRootsPath, updated);
124
+ return {
125
+ ok: true,
126
+ message: `${result.message} Trusted workspace root ${normalizedPath} added.`,
127
+ };
128
+ }
31
129
  const allowlist = await loadFsScopeAllowlist(params.store.allowlistPath);
32
130
  const normalizedPath = normalizeAllowlistPath(params.scopePath.trim());
33
131
  const updated = addPathToAllowlist(allowlist, {
@@ -1,4 +1,5 @@
1
1
  import { canonicalStringify, subagentFingerprint } from './fingerprint.js';
2
+ import { subagentFingerprintSource } from './replay-scrub.js';
2
3
  import { scrubValue } from './scrub.js';
3
4
  const EXTERNAL_TERMS = ['deploy', 'production', 'publish', 'release', 'ship', 'notify', 'email'];
4
5
  function extractSubagentText(payload, options) {
@@ -22,26 +23,7 @@ function extractSubagentText(payload, options) {
22
23
  return canonicalStringify(scrubValue(payload, options.scrubOptions));
23
24
  }
24
25
  function fingerprintSource(payload, options) {
25
- const toolInput = payload.tool_input;
26
- if (toolInput && typeof toolInput === 'object') {
27
- const input = toolInput;
28
- return scrubValue({
29
- description: input.description ?? '',
30
- prompt: input.prompt ?? '',
31
- }, options.scrubOptions);
32
- }
33
- const task = payload.task;
34
- if (typeof task === 'string') {
35
- return scrubValue({ task }, options.scrubOptions);
36
- }
37
- if (task && typeof task === 'object') {
38
- const taskObj = task;
39
- return scrubValue({
40
- description: taskObj.description ?? '',
41
- prompt: taskObj.prompt ?? '',
42
- }, options.scrubOptions);
43
- }
44
- return scrubValue(payload, options.scrubOptions);
26
+ return subagentFingerprintSource(payload, options.scrubOptions ?? {});
45
27
  }
46
28
  export function classifySubagent(payload, repoRoot, options = {}) {
47
29
  const kind = payload.tool_name === 'Task' ? 'Task' : String(payload.subagent_type ?? 'generalPurpose');
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { canonicalStringify, toolFingerprint } from './fingerprint.js';
3
3
  import { matchesSensitivePath } from './glob.js';
4
- import { pathWithinRoot, relativeWithinRepo } from './path-utils.js';
4
+ import { pathWithinRoot, resolveWorkspaceRootMatch } from './path-utils.js';
5
5
  import { scrubValue } from './scrub.js';
6
6
  import { classifyShell, resolveClassifierTrustedCwd } from './verdict/adapter.js';
7
7
  import { mutationPrescanRequiresAsk, tier1RequiresAsk } from './verdict/judge.js';
@@ -87,6 +87,7 @@ async function classifyFileMutationWithTier1(params) {
87
87
  cwd: params.cwd,
88
88
  repoRoot: params.repoRoot,
89
89
  trustedCwd,
90
+ trustedWorkspaceRoots: params.options.trustedWorkspaceRoots,
90
91
  sensitivePaths: params.options.sensitivePaths ?? params.config.classifier.sensitivePaths,
91
92
  });
92
93
  if (prescan) {
@@ -243,8 +244,8 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
243
244
  },
244
245
  };
245
246
  }
246
- const relativePath = relativeWithinRepo(repoRoot, resolvedPath);
247
- if (relativePath === null) {
247
+ const workspaceMatch = resolveWorkspaceRootMatch(repoRoot, options.trustedWorkspaceRoots, resolvedPath);
248
+ if (workspaceMatch === null) {
248
249
  signals.push('outside_repo_path');
249
250
  return classifyFileMutationWithTier1({
250
251
  toolName,
@@ -260,6 +261,34 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
260
261
  locationLabel: 'outside_repo',
261
262
  });
262
263
  }
264
+ if (workspaceMatch.kind === 'trusted') {
265
+ signals.push('trusted_workspace_root');
266
+ }
267
+ const trustedCwd = resolveClassifierTrustedCwd(cwd, options);
268
+ const workspacePrescan = mutationPrescanRequiresAsk({
269
+ targets: [filePath],
270
+ cwd,
271
+ repoRoot,
272
+ trustedCwd,
273
+ trustedWorkspaceRoots: options.trustedWorkspaceRoots,
274
+ sensitivePaths,
275
+ });
276
+ if (workspacePrescan) {
277
+ return classifyFileMutationWithTier1({
278
+ toolName,
279
+ toolKind,
280
+ filePath,
281
+ resolvedPath,
282
+ repoRoot,
283
+ cwd,
284
+ config,
285
+ options,
286
+ signals,
287
+ isDelete: FILE_DELETE_TOOL_NAMES.has(toolKind),
288
+ locationLabel: 'sensitive_path',
289
+ });
290
+ }
291
+ const relativePath = workspaceMatch.relativePath;
263
292
  if (matchesSensitivePath(relativePath, sensitivePaths)) {
264
293
  signals.push('sensitive_path');
265
294
  return classifyFileMutationWithTier1({
@@ -1,4 +1,5 @@
1
1
  import type { ClassifierOptions, ControlPlaneIntegrity, ScrubOptions, UnknownLocalEffectPolicy, UnparseableShellPolicy } from './types.js';
2
+ import { type BelayJudgeRuntimeConfig } from './verdict/judge-runtime-config.js';
2
3
  export type BelayMode = 'enforce' | 'audit';
3
4
  export type { UnknownLocalEffectPolicy };
4
5
  export interface BelayConfigV1 {
@@ -102,6 +103,7 @@ export interface BelayJudgeConfig {
102
103
  keepAlive: string | null;
103
104
  cloudConsent?: JudgeCloudConsent;
104
105
  credential?: JudgeCredentialConfig;
106
+ runtime?: BelayJudgeRuntimeConfig;
105
107
  }
106
108
  export declare const DEFAULT_JUDGE_LOCAL_OLLAMA: BelayJudgeConfig;
107
109
  export declare const DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE: BelayJudgeConfig;
@@ -137,6 +139,17 @@ export interface BelayApprovalSigningConfig {
137
139
  /** When true, out-of-band approvals must present a signed token. */
138
140
  required: boolean;
139
141
  }
142
+ export type ApprovalFlow = 'one_step' | 'two_step';
143
+ export interface BelayApprovalAutoReplayScopes {
144
+ shell: boolean;
145
+ tool: boolean;
146
+ subagent: boolean;
147
+ }
148
+ export interface BelayApprovalConfig {
149
+ flow: ApprovalFlow;
150
+ autoReplayScopes: BelayApprovalAutoReplayScopes;
151
+ executionLeaseMs: number;
152
+ }
140
153
  export interface BelayEgressConfig {
141
154
  enabled: boolean;
142
155
  listenHost: string;
@@ -160,6 +173,7 @@ export interface BelayConfigV4 {
160
173
  controlPlane: BelayControlPlaneConfig;
161
174
  notifications: BelayNotificationsConfig;
162
175
  approvalSigning: BelayApprovalSigningConfig;
176
+ approval: BelayApprovalConfig;
163
177
  egress: BelayEgressConfig;
164
178
  sandbox: BelaySandboxConfig;
165
179
  audit: BelayConfigV2['audit'];
@@ -184,6 +198,7 @@ export declare const DEFAULT_CONTROL_PLANE_V3: BelayControlPlaneConfig;
184
198
  export declare const DEFAULT_SANDBOX_V3: BelaySandboxConfig;
185
199
  export declare const DEFAULT_NOTIFICATIONS_V3: BelayNotificationsConfig;
186
200
  export declare const DEFAULT_APPROVAL_SIGNING_V3: BelayApprovalSigningConfig;
201
+ export declare const DEFAULT_APPROVAL_CONFIG: BelayApprovalConfig;
187
202
  export declare const DEFAULT_EGRESS_V3: BelayEgressConfig;
188
203
  export declare function normalizeEgressListenHost(host: string): string;
189
204
  export declare const DEFAULT_CONFIG_V2: BelayConfigV2;
@@ -217,6 +232,9 @@ type RawConfigInput = Partial<{
217
232
  controlPlane: Partial<BelayControlPlaneConfig>;
218
233
  notifications: Partial<BelayNotificationsConfig>;
219
234
  approvalSigning: Partial<BelayApprovalSigningConfig>;
235
+ approval: Partial<BelayApprovalConfig> & {
236
+ autoReplayScopes?: Partial<BelayApprovalAutoReplayScopes>;
237
+ };
220
238
  egress: Partial<BelayEgressConfig>;
221
239
  sandbox: Partial<BelaySandboxConfig>;
222
240
  audit: Partial<BelayConfigV2['audit']>;
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { warnDeprecatedJudgeModelAuto } from './judge-model-policy.js';
3
3
  import { getJudgeProviderSpec, inferProviderIdFromConfig, isRemovedProviderId, normalizeLegacyProviderId, warnRemovedProviderId, } from './verdict/judge-catalog.js';
4
+ import { normalizeJudgeRuntimeConfig, } from './verdict/judge-runtime-config.js';
4
5
  export const DEFAULT_JUDGE_LOCAL_OLLAMA = {
5
6
  provider: 'ollama',
6
7
  providerId: 'ollama',
@@ -95,6 +96,15 @@ export const DEFAULT_NOTIFICATIONS_V3 = {};
95
96
  export const DEFAULT_APPROVAL_SIGNING_V3 = {
96
97
  required: false,
97
98
  };
99
+ export const DEFAULT_APPROVAL_CONFIG = {
100
+ flow: 'one_step',
101
+ autoReplayScopes: {
102
+ shell: true,
103
+ tool: false,
104
+ subagent: false,
105
+ },
106
+ executionLeaseMs: 60_000,
107
+ };
98
108
  export const DEFAULT_EGRESS_V3 = {
99
109
  enabled: false,
100
110
  listenHost: '127.0.0.1',
@@ -148,6 +158,7 @@ export const DEFAULT_CONFIG_V4 = {
148
158
  controlPlane: { ...DEFAULT_CONTROL_PLANE_V3 },
149
159
  notifications: { ...DEFAULT_NOTIFICATIONS_V3 },
150
160
  approvalSigning: { ...DEFAULT_APPROVAL_SIGNING_V3 },
161
+ approval: { ...DEFAULT_APPROVAL_CONFIG },
151
162
  egress: { ...DEFAULT_EGRESS_V3 },
152
163
  sandbox: { ...DEFAULT_SANDBOX_V3 },
153
164
  audit: { ...DEFAULT_CONFIG_V2.audit },
@@ -190,6 +201,7 @@ export function migrateV2ToV3(v2, rawOverrides) {
190
201
  controlPlane: { ...LEGACY_CONTROL_PLANE_V3 },
191
202
  notifications: { ...DEFAULT_NOTIFICATIONS_V3 },
192
203
  approvalSigning: { ...DEFAULT_APPROVAL_SIGNING_V3 },
204
+ approval: { ...DEFAULT_APPROVAL_CONFIG },
193
205
  egress: { ...DEFAULT_EGRESS_V3 },
194
206
  sandbox: { ...DEFAULT_SANDBOX_V3 },
195
207
  audit: v2.audit,
@@ -283,6 +295,7 @@ export function normalizeJudgeConfig(judge) {
283
295
  : null,
284
296
  ...(judge.cloudConsent ? { cloudConsent: judge.cloudConsent } : {}),
285
297
  ...(judge.credential ? { credential: judge.credential } : {}),
298
+ runtime: normalizeJudgeRuntimeConfig(judge.runtime),
286
299
  };
287
300
  }
288
301
  let providerId = rawProviderId && normalizeLegacyProviderId(rawProviderId)
@@ -333,6 +346,7 @@ export function normalizeJudgeConfig(judge) {
333
346
  ...(judge.credential.ref ? { ref: judge.credential.ref } : {}),
334
347
  };
335
348
  }
349
+ normalized.runtime = normalizeJudgeRuntimeConfig(judge.runtime);
336
350
  return normalized;
337
351
  }
338
352
  export function rejectTeamLayerJudgeSecrets(judge, source) {
@@ -395,6 +409,14 @@ function mergeV3FromRaw(base, raw) {
395
409
  ...base.approvalSigning,
396
410
  ...(raw.approvalSigning ?? {}),
397
411
  },
412
+ approval: {
413
+ ...base.approval,
414
+ ...(raw.approval ?? {}),
415
+ autoReplayScopes: {
416
+ ...base.approval.autoReplayScopes,
417
+ ...(raw.approval?.autoReplayScopes ?? {}),
418
+ },
419
+ },
398
420
  egress: {
399
421
  ...base.egress,
400
422
  ...(raw.egress ?? {}),
@@ -446,6 +468,14 @@ function normalizeV3Raw(raw) {
446
468
  approvalSigning: {
447
469
  required: raw.approvalSigning?.required === true,
448
470
  },
471
+ approval: {
472
+ ...DEFAULT_APPROVAL_CONFIG,
473
+ ...(raw.approval ?? {}),
474
+ autoReplayScopes: {
475
+ ...DEFAULT_APPROVAL_CONFIG.autoReplayScopes,
476
+ ...(raw.approval?.autoReplayScopes ?? {}),
477
+ },
478
+ },
449
479
  egress: {
450
480
  ...DEFAULT_EGRESS_V3,
451
481
  ...(raw.egress ?? {}),
@@ -676,6 +706,17 @@ export function normalizeConfig(config) {
676
706
  approvalSigning: {
677
707
  required: v4.approvalSigning?.required === true,
678
708
  },
709
+ approval: {
710
+ flow: v4.approval?.flow === 'two_step' ? 'two_step' : DEFAULT_APPROVAL_CONFIG.flow,
711
+ autoReplayScopes: {
712
+ shell: v4.approval?.autoReplayScopes?.shell !== false,
713
+ tool: v4.approval?.autoReplayScopes?.tool === true,
714
+ subagent: v4.approval?.autoReplayScopes?.subagent === true,
715
+ },
716
+ executionLeaseMs: typeof v4.approval?.executionLeaseMs === 'number' && v4.approval.executionLeaseMs > 0
717
+ ? v4.approval.executionLeaseMs
718
+ : DEFAULT_APPROVAL_CONFIG.executionLeaseMs,
719
+ },
679
720
  egress: {
680
721
  enabled: v4.egress?.enabled === true,
681
722
  listenHost: normalizeEgressListenHost(typeof v4.egress?.listenHost === 'string' && v4.egress.listenHost.trim()
@@ -752,6 +793,14 @@ export function mergeConfig(existing, defaults = DEFAULT_CONFIG_V4) {
752
793
  ...defaults.approvalSigning,
753
794
  ...migrated.approvalSigning,
754
795
  },
796
+ approval: {
797
+ ...defaults.approval,
798
+ ...migrated.approval,
799
+ autoReplayScopes: {
800
+ ...defaults.approval.autoReplayScopes,
801
+ ...migrated.approval.autoReplayScopes,
802
+ },
803
+ },
755
804
  egress: {
756
805
  ...defaults.egress,
757
806
  ...migrated.egress,
@@ -167,28 +167,28 @@ function applyFsScopePeripheralPolicy(outsideRepoPaths, result, options) {
167
167
  return result;
168
168
  }
169
169
  function applySandboxOutsideBoundary(command, action, result, options) {
170
- const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
170
+ const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot, options.trustedWorkspaceRoots);
171
171
  return applySandboxFsScopeBoundary(outsideRepoPaths, result, options, {
172
172
  redirect: command.includes('>'),
173
173
  });
174
174
  }
175
175
  function applyShellPeripheralPolicy(command, action, result, options) {
176
- const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
176
+ const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot, options.trustedWorkspaceRoots);
177
177
  return applyFsScopePeripheralPolicy(outsideRepoPaths, result, options);
178
178
  }
179
- function outsideRepoPathsForToolAction(action) {
179
+ function outsideRepoPathsForToolAction(action, options) {
180
180
  const payload = action.payload ?? {};
181
- const paths = new Set(collectOutsideRepoPathsFromToolPayload(payload, action.cwd, action.repoRoot));
181
+ const paths = new Set(collectOutsideRepoPathsFromToolPayload(payload, action.cwd, action.repoRoot, options.trustedWorkspaceRoots));
182
182
  const command = shellCommandFromPayload(payload);
183
183
  if (command) {
184
- for (const resolved of collectOutsideRepoPaths(command, action.cwd, action.repoRoot)) {
184
+ for (const resolved of collectOutsideRepoPaths(command, action.cwd, action.repoRoot, options.trustedWorkspaceRoots)) {
185
185
  paths.add(resolved);
186
186
  }
187
187
  }
188
188
  return [...paths];
189
189
  }
190
190
  function applyToolSandboxPolicies(action, result, options) {
191
- const outsideRepoPaths = outsideRepoPathsForToolAction(action);
191
+ const outsideRepoPaths = outsideRepoPathsForToolAction(action, options);
192
192
  const command = shellCommandFromPayload(action.payload ?? {});
193
193
  let next = applySandboxFsScopeBoundary(outsideRepoPaths, result, options, {
194
194
  redirect: command.includes('>'),
@@ -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
+ };