@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
|
@@ -2,18 +2,25 @@ import { approvedApprovalsPath, loadApprovalState, pendingApprovalsPath, saveApp
|
|
|
2
2
|
import { compactApprovals } from './approval.js';
|
|
3
3
|
import { buildApprovalRecordedMessage } from './approval-replay.js';
|
|
4
4
|
import { verifyApprovalToken } from './approval-token.js';
|
|
5
|
+
import { mutateApprovalStateWithRetry, mutatePendingAndApprovedWithRetry, } from './capability/approval-state-mutation.js';
|
|
5
6
|
import { APPROVAL_STATE_VERSION_V3, mintGrantForApprovedRecord } from './capability/approval-v3.js';
|
|
6
7
|
import { configuredControlPlaneDir } from './config.js';
|
|
7
8
|
export async function recordApproval(params) {
|
|
8
9
|
const { approvalId, config, store, token, requireSignedToken = false, adapter } = params;
|
|
9
|
-
const pending = await store.loadPending();
|
|
10
|
+
const [pending, approved] = await Promise.all([store.loadPending(), store.loadApproved()]);
|
|
10
11
|
pending.state = compactApprovals(pending.state);
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
approved.state = compactApprovals(approved.state);
|
|
13
|
+
const pendingApproval = pending.state.approvals.find((entry) => entry.approvalId === approvalId);
|
|
14
|
+
const approvedApproval = approved.state.approvals.find((entry) => entry.approvalId === approvalId);
|
|
15
|
+
const approval = pendingApproval ?? approvedApproval;
|
|
16
|
+
if (!approval) {
|
|
17
|
+
await mutateApprovalStateWithRetry({
|
|
18
|
+
load: store.loadPending,
|
|
19
|
+
write: store.writePending,
|
|
20
|
+
mutate: (state) => ({ state: compactApprovals(state), result: true }),
|
|
21
|
+
});
|
|
14
22
|
return { ok: false, message: 'Belay approval not found or expired.' };
|
|
15
23
|
}
|
|
16
|
-
const [approval] = pending.state.approvals.slice(index, index + 1);
|
|
17
24
|
if (requireSignedToken) {
|
|
18
25
|
if (!token) {
|
|
19
26
|
return { ok: false, message: 'Signed approval token required for out-of-band approval.' };
|
|
@@ -27,34 +34,86 @@ export async function recordApproval(params) {
|
|
|
27
34
|
return { ok: false, message: 'Signed approval token does not match the pending approval.' };
|
|
28
35
|
}
|
|
29
36
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
if (!pendingApproval && approvedApproval) {
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
message: buildApprovalRecordedMessage(config, approvedApproval, adapter),
|
|
41
|
+
approval: approvedApproval,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const recorded = await mutatePendingAndApprovedWithRetry({
|
|
45
|
+
loadPending: store.loadPending,
|
|
46
|
+
loadApproved: store.loadApproved,
|
|
47
|
+
writePending: store.writePending,
|
|
48
|
+
writeApproved: store.writeApproved,
|
|
49
|
+
mutate: (pendingState, approvedState) => {
|
|
50
|
+
const compactedPending = compactApprovals(pendingState);
|
|
51
|
+
const compactedApproved = compactApprovals(approvedState);
|
|
52
|
+
const existing = compactedApproved.approvals.find((entry) => entry.approvalId === approvalId);
|
|
53
|
+
if (existing &&
|
|
54
|
+
(existing.fingerprint !== approval.fingerprint || existing.repoRoot !== approval.repoRoot)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const pendingIndex = compactedPending.approvals.findIndex((entry) => entry.approvalId === approvalId &&
|
|
58
|
+
entry.fingerprint === approval.fingerprint &&
|
|
59
|
+
entry.repoRoot === approval.repoRoot);
|
|
60
|
+
if (pendingIndex === -1) {
|
|
61
|
+
return existing
|
|
62
|
+
? {
|
|
63
|
+
pending: compactedPending,
|
|
64
|
+
approved: compactedApproved,
|
|
65
|
+
result: existing,
|
|
66
|
+
}
|
|
67
|
+
: null;
|
|
68
|
+
}
|
|
69
|
+
const [claimed] = compactedPending.approvals.splice(pendingIndex, 1);
|
|
70
|
+
if (!claimed) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const approvedRecord = existing ??
|
|
74
|
+
mintGrantForApprovedRecord({
|
|
75
|
+
...claimed,
|
|
76
|
+
approvedAt: new Date().toISOString(),
|
|
77
|
+
});
|
|
78
|
+
if (!existing) {
|
|
79
|
+
compactedApproved.version = APPROVAL_STATE_VERSION_V3;
|
|
80
|
+
compactedApproved.approvals.push(approvedRecord);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
pending: compactedPending,
|
|
84
|
+
approved: compactedApproved,
|
|
85
|
+
result: approvedRecord,
|
|
86
|
+
};
|
|
87
|
+
},
|
|
37
88
|
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
await store.writeApproved(approved.filePath, approved.state);
|
|
89
|
+
if (!recorded) {
|
|
90
|
+
return { ok: false, message: 'Belay approval not found, expired, or already claimed.' };
|
|
91
|
+
}
|
|
42
92
|
return {
|
|
43
93
|
ok: true,
|
|
44
|
-
message: buildApprovalRecordedMessage(config,
|
|
45
|
-
approval:
|
|
94
|
+
message: buildApprovalRecordedMessage(config, recorded, adapter),
|
|
95
|
+
approval: recorded,
|
|
46
96
|
};
|
|
47
97
|
}
|
|
48
|
-
/**
|
|
98
|
+
/** @deprecated Replay callers must claim before execution with `claimApprovedForReplay`. */
|
|
49
99
|
export async function consumeApprovedAfterCliReplay(params) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
100
|
+
await claimApprovedForReplay(params);
|
|
101
|
+
}
|
|
102
|
+
/** Atomically spend and return a one-shot approval before executing its replay. */
|
|
103
|
+
export async function claimApprovedForReplay(params) {
|
|
104
|
+
return mutateApprovalStateWithRetry({
|
|
105
|
+
load: params.store.loadApproved,
|
|
106
|
+
write: params.store.writeApproved,
|
|
107
|
+
mutate: (state) => {
|
|
108
|
+
const compacted = compactApprovals(state);
|
|
109
|
+
const index = compacted.approvals.findIndex((approval) => approval.approvalId === params.approvalId);
|
|
110
|
+
if (index === -1) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const [claimed] = compacted.approvals.splice(index, 1);
|
|
114
|
+
return { state: compacted, result: claimed ?? null };
|
|
115
|
+
},
|
|
116
|
+
});
|
|
58
117
|
}
|
|
59
118
|
export function createGateApprovalStore(repoRoot, config) {
|
|
60
119
|
return {
|
package/dist/core/approval.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export declare function createApprovalRecordWithEnvelope(params: {
|
|
|
25
25
|
};
|
|
26
26
|
scopeHint?: ApprovalRecord['scopeHint'];
|
|
27
27
|
capabilityRequests?: ApprovalRecord['capabilityRequests'];
|
|
28
|
+
effectPlanHash?: string;
|
|
28
29
|
}): ApprovalRecord;
|
|
29
30
|
export declare function createApprovalRecord(params: {
|
|
30
31
|
kind: ApprovalRecord['kind'];
|
|
@@ -42,4 +43,5 @@ export declare function createApprovalRecord(params: {
|
|
|
42
43
|
payloadJson?: string;
|
|
43
44
|
scopeHint?: ApprovalRecord['scopeHint'];
|
|
44
45
|
capabilityRequests?: ApprovalRecord['capabilityRequests'];
|
|
46
|
+
effectPlanHash?: string;
|
|
45
47
|
}): ApprovalRecord;
|
package/dist/core/approval.js
CHANGED
|
@@ -7,13 +7,15 @@ export function nowIso() {
|
|
|
7
7
|
return new Date().toISOString();
|
|
8
8
|
}
|
|
9
9
|
export function isExpired(approval) {
|
|
10
|
-
|
|
10
|
+
const expiresAt = Date.parse(approval.expiresAt);
|
|
11
|
+
return !Number.isFinite(expiresAt) || expiresAt <= Date.now();
|
|
11
12
|
}
|
|
12
13
|
export function isExecutionLeaseExpired(approval) {
|
|
13
14
|
if (!approval.executionLeaseExpiresAt) {
|
|
14
15
|
return false;
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
+
const expiresAt = Date.parse(approval.executionLeaseExpiresAt);
|
|
18
|
+
return !Number.isFinite(expiresAt) || expiresAt <= Date.now();
|
|
17
19
|
}
|
|
18
20
|
export function compactApprovals(state) {
|
|
19
21
|
const compacted = {
|
|
@@ -89,6 +91,7 @@ export function createApprovalRecordWithEnvelope(params) {
|
|
|
89
91
|
payloadJson: envelope.payloadJson,
|
|
90
92
|
scopeHint: params.scopeHint,
|
|
91
93
|
capabilityRequests: params.capabilityRequests,
|
|
94
|
+
effectPlanHash: params.effectPlanHash,
|
|
92
95
|
});
|
|
93
96
|
}
|
|
94
97
|
export function createApprovalRecord(params) {
|
|
@@ -123,5 +126,5 @@ export function createApprovalRecord(params) {
|
|
|
123
126
|
if (params.scopeHint) {
|
|
124
127
|
record.scopeHint = params.scopeHint;
|
|
125
128
|
}
|
|
126
|
-
return attachCapabilityEnvelope(record, params.capabilityRequests);
|
|
129
|
+
return attachCapabilityEnvelope(record, params.capabilityRequests, params.effectPlanHash);
|
|
127
130
|
}
|
|
@@ -20,10 +20,23 @@ export interface AuditRecord {
|
|
|
20
20
|
predictedAssessment?: Assessment;
|
|
21
21
|
observedAssessment?: Assessment;
|
|
22
22
|
transactional?: boolean;
|
|
23
|
+
transactionalBackend?: 'git_worktree' | 'file_checkpoint';
|
|
24
|
+
resourceKind?: 'git_repository' | 'directory';
|
|
25
|
+
baselineTreeHash?: string;
|
|
26
|
+
snapshotFileCount?: number;
|
|
27
|
+
snapshotSourceBytes?: number;
|
|
28
|
+
snapshotWorkspaceBytes?: number;
|
|
29
|
+
snapshotCopyStrategy?: 'clonefile' | 'reflink' | 'copy';
|
|
30
|
+
snapshotPrepareMs?: number;
|
|
23
31
|
transactionalReason?: string;
|
|
24
32
|
transactionalCategories?: string[];
|
|
25
33
|
transactionalChangeCount?: number;
|
|
26
34
|
transactionalSkipReason?: string;
|
|
35
|
+
transactionalSkipDetail?: string;
|
|
36
|
+
recoveryCheckpointId?: string;
|
|
37
|
+
recoveryBackend?: string;
|
|
38
|
+
recoveryProofHash?: string;
|
|
39
|
+
recoveryState?: string;
|
|
27
40
|
replayContext?: AuditReplayContext;
|
|
28
41
|
actionSnapshot?: AuditActionSnapshot;
|
|
29
42
|
[key: string]: unknown;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ApprovalStateFile } from '../types.js';
|
|
2
|
+
export declare function withStateFileLock<T>(filePath: string, operation: () => Promise<T>, maxRetries?: number): Promise<T>;
|
|
2
3
|
export declare function mutateApprovalStateWithRetry<T>(params: {
|
|
3
4
|
load: () => Promise<{
|
|
4
5
|
filePath: string;
|
|
@@ -11,3 +12,21 @@ export declare function mutateApprovalStateWithRetry<T>(params: {
|
|
|
11
12
|
} | null;
|
|
12
13
|
maxRetries?: number;
|
|
13
14
|
}): Promise<T | null>;
|
|
15
|
+
export declare function mutatePendingAndApprovedWithRetry<T>(params: {
|
|
16
|
+
loadPending: () => Promise<{
|
|
17
|
+
filePath: string;
|
|
18
|
+
state: ApprovalStateFile;
|
|
19
|
+
}>;
|
|
20
|
+
loadApproved: () => Promise<{
|
|
21
|
+
filePath: string;
|
|
22
|
+
state: ApprovalStateFile;
|
|
23
|
+
}>;
|
|
24
|
+
writePending: (filePath: string, state: ApprovalStateFile) => Promise<void>;
|
|
25
|
+
writeApproved: (filePath: string, state: ApprovalStateFile) => Promise<void>;
|
|
26
|
+
mutate: (pending: ApprovalStateFile, approved: ApprovalStateFile) => {
|
|
27
|
+
pending: ApprovalStateFile;
|
|
28
|
+
approved: ApprovalStateFile;
|
|
29
|
+
result: T;
|
|
30
|
+
} | null;
|
|
31
|
+
maxRetries?: number;
|
|
32
|
+
}): Promise<T | null>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { constants } from 'node:fs';
|
|
2
|
-
import { open, readFile, unlink } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, open, readFile, unlink } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
import { upgradeApprovalStateToV3 } from './approval-v3.js';
|
|
4
5
|
const DEFAULT_RETRIES = 8;
|
|
5
6
|
const LOCK_STALE_MS = 5 * 60_000;
|
|
@@ -31,6 +32,7 @@ async function isStaleApprovalLock(lockPath) {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
async function acquireApprovalStateLock(lockPath, maxRetries) {
|
|
35
|
+
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
34
36
|
for (let attempt = 0; attempt < maxRetries; attempt += 1) {
|
|
35
37
|
try {
|
|
36
38
|
const handle = await open(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
@@ -53,14 +55,27 @@ async function acquireApprovalStateLock(lockPath, maxRetries) {
|
|
|
53
55
|
}
|
|
54
56
|
throw new Error(`Failed to acquire approval state lock: ${lockPath}`);
|
|
55
57
|
}
|
|
58
|
+
export async function withStateFileLock(filePath, operation, maxRetries = DEFAULT_RETRIES) {
|
|
59
|
+
const release = await acquireApprovalStateLock(`${filePath}.lock`, maxRetries);
|
|
60
|
+
try {
|
|
61
|
+
return await operation();
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
await release();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
56
67
|
export async function mutateApprovalStateWithRetry(params) {
|
|
57
68
|
const maxRetries = params.maxRetries ?? DEFAULT_RETRIES;
|
|
58
69
|
for (let attempt = 0; attempt < maxRetries; attempt += 1) {
|
|
59
|
-
const
|
|
60
|
-
const lockPath = `${
|
|
70
|
+
const initial = await params.load();
|
|
71
|
+
const lockPath = `${initial.filePath}.lock`;
|
|
61
72
|
let release = null;
|
|
62
73
|
try {
|
|
63
74
|
release = await acquireApprovalStateLock(lockPath, maxRetries);
|
|
75
|
+
const loaded = await params.load();
|
|
76
|
+
if (loaded.filePath !== initial.filePath) {
|
|
77
|
+
throw new Error('Approval state path changed while acquiring lock');
|
|
78
|
+
}
|
|
64
79
|
const base = upgradeApprovalStateToV3(loaded.state);
|
|
65
80
|
const revision = base.revision ?? 0;
|
|
66
81
|
const outcome = params.mutate(base);
|
|
@@ -90,3 +105,61 @@ export async function mutateApprovalStateWithRetry(params) {
|
|
|
90
105
|
}
|
|
91
106
|
return null;
|
|
92
107
|
}
|
|
108
|
+
export async function mutatePendingAndApprovedWithRetry(params) {
|
|
109
|
+
const maxRetries = params.maxRetries ?? DEFAULT_RETRIES;
|
|
110
|
+
const [initialPending, initialApproved] = await Promise.all([
|
|
111
|
+
params.loadPending(),
|
|
112
|
+
params.loadApproved(),
|
|
113
|
+
]);
|
|
114
|
+
const lockPaths = [`${initialPending.filePath}.lock`, `${initialApproved.filePath}.lock`].sort();
|
|
115
|
+
if (lockPaths[0] === lockPaths[1]) {
|
|
116
|
+
throw new Error('Pending and approved state must use different files');
|
|
117
|
+
}
|
|
118
|
+
const releases = [];
|
|
119
|
+
try {
|
|
120
|
+
for (const lockPath of lockPaths) {
|
|
121
|
+
releases.push(await acquireApprovalStateLock(lockPath, maxRetries));
|
|
122
|
+
}
|
|
123
|
+
const [pendingLoaded, approvedLoaded] = await Promise.all([
|
|
124
|
+
params.loadPending(),
|
|
125
|
+
params.loadApproved(),
|
|
126
|
+
]);
|
|
127
|
+
if (pendingLoaded.filePath !== initialPending.filePath ||
|
|
128
|
+
approvedLoaded.filePath !== initialApproved.filePath) {
|
|
129
|
+
throw new Error('Approval state paths changed while acquiring locks');
|
|
130
|
+
}
|
|
131
|
+
const pending = upgradeApprovalStateToV3(pendingLoaded.state);
|
|
132
|
+
const approved = upgradeApprovalStateToV3(approvedLoaded.state);
|
|
133
|
+
const outcome = params.mutate(pending, approved);
|
|
134
|
+
if (!outcome) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const nextApproved = {
|
|
138
|
+
...outcome.approved,
|
|
139
|
+
version: 3,
|
|
140
|
+
revision: (approved.revision ?? 0) + 1,
|
|
141
|
+
};
|
|
142
|
+
const nextPending = {
|
|
143
|
+
...outcome.pending,
|
|
144
|
+
version: 3,
|
|
145
|
+
revision: (pending.revision ?? 0) + 1,
|
|
146
|
+
};
|
|
147
|
+
// Claim first: interruption may require a fresh approval, but can never duplicate a grant.
|
|
148
|
+
await params.writePending(pendingLoaded.filePath, nextPending);
|
|
149
|
+
await params.writeApproved(approvedLoaded.filePath, nextApproved);
|
|
150
|
+
const [verifiedPending, verifiedApproved] = await Promise.all([
|
|
151
|
+
params.loadPending(),
|
|
152
|
+
params.loadApproved(),
|
|
153
|
+
]);
|
|
154
|
+
if ((upgradeApprovalStateToV3(verifiedPending.state).revision ?? 0) !== nextPending.revision ||
|
|
155
|
+
(upgradeApprovalStateToV3(verifiedApproved.state).revision ?? 0) !== nextApproved.revision) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return outcome.result;
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
for (const release of releases.reverse()) {
|
|
162
|
+
await release();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -2,14 +2,31 @@ import type { ApprovalRecord, ApprovalStateFile } from '../types.js';
|
|
|
2
2
|
import type { CapabilityGrantV1 } from './grant.js';
|
|
3
3
|
import type { CapabilityRequestV1 } from './request.js';
|
|
4
4
|
export declare const APPROVAL_STATE_VERSION_V3: 3;
|
|
5
|
+
export declare const EFFECT_PLAN_HASH_VERSION: "effect-plan:v1";
|
|
5
6
|
export declare function normalizeApprovalStateVersion(state: ApprovalStateFile): ApprovalStateFile;
|
|
6
7
|
export declare function upgradeApprovalStateToV3(state: ApprovalStateFile): ApprovalStateFile;
|
|
7
|
-
export declare function attachCapabilityEnvelope(approval: ApprovalRecord, capabilityRequests?: CapabilityRequestV1[]): ApprovalRecord;
|
|
8
|
+
export declare function attachCapabilityEnvelope(approval: ApprovalRecord, capabilityRequests?: CapabilityRequestV1[], effectPlanHash?: string): ApprovalRecord;
|
|
9
|
+
/** Dual-read: expose legacy single grant as a one-element bundle without mutating storage. */
|
|
10
|
+
export declare function normalizeApprovalGrants(approval: ApprovalRecord): ApprovalRecord;
|
|
11
|
+
export declare function mintCapabilityGrantForRequest(params: {
|
|
12
|
+
approval: ApprovalRecord;
|
|
13
|
+
request: CapabilityRequestV1;
|
|
14
|
+
grantSuffix?: string;
|
|
15
|
+
issuer?: string;
|
|
16
|
+
maxUses?: number;
|
|
17
|
+
}): CapabilityGrantV1;
|
|
18
|
+
/** @deprecated Use mintCapabilityGrantBundle; returns first grant only for legacy callers. */
|
|
8
19
|
export declare function mintCapabilityGrant(params: {
|
|
9
20
|
approval: ApprovalRecord;
|
|
10
21
|
capabilityRequests: CapabilityRequestV1[];
|
|
11
22
|
issuer?: string;
|
|
12
23
|
maxUses?: number;
|
|
13
24
|
}): CapabilityGrantV1 | null;
|
|
25
|
+
export declare function mintCapabilityGrantBundle(params: {
|
|
26
|
+
approval: ApprovalRecord;
|
|
27
|
+
capabilityRequests: CapabilityRequestV1[];
|
|
28
|
+
issuer?: string;
|
|
29
|
+
maxUses?: number;
|
|
30
|
+
}): CapabilityGrantV1[];
|
|
14
31
|
export declare function mintGrantForApprovedRecord(approval: ApprovalRecord): ApprovalRecord;
|
|
15
32
|
export declare function newGrantId(): string;
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { hashCapabilityRequests } from './capability-request-hash.js';
|
|
3
3
|
import { CAPABILITY_GRANT_VERSION } from './grant.js';
|
|
4
4
|
export const APPROVAL_STATE_VERSION_V3 = 3;
|
|
5
|
+
export const EFFECT_PLAN_HASH_VERSION = 'effect-plan:v1';
|
|
5
6
|
export function normalizeApprovalStateVersion(state) {
|
|
6
7
|
const version = state.version === APPROVAL_STATE_VERSION_V3
|
|
7
8
|
? APPROVAL_STATE_VERSION_V3
|
|
@@ -26,29 +27,45 @@ export function upgradeApprovalStateToV3(state) {
|
|
|
26
27
|
})),
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
|
-
export function attachCapabilityEnvelope(approval, capabilityRequests) {
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
export function attachCapabilityEnvelope(approval, capabilityRequests, effectPlanHash) {
|
|
31
|
+
const withRequests = !capabilityRequests?.length
|
|
32
|
+
? approval
|
|
33
|
+
: {
|
|
34
|
+
...approval,
|
|
35
|
+
capabilityRequests,
|
|
36
|
+
capabilityRequestHash: hashCapabilityRequests(capabilityRequests),
|
|
37
|
+
};
|
|
38
|
+
if (!effectPlanHash) {
|
|
39
|
+
return withRequests;
|
|
32
40
|
}
|
|
33
41
|
return {
|
|
34
|
-
...
|
|
35
|
-
|
|
36
|
-
capabilityRequestHash: hashCapabilityRequests(capabilityRequests),
|
|
42
|
+
...withRequests,
|
|
43
|
+
effectPlanHash,
|
|
37
44
|
};
|
|
38
45
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
return
|
|
46
|
+
/** Dual-read: expose legacy single grant as a one-element bundle without mutating storage. */
|
|
47
|
+
export function normalizeApprovalGrants(approval) {
|
|
48
|
+
if (approval.grants?.length) {
|
|
49
|
+
return approval;
|
|
43
50
|
}
|
|
51
|
+
if (approval.grant) {
|
|
52
|
+
return { ...approval, grants: [approval.grant] };
|
|
53
|
+
}
|
|
54
|
+
return approval;
|
|
55
|
+
}
|
|
56
|
+
export function mintCapabilityGrantForRequest(params) {
|
|
44
57
|
const expiresAt = params.approval.expiresAt;
|
|
58
|
+
const suffix = params.grantSuffix ?? '';
|
|
59
|
+
const grantId = suffix === '' || suffix === '0'
|
|
60
|
+
? `grant_${params.approval.approvalId}${suffix === '0' && params.grantSuffix !== undefined ? '_0' : ''}`
|
|
61
|
+
: `grant_${params.approval.approvalId}_${suffix}`;
|
|
45
62
|
return {
|
|
46
63
|
version: CAPABILITY_GRANT_VERSION,
|
|
47
|
-
grantId
|
|
48
|
-
principal: request.principal,
|
|
49
|
-
action: request.action,
|
|
50
|
-
resource: request.resource,
|
|
51
|
-
inputFingerprint: request.context.inputFingerprint,
|
|
64
|
+
grantId,
|
|
65
|
+
principal: params.request.principal,
|
|
66
|
+
action: params.request.action,
|
|
67
|
+
resource: params.request.resource,
|
|
68
|
+
inputFingerprint: params.request.context.inputFingerprint,
|
|
52
69
|
issuedAt: params.approval.approvedAt ?? new Date().toISOString(),
|
|
53
70
|
expiresAt,
|
|
54
71
|
maxUses: params.maxUses ?? 1,
|
|
@@ -56,18 +73,41 @@ export function mintCapabilityGrant(params) {
|
|
|
56
73
|
issuer: params.issuer ?? 'belay-approval-v3',
|
|
57
74
|
};
|
|
58
75
|
}
|
|
76
|
+
/** @deprecated Use mintCapabilityGrantBundle; returns first grant only for legacy callers. */
|
|
77
|
+
export function mintCapabilityGrant(params) {
|
|
78
|
+
const bundle = mintCapabilityGrantBundle(params);
|
|
79
|
+
return bundle[0] ?? null;
|
|
80
|
+
}
|
|
81
|
+
export function mintCapabilityGrantBundle(params) {
|
|
82
|
+
if (!params.capabilityRequests.length) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
return params.capabilityRequests.map((request, index) => mintCapabilityGrantForRequest({
|
|
86
|
+
approval: params.approval,
|
|
87
|
+
request,
|
|
88
|
+
grantSuffix: params.capabilityRequests.length === 1 ? undefined : String(index),
|
|
89
|
+
issuer: params.issuer,
|
|
90
|
+
maxUses: params.maxUses,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
59
93
|
export function mintGrantForApprovedRecord(approval) {
|
|
60
|
-
|
|
61
|
-
|
|
94
|
+
const normalized = normalizeApprovalGrants(approval);
|
|
95
|
+
if ((normalized.grants?.length ?? 0) > 0 || !normalized.capabilityRequests?.length) {
|
|
96
|
+
return normalized;
|
|
62
97
|
}
|
|
63
|
-
const
|
|
64
|
-
approval,
|
|
65
|
-
capabilityRequests:
|
|
98
|
+
const grants = mintCapabilityGrantBundle({
|
|
99
|
+
approval: normalized,
|
|
100
|
+
capabilityRequests: normalized.capabilityRequests,
|
|
66
101
|
});
|
|
67
|
-
if (!
|
|
68
|
-
return
|
|
102
|
+
if (!grants.length) {
|
|
103
|
+
return normalized;
|
|
69
104
|
}
|
|
70
|
-
return {
|
|
105
|
+
return {
|
|
106
|
+
...normalized,
|
|
107
|
+
grants,
|
|
108
|
+
grant: grants[0],
|
|
109
|
+
grantBundleVersion: 1,
|
|
110
|
+
};
|
|
71
111
|
}
|
|
72
112
|
export function newGrantId() {
|
|
73
113
|
return `grant_${randomUUID().replaceAll('-', '').slice(0, 16)}`;
|
|
@@ -8,6 +8,9 @@ export interface BoundaryAttestation {
|
|
|
8
8
|
deniesUngrantedEffects: boolean;
|
|
9
9
|
materializesGrants: boolean;
|
|
10
10
|
probeSignals: string[];
|
|
11
|
+
/** When true, the driver can mount an execution mirror at the original workspace path. */
|
|
12
|
+
isolatesWorkspaceMounts?: boolean;
|
|
11
13
|
}
|
|
12
14
|
export declare function validateBoundaryAttestation(value: unknown): value is BoundaryAttestation;
|
|
13
15
|
export declare function isAttestationFresh(attestation: BoundaryAttestation, now?: number): boolean;
|
|
16
|
+
export declare function attestsWorkspaceMountIsolation(attestation: BoundaryAttestation | null | undefined): boolean;
|
|
@@ -30,6 +30,10 @@ export function validateBoundaryAttestation(value) {
|
|
|
30
30
|
!record.probeSignals.every((s) => typeof s === 'string')) {
|
|
31
31
|
return false;
|
|
32
32
|
}
|
|
33
|
+
if (record.isolatesWorkspaceMounts !== undefined &&
|
|
34
|
+
typeof record.isolatesWorkspaceMounts !== 'boolean') {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
33
37
|
if (record.driver === 'host-integration' && record.materializesGrants) {
|
|
34
38
|
return false;
|
|
35
39
|
}
|
|
@@ -53,3 +57,6 @@ export function isAttestationFresh(attestation, now = Date.now()) {
|
|
|
53
57
|
}
|
|
54
58
|
return attestation.deniesUngrantedEffects;
|
|
55
59
|
}
|
|
60
|
+
export function attestsWorkspaceMountIsolation(attestation) {
|
|
61
|
+
return attestation?.isolatesWorkspaceMounts === true;
|
|
62
|
+
}
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { BoundaryDriver } from './boundary-driver.js';
|
|
2
|
+
import { type BoundaryRunOptions } from './boundary-run.js';
|
|
2
3
|
export declare function isDockerAvailable(): Promise<boolean>;
|
|
4
|
+
export interface ContainerRunParams {
|
|
5
|
+
image: string;
|
|
6
|
+
command: string;
|
|
7
|
+
cwd: string;
|
|
8
|
+
proxyEnv: Record<string, string>;
|
|
9
|
+
mountReadOnly: boolean;
|
|
10
|
+
repoRoot?: string;
|
|
11
|
+
runOptions?: BoundaryRunOptions;
|
|
12
|
+
containerName?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function buildContainerRunArgs(params: ContainerRunParams): string[];
|
|
3
15
|
export declare function createContainerBoundaryDriver(options?: {
|
|
4
16
|
image?: string;
|
|
5
17
|
egressProxyEnv?: Record<string, string>;
|