@guilz-dev/belay 0.6.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 (143) hide show
  1. package/README.md +20 -1
  2. package/dist/adapters/claude/runtime-entry.js +54 -25
  3. package/dist/adapters/shared/gate-runtime.d.ts +7 -0
  4. package/dist/adapters/shared/gate-runtime.js +420 -220
  5. package/dist/bundle/claude-runtime.mjs +13215 -8864
  6. package/dist/bundle/codex-runtime.mjs +10771 -6453
  7. package/dist/bundle/cursor-runtime.mjs +10797 -6484
  8. package/dist/cli.js +37 -1
  9. package/dist/commands/approve.js +34 -10
  10. package/dist/commands/doctor.js +51 -0
  11. package/dist/commands/recovery-checkpoints.d.ts +164 -0
  12. package/dist/commands/recovery-checkpoints.js +347 -0
  13. package/dist/commands/revoke.js +19 -7
  14. package/dist/commands/status.js +7 -0
  15. package/dist/config-io.js +49 -18
  16. package/dist/conformance/guarantee-table.js +6 -0
  17. package/dist/conformance/types.d.ts +1 -0
  18. package/dist/core/approval-replay.d.ts +1 -1
  19. package/dist/core/approval-replay.js +4 -1
  20. package/dist/core/approval-service.d.ts +6 -1
  21. package/dist/core/approval-service.js +86 -27
  22. package/dist/core/approval.d.ts +2 -0
  23. package/dist/core/approval.js +6 -3
  24. package/dist/core/audit-types.d.ts +13 -0
  25. package/dist/core/capability/approval-state-mutation.d.ts +19 -0
  26. package/dist/core/capability/approval-state-mutation.js +76 -3
  27. package/dist/core/capability/approval-v3.d.ts +18 -1
  28. package/dist/core/capability/approval-v3.js +63 -23
  29. package/dist/core/capability/attestation.d.ts +3 -0
  30. package/dist/core/capability/attestation.js +7 -0
  31. package/dist/core/capability/boundary-driver-container.d.ts +12 -0
  32. package/dist/core/capability/boundary-driver-container.js +101 -34
  33. package/dist/core/capability/boundary-driver.js +1 -0
  34. package/dist/core/capability/boundary-grant-materialize.d.ts +1 -0
  35. package/dist/core/capability/boundary-grant-materialize.js +15 -0
  36. package/dist/core/capability/boundary-run.d.ts +25 -0
  37. package/dist/core/capability/boundary-run.js +37 -9
  38. package/dist/core/capability/boundary-session.d.ts +4 -0
  39. package/dist/core/capability/boundary-session.js +9 -0
  40. package/dist/core/capability/boundary-workspace-mount.d.ts +5 -0
  41. package/dist/core/capability/boundary-workspace-mount.js +68 -0
  42. package/dist/core/capability/gate-policy-shadow.js +23 -1
  43. package/dist/core/capability/grant-consumption.d.ts +4 -0
  44. package/dist/core/capability/grant-consumption.js +11 -0
  45. package/dist/core/capability/grant-lease.d.ts +36 -1
  46. package/dist/core/capability/grant-lease.js +213 -16
  47. package/dist/core/capability/grant-match.js +3 -0
  48. package/dist/core/capability/index.d.ts +1 -1
  49. package/dist/core/capability/index.js +1 -1
  50. package/dist/core/capability/policy-engine.d.ts +7 -0
  51. package/dist/core/capability/policy-engine.js +202 -15
  52. package/dist/core/capability/request.d.ts +3 -0
  53. package/dist/core/config.d.ts +20 -0
  54. package/dist/core/config.js +62 -0
  55. package/dist/core/effect-ir/audit.d.ts +9 -0
  56. package/dist/core/effect-ir/audit.js +86 -0
  57. package/dist/core/effect-ir/build.d.ts +31 -0
  58. package/dist/core/effect-ir/build.js +277 -0
  59. package/dist/core/effect-ir/index.d.ts +8 -0
  60. package/dist/core/effect-ir/index.js +7 -0
  61. package/dist/core/effect-ir/normalize.d.ts +4 -0
  62. package/dist/core/effect-ir/normalize.js +104 -0
  63. package/dist/core/effect-ir/package-exec-eval.d.ts +8 -0
  64. package/dist/core/effect-ir/package-exec-eval.js +152 -0
  65. package/dist/core/effect-ir/package-exec.d.ts +29 -0
  66. package/dist/core/effect-ir/package-exec.js +292 -0
  67. package/dist/core/effect-ir/policy.d.ts +13 -0
  68. package/dist/core/effect-ir/policy.js +77 -0
  69. package/dist/core/effect-ir/types.d.ts +55 -0
  70. package/dist/core/effect-ir/types.js +1 -0
  71. package/dist/core/egress/allowlist.d.ts +1 -0
  72. package/dist/core/egress/allowlist.js +8 -0
  73. package/dist/core/egress-approval.js +45 -51
  74. package/dist/core/gate-contract.d.ts +2 -0
  75. package/dist/core/gate-contract.js +2 -0
  76. package/dist/core/gate-engine.js +26 -9
  77. package/dist/core/index.d.ts +3 -1
  78. package/dist/core/index.js +2 -1
  79. package/dist/core/judge-cloud-consent.js +29 -15
  80. package/dist/core/network-endpoint.d.ts +10 -0
  81. package/dist/core/network-endpoint.js +51 -0
  82. package/dist/core/process-runner.d.ts +10 -0
  83. package/dist/core/process-runner.js +124 -0
  84. package/dist/core/recover-advice.js +2 -1
  85. package/dist/core/recovery/artifact-store.d.ts +29 -0
  86. package/dist/core/recovery/artifact-store.js +336 -0
  87. package/dist/core/recovery/checkpoint.d.ts +25 -0
  88. package/dist/core/recovery/checkpoint.js +260 -0
  89. package/dist/core/recovery/index.d.ts +3 -1
  90. package/dist/core/recovery/index.js +2 -0
  91. package/dist/core/recovery/operator-guidance.d.ts +7 -0
  92. package/dist/core/recovery/operator-guidance.js +39 -0
  93. package/dist/core/recovery/reconcile.d.ts +3 -0
  94. package/dist/core/recovery/reconcile.js +48 -0
  95. package/dist/core/recovery/resource-identity.d.ts +4 -0
  96. package/dist/core/recovery/resource-identity.js +34 -0
  97. package/dist/core/recovery/restore.d.ts +13 -0
  98. package/dist/core/recovery/restore.js +114 -0
  99. package/dist/core/recovery/snapshot-node.d.ts +27 -0
  100. package/dist/core/recovery/snapshot-node.js +261 -0
  101. package/dist/core/recovery/types.d.ts +85 -1
  102. package/dist/core/transactional/apply-observed-changes.d.ts +13 -0
  103. package/dist/core/transactional/apply-observed-changes.js +275 -0
  104. package/dist/core/transactional/backend-selector.d.ts +8 -0
  105. package/dist/core/transactional/backend-selector.js +118 -0
  106. package/dist/core/transactional/backend.d.ts +55 -0
  107. package/dist/core/transactional/backend.js +1 -0
  108. package/dist/core/transactional/file-checkpoint-backend.d.ts +3 -0
  109. package/dist/core/transactional/file-checkpoint-backend.js +278 -0
  110. package/dist/core/transactional/file-checkpoint-git.d.ts +11 -0
  111. package/dist/core/transactional/file-checkpoint-git.js +217 -0
  112. package/dist/core/transactional/file-checkpoint-staging.d.ts +13 -0
  113. package/dist/core/transactional/file-checkpoint-staging.js +54 -0
  114. package/dist/core/transactional/file-clone.d.ts +15 -0
  115. package/dist/core/transactional/file-clone.js +139 -0
  116. package/dist/core/transactional/file-tree-path.d.ts +7 -0
  117. package/dist/core/transactional/file-tree-path.js +50 -0
  118. package/dist/core/transactional/file-tree.d.ts +41 -0
  119. package/dist/core/transactional/file-tree.js +186 -0
  120. package/dist/core/transactional/git-worktree-backend.d.ts +2 -0
  121. package/dist/core/transactional/git-worktree-backend.js +50 -0
  122. package/dist/core/transactional/git-worktree.d.ts +9 -11
  123. package/dist/core/transactional/git-worktree.js +24 -119
  124. package/dist/core/transactional/index.d.ts +11 -0
  125. package/dist/core/transactional/index.js +10 -0
  126. package/dist/core/transactional/reasons.js +1 -0
  127. package/dist/core/transactional/runner.js +157 -25
  128. package/dist/core/transactional/snapshot-node.d.ts +26 -0
  129. package/dist/core/transactional/snapshot-node.js +87 -0
  130. package/dist/core/transactional/types.d.ts +17 -1
  131. package/dist/core/types.d.ts +10 -1
  132. package/dist/core/verdict/adapter.js +8 -0
  133. package/dist/core/verdict/launcher-resolve.js +2 -20
  134. package/dist/core/verdict/shell-policy.d.ts +1 -0
  135. package/dist/core/verdict/shell-policy.js +1 -1
  136. package/dist/core/verdict/types.d.ts +5 -0
  137. package/dist/core/verdict/verdict.js +37 -20
  138. package/dist/types.d.ts +11 -0
  139. package/dist/version.d.ts +1 -1
  140. package/dist/version.js +1 -1
  141. package/package.json +1 -1
  142. package/skills/belay/SKILL.md +8 -3
  143. package/skills/belay/belay-approve.md +10 -7
