@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,9 +1,11 @@
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
- import { classifyShell } from './verdict/adapter.js';
6
+ import { classifyShell, resolveClassifierTrustedCwd } from './verdict/adapter.js';
7
+ import { mutationPrescanRequiresAsk, tier1RequiresAsk } from './verdict/judge.js';
8
+ import { createJudgeFromConfig } from './verdict/judge-factory.js';
7
9
  const DEFAULT_SENSITIVE_PATHS = ['.env', '.env.*', '**/credentials/**'];
8
10
  const FILE_WRITE_TOOL_NAMES = new Set(['write']);
9
11
  const FILE_EDIT_TOOL_NAMES = new Set([
@@ -74,6 +76,75 @@ function applyPatchTargets(patch) {
74
76
  function normalizedToolName(toolName) {
75
77
  return toolName.trim().toLowerCase();
76
78
  }
79
+ function resolveFileTier1Judge(config, options, repoRoot) {
80
+ return options.tier1Judge ?? createJudgeFromConfig(config, { repoRoot });
81
+ }
82
+ async function classifyFileMutationWithTier1(params) {
83
+ const judge = resolveFileTier1Judge(params.config, params.options, params.repoRoot);
84
+ const trustedCwd = resolveClassifierTrustedCwd(params.cwd, params.options);
85
+ const prescan = mutationPrescanRequiresAsk({
86
+ targets: [params.filePath],
87
+ cwd: params.cwd,
88
+ repoRoot: params.repoRoot,
89
+ trustedCwd,
90
+ trustedWorkspaceRoots: params.options.trustedWorkspaceRoots,
91
+ sensitivePaths: params.options.sensitivePaths ?? params.config.classifier.sensitivePaths,
92
+ });
93
+ if (prescan) {
94
+ return {
95
+ verdict: 'deny_pending_approval',
96
+ reason: 'tier1_catastrophic',
97
+ summary: params.filePath,
98
+ fingerprint: toolFingerprint(params.toolName, { path: params.filePath }, params.repoRoot),
99
+ assessment: {
100
+ reversibility: 'irreversible',
101
+ external: params.locationLabel === 'outside_repo',
102
+ blastRadius: params.locationLabel === 'outside_repo'
103
+ ? 'outside the repository'
104
+ : 'sensitive repository file',
105
+ confidence: 0.95,
106
+ signals: [...params.signals, 'tier1_catastrophic', prescan.reason],
107
+ },
108
+ };
109
+ }
110
+ const tier1Text = `file_mutation: ${params.toolKind} path=${params.resolvedPath} location=${params.locationLabel}`;
111
+ const tier1 = await judge.evaluate({
112
+ text: tier1Text,
113
+ context: { cwd: params.cwd, repoRoot: params.repoRoot },
114
+ });
115
+ const fingerprint = toolFingerprint(params.toolName, { path: params.filePath }, params.repoRoot);
116
+ if (tier1RequiresAsk(tier1)) {
117
+ return {
118
+ verdict: 'deny_pending_approval',
119
+ reason: 'tier1_catastrophic',
120
+ summary: params.filePath,
121
+ fingerprint,
122
+ assessment: {
123
+ reversibility: 'irreversible',
124
+ external: params.locationLabel === 'outside_repo',
125
+ blastRadius: params.locationLabel === 'outside_repo'
126
+ ? 'outside the repository'
127
+ : 'sensitive repository file',
128
+ confidence: 0.82,
129
+ signals: [...params.signals, 'tier1_catastrophic', tier1.reason],
130
+ },
131
+ };
132
+ }
133
+ const reason = params.isDelete ? 'file_delete' : 'file_mutation';
134
+ return {
135
+ verdict: 'allow_flagged',
136
+ reason,
137
+ summary: params.filePath,
138
+ fingerprint,
139
+ assessment: {
140
+ reversibility: 'recoverable_with_cost',
141
+ external: params.locationLabel === 'outside_repo',
142
+ blastRadius: params.locationLabel === 'outside_repo' ? 'outside the repository' : 'this repository',
143
+ confidence: 0.72,
144
+ signals: [...params.signals, 'tier1_restorable', tier1.reason],
145
+ },
146
+ };
147
+ }
77
148
  export async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
78
149
  const toolName = String(payload.tool_name ?? '');
79
150
  const toolKind = normalizedToolName(toolName);
@@ -173,38 +244,66 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
173
244
  },
174
245
  };
175
246
  }
