@guilz-dev/belay 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +7 -1
  2. package/dist/adapters/layouts/scope.js +3 -2
  3. package/dist/adapters/shared/gate-runtime.d.ts +1 -2
  4. package/dist/adapters/shared/gate-runtime.js +226 -28
  5. package/dist/bundle/claude-runtime.mjs +6967 -4078
  6. package/dist/bundle/codex-runtime.mjs +5951 -3064
  7. package/dist/bundle/cursor-runtime.mjs +5951 -3064
  8. package/dist/cli.js +43 -1
  9. package/dist/commands/classify-for-report.js +1 -1
  10. package/dist/commands/config.d.ts +8 -0
  11. package/dist/commands/config.js +134 -11
  12. package/dist/commands/doctor.js +36 -1
  13. package/dist/commands/explain.js +11 -1
  14. package/dist/commands/judge.js +3 -2
  15. package/dist/commands/session.d.ts +20 -0
  16. package/dist/commands/session.js +51 -0
  17. package/dist/config-io.js +49 -12
  18. package/dist/conformance/guarantee-posture.d.ts +21 -0
  19. package/dist/conformance/guarantee-posture.js +46 -0
  20. package/dist/conformance/guarantee-table.d.ts +1 -0
  21. package/dist/conformance/guarantee-table.js +14 -9
  22. package/dist/core/approval-service.js +6 -2
  23. package/dist/core/approval.d.ts +2 -0
  24. package/dist/core/approval.js +14 -3
  25. package/dist/core/capability/approval-state-mutation.d.ts +13 -0
  26. package/dist/core/capability/approval-state-mutation.js +92 -0
  27. package/dist/core/capability/approval-v3.d.ts +15 -0
  28. package/dist/core/capability/approval-v3.js +74 -0
  29. package/dist/core/capability/attestation.d.ts +13 -0
  30. package/dist/core/capability/attestation.js +55 -0
  31. package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
  32. package/dist/core/capability/boundary-attestation-sign.js +84 -0
  33. package/dist/core/capability/boundary-driver-container.d.ts +7 -0
  34. package/dist/core/capability/boundary-driver-container.js +122 -0
  35. package/dist/core/capability/boundary-driver.d.ts +29 -0
  36. package/dist/core/capability/boundary-driver.js +43 -0
  37. package/dist/core/capability/boundary-egress.d.ts +21 -0
  38. package/dist/core/capability/boundary-egress.js +109 -0
  39. package/dist/core/capability/boundary-grant-materialize.d.ts +13 -0
  40. package/dist/core/capability/boundary-grant-materialize.js +78 -0
  41. package/dist/core/capability/boundary-profile.d.ts +15 -0
  42. package/dist/core/capability/boundary-profile.js +25 -0
  43. package/dist/core/capability/boundary-run.d.ts +23 -0
  44. package/dist/core/capability/boundary-run.js +23 -0
  45. package/dist/core/capability/boundary-session.d.ts +57 -0
  46. package/dist/core/capability/boundary-session.js +129 -0
  47. package/dist/core/capability/capability-request-hash.d.ts +2 -0
  48. package/dist/core/capability/capability-request-hash.js +8 -0
  49. package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
  50. package/dist/core/capability/gate-latency-ratchet.js +18 -0
  51. package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
  52. package/dist/core/capability/gate-policy-shadow.js +108 -0
  53. package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
  54. package/dist/core/capability/gate-shadow-audit.js +1 -0
  55. package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
  56. package/dist/core/capability/gate-shadow-ratchet.js +81 -0
  57. package/dist/core/capability/grant-lease.d.ts +9 -0
  58. package/dist/core/capability/grant-lease.js +46 -0
  59. package/dist/core/capability/grant-loader.d.ts +12 -0
  60. package/dist/core/capability/grant-loader.js +23 -0
  61. package/dist/core/capability/grant-match.d.ts +6 -0
  62. package/dist/core/capability/grant-match.js +93 -0
  63. package/dist/core/capability/grant.d.ts +16 -0
  64. package/dist/core/capability/grant.js +16 -0
  65. package/dist/core/capability/index.d.ts +8 -0
  66. package/dist/core/capability/index.js +7 -0
  67. package/dist/core/capability/limits.d.ts +7 -0
  68. package/dist/core/capability/limits.js +55 -0
  69. package/dist/core/capability/policy-bridge.d.ts +18 -0
  70. package/dist/core/capability/policy-bridge.js +51 -0
  71. package/dist/core/capability/policy-engine.d.ts +70 -0
  72. package/dist/core/capability/policy-engine.js +476 -0
  73. package/dist/core/capability/policy-types.d.ts +20 -0
  74. package/dist/core/capability/policy-types.js +1 -0
  75. package/dist/core/capability/request.d.ts +45 -0
  76. package/dist/core/capability/request.js +1 -0
  77. package/dist/core/capability/resolver.d.ts +4 -0
  78. package/dist/core/capability/resolver.js +4 -0
  79. package/dist/core/classify-subagent.d.ts +2 -1
  80. package/dist/core/classify-subagent.js +38 -2
  81. package/dist/core/classify-tool.js +126 -62
  82. package/dist/core/config.d.ts +14 -3
  83. package/dist/core/config.js +56 -3
  84. package/dist/core/gate-contract.d.ts +3 -0
  85. package/dist/core/gate-contract.js +3 -0
  86. package/dist/core/gate-engine.js +6 -1
  87. package/dist/core/judge-doctor.js +39 -14
  88. package/dist/core/judge-fallback-hints.d.ts +13 -0
  89. package/dist/core/judge-fallback-hints.js +108 -0
  90. package/dist/core/path-utils.d.ts +1 -0
  91. package/dist/core/path-utils.js +5 -2
  92. package/dist/core/recovery/capability.d.ts +3 -0
  93. package/dist/core/recovery/capability.js +22 -0
  94. package/dist/core/recovery/fail-closed.d.ts +6 -0
  95. package/dist/core/recovery/fail-closed.js +25 -0
  96. package/dist/core/recovery/index.d.ts +3 -0
  97. package/dist/core/recovery/index.js +2 -0
  98. package/dist/core/recovery/types.d.ts +20 -0
  99. package/dist/core/recovery/types.js +1 -0
  100. package/dist/core/standing-allow.js +3 -0
  101. package/dist/core/transactional/diff-evaluator.js +4 -1
  102. package/dist/core/transactional/eligibility.js +4 -0
  103. package/dist/core/transactional/git-worktree.d.ts +10 -3
  104. package/dist/core/transactional/git-worktree.js +81 -11
  105. package/dist/core/transactional/reasons.d.ts +1 -0
  106. package/dist/core/transactional/reasons.js +4 -0
  107. package/dist/core/transactional/runner.js +22 -6
  108. package/dist/core/transactional/types.d.ts +4 -0
  109. package/dist/core/types.d.ts +21 -3
  110. package/dist/core/verdict/adapter.d.ts +3 -4
  111. package/dist/core/verdict/adapter.js +16 -12
  112. package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
  113. package/dist/core/verdict/cursor-acp-client.js +345 -0
  114. package/dist/core/verdict/egress-classify.d.ts +2 -2
  115. package/dist/core/verdict/egress-classify.js +2 -2
  116. package/dist/core/verdict/git-classifier.d.ts +26 -0
  117. package/dist/core/verdict/git-classifier.js +533 -0
  118. package/dist/core/verdict/judge-audit.js +1 -0
  119. package/dist/core/verdict/judge-baseline.d.ts +1 -1
  120. package/dist/core/verdict/judge-baseline.js +1 -0
  121. package/dist/core/verdict/judge-broker-service.js +12 -0
  122. package/dist/core/verdict/judge-catalog.js +1 -0
  123. package/dist/core/verdict/judge-cli-fingerprint.d.ts +1 -0
  124. package/dist/core/verdict/judge-cli-fingerprint.js +6 -0
  125. package/dist/core/verdict/judge-cli.js +5 -3
  126. package/dist/core/verdict/judge-provider-matrix.js +1 -1
  127. package/dist/core/verdict/judge-runtime-config.d.ts +2 -0
  128. package/dist/core/verdict/judge-runtime-config.js +10 -6
  129. package/dist/core/verdict/judge-session-broker.d.ts +1 -1
  130. package/dist/core/verdict/judge-session-broker.js +10 -5
  131. package/dist/core/verdict/judge-transport.d.ts +5 -1
  132. package/dist/core/verdict/judge-transport.js +24 -14
  133. package/dist/core/verdict/judge.d.ts +2 -15
  134. package/dist/core/verdict/judge.js +5 -68
  135. package/dist/core/verdict/launcher-resolve.d.ts +2 -0
  136. package/dist/core/verdict/launcher-resolve.js +16 -0
  137. package/dist/core/verdict/prescan.d.ts +15 -0
  138. package/dist/core/verdict/prescan.js +68 -0
  139. package/dist/core/verdict/shell-policy.d.ts +25 -0
  140. package/dist/core/verdict/shell-policy.js +40 -0
  141. package/dist/core/verdict/shell-semantics.d.ts +24 -0
  142. package/dist/core/verdict/shell-semantics.js +225 -0
  143. package/dist/core/verdict/types.d.ts +13 -1
  144. package/dist/core/verdict/verdict.js +296 -178
  145. package/dist/corpus/evaluate.js +3 -7
  146. package/dist/corpus/gate-latency-budget.d.ts +39 -0
  147. package/dist/corpus/gate-latency-budget.js +33 -0
  148. package/dist/corpus/mutators.d.ts +1 -1
  149. package/dist/corpus/mutators.js +11 -0
  150. package/dist/corpus/runtime-match.js +1 -3
  151. package/dist/corpus/standing-allow-catalog.generated.js +29 -9
  152. package/dist/judge-broker-daemon.js +15 -1
  153. package/dist/services/sandbox-service.d.ts +4 -0
  154. package/dist/services/sandbox-service.js +28 -2
  155. package/dist/types.d.ts +2 -2
  156. package/dist/version.d.ts +1 -1
  157. package/dist/version.js +1 -1
  158. package/package.json +1 -1