@@ -1,36 +1,212 @@
1
+ import { canonicalStringify } from '../fingerprint.js';
1
2
  import { isGrantScopeTooBroad } from './grant.js';
2
3
  import { grantMatchesRequest } from './grant-match.js';
3
- export function grantsFromApprovedState(state, repoRoot) {
4
+ function isActiveGrant(grant, now) {
5
+ if (isGrantScopeTooBroad(grant)) {
6
+ return false;
7
+ }
8
+ const expires = Date.parse(grant.expiresAt);
9
+ return Number.isFinite(expires) && expires > now && grant.usesRemaining > 0;
10
+ }
11
+ export function grantsFromApproval(approval) {
12
+ if (approval.grants?.length) {
13
+ return approval.grants;
14
+ }
15
+ if (approval.grant) {
16
+ return [approval.grant];
17
+ }
18
+ return [];
19
+ }
20
+ export function approvalGrantBundleExhausted(approval) {
21
+ const bundle = grantsFromApproval(approval);
22
+ if (!bundle.length) {
23
+ return false;
24
+ }
25
+ return bundle.every((grant) => grant.usesRemaining <= 0);
26
+ }
27
+ export function consumeApprovalGrantBundle(approval, requests) {
28
+ const bundle = grantsFromApproval(approval);
29
+ if (!bundle.length || !requests.length) {
30
+ return { approval, consumed: false };
31
+ }
32
+ const grants = [...bundle];
4
33
  const now = Date.now();
5
- return state.approvals
6
- .filter((approval) => approval.repoRoot === repoRoot)
7
- .map((approval) => approval.grant)
8
- .filter((grant) => {
34
+ for (const request of requests) {
35
+ const index = grants.findIndex((grant) => {
36
+ return isActiveGrant(grant, now) && grantMatchesRequest(grant, request);
37
+ });
38
+ if (index === -1) {
39
+ return { approval, consumed: false };
40
+ }
41
+ const grant = grants[index];
9
42
  if (!grant) {
10
- return false;
43
+ return { approval, consumed: false };
11
44
  }
12
- if (isGrantScopeTooBroad(grant)) {
45
+ grants[index] = decrementGrant(grant);
46
+ }
47
+ return {
48
+ approval: {
49
+ ...approval,
50
+ grants,
51
+ grant: grants[0],
52
+ },
53
+ consumed: true,
54
+ };
55
+ }
56
+ function exactGrantMatchesRequest(grant, request) {
57
+ return (persistedCanonicalStringify(grant.principal) ===
58
+ persistedCanonicalStringify(request.principal) &&
59
+ grant.action === request.action &&
60
+ persistedCanonicalStringify(grant.resource) === persistedCanonicalStringify(request.resource) &&
61
+ grant.inputFingerprint === request.context.inputFingerprint);
62
+ }
63
+ function validateExactGrantBundle(approval, requests, now, usage) {
64
+ if (approval.grantBundleVersion !== 1) {
65
+ return { ok: false, reason: 'legacy_record' };
66
+ }
67
+ if (!requests.length) {
68
+ return { ok: false, reason: 'empty_requests' };
69
+ }
70
+ const bundle = grantsFromApproval(approval);
71
+ if (bundle.length !== requests.length) {
72
+ return { ok: false, reason: 'cardinality_mismatch' };
73
+ }
74
+ if (bundle.some((grant) => isGrantScopeTooBroad(grant))) {
75
+ return { ok: false, reason: 'grant_scope_too_broad' };
76
+ }
77
+ const usageIsValid = bundle.every((grant) => {
78
+ const expires = Date.parse(grant.expiresAt);
79
+ if (!Number.isFinite(expires) || expires <= now) {
13
80
  return false;
14
81
  }
15
- const expires = Date.parse(grant.expiresAt);
16
- return Number.isFinite(expires) && expires > now && grant.usesRemaining > 0;
82
+ return usage === 'available'
83
+ ? grant.usesRemaining > 0
84
+ : grant.usesRemaining >= 0 && grant.usesRemaining < grant.maxUses;
17
85
  });
86
+ if (!usageIsValid) {
87
+ return { ok: false, reason: 'grant_inactive' };
88
+ }
89
+ const unmatched = [...bundle];
90
+ for (const request of requests) {
91
+ const index = unmatched.findIndex((grant) => exactGrantMatchesRequest(grant, request));
92
+ if (index === -1) {
93
+ return { ok: false, reason: 'grant_mismatch' };
94
+ }
95
+ unmatched.splice(index, 1);
96
+ }
97
+ if (unmatched.length > 0) {
98
+ return { ok: false, reason: 'grant_mismatch' };
99
+ }
100
+ return { ok: true, approval };
101
+ }
102
+ function persistedCanonicalStringify(value) {
103
+ return canonicalStringify(omitUndefined(value));
104
+ }
105
+ function omitUndefined(value) {
106
+ if (Array.isArray(value)) {
107
+ return value.map((entry) => omitUndefined(entry));
108
+ }
109
+ if (!value || typeof value !== 'object') {
110
+ return value;
111
+ }
112
+ return Object.fromEntries(Object.entries(value)
113
+ .filter(([, entry]) => entry !== undefined)
114
+ .map(([key, entry]) => [key, omitUndefined(entry)]));
115
+ }
116
+ /** Validate and consume a marked exact bundle without mutating on any failure. */
117
+ export function validateAndConsumeGrantBundle(approval, requests, now = Date.now()) {
118
+ const validated = validateExactGrantBundle(approval, requests, now, 'available');
119
+ if (!validated.ok) {
120
+ return validated;
121
+ }
122
+ const grants = grantsFromApproval(approval).map((grant) => decrementGrant(grant));
123
+ return {
124
+ ok: true,
125
+ approval: {
126
+ ...approval,
127
+ grants,
128
+ grant: grants[0],
129
+ },
130
+ };
131
+ }
132
+ /** Revalidate a marked bundle during an active replay lease without consuming it twice. */
133
+ export function validateGrantBundleForLeaseReuse(approval, requests, now = Date.now()) {
134
+ return validateExactGrantBundle(approval, requests, now, 'previously_consumed');
135
+ }
136
+ /** Consume every grant in an approved record after fingerprint/hash replay validation. */
137
+ export function consumeApprovedRecordGrantBundle(approval) {
138
+ const bundle = grantsFromApproval(approval);
139
+ const now = Date.now();
140
+ if (!bundle.length ||
141
+ bundle.some((grant) => {
142
+ const expires = Date.parse(grant.expiresAt);
143
+ return !Number.isFinite(expires) || expires <= now || grant.usesRemaining <= 0;
144
+ })) {
145
+ return { approval, consumed: false };
146
+ }
147
+ const grants = bundle.map((grant) => decrementGrant(grant));
148
+ return {
149
+ approval: {
150
+ ...approval,
151
+ grants,
152
+ grant: grants[0],
153
+ },
154
+ consumed: true,
155
+ };
156
+ }
157
+ export function decrementApprovalLegacyGrant(approval) {
158
+ const bundle = grantsFromApproval(approval);
159
+ if (!bundle.length) {
160
+ return approval;
161
+ }
162
+ const grants = bundle.map((grant, index) => (index === 0 ? decrementGrant(grant) : grant));
163
+ return {
164
+ ...approval,
165
+ grants,
166
+ grant: grants[0],
167
+ };
168
+ }
169
+ export function grantsFromApprovedState(state, repoRoot) {
170
+ const now = Date.now();
171
+ const collected = [];
172
+ for (const approval of state.approvals) {
173
+ if (approval.repoRoot !== repoRoot) {
174
+ continue;
175
+ }
176
+ for (const grant of grantsFromApproval(approval)) {
177
+ if (isActiveGrant(grant, now)) {
178
+ collected.push(grant);
179
+ }
180
+ }
181
+ }
182
+ return collected;
183
+ }
184
+ function decrementGrant(grant) {
185
+ return {
186
+ ...grant,
187
+ usesRemaining: grant.usesRemaining - 1,
188
+ };
18
189
  }
19
190
  export function consumeGrantLease(state, grantId) {
20
191
  let consumed = false;
21
192
  const approvals = state.approvals.map((approval) => {
22
- const grant = approval.grant;
23
- if (!grant || grant.grantId !== grantId || grant.usesRemaining <= 0) {
193
+ const bundle = grantsFromApproval(approval);
194
+ const index = bundle.findIndex((g) => g.grantId === grantId);
195
+ if (index === -1) {
196
+ return approval;
197
+ }
198
+ const grant = bundle[index];
199
+ if (!grant || grant.usesRemaining <= 0) {
24
200
  return approval;
25
201
  }
26
202
  consumed = true;
27
- const usesRemaining = grant.usesRemaining - 1;
203
+ const updated = decrementGrant(grant);
204
+ const grants = [...bundle];
205
+ grants[index] = updated;
28
206
  return {
29
207
  ...approval,
30
- grant: {
31
- ...grant,
32
- usesRemaining,
33
- },
208
+ grants,
209
+ grant: grants[0],
34
210
  };
35
211
  });
36
212
  return {
@@ -38,6 +214,27 @@ export function consumeGrantLease(state, grantId) {
38
214
  consumed,
39
215
  };
40
216
  }
217
+ /** Consume one lease for each request; all must succeed (atomic bundle consumption). */
218
+ export function consumeGrantLeasesForRequests(state, requests) {
219
+ const repoRoot = requests[0]?.principal.repoRoot;
220
+ if (!repoRoot || requests.some((request) => request.principal.repoRoot !== repoRoot)) {
221
+ return { state, consumed: false };
222
+ }
223
+ for (let index = 0; index < state.approvals.length; index += 1) {
224
+ const approval = state.approvals[index];
225
+ if (!approval || approval.repoRoot !== repoRoot) {
226
+ continue;
227
+ }
228
+ const consumed = consumeApprovalGrantBundle(approval, requests);
229
+ if (!consumed.consumed) {
230
+ continue;
231
+ }
232
+ const approvals = [...state.approvals];
233
+ approvals[index] = consumed.approval;
234
+ return { state: { ...state, approvals }, consumed: true };
235
+ }
236
+ return { state, consumed: false };
237
+ }
41
238
  export function findMatchingGrant(grants, request) {
42
239
  if (!grants?.length) {
43
240
  return undefined;
@@ -53,6 +53,9 @@ function grantTargetsResource(grant, request) {
53
53
  if (grant.resource.kind === 'executable' && request.resource.kind === 'executable') {
54
54
  return grant.resource.command === request.resource.command;
55
55
  }
56
+ if (grant.resource.kind === 'package-cache' && request.resource.kind === 'package-cache') {
57
+ return grant.resource.manager === request.resource.manager;
58
+ }
56
59
  if (grant.resource.kind === 'git-ref' && request.resource.kind === 'git-ref') {
57
60
  return grant.resource.ref === request.resource.ref;
58
61
  }
@@ -1,5 +1,5 @@
1
1
  export { addPathToAllowlist, allPathsAllowlisted, fsScopeAllowlistPath, isPathAllowlisted, loadFsScopeAllowlist, loadFsScopeAllowlistSync, saveFsScopeAllowlist, } from './allowlist.js';
2
- export { BOUNDARY_ATTESTATION_VERSION, type BoundaryAttestation, type BoundaryDriverId, isAttestationFresh, } from './attestation.js';
2
+ export { attestsWorkspaceMountIsolation, BOUNDARY_ATTESTATION_VERSION, type BoundaryAttestation, type BoundaryDriverId, isAttestationFresh, } from './attestation.js';
3
3
  export { BOUNDARY_PROFILE_L1_ATTESTED, BOUNDARY_PROFILE_L3_L4_ONLY, BOUNDARY_PROFILE_L3_POLICY, boundaryVerifiedAllowEnabled, isAttestedBoundary, resolveBoundaryProfile, } from './boundary-profile.js';
4
4
  export { evaluateL1FullStatus, hasSandboxRuntime, isCapabilityBrokerDemotionActive, isSandboxBrokerEnabled, } from './broker.js';
5
5
  export { CAPABILITY_GRANT_VERSION, type CapabilityGrantV1, isGrantScopeTooBroad, } from './grant.js';
@@ -1,5 +1,5 @@
1
1
  export { addPathToAllowlist, allPathsAllowlisted, fsScopeAllowlistPath, isPathAllowlisted, loadFsScopeAllowlist, loadFsScopeAllowlistSync, saveFsScopeAllowlist, } from './allowlist.js';
2
- export { BOUNDARY_ATTESTATION_VERSION, isAttestationFresh, } from './attestation.js';
2
+ export { attestsWorkspaceMountIsolation, BOUNDARY_ATTESTATION_VERSION, isAttestationFresh, } from './attestation.js';
3
3
  export { BOUNDARY_PROFILE_L1_ATTESTED, BOUNDARY_PROFILE_L3_L4_ONLY, BOUNDARY_PROFILE_L3_POLICY, boundaryVerifiedAllowEnabled, isAttestedBoundary, resolveBoundaryProfile, } from './boundary-profile.js';
4
4
  export { evaluateL1FullStatus, hasSandboxRuntime, isCapabilityBrokerDemotionActive, isSandboxBrokerEnabled, } from './broker.js';
5
5
  export { CAPABILITY_GRANT_VERSION, isGrantScopeTooBroad, } from './grant.js';
@@ -56,12 +56,19 @@ export declare function evaluateSubagentPolicy(analysis: SubagentCapabilityAnaly
56
56
  decision: PolicyDecision;
57
57
  };
58
58
  export declare function buildShellCapabilityRequest(analysis: ShellCapabilityAnalysis): CapabilityRequestV1;
59
+ export declare function buildShellCapabilityRequests(analysis: ShellCapabilityAnalysis): CapabilityRequestV1[];
59
60
  export declare function buildFileMutationCapabilityRequest(analysis: FileMutationCapabilityAnalysis): CapabilityRequestV1;
60
61
  export declare function createTypeScriptPolicyEngine(): PolicyEngine;
61
62
  export declare function getDefaultPolicyEngine(): PolicyEngine;
62
63
  export declare function policyDecisionRequiresAsk(decision: PolicyDecision): boolean;
64
+ export declare function combinePolicyDecisions(decisions: readonly PolicyDecision[]): PolicyDecision;
65
+ export declare function evaluateCapabilityRequestsPolicy(requests: readonly CapabilityRequestV1[], config: BelayConfigV4, auth?: PolicyAuthExtras, trustedWorkspaceRoots?: string[]): {
66
+ decisions: PolicyDecision[];
67
+ decision: PolicyDecision;
68
+ };
63
69
  export declare function evaluateShellPolicy(analysis: ShellCapabilityAnalysis, config: BelayConfigV4, auth?: PolicyAuthExtras): {
64
70
  request: CapabilityRequestV1;
71
+ requests: CapabilityRequestV1[];
65
72
  decision: PolicyDecision;
66
73
  };
67
74
  export declare function evaluateFileMutationPolicy(analysis: FileMutationCapabilityAnalysis, config: BelayConfigV4, auth?: PolicyAuthExtras): {
@@ -1,7 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import path from 'node:path';
3
3
  import { matchesSensitivePath } from '../glob.js';
4
+ import { parseNetworkEndpoint } from '../network-endpoint.js';
4
5
  import { canonicalPath, pathWithinRoot, resolveWorkspaceRootMatch } from '../path-utils.js';
6
+ import { tokenizeShell } from '../shell-tokenizer.js';
5
7
  import { BOUNDARY_GRANT_ISSUER_CONTAINER, isPathWithinBoundaryMount, materializeContainerBoundaryGrant, } from './boundary-grant-materialize.js';
6
8
  import { boundaryVerifiedAllowEnabled } from './boundary-profile.js';
7
9
  import { isGrantScopeTooBroad } from './grant.js';
@@ -116,21 +118,27 @@ function resourcePathForShellAnalysis(analysis) {
116
118
  const resolved = targets.map((target) => analysis.resolvedPathTargets?.length
117
119
  ? canonicalPath(target)
118
120
  : resolveCapabilityPath(target, analysis.cwd));
121
+ const [first] = resolved;
122
+ if (!first) {
123
+ return null;
124
+ }
119
125
  if (analysis.effect === 'local_mutation') {
120
126
  const outside = resolved.find((candidate) => !pathWithinRoot(repoRoot, candidate));
121
- return outside ?? resolved[resolved.length - 1];
127
+ return outside ?? resolved.at(-1) ?? first;
122
128
  }
123
- return resolved[0];
129
+ return first;
124
130
  }
125
131
  function resourceForShellAnalysis(analysis) {
126
132
  if (isGitRefWrite(analysis)) {
127
133
  return { kind: 'git-ref', ref: 'push' };
128
134
  }
129
- if (analysis.egressClass === 'ambiguous' ||
130
- analysis.egressClass === 'read' ||
131
- analysis.egressClass === 'destructive' ||
132
- analysis.location === 'external') {
133
- return { kind: 'network', host: '*', protocol: 'unknown' };
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
+ });
134
142
  }
135
143
  const resourcePath = resourcePathForShellAnalysis(analysis);
136
144
  if (resourcePath) {
@@ -138,8 +146,114 @@ function resourceForShellAnalysis(analysis) {
138
146
  }
139
147
  return { kind: 'executable', command: analysis.segmentHead };
140
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
+ }
141
246
  export function buildShellCapabilityRequest(analysis) {
142
- return {
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) => ({
143
257
  version: CAPABILITY_REQUEST_VERSION,
144
258
  principal: {
145
259
  adapter: analysis.adapter,
@@ -147,7 +261,7 @@ export function buildShellCapabilityRequest(analysis) {
147
261
  sessionHash: hashSession(`${analysis.repoRoot}:${analysis.cwd}`),
148
262
  },
149
263
  action: actionForShellAnalysis(analysis),
150
- resource: resourceForShellAnalysis(analysis),
264
+ resource,
151
265
  context: {
152
266
  cwd: analysis.cwd,
153
267
  inputFingerprint: analysis.inputFingerprint,
@@ -163,7 +277,7 @@ export function buildShellCapabilityRequest(analysis) {
163
277
  level: evidenceLevelForOpacity(analysis.opacity),
164
278
  signals: [...analysis.signals],
165
279
  },
166
- };
280
+ }));
167
281
  }
168
282
  function actionForFileMutation(analysis) {
169
283
  if (analysis.locationLabel === 'control_plane') {
@@ -233,6 +347,16 @@ function shellLocationFromRequest(request) {
233
347
  function isRepoLocalShellLocation(request) {
234
348
  return shellLocationFromRequest(request) === 'repo_local';
235
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
+ }
236
360
  function isRepoLocalRoutineWrite(request, sensitivePaths) {
237
361
  if (!isRepoLocalShellLocation(request)) {
238
362
  return false;
@@ -297,6 +421,14 @@ function builtInRule(request, context) {
297
421
  matchedRule: 'builtin.subagent',
298
422
  };
299
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
+ }
300
432
  if (request.action === 'secret.read' || request.action === 'git.ref.write') {
301
433
  return {
302
434
  outcome: 'require_approval',
@@ -456,14 +588,69 @@ export function getDefaultPolicyEngine() {
456
588
  export function policyDecisionRequiresAsk(decision) {
457
589
  return decision.outcome === 'require_approval' || decision.outcome === 'deny';
458
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
+ }
459
642
  export function evaluateShellPolicy(analysis, config, auth) {
460
- const request = buildShellCapabilityRequest(analysis);
461
- const enriched = enrichAuthWithMaterializedGrants(request, config, {
643
+ const requests = buildShellCapabilityRequests(analysis);
644
+ const enrichedAuth = {
462
645
  ...auth,
463
646
  sensitivePaths: auth?.sensitivePaths ?? analysis.sensitivePaths,
464
- });
465
- const decision = getDefaultPolicyEngine().evaluate(request, buildAuthorizationContext(config, analysis.trustedWorkspaceRoots, enriched));
466
- return { request, decision };
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 };
467
654
  }
468
655
  export function evaluateFileMutationPolicy(analysis, config, auth) {
469
656
  const request = buildFileMutationCapabilityRequest(analysis);
@@ -18,6 +18,9 @@ export type CapabilityResource = {
18
18
  } | {
19
19
  kind: 'executable';
20
20
  command: string;
21
+ } | {
22
+ kind: 'package-cache';
23
+ manager: 'npm' | 'pnpm';
21
24
  } | {
22
25
  kind: 'git-ref';
23
26
  ref: string;
@@ -46,6 +46,15 @@ export interface BelayModelAssistConfig {
46
46
  model?: string;
47
47
  timeoutMs?: number;
48
48
  }
49
+ export interface BelayFileCheckpointConfig {
50
+ enabled: boolean;
51
+ allowNonGit: boolean;
52
+ maxFiles: number;
53
+ maxSourceBytes: number;
54
+ maxWorkspaceBytes: number;
55
+ prepareTimeoutMs: number;
56
+ copyConcurrency: number;
57
+ }
49
58
  export interface BelayTransactionalConfig {
50
59
  enabled: boolean;
51
60
  minConfidence: number;
@@ -55,6 +64,14 @@ export interface BelayTransactionalConfig {
55
64
  gates: {
56
65
  shell: boolean;
57
66
  };
67
+ fileCheckpoint: BelayFileCheckpointConfig;
68
+ checkpoint?: {
69
+ enabled: boolean;
70
+ appliedRetentionHours: number;
71
+ restoredRetentionHours: number;
72
+ maxCheckpoints: number;
73
+ maxBytes: number;
74
+ };
58
75
  }
59
76
  export interface BelayPolicyConfig {
60
77
  unknownLocalEffect: UnknownLocalEffectPolicy;
@@ -196,6 +213,9 @@ export type BelayConfig = BelayConfigV4;
196
213
  export declare const DEFAULT_FENCE_WARN_THRESHOLD = 0.5;
197
214
  export declare const DEFAULT_CONFIDENCE_THRESHOLDS: BelayConfidenceThresholds;
198
215
  export declare const DEFAULT_MODEL_ASSIST: BelayModelAssistConfig;
216
+ export declare const DEFAULT_FILE_CHECKPOINT: BelayFileCheckpointConfig;
217
+ export declare function normalizeFileCheckpointConfig(raw: Partial<BelayFileCheckpointConfig> | undefined): BelayFileCheckpointConfig;
218
+ export declare const DEFAULT_RECOVERY_CHECKPOINT: NonNullable<BelayTransactionalConfig['checkpoint']>;
199
219
  export declare const DEFAULT_TRANSACTIONAL_V3: BelayTransactionalConfig;
200
220
  export declare const LEGACY_POLICY_V3: BelayPolicyConfig;
201
221
  /** Fresh install defaults: recoverable-first with opaque/unparseable fail-closed. */