@guilz-dev/belay 0.4.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -2
- package/dist/adapters/claude/runtime-entry.js +54 -25
- package/dist/adapters/layouts/scope.js +3 -2
- package/dist/adapters/shared/gate-runtime.d.ts +8 -2
- package/dist/adapters/shared/gate-runtime.js +579 -181
- package/dist/bundle/claude-runtime.mjs +13574 -6334
- package/dist/bundle/codex-runtime.mjs +13108 -5903
- package/dist/bundle/cursor-runtime.mjs +12676 -5476
- package/dist/cli.js +80 -2
- package/dist/commands/approve.js +34 -10
- package/dist/commands/classify-for-report.js +1 -1
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +134 -11
- package/dist/commands/doctor.js +87 -1
- package/dist/commands/explain.js +11 -1
- package/dist/commands/judge.js +3 -2
- 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/session.d.ts +20 -0
- package/dist/commands/session.js +51 -0
- package/dist/commands/status.js +7 -0
- package/dist/config-io.js +91 -23
- package/dist/conformance/guarantee-posture.d.ts +21 -0
- package/dist/conformance/guarantee-posture.js +46 -0
- package/dist/conformance/guarantee-table.d.ts +1 -0
- package/dist/conformance/guarantee-table.js +20 -9
- 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 +87 -24
- package/dist/core/approval.d.ts +4 -0
- package/dist/core/approval.js +19 -5
- package/dist/core/audit-types.d.ts +13 -0
- package/dist/core/capability/approval-state-mutation.d.ts +32 -0
- package/dist/core/capability/approval-state-mutation.js +165 -0
- package/dist/core/capability/approval-v3.d.ts +32 -0
- package/dist/core/capability/approval-v3.js +114 -0
- package/dist/core/capability/attestation.d.ts +16 -0
- package/dist/core/capability/attestation.js +62 -0
- package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
- package/dist/core/capability/boundary-attestation-sign.js +84 -0
- package/dist/core/capability/boundary-driver-container.d.ts +19 -0
- package/dist/core/capability/boundary-driver-container.js +189 -0
- package/dist/core/capability/boundary-driver.d.ts +29 -0
- package/dist/core/capability/boundary-driver.js +44 -0
- package/dist/core/capability/boundary-egress.d.ts +21 -0
- package/dist/core/capability/boundary-egress.js +109 -0
- package/dist/core/capability/boundary-grant-materialize.d.ts +14 -0
- package/dist/core/capability/boundary-grant-materialize.js +93 -0
- package/dist/core/capability/boundary-profile.d.ts +15 -0
- package/dist/core/capability/boundary-profile.js +25 -0
- package/dist/core/capability/boundary-run.d.ts +48 -0
- package/dist/core/capability/boundary-run.js +51 -0
- package/dist/core/capability/boundary-session.d.ts +61 -0
- package/dist/core/capability/boundary-session.js +138 -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/capability-request-hash.d.ts +2 -0
- package/dist/core/capability/capability-request-hash.js +8 -0
- package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
- package/dist/core/capability/gate-latency-ratchet.js +18 -0
- package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
- package/dist/core/capability/gate-policy-shadow.js +130 -0
- package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
- package/dist/core/capability/gate-shadow-audit.js +1 -0
- package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
- package/dist/core/capability/gate-shadow-ratchet.js +81 -0
- 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 +44 -0
- package/dist/core/capability/grant-lease.js +243 -0
- package/dist/core/capability/grant-loader.d.ts +12 -0
- package/dist/core/capability/grant-loader.js +23 -0
- package/dist/core/capability/grant-match.d.ts +6 -0
- package/dist/core/capability/grant-match.js +96 -0
- package/dist/core/capability/grant.d.ts +16 -0
- package/dist/core/capability/grant.js +16 -0
- package/dist/core/capability/index.d.ts +8 -0
- package/dist/core/capability/index.js +7 -0
- package/dist/core/capability/limits.d.ts +7 -0
- package/dist/core/capability/limits.js +55 -0
- package/dist/core/capability/policy-bridge.d.ts +18 -0
- package/dist/core/capability/policy-bridge.js +51 -0
- package/dist/core/capability/policy-engine.d.ts +77 -0
- package/dist/core/capability/policy-engine.js +663 -0
- package/dist/core/capability/policy-types.d.ts +20 -0
- package/dist/core/capability/policy-types.js +1 -0
- package/dist/core/capability/request.d.ts +48 -0
- package/dist/core/capability/request.js +1 -0
- package/dist/core/capability/resolver.d.ts +4 -0
- package/dist/core/capability/resolver.js +4 -0
- package/dist/core/classify-subagent.d.ts +2 -1
- package/dist/core/classify-subagent.js +38 -2
- package/dist/core/classify-tool.js +126 -62
- package/dist/core/config.d.ts +34 -3
- package/dist/core/config.js +118 -3
- 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 +5 -0
- package/dist/core/gate-contract.js +5 -0
- package/dist/core/gate-engine.js +30 -8
- 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/judge-doctor.js +39 -14
- package/dist/core/judge-fallback-hints.d.ts +13 -0
- package/dist/core/judge-fallback-hints.js +108 -0
- package/dist/core/network-endpoint.d.ts +10 -0
- package/dist/core/network-endpoint.js +51 -0
- package/dist/core/path-utils.d.ts +1 -0
- package/dist/core/path-utils.js +5 -2
- 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/capability.d.ts +3 -0
- package/dist/core/recovery/capability.js +22 -0
- package/dist/core/recovery/checkpoint.d.ts +25 -0
- package/dist/core/recovery/checkpoint.js +260 -0
- package/dist/core/recovery/fail-closed.d.ts +6 -0
- package/dist/core/recovery/fail-closed.js +25 -0
- package/dist/core/recovery/index.d.ts +5 -0
- package/dist/core/recovery/index.js +4 -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 +104 -0
- package/dist/core/recovery/types.js +1 -0
- package/dist/core/standing-allow.js +3 -0
- 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/diff-evaluator.js +4 -1
- package/dist/core/transactional/eligibility.js +4 -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 +14 -9
- package/dist/core/transactional/git-worktree.js +62 -87
- package/dist/core/transactional/index.d.ts +11 -0
- package/dist/core/transactional/index.js +10 -0
- package/dist/core/transactional/reasons.d.ts +1 -0
- package/dist/core/transactional/reasons.js +5 -0
- package/dist/core/transactional/runner.js +171 -23
- 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 +21 -1
- package/dist/core/types.d.ts +30 -3
- package/dist/core/verdict/adapter.d.ts +3 -4
- package/dist/core/verdict/adapter.js +24 -12
- package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
- package/dist/core/verdict/cursor-acp-client.js +345 -0
- package/dist/core/verdict/egress-classify.d.ts +2 -2
- package/dist/core/verdict/egress-classify.js +2 -2
- package/dist/core/verdict/git-classifier.d.ts +26 -0
- package/dist/core/verdict/git-classifier.js +533 -0
- package/dist/core/verdict/judge-audit.js +1 -0
- package/dist/core/verdict/judge-baseline.d.ts +1 -1
- package/dist/core/verdict/judge-baseline.js +1 -0
- package/dist/core/verdict/judge-broker-service.js +12 -0
- package/dist/core/verdict/judge-catalog.js +1 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +1 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +6 -0
- package/dist/core/verdict/judge-cli.js +5 -3
- package/dist/core/verdict/judge-provider-matrix.js +1 -1
- package/dist/core/verdict/judge-runtime-config.d.ts +2 -0
- package/dist/core/verdict/judge-runtime-config.js +10 -6
- package/dist/core/verdict/judge-session-broker.d.ts +1 -1
- package/dist/core/verdict/judge-session-broker.js +10 -5
- package/dist/core/verdict/judge-transport.d.ts +5 -1
- package/dist/core/verdict/judge-transport.js +24 -14
- package/dist/core/verdict/judge.d.ts +2 -15
- package/dist/core/verdict/judge.js +5 -68
- package/dist/core/verdict/launcher-resolve.d.ts +2 -0
- package/dist/core/verdict/launcher-resolve.js +14 -16
- package/dist/core/verdict/prescan.d.ts +15 -0
- package/dist/core/verdict/prescan.js +68 -0
- package/dist/core/verdict/shell-policy.d.ts +26 -0
- package/dist/core/verdict/shell-policy.js +40 -0
- package/dist/core/verdict/shell-semantics.d.ts +24 -0
- package/dist/core/verdict/shell-semantics.js +225 -0
- package/dist/core/verdict/types.d.ts +18 -1
- package/dist/core/verdict/verdict.js +313 -178
- package/dist/corpus/evaluate.js +3 -7
- package/dist/corpus/gate-latency-budget.d.ts +39 -0
- package/dist/corpus/gate-latency-budget.js +33 -0
- package/dist/corpus/mutators.d.ts +1 -1
- package/dist/corpus/mutators.js +11 -0
- package/dist/corpus/runtime-match.js +1 -3
- package/dist/corpus/standing-allow-catalog.generated.js +29 -9
- package/dist/judge-broker-daemon.js +15 -1
- package/dist/services/sandbox-service.d.ts +4 -0
- package/dist/services/sandbox-service.js +28 -2
- package/dist/types.d.ts +13 -2
- 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
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { mkdir, open, readFile, unlink } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { upgradeApprovalStateToV3 } from './approval-v3.js';
|
|
5
|
+
const DEFAULT_RETRIES = 8;
|
|
6
|
+
const LOCK_STALE_MS = 5 * 60_000;
|
|
7
|
+
async function sleep(ms) {
|
|
8
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
async function isStaleApprovalLock(lockPath) {
|
|
11
|
+
try {
|
|
12
|
+
const raw = await readFile(lockPath, 'utf8');
|
|
13
|
+
const [pidText, timestampText] = raw.trim().split(':');
|
|
14
|
+
const timestamp = Number(timestampText);
|
|
15
|
+
if (Number.isFinite(timestamp) && Date.now() - timestamp > LOCK_STALE_MS) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
const pid = Number(pidText);
|
|
19
|
+
if (!Number.isFinite(pid)) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
process.kill(pid, 0);
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function acquireApprovalStateLock(lockPath, maxRetries) {
|
|
35
|
+
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
36
|
+
for (let attempt = 0; attempt < maxRetries; attempt += 1) {
|
|
37
|
+
try {
|
|
38
|
+
const handle = await open(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
39
|
+
await handle.writeFile(`${process.pid}:${Date.now()}`);
|
|
40
|
+
await handle.close();
|
|
41
|
+
return async () => {
|
|
42
|
+
await unlink(lockPath).catch(() => { });
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const code = error.code;
|
|
47
|
+
if (code !== 'EEXIST') {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
if (await isStaleApprovalLock(lockPath)) {
|
|
51
|
+
await unlink(lockPath).catch(() => { });
|
|
52
|
+
}
|
|
53
|
+
await sleep(5 + attempt * 8);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`Failed to acquire approval state lock: ${lockPath}`);
|
|
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
|
+
}
|
|
67
|
+
export async function mutateApprovalStateWithRetry(params) {
|
|
68
|
+
const maxRetries = params.maxRetries ?? DEFAULT_RETRIES;
|
|
69
|
+
for (let attempt = 0; attempt < maxRetries; attempt += 1) {
|
|
70
|
+
const initial = await params.load();
|
|
71
|
+
const lockPath = `${initial.filePath}.lock`;
|
|
72
|
+
let release = null;
|
|
73
|
+
try {
|
|
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
|
+
}
|
|
79
|
+
const base = upgradeApprovalStateToV3(loaded.state);
|
|
80
|
+
const revision = base.revision ?? 0;
|
|
81
|
+
const outcome = params.mutate(base);
|
|
82
|
+
if (!outcome) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const next = {
|
|
86
|
+
...outcome.state,
|
|
87
|
+
version: 3,
|
|
88
|
+
revision: revision + 1,
|
|
89
|
+
};
|
|
90
|
+
const verifyBeforeWrite = upgradeApprovalStateToV3((await params.load()).state);
|
|
91
|
+
if ((verifyBeforeWrite.revision ?? 0) !== revision) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
await params.write(loaded.filePath, next);
|
|
95
|
+
const verify = upgradeApprovalStateToV3((await params.load()).state);
|
|
96
|
+
if ((verify.revision ?? 0) === revision + 1) {
|
|
97
|
+
return outcome.result;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (release) {
|
|
102
|
+
await release();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
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
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ApprovalRecord, ApprovalStateFile } from '../types.js';
|
|
2
|
+
import type { CapabilityGrantV1 } from './grant.js';
|
|
3
|
+
import type { CapabilityRequestV1 } from './request.js';
|
|
4
|
+
export declare const APPROVAL_STATE_VERSION_V3: 3;
|
|
5
|
+
export declare const EFFECT_PLAN_HASH_VERSION: "effect-plan:v1";
|
|
6
|
+
export declare function normalizeApprovalStateVersion(state: ApprovalStateFile): ApprovalStateFile;
|
|
7
|
+
export declare function upgradeApprovalStateToV3(state: ApprovalStateFile): ApprovalStateFile;
|
|
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. */
|
|
19
|
+
export declare function mintCapabilityGrant(params: {
|
|
20
|
+
approval: ApprovalRecord;
|
|
21
|
+
capabilityRequests: CapabilityRequestV1[];
|
|
22
|
+
issuer?: string;
|
|
23
|
+
maxUses?: number;
|
|
24
|
+
}): CapabilityGrantV1 | null;
|
|
25
|
+
export declare function mintCapabilityGrantBundle(params: {
|
|
26
|
+
approval: ApprovalRecord;
|
|
27
|
+
capabilityRequests: CapabilityRequestV1[];
|
|
28
|
+
issuer?: string;
|
|
29
|
+
maxUses?: number;
|
|
30
|
+
}): CapabilityGrantV1[];
|
|
31
|
+
export declare function mintGrantForApprovedRecord(approval: ApprovalRecord): ApprovalRecord;
|
|
32
|
+
export declare function newGrantId(): string;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { hashCapabilityRequests } from './capability-request-hash.js';
|
|
3
|
+
import { CAPABILITY_GRANT_VERSION } from './grant.js';
|
|
4
|
+
export const APPROVAL_STATE_VERSION_V3 = 3;
|
|
5
|
+
export const EFFECT_PLAN_HASH_VERSION = 'effect-plan:v1';
|
|
6
|
+
export function normalizeApprovalStateVersion(state) {
|
|
7
|
+
const version = state.version === APPROVAL_STATE_VERSION_V3
|
|
8
|
+
? APPROVAL_STATE_VERSION_V3
|
|
9
|
+
: state.version === 2
|
|
10
|
+
? 2
|
|
11
|
+
: 1;
|
|
12
|
+
return { ...state, version };
|
|
13
|
+
}
|
|
14
|
+
export function upgradeApprovalStateToV3(state) {
|
|
15
|
+
const normalized = normalizeApprovalStateVersion(state);
|
|
16
|
+
if (normalized.version === APPROVAL_STATE_VERSION_V3) {
|
|
17
|
+
return normalized;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
version: APPROVAL_STATE_VERSION_V3,
|
|
21
|
+
approvals: normalized.approvals.map((approval) => ({
|
|
22
|
+
...approval,
|
|
23
|
+
capabilityRequestHash: approval.capabilityRequestHash ??
|
|
24
|
+
(approval.capabilityRequests?.length
|
|
25
|
+
? hashCapabilityRequests(approval.capabilityRequests)
|
|
26
|
+
: undefined),
|
|
27
|
+
})),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
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;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
...withRequests,
|
|
43
|
+
effectPlanHash,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
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;
|
|
50
|
+
}
|
|
51
|
+
if (approval.grant) {
|
|
52
|
+
return { ...approval, grants: [approval.grant] };
|
|
53
|
+
}
|
|
54
|
+
return approval;
|
|
55
|
+
}
|
|
56
|
+
export function mintCapabilityGrantForRequest(params) {
|
|
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}`;
|
|
62
|
+
return {
|
|
63
|
+
version: CAPABILITY_GRANT_VERSION,
|
|
64
|
+
grantId,
|
|
65
|
+
principal: params.request.principal,
|
|
66
|
+
action: params.request.action,
|
|
67
|
+
resource: params.request.resource,
|
|
68
|
+
inputFingerprint: params.request.context.inputFingerprint,
|
|
69
|
+
issuedAt: params.approval.approvedAt ?? new Date().toISOString(),
|
|
70
|
+
expiresAt,
|
|
71
|
+
maxUses: params.maxUses ?? 1,
|
|
72
|
+
usesRemaining: params.maxUses ?? 1,
|
|
73
|
+
issuer: params.issuer ?? 'belay-approval-v3',
|
|
74
|
+
};
|
|
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
|
+
}
|
|
93
|
+
export function mintGrantForApprovedRecord(approval) {
|
|
94
|
+
const normalized = normalizeApprovalGrants(approval);
|
|
95
|
+
if ((normalized.grants?.length ?? 0) > 0 || !normalized.capabilityRequests?.length) {
|
|
96
|
+
return normalized;
|
|
97
|
+
}
|
|
98
|
+
const grants = mintCapabilityGrantBundle({
|
|
99
|
+
approval: normalized,
|
|
100
|
+
capabilityRequests: normalized.capabilityRequests,
|
|
101
|
+
});
|
|
102
|
+
if (!grants.length) {
|
|
103
|
+
return normalized;
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
...normalized,
|
|
107
|
+
grants,
|
|
108
|
+
grant: grants[0],
|
|
109
|
+
grantBundleVersion: 1,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function newGrantId() {
|
|
113
|
+
return `grant_${randomUUID().replaceAll('-', '').slice(0, 16)}`;
|
|
114
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const BOUNDARY_ATTESTATION_VERSION: 1;
|
|
2
|
+
export type BoundaryDriverId = 'container' | 'cursor-sandbox' | 'seatbelt' | 'landlock' | 'host-integration';
|
|
3
|
+
export interface BoundaryAttestation {
|
|
4
|
+
version: typeof BOUNDARY_ATTESTATION_VERSION;
|
|
5
|
+
driver: BoundaryDriverId;
|
|
6
|
+
probedAt: string;
|
|
7
|
+
expiresAt: string;
|
|
8
|
+
deniesUngrantedEffects: boolean;
|
|
9
|
+
materializesGrants: boolean;
|
|
10
|
+
probeSignals: string[];
|
|
11
|
+
/** When true, the driver can mount an execution mirror at the original workspace path. */
|
|
12
|
+
isolatesWorkspaceMounts?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function validateBoundaryAttestation(value: unknown): value is BoundaryAttestation;
|
|
15
|
+
export declare function isAttestationFresh(attestation: BoundaryAttestation, now?: number): boolean;
|
|
16
|
+
export declare function attestsWorkspaceMountIsolation(attestation: BoundaryAttestation | null | undefined): boolean;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const BOUNDARY_ATTESTATION_VERSION = 1;
|
|
2
|
+
const KNOWN_DRIVERS = new Set([
|
|
3
|
+
'container',
|
|
4
|
+
'cursor-sandbox',
|
|
5
|
+
'seatbelt',
|
|
6
|
+
'landlock',
|
|
7
|
+
'host-integration',
|
|
8
|
+
]);
|
|
9
|
+
export function validateBoundaryAttestation(value) {
|
|
10
|
+
if (!value || typeof value !== 'object') {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const record = value;
|
|
14
|
+
if (record.version !== BOUNDARY_ATTESTATION_VERSION) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (!KNOWN_DRIVERS.has(record.driver)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (typeof record.probedAt !== 'string' || typeof record.expiresAt !== 'string') {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (typeof record.deniesUngrantedEffects !== 'boolean') {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (typeof record.materializesGrants !== 'boolean') {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
if (!Array.isArray(record.probeSignals) ||
|
|
30
|
+
!record.probeSignals.every((s) => typeof s === 'string')) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (record.isolatesWorkspaceMounts !== undefined &&
|
|
34
|
+
typeof record.isolatesWorkspaceMounts !== 'boolean') {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (record.driver === 'host-integration' && record.materializesGrants) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (record.driver === 'container' &&
|
|
41
|
+
record.materializesGrants &&
|
|
42
|
+
!record.deniesUngrantedEffects) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
export function isAttestationFresh(attestation, now = Date.now()) {
|
|
48
|
+
const expires = Date.parse(attestation.expiresAt);
|
|
49
|
+
if (!Number.isFinite(expires)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (expires <= now) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (attestation.driver === 'host-integration') {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return attestation.deniesUngrantedEffects;
|
|
59
|
+
}
|
|
60
|
+
export function attestsWorkspaceMountIsolation(attestation) {
|
|
61
|
+
return attestation?.isolatesWorkspaceMounts === true;
|
|
62
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BoundaryAttestation } from './attestation.js';
|
|
2
|
+
export type BoundaryAttestationFileFormat = 'signed' | 'legacy' | 'missing' | 'invalid';
|
|
3
|
+
export interface SignedBoundaryAttestationFile {
|
|
4
|
+
version: 1;
|
|
5
|
+
repoRoot: string;
|
|
6
|
+
attestation: BoundaryAttestation;
|
|
7
|
+
signature: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function signBoundaryAttestation(params: {
|
|
10
|
+
repoRoot: string;
|
|
11
|
+
attestation: BoundaryAttestation;
|
|
12
|
+
controlPlaneDir: string;
|
|
13
|
+
}): Promise<SignedBoundaryAttestationFile>;
|
|
14
|
+
export declare function verifySignedBoundaryAttestation(params: {
|
|
15
|
+
file: unknown;
|
|
16
|
+
expectedRepoRoot: string;
|
|
17
|
+
controlPlaneDir: string;
|
|
18
|
+
}): Promise<BoundaryAttestation | null>;
|
|
19
|
+
export declare function readSignedAttestationFile(filePath: string): Promise<unknown>;
|
|
20
|
+
export declare function isLegacyAttestationFormat(file: unknown): file is BoundaryAttestation;
|
|
21
|
+
export declare function inspectBoundaryAttestationFile(filePath: string): Promise<BoundaryAttestationFileFormat>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { access, readFile } from 'node:fs/promises';
|
|
3
|
+
import { loadOrCreateApprovalSigningKey } from '../approval-token.js';
|
|
4
|
+
import { canonicalStringify } from '../fingerprint.js';
|
|
5
|
+
import { validateBoundaryAttestation } from './attestation.js';
|
|
6
|
+
function signBody(repoRoot, attestation, key) {
|
|
7
|
+
const payload = canonicalStringify({ repoRoot, attestation });
|
|
8
|
+
return createHmac('sha256', key).update(payload).digest('base64url');
|
|
9
|
+
}
|
|
10
|
+
export async function signBoundaryAttestation(params) {
|
|
11
|
+
const key = await loadOrCreateApprovalSigningKey(params.controlPlaneDir);
|
|
12
|
+
const signature = signBody(params.repoRoot, params.attestation, key);
|
|
13
|
+
return {
|
|
14
|
+
version: 1,
|
|
15
|
+
repoRoot: params.repoRoot,
|
|
16
|
+
attestation: params.attestation,
|
|
17
|
+
signature,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export async function verifySignedBoundaryAttestation(params) {
|
|
21
|
+
if (!params.file || typeof params.file !== 'object') {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const record = params.file;
|
|
25
|
+
if (record.version !== 1 || typeof record.repoRoot !== 'string' || !record.attestation) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (record.repoRoot !== params.expectedRepoRoot) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (!validateBoundaryAttestation(record.attestation)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (typeof record.signature !== 'string' || !record.signature) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const key = await loadOrCreateApprovalSigningKey(params.controlPlaneDir);
|
|
38
|
+
const expected = signBody(record.repoRoot, record.attestation, key);
|
|
39
|
+
const actualBuffer = Buffer.from(record.signature);
|
|
40
|
+
const expectedBuffer = Buffer.from(expected);
|
|
41
|
+
if (actualBuffer.length !== expectedBuffer.length ||
|
|
42
|
+
!timingSafeEqual(actualBuffer, expectedBuffer)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return record.attestation;
|
|
46
|
+
}
|
|
47
|
+
export async function readSignedAttestationFile(filePath) {
|
|
48
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
49
|
+
}
|
|
50
|
+
export function isLegacyAttestationFormat(file) {
|
|
51
|
+
if (!file || typeof file !== 'object') {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const record = file;
|
|
55
|
+
return (record.version === 1 &&
|
|
56
|
+
typeof record.driver === 'string' &&
|
|
57
|
+
!('signature' in record) &&
|
|
58
|
+
!('attestation' in record));
|
|
59
|
+
}
|
|
60
|
+
export async function inspectBoundaryAttestationFile(filePath) {
|
|
61
|
+
try {
|
|
62
|
+
await access(filePath);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return 'missing';
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const raw = await readSignedAttestationFile(filePath);
|
|
69
|
+
if (isLegacyAttestationFormat(raw)) {
|
|
70
|
+
return validateBoundaryAttestation(raw) ? 'legacy' : 'invalid';
|
|
71
|
+
}
|
|
72
|
+
const record = raw;
|
|
73
|
+
if (record.version === 1 &&
|
|
74
|
+
typeof record.repoRoot === 'string' &&
|
|
75
|
+
record.attestation &&
|
|
76
|
+
typeof record.signature === 'string') {
|
|
77
|
+
return 'signed';
|
|
78
|
+
}
|
|
79
|
+
return 'invalid';
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return 'invalid';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BoundaryDriver } from './boundary-driver.js';
|
|
2
|
+
import { type BoundaryRunOptions } from './boundary-run.js';
|
|
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[];
|
|
15
|
+
export declare function createContainerBoundaryDriver(options?: {
|
|
16
|
+
image?: string;
|
|
17
|
+
egressProxyEnv?: Record<string, string>;
|
|
18
|
+
repoRoot?: string;
|
|
19
|
+
}): BoundaryDriver;
|