@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.
- package/README.md +20 -1
- package/dist/adapters/claude/runtime-entry.js +54 -25
- package/dist/adapters/shared/gate-runtime.d.ts +7 -0
- package/dist/adapters/shared/gate-runtime.js +420 -220
- package/dist/bundle/claude-runtime.mjs +13215 -8864
- package/dist/bundle/codex-runtime.mjs +10771 -6453
- package/dist/bundle/cursor-runtime.mjs +10797 -6484
- package/dist/cli.js +37 -1
- package/dist/commands/approve.js +34 -10
- package/dist/commands/doctor.js +51 -0
- package/dist/commands/recovery-checkpoints.d.ts +164 -0
- package/dist/commands/recovery-checkpoints.js +347 -0
- package/dist/commands/revoke.js +19 -7
- package/dist/commands/status.js +7 -0
- package/dist/config-io.js +49 -18
- package/dist/conformance/guarantee-table.js +6 -0
- package/dist/conformance/types.d.ts +1 -0
- package/dist/core/approval-replay.d.ts +1 -1
- package/dist/core/approval-replay.js +4 -1
- package/dist/core/approval-service.d.ts +6 -1
- package/dist/core/approval-service.js +86 -27
- package/dist/core/approval.d.ts +2 -0
- package/dist/core/approval.js +6 -3
- package/dist/core/audit-types.d.ts +13 -0
- package/dist/core/capability/approval-state-mutation.d.ts +19 -0
- package/dist/core/capability/approval-state-mutation.js +76 -3
- package/dist/core/capability/approval-v3.d.ts +18 -1
- package/dist/core/capability/approval-v3.js +63 -23
- package/dist/core/capability/attestation.d.ts +3 -0
- package/dist/core/capability/attestation.js +7 -0
- package/dist/core/capability/boundary-driver-container.d.ts +12 -0
- package/dist/core/capability/boundary-driver-container.js +101 -34
- package/dist/core/capability/boundary-driver.js +1 -0
- package/dist/core/capability/boundary-grant-materialize.d.ts +1 -0
- package/dist/core/capability/boundary-grant-materialize.js +15 -0
- package/dist/core/capability/boundary-run.d.ts +25 -0
- package/dist/core/capability/boundary-run.js +37 -9
- package/dist/core/capability/boundary-session.d.ts +4 -0
- package/dist/core/capability/boundary-session.js +9 -0
- package/dist/core/capability/boundary-workspace-mount.d.ts +5 -0
- package/dist/core/capability/boundary-workspace-mount.js +68 -0
- package/dist/core/capability/gate-policy-shadow.js +23 -1
- package/dist/core/capability/grant-consumption.d.ts +4 -0
- package/dist/core/capability/grant-consumption.js +11 -0
- package/dist/core/capability/grant-lease.d.ts +36 -1
- package/dist/core/capability/grant-lease.js +213 -16
- package/dist/core/capability/grant-match.js +3 -0
- package/dist/core/capability/index.d.ts +1 -1
- package/dist/core/capability/index.js +1 -1
- package/dist/core/capability/policy-engine.d.ts +7 -0
- package/dist/core/capability/policy-engine.js +202 -15
- package/dist/core/capability/request.d.ts +3 -0
- package/dist/core/config.d.ts +20 -0
- package/dist/core/config.js +62 -0
- package/dist/core/effect-ir/audit.d.ts +9 -0
- package/dist/core/effect-ir/audit.js +86 -0
- package/dist/core/effect-ir/build.d.ts +31 -0
- package/dist/core/effect-ir/build.js +277 -0
- package/dist/core/effect-ir/index.d.ts +8 -0
- package/dist/core/effect-ir/index.js +7 -0
- package/dist/core/effect-ir/normalize.d.ts +4 -0
- package/dist/core/effect-ir/normalize.js +104 -0
- package/dist/core/effect-ir/package-exec-eval.d.ts +8 -0
- package/dist/core/effect-ir/package-exec-eval.js +152 -0
- package/dist/core/effect-ir/package-exec.d.ts +29 -0
- package/dist/core/effect-ir/package-exec.js +292 -0
- package/dist/core/effect-ir/policy.d.ts +13 -0
- package/dist/core/effect-ir/policy.js +77 -0
- package/dist/core/effect-ir/types.d.ts +55 -0
- package/dist/core/effect-ir/types.js +1 -0
- package/dist/core/egress/allowlist.d.ts +1 -0
- package/dist/core/egress/allowlist.js +8 -0
- package/dist/core/egress-approval.js +45 -51
- package/dist/core/gate-contract.d.ts +2 -0
- package/dist/core/gate-contract.js +2 -0
- package/dist/core/gate-engine.js +26 -9
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +2 -1
- package/dist/core/judge-cloud-consent.js +29 -15
- package/dist/core/network-endpoint.d.ts +10 -0
- package/dist/core/network-endpoint.js +51 -0
- package/dist/core/process-runner.d.ts +10 -0
- package/dist/core/process-runner.js +124 -0
- package/dist/core/recover-advice.js +2 -1
- package/dist/core/recovery/artifact-store.d.ts +29 -0
- package/dist/core/recovery/artifact-store.js +336 -0
- package/dist/core/recovery/checkpoint.d.ts +25 -0
- package/dist/core/recovery/checkpoint.js +260 -0
- package/dist/core/recovery/index.d.ts +3 -1
- package/dist/core/recovery/index.js +2 -0
- package/dist/core/recovery/operator-guidance.d.ts +7 -0
- package/dist/core/recovery/operator-guidance.js +39 -0
- package/dist/core/recovery/reconcile.d.ts +3 -0
- package/dist/core/recovery/reconcile.js +48 -0
- package/dist/core/recovery/resource-identity.d.ts +4 -0
- package/dist/core/recovery/resource-identity.js +34 -0
- package/dist/core/recovery/restore.d.ts +13 -0
- package/dist/core/recovery/restore.js +114 -0
- package/dist/core/recovery/snapshot-node.d.ts +27 -0
- package/dist/core/recovery/snapshot-node.js +261 -0
- package/dist/core/recovery/types.d.ts +85 -1
- package/dist/core/transactional/apply-observed-changes.d.ts +13 -0
- package/dist/core/transactional/apply-observed-changes.js +275 -0
- package/dist/core/transactional/backend-selector.d.ts +8 -0
- package/dist/core/transactional/backend-selector.js +118 -0
- package/dist/core/transactional/backend.d.ts +55 -0
- package/dist/core/transactional/backend.js +1 -0
- package/dist/core/transactional/file-checkpoint-backend.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-backend.js +278 -0
- package/dist/core/transactional/file-checkpoint-git.d.ts +11 -0
- package/dist/core/transactional/file-checkpoint-git.js +217 -0
- package/dist/core/transactional/file-checkpoint-staging.d.ts +13 -0
- package/dist/core/transactional/file-checkpoint-staging.js +54 -0
- package/dist/core/transactional/file-clone.d.ts +15 -0
- package/dist/core/transactional/file-clone.js +139 -0
- package/dist/core/transactional/file-tree-path.d.ts +7 -0
- package/dist/core/transactional/file-tree-path.js +50 -0
- package/dist/core/transactional/file-tree.d.ts +41 -0
- package/dist/core/transactional/file-tree.js +186 -0
- package/dist/core/transactional/git-worktree-backend.d.ts +2 -0
- package/dist/core/transactional/git-worktree-backend.js +50 -0
- package/dist/core/transactional/git-worktree.d.ts +9 -11
- package/dist/core/transactional/git-worktree.js +24 -119
- package/dist/core/transactional/index.d.ts +11 -0
- package/dist/core/transactional/index.js +10 -0
- package/dist/core/transactional/reasons.js +1 -0
- package/dist/core/transactional/runner.js +157 -25
- package/dist/core/transactional/snapshot-node.d.ts +26 -0
- package/dist/core/transactional/snapshot-node.js +87 -0
- package/dist/core/transactional/types.d.ts +17 -1
- package/dist/core/types.d.ts +10 -1
- package/dist/core/verdict/adapter.js +8 -0
- package/dist/core/verdict/launcher-resolve.js +2 -20
- package/dist/core/verdict/shell-policy.d.ts +1 -0
- package/dist/core/verdict/shell-policy.js +1 -1
- package/dist/core/verdict/types.d.ts +5 -0
- package/dist/core/verdict/verdict.js +37 -20
- package/dist/types.d.ts +11 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/belay/SKILL.md +8 -3
- 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
|
-
|
|
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
|
-
|
|
6
|
-
.
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|
23
|
-
|
|
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
|
|
203
|
+
const updated = decrementGrant(grant);
|
|
204
|
+
const grants = [...bundle];
|
|
205
|
+
grants[index] = updated;
|
|
28
206
|
return {
|
|
29
207
|
...approval,
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
127
|
+
return outside ?? resolved.at(-1) ?? first;
|
|
122
128
|
}
|
|
123
|
-
return
|
|
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
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
461
|
-
const
|
|
643
|
+
const requests = buildShellCapabilityRequests(analysis);
|
|
644
|
+
const enrichedAuth = {
|
|
462
645
|
...auth,
|
|
463
646
|
sensitivePaths: auth?.sensitivePaths ?? analysis.sensitivePaths,
|
|
464
|
-
}
|
|
465
|
-
const decision =
|
|
466
|
-
|
|
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);
|
package/dist/core/config.d.ts
CHANGED
|
@@ -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. */
|