@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,23 +1,15 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { compactApprovals, createApprovalRecord } from './approval.js';
|
|
3
3
|
import { issueApprovalToken } from './approval-token.js';
|
|
4
|
+
import { mutateApprovalStateWithRetry } from './capability/approval-state-mutation.js';
|
|
4
5
|
import { configuredControlPlaneDir } from './config.js';
|
|
5
|
-
import { addDomainToAllowlist,
|
|
6
|
+
import { addDomainToAllowlist, mutateEgressAllowlist } from './egress/allowlist.js';
|
|
6
7
|
import { parseHostFromSummary } from './egress/fingerprint.js';
|
|
7
8
|
import { notifyDeny } from './notify.js';
|
|
8
9
|
export async function ensurePendingEgressApproval(params) {
|
|
9
10
|
const { config, repoRoot, policyResult, store } = params;
|
|
10
|
-
const pending = await store.loadPending();
|
|
11
|
-
pending.state = compactApprovals(pending.state);
|
|
12
|
-
const existing = pending.state.approvals.find((approval) => approval.kind === 'egress' &&
|
|
13
|
-
approval.fingerprint === policyResult.fingerprint &&
|
|
14
|
-
approval.repoRoot === repoRoot);
|
|
15
|
-
if (existing) {
|
|
16
|
-
await store.writePending(pending.filePath, pending.state);
|
|
17
|
-
return { approvalId: existing.approvalId, approval: existing, created: false };
|
|
18
|
-
}
|
|
19
11
|
const approvalId = `belay_${randomUUID().replaceAll('-', '').slice(0, 12)}`;
|
|
20
|
-
const
|
|
12
|
+
const candidate = createApprovalRecord({
|
|
21
13
|
kind: 'egress',
|
|
22
14
|
fingerprint: policyResult.fingerprint,
|
|
23
15
|
repoRoot,
|
|
@@ -26,44 +18,47 @@ export async function ensurePendingEgressApproval(params) {
|
|
|
26
18
|
approvalTtlMinutes: config.approvalTtlMinutes,
|
|
27
19
|
approvalId,
|
|
28
20
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
const outcome = await mutateApprovalStateWithRetry({
|
|
22
|
+
load: store.loadPending,
|
|
23
|
+
write: store.writePending,
|
|
24
|
+
mutate: (state) => {
|
|
25
|
+
const compacted = compactApprovals(state);
|
|
26
|
+
const existing = compacted.approvals.find((approval) => approval.kind === 'egress' &&
|
|
27
|
+
approval.fingerprint === policyResult.fingerprint &&
|
|
28
|
+
approval.repoRoot === repoRoot);
|
|
29
|
+
if (existing) {
|
|
30
|
+
return {
|
|
31
|
+
state: compacted,
|
|
32
|
+
result: { approvalId: existing.approvalId, approval: existing, created: false },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
compacted.approvals.push(candidate);
|
|
36
|
+
return {
|
|
37
|
+
state: compacted,
|
|
38
|
+
result: { approvalId, approval: candidate, created: true },
|
|
39
|
+
};
|
|
40
|
+
},
|
|
39
41
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
return await fn();
|
|
44
|
-
}
|
|
45
|
-
finally {
|
|
46
|
-
release();
|
|
47
|
-
if (consumeLocks.get(key) === gate) {
|
|
48
|
-
consumeLocks.delete(key);
|
|
49
|
-
}
|
|
42
|
+
if (!outcome) {
|
|
43
|
+
throw new Error('Failed to persist pending egress approval');
|
|
50
44
|
}
|
|
45
|
+
return outcome;
|
|
51
46
|
}
|
|
52
47
|
export async function consumeApprovedEgress(params) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
approval.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
48
|
+
return mutateApprovalStateWithRetry({
|
|
49
|
+
load: params.store.loadApproved,
|
|
50
|
+
write: params.store.writeApproved,
|
|
51
|
+
mutate: (state) => {
|
|
52
|
+
const compacted = compactApprovals(state);
|
|
53
|
+
const index = compacted.approvals.findIndex((approval) => approval.kind === 'egress' &&
|
|
54
|
+
approval.fingerprint === params.fingerprint &&
|
|
55
|
+
approval.repoRoot === params.repoRoot);
|
|
56
|
+
if (index === -1) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const [approval] = compacted.approvals.splice(index, 1);
|
|
60
|
+
return { state: compacted, result: approval ?? null };
|
|
61
|
+
},
|
|
67
62
|
});
|
|
68
63
|
}
|
|
69
64
|
export async function notifyEgressDeny(params) {
|
|
@@ -98,8 +93,8 @@ export async function recordEgressApproval(params) {
|
|
|
98
93
|
const { recordApproval } = await import('./approval-service.js');
|
|
99
94
|
const pending = await params.store.loadPending();
|
|
100
95
|
const match = pending.state.approvals.find((approval) => approval.approvalId === params.approvalId);
|
|
101
|
-
const
|
|
102
|
-
if (params.scope === 'domain' && match && !
|
|
96
|
+
const pendingHost = match ? parseHostFromSummary(match.summary) : null;
|
|
97
|
+
if (params.scope === 'domain' && match && !pendingHost) {
|
|
103
98
|
return {
|
|
104
99
|
ok: false,
|
|
105
100
|
message: `Cannot add domain to egress allowlist: could not parse host from summary "${match.summary}".`,
|
|
@@ -112,16 +107,15 @@ export async function recordEgressApproval(params) {
|
|
|
112
107
|
requireSignedToken: params.requireSignedToken ?? false,
|
|
113
108
|
store: params.store,
|
|
114
109
|
});
|
|
110
|
+
const host = result.approval ? parseHostFromSummary(result.approval.summary) : pendingHost;
|
|
115
111
|
if (!result.ok || params.scope !== 'domain' || !host) {
|
|
116
112
|
return { ok: result.ok, message: result.message };
|
|
117
113
|
}
|
|
118
|
-
|
|
119
|
-
const updated = addDomainToAllowlist(allowlist, {
|
|
114
|
+
await mutateEgressAllowlist(params.store.allowlistPath, (allowlist) => addDomainToAllowlist(allowlist, {
|
|
120
115
|
host,
|
|
121
116
|
approvedAt: new Date().toISOString(),
|
|
122
117
|
approvalId: params.approvalId,
|
|
123
|
-
});
|
|
124
|
-
await saveEgressAllowlist(params.store.allowlistPath, updated);
|
|
118
|
+
}));
|
|
125
119
|
return {
|
|
126
120
|
ok: true,
|
|
127
121
|
message: `${result.message} Domain ${host} added to egress allowlist.`,
|
|
@@ -31,6 +31,8 @@ export interface GateVerdict extends GatePermissionResponse {
|
|
|
31
31
|
axes?: ClassifyResult['axes'];
|
|
32
32
|
capabilityRequests?: ClassifyResult['capabilityRequests'];
|
|
33
33
|
authorizationDecision?: ClassifyResult['authorizationDecision'];
|
|
34
|
+
effectPlan?: ClassifyResult['effectPlan'];
|
|
35
|
+
effectPlanPolicyDecisions?: ClassifyResult['effectPlanPolicyDecisions'];
|
|
34
36
|
boundaryProfile?: string;
|
|
35
37
|
}
|
|
36
38
|
export declare function isGatedAction(value: unknown): value is GatedAction;
|
|
@@ -28,6 +28,8 @@ export function classifyResultToGateVerdict(params) {
|
|
|
28
28
|
axes: result.axes,
|
|
29
29
|
capabilityRequests: result.capabilityRequests,
|
|
30
30
|
authorizationDecision: result.authorizationDecision,
|
|
31
|
+
effectPlan: result.effectPlan,
|
|
32
|
+
effectPlanPolicyDecisions: result.effectPlanPolicyDecisions,
|
|
31
33
|
boundaryProfile: result.boundaryProfile,
|
|
32
34
|
};
|
|
33
35
|
}
|
package/dist/core/gate-engine.js
CHANGED
|
@@ -4,6 +4,7 @@ import { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload, } from
|
|
|
4
4
|
import { classifySubagent } from './classify-subagent.js';
|
|
5
5
|
import { classifyToolUse } from './classify-tool.js';
|
|
6
6
|
import { classifierOptionsFromConfig } from './config.js';
|
|
7
|
+
import { buildCapabilityEffectPlan } from './effect-ir/build.js';
|
|
7
8
|
import { GATE_CONTRACT_VERSION } from './gate-contract.js';
|
|
8
9
|
import { mergeAgentAssessment } from './judgment.js';
|
|
9
10
|
import { classifyShell } from './verdict/adapter.js';
|
|
@@ -200,7 +201,7 @@ function applyToolSandboxPolicies(action, result, options) {
|
|
|
200
201
|
export async function classifyGatedAction(action, config, extraOptions = {}) {
|
|
201
202
|
const limitResult = checkGatedActionLimits(action);
|
|
202
203
|
if (limitResult) {
|
|
203
|
-
return limitResult;
|
|
204
|
+
return attachEffectPlan(action, limitResult);
|
|
204
205
|
}
|
|
205
206
|
const options = { ...classifierOptionsFromConfig(config), ...extraOptions };
|
|
206
207
|
if (action.kind === 'shell') {
|
|
@@ -212,36 +213,52 @@ export async function classifyGatedAction(action, config, extraOptions = {}) {
|
|
|
212
213
|
result = applySandboxOutsideBoundary(command, action, result, options);
|
|
213
214
|
result = applyShellPeripheralPolicy(command, action, result, options);
|
|
214
215
|
if (!action.agentAssessment) {
|
|
215
|
-
return result;
|
|
216
|
+
return attachEffectPlan(action, result);
|
|
216
217
|
}
|
|
217
218
|
const merged = mergeAgentAssessment(result.assessment, action.agentAssessment);
|
|
218
219
|
if (!merged.mismatch) {
|
|
219
|
-
return { ...result, assessment: merged.assessment };
|
|
220
|
+
return attachEffectPlan(action, { ...result, assessment: merged.assessment });
|
|
220
221
|
}
|
|
221
|
-
return {
|
|
222
|
+
return attachEffectPlan(action, {
|
|
222
223
|
...result,
|
|
223
224
|
verdict: 'deny_pending_approval',
|
|
224
225
|
reason: 'agent_assessment_mismatch',
|
|
225
226
|
assessment: merged.assessment,
|
|
226
|
-
};
|
|
227
|
+
});
|
|
227
228
|
}
|
|
228
229
|
if (action.kind === 'subagent') {
|
|
229
|
-
return classifySubagent(action.payload ?? {}, action.repoRoot, options, config);
|
|
230
|
+
return attachEffectPlan(action, classifySubagent(action.payload ?? {}, action.repoRoot, options, config));
|
|
230
231
|
}
|
|
231
232
|
let result = await classifyToolUse(action.payload ?? {}, action.repoRoot, action.cwd, config, options);
|
|
232
233
|
result = applyToolSandboxPolicies(action, result, options);
|
|
233
234
|
if (!action.agentAssessment) {
|
|
234
|
-
return result;
|
|
235
|
+
return attachEffectPlan(action, result);
|
|
235
236
|
}
|
|
236
237
|
const merged = mergeAgentAssessment(result.assessment, action.agentAssessment);
|
|
237
238
|
if (!merged.mismatch) {
|
|
238
|
-
return { ...result, assessment: merged.assessment };
|
|
239
|
+
return attachEffectPlan(action, { ...result, assessment: merged.assessment });
|
|
239
240
|
}
|
|
240
|
-
return {
|
|
241
|
+
return attachEffectPlan(action, {
|
|
241
242
|
...result,
|
|
242
243
|
verdict: 'deny_pending_approval',
|
|
243
244
|
reason: 'agent_assessment_mismatch',
|
|
244
245
|
assessment: merged.assessment,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
function attachEffectPlan(action, result) {
|
|
249
|
+
if (result.effectPlan) {
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
const summary = result.normalizedCommand ?? result.summary ?? action.command ?? action.kind;
|
|
253
|
+
return {
|
|
254
|
+
...result,
|
|
255
|
+
effectPlan: buildCapabilityEffectPlan({
|
|
256
|
+
actionKind: action.kind,
|
|
257
|
+
summary,
|
|
258
|
+
inputFingerprint: result.fingerprint,
|
|
259
|
+
requests: result.capabilityRequests ?? [],
|
|
260
|
+
effectFree: result.reason === 'read_only',
|
|
261
|
+
}),
|
|
245
262
|
};
|
|
246
263
|
}
|
|
247
264
|
export async function classifyGatedActionAsync(action, config, extraOptions = {}) {
|
package/dist/core/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { ApprovalReplayHint, ReplayActionContext, ReplayAdapterId, } from '
|
|
|
3
3
|
/** @deprecated Use `buildRetryInstructionForConfig` */
|
|
4
4
|
export { approvalFlow, buildApprovalRecordedMessage, buildReplayEnvelopeFields, buildReplayHint, buildRetryInstructionForConfig, buildRetryInstructionForConfig as buildRetryInstruction, canAutoReplay, getExecutionLeaseMs, replayShellCommand, validateReplayEnvelope, } from './approval-replay.js';
|
|
5
5
|
export type { ApprovalStore } from './approval-service.js';
|
|
6
|
-
export { consumeApprovedAfterCliReplay, createGateApprovalStore, gateApprovalStoreFromDeps, recordApproval, } from './approval-service.js';
|
|
6
|
+
export { claimApprovedForReplay, consumeApprovedAfterCliReplay, createGateApprovalStore, gateApprovalStoreFromDeps, recordApproval, } from './approval-service.js';
|
|
7
7
|
export type { AuditMetricsReport } from './audit-metrics.js';
|
|
8
8
|
export { computeAuditMetrics, parseAuditNdjson } from './audit-metrics.js';
|
|
9
9
|
export { classifySubagent } from './classify-subagent.js';
|
|
@@ -16,6 +16,8 @@ export { classifyResultToGateVerdict, GATE_CONTRACT_VERSION, isGatedAction, unno
|
|
|
16
16
|
export { classifyGatedAction, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from './gate-engine.js';
|
|
17
17
|
export { matchesSensitivePath } from './glob.js';
|
|
18
18
|
export { canonicalPath, hasOutsideRepoPath, normalizeToken, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, } from './path-utils.js';
|
|
19
|
+
export type { RecoveryBackend, RecoveryCheckpointEntry, RecoveryCheckpointEntryV1, RecoveryCheckpointEntryV2, RecoveryCheckpointManifest, RecoveryCheckpointManifestV1, RecoveryCheckpointManifestV2, RecoveryCheckpointState, RecoveryCheckpointSummary, RecoveryFileSnapshotV1, RecoveryFileSnapshotV2, RecoveryProofV1, RecoveryReceiptV1, } from './recovery/index.js';
|
|
20
|
+
export { listRecoveryCheckpoints, RECOVERY_CHECKPOINT_CORRUPT, RECOVERY_CHECKPOINT_QUOTA, RECOVERY_RESTORE_CONFLICT, recoveryCheckpointStorageBytes, recoveryRestoreBinding, restoreRecoveryCheckpoint, showRecoveryCheckpoint, } from './recovery/index.js';
|
|
19
21
|
export { fingerprintReplayPayload, subagentFingerprintSource } from './replay-scrub.js';
|
|
20
22
|
export { scrubString, scrubValue } from './scrub.js';
|
|
21
23
|
export { findCommandSubstitutions, MAX_SUBSTITUTION_DEPTH } from './shell-substitution.js';
|
package/dist/core/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { APPROVAL_EXECUTION_LEASE_MS, approvalCommandMatch, compactApprovals, createApprovalRecord, createApprovalRecordWithEnvelope, isExecutionLeaseExpired, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
|
|
2
2
|
/** @deprecated Use `buildRetryInstructionForConfig` */
|
|
3
3
|
export { approvalFlow, buildApprovalRecordedMessage, buildReplayEnvelopeFields, buildReplayHint, buildRetryInstructionForConfig, buildRetryInstructionForConfig as buildRetryInstruction, canAutoReplay, getExecutionLeaseMs, replayShellCommand, validateReplayEnvelope, } from './approval-replay.js';
|
|
4
|
-
export { consumeApprovedAfterCliReplay, createGateApprovalStore, gateApprovalStoreFromDeps, recordApproval, } from './approval-service.js';
|
|
4
|
+
export { claimApprovedForReplay, consumeApprovedAfterCliReplay, createGateApprovalStore, gateApprovalStoreFromDeps, recordApproval, } from './approval-service.js';
|
|
5
5
|
export { computeAuditMetrics, parseAuditNdjson } from './audit-metrics.js';
|
|
6
6
|
export { classifySubagent } from './classify-subagent.js';
|
|
7
7
|
export { classifyToolUse } from './classify-tool.js';
|
|
@@ -12,6 +12,7 @@ export { classifyResultToGateVerdict, GATE_CONTRACT_VERSION, isGatedAction, unno
|
|
|
12
12
|
export { classifyGatedAction, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from './gate-engine.js';
|
|
13
13
|
export { matchesSensitivePath } from './glob.js';
|
|
14
14
|
export { canonicalPath, hasOutsideRepoPath, normalizeToken, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, } from './path-utils.js';
|
|
15
|
+
export { listRecoveryCheckpoints, RECOVERY_CHECKPOINT_CORRUPT, RECOVERY_CHECKPOINT_QUOTA, RECOVERY_RESTORE_CONFLICT, recoveryCheckpointStorageBytes, recoveryRestoreBinding, restoreRecoveryCheckpoint, showRecoveryCheckpoint, } from './recovery/index.js';
|
|
15
16
|
export { fingerprintReplayPayload, subagentFingerprintSource } from './replay-scrub.js';
|
|
16
17
|
export { scrubString, scrubValue } from './scrub.js';
|
|
17
18
|
export { findCommandSubstitutions, MAX_SUBSTITUTION_DEPTH } from './shell-substitution.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { loadApprovalState, saveApprovalState } from '../config-io.js';
|
|
2
|
+
import { loadApprovalState, pendingApprovalsPath, saveApprovalState } from '../config-io.js';
|
|
3
3
|
import { compactApprovals, createApprovalRecord } from './approval.js';
|
|
4
|
+
import { mutateApprovalStateWithRetry } from './capability/approval-state-mutation.js';
|
|
4
5
|
import { JUDGE_CLOUD_CONSENT_REASON } from './capability/reasons.js';
|
|
5
6
|
export { JUDGE_CLOUD_CONSENT_REASON };
|
|
6
7
|
export function judgeCloudConsentFingerprint(providerId, endpoint) {
|
|
@@ -10,18 +11,8 @@ export async function ensurePendingJudgeCloudConsentApproval(params) {
|
|
|
10
11
|
const { repoRoot, config, providerId, endpoint } = params;
|
|
11
12
|
const normalizedEndpoint = endpoint.trim();
|
|
12
13
|
const fingerprint = judgeCloudConsentFingerprint(providerId, normalizedEndpoint);
|
|
13
|
-
const pending = await loadApprovalState(repoRoot, 'pending-approvals.json', config);
|
|
14
|
-
const compacted = compactApprovals(pending);
|
|
15
|
-
const existing = compacted.approvals.find((approval) => approval.kind === 'capability' &&
|
|
16
|
-
approval.reason === JUDGE_CLOUD_CONSENT_REASON &&
|
|
17
|
-
approval.fingerprint === fingerprint &&
|
|
18
|
-
approval.repoRoot === repoRoot);
|
|
19
|
-
if (existing) {
|
|
20
|
-
await saveApprovalState(repoRoot, 'pending-approvals.json', compacted, config);
|
|
21
|
-
return { approvalId: existing.approvalId, created: false };
|
|
22
|
-
}
|
|
23
14
|
const approvalId = `belay_${randomUUID().replaceAll('-', '').slice(0, 12)}`;
|
|
24
|
-
const
|
|
15
|
+
const candidate = createApprovalRecord({
|
|
25
16
|
kind: 'capability',
|
|
26
17
|
fingerprint,
|
|
27
18
|
repoRoot,
|
|
@@ -32,7 +23,30 @@ export async function ensurePendingJudgeCloudConsentApproval(params) {
|
|
|
32
23
|
input: `belay judge use ${providerId} --endpoint ${normalizedEndpoint}`,
|
|
33
24
|
inputKind: 'shell',
|
|
34
25
|
});
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
const outcome = await mutateApprovalStateWithRetry({
|
|
27
|
+
load: async () => ({
|
|
28
|
+
filePath: pendingApprovalsPath(repoRoot, config),
|
|
29
|
+
state: await loadApprovalState(repoRoot, 'pending-approvals.json', config),
|
|
30
|
+
}),
|
|
31
|
+
write: async (_filePath, state) => saveApprovalState(repoRoot, 'pending-approvals.json', state, config),
|
|
32
|
+
mutate: (state) => {
|
|
33
|
+
const compacted = compactApprovals(state);
|
|
34
|
+
const existing = compacted.approvals.find((approval) => approval.kind === 'capability' &&
|
|
35
|
+
approval.reason === JUDGE_CLOUD_CONSENT_REASON &&
|
|
36
|
+
approval.fingerprint === fingerprint &&
|
|
37
|
+
approval.repoRoot === repoRoot);
|
|
38
|
+
if (existing) {
|
|
39
|
+
return {
|
|
40
|
+
state: compacted,
|
|
41
|
+
result: { approvalId: existing.approvalId, created: false },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
compacted.approvals.push(candidate);
|
|
45
|
+
return { state: compacted, result: { approvalId, created: true } };
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
if (!outcome) {
|
|
49
|
+
throw new Error('Failed to persist cloud judge consent approval');
|
|
50
|
+
}
|
|
51
|
+
return outcome;
|
|
38
52
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface NetworkEndpoint {
|
|
2
|
+
host: string;
|
|
3
|
+
port?: number;
|
|
4
|
+
protocol: string;
|
|
5
|
+
}
|
|
6
|
+
export interface NetworkEndpointParseOptions {
|
|
7
|
+
allowHostedGitShorthand?: boolean;
|
|
8
|
+
allowScpStyle?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function parseNetworkEndpoint(spec: string, options?: NetworkEndpointParseOptions): NetworkEndpoint | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const SUPPORTED_URL_PROTOCOLS = new Set(['http:', 'https:', 'ssh:', 'git:']);
|
|
2
|
+
const NON_NETWORK_COLON_PREFIXES = new Set([
|
|
3
|
+
'catalog',
|
|
4
|
+
'exec',
|
|
5
|
+
'file',
|
|
6
|
+
'link',
|
|
7
|
+
'npm',
|
|
8
|
+
'patch',
|
|
9
|
+
'portal',
|
|
10
|
+
'workspace',
|
|
11
|
+
]);
|
|
12
|
+
const HOSTED_GIT_PREFIXES = {
|
|
13
|
+
bitbucket: 'bitbucket.org',
|
|
14
|
+
github: 'github.com',
|
|
15
|
+
gitlab: 'gitlab.com',
|
|
16
|
+
};
|
|
17
|
+
export function parseNetworkEndpoint(spec, options = {}) {
|
|
18
|
+
const normalized = spec.trim().replace(/^git\+/, '');
|
|
19
|
+
if (options.allowHostedGitShorthand) {
|
|
20
|
+
const hostedGit = normalized.match(/^([a-z]+):(.+)$/i);
|
|
21
|
+
const hostedGitHost = hostedGit?.[1]
|
|
22
|
+
? HOSTED_GIT_PREFIXES[hostedGit[1].toLowerCase()]
|
|
23
|
+
: undefined;
|
|
24
|
+
if (hostedGitHost && hostedGit?.[2]) {
|
|
25
|
+
return { host: hostedGitHost, protocol: 'git' };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const url = new URL(normalized);
|
|
30
|
+
if (SUPPORTED_URL_PROTOCOLS.has(url.protocol) && url.hostname) {
|
|
31
|
+
return {
|
|
32
|
+
host: url.hostname,
|
|
33
|
+
...(url.port ? { port: Number(url.port) } : {}),
|
|
34
|
+
protocol: url.protocol.slice(0, -1),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
if (!options.allowScpStyle || /^[A-Za-z]:[\\/]/.test(normalized)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const scpStyle = normalized.match(/^(?:([^@/\s:]+)@)?([^:/\s]+):(.+)$/);
|
|
43
|
+
if (!scpStyle) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const [, , host, remotePath] = scpStyle;
|
|
47
|
+
if (!host || !remotePath || NON_NETWORK_COLON_PREFIXES.has(host.toLowerCase())) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return { host: host.toLowerCase(), protocol: 'ssh' };
|
|
51
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type SpawnOptionsWithoutStdio } from 'node:child_process';
|
|
2
|
+
export interface ShellRunResult {
|
|
3
|
+
exitCode: number | null;
|
|
4
|
+
signal: string | null;
|
|
5
|
+
timedOut: boolean;
|
|
6
|
+
stdout?: string;
|
|
7
|
+
stderr?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function windowsProcessTreeKillArgs(pid: number): string[];
|
|
10
|
+
export declare function runProcessWithBoundedOutput(file: string, args: string[], options: SpawnOptionsWithoutStdio, timeoutMs: number): Promise<ShellRunResult>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
const PROCESS_OUTPUT_TAIL_BYTES = 16_384;
|
|
3
|
+
const EXIT_OUTPUT_GRACE_MS = 25;
|
|
4
|
+
const TIMEOUT_KILL_GRACE_MS = 250;
|
|
5
|
+
const WINDOWS_TASKKILL_TIMEOUT_MS = 5_000;
|
|
6
|
+
function appendOutputTail(current, chunk) {
|
|
7
|
+
const next = Buffer.concat([current, Buffer.from(chunk)]);
|
|
8
|
+
return next.length > PROCESS_OUTPUT_TAIL_BYTES ? next.subarray(-PROCESS_OUTPUT_TAIL_BYTES) : next;
|
|
9
|
+
}
|
|
10
|
+
export function windowsProcessTreeKillArgs(pid) {
|
|
11
|
+
return ['/pid', String(pid), '/t', '/f'];
|
|
12
|
+
}
|
|
13
|
+
function forceKillWindowsProcessTree(pid, fallback) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const taskkill = spawn('taskkill', windowsProcessTreeKillArgs(pid), {
|
|
16
|
+
stdio: 'ignore',
|
|
17
|
+
windowsHide: true,
|
|
18
|
+
});
|
|
19
|
+
let settled = false;
|
|
20
|
+
const finish = (succeeded) => {
|
|
21
|
+
if (settled) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
settled = true;
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
if (!succeeded) {
|
|
27
|
+
fallback();
|
|
28
|
+
}
|
|
29
|
+
resolve();
|
|
30
|
+
};
|
|
31
|
+
const timer = setTimeout(() => {
|
|
32
|
+
taskkill.kill();
|
|
33
|
+
finish(false);
|
|
34
|
+
}, WINDOWS_TASKKILL_TIMEOUT_MS);
|
|
35
|
+
taskkill.on('error', () => finish(false));
|
|
36
|
+
taskkill.on('close', (code) => finish(code === 0));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function runProcessWithBoundedOutput(file, args, options, timeoutMs) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const detached = process.platform !== 'win32';
|
|
42
|
+
const child = spawn(file, args, {
|
|
43
|
+
...options,
|
|
44
|
+
detached,
|
|
45
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
46
|
+
});
|
|
47
|
+
let stdout = Buffer.alloc(0);
|
|
48
|
+
let stderr = Buffer.alloc(0);
|
|
49
|
+
let timedOut = false;
|
|
50
|
+
let settled = false;
|
|
51
|
+
let windowsCleanupPending = false;
|
|
52
|
+
let exitTimer;
|
|
53
|
+
child.stdout.on('data', (chunk) => {
|
|
54
|
+
stdout = appendOutputTail(stdout, chunk);
|
|
55
|
+
});
|
|
56
|
+
child.stderr.on('data', (chunk) => {
|
|
57
|
+
stderr = appendOutputTail(stderr, chunk);
|
|
58
|
+
});
|
|
59
|
+
const finish = (exitCode, signal) => {
|
|
60
|
+
if (settled) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
settled = true;
|
|
64
|
+
clearTimeout(timeoutTimer);
|
|
65
|
+
if (exitTimer) {
|
|
66
|
+
clearTimeout(exitTimer);
|
|
67
|
+
}
|
|
68
|
+
child.stdout.destroy();
|
|
69
|
+
child.stderr.destroy();
|
|
70
|
+
resolve({
|
|
71
|
+
exitCode,
|
|
72
|
+
signal: signal ? String(signal) : null,
|
|
73
|
+
timedOut,
|
|
74
|
+
stdout: stdout.toString('utf8'),
|
|
75
|
+
stderr: stderr.toString('utf8'),
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
const kill = (signal) => {
|
|
79
|
+
try {
|
|
80
|
+
if (detached && child.pid) {
|
|
81
|
+
process.kill(-child.pid, signal);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
child.kill(signal);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// The process may already have exited between the state check and kill.
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const timeoutTimer = setTimeout(() => {
|
|
91
|
+
timedOut = true;
|
|
92
|
+
if (process.platform === 'win32' && child.pid) {
|
|
93
|
+
windowsCleanupPending = true;
|
|
94
|
+
void forceKillWindowsProcessTree(child.pid, () => child.kill('SIGKILL')).then(() => {
|
|
95
|
+
windowsCleanupPending = false;
|
|
96
|
+
finish(child.exitCode, child.signalCode ?? 'SIGKILL');
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
kill('SIGTERM');
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
kill('SIGKILL');
|
|
103
|
+
finish(null, 'SIGKILL');
|
|
104
|
+
}, TIMEOUT_KILL_GRACE_MS);
|
|
105
|
+
}, timeoutMs);
|
|
106
|
+
child.on('error', (error) => {
|
|
107
|
+
stderr = appendOutputTail(stderr, error.message);
|
|
108
|
+
finish(1, null);
|
|
109
|
+
});
|
|
110
|
+
child.on('exit', (exitCode, signal) => {
|
|
111
|
+
clearTimeout(timeoutTimer);
|
|
112
|
+
if (windowsCleanupPending) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
exitTimer = setTimeout(() => finish(exitCode, signal), EXIT_OUTPUT_GRACE_MS);
|
|
116
|
+
});
|
|
117
|
+
child.on('close', (exitCode, signal) => {
|
|
118
|
+
if (windowsCleanupPending) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
finish(exitCode, signal);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const RECOVER_DISCLAIMER = [
|
|
2
|
-
'Advisory only —
|
|
2
|
+
'Advisory only — this report is not proof that the selected action executed. Only a checkpoint receipt shown by `belay recover list` is an executable recovery point.',
|
|
3
|
+
'`belay recover advice` does not run recovery commands; checkpoint restore requires a separate one-shot human approval.',
|
|
3
4
|
'Advice is based on what belay observed through hooks; actions outside hook scope may not be visible.',
|
|
4
5
|
'Recovery commands themselves pass through belay hooks — destructive undo steps may be blocked again.',
|
|
5
6
|
];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RecoveryCheckpointManifest, RecoveryCheckpointState, RecoveryCheckpointStateV1, RecoveryReceiptV1 } from './types.js';
|
|
2
|
+
export declare const RECOVERY_CHECKPOINT_CORRUPT = "recovery_checkpoint_corrupt";
|
|
3
|
+
export interface PreparedRecoveryCheckpoint {
|
|
4
|
+
checkpointId: string;
|
|
5
|
+
manifest: RecoveryCheckpointManifest;
|
|
6
|
+
manifestHash: string;
|
|
7
|
+
proofHash: string;
|
|
8
|
+
}
|
|
9
|
+
export interface RecoveryArtifact {
|
|
10
|
+
artifactDir: string;
|
|
11
|
+
manifest: RecoveryCheckpointManifest;
|
|
12
|
+
state: RecoveryCheckpointStateV1;
|
|
13
|
+
manifestHash: string;
|
|
14
|
+
receipt?: RecoveryReceiptV1;
|
|
15
|
+
}
|
|
16
|
+
export declare function checkpointsRoot(stateDir: string): string;
|
|
17
|
+
export declare function checkpointDir(stateDir: string, checkpointId: string): string;
|
|
18
|
+
export declare function fsyncPath(filePath: string): Promise<void>;
|
|
19
|
+
export declare function atomicWriteJson(filePath: string, value: unknown): Promise<void>;
|
|
20
|
+
export declare function writeRecoveryState(artifactDir: string, state: RecoveryCheckpointState, manifestHash: string, detail?: string): Promise<void>;
|
|
21
|
+
export declare function directorySize(root: string): Promise<number>;
|
|
22
|
+
export declare function readRecoveryArtifact(stateDir: string, checkpointId: string): Promise<RecoveryArtifact>;
|
|
23
|
+
export declare function ensureRecoveryReceipt(artifactDir: string, manifest: RecoveryCheckpointManifest, manifestHash: string): Promise<RecoveryReceiptV1>;
|
|
24
|
+
export declare function checkpointIds(stateDir: string): Promise<string[]>;
|
|
25
|
+
export declare function artifactRepoRoot(stateDir: string, checkpointId: string): Promise<string | null>;
|
|
26
|
+
export declare function checkpointIdsForRepo(stateDir: string, repoRoot: string): Promise<string[]>;
|
|
27
|
+
export declare function cleanupOrphanedStaging(stateDir: string): Promise<void>;
|
|
28
|
+
export declare function markRecoveryCheckpointApplying(stateDir: string, checkpoint: PreparedRecoveryCheckpoint): Promise<void>;
|
|
29
|
+
export declare function markRecoveryCheckpointApplied(stateDir: string, checkpoint: PreparedRecoveryCheckpoint): Promise<RecoveryReceiptV1>;
|