@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
|
@@ -1,22 +1,37 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { compactApprovals, createApprovalRecordWithEnvelope } from '../../core/approval.js';
|
|
6
|
-
import { buildReplayHint, buildRetryInstructionForConfig,
|
|
7
|
-
import {
|
|
6
|
+
import { buildReplayHint, buildRetryInstructionForConfig, canAutoReplay, getExecutionLeaseMs, validateReplayEnvelope, } from '../../core/approval-replay.js';
|
|
7
|
+
import { claimApprovedForReplay, gateApprovalStoreFromDeps, recordApproval, } from '../../core/approval-service.js';
|
|
8
8
|
import { issueApprovalToken } from '../../core/approval-token.js';
|
|
9
9
|
import { buildAuditActionSnapshot, buildAuditReplayContext, } from '../../core/audit-replay-context.js';
|
|
10
|
+
import { mutateApprovalStateWithRetry } from '../../core/capability/approval-state-mutation.js';
|
|
11
|
+
import { APPROVAL_STATE_VERSION_V3 } from '../../core/capability/approval-v3.js';
|
|
12
|
+
import { isEgressProxyActive } from '../../core/capability/boundary-egress.js';
|
|
13
|
+
import { isBoundaryCleanupError, safeBoundaryCleanupResourceId, } from '../../core/capability/boundary-run.js';
|
|
14
|
+
import { resolveBoundaryDriverContext, runBoundaryAgentCommand, } from '../../core/capability/boundary-session.js';
|
|
15
|
+
import { hashCapabilityRequests } from '../../core/capability/capability-request-hash.js';
|
|
16
|
+
import { recordGateApprovalAsk, scheduleGateShadowAudit, } from '../../core/capability/gate-shadow-audit.js';
|
|
17
|
+
import { canConsumeCapabilityGrantLease } from '../../core/capability/grant-consumption.js';
|
|
18
|
+
import { approvalGrantBundleExhausted, consumeApprovedRecordGrantBundle, consumeGrantLeasesForRequests, decrementApprovalLegacyGrant, grantsFromApproval, validateAndConsumeGrantBundle, validateGrantBundleForLeaseReuse, } from '../../core/capability/grant-lease.js';
|
|
19
|
+
import { loadClassifierAuthorization } from '../../core/capability/grant-loader.js';
|
|
10
20
|
import { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload, fsScopeAllowlistPath, isCapabilityBrokerDemotionActive, loadFsScopeAllowlistSync, loadTrustedWorkspaceRootsSync, shouldSkipBrokerApprovedOnce, shouldSkipBrokerApprovedRecord, trustedWorkspaceRootsPath, validateTrustedWorkspaceRootCandidate, } from '../../core/capability/index.js';
|
|
11
21
|
import { resolveLayeredConfig, teamConfigPath } from '../../core/config-layers.js';
|
|
22
|
+
import { effectPlanAuditFields, hashEffectPlan } from '../../core/effect-ir/audit.js';
|
|
23
|
+
import { buildCapabilityEffectPlan } from '../../core/effect-ir/build.js';
|
|
12
24
|
import { classifyResultToGateVerdict, unnormalizedGateVerdict, } from '../../core/gate-contract.js';
|
|
13
25
|
import { classifyGatedActionAsync, extractAgentAssessment, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from '../../core/gate-engine.js';
|
|
14
|
-
import { approvalCommandMatch, approvedApprovalsFile, canonicalStringify, classifierOptionsFromConfig, configuredControlPlaneDir, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, scrubValue, toolFingerprint, } from '../../core/index.js';
|
|
26
|
+
import { approvalCommandMatch, approvedApprovalsFile, belayStateDir, canonicalStringify, classifierOptionsFromConfig, configuredControlPlaneDir, hashValue, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, scrubValue, toolFingerprint, } from '../../core/index.js';
|
|
27
|
+
import { extractJudgeFallbackReason, formatJudgeInfrastructureDenyMessage, inferProviderIdFromFallbackReason, isJudgeInfrastructureFailure, } from '../../core/judge-fallback-hints.js';
|
|
15
28
|
import { notifyDeny } from '../../core/notify.js';
|
|
16
29
|
import { canonicalPath } from '../../core/path-utils.js';
|
|
30
|
+
import { recoveryFailClosedResult, recoveryFailReasonFromSkip, } from '../../core/recovery/fail-closed.js';
|
|
17
31
|
import { fingerprintReplayPayload } from '../../core/replay-scrub.js';
|
|
18
32
|
import { loadStandingAllow, resolveStandingAllowMatch, standingAllowFile, } from '../../core/standing-allow.js';
|
|
19
|
-
import { isTransactionalEligible, runTransactionalExecution, TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPROVAL_BYPASS_REASONS, } from '../../core/transactional/index.js';
|
|
33
|
+
import { FILE_CHECKPOINT_ISOLATION_UNAVAILABLE, isTransactionalEligible, runTransactionalExecution, TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPROVAL_BYPASS_REASONS, } from '../../core/transactional/index.js';
|
|
34
|
+
import { egressStatus } from '../../services/egress-service.js';
|
|
20
35
|
import { protectedArtifactRoots } from '../layouts/protected-paths.js';
|
|
21
36
|
const EMPTY_APPROVALS = {
|
|
22
37
|
version: 1,
|
|
@@ -33,6 +48,16 @@ function adapterIdFromContext(ctx) {
|
|
|
33
48
|
}
|
|
34
49
|
return undefined;
|
|
35
50
|
}
|
|
51
|
+
const REPLAY_AUDIT_FAILURE_NOTE = ' Audit recording failed; inspect audit storage before the next approval.';
|
|
52
|
+
async function appendReplayAuditSafely(ctx, deps, event) {
|
|
53
|
+
try {
|
|
54
|
+
await deps.appendAudit(ctx, event);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
36
61
|
async function loadJsonFile(filePath, fallback) {
|
|
37
62
|
try {
|
|
38
63
|
const raw = await readFile(filePath, 'utf8');
|
|
@@ -66,10 +91,12 @@ export function createDefaultGateRuntimeDeps() {
|
|
|
66
91
|
? pendingApprovalsFile(ctx.config, repoLocalStateDir)
|
|
67
92
|
: approvedApprovalsFile(ctx.config, repoLocalStateDir);
|
|
68
93
|
const loaded = await loadJsonFile(filePath, EMPTY_APPROVALS);
|
|
94
|
+
const version = loaded.version === 3 ? 3 : loaded.version === 2 ? 2 : 1;
|
|
69
95
|
return {
|
|
70
96
|
filePath,
|
|
71
97
|
state: {
|
|
72
|
-
version
|
|
98
|
+
version,
|
|
99
|
+
...(loaded.revision !== undefined ? { revision: loaded.revision } : {}),
|
|
73
100
|
approvals: Array.isArray(loaded.approvals) ? loaded.approvals : [],
|
|
74
101
|
},
|
|
75
102
|
};
|
|
@@ -78,6 +105,16 @@ export function createDefaultGateRuntimeDeps() {
|
|
|
78
105
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
79
106
|
await writeFile(filePath, `${JSON.stringify(compactApprovals(state), null, 2)}\n`, 'utf8');
|
|
80
107
|
},
|
|
108
|
+
async replayApprovedShell(ctx, command, cwd, timeoutMs) {
|
|
109
|
+
return runBoundaryAgentCommand({
|
|
110
|
+
repoRoot: ctx.repoRoot,
|
|
111
|
+
config: ctx.config,
|
|
112
|
+
command,
|
|
113
|
+
cwd,
|
|
114
|
+
timeoutMs,
|
|
115
|
+
runOptions: { mountReadOnly: false },
|
|
116
|
+
});
|
|
117
|
+
},
|
|
81
118
|
};
|
|
82
119
|
}
|
|
83
120
|
export async function resolveGateConfig(ctx, deps) {
|
|
@@ -126,16 +163,7 @@ function gateAuditEventName(kind) {
|
|
|
126
163
|
return 'subagentGate';
|
|
127
164
|
}
|
|
128
165
|
async function ensurePendingApproval(ctx, deps, kind, result, approvalInput, scopeHint) {
|
|
129
|
-
const
|
|
130
|
-
pending.state = compactApprovals(pending.state);
|
|
131
|
-
const existing = pending.state.approvals.find((approval) => approval.kind === kind &&
|
|
132
|
-
approval.fingerprint === result.fingerprint &&
|
|
133
|
-
approval.repoRoot === ctx.repoRoot);
|
|
134
|
-
if (existing) {
|
|
135
|
-
await deps.writeApprovals(pending.filePath, pending.state);
|
|
136
|
-
return existing;
|
|
137
|
-
}
|
|
138
|
-
const approval = createApprovalRecordWithEnvelope({
|
|
166
|
+
const candidate = createApprovalRecordWithEnvelope({
|
|
139
167
|
kind,
|
|
140
168
|
fingerprint: result.fingerprint,
|
|
141
169
|
repoRoot: ctx.repoRoot,
|
|
@@ -145,11 +173,32 @@ async function ensurePendingApproval(ctx, deps, kind, result, approvalInput, sco
|
|
|
145
173
|
approvalId: `belay_${randomUUID().replaceAll('-', '').slice(0, 12)}`,
|
|
146
174
|
approvalInput,
|
|
147
175
|
scopeHint,
|
|
176
|
+
capabilityRequests: result.capabilityRequests,
|
|
177
|
+
effectPlanHash: result.effectPlan ? hashEffectPlan(result.effectPlan) : undefined,
|
|
178
|
+
});
|
|
179
|
+
const outcome = await mutateApprovalStateWithRetry({
|
|
180
|
+
load: () => deps.loadApprovals(ctx, 'pending-approvals.json'),
|
|
181
|
+
write: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
182
|
+
mutate: (state) => {
|
|
183
|
+
const compacted = compactApprovals(state);
|
|
184
|
+
const existing = compacted.approvals.find((approval) => approval.kind === kind &&
|
|
185
|
+
approval.fingerprint === result.fingerprint &&
|
|
186
|
+
approval.repoRoot === ctx.repoRoot);
|
|
187
|
+
if (existing) {
|
|
188
|
+
return { state: compacted, result: { approval: existing, created: false } };
|
|
189
|
+
}
|
|
190
|
+
compacted.version = APPROVAL_STATE_VERSION_V3;
|
|
191
|
+
compacted.approvals.push(candidate);
|
|
192
|
+
return { state: compacted, result: { approval: candidate, created: true } };
|
|
193
|
+
},
|
|
148
194
|
});
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return
|
|
195
|
+
if (!outcome) {
|
|
196
|
+
throw new Error('Failed to persist pending approval');
|
|
197
|
+
}
|
|
198
|
+
return outcome;
|
|
199
|
+
}
|
|
200
|
+
function approvalCorrelationId(approvalId) {
|
|
201
|
+
return createHash('sha256').update(approvalId).digest('hex').slice(0, 16);
|
|
153
202
|
}
|
|
154
203
|
function extractShellCommandFromPayload(payload) {
|
|
155
204
|
const toolInput = payload.tool_input;
|
|
@@ -207,27 +256,93 @@ function deriveWorkspaceRootScopeHint(params) {
|
|
|
207
256
|
}
|
|
208
257
|
return { scope: 'workspace-root', path: validation.normalizedPath };
|
|
209
258
|
}
|
|
210
|
-
async function consumeApprovedApproval(ctx, deps, kind, fingerprint) {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
259
|
+
async function consumeApprovedApproval(ctx, deps, kind, fingerprint, requests) {
|
|
260
|
+
const consumed = await mutateApprovalStateWithRetry({
|
|
261
|
+
load: () => deps.loadApprovals(ctx, 'approved-approvals.json'),
|
|
262
|
+
write: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
263
|
+
mutate: (state) => {
|
|
264
|
+
const compacted = compactApprovals(state);
|
|
265
|
+
const matchIndex = compacted.approvals.findIndex((approval) => approval.kind === kind &&
|
|
266
|
+
approval.fingerprint === fingerprint &&
|
|
267
|
+
approval.repoRoot === ctx.repoRoot);
|
|
268
|
+
if (matchIndex === -1) {
|
|
269
|
+
return { state: compacted, result: null };
|
|
270
|
+
}
|
|
271
|
+
const approval = compacted.approvals[matchIndex];
|
|
272
|
+
if (approval.executionLeaseExpiresAt) {
|
|
273
|
+
if (approval.grantBundleVersion === 1) {
|
|
274
|
+
const validated = validateGrantBundleForLeaseReuse(approval, requests);
|
|
275
|
+
if (!validated.ok) {
|
|
276
|
+
compacted.approvals.splice(matchIndex, 1);
|
|
277
|
+
return {
|
|
278
|
+
state: compacted,
|
|
279
|
+
result: { status: 'invalid_bundle', approval, reason: validated.reason },
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
state: compacted,
|
|
285
|
+
result: { status: 'consumed', approval, firstExecution: false },
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (approvalGrantBundleExhausted(approval) && !approval.executionLeaseExpiresAt) {
|
|
289
|
+
compacted.approvals.splice(matchIndex, 1);
|
|
290
|
+
return { state: compacted, result: null };
|
|
291
|
+
}
|
|
292
|
+
let updatedApproval = approval;
|
|
293
|
+
const bundle = grantsFromApproval(approval);
|
|
294
|
+
if (approval.grantBundleVersion === 1) {
|
|
295
|
+
const validated = validateAndConsumeGrantBundle(approval, requests);
|
|
296
|
+
if (!validated.ok) {
|
|
297
|
+
compacted.approvals.splice(matchIndex, 1);
|
|
298
|
+
return {
|
|
299
|
+
state: compacted,
|
|
300
|
+
result: { status: 'invalid_bundle', approval, reason: validated.reason },
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
updatedApproval = validated.approval;
|
|
304
|
+
}
|
|
305
|
+
else if (bundle.length > 0) {
|
|
306
|
+
const consumed = consumeApprovedRecordGrantBundle(approval);
|
|
307
|
+
if (!consumed.consumed) {
|
|
308
|
+
return { state: compacted, result: null };
|
|
309
|
+
}
|
|
310
|
+
updatedApproval = consumed.approval;
|
|
311
|
+
}
|
|
312
|
+
else if (approval.grant) {
|
|
313
|
+
updatedApproval = decrementApprovalLegacyGrant(approval);
|
|
314
|
+
}
|
|
315
|
+
compacted.approvals[matchIndex] = {
|
|
316
|
+
...updatedApproval,
|
|
317
|
+
executionLeaseExpiresAt: new Date(Date.now() + getExecutionLeaseMs(ctx.config)).toISOString(),
|
|
318
|
+
};
|
|
319
|
+
return {
|
|
320
|
+
state: compacted,
|
|
321
|
+
result: { status: 'consumed', approval: updatedApproval, firstExecution: true },
|
|
322
|
+
};
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
if (!consumed) {
|
|
218
326
|
return null;
|
|
219
327
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
328
|
+
return consumed;
|
|
329
|
+
}
|
|
330
|
+
async function discardApprovedApproval(ctx, deps, approvalId) {
|
|
331
|
+
const discarded = await mutateApprovalStateWithRetry({
|
|
332
|
+
load: () => deps.loadApprovals(ctx, 'approved-approvals.json'),
|
|
333
|
+
write: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
334
|
+
mutate: (state) => {
|
|
335
|
+
const compacted = compactApprovals(state);
|
|
336
|
+
const index = compacted.approvals.findIndex((approval) => approval.approvalId === approvalId);
|
|
337
|
+
if (index !== -1) {
|
|
338
|
+
compacted.approvals.splice(index, 1);
|
|
339
|
+
}
|
|
340
|
+
return { state: compacted, result: true };
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
if (discarded !== true) {
|
|
344
|
+
throw new Error(`Failed to discard rejected approval ${approvalId}`);
|
|
224
345
|
}
|
|
225
|
-
approved.state.approvals[index] = {
|
|
226
|
-
...approval,
|
|
227
|
-
executionLeaseExpiresAt: new Date(Date.now() + getExecutionLeaseMs(ctx.config)).toISOString(),
|
|
228
|
-
};
|
|
229
|
-
await deps.writeApprovals(approved.filePath, approved.state);
|
|
230
|
-
return approval;
|
|
231
346
|
}
|
|
232
347
|
export async function evaluateGatedAction(ctx, deps, params) {
|
|
233
348
|
let action;
|
|
@@ -243,65 +358,124 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
243
358
|
});
|
|
244
359
|
}
|
|
245
360
|
catch {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
361
|
+
const scrubbedInput = scrubValue({
|
|
362
|
+
kind: params.kind,
|
|
363
|
+
cwd: params.cwd,
|
|
364
|
+
command: params.command,
|
|
365
|
+
payload: params.payload,
|
|
366
|
+
toolName: params.toolName,
|
|
367
|
+
}, scrubOptionsFromConfig(ctx.config));
|
|
368
|
+
const inputFingerprint = hashValue(`unnormalized-gated-action:v1:${canonicalStringify(scrubbedInput)}`);
|
|
369
|
+
const verdict = {
|
|
370
|
+
...unnormalizedGateVerdict({
|
|
371
|
+
reason: 'normalization_failed',
|
|
372
|
+
mode: ctx.config.mode,
|
|
373
|
+
user_message: 'belay could not normalize this gated action. Run belay doctor, then retry.',
|
|
374
|
+
agent_message: 'Belay denied this action because the hook payload could not be normalized.',
|
|
375
|
+
}),
|
|
376
|
+
fingerprint: inputFingerprint,
|
|
377
|
+
effectPlan: buildCapabilityEffectPlan({
|
|
378
|
+
actionKind: params.kind,
|
|
379
|
+
summary: params.command ?? params.toolName ?? params.kind,
|
|
380
|
+
inputFingerprint,
|
|
381
|
+
requests: [],
|
|
382
|
+
effectFree: false,
|
|
383
|
+
}),
|
|
384
|
+
};
|
|
252
385
|
await deps.appendAudit(ctx, {
|
|
253
386
|
event: gateAuditEventName(params.kind),
|
|
254
387
|
kind: params.kind,
|
|
388
|
+
fingerprint: verdict.fingerprint,
|
|
255
389
|
verdict: verdict.verdict,
|
|
256
390
|
reason: verdict.reason,
|
|
257
391
|
mode: ctx.config.mode,
|
|
392
|
+
...effectPlanAuditFields(verdict.effectPlan),
|
|
258
393
|
wouldBlock: true,
|
|
259
394
|
permission: 'deny',
|
|
260
395
|
});
|
|
261
396
|
return verdict;
|
|
262
397
|
}
|
|
263
398
|
if (!gateEnabledForAction(ctx.config, action)) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
signals: ['gate_disabled'],
|
|
275
|
-
},
|
|
399
|
+
const result = {
|
|
400
|
+
verdict: 'allow',
|
|
401
|
+
reason: 'gate_disabled',
|
|
402
|
+
fingerprint: 'gate_disabled',
|
|
403
|
+
assessment: {
|
|
404
|
+
reversibility: 'reversible',
|
|
405
|
+
external: false,
|
|
406
|
+
blastRadius: 'none',
|
|
407
|
+
confidence: 1,
|
|
408
|
+
signals: ['gate_disabled'],
|
|
276
409
|
},
|
|
410
|
+
effectPlan: buildCapabilityEffectPlan({
|
|
411
|
+
actionKind: action.kind,
|
|
412
|
+
summary: action.command ?? action.toolName ?? action.kind,
|
|
413
|
+
inputFingerprint: 'gate_disabled',
|
|
414
|
+
requests: [],
|
|
415
|
+
effectFree: false,
|
|
416
|
+
}),
|
|
417
|
+
};
|
|
418
|
+
return classifyResultToGateVerdict({
|
|
419
|
+
result,
|
|
277
420
|
mode: ctx.config.mode,
|
|
278
421
|
permission: 'allow',
|
|
279
422
|
wouldBlock: false,
|
|
280
423
|
});
|
|
281
424
|
}
|
|
282
425
|
const classifierOptions = runtimeClassifierOptions(ctx, ctx.config);
|
|
283
|
-
const
|
|
426
|
+
const approvedForAuth = await deps.loadApprovals(ctx, 'approved-approvals.json');
|
|
427
|
+
const authorization = await loadClassifierAuthorization({
|
|
428
|
+
repoRoot: ctx.repoRoot,
|
|
429
|
+
config: ctx.config,
|
|
430
|
+
approvedState: compactApprovals(approvedForAuth.state),
|
|
431
|
+
});
|
|
432
|
+
const egress = await egressStatus({ targetDir: ctx.repoRoot });
|
|
433
|
+
const egressProxyActive = isEgressProxyActive({
|
|
434
|
+
config: ctx.config,
|
|
435
|
+
running: egress.running,
|
|
436
|
+
foreignProxy: egress.foreignProxy,
|
|
437
|
+
repoRootMismatch: egress.repoRootMismatch,
|
|
438
|
+
});
|
|
439
|
+
const enrichedClassifierOptions = {
|
|
440
|
+
...classifierOptions,
|
|
441
|
+
...authorization,
|
|
442
|
+
egressProxyActive,
|
|
443
|
+
};
|
|
444
|
+
const predicted = await classifyGatedActionAsync(action, ctx.config, enrichedClassifierOptions);
|
|
284
445
|
let result = predicted;
|
|
285
446
|
let predictedAssessment;
|
|
286
447
|
let observedAssessment;
|
|
287
448
|
let transactionalLayer;
|
|
288
|
-
|
|
449
|
+
const recoveryCandidate = isTransactionalEligible(ctx.config, params.kind, predicted) &&
|
|
289
450
|
params.kind === 'shell' &&
|
|
290
|
-
params.command)
|
|
451
|
+
Boolean(params.command);
|
|
452
|
+
if (recoveryCandidate && params.command) {
|
|
291
453
|
const transactional = ctx.config.policy.transactional;
|
|
454
|
+
const boundaryDriverId = ctx.config.capability?.boundaryDriver ??
|
|
455
|
+
(ctx.config.sandbox.runtime === 'container' ? 'container' : 'host-integration');
|
|
456
|
+
const boundaryContext = await resolveBoundaryDriverContext({
|
|
457
|
+
repoRoot: ctx.repoRoot,
|
|
458
|
+
config: ctx.config,
|
|
459
|
+
driverId: boundaryDriverId,
|
|
460
|
+
egressProxyRunning: egressProxyActive,
|
|
461
|
+
});
|
|
292
462
|
const txResult = await runTransactionalExecution({
|
|
293
463
|
command: params.command,
|
|
294
464
|
cwd: params.cwd,
|
|
295
465
|
repoRoot: ctx.repoRoot,
|
|
296
|
-
stateDir:
|
|
466
|
+
stateDir: belayStateDir(ctx.config, ctx.layout.repoLocalStateDir(ctx.repoRoot)),
|
|
297
467
|
timeoutMs: transactional.timeoutMs,
|
|
298
468
|
predicted,
|
|
299
469
|
diffContext: {
|
|
300
470
|
repoRoot: ctx.repoRoot,
|
|
301
471
|
sensitivePaths: ctx.config.classifier.sensitivePaths,
|
|
302
|
-
protectedRoots:
|
|
472
|
+
protectedRoots: enrichedClassifierOptions.protectedArtifactRoots ?? [],
|
|
303
473
|
maxDeletionCount: transactional.maxDeletionCount,
|
|
304
474
|
},
|
|
475
|
+
boundaryContext,
|
|
476
|
+
dirtyIgnoreRoots: protectedArtifactRoots(ctx.layout, ctx.repoRoot, ctx.config.controlPlane.enabled ? configuredControlPlaneDir(ctx.config) : null),
|
|
477
|
+
fileCheckpoint: transactional.fileCheckpoint,
|
|
478
|
+
checkpoint: transactional.checkpoint,
|
|
305
479
|
});
|
|
306
480
|
if (!txResult.skipped && txResult.observed) {
|
|
307
481
|
result = txResult.result;
|
|
@@ -309,16 +483,49 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
309
483
|
observedAssessment = txResult.observed.assessment;
|
|
310
484
|
transactionalLayer = {
|
|
311
485
|
transactional: true,
|
|
486
|
+
transactionalBackend: txResult.transactionalBackend,
|
|
487
|
+
resourceKind: txResult.resourceKind,
|
|
488
|
+
baselineTreeHash: txResult.baselineTreeHash,
|
|
489
|
+
snapshotFileCount: txResult.snapshotFileCount,
|
|
490
|
+
snapshotSourceBytes: txResult.snapshotSourceBytes,
|
|
491
|
+
snapshotWorkspaceBytes: txResult.snapshotWorkspaceBytes,
|
|
492
|
+
snapshotCopyStrategy: txResult.snapshotCopyStrategy,
|
|
493
|
+
snapshotPrepareMs: txResult.snapshotPrepareMs,
|
|
494
|
+
executionRoute: `boundary:${boundaryContext.driverId}`,
|
|
495
|
+
enforcementStatus: 'mediated_unattested',
|
|
312
496
|
transactionalReason: txResult.observed.reason,
|
|
313
497
|
transactionalCategories: txResult.observed.categories,
|
|
314
498
|
transactionalChangeCount: txResult.observed.changes.length,
|
|
315
499
|
transactionalTimedOut: txResult.timedOut === true,
|
|
500
|
+
...(txResult.recoveryCheckpointId
|
|
501
|
+
? {
|
|
502
|
+
recoveryCheckpointId: txResult.recoveryCheckpointId,
|
|
503
|
+
recoveryBackend: txResult.recoveryBackend,
|
|
504
|
+
recoveryProofHash: txResult.recoveryProofHash,
|
|
505
|
+
recoveryState: txResult.recoveryState,
|
|
506
|
+
}
|
|
507
|
+
: {}),
|
|
316
508
|
};
|
|
317
509
|
}
|
|
318
|
-
else
|
|
510
|
+
else {
|
|
511
|
+
const skipReason = txResult.skipReason ?? 'recovery_observation_failed';
|
|
512
|
+
const failReason = skipReason === FILE_CHECKPOINT_ISOLATION_UNAVAILABLE
|
|
513
|
+
? FILE_CHECKPOINT_ISOLATION_UNAVAILABLE
|
|
514
|
+
: recoveryFailReasonFromSkip(skipReason);
|
|
515
|
+
result = recoveryFailClosedResult(predicted, failReason, [skipReason]);
|
|
319
516
|
transactionalLayer = {
|
|
320
517
|
transactional: false,
|
|
321
|
-
|
|
518
|
+
transactionalBackend: txResult.transactionalBackend,
|
|
519
|
+
resourceKind: txResult.resourceKind,
|
|
520
|
+
baselineTreeHash: txResult.baselineTreeHash,
|
|
521
|
+
snapshotFileCount: txResult.snapshotFileCount,
|
|
522
|
+
snapshotSourceBytes: txResult.snapshotSourceBytes,
|
|
523
|
+
snapshotWorkspaceBytes: txResult.snapshotWorkspaceBytes,
|
|
524
|
+
snapshotCopyStrategy: txResult.snapshotCopyStrategy,
|
|
525
|
+
snapshotPrepareMs: txResult.snapshotPrepareMs,
|
|
526
|
+
transactionalSkipReason: skipReason,
|
|
527
|
+
transactionalSkipDetail: txResult.skipDetail,
|
|
528
|
+
recoveryFailClosed: true,
|
|
322
529
|
};
|
|
323
530
|
}
|
|
324
531
|
}
|
|
@@ -353,7 +560,7 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
353
560
|
fingerprint: result.fingerprint,
|
|
354
561
|
repoRoot: ctx.repoRoot,
|
|
355
562
|
},
|
|
356
|
-
classifierOptions,
|
|
563
|
+
classifierOptions: enrichedClassifierOptions,
|
|
357
564
|
scopeHintPayload: params.payload,
|
|
358
565
|
});
|
|
359
566
|
}
|
|
@@ -392,9 +599,58 @@ export async function gateUnmappedToolVerdict(ctx, deps, toolName, payload) {
|
|
|
392
599
|
},
|
|
393
600
|
});
|
|
394
601
|
}
|
|
602
|
+
async function consumeCapabilityGrantIfUsed(ctx, deps, result) {
|
|
603
|
+
if (result.reason !== 'capability_grant' &&
|
|
604
|
+
result.authorizationDecision?.matchedRule !== 'grant.exact') {
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
if (!canConsumeCapabilityGrantLease(result)) {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
const requests = result.capabilityRequests ?? [];
|
|
611
|
+
if (!requests.length) {
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
const consumed = await mutateApprovalStateWithRetry({
|
|
615
|
+
load: () => deps.loadApprovals(ctx, 'approved-approvals.json'),
|
|
616
|
+
write: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
617
|
+
mutate: (state) => {
|
|
618
|
+
const compacted = compactApprovals(state);
|
|
619
|
+
const lease = consumeGrantLeasesForRequests(compacted, requests);
|
|
620
|
+
if (!lease.consumed) {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
return { state: lease.state, result: true };
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
return consumed === true;
|
|
627
|
+
}
|
|
395
628
|
async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {}) {
|
|
629
|
+
if (!result.effectPlan) {
|
|
630
|
+
result = {
|
|
631
|
+
...result,
|
|
632
|
+
effectPlan: buildCapabilityEffectPlan({
|
|
633
|
+
actionKind: kind,
|
|
634
|
+
summary: result.normalizedCommand ?? result.summary ?? kind,
|
|
635
|
+
inputFingerprint: result.fingerprint,
|
|
636
|
+
requests: result.capabilityRequests ?? [],
|
|
637
|
+
effectFree: result.reason === 'read_only',
|
|
638
|
+
}),
|
|
639
|
+
};
|
|
640
|
+
}
|
|
396
641
|
const replayContext = buildAuditReplayContext(kind, result, auditExtras.replayAction);
|
|
397
642
|
const actionSnapshot = buildAuditActionSnapshot(kind, result, auditExtras.replayAction);
|
|
643
|
+
const providerId = String(ctx.config.judge?.providerId ?? 'cursor');
|
|
644
|
+
const stateDir = belayStateDir(ctx.config, ctx.layout.repoLocalStateDir(ctx.repoRoot));
|
|
645
|
+
const capabilityAudit = scheduleGateShadowAudit({
|
|
646
|
+
repoRoot: ctx.repoRoot,
|
|
647
|
+
config: ctx.config,
|
|
648
|
+
providerId,
|
|
649
|
+
result,
|
|
650
|
+
command: auditExtras.replayAction?.command ??
|
|
651
|
+
(kind === 'shell' ? auditExtras.approvalInput?.input : undefined),
|
|
652
|
+
stateDir,
|
|
653
|
+
});
|
|
398
654
|
const gateBase = {
|
|
399
655
|
event: gateAuditEventName(kind),
|
|
400
656
|
kind,
|
|
@@ -406,10 +662,92 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
406
662
|
mode: ctx.config.mode,
|
|
407
663
|
schemaVersion: result.axes ? 2 : 1,
|
|
408
664
|
...(result.axes ?? {}),
|
|
665
|
+
...capabilityAudit,
|
|
409
666
|
...auditExtras.transactionalLayer,
|
|
410
667
|
...(replayContext ? { replayContext } : {}),
|
|
411
668
|
...(actionSnapshot ? { actionSnapshot } : {}),
|
|
412
669
|
};
|
|
670
|
+
const denyWithPendingApproval = async (failure) => {
|
|
671
|
+
const { approval, created } = await ensurePendingApproval(ctx, deps, kind, result, auditExtras.approvalInput, deriveWorkspaceRootScopeHint({
|
|
672
|
+
result,
|
|
673
|
+
replayAction: auditExtras.replayAction,
|
|
674
|
+
payloadForScopeHint: auditExtras.scopeHintPayload,
|
|
675
|
+
options: auditExtras.classifierOptions ?? {},
|
|
676
|
+
}));
|
|
677
|
+
if (created) {
|
|
678
|
+
await recordGateApprovalAsk(stateDir, result.reason, false);
|
|
679
|
+
}
|
|
680
|
+
let approvalToken;
|
|
681
|
+
try {
|
|
682
|
+
approvalToken = await issueApprovalToken({
|
|
683
|
+
approvalId: approval.approvalId,
|
|
684
|
+
fingerprint: approval.fingerprint,
|
|
685
|
+
repoRoot: approval.repoRoot,
|
|
686
|
+
issuedAt: approval.createdAt,
|
|
687
|
+
expiresAt: approval.expiresAt,
|
|
688
|
+
}, configuredControlPlaneDir(ctx.config));
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
approvalToken = undefined;
|
|
692
|
+
}
|
|
693
|
+
const denialReason = failure?.reason ?? result.reason;
|
|
694
|
+
if (ctx.config.notifications.webhookUrl || ctx.config.notifications.commandHook) {
|
|
695
|
+
await notifyDeny(ctx.config.notifications, {
|
|
696
|
+
approvalId: approval.approvalId,
|
|
697
|
+
reason: denialReason,
|
|
698
|
+
summary: result.normalizedCommand ?? result.summary ?? '',
|
|
699
|
+
repoRoot: ctx.repoRoot,
|
|
700
|
+
fingerprint: result.fingerprint,
|
|
701
|
+
approvalToken,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
const failureAuditFields = typeof failure?.auditFields === 'function'
|
|
705
|
+
? failure.auditFields(approval)
|
|
706
|
+
: failure?.auditFields;
|
|
707
|
+
await deps.appendAudit(ctx, {
|
|
708
|
+
...gateBase,
|
|
709
|
+
verdict: failure ? 'deny_pending_approval' : result.verdict,
|
|
710
|
+
reason: denialReason,
|
|
711
|
+
approvalId: approval.approvalId,
|
|
712
|
+
...(failure?.grantBundleFailureReason
|
|
713
|
+
? { grantBundleFailureReason: failure.grantBundleFailureReason }
|
|
714
|
+
: {}),
|
|
715
|
+
...failureAuditFields,
|
|
716
|
+
wouldBlock: true,
|
|
717
|
+
permission: 'deny',
|
|
718
|
+
});
|
|
719
|
+
const adapter = adapterIdFromContext(ctx);
|
|
720
|
+
const autoReplayShell = kind === 'shell' && canAutoReplay(ctx.config, kind, adapter);
|
|
721
|
+
return classifyResultToGateVerdict({
|
|
722
|
+
result: failure
|
|
723
|
+
? { ...result, verdict: 'deny_pending_approval', reason: denialReason }
|
|
724
|
+
: result,
|
|
725
|
+
mode: ctx.config.mode,
|
|
726
|
+
permission: 'deny',
|
|
727
|
+
wouldBlock: true,
|
|
728
|
+
approvalId: approval.approvalId,
|
|
729
|
+
user_message: failure?.userMessage(approval.approvalId) ??
|
|
730
|
+
`Belay blocked this high-risk action. Approval ID: ${approval.approvalId}. ${buildRetryInstructionForConfig(ctx.config, ctx.config.tokenPrefix, approval.approvalId, kind, adapter)} For details, run belay explain or /belay why.`,
|
|
731
|
+
agent_message: failure?.agentMessage ??
|
|
732
|
+
(autoReplayShell
|
|
733
|
+
? `Belay denied this action as ${result.reason}. Wait for approval; Belay will replay the exact shell action automatically. Do not retry unless replay fails.`
|
|
734
|
+
: `Belay denied this action as ${result.reason}. Wait for approval, then retry the exact same action once.`),
|
|
735
|
+
});
|
|
736
|
+
};
|
|
737
|
+
const denyReplayMismatch = async (approval, mismatchKind, messages) => {
|
|
738
|
+
await discardApprovedApproval(ctx, deps, approval.approvalId);
|
|
739
|
+
return denyWithPendingApproval({
|
|
740
|
+
reason: 'approval_replay_mismatch',
|
|
741
|
+
auditFields: (replacement) => ({
|
|
742
|
+
rejectedApprovalId: approval.approvalId,
|
|
743
|
+
rejectedApprovalCorrelationId: approvalCorrelationId(approval.approvalId),
|
|
744
|
+
replacementApprovalCorrelationId: approvalCorrelationId(replacement.approvalId),
|
|
745
|
+
replayMismatchKind: mismatchKind,
|
|
746
|
+
}),
|
|
747
|
+
userMessage: (approvalId) => `${messages.user} New approval ID: ${approvalId}.`,
|
|
748
|
+
agentMessage: `${messages.agent} Wait for the new exact approval, then retry once.`,
|
|
749
|
+
});
|
|
750
|
+
};
|
|
413
751
|
if (result.reason === TRANSACTIONAL_ALREADY_APPLIED) {
|
|
414
752
|
const userMessage = 'Belay executed this command safely in an isolated git worktree. Observed-safe file changes are already applied; do not retry the same command.';
|
|
415
753
|
const agentMessage = 'Belay already applied the observed-safe effects of this shell command in isolation. Do not run it again.';
|
|
@@ -470,40 +808,56 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
470
808
|
entry.fingerprint === result.fingerprint &&
|
|
471
809
|
entry.repoRoot === ctx.repoRoot);
|
|
472
810
|
if (!shouldSkipBrokerApprovedRecord(brokerActive, matchedApproval?.reason)) {
|
|
811
|
+
if (matchedApproval?.capabilityRequestHash &&
|
|
812
|
+
(!result.capabilityRequests?.length ||
|
|
813
|
+
matchedApproval.capabilityRequestHash !==
|
|
814
|
+
hashCapabilityRequests(result.capabilityRequests))) {
|
|
815
|
+
return denyReplayMismatch(matchedApproval, 'capability_requests', {
|
|
816
|
+
user: 'Belay denied this action because capability requests changed after approval. Re-approve the exact action or run belay explain.',
|
|
817
|
+
agent: 'Belay denied this action because capability requests changed after approval.',
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
if (matchedApproval?.effectPlanHash &&
|
|
821
|
+
(!result.effectPlan || matchedApproval.effectPlanHash !== hashEffectPlan(result.effectPlan))) {
|
|
822
|
+
return denyReplayMismatch(matchedApproval, 'effect_plan', {
|
|
823
|
+
user: 'Belay denied this action because the effect plan changed after approval. Re-approve the exact action or run belay explain.',
|
|
824
|
+
agent: 'Belay denied this action because the effect plan changed after approval.',
|
|
825
|
+
});
|
|
826
|
+
}
|
|
473
827
|
if (matchedApproval &&
|
|
474
828
|
auditExtras.replayAction &&
|
|
475
829
|
!validateReplayEnvelope(matchedApproval, auditExtras.replayAction)) {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
reason: 'approval_replay_mismatch',
|
|
480
|
-
approvalId: matchedApproval.approvalId,
|
|
481
|
-
wouldBlock: true,
|
|
482
|
-
permission: 'deny',
|
|
483
|
-
});
|
|
484
|
-
return classifyResultToGateVerdict({
|
|
485
|
-
result: {
|
|
486
|
-
...result,
|
|
487
|
-
verdict: 'deny_pending_approval',
|
|
488
|
-
reason: 'approval_replay_mismatch',
|
|
489
|
-
},
|
|
490
|
-
mode: ctx.config.mode,
|
|
491
|
-
permission: 'deny',
|
|
492
|
-
wouldBlock: true,
|
|
493
|
-
approvalId: matchedApproval.approvalId,
|
|
494
|
-
user_message: 'Belay denied this action because it does not match the approved replay envelope. Re-approve the exact action or run belay explain.',
|
|
495
|
-
agent_message: 'Belay denied this action because cwd, tool, or payload changed after approval.',
|
|
830
|
+
return denyReplayMismatch(matchedApproval, 'replay_envelope', {
|
|
831
|
+
user: 'Belay denied this action because it does not match the approved replay envelope. Re-approve the exact action or run belay explain.',
|
|
832
|
+
agent: 'Belay denied this action because cwd, tool, or payload changed after approval.',
|
|
496
833
|
});
|
|
497
834
|
}
|
|
498
|
-
approved = await consumeApprovedApproval(ctx, deps, kind, result.fingerprint);
|
|
835
|
+
approved = await consumeApprovedApproval(ctx, deps, kind, result.fingerprint, result.capabilityRequests ?? []);
|
|
499
836
|
}
|
|
500
837
|
}
|
|
501
|
-
if (approved) {
|
|
838
|
+
if (approved?.status === 'invalid_bundle') {
|
|
839
|
+
return denyWithPendingApproval({
|
|
840
|
+
reason: 'capability_grant_unavailable',
|
|
841
|
+
grantBundleFailureReason: approved.reason,
|
|
842
|
+
auditFields: (replacement) => ({
|
|
843
|
+
rejectedApprovalId: approved.approval.approvalId,
|
|
844
|
+
rejectedApprovalCorrelationId: approvalCorrelationId(approved.approval.approvalId),
|
|
845
|
+
replacementApprovalCorrelationId: approvalCorrelationId(replacement.approvalId),
|
|
846
|
+
}),
|
|
847
|
+
userMessage: (approvalId) => `Belay denied this action because its approved capability bundle did not exactly match the current request. New approval ID: ${approvalId}. Re-approve the exact action or run belay explain.`,
|
|
848
|
+
agentMessage: `Belay denied this action because the approved capability bundle failed exact validation (${approved.reason}). Wait for the new exact approval, then retry once.`,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
if (approved?.status === 'consumed') {
|
|
852
|
+
if (approved.firstExecution) {
|
|
853
|
+
await recordGateApprovalAsk(stateDir, result.reason, true);
|
|
854
|
+
}
|
|
502
855
|
await deps.appendAudit(ctx, {
|
|
503
856
|
...gateBase,
|
|
504
857
|
verdict: 'allow',
|
|
505
858
|
reason: 'approved_once',
|
|
506
|
-
approvalId: approved.approvalId,
|
|
859
|
+
approvalId: approved.approval.approvalId,
|
|
860
|
+
authorizationMode: approved.approval.grantBundleVersion === 1 ? 'exact_bundle' : 'legacy_approval',
|
|
507
861
|
wouldBlock: false,
|
|
508
862
|
permission: 'allow',
|
|
509
863
|
});
|
|
@@ -512,10 +866,32 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
512
866
|
mode: ctx.config.mode,
|
|
513
867
|
permission: 'allow',
|
|
514
868
|
wouldBlock: false,
|
|
515
|
-
approvalId: approved.approvalId,
|
|
869
|
+
approvalId: approved.approval.approvalId,
|
|
516
870
|
});
|
|
517
871
|
}
|
|
518
872
|
if (result.verdict === 'allow' || result.verdict === 'allow_flagged') {
|
|
873
|
+
const grantConsumed = await consumeCapabilityGrantIfUsed(ctx, deps, result);
|
|
874
|
+
if (!grantConsumed) {
|
|
875
|
+
await deps.appendAudit(ctx, {
|
|
876
|
+
...gateBase,
|
|
877
|
+
verdict: 'deny_pending_approval',
|
|
878
|
+
reason: 'capability_grant_unavailable',
|
|
879
|
+
wouldBlock: true,
|
|
880
|
+
permission: 'deny',
|
|
881
|
+
});
|
|
882
|
+
return classifyResultToGateVerdict({
|
|
883
|
+
result: {
|
|
884
|
+
...result,
|
|
885
|
+
verdict: 'deny_pending_approval',
|
|
886
|
+
reason: 'capability_grant_unavailable',
|
|
887
|
+
},
|
|
888
|
+
mode: ctx.config.mode,
|
|
889
|
+
permission: 'deny',
|
|
890
|
+
wouldBlock: true,
|
|
891
|
+
user_message: 'Belay denied this action because the capability grant could not be consumed. Re-approve or run belay explain.',
|
|
892
|
+
agent_message: 'Belay denied this action because the capability grant was missing, expired, or already used.',
|
|
893
|
+
});
|
|
894
|
+
}
|
|
519
895
|
await deps.appendAudit(ctx, {
|
|
520
896
|
...gateBase,
|
|
521
897
|
verdict: result.verdict,
|
|
@@ -545,58 +921,62 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
545
921
|
wouldBlock: true,
|
|
546
922
|
});
|
|
547
923
|
}
|
|
548
|
-
|
|
549
|
-
result
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
924
|
+
if (isJudgeInfrastructureFailure(result)) {
|
|
925
|
+
const fallbackReason = extractJudgeFallbackReason(result);
|
|
926
|
+
const providerId = inferProviderIdFromFallbackReason(fallbackReason, String(ctx.config.judge.providerId ?? 'cursor'));
|
|
927
|
+
const denyMessages = formatJudgeInfrastructureDenyMessage({
|
|
928
|
+
providerId,
|
|
929
|
+
fallbackReason,
|
|
930
|
+
command: result.normalizedCommand ?? result.summary,
|
|
931
|
+
});
|
|
932
|
+
await deps.appendAudit(ctx, {
|
|
933
|
+
...gateBase,
|
|
934
|
+
verdict: result.verdict,
|
|
935
|
+
reason: 'judge_transport_unavailable',
|
|
936
|
+
judgeFallbackReason: fallbackReason,
|
|
937
|
+
wouldBlock: true,
|
|
938
|
+
permission: 'deny',
|
|
939
|
+
});
|
|
940
|
+
return classifyResultToGateVerdict({
|
|
941
|
+
result: {
|
|
942
|
+
...result,
|
|
943
|
+
verdict: 'deny_pending_approval',
|
|
944
|
+
reason: 'judge_transport_unavailable',
|
|
945
|
+
},
|
|
946
|
+
mode: ctx.config.mode,
|
|
947
|
+
permission: 'deny',
|
|
948
|
+
wouldBlock: true,
|
|
949
|
+
user_message: denyMessages.user_message,
|
|
950
|
+
agent_message: denyMessages.agent_message,
|
|
951
|
+
});
|
|
566
952
|
}
|
|
567
|
-
if (
|
|
568
|
-
|
|
569
|
-
|
|
953
|
+
if (result.reason === FILE_CHECKPOINT_ISOLATION_UNAVAILABLE) {
|
|
954
|
+
const userMessage = 'Belay cannot run file_checkpoint because attested workspace isolation is unavailable. Run `belay session start`, then retry.';
|
|
955
|
+
const agentMessage = 'Belay denied this action because file_checkpoint lacks a fresh matching workspace-isolation attestation. Ask the operator to run `belay session start`, then retry the exact action.';
|
|
956
|
+
await deps.appendAudit(ctx, {
|
|
957
|
+
...gateBase,
|
|
958
|
+
verdict: result.verdict,
|
|
570
959
|
reason: result.reason,
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
960
|
+
wouldBlock: true,
|
|
961
|
+
permission: 'deny',
|
|
962
|
+
});
|
|
963
|
+
return classifyResultToGateVerdict({
|
|
964
|
+
result,
|
|
965
|
+
mode: ctx.config.mode,
|
|
966
|
+
permission: 'deny',
|
|
967
|
+
wouldBlock: true,
|
|
968
|
+
user_message: userMessage,
|
|
969
|
+
agent_message: agentMessage,
|
|
575
970
|
});
|
|
576
971
|
}
|
|
577
|
-
|
|
578
|
-
...gateBase,
|
|
579
|
-
verdict: result.verdict,
|
|
580
|
-
reason: result.reason,
|
|
581
|
-
approvalId: approval.approvalId,
|
|
582
|
-
wouldBlock: true,
|
|
583
|
-
permission: 'deny',
|
|
584
|
-
});
|
|
585
|
-
return classifyResultToGateVerdict({
|
|
586
|
-
result,
|
|
587
|
-
mode: ctx.config.mode,
|
|
588
|
-
permission: 'deny',
|
|
589
|
-
wouldBlock: true,
|
|
590
|
-
approvalId: approval.approvalId,
|
|
591
|
-
user_message: `Belay blocked this high-risk action. Approval ID: ${approval.approvalId}. ${buildRetryInstructionForConfig(ctx.config, ctx.config.tokenPrefix, approval.approvalId)} For details, run belay explain or /belay why.`,
|
|
592
|
-
agent_message: `Belay denied this action as ${result.reason}. Wait for approval, then retry the exact same action once.`,
|
|
593
|
-
});
|
|
972
|
+
return denyWithPendingApproval();
|
|
594
973
|
}
|
|
595
974
|
export async function processApprovalPrompt(ctx, deps, prompt) {
|
|
596
975
|
const approvalId = approvalCommandMatch(prompt, ctx.config.tokenPrefix);
|
|
597
976
|
if (!approvalId) {
|
|
598
977
|
return { continue: true };
|
|
599
978
|
}
|
|
979
|
+
const hasFollowupInstruction = prompt.split(/\r?\n/).filter((line) => line.trim().length > 0).length > 1;
|
|
600
980
|
const adapter = adapterIdFromContext(ctx);
|
|
601
981
|
if (ctx.config.approvalSigning.required) {
|
|
602
982
|
const message = `Signed approval token required for ${approvalId}. Editor prompt approval is disabled in this configuration. ` +
|
|
@@ -639,62 +1019,68 @@ export async function processApprovalPrompt(ctx, deps, prompt) {
|
|
|
639
1019
|
};
|
|
640
1020
|
}
|
|
641
1021
|
const replay = recorded.approval ? buildReplayHint(ctx.config, recorded.approval, adapter) : null;
|
|
642
|
-
const
|
|
643
|
-
replay?.kind === 'shell' &&
|
|
1022
|
+
const shouldReplayApprovedShell = replay?.kind === 'shell' &&
|
|
644
1023
|
replay.autoReplay === true &&
|
|
645
1024
|
Boolean(recorded.approval?.input) &&
|
|
646
1025
|
(!process.env.VITEST || process.env.BELAY_TEST_APPROVAL_REPLAY === '1');
|
|
647
|
-
if (
|
|
1026
|
+
if (shouldReplayApprovedShell && recorded.approval) {
|
|
648
1027
|
const approvalStore = gateApprovalStoreFromDeps({
|
|
649
1028
|
loadApprovals: (fileName) => deps.loadApprovals(ctx, fileName),
|
|
650
1029
|
writeApprovals: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
651
1030
|
});
|
|
1031
|
+
let claimed;
|
|
1032
|
+
try {
|
|
1033
|
+
claimed = await claimApprovedForReplay({ approvalId, store: approvalStore });
|
|
1034
|
+
}
|
|
1035
|
+
catch {
|
|
1036
|
+
return {
|
|
1037
|
+
continue: false,
|
|
1038
|
+
user_message: `Belay approval ${approvalId} could not be claimed for one-step replay. ` +
|
|
1039
|
+
'The shell action was not started.',
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
if (!claimed) {
|
|
1043
|
+
return {
|
|
1044
|
+
continue: false,
|
|
1045
|
+
user_message: `Belay approval ${approvalId} could not be claimed for one-step replay.`,
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
652
1048
|
let replayResult;
|
|
653
1049
|
try {
|
|
654
|
-
replayResult = await
|
|
1050
|
+
replayResult = await deps.replayApprovedShell(ctx, claimed.input ?? '', claimed.cwd ?? ctx.repoRoot, getExecutionLeaseMs(ctx.config));
|
|
655
1051
|
}
|
|
656
1052
|
catch (error) {
|
|
657
|
-
const
|
|
658
|
-
await
|
|
1053
|
+
const cleanupUnconfirmed = isBoundaryCleanupError(error);
|
|
1054
|
+
const auditRecorded = await appendReplayAuditSafely(ctx, deps, {
|
|
659
1055
|
event: 'approval',
|
|
660
1056
|
kind: 'approval',
|
|
661
1057
|
verdict: 'allow',
|
|
662
1058
|
approvalId,
|
|
663
|
-
reason:
|
|
1059
|
+
reason: cleanupUnconfirmed
|
|
1060
|
+
? 'approval_replay_cleanup_unconfirmed'
|
|
1061
|
+
: 'approval_replay_error',
|
|
664
1062
|
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt,
|
|
665
1063
|
});
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
user_message: `Belay approval recorded for ${approvalId}, but one-step shell replay could not start (${detail}). ` +
|
|
669
|
-
'Approval remains active for one hook retry.',
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
const output = [replayResult.stdout, replayResult.stderr].filter(Boolean).join('\n').trim();
|
|
673
|
-
if (replayResult.exitCode === 0) {
|
|
674
|
-
try {
|
|
675
|
-
await consumeApprovedAfterCliReplay({
|
|
676
|
-
approvalId: approvalId,
|
|
677
|
-
store: approvalStore,
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
catch (error) {
|
|
681
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
682
|
-
await deps.appendAudit(ctx, {
|
|
683
|
-
event: 'approval',
|
|
684
|
-
kind: 'approval',
|
|
685
|
-
verdict: 'allow',
|
|
686
|
-
approvalId,
|
|
687
|
-
reason: 'approval_replay_consume_error',
|
|
688
|
-
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt,
|
|
689
|
-
});
|
|
1064
|
+
if (cleanupUnconfirmed) {
|
|
1065
|
+
const resourceId = safeBoundaryCleanupResourceId(error);
|
|
690
1066
|
return {
|
|
691
1067
|
continue: false,
|
|
692
|
-
user_message: `Belay approval recorded for ${approvalId}
|
|
693
|
-
|
|
694
|
-
|
|
1068
|
+
user_message: `Belay approval recorded for ${approvalId}. The one-step shell replay started, but cleanup could not be confirmed; the container may still be running.` +
|
|
1069
|
+
(resourceId ? ` Container: ${resourceId}.` : '') +
|
|
1070
|
+
' Inspect Docker and remove the container manually if it is still present. ' +
|
|
1071
|
+
'The one-shot approval was consumed and was not re-armed.' +
|
|
1072
|
+
(auditRecorded ? '' : REPLAY_AUDIT_FAILURE_NOTE),
|
|
695
1073
|
};
|
|
696
1074
|
}
|
|
697
|
-
|
|
1075
|
+
return {
|
|
1076
|
+
continue: false,
|
|
1077
|
+
user_message: `Belay approval recorded for ${approvalId}, but one-step shell replay could not start. ` +
|
|
1078
|
+
'The one-shot approval was consumed before replay and was not re-armed.' +
|
|
1079
|
+
(auditRecorded ? '' : REPLAY_AUDIT_FAILURE_NOTE),
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
if (replayResult.exitCode === 0 && !replayResult.timedOut) {
|
|
1083
|
+
const auditRecorded = await appendReplayAuditSafely(ctx, deps, {
|
|
698
1084
|
event: 'approval',
|
|
699
1085
|
kind: 'approval',
|
|
700
1086
|
verdict: 'allow',
|
|
@@ -703,12 +1089,12 @@ export async function processApprovalPrompt(ctx, deps, prompt) {
|
|
|
703
1089
|
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt,
|
|
704
1090
|
});
|
|
705
1091
|
return {
|
|
706
|
-
continue:
|
|
1092
|
+
continue: true,
|
|
707
1093
|
user_message: `Belay approval recorded for ${approvalId}. One-step shell replay succeeded; no manual retry required.` +
|
|
708
|
-
(
|
|
1094
|
+
(auditRecorded ? '' : REPLAY_AUDIT_FAILURE_NOTE),
|
|
709
1095
|
};
|
|
710
1096
|
}
|
|
711
|
-
await
|
|
1097
|
+
const auditRecorded = await appendReplayAuditSafely(ctx, deps, {
|
|
712
1098
|
event: 'approval',
|
|
713
1099
|
kind: 'approval',
|
|
714
1100
|
verdict: 'allow',
|
|
@@ -716,16 +1102,18 @@ export async function processApprovalPrompt(ctx, deps, prompt) {
|
|
|
716
1102
|
reason: 'approval_replay_failed',
|
|
717
1103
|
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt,
|
|
718
1104
|
});
|
|
719
|
-
const
|
|
1105
|
+
const failureSummary = replayResult.timedOut
|
|
1106
|
+
? ' One-step shell replay timed out.'
|
|
1107
|
+
: ` One-step shell replay failed (exit ${replayResult.exitCode}).`;
|
|
720
1108
|
return {
|
|
721
1109
|
continue: false,
|
|
722
|
-
user_message: `Belay approval recorded for ${approvalId}
|
|
723
|
-
|
|
724
|
-
(
|
|
1110
|
+
user_message: `Belay approval recorded for ${approvalId}.${failureSummary} ` +
|
|
1111
|
+
'The one-shot approval was consumed before replay and was not re-armed.' +
|
|
1112
|
+
(auditRecorded ? '' : REPLAY_AUDIT_FAILURE_NOTE),
|
|
725
1113
|
};
|
|
726
1114
|
}
|
|
727
1115
|
return {
|
|
728
|
-
continue:
|
|
1116
|
+
continue: replay !== null || hasFollowupInstruction,
|
|
729
1117
|
user_message: recorded.message,
|
|
730
1118
|
...(replay ? { replay } : {}),
|
|
731
1119
|
};
|
|
@@ -753,15 +1141,18 @@ export function gateVerdictToClaudePreToolUseResponse(verdict) {
|
|
|
753
1141
|
}
|
|
754
1142
|
export function gateVerdictToClaudeUserPromptResponse(verdict) {
|
|
755
1143
|
if (verdict.continue) {
|
|
756
|
-
return
|
|
1144
|
+
return verdict.user_message
|
|
1145
|
+
? {
|
|
1146
|
+
hookSpecificOutput: {
|
|
1147
|
+
hookEventName: 'UserPromptSubmit',
|
|
1148
|
+
additionalContext: verdict.user_message,
|
|
1149
|
+
},
|
|
1150
|
+
}
|
|
1151
|
+
: {};
|
|
757
1152
|
}
|
|
758
1153
|
return {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
continue: false,
|
|
762
|
-
user_message: verdict.user_message,
|
|
763
|
-
...(verdict.replay ? { replay: verdict.replay } : {}),
|
|
764
|
-
},
|
|
1154
|
+
decision: 'block',
|
|
1155
|
+
reason: verdict.user_message,
|
|
765
1156
|
};
|
|
766
1157
|
}
|
|
767
1158
|
// Codex PreToolUse deny contract is identical to Claude's
|
|
@@ -772,7 +1163,14 @@ export function gateVerdictToCodexPreToolUseResponse(verdict) {
|
|
|
772
1163
|
// Codex UserPromptSubmit blocks via `decision: "block"` (per developers.openai.com/codex/hooks).
|
|
773
1164
|
export function gateVerdictToCodexUserPromptResponse(verdict) {
|
|
774
1165
|
if (verdict.continue) {
|
|
775
|
-
return
|
|
1166
|
+
return verdict.user_message
|
|
1167
|
+
? {
|
|
1168
|
+
hookSpecificOutput: {
|
|
1169
|
+
hookEventName: 'UserPromptSubmit',
|
|
1170
|
+
additionalContext: verdict.user_message,
|
|
1171
|
+
},
|
|
1172
|
+
}
|
|
1173
|
+
: {};
|
|
776
1174
|
}
|
|
777
1175
|
return {
|
|
778
1176
|
decision: 'block',
|