@guilz-dev/belay 0.4.0 → 0.7.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 (240) hide show
  1. package/README.md +27 -2
  2. package/dist/adapters/claude/runtime-entry.js +54 -25
  3. package/dist/adapters/layouts/scope.js +3 -2
  4. package/dist/adapters/shared/gate-runtime.d.ts +8 -2
  5. package/dist/adapters/shared/gate-runtime.js +579 -181
  6. package/dist/bundle/claude-runtime.mjs +13574 -6334
  7. package/dist/bundle/codex-runtime.mjs +13108 -5903
  8. package/dist/bundle/cursor-runtime.mjs +12676 -5476
  9. package/dist/cli.js +80 -2
  10. package/dist/commands/approve.js +34 -10
  11. package/dist/commands/classify-for-report.js +1 -1
  12. package/dist/commands/config.d.ts +8 -0
  13. package/dist/commands/config.js +134 -11
  14. package/dist/commands/doctor.js +87 -1
  15. package/dist/commands/explain.js +11 -1
  16. package/dist/commands/judge.js +3 -2
  17. package/dist/commands/recovery-checkpoints.d.ts +164 -0
  18. package/dist/commands/recovery-checkpoints.js +347 -0
  19. package/dist/commands/revoke.js +19 -7
  20. package/dist/commands/session.d.ts +20 -0
  21. package/dist/commands/session.js +51 -0
  22. package/dist/commands/status.js +7 -0
  23. package/dist/config-io.js +91 -23
  24. package/dist/conformance/guarantee-posture.d.ts +21 -0
  25. package/dist/conformance/guarantee-posture.js +46 -0
  26. package/dist/conformance/guarantee-table.d.ts +1 -0
  27. package/dist/conformance/guarantee-table.js +20 -9
  28. package/dist/conformance/types.d.ts +1 -0
  29. package/dist/core/approval-replay.d.ts +1 -1
  30. package/dist/core/approval-replay.js +4 -1
  31. package/dist/core/approval-service.d.ts +6 -1
  32. package/dist/core/approval-service.js +87 -24
  33. package/dist/core/approval.d.ts +4 -0
  34. package/dist/core/approval.js +19 -5
  35. package/dist/core/audit-types.d.ts +13 -0
  36. package/dist/core/capability/approval-state-mutation.d.ts +32 -0
  37. package/dist/core/capability/approval-state-mutation.js +165 -0
  38. package/dist/core/capability/approval-v3.d.ts +32 -0
  39. package/dist/core/capability/approval-v3.js +114 -0
  40. package/dist/core/capability/attestation.d.ts +16 -0
  41. package/dist/core/capability/attestation.js +62 -0
  42. package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
  43. package/dist/core/capability/boundary-attestation-sign.js +84 -0
  44. package/dist/core/capability/boundary-driver-container.d.ts +19 -0
  45. package/dist/core/capability/boundary-driver-container.js +189 -0
  46. package/dist/core/capability/boundary-driver.d.ts +29 -0
  47. package/dist/core/capability/boundary-driver.js +44 -0
  48. package/dist/core/capability/boundary-egress.d.ts +21 -0
  49. package/dist/core/capability/boundary-egress.js +109 -0
  50. package/dist/core/capability/boundary-grant-materialize.d.ts +14 -0
  51. package/dist/core/capability/boundary-grant-materialize.js +93 -0
  52. package/dist/core/capability/boundary-profile.d.ts +15 -0
  53. package/dist/core/capability/boundary-profile.js +25 -0
  54. package/dist/core/capability/boundary-run.d.ts +48 -0
  55. package/dist/core/capability/boundary-run.js +51 -0
  56. package/dist/core/capability/boundary-session.d.ts +61 -0
  57. package/dist/core/capability/boundary-session.js +138 -0
  58. package/dist/core/capability/boundary-workspace-mount.d.ts +5 -0
  59. package/dist/core/capability/boundary-workspace-mount.js +68 -0
  60. package/dist/core/capability/capability-request-hash.d.ts +2 -0
  61. package/dist/core/capability/capability-request-hash.js +8 -0
  62. package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
  63. package/dist/core/capability/gate-latency-ratchet.js +18 -0
  64. package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
  65. package/dist/core/capability/gate-policy-shadow.js +130 -0
  66. package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
  67. package/dist/core/capability/gate-shadow-audit.js +1 -0
  68. package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
  69. package/dist/core/capability/gate-shadow-ratchet.js +81 -0
  70. package/dist/core/capability/grant-consumption.d.ts +4 -0
  71. package/dist/core/capability/grant-consumption.js +11 -0
  72. package/dist/core/capability/grant-lease.d.ts +44 -0
  73. package/dist/core/capability/grant-lease.js +243 -0
  74. package/dist/core/capability/grant-loader.d.ts +12 -0
  75. package/dist/core/capability/grant-loader.js +23 -0
  76. package/dist/core/capability/grant-match.d.ts +6 -0
  77. package/dist/core/capability/grant-match.js +96 -0
  78. package/dist/core/capability/grant.d.ts +16 -0
  79. package/dist/core/capability/grant.js +16 -0
  80. package/dist/core/capability/index.d.ts +8 -0
  81. package/dist/core/capability/index.js +7 -0
  82. package/dist/core/capability/limits.d.ts +7 -0
  83. package/dist/core/capability/limits.js +55 -0
  84. package/dist/core/capability/policy-bridge.d.ts +18 -0
  85. package/dist/core/capability/policy-bridge.js +51 -0
  86. package/dist/core/capability/policy-engine.d.ts +77 -0
  87. package/dist/core/capability/policy-engine.js +663 -0
  88. package/dist/core/capability/policy-types.d.ts +20 -0
  89. package/dist/core/capability/policy-types.js +1 -0
  90. package/dist/core/capability/request.d.ts +48 -0
  91. package/dist/core/capability/request.js +1 -0
  92. package/dist/core/capability/resolver.d.ts +4 -0
  93. package/dist/core/capability/resolver.js +4 -0
  94. package/dist/core/classify-subagent.d.ts +2 -1
  95. package/dist/core/classify-subagent.js +38 -2
  96. package/dist/core/classify-tool.js +126 -62
  97. package/dist/core/config.d.ts +34 -3
  98. package/dist/core/config.js +118 -3
  99. package/dist/core/effect-ir/audit.d.ts +9 -0
  100. package/dist/core/effect-ir/audit.js +86 -0
  101. package/dist/core/effect-ir/build.d.ts +31 -0
  102. package/dist/core/effect-ir/build.js +277 -0
  103. package/dist/core/effect-ir/index.d.ts +8 -0
  104. package/dist/core/effect-ir/index.js +7 -0
  105. package/dist/core/effect-ir/normalize.d.ts +4 -0
  106. package/dist/core/effect-ir/normalize.js +104 -0
  107. package/dist/core/effect-ir/package-exec-eval.d.ts +8 -0
  108. package/dist/core/effect-ir/package-exec-eval.js +152 -0
  109. package/dist/core/effect-ir/package-exec.d.ts +29 -0
  110. package/dist/core/effect-ir/package-exec.js +292 -0
  111. package/dist/core/effect-ir/policy.d.ts +13 -0
  112. package/dist/core/effect-ir/policy.js +77 -0
  113. package/dist/core/effect-ir/types.d.ts +55 -0
  114. package/dist/core/effect-ir/types.js +1 -0
  115. package/dist/core/egress/allowlist.d.ts +1 -0
  116. package/dist/core/egress/allowlist.js +8 -0
  117. package/dist/core/egress-approval.js +45 -51
  118. package/dist/core/gate-contract.d.ts +5 -0
  119. package/dist/core/gate-contract.js +5 -0
  120. package/dist/core/gate-engine.js +30 -8
  121. package/dist/core/index.d.ts +3 -1
  122. package/dist/core/index.js +2 -1
  123. package/dist/core/judge-cloud-consent.js +29 -15
  124. package/dist/core/judge-doctor.js +39 -14
  125. package/dist/core/judge-fallback-hints.d.ts +13 -0
  126. package/dist/core/judge-fallback-hints.js +108 -0
  127. package/dist/core/network-endpoint.d.ts +10 -0
  128. package/dist/core/network-endpoint.js +51 -0
  129. package/dist/core/path-utils.d.ts +1 -0
  130. package/dist/core/path-utils.js +5 -2
  131. package/dist/core/process-runner.d.ts +10 -0
  132. package/dist/core/process-runner.js +124 -0
  133. package/dist/core/recover-advice.js +2 -1
  134. package/dist/core/recovery/artifact-store.d.ts +29 -0
  135. package/dist/core/recovery/artifact-store.js +336 -0
  136. package/dist/core/recovery/capability.d.ts +3 -0
  137. package/dist/core/recovery/capability.js +22 -0
  138. package/dist/core/recovery/checkpoint.d.ts +25 -0
  139. package/dist/core/recovery/checkpoint.js +260 -0
  140. package/dist/core/recovery/fail-closed.d.ts +6 -0
  141. package/dist/core/recovery/fail-closed.js +25 -0
  142. package/dist/core/recovery/index.d.ts +5 -0
  143. package/dist/core/recovery/index.js +4 -0
  144. package/dist/core/recovery/operator-guidance.d.ts +7 -0
  145. package/dist/core/recovery/operator-guidance.js +39 -0
  146. package/dist/core/recovery/reconcile.d.ts +3 -0
  147. package/dist/core/recovery/reconcile.js +48 -0
  148. package/dist/core/recovery/resource-identity.d.ts +4 -0
  149. package/dist/core/recovery/resource-identity.js +34 -0
  150. package/dist/core/recovery/restore.d.ts +13 -0
  151. package/dist/core/recovery/restore.js +114 -0
  152. package/dist/core/recovery/snapshot-node.d.ts +27 -0
  153. package/dist/core/recovery/snapshot-node.js +261 -0
  154. package/dist/core/recovery/types.d.ts +104 -0
  155. package/dist/core/recovery/types.js +1 -0
  156. package/dist/core/standing-allow.js +3 -0
  157. package/dist/core/transactional/apply-observed-changes.d.ts +13 -0
  158. package/dist/core/transactional/apply-observed-changes.js +275 -0
  159. package/dist/core/transactional/backend-selector.d.ts +8 -0
  160. package/dist/core/transactional/backend-selector.js +118 -0
  161. package/dist/core/transactional/backend.d.ts +55 -0
  162. package/dist/core/transactional/backend.js +1 -0
  163. package/dist/core/transactional/diff-evaluator.js +4 -1
  164. package/dist/core/transactional/eligibility.js +4 -0
  165. package/dist/core/transactional/file-checkpoint-backend.d.ts +3 -0
  166. package/dist/core/transactional/file-checkpoint-backend.js +278 -0
  167. package/dist/core/transactional/file-checkpoint-git.d.ts +11 -0
  168. package/dist/core/transactional/file-checkpoint-git.js +217 -0
  169. package/dist/core/transactional/file-checkpoint-staging.d.ts +13 -0
  170. package/dist/core/transactional/file-checkpoint-staging.js +54 -0
  171. package/dist/core/transactional/file-clone.d.ts +15 -0
  172. package/dist/core/transactional/file-clone.js +139 -0
  173. package/dist/core/transactional/file-tree-path.d.ts +7 -0
  174. package/dist/core/transactional/file-tree-path.js +50 -0
  175. package/dist/core/transactional/file-tree.d.ts +41 -0
  176. package/dist/core/transactional/file-tree.js +186 -0
  177. package/dist/core/transactional/git-worktree-backend.d.ts +2 -0
  178. package/dist/core/transactional/git-worktree-backend.js +50 -0
  179. package/dist/core/transactional/git-worktree.d.ts +14 -9
  180. package/dist/core/transactional/git-worktree.js +62 -87
  181. package/dist/core/transactional/index.d.ts +11 -0
  182. package/dist/core/transactional/index.js +10 -0
  183. package/dist/core/transactional/reasons.d.ts +1 -0
  184. package/dist/core/transactional/reasons.js +5 -0
  185. package/dist/core/transactional/runner.js +171 -23
  186. package/dist/core/transactional/snapshot-node.d.ts +26 -0
  187. package/dist/core/transactional/snapshot-node.js +87 -0
  188. package/dist/core/transactional/types.d.ts +21 -1
  189. package/dist/core/types.d.ts +30 -3
  190. package/dist/core/verdict/adapter.d.ts +3 -4
  191. package/dist/core/verdict/adapter.js +24 -12
  192. package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
  193. package/dist/core/verdict/cursor-acp-client.js +345 -0
  194. package/dist/core/verdict/egress-classify.d.ts +2 -2
  195. package/dist/core/verdict/egress-classify.js +2 -2
  196. package/dist/core/verdict/git-classifier.d.ts +26 -0
  197. package/dist/core/verdict/git-classifier.js +533 -0
  198. package/dist/core/verdict/judge-audit.js +1 -0
  199. package/dist/core/verdict/judge-baseline.d.ts +1 -1
  200. package/dist/core/verdict/judge-baseline.js +1 -0
  201. package/dist/core/verdict/judge-broker-service.js +12 -0
  202. package/dist/core/verdict/judge-catalog.js +1 -0
  203. package/dist/core/verdict/judge-cli-fingerprint.d.ts +1 -0
  204. package/dist/core/verdict/judge-cli-fingerprint.js +6 -0
  205. package/dist/core/verdict/judge-cli.js +5 -3
  206. package/dist/core/verdict/judge-provider-matrix.js +1 -1
  207. package/dist/core/verdict/judge-runtime-config.d.ts +2 -0
  208. package/dist/core/verdict/judge-runtime-config.js +10 -6
  209. package/dist/core/verdict/judge-session-broker.d.ts +1 -1
  210. package/dist/core/verdict/judge-session-broker.js +10 -5
  211. package/dist/core/verdict/judge-transport.d.ts +5 -1
  212. package/dist/core/verdict/judge-transport.js +24 -14
  213. package/dist/core/verdict/judge.d.ts +2 -15
  214. package/dist/core/verdict/judge.js +5 -68
  215. package/dist/core/verdict/launcher-resolve.d.ts +2 -0
  216. package/dist/core/verdict/launcher-resolve.js +14 -16
  217. package/dist/core/verdict/prescan.d.ts +15 -0
  218. package/dist/core/verdict/prescan.js +68 -0
  219. package/dist/core/verdict/shell-policy.d.ts +26 -0
  220. package/dist/core/verdict/shell-policy.js +40 -0
  221. package/dist/core/verdict/shell-semantics.d.ts +24 -0
  222. package/dist/core/verdict/shell-semantics.js +225 -0
  223. package/dist/core/verdict/types.d.ts +18 -1
  224. package/dist/core/verdict/verdict.js +313 -178
  225. package/dist/corpus/evaluate.js +3 -7
  226. package/dist/corpus/gate-latency-budget.d.ts +39 -0
  227. package/dist/corpus/gate-latency-budget.js +33 -0
  228. package/dist/corpus/mutators.d.ts +1 -1
  229. package/dist/corpus/mutators.js +11 -0
  230. package/dist/corpus/runtime-match.js +1 -3
  231. package/dist/corpus/standing-allow-catalog.generated.js +29 -9
  232. package/dist/judge-broker-daemon.js +15 -1
  233. package/dist/services/sandbox-service.d.ts +4 -0
  234. package/dist/services/sandbox-service.js +28 -2
  235. package/dist/types.d.ts +13 -2
  236. package/dist/version.d.ts +1 -1
  237. package/dist/version.js +1 -1
  238. package/package.json +1 -1
  239. package/skills/belay/SKILL.md +8 -3
  240. package/skills/belay/belay-approve.md +10 -7