@@ -1,3 +1,6 @@
1
+ import { BOUNDARY_PROFILE_L3_L4_ONLY } from './capability/boundary-profile.js';
2
+ import { policyReasonToLegacyReason } from './capability/policy-bridge.js';
3
+ import { evaluateSubagentPolicy, policyDecisionRequiresAsk } from './capability/policy-engine.js';
1
4
  import { canonicalStringify, subagentFingerprint } from './fingerprint.js';
2
5
  import { subagentFingerprintSource } from './replay-scrub.js';
3
6
  import { scrubValue } from './scrub.js';
@@ -25,7 +28,7 @@ function extractSubagentText(payload, options) {
25
28
  function fingerprintSource(payload, options) {
26
29
  return subagentFingerprintSource(payload, options.scrubOptions ?? {});
27
30
  }
28
- export function classifySubagent(payload, repoRoot, options = {}) {
31
+ export function classifySubagent(payload, repoRoot, options = {}, config) {
29
32
  const kind = payload.tool_name === 'Task' ? 'Task' : String(payload.subagent_type ?? 'generalPurpose');
30
33
  const scrubbed = fingerprintSource(payload, options);
31
34
  const summary = extractSubagentText(payload, options);
@@ -35,6 +38,36 @@ export function classifySubagent(payload, repoRoot, options = {}) {
35
38
  const pattern = new RegExp(`\\b${term}\\b`, 'i');
36
39
  return pattern.test(lowered);
37
40
  });
41
+ const signals = hasExternalTerm ? ['subagent_external_intent_hint'] : ['subagent_default_review'];
42
+ const { request, decision } = evaluateSubagentPolicy({
43
+ subagentType: kind,
44
+ summary,
45
+ repoRoot,
46
+ cwd: repoRoot,
47
+ inputFingerprint: fingerprint,
48
+ signals,
49
+ }, config, {
50
+ grants: options.grants,
51
+ attestation: options.attestation,
52
+ egressProxyActive: options.egressProxyActive,
53
+ });
54
+ if (policyDecisionRequiresAsk(decision)) {
55
+ return {
56
+ verdict: 'deny_pending_approval',
57
+ reason: policyReasonToLegacyReason(decision),
58
+ summary,
59
+ fingerprint,
60
+ assessment: {
61
+ reversibility: 'recoverable_with_cost',
62
+ external: hasExternalTerm,
63
+ blastRadius: 'subagent task scope',
64
+ confidence: hasExternalTerm ? 0.7 : 0.67,
65
+ signals: [...signals, ...decision.signals],
66
+ },
67
+ capabilityRequests: [request],
68
+ authorizationDecision: decision,
69
+ };
70
+ }
38
71
  return {
39
72
  verdict: 'allow_flagged',
40
73
  reason: 'subagent_review',
@@ -45,7 +78,10 @@ export function classifySubagent(payload, repoRoot, options = {}) {
45
78
  external: false,
46
79
  blastRadius: 'subagent task scope',
47
80
  confidence: hasExternalTerm ? 0.7 : 0.67,
48
- signals: hasExternalTerm ? ['subagent_external_intent_hint'] : ['subagent_default_review'],
81
+ signals,
49
82
  },
83
+ capabilityRequests: [request],
84
+ authorizationDecision: decision,
85
+ boundaryProfile: options.boundaryProfile ?? BOUNDARY_PROFILE_L3_L4_ONLY,
50
86
  };
51
87
  }
@@ -1,11 +1,14 @@
1
1
  import path from 'node:path';
2
+ import { BOUNDARY_PROFILE_L3_L4_ONLY } from './capability/boundary-profile.js';
3
+ import { policyReasonToLegacyReason } from './capability/policy-bridge.js';
4
+ import { evaluateFileMutationPolicy, policyDecisionRequiresAsk, } from './capability/policy-engine.js';
2
5
  import { canonicalStringify, toolFingerprint } from './fingerprint.js';
3
6
  import { matchesSensitivePath } from './glob.js';
4
7
  import { pathWithinRoot, resolveWorkspaceRootMatch } from './path-utils.js';
5
8
  import { scrubValue } from './scrub.js';
6
9
  import { classifyShell, resolveClassifierTrustedCwd } from './verdict/adapter.js';
7
- import { mutationPrescanRequiresAsk, tier1RequiresAsk } from './verdict/judge.js';
8
- import { createJudgeFromConfig } from './verdict/judge-factory.js';
10
+ import { isGitPath } from './verdict/containment.js';
11
+ import { mutationPrescanRequiresAsk } from './verdict/prescan.js';
9
12
  const DEFAULT_SENSITIVE_PATHS = ['.env', '.env.*', '**/credentials/**'];
10
13
  const FILE_WRITE_TOOL_NAMES = new Set(['write']);
11
14
  const FILE_EDIT_TOOL_NAMES = new Set([
@@ -18,6 +21,19 @@ const FILE_EDIT_TOOL_NAMES = new Set([
18
21
  ]);
19
22
  const FILE_DELETE_TOOL_NAMES = new Set(['delete']);
20
23
  const APPLY_PATCH_TOOL_NAMES = new Set(['apply_patch', 'applypatch']);
24
+ function policyAuth(options) {
25
+ if (!options.grants &&
26
+ options.attestation === undefined &&
27
+ options.egressProxyActive === undefined) {
28
+ return undefined;
29
+ }
30
+ return {
31
+ grants: options.grants,
32
+ attestation: options.attestation,
33
+ egressProxyActive: options.egressProxyActive,
34
+ sensitivePaths: options.sensitivePaths,
35
+ };
36
+ }
21
37
  function scrubPayload(value, options) {
22
38
  return scrubValue(value, options.scrubOptions);
23
39
  }
@@ -76,11 +92,7 @@ function applyPatchTargets(patch) {
76
92
  function normalizedToolName(toolName) {
77
93
  return toolName.trim().toLowerCase();
78
94
  }
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);
95
+ function classifyFileMutationWithPolicy(params) {
84
96
  const trustedCwd = resolveClassifierTrustedCwd(params.cwd, params.options);
85
97
  const prescan = mutationPrescanRequiresAsk({
86
98
  targets: [params.filePath],
@@ -91,11 +103,29 @@ async function classifyFileMutationWithTier1(params) {
91
103
  sensitivePaths: params.options.sensitivePaths ?? params.config.classifier.sensitivePaths,
92
104
  });
93
105
  if (prescan) {
106
+ const fingerprint = toolFingerprint(params.toolName, { path: params.filePath }, params.repoRoot);
107
+ const { request, decision } = evaluateFileMutationPolicy({
108
+ hookKind: 'tool',
109
+ toolKind: params.toolKind,
110
+ filePath: params.filePath,
111
+ resolvedPath: params.resolvedPath,
112
+ repoRoot: params.repoRoot,
113
+ cwd: params.cwd,
114
+ inputFingerprint: fingerprint,
115
+ signals: [...params.signals, 'tier1_catastrophic', prescan.reason],
116
+ isDelete: params.isDelete,
117
+ locationLabel: params.locationLabel,
118
+ trustedWorkspaceRoots: params.options.trustedWorkspaceRoots,
119
+ sensitivePaths: params.options.sensitivePaths ?? params.config.classifier.sensitivePaths,
120
+ }, params.config, policyAuth(params.options));
121
+ const legacyReason = policyDecisionRequiresAsk(decision)
122
+ ? policyReasonToLegacyReason(decision)
123
+ : 'tier1_catastrophic';
94
124
  return {
95
125
  verdict: 'deny_pending_approval',
96
- reason: 'tier1_catastrophic',
126
+ reason: legacyReason,
97
127
  summary: params.filePath,
98
- fingerprint: toolFingerprint(params.toolName, { path: params.filePath }, params.repoRoot),
128
+ fingerprint,
99
129
  assessment: {
100
130
  reversibility: 'irreversible',
101
131
  external: params.locationLabel === 'outside_repo',
@@ -103,20 +133,32 @@ async function classifyFileMutationWithTier1(params) {
103
133
  ? 'outside the repository'
104
134
  : 'sensitive repository file',
105
135
  confidence: 0.95,
106
- signals: [...params.signals, 'tier1_catastrophic', prescan.reason],
136
+ signals: [...params.signals, 'tier1_catastrophic', prescan.reason, ...decision.signals],
107
137
  },
138
+ capabilityRequests: [request],
139
+ authorizationDecision: decision,
140
+ boundaryProfile: params.options.boundaryProfile ?? BOUNDARY_PROFILE_L3_L4_ONLY,
108
141
  };
109
142
  }
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
143
  const fingerprint = toolFingerprint(params.toolName, { path: params.filePath }, params.repoRoot);
116
- if (tier1RequiresAsk(tier1)) {
144
+ const { request, decision } = evaluateFileMutationPolicy({
145
+ hookKind: 'tool',
146
+ toolKind: params.toolKind,
147
+ filePath: params.filePath,
148
+ resolvedPath: params.resolvedPath,
149
+ repoRoot: params.repoRoot,
150
+ cwd: params.cwd,
151
+ inputFingerprint: fingerprint,
152
+ signals: params.signals,
153
+ isDelete: params.isDelete,
154
+ locationLabel: params.locationLabel,
155
+ trustedWorkspaceRoots: params.options.trustedWorkspaceRoots,
156
+ sensitivePaths: params.options.sensitivePaths ?? params.config.classifier.sensitivePaths,
157
+ }, params.config, policyAuth(params.options));
158
+ if (policyDecisionRequiresAsk(decision)) {
117
159
  return {
118
160
  verdict: 'deny_pending_approval',
119
- reason: 'tier1_catastrophic',
161
+ reason: policyReasonToLegacyReason(decision),
120
162
  summary: params.filePath,
121
163
  fingerprint,
122
164
  assessment: {
@@ -125,9 +167,12 @@ async function classifyFileMutationWithTier1(params) {
125
167
  blastRadius: params.locationLabel === 'outside_repo'
126
168
  ? 'outside the repository'
127
169
  : 'sensitive repository file',
128
- confidence: 0.82,
129
- signals: [...params.signals, 'tier1_catastrophic', tier1.reason],
170
+ confidence: 0.95,
171
+ signals: [...params.signals, ...decision.signals],
130
172
  },
173
+ capabilityRequests: [request],
174
+ authorizationDecision: decision,
175
+ boundaryProfile: params.options.boundaryProfile ?? BOUNDARY_PROFILE_L3_L4_ONLY,
131
176
  };
132
177
  }
133
178
  const reason = params.isDelete ? 'file_delete' : 'file_mutation';
@@ -141,8 +186,11 @@ async function classifyFileMutationWithTier1(params) {
141
186
  external: params.locationLabel === 'outside_repo',
142
187
  blastRadius: params.locationLabel === 'outside_repo' ? 'outside the repository' : 'this repository',
143
188
  confidence: 0.72,
144
- signals: [...params.signals, 'tier1_restorable', tier1.reason],
189
+ signals: [...params.signals, ...decision.signals],
145
190
  },
191
+ capabilityRequests: [request],
192
+ authorizationDecision: decision,
193
+ boundaryProfile: params.options.boundaryProfile ?? BOUNDARY_PROFILE_L3_L4_ONLY,
146
194
  };
147
195
  }
148
196
  export async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
@@ -230,24 +278,24 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
230
278
  const hitsProtectedRoot = protectedRoots.some((root) => pathWithinRoot(root, resolvedPath));
231
279
  if (hitsProtectedRoot) {
232
280
  signals.push('control_plane_path');
233
- return {
234
- verdict: 'deny_pending_approval',
235
- reason: 'control_plane_mutation',
236
- summary: filePath,
237
- fingerprint: toolFingerprint(toolName, { path: filePath }, repoRoot),
238
- assessment: {
239
- reversibility: 'irreversible',
240
- external: false,
241
- blastRadius: 'agent-belay control plane',
242
- confidence: 0.97,
243
- signals,
244
- },
245
- };
281
+ return classifyFileMutationWithPolicy({
282
+ toolName,
283
+ toolKind,
284
+ filePath,
285
+ resolvedPath,
286
+ repoRoot,
287
+ cwd,
288
+ config,
289
+ options,
290
+ signals,
291
+ isDelete: FILE_DELETE_TOOL_NAMES.has(toolKind),
292
+ locationLabel: 'control_plane',
293
+ });
246
294
  }
247
295
  const workspaceMatch = resolveWorkspaceRootMatch(repoRoot, options.trustedWorkspaceRoots, resolvedPath);
248
296
  if (workspaceMatch === null) {
249
297
  signals.push('outside_repo_path');
250
- return classifyFileMutationWithTier1({
298
+ return classifyFileMutationWithPolicy({
251
299
  toolName,
252
300
  toolKind,
253
301
  filePath,
@@ -274,7 +322,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
274
322
  sensitivePaths,
275
323
  });
276
324
  if (workspacePrescan) {
277
- return classifyFileMutationWithTier1({
325
+ return classifyFileMutationWithPolicy({
278
326
  toolName,
279
327
  toolKind,
280
328
  filePath,
@@ -291,7 +339,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
291
339
  const relativePath = workspaceMatch.relativePath;
292
340
  if (matchesSensitivePath(relativePath, sensitivePaths)) {
293
341
  signals.push('sensitive_path');
294
- return classifyFileMutationWithTier1({
342
+ return classifyFileMutationWithPolicy({
295
343
  toolName,
296
344
  toolKind,
297
345
  filePath,
@@ -306,35 +354,51 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
306
354
  });
307
355
  }
308
356
  if (FILE_DELETE_TOOL_NAMES.has(toolKind)) {
309
- signals.push('file_delete');
310
- return {
311
- verdict: 'allow_flagged',
312
- reason: 'file_delete',
313
- summary: filePath,
314
- fingerprint: toolFingerprint(toolName, { path: filePath }, repoRoot),
315
- assessment: {
316
- reversibility: 'recoverable_with_cost',
317
- external: false,
318
- blastRadius: 'this repository',
319
- confidence: 0.7,
357
+ if (isGitPath(resolvedPath, repoRoot)) {
358
+ signals.push('protected_artifact');
359
+ return classifyFileMutationWithPolicy({
360
+ toolName,
361
+ toolKind,
362
+ filePath,
363
+ resolvedPath,
364
+ repoRoot,
365
+ cwd,
366
+ config,
367
+ options,
320
368
  signals,
321
- },
322
- };
369
+ isDelete: true,
370
+ locationLabel: 'sensitive_path',
371
+ });
372
+ }
373
+ signals.push('file_delete');
374
+ return classifyFileMutationWithPolicy({
375
+ toolName,
376
+ toolKind,
377
+ filePath,
378
+ resolvedPath,
379
+ repoRoot,
380
+ cwd,
381
+ config,
382
+ options,
383
+ signals,
384
+ isDelete: true,
385
+ locationLabel: 'repo_local',
386
+ });
323
387
  }
324
388
  signals.push('file_mutation');
325
- return {
326
- verdict: 'allow_flagged',
327
- reason: 'file_mutation',
328
- summary: filePath,
329
- fingerprint: toolFingerprint(toolName, { path: filePath }, repoRoot),
330
- assessment: {
331
- reversibility: 'recoverable_with_cost',
332
- external: false,
333
- blastRadius: 'this repository',
334
- confidence: 0.68,
335
- signals,
336
- },
337
- };
389
+ return classifyFileMutationWithPolicy({
390
+ toolName,
391
+ toolKind,
392
+ filePath,
393
+ resolvedPath,
394
+ repoRoot,
395
+ cwd,
396
+ config,
397
+ options,
398
+ signals,
399
+ isDelete: false,
400
+ locationLabel: 'repo_local',
401
+ });
338
402
  }
339
403
  if (APPLY_PATCH_TOOL_NAMES.has(toolKind)) {
340
404
  const patch = extractPatch(payload);
@@ -1,6 +1,6 @@
1
- import type { ClassifierOptions, ControlPlaneIntegrity, ScrubOptions, UnknownLocalEffectPolicy, UnparseableShellPolicy } from './types.js';
1
+ import type { BoundaryDriverId } from './capability/attestation.js';
2
+ import type { BelayMode, ClassifierOptions, ControlPlaneIntegrity, ScrubOptions, UnknownLocalEffectPolicy, UnparseableShellPolicy } from './types.js';
2
3
  import { type BelayJudgeRuntimeConfig } from './verdict/judge-runtime-config.js';
3
- export type BelayMode = 'enforce' | 'audit';
4
4
  export type { UnknownLocalEffectPolicy };
5
5
  export interface BelayConfigV1 {
6
6
  version: 1;
@@ -94,7 +94,10 @@ export interface JudgeCloudConsent {
94
94
  endpoint: string;
95
95
  by: string;
96
96
  }
97
+ export type BelayJudgeMode = 'shadow' | 'off';
97
98
  export interface BelayJudgeConfig {
99
+ /** Gate uses PolicyEngine; judge runs async shadow/compare only. */
100
+ mode?: BelayJudgeMode;
98
101
  provider: JudgeProvider;
99
102
  providerId?: JudgeProviderId | DeprecatedJudgeProviderId;
100
103
  model: string;
@@ -157,8 +160,14 @@ export interface BelayEgressConfig {
157
160
  /** When true with egress enabled, L3 external command lists become hints only. */
158
161
  demoteL3External: boolean;
159
162
  }
163
+ export interface BelayCapabilityConfig {
164
+ grantsEnabled?: boolean;
165
+ boundaryDriver?: BoundaryDriverId;
166
+ attestationRelPath?: string;
167
+ }
168
+ export declare const DEFAULT_CAPABILITY_V5: BelayCapabilityConfig;
160
169
  export interface BelayConfigV4 {
161
- version: 4;
170
+ version: 4 | 5;
162
171
  adapter?: 'cursor' | 'claude' | 'codex';
163
172
  /** Where hooks/runtime/skill artifacts are installed. Defaults to project. */
164
173
  installScope?: 'project' | 'global';
@@ -178,6 +187,7 @@ export interface BelayConfigV4 {
178
187
  sandbox: BelaySandboxConfig;
179
188
  audit: BelayConfigV2['audit'];
180
189
  judge: BelayJudgeConfig;
190
+ capability?: BelayCapabilityConfig;
181
191
  }
182
192
  /** @deprecated Use BelayConfigV4 */
183
193
  export type BelayConfigV3 = BelayConfigV4;
@@ -239,6 +249,7 @@ type RawConfigInput = Partial<{
239
249
  sandbox: Partial<BelaySandboxConfig>;
240
250
  audit: Partial<BelayConfigV2['audit']>;
241
251
  installScope: 'project' | 'global';
252
+ capability: Partial<BelayCapabilityConfig>;
242
253
  }>;
243
254
  export declare function migrateConfig(loaded: unknown): BelayConfigV4;
244
255
  export declare function normalizeConfigV2(config: BelayConfigV2): BelayConfigV2;
@@ -3,6 +3,7 @@ import { warnDeprecatedJudgeModelAuto } from './judge-model-policy.js';
3
3
  import { getJudgeProviderSpec, inferProviderIdFromConfig, isRemovedProviderId, normalizeLegacyProviderId, warnRemovedProviderId, } from './verdict/judge-catalog.js';
4
4
  import { normalizeJudgeRuntimeConfig, } from './verdict/judge-runtime-config.js';
5
5
  export const DEFAULT_JUDGE_LOCAL_OLLAMA = {
6
+ mode: 'shadow',
6
7
  provider: 'ollama',
7
8
  providerId: 'ollama',
8
9
  model: 'gemma4:e2b',
@@ -11,6 +12,7 @@ export const DEFAULT_JUDGE_LOCAL_OLLAMA = {
11
12
  keepAlive: '30m',
12
13
  };
13
14
  export const DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE = {
15
+ mode: 'shadow',
14
16
  provider: 'openai-compatible',
15
17
  providerId: 'codex',
16
18
  model: 'gpt-5.3-codex-high',
@@ -20,6 +22,23 @@ export const DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE = {
20
22
  };
21
23
  /** @deprecated Use DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE */
22
24
  export const DEFAULT_JUDGE_CURSOR_COMPOSER = DEFAULT_JUDGE_OPENAI_COMPATIBLE_TEMPLATE;
25
+ export const DEFAULT_CAPABILITY_V5 = {
26
+ grantsEnabled: true,
27
+ boundaryDriver: 'host-integration',
28
+ attestationRelPath: '.belay/attestation.json',
29
+ };
30
+ function normalizeCapabilityConfig(capability) {
31
+ if (!capability) {
32
+ return undefined;
33
+ }
34
+ return {
35
+ ...DEFAULT_CAPABILITY_V5,
36
+ ...capability,
37
+ grantsEnabled: capability.grantsEnabled !== false,
38
+ boundaryDriver: capability.boundaryDriver ?? DEFAULT_CAPABILITY_V5.boundaryDriver,
39
+ attestationRelPath: capability.attestationRelPath ?? DEFAULT_CAPABILITY_V5.attestationRelPath,
40
+ };
41
+ }
23
42
  /** Pre-v0.4 defaults preserved when migrating existing v1/v2/v3 configs. */
24
43
  import { DEFAULT_SILENT_PASS_THRESHOLD } from './audit-summary.js';
25
44
  /** @deprecated Use DEFAULT_SILENT_PASS_THRESHOLD from audit-summary.js */
@@ -222,7 +241,8 @@ export function isConfigV3(value) {
222
241
  return version === 3 || version === 4;
223
242
  }
224
243
  export function isConfigV4(value) {
225
- return typeof value === 'object' && value !== null && value.version === 4;
244
+ const version = typeof value === 'object' && value !== null ? value.version : null;
245
+ return version === 4 || version === 5;
226
246
  }
227
247
  export function normalizeJudgeProvider(provider) {
228
248
  if (provider === 'anthropic') {
@@ -239,6 +259,7 @@ function defaultJudgeTemplateForProvider(provider) {
239
259
  }
240
260
  if (provider === 'anthropic') {
241
261
  return {
262
+ mode: 'shadow',
242
263
  provider: 'anthropic',
243
264
  providerId: 'claude',
244
265
  model: 'claude-sonnet-4-6',
@@ -283,6 +304,7 @@ export function normalizeJudgeConfig(judge) {
283
304
  const timeoutMs = typeof judge.timeoutMs === 'number' && judge.timeoutMs > 0 ? judge.timeoutMs : base.timeoutMs;
284
305
  const endpoint = typeof judge.endpoint === 'string' && judge.endpoint.trim() ? judge.endpoint.trim() : null;
285
306
  return {
307
+ mode: judge.mode === 'off' ? 'off' : 'shadow',
286
308
  provider,
287
309
  providerId: rawProviderId,
288
310
  model,
@@ -320,6 +342,7 @@ export function normalizeJudgeConfig(judge) {
320
342
  endpoint = catalogSpec.defaultEndpoint;
321
343
  }
322
344
  const normalized = {
345
+ mode: judge.mode === 'off' ? 'off' : 'shadow',
323
346
  provider,
324
347
  providerId,
325
348
  model,
@@ -490,6 +513,19 @@ function normalizeV3Raw(raw) {
490
513
  },
491
514
  });
492
515
  }
516
+ function normalizeV5Raw(raw) {
517
+ const base = normalizeV3Raw({ ...raw, version: 4 });
518
+ return {
519
+ ...base,
520
+ version: 5,
521
+ capability: {
522
+ grantsEnabled: raw.capability?.grantsEnabled !== false,
523
+ boundaryDriver: raw.capability?.boundaryDriver ?? 'host-integration',
524
+ attestationRelPath: raw.capability?.attestationRelPath ?? '.belay/attestation.json',
525
+ ...(raw.capability ?? {}),
526
+ },
527
+ };
528
+ }
493
529
  export function migrateConfig(loaded) {
494
530
  if (typeof loaded !== 'object' || loaded === null) {
495
531
  return { ...DEFAULT_CONFIG_V4 };
@@ -498,6 +534,9 @@ export function migrateConfig(loaded) {
498
534
  if (raw.version === 4) {
499
535
  return normalizeV3Raw(raw);
500
536
  }
537
+ if (raw.version === 5) {
538
+ return normalizeV5Raw(raw);
539
+ }
501
540
  if (raw.version === 3 || (raw.version === undefined && hasV3Sections(raw))) {
502
541
  return normalizeV3Raw(raw);
503
542
  }
@@ -576,8 +615,9 @@ export function normalizeConfig(config) {
576
615
  return normalizeConfigV2(config);
577
616
  }
578
617
  const v4 = config;
579
- return {
580
- version: 4,
618
+ const version = v4.version === 5 ? 5 : 4;
619
+ const normalized = {
620
+ version,
581
621
  ...(v4.installScope === 'global' || v4.installScope === 'project'
582
622
  ? { installScope: v4.installScope }
583
623
  : {}),
@@ -743,6 +783,10 @@ export function normalizeConfig(config) {
743
783
  },
744
784
  judge: normalizeJudgeConfig(v4.judge ?? DEFAULT_JUDGE_LOCAL_OLLAMA),
745
785
  };
786
+ if (version === 5 || v4.capability) {
787
+ normalized.capability = normalizeCapabilityConfig(v4.capability ?? DEFAULT_CAPABILITY_V5);
788
+ }
789
+ return normalized;
746
790
  }
747
791
  export function isFreshConfigInput(loaded) {
748
792
  if (loaded === null || loaded === undefined) {
@@ -814,6 +858,15 @@ export function mergeConfig(existing, defaults = DEFAULT_CONFIG_V4) {
814
858
  ...migrated.audit,
815
859
  },
816
860
  ...(migrated.installScope ? { installScope: migrated.installScope } : {}),
861
+ ...(migrated.version === 5 || migrated.capability
862
+ ? {
863
+ version: migrated.version === 5 ? 5 : 4,
864
+ capability: normalizeCapabilityConfig({
865
+ ...DEFAULT_CAPABILITY_V5,
866
+ ...migrated.capability,
867
+ }),
868
+ }
869
+ : {}),
817
870
  });
818
871
  }
819
872
  export function scrubOptionsFromConfig(config) {
@@ -29,6 +29,9 @@ export interface GateVerdict extends GatePermissionResponse {
29
29
  wouldBlock: boolean;
30
30
  mode: 'enforce' | 'audit';
31
31
  axes?: ClassifyResult['axes'];
32
+ capabilityRequests?: ClassifyResult['capabilityRequests'];
33
+ authorizationDecision?: ClassifyResult['authorizationDecision'];
34
+ boundaryProfile?: string;
32
35
  }
33
36
  export declare function isGatedAction(value: unknown): value is GatedAction;
34
37
  export declare function classifyResultToGateVerdict(params: {
@@ -26,6 +26,9 @@ export function classifyResultToGateVerdict(params) {
26
26
  user_message,
27
27
  agent_message,
28
28
  axes: result.axes,
29
+ capabilityRequests: result.capabilityRequests,
30
+ authorizationDecision: result.authorizationDecision,
31
+ boundaryProfile: result.boundaryProfile,
29
32
  };
30
33
  }
31
34
  export function unnormalizedGateVerdict(params) {
@@ -1,4 +1,5 @@
1
1
  import { allPathsAllowlisted } from './capability/allowlist.js';
2
+ import { checkGatedActionLimits } from './capability/limits.js';
2
3
  import { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload, } from './capability/paths.js';
3
4
  import { classifySubagent } from './classify-subagent.js';
4
5
  import { classifyToolUse } from './classify-tool.js';
@@ -197,6 +198,10 @@ function applyToolSandboxPolicies(action, result, options) {
197
198
  return next;
198
199
  }
199
200
  export async function classifyGatedAction(action, config, extraOptions = {}) {
201
+ const limitResult = checkGatedActionLimits(action);
202
+ if (limitResult) {
203
+ return limitResult;
204
+ }
200
205
  const options = { ...classifierOptionsFromConfig(config), ...extraOptions };
201
206
  if (action.kind === 'shell') {
202
207
  const command = action.command ?? shellCommandFromPayload(action.payload ?? {});
@@ -221,7 +226,7 @@ export async function classifyGatedAction(action, config, extraOptions = {}) {
221
226
  };
222
227
  }
223
228
  if (action.kind === 'subagent') {
224
- return classifySubagent(action.payload ?? {}, action.repoRoot, options);
229
+ return classifySubagent(action.payload ?? {}, action.repoRoot, options, config);
225
230
  }
226
231
  let result = await classifyToolUse(action.payload ?? {}, action.repoRoot, action.cwd, config, options);
227
232
  result = applyToolSandboxPolicies(action, result, options);