176
- const relativePath = relativeWithinRepo(repoRoot, resolvedPath);
177
- if (relativePath === null) {
247
+ const workspaceMatch = resolveWorkspaceRootMatch(repoRoot, options.trustedWorkspaceRoots, resolvedPath);
248
+ if (workspaceMatch === null) {
178
249
  signals.push('outside_repo_path');
179
- return {
180
- verdict: 'deny_pending_approval',
181
- reason: 'outside_repo_file_mutation',
182
- summary: filePath,
183
- fingerprint: toolFingerprint(toolName, { path: filePath }, repoRoot),
184
- assessment: {
185
- reversibility: 'irreversible',
186
- external: true,
187
- blastRadius: 'outside the repository',
188
- confidence: 0.9,
189
- signals,
190
- },
191
- };
250
+ return classifyFileMutationWithTier1({
251
+ toolName,
252
+ toolKind,
253
+ filePath,
254
+ resolvedPath,
255
+ repoRoot,
256
+ cwd,
257
+ config,
258
+ options,
259
+ signals,
260
+ isDelete: FILE_DELETE_TOOL_NAMES.has(toolKind),
261
+ locationLabel: 'outside_repo',
262
+ });
263
+ }
264
+ if (workspaceMatch.kind === 'trusted') {
265
+ signals.push('trusted_workspace_root');
192
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;
193
292
  if (matchesSensitivePath(relativePath, sensitivePaths)) {
194
293
  signals.push('sensitive_path');
195
- return {
196
- verdict: 'deny_pending_approval',
197
- reason: 'sensitive_file_mutation',
198
- summary: filePath,
199
- fingerprint: toolFingerprint(toolName, { path: filePath }, repoRoot),
200
- assessment: {
201
- reversibility: 'irreversible',
202
- external: false,
203
- blastRadius: 'sensitive repository file',
204
- confidence: 0.88,
205
- signals,
206
- },
207
- };
294
+ return classifyFileMutationWithTier1({
295
+ toolName,
296
+ toolKind,
297
+ filePath,
298
+ resolvedPath,
299
+ repoRoot,
300
+ cwd,
301
+ config,
302
+ options,
303
+ signals,
304
+ isDelete: FILE_DELETE_TOOL_NAMES.has(toolKind),
305
+ locationLabel: 'sensitive_path',
306
+ });
208
307
  }
209
308
  if (FILE_DELETE_TOOL_NAMES.has(toolKind)) {
210
309
  signals.push('file_delete');
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { applyConfigPreset } from '../presets.js';
3
- import { DEFAULT_CONFIG_V3, mergeConfig } from './config.js';
3
+ import { DEFAULT_CONFIG_V3, mergeConfig, rejectTeamLayerJudgeSecrets } from './config.js';
4
4
  export function teamConfigPath(homedir = () => process.env.HOME ?? process.env.USERPROFILE ?? '') {
5
5
  const xdg = process.env.XDG_CONFIG_HOME?.trim();
6
6
  const base = xdg || path.join(homedir(), '.config');
@@ -40,6 +40,7 @@ export function resolveLayeredConfig(params) {
40
40
  const teamRaw = teamFile.preset
41
41
  ? applyConfigPreset(teamFile.preset, teamFile.config ?? {})
42
42
  : (teamFile.config ?? params.teamConfig);
43
+ rejectTeamLayerJudgeSecrets(teamRaw.judge, 'team');
43
44
  config = mergeConfigLayer(config, asV3Layer(teamRaw));
44
45
  provenance.push({
45
46
  path: params.teamConfigPath ?? teamConfigPath(),
@@ -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 {
@@ -76,13 +77,33 @@ export interface BelayRedactionConfig {
76
77
  maskKeyValueSecrets: boolean;
77
78
  maskHighEntropyStrings: boolean;
78
79
  }
79
- export type JudgeProvider = 'ollama' | 'openai-compatible';
80
+ export type JudgeProvider = 'ollama' | 'openai-compatible' | 'anthropic';
81
+ export type JudgeProviderId = 'ollama' | 'codex' | 'claude' | 'cursor';
82
+ /** Read-only legacy ids; preserved on load with warning until `belay config` migrates. */
83
+ export type DeprecatedJudgeProviderId = 'openrouter' | 'custom';
84
+ export type JudgeCredentialMode = 'project' | 'apiKey';
85
+ export type JudgeCredentialRef = `store:judge` | `env:${string}`;
86
+ export interface JudgeCredentialConfig {
87
+ mode: JudgeCredentialMode;
88
+ ref?: JudgeCredentialRef;
89
+ }
90
+ export interface JudgeCloudConsent {
91
+ accepted: boolean;
92
+ at: string;
93
+ providerId: JudgeProviderId;
94
+ endpoint: string;
95
+ by: string;
96
+ }
80
97
  export interface BelayJudgeConfig {
81
98
  provider: JudgeProvider;
99
+ providerId?: JudgeProviderId | DeprecatedJudgeProviderId;
82
100
  model: string;
83
101
  timeoutMs: number;
84
102
  endpoint: string | null;
85
103
  keepAlive: string | null;
104
+ cloudConsent?: JudgeCloudConsent;
105
+ credential?: JudgeCredentialConfig;
106
+ runtime?: BelayJudgeRuntimeConfig;
86
107
  }
87
108
  export declare const DEFAULT_JUDGE_LOCAL_OLLAMA: BelayJudgeConfig;
88
109
  export declare const DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE: BelayJudgeConfig;
@@ -118,6 +139,17 @@ export interface BelayApprovalSigningConfig {
118
139
  /** When true, out-of-band approvals must present a signed token. */
119
140
  required: boolean;
120
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
+ }
121
153
  export interface BelayEgressConfig {
122
154
  enabled: boolean;
123
155
  listenHost: string;
@@ -141,6 +173,7 @@ export interface BelayConfigV4 {
141
173
  controlPlane: BelayControlPlaneConfig;
142
174
  notifications: BelayNotificationsConfig;
143
175
  approvalSigning: BelayApprovalSigningConfig;
176
+ approval: BelayApprovalConfig;
144
177
  egress: BelayEgressConfig;
145
178
  sandbox: BelaySandboxConfig;
146
179
  audit: BelayConfigV2['audit'];
@@ -165,6 +198,7 @@ export declare const DEFAULT_CONTROL_PLANE_V3: BelayControlPlaneConfig;
165
198
  export declare const DEFAULT_SANDBOX_V3: BelaySandboxConfig;
166
199
  export declare const DEFAULT_NOTIFICATIONS_V3: BelayNotificationsConfig;
167
200
  export declare const DEFAULT_APPROVAL_SIGNING_V3: BelayApprovalSigningConfig;
201
+ export declare const DEFAULT_APPROVAL_CONFIG: BelayApprovalConfig;
168
202
  export declare const DEFAULT_EGRESS_V3: BelayEgressConfig;
169
203
  export declare function normalizeEgressListenHost(host: string): string;
170
204
  export declare const DEFAULT_CONFIG_V2: BelayConfigV2;
@@ -180,8 +214,9 @@ export declare function isConfigV1(value: unknown): value is BelayConfigV1;
180
214
  export declare function isConfigV2(value: unknown): value is BelayConfigV2;
181
215
  export declare function isConfigV3(value: unknown): value is BelayConfigV4;
182
216
  export declare function isConfigV4(value: unknown): value is BelayConfigV4;
183
- export declare function normalizeJudgeProvider(provider: string | undefined): 'ollama' | 'openai-compatible';
217
+ export declare function normalizeJudgeProvider(provider: string | undefined): 'ollama' | 'openai-compatible' | 'anthropic';
184
218
  export declare function normalizeJudgeConfig(judge: BelayJudgeConfig): BelayJudgeConfig;
219
+ export declare function rejectTeamLayerJudgeSecrets(judge: Partial<BelayJudgeConfig> | undefined, source: 'team' | 'repo'): void;
185
220
  export declare function migrateV3ToV4(v3: BelayConfigV4, raw?: RawConfigInput): BelayConfigV4;
186
221
  type RawConfigInput = Partial<{
187
222
  version: number;
@@ -197,6 +232,9 @@ type RawConfigInput = Partial<{
197
232
  controlPlane: Partial<BelayControlPlaneConfig>;
198
233
  notifications: Partial<BelayNotificationsConfig>;
199
234
  approvalSigning: Partial<BelayApprovalSigningConfig>;
235
+ approval: Partial<BelayApprovalConfig> & {
236
+ autoReplayScopes?: Partial<BelayApprovalAutoReplayScopes>;
237
+ };
200
238
  egress: Partial<BelayEgressConfig>;
201
239
  sandbox: Partial<BelaySandboxConfig>;
202
240
  audit: Partial<BelayConfigV2['audit']>;
@@ -1,6 +1,10 @@
1
1
  import path from 'node:path';
2
+ import { warnDeprecatedJudgeModelAuto } from './judge-model-policy.js';
3
+ import { getJudgeProviderSpec, inferProviderIdFromConfig, isRemovedProviderId, normalizeLegacyProviderId, warnRemovedProviderId, } from './verdict/judge-catalog.js';
4
+ import { normalizeJudgeRuntimeConfig, } from './verdict/judge-runtime-config.js';
2
5
  export const DEFAULT_JUDGE_LOCAL_OLLAMA = {
3
6
  provider: 'ollama',
7
+ providerId: 'ollama',
4
8
  model: 'gemma4:e2b',
5
9
  endpoint: 'http://localhost:11434',
6
10
  timeoutMs: 25000,
@@ -8,7 +12,8 @@ export const DEFAULT_JUDGE_LOCAL_OLLAMA = {
8
12
  };
9
13
  export const DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE = {
10
14
  provider: 'openai-compatible',
11
- model: 'auto',
15
+ providerId: 'codex',
16
+ model: 'gpt-5.3-codex-high',
12
17
  timeoutMs: 8000,
13
18
  endpoint: null,
14
19
  keepAlive: null,
@@ -91,6 +96,15 @@ export const DEFAULT_NOTIFICATIONS_V3 = {};
91
96
  export const DEFAULT_APPROVAL_SIGNING_V3 = {
92
97
  required: false,
93
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
+ };
94
108
  export const DEFAULT_EGRESS_V3 = {
95
109
  enabled: false,
96
110
  listenHost: '127.0.0.1',
@@ -144,6 +158,7 @@ export const DEFAULT_CONFIG_V4 = {
144
158
  controlPlane: { ...DEFAULT_CONTROL_PLANE_V3 },
145
159
  notifications: { ...DEFAULT_NOTIFICATIONS_V3 },
146
160
  approvalSigning: { ...DEFAULT_APPROVAL_SIGNING_V3 },
161
+ approval: { ...DEFAULT_APPROVAL_CONFIG },
147
162
  egress: { ...DEFAULT_EGRESS_V3 },
148
163
  sandbox: { ...DEFAULT_SANDBOX_V3 },
149
164
  audit: { ...DEFAULT_CONFIG_V2.audit },
@@ -186,6 +201,7 @@ export function migrateV2ToV3(v2, rawOverrides) {
186
201
  controlPlane: { ...LEGACY_CONTROL_PLANE_V3 },
187
202
  notifications: { ...DEFAULT_NOTIFICATIONS_V3 },
188
203
  approvalSigning: { ...DEFAULT_APPROVAL_SIGNING_V3 },
204
+ approval: { ...DEFAULT_APPROVAL_CONFIG },
189
205
  egress: { ...DEFAULT_EGRESS_V3 },
190
206
  sandbox: { ...DEFAULT_SANDBOX_V3 },
191
207
  audit: v2.audit,
@@ -209,44 +225,141 @@ export function isConfigV4(value) {
209
225
  return typeof value === 'object' && value !== null && value.version === 4;
210
226
  }
211
227
  export function normalizeJudgeProvider(provider) {
228
+ if (provider === 'anthropic') {
229
+ return 'anthropic';
230
+ }
212
231
  if (provider === 'openai-compatible' || provider === 'cursor') {
213
232
  return 'openai-compatible';
214
233
  }
215
234
  return 'ollama';
216
235
  }
236
+ function defaultJudgeTemplateForProvider(provider) {
237
+ if (provider === 'ollama') {
238
+ return DEFAULT_JUDGE_LOCAL_OLLAMA;
239
+ }
240
+ if (provider === 'anthropic') {
241
+ return {
242
+ provider: 'anthropic',
243
+ providerId: 'claude',
244
+ model: 'claude-sonnet-4-6',
245
+ timeoutMs: 8000,
246
+ endpoint: null,
247
+ keepAlive: null,
248
+ };
249
+ }
250
+ return DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE;
251
+ }
217
252
  function synthesizeJudgeFromRaw(raw) {
218
253
  const judge = raw.judge;
219
254
  if (judge?.provider) {
220
- const provider = normalizeJudgeProvider(judge.provider);
221
- const base = provider === 'openai-compatible'
222
- ? DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE
223
- : DEFAULT_JUDGE_LOCAL_OLLAMA;
255
+ const rawProvider = String(judge.provider);
256
+ const provider = normalizeJudgeProvider(rawProvider);
257
+ const base = defaultJudgeTemplateForProvider(provider);
258
+ const providerId = judge.providerId && normalizeLegacyProviderId(judge.providerId)
259
+ ? normalizeLegacyProviderId(judge.providerId)
260
+ : rawProvider === 'cursor'
261
+ ? 'cursor'
262
+ : undefined;
224
263
  return normalizeJudgeConfig({
225
264
  ...base,
226
265
  ...judge,
227
266
  provider,
267
+ ...(providerId ? { providerId } : {}),
228
268
  });
229
269
  }
230
270
  return { ...DEFAULT_JUDGE_LOCAL_OLLAMA };
231
271
  }
232
272
  export function normalizeJudgeConfig(judge) {
233
273
  const provider = normalizeJudgeProvider(judge.provider);
234
- const base = provider === 'openai-compatible'
235
- ? DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE
236
- : DEFAULT_JUDGE_LOCAL_OLLAMA;
237
- const model = typeof judge.model === 'string' && judge.model.trim() ? judge.model.trim() : base.model;
274
+ const base = defaultJudgeTemplateForProvider(provider);
275
+ const rawProviderId = judge.providerId ? String(judge.providerId) : undefined;
276
+ if (rawProviderId && isRemovedProviderId(rawProviderId)) {
277
+ warnRemovedProviderId(rawProviderId);
278
+ let model = typeof judge.model === 'string' && judge.model.trim() ? judge.model.trim() : base.model;
279
+ if (model === 'auto') {
280
+ warnDeprecatedJudgeModelAuto();
281
+ model = base.model;
282
+ }
283
+ const timeoutMs = typeof judge.timeoutMs === 'number' && judge.timeoutMs > 0 ? judge.timeoutMs : base.timeoutMs;
284
+ const endpoint = typeof judge.endpoint === 'string' && judge.endpoint.trim() ? judge.endpoint.trim() : null;
285
+ return {
286
+ provider,
287
+ providerId: rawProviderId,
288
+ model,
289
+ timeoutMs,
290
+ endpoint,
291
+ keepAlive: provider === 'ollama' && typeof judge.keepAlive === 'string' && judge.keepAlive.trim()
292
+ ? judge.keepAlive.trim()
293
+ : provider === 'ollama'
294
+ ? DEFAULT_JUDGE_LOCAL_OLLAMA.keepAlive
295
+ : null,
296
+ ...(judge.cloudConsent ? { cloudConsent: judge.cloudConsent } : {}),
297
+ ...(judge.credential ? { credential: judge.credential } : {}),
298
+ runtime: normalizeJudgeRuntimeConfig(judge.runtime),
299
+ };
300
+ }
301
+ let providerId = rawProviderId && normalizeLegacyProviderId(rawProviderId)
302
+ ? normalizeLegacyProviderId(rawProviderId)
303
+ : inferProviderIdFromConfig({ ...judge, provider });
304
+ const spec = getJudgeProviderSpec(providerId);
305
+ if (spec && spec.driver !== provider) {
306
+ providerId = inferProviderIdFromConfig({ ...judge, provider });
307
+ }
308
+ const catalogSpec = getJudgeProviderSpec(providerId);
309
+ let model = typeof judge.model === 'string' && judge.model.trim() ? judge.model.trim() : base.model;
310
+ if (model === 'auto' && catalogSpec?.defaultModel) {
311
+ warnDeprecatedJudgeModelAuto();
312
+ model = catalogSpec.defaultModel;
313
+ }
314
+ if (!model && catalogSpec?.defaultModel) {
315
+ model = catalogSpec.defaultModel;
316
+ }
238
317
  const timeoutMs = typeof judge.timeoutMs === 'number' && judge.timeoutMs > 0 ? judge.timeoutMs : base.timeoutMs;
239
- return {
318
+ let endpoint = typeof judge.endpoint === 'string' && judge.endpoint.trim() ? judge.endpoint.trim() : null;
319
+ if (!endpoint && catalogSpec?.defaultEndpoint) {
320
+ endpoint = catalogSpec.defaultEndpoint;
321
+ }
322
+ const normalized = {
240
323
  provider,
324
+ providerId,
241
325
  model,
242
326
  timeoutMs,
243
- endpoint: typeof judge.endpoint === 'string' && judge.endpoint.trim() ? judge.endpoint.trim() : null,
327
+ endpoint,
244
328
  keepAlive: provider === 'ollama' && typeof judge.keepAlive === 'string' && judge.keepAlive.trim()
245
329
  ? judge.keepAlive.trim()
246
330
  : provider === 'ollama'
247
331
  ? DEFAULT_JUDGE_LOCAL_OLLAMA.keepAlive
248
332
  : null,
249
333
  };
334
+ if (judge.cloudConsent?.accepted) {
335
+ normalized.cloudConsent = {
336
+ accepted: true,
337
+ at: judge.cloudConsent.at,
338
+ providerId: judge.cloudConsent.providerId ?? providerId,
339
+ endpoint: judge.cloudConsent.endpoint,
340
+ by: judge.cloudConsent.by,
341
+ };
342
+ }
343
+ if (judge.credential?.mode === 'project' || judge.credential?.mode === 'apiKey') {
344
+ normalized.credential = {
345
+ mode: judge.credential.mode,
346
+ ...(judge.credential.ref ? { ref: judge.credential.ref } : {}),
347
+ };
348
+ }
349
+ normalized.runtime = normalizeJudgeRuntimeConfig(judge.runtime);
350
+ return normalized;
351
+ }
352
+ export function rejectTeamLayerJudgeSecrets(judge, source) {
353
+ if (source !== 'team' || !judge) {
354
+ return;
355
+ }
356
+ if (judge.credential?.mode === 'apiKey') {
357
+ throw new Error('team config cannot set judge.credential.mode to apiKey.');
358
+ }
359
+ const raw = judge;
360
+ if (raw.credential && 'key' in raw.credential && raw.credential.key) {
361
+ throw new Error('team config cannot contain inline judge credential keys.');
362
+ }
250
363
  }
251
364
  export function migrateV3ToV4(v3, raw) {
252
365
  return normalizeConfig({
@@ -296,6 +409,14 @@ function mergeV3FromRaw(base, raw) {
296
409
  ...base.approvalSigning,
297
410
  ...(raw.approvalSigning ?? {}),
298
411
  },
412
+ approval: {
413
+ ...base.approval,
414
+ ...(raw.approval ?? {}),
415
+ autoReplayScopes: {
416
+ ...base.approval.autoReplayScopes,
417
+ ...(raw.approval?.autoReplayScopes ?? {}),
418
+ },
419
+ },
299
420
  egress: {
300
421
  ...base.egress,
301
422
  ...(raw.egress ?? {}),
@@ -347,6 +468,14 @@ function normalizeV3Raw(raw) {
347
468
  approvalSigning: {
348
469
  required: raw.approvalSigning?.required === true,
349
470
  },
471
+ approval: {
472
+ ...DEFAULT_APPROVAL_CONFIG,
473
+ ...(raw.approval ?? {}),
474
+ autoReplayScopes: {
475
+ ...DEFAULT_APPROVAL_CONFIG.autoReplayScopes,
476
+ ...(raw.approval?.autoReplayScopes ?? {}),
477
+ },
478
+ },
350
479
  egress: {
351
480
  ...DEFAULT_EGRESS_V3,
352
481
  ...(raw.egress ?? {}),
@@ -577,6 +706,17 @@ export function normalizeConfig(config) {
577
706
  approvalSigning: {
578
707
  required: v4.approvalSigning?.required === true,
579
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
+ },
580
720
  egress: {
581
721
  enabled: v4.egress?.enabled === true,
582
722
  listenHost: normalizeEgressListenHost(typeof v4.egress?.listenHost === 'string' && v4.egress.listenHost.trim()
@@ -653,6 +793,14 @@ export function mergeConfig(existing, defaults = DEFAULT_CONFIG_V4) {
653
793
  ...defaults.approvalSigning,
654
794
  ...migrated.approvalSigning,
655
795
  },
796
+ approval: {
797
+ ...defaults.approval,
798
+ ...migrated.approval,
799
+ autoReplayScopes: {
800
+ ...defaults.approval.autoReplayScopes,
801
+ ...migrated.approval.autoReplayScopes,
802
+ },
803
+ },
656
804
  egress: {
657
805
  ...defaults.egress,
658
806
  ...migrated.egress,
@@ -0,0 +1,11 @@
1
+ export interface JudgeCredentialStoreFile {
2
+ version: 1;
3
+ judge?: string;
4
+ }
5
+ export declare function credentialStorePath(stateDir: string): string;
6
+ export declare function writeJudgeCredentialStore(stateDir: string, apiKey: string): Promise<string>;
7
+ export declare function clearJudgeCredentialStore(stateDir: string): Promise<void>;
8
+ export declare function readJudgeCredentialStore(stateDir: string): Promise<string | null>;
9
+ export declare function judgeCredentialStoreExists(stateDir: string): Promise<boolean>;
10
+ export type CredentialRef = `store:judge` | `env:${string}`;
11
+ export declare function parseCredentialRef(ref: string): CredentialRef | null;
@@ -0,0 +1,60 @@
1
+ import { chmod, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ const STORE_FILENAME = 'credentials.json';
4
+ export function credentialStorePath(stateDir) {
5
+ return path.join(stateDir, STORE_FILENAME);
6
+ }
7
+ export async function writeJudgeCredentialStore(stateDir, apiKey) {
8
+ await mkdir(stateDir, { recursive: true, mode: 0o700 });
9
+ const filePath = credentialStorePath(stateDir);
10
+ const payload = {
11
+ version: 1,
12
+ judge: apiKey.trim(),
13
+ };
14
+ await writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, {
15
+ encoding: 'utf8',
16
+ mode: 0o600,
17
+ });
18
+ await chmod(filePath, 0o600);
19
+ return filePath;
20
+ }
21
+ export async function clearJudgeCredentialStore(stateDir) {
22
+ try {
23
+ await unlink(credentialStorePath(stateDir));
24
+ }
25
+ catch (error) {
26
+ if (error.code !== 'ENOENT') {
27
+ throw error;
28
+ }
29
+ }
30
+ }
31
+ export async function readJudgeCredentialStore(stateDir) {
32
+ const filePath = credentialStorePath(stateDir);
33
+ try {
34
+ const raw = await readFile(filePath, 'utf8');
35
+ const parsed = JSON.parse(raw);
36
+ const key = parsed.judge?.trim();
37
+ return key || null;
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ export async function judgeCredentialStoreExists(stateDir) {
44
+ try {
45
+ await readFile(credentialStorePath(stateDir), 'utf8');
46
+ return true;
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ export function parseCredentialRef(ref) {
53
+ if (ref === 'store:judge') {
54
+ return ref;
55
+ }
56
+ if (ref.startsWith('env:') && ref.length > 4) {
57
+ return ref;
58
+ }
59
+ return null;
60
+ }