@@ -0,0 +1,663 @@
1
+ import { createHash } from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { matchesSensitivePath } from '../glob.js';
4
+ import { parseNetworkEndpoint } from '../network-endpoint.js';
5
+ import { canonicalPath, pathWithinRoot, resolveWorkspaceRootMatch } from '../path-utils.js';
6
+ import { tokenizeShell } from '../shell-tokenizer.js';
7
+ import { BOUNDARY_GRANT_ISSUER_CONTAINER, isPathWithinBoundaryMount, materializeContainerBoundaryGrant, } from './boundary-grant-materialize.js';
8
+ import { boundaryVerifiedAllowEnabled } from './boundary-profile.js';
9
+ import { isGrantScopeTooBroad } from './grant.js';
10
+ import { broadGrantTargetsRequest, grantMatchesRequest } from './grant-match.js';
11
+ import { CAPABILITY_REQUEST_VERSION, } from './request.js';
12
+ function enrichAuthWithMaterializedGrants(request, config, auth) {
13
+ if (!auth?.attestation || !boundaryVerifiedAllowEnabled(auth.attestation)) {
14
+ return auth;
15
+ }
16
+ const materialized = materializeContainerBoundaryGrant(request, {
17
+ attestation: auth.attestation,
18
+ mountRoot: request.context.cwd,
19
+ egressProxyActive: auth.egressProxyActive === true,
20
+ existingGrants: auth.grants,
21
+ sensitivePaths: auth.sensitivePaths ?? config.classifier.sensitivePaths,
22
+ });
23
+ if (!materialized) {
24
+ return auth;
25
+ }
26
+ return { ...auth, grants: [...(auth.grants ?? []), materialized] };
27
+ }
28
+ function buildAuthorizationContext(config, trustedWorkspaceRoots, auth) {
29
+ return {
30
+ config,
31
+ grants: auth?.grants,
32
+ attestation: auth?.attestation ?? null,
33
+ trustedWorkspaceRoots,
34
+ };
35
+ }
36
+ export function buildSubagentCapabilityRequest(analysis) {
37
+ return {
38
+ version: CAPABILITY_REQUEST_VERSION,
39
+ principal: {
40
+ adapter: analysis.adapter,
41
+ repoRoot: analysis.repoRoot,
42
+ sessionHash: hashSession(`${analysis.repoRoot}:${analysis.cwd}`),
43
+ },
44
+ action: 'process.exec',
45
+ resource: { kind: 'executable', command: analysis.subagentType },
46
+ context: {
47
+ cwd: analysis.cwd,
48
+ inputFingerprint: analysis.inputFingerprint,
49
+ hookKind: 'subagent',
50
+ analysisBasis: [`subagent:${analysis.subagentType}`],
51
+ },
52
+ evidence: {
53
+ level: 'possible',
54
+ signals: [...analysis.signals],
55
+ },
56
+ };
57
+ }
58
+ export function evaluateSubagentPolicy(analysis, config, auth) {
59
+ const request = buildSubagentCapabilityRequest(analysis);
60
+ const enriched = enrichAuthWithMaterializedGrants(request, config, auth);
61
+ const decision = getDefaultPolicyEngine().evaluate(request, buildAuthorizationContext(config, undefined, enriched));
62
+ return { request, decision };
63
+ }
64
+ function hashSession(value) {
65
+ return createHash('sha256').update(value).digest('hex').slice(0, 16);
66
+ }
67
+ function resolveCapabilityPath(targetPath, cwd) {
68
+ const joined = path.isAbsolute(targetPath) ? targetPath : path.join(cwd, targetPath);
69
+ return canonicalPath(joined);
70
+ }
71
+ function evidenceLevelForOpacity(opacity) {
72
+ if (opacity === 'transparent' || opacity === 'recursive') {
73
+ return 'certain';
74
+ }
75
+ if (opacity === 'opaque') {
76
+ return 'possible';
77
+ }
78
+ return 'indeterminate';
79
+ }
80
+ function isGitRefWrite(analysis) {
81
+ if (analysis.segmentHead !== 'git') {
82
+ return false;
83
+ }
84
+ return analysis.signals.includes('git.push');
85
+ }
86
+ function actionForShellAnalysis(analysis) {
87
+ if (isGitRefWrite(analysis)) {
88
+ return 'git.ref.write';
89
+ }
90
+ if (analysis.effect === 'remote_mutation' ||
91
+ analysis.egressClass === 'ambiguous' ||
92
+ analysis.egressClass === 'read' ||
93
+ analysis.egressClass === 'destructive') {
94
+ return 'network.connect';
95
+ }
96
+ if (analysis.opacity === 'unparseable' || analysis.effect === 'unknown') {
97
+ return 'indeterminate';
98
+ }
99
+ if (analysis.effect === 'read_only') {
100
+ return 'fs.read';
101
+ }
102
+ if (analysis.location === 'external') {
103
+ return 'network.connect';
104
+ }
105
+ if (analysis.effect === 'local_mutation') {
106
+ return 'fs.write';
107
+ }
108
+ return 'indeterminate';
109
+ }
110
+ function resourcePathForShellAnalysis(analysis) {
111
+ const targets = analysis.resolvedPathTargets && analysis.resolvedPathTargets.length > 0
112
+ ? analysis.resolvedPathTargets
113
+ : analysis.pathArgs;
114
+ if (targets.length === 0) {
115
+ return null;
116
+ }
117
+ const repoRoot = canonicalPath(analysis.repoRoot);
118
+ const resolved = targets.map((target) => analysis.resolvedPathTargets?.length
119
+ ? canonicalPath(target)
120
+ : resolveCapabilityPath(target, analysis.cwd));
121
+ const [first] = resolved;
122
+ if (!first) {
123
+ return null;
124
+ }
125
+ if (analysis.effect === 'local_mutation') {
126
+ const outside = resolved.find((candidate) => !pathWithinRoot(repoRoot, candidate));
127
+ return outside ?? resolved.at(-1) ?? first;
128
+ }
129
+ return first;
130
+ }
131
+ function resourceForShellAnalysis(analysis) {
132
+ if (isGitRefWrite(analysis)) {
133
+ return { kind: 'git-ref', ref: 'push' };
134
+ }
135
+ if (isNetworkShellAnalysis(analysis)) {
136
+ const exact = exactNetworkResources(analysis);
137
+ return ((exact.length === 1 ? exact[0] : undefined) ?? {
138
+ kind: 'network',
139
+ host: '*',
140
+ protocol: 'unknown',
141
+ });
142
+ }
143
+ const resourcePath = resourcePathForShellAnalysis(analysis);
144
+ if (resourcePath) {
145
+ return { kind: 'path', path: resourcePath };
146
+ }
147
+ return { kind: 'executable', command: analysis.segmentHead };
148
+ }
149
+ function isNetworkShellAnalysis(analysis) {
150
+ return (analysis.egressClass === 'ambiguous' ||
151
+ analysis.egressClass === 'read' ||
152
+ analysis.egressClass === 'destructive' ||
153
+ analysis.location === 'external');
154
+ }
155
+ const SCP_STYLE_COMMANDS = new Set(['git', 'rsync', 'scp']);
156
+ const PACKAGE_SPEC_COMMANDS = new Set(['bun', 'npm', 'npx', 'pnpm', 'yarn']);
157
+ const NETWORK_VALUE_OPTIONS = new Set(['--registry', '--url']);
158
+ const GIT_OPTIONS_WITH_VALUE = new Set([
159
+ '-C',
160
+ '-b',
161
+ '-c',
162
+ '-o',
163
+ '--branch',
164
+ '--config',
165
+ '--depth',
166
+ '--filter',
167
+ '--git-dir',
168
+ '--namespace',
169
+ '--origin',
170
+ '--reference',
171
+ '--separate-git-dir',
172
+ '--upload-pack',
173
+ '--work-tree',
174
+ ]);
175
+ function gitRemoteOperand(tokens) {
176
+ let cursor = 1;
177
+ while (cursor < tokens.length) {
178
+ const token = tokens[cursor];
179
+ if (!token) {
180
+ return null;
181
+ }
182
+ if (GIT_OPTIONS_WITH_VALUE.has(token)) {
183
+ cursor += 2;
184
+ continue;
185
+ }
186
+ if (token.startsWith('-')) {
187
+ cursor += 1;
188
+ continue;
189
+ }
190
+ break;
191
+ }
192
+ const subcommand = tokens[cursor];
193
+ if (!subcommand) {
194
+ return null;
195
+ }
196
+ const positionals = [];
197
+ let skipOptionValue = false;
198
+ for (const token of tokens.slice(cursor + 1)) {
199
+ if (skipOptionValue) {
200
+ skipOptionValue = false;
201
+ continue;
202
+ }
203
+ if (GIT_OPTIONS_WITH_VALUE.has(token)) {
204
+ skipOptionValue = true;
205
+ continue;
206
+ }
207
+ if (token.startsWith('-')) {
208
+ continue;
209
+ }
210
+ positionals.push(token);
211
+ }
212
+ if (['clone', 'fetch', 'ls-remote', 'pull', 'push'].includes(subcommand)) {
213
+ return positionals[0] ?? null;
214
+ }
215
+ if (subcommand === 'remote' && positionals[0] === 'add') {
216
+ return positionals[2] ?? null;
217
+ }
218
+ if (subcommand === 'submodule' && positionals[0] === 'add') {
219
+ return positionals[1] ?? null;
220
+ }
221
+ return null;
222
+ }
223
+ function exactNetworkResources(analysis) {
224
+ const resources = new Map();
225
+ const shellTokens = tokenizeShell(analysis.command);
226
+ const gitRemote = analysis.segmentHead === 'git' ? gitRemoteOperand(shellTokens) : null;
227
+ for (const rawToken of shellTokens) {
228
+ const optionSeparator = rawToken.startsWith('-') ? rawToken.indexOf('=') : -1;
229
+ const optionName = optionSeparator === -1 ? null : rawToken.slice(0, optionSeparator);
230
+ const token = optionSeparator !== -1 && optionName && NETWORK_VALUE_OPTIONS.has(optionName)
231
+ ? rawToken.slice(optionSeparator + 1)
232
+ : rawToken;
233
+ const endpoint = parseNetworkEndpoint(token, {
234
+ allowHostedGitShorthand: PACKAGE_SPEC_COMMANDS.has(analysis.segmentHead),
235
+ allowScpStyle: SCP_STYLE_COMMANDS.has(analysis.segmentHead) &&
236
+ (analysis.segmentHead !== 'git' || rawToken === gitRemote),
237
+ });
238
+ if (!endpoint) {
239
+ continue;
240
+ }
241
+ const resource = { kind: 'network', ...endpoint };
242
+ resources.set(`${resource.host}:${resource.port ?? ''}:${resource.protocol}`, resource);
243
+ }
244
+ return [...resources.values()];
245
+ }
246
+ export function buildShellCapabilityRequest(analysis) {
247
+ const [request] = buildShellCapabilityRequests(analysis);
248
+ if (!request) {
249
+ throw new Error('shell analysis produced no capability request');
250
+ }
251
+ return request;
252
+ }
253
+ export function buildShellCapabilityRequests(analysis) {
254
+ const resources = isNetworkShellAnalysis(analysis) ? exactNetworkResources(analysis) : [];
255
+ const selectedResources = resources.length > 0 ? resources : [resourceForShellAnalysis(analysis)];
256
+ return selectedResources.map((resource) => ({
257
+ version: CAPABILITY_REQUEST_VERSION,
258
+ principal: {
259
+ adapter: analysis.adapter,
260
+ repoRoot: analysis.repoRoot,
261
+ sessionHash: hashSession(`${analysis.repoRoot}:${analysis.cwd}`),
262
+ },
263
+ action: actionForShellAnalysis(analysis),
264
+ resource,
265
+ context: {
266
+ cwd: analysis.cwd,
267
+ inputFingerprint: analysis.inputFingerprint,
268
+ hookKind: analysis.hookKind,
269
+ analysisBasis: [
270
+ `segment:${analysis.segmentHead}`,
271
+ `effect:${analysis.effect}`,
272
+ `location:${analysis.location}`,
273
+ `opacity:${analysis.opacity}`,
274
+ ],
275
+ },
276
+ evidence: {
277
+ level: evidenceLevelForOpacity(analysis.opacity),
278
+ signals: [...analysis.signals],
279
+ },
280
+ }));
281
+ }
282
+ function actionForFileMutation(analysis) {
283
+ if (analysis.locationLabel === 'control_plane') {
284
+ return 'control_plane.write';
285
+ }
286
+ if (analysis.locationLabel === 'sensitive_path') {
287
+ return 'fs.write';
288
+ }
289
+ if (analysis.isDelete) {
290
+ return 'fs.write';
291
+ }
292
+ return 'fs.write';
293
+ }
294
+ export function buildFileMutationCapabilityRequest(analysis) {
295
+ return {
296
+ version: CAPABILITY_REQUEST_VERSION,
297
+ principal: {
298
+ adapter: analysis.adapter,
299
+ repoRoot: analysis.repoRoot,
300
+ sessionHash: hashSession(`${analysis.repoRoot}:${analysis.cwd}`),
301
+ },
302
+ action: actionForFileMutation(analysis),
303
+ resource: { kind: 'path', path: analysis.resolvedPath },
304
+ context: {
305
+ cwd: analysis.cwd,
306
+ inputFingerprint: analysis.inputFingerprint,
307
+ hookKind: analysis.hookKind,
308
+ analysisBasis: [
309
+ `tool:${analysis.toolKind}`,
310
+ `location:${analysis.locationLabel}`,
311
+ analysis.isDelete ? 'delete' : 'write',
312
+ ],
313
+ },
314
+ evidence: {
315
+ level: analysis.locationLabel === 'repo_local' ? 'certain' : 'possible',
316
+ signals: [...analysis.signals],
317
+ },
318
+ };
319
+ }
320
+ function findFreshGrant(request, grants) {
321
+ if (!grants?.length) {
322
+ return null;
323
+ }
324
+ const now = Date.now();
325
+ for (const grant of grants) {
326
+ const expires = Date.parse(grant.expiresAt);
327
+ if (Number.isFinite(expires) && expires > now && grantMatchesRequest(grant, request)) {
328
+ return grant;
329
+ }
330
+ }
331
+ return null;
332
+ }
333
+ function hasFreshGrant(request, grants) {
334
+ return findFreshGrant(request, grants) !== null;
335
+ }
336
+ function grantDecisionSignals(request, grant) {
337
+ const signals = [...request.evidence.signals, 'capability_grant'];
338
+ if (grant.issuer === BOUNDARY_GRANT_ISSUER_CONTAINER) {
339
+ signals.push('boundary_materialized_grant');
340
+ }
341
+ return signals;
342
+ }
343
+ function shellLocationFromRequest(request) {
344
+ const entry = request.context.analysisBasis.find((basis) => basis.startsWith('location:'));
345
+ return entry?.slice('location:'.length) ?? null;
346
+ }
347
+ function isRepoLocalShellLocation(request) {
348
+ return shellLocationFromRequest(request) === 'repo_local';
349
+ }
350
+ function isRepoLocalPackageExec(request) {
351
+ if (request.action !== 'process.exec' || request.resource.kind !== 'executable') {
352
+ return false;
353
+ }
354
+ if (!request.evidence.signals.includes('package_exec.local_bin_resolved')) {
355
+ return false;
356
+ }
357
+ const commandPath = canonicalPath(request.resource.command);
358
+ return path.isAbsolute(commandPath) && pathWithinRoot(request.principal.repoRoot, commandPath);
359
+ }
360
+ function isRepoLocalRoutineWrite(request, sensitivePaths) {
361
+ if (!isRepoLocalShellLocation(request)) {
362
+ return false;
363
+ }
364
+ if (request.action !== 'fs.write' && request.action !== 'fs.read') {
365
+ return false;
366
+ }
367
+ if (request.resource.kind !== 'path') {
368
+ return false;
369
+ }
370
+ const resolved = resolveCapabilityPath(request.resource.path, request.context.cwd);
371
+ const repoRoot = canonicalPath(request.principal.repoRoot);
372
+ if (!pathWithinRoot(repoRoot, resolved)) {
373
+ return false;
374
+ }
375
+ const relative = resolved.slice(repoRoot.length).replace(/^[/\\]/, '');
376
+ return !matchesSensitivePath(relative, sensitivePaths);
377
+ }
378
+ function isTrustedWorkspaceWrite(request, trustedWorkspaceRoots) {
379
+ if (!trustedWorkspaceRoots?.length || request.resource.kind !== 'path') {
380
+ return false;
381
+ }
382
+ const match = resolveWorkspaceRootMatch(request.principal.repoRoot, trustedWorkspaceRoots, request.resource.path);
383
+ return match?.kind === 'trusted';
384
+ }
385
+ /**
386
+ * boundary.verified widens only when builtin already allows, or a boundary-materialized
387
+ * grant is present for an approved high-risk action (network via egress chokepoint).
388
+ */
389
+ function verifiedBoundaryAllows(request, context) {
390
+ if (!boundaryVerifiedAllowEnabled(context.attestation)) {
391
+ return false;
392
+ }
393
+ if (request.evidence.level !== 'certain') {
394
+ return false;
395
+ }
396
+ const builtin = builtInRule(request, context);
397
+ if (builtin?.outcome === 'allow') {
398
+ return isPathWithinBoundaryMount(request);
399
+ }
400
+ if (builtin?.outcome === 'deny') {
401
+ return false;
402
+ }
403
+ return hasFreshGrant(request, context.grants?.filter((grant) => grant.issuer === BOUNDARY_GRANT_ISSUER_CONTAINER));
404
+ }
405
+ function builtInRule(request, context) {
406
+ const trustedWorkspaceRoots = context.trustedWorkspaceRoots;
407
+ const sensitivePaths = context.config.classifier.sensitivePaths;
408
+ if (request.action === 'control_plane.write') {
409
+ return {
410
+ outcome: 'require_approval',
411
+ reason: 'control_plane_mutation',
412
+ signals: [...request.evidence.signals, 'control_plane_path'],
413
+ matchedRule: 'builtin.control_plane',
414
+ };
415
+ }
416
+ if (request.action === 'process.exec' && request.context.hookKind === 'subagent') {
417
+ return {
418
+ outcome: 'allow',
419
+ reason: 'subagent_review',
420
+ signals: [...request.evidence.signals],
421
+ matchedRule: 'builtin.subagent',
422
+ };
423
+ }
424
+ if (isRepoLocalPackageExec(request)) {
425
+ return {
426
+ outcome: 'allow',
427
+ reason: 'read_only',
428
+ signals: [...request.evidence.signals, 'repo_local_exec'],
429
+ matchedRule: 'builtin.repo_local_exec',
430
+ };
431
+ }
432
+ if (request.action === 'secret.read' || request.action === 'git.ref.write') {
433
+ return {
434
+ outcome: 'require_approval',
435
+ reason: 'high_stakes_path',
436
+ signals: [...request.evidence.signals, 'secret_path'],
437
+ matchedRule: 'builtin.secret',
438
+ };
439
+ }
440
+ if (request.action === 'network.connect') {
441
+ return {
442
+ outcome: 'require_approval',
443
+ reason: 'external_effect',
444
+ signals: [...request.evidence.signals, 'network_connect'],
445
+ matchedRule: 'builtin.network',
446
+ };
447
+ }
448
+ if (request.action === 'indeterminate' || request.evidence.level === 'indeterminate') {
449
+ return {
450
+ outcome: 'require_approval',
451
+ reason: 'indeterminate_effect',
452
+ signals: [...request.evidence.signals, 'indeterminate'],
453
+ matchedRule: 'builtin.indeterminate',
454
+ };
455
+ }
456
+ if (request.resource.kind === 'path' && sensitivePaths?.length) {
457
+ const repoRoot = canonicalPath(request.principal.repoRoot);
458
+ const resolved = resolveCapabilityPath(request.resource.path, request.context.cwd);
459
+ if (pathWithinRoot(repoRoot, resolved)) {
460
+ const relative = resolved.slice(repoRoot.length).replace(/^[/\\]/, '');
461
+ if (matchesSensitivePath(relative, sensitivePaths)) {
462
+ return {
463
+ outcome: 'require_approval',
464
+ reason: 'high_stakes_path',
465
+ signals: [...request.evidence.signals, 'sensitive_path'],
466
+ matchedRule: 'builtin.sensitive_path',
467
+ };
468
+ }
469
+ }
470
+ }
471
+ if (request.resource.kind === 'path') {
472
+ const repoRoot = canonicalPath(request.principal.repoRoot);
473
+ const resolved = resolveCapabilityPath(request.resource.path, request.context.cwd);
474
+ const insideRepo = pathWithinRoot(repoRoot, resolved);
475
+ if (!insideRepo && !isTrustedWorkspaceWrite(request, trustedWorkspaceRoots)) {
476
+ return {
477
+ outcome: 'require_approval',
478
+ reason: 'outside_repo_mutation',
479
+ signals: [...request.evidence.signals, 'outside_repo_path'],
480
+ matchedRule: 'builtin.outside_repo',
481
+ };
482
+ }
483
+ }
484
+ if (request.action === 'fs.read' || request.action === 'fs.write') {
485
+ if (request.action === 'fs.read' &&
486
+ request.resource.kind === 'executable' &&
487
+ isRepoLocalShellLocation(request)) {
488
+ return {
489
+ outcome: 'allow',
490
+ reason: 'read_only',
491
+ signals: [...request.evidence.signals],
492
+ matchedRule: 'builtin.repo_local',
493
+ };
494
+ }
495
+ if (isRepoLocalRoutineWrite(request, sensitivePaths)) {
496
+ return {
497
+ outcome: 'allow',
498
+ reason: request.action === 'fs.read' ? 'read_only' : 'repo_local_mutation',
499
+ signals: [...request.evidence.signals],
500
+ matchedRule: 'builtin.repo_local',
501
+ };
502
+ }
503
+ if (isTrustedWorkspaceWrite(request, trustedWorkspaceRoots)) {
504
+ return {
505
+ outcome: 'allow',
506
+ reason: 'trusted_workspace_root',
507
+ signals: [...request.evidence.signals, 'trusted_workspace_root'],
508
+ matchedRule: 'builtin.trusted_workspace',
509
+ };
510
+ }
511
+ }
512
+ if (request.evidence.signals.includes('unparseable_shell')) {
513
+ const deny = context.config.policy.unparseableShell === 'deny';
514
+ return {
515
+ outcome: deny ? 'require_approval' : 'allow',
516
+ reason: 'unparseable_shell',
517
+ signals: [...request.evidence.signals],
518
+ matchedRule: deny ? 'builtin.unparseable' : 'builtin.unparseable_allow_flagged',
519
+ };
520
+ }
521
+ if (request.evidence.signals.includes('opaque_execution')) {
522
+ return {
523
+ outcome: 'require_approval',
524
+ reason: 'opaque_execution',
525
+ signals: [...request.evidence.signals],
526
+ matchedRule: 'builtin.opaque',
527
+ };
528
+ }
529
+ return null;
530
+ }
531
+ export function createTypeScriptPolicyEngine() {
532
+ return {
533
+ evaluate(request, context) {
534
+ if (request.evidence.signals.includes('forged_grant')) {
535
+ return {
536
+ outcome: 'deny',
537
+ reason: 'grant_forgery',
538
+ signals: [...request.evidence.signals, 'grant_forgery'],
539
+ matchedRule: 'forbid.grant_forgery',
540
+ };
541
+ }
542
+ const grants = context.grants;
543
+ if (grants?.some((grant) => isGrantScopeTooBroad(grant) && broadGrantTargetsRequest(grant, request))) {
544
+ return {
545
+ outcome: 'deny',
546
+ reason: 'grant_scope_too_broad',
547
+ signals: [...request.evidence.signals, 'grant_scope_too_broad'],
548
+ matchedRule: 'forbid.broad_grant',
549
+ };
550
+ }
551
+ const matchedGrant = findFreshGrant(request, grants);
552
+ if (matchedGrant) {
553
+ return {
554
+ outcome: 'allow',
555
+ reason: 'capability_grant',
556
+ signals: grantDecisionSignals(request, matchedGrant),
557
+ matchedRule: 'grant.exact',
558
+ };
559
+ }
560
+ if (verifiedBoundaryAllows(request, context)) {
561
+ return {
562
+ outcome: 'allow',
563
+ reason: 'verified_boundary',
564
+ signals: [...request.evidence.signals, 'verified_boundary'],
565
+ matchedRule: 'boundary.verified',
566
+ };
567
+ }
568
+ const builtin = builtInRule(request, context);
569
+ if (builtin) {
570
+ return builtin;
571
+ }
572
+ return {
573
+ outcome: 'require_approval',
574
+ reason: 'policy_default',
575
+ signals: [...request.evidence.signals, 'default_deny'],
576
+ matchedRule: 'default.require_approval',
577
+ };
578
+ },
579
+ };
580
+ }
581
+ let defaultPolicyEngine = null;
582
+ export function getDefaultPolicyEngine() {
583
+ if (!defaultPolicyEngine) {
584
+ defaultPolicyEngine = createTypeScriptPolicyEngine();
585
+ }
586
+ return defaultPolicyEngine;
587
+ }
588
+ export function policyDecisionRequiresAsk(decision) {
589
+ return decision.outcome === 'require_approval' || decision.outcome === 'deny';
590
+ }
591
+ function policyOutcomeRank(outcome) {
592
+ switch (outcome) {
593
+ case 'deny':
594
+ return 3;
595
+ case 'require_approval':
596
+ return 2;
597
+ case 'allow':
598
+ return 1;
599
+ default:
600
+ return 0;
601
+ }
602
+ }
603
+ export function combinePolicyDecisions(decisions) {
604
+ if (!decisions.length) {
605
+ return {
606
+ outcome: 'allow',
607
+ reason: 'read_only',
608
+ signals: [],
609
+ matchedRule: 'plan.conjunction',
610
+ };
611
+ }
612
+ const sorted = [...decisions].sort((left, right) => policyOutcomeRank(right.outcome) - policyOutcomeRank(left.outcome));
613
+ const worst = sorted[0];
614
+ if (!worst) {
615
+ return {
616
+ outcome: 'allow',
617
+ reason: 'read_only',
618
+ signals: [],
619
+ matchedRule: 'plan.conjunction',
620
+ };
621
+ }
622
+ return worst;
623
+ }
624
+ export function evaluateCapabilityRequestsPolicy(requests, config, auth, trustedWorkspaceRoots) {
625
+ let workingAuth = auth;
626
+ const decisions = [];
627
+ for (const request of requests) {
628
+ const enriched = enrichAuthWithMaterializedGrants(request, config, workingAuth);
629
+ const decision = getDefaultPolicyEngine().evaluate(request, buildAuthorizationContext(config, trustedWorkspaceRoots, enriched));
630
+ decisions.push(decision);
631
+ const previousCount = workingAuth?.grants?.length ?? 0;
632
+ const nextCount = enriched?.grants?.length ?? 0;
633
+ if (nextCount > previousCount) {
634
+ workingAuth = enriched;
635
+ }
636
+ }
637
+ return {
638
+ decisions,
639
+ decision: combinePolicyDecisions(decisions),
640
+ };
641
+ }
642
+ export function evaluateShellPolicy(analysis, config, auth) {
643
+ const requests = buildShellCapabilityRequests(analysis);
644
+ const enrichedAuth = {
645
+ ...auth,
646
+ sensitivePaths: auth?.sensitivePaths ?? analysis.sensitivePaths,
647
+ };
648
+ const { decision } = evaluateCapabilityRequestsPolicy(requests, config, enrichedAuth, analysis.trustedWorkspaceRoots);
649
+ const [request] = requests;
650
+ if (!request) {
651
+ throw new Error('shell analysis produced no capability request');
652
+ }
653
+ return { request, requests, decision };
654
+ }
655
+ export function evaluateFileMutationPolicy(analysis, config, auth) {
656
+ const request = buildFileMutationCapabilityRequest(analysis);
657
+ const enriched = enrichAuthWithMaterializedGrants(request, config, {
658
+ ...auth,
659
+ sensitivePaths: auth?.sensitivePaths ?? analysis.sensitivePaths,
660
+ });
661
+ const decision = getDefaultPolicyEngine().evaluate(request, buildAuthorizationContext(config, analysis.trustedWorkspaceRoots, enriched));
662
+ return { request, decision };
663
+ }
@@ -0,0 +1,20 @@
1
+ import type { BelayConfigV4 } from '../config.js';
2
+ import type { BoundaryAttestation } from './attestation.js';
3
+ import type { CapabilityGrantV1 } from './grant.js';
4
+ import type { CapabilityRequestV1 } from './request.js';
5
+ export type PolicyOutcome = 'allow' | 'require_approval' | 'deny';
6
+ export interface PolicyDecision {
7
+ outcome: PolicyOutcome;
8
+ reason: string;
9
+ signals: string[];
10
+ matchedRule?: string;
11
+ }
12
+ export interface AuthorizationContext {
13
+ config: BelayConfigV4;
14
+ grants?: CapabilityGrantV1[];
15
+ attestation?: BoundaryAttestation | null;
16
+ trustedWorkspaceRoots?: string[];
17
+ }
18
+ export interface PolicyEngine {
19
+ evaluate(request: CapabilityRequestV1, context: AuthorizationContext): PolicyDecision;
20
+ }
@@ -0,0 +1 @@
1
+ export {};