@guilz-dev/belay 0.3.0 → 0.6.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 +7 -1
- package/dist/adapters/cursor/runtime-entry.js +1 -0
- package/dist/adapters/layouts/scope.js +3 -2
- package/dist/adapters/shared/gate-runtime.d.ts +12 -16
- package/dist/adapters/shared/gate-runtime.js +499 -43
- package/dist/bundle/claude-runtime.mjs +8004 -2676
- package/dist/bundle/codex-runtime.mjs +8021 -2683
- package/dist/bundle/cursor-runtime.mjs +7993 -2667
- package/dist/cli.js +180 -6
- package/dist/commands/approve.d.ts +1 -0
- package/dist/commands/approve.js +45 -26
- package/dist/commands/classify-for-report.js +1 -1
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +264 -58
- package/dist/commands/doctor.js +47 -2
- package/dist/commands/explain.js +17 -2
- package/dist/commands/harvest.d.ts +29 -0
- package/dist/commands/harvest.js +72 -0
- package/dist/commands/judge.d.ts +11 -1
- package/dist/commands/judge.js +24 -4
- package/dist/commands/metrics.js +34 -2
- package/dist/commands/quality.d.ts +38 -0
- package/dist/commands/quality.js +105 -0
- package/dist/commands/session.d.ts +20 -0
- package/dist/commands/session.js +51 -0
- package/dist/commands/simulate.d.ts +1 -0
- package/dist/commands/simulate.js +7 -2
- package/dist/commands/standing-allow.d.ts +10 -0
- package/dist/commands/standing-allow.js +26 -0
- package/dist/commands/tui.d.ts +48 -0
- package/dist/commands/tui.js +150 -0
- package/dist/config-io.js +59 -17
- 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 +14 -9
- package/dist/core/approval-replay-cli.d.ts +7 -0
- package/dist/core/approval-replay-cli.js +36 -0
- package/dist/core/approval-replay.d.ts +45 -0
- package/dist/core/approval-replay.js +141 -0
- package/dist/core/approval-service.d.ts +15 -0
- package/dist/core/approval-service.js +53 -4
- package/dist/core/approval.d.ts +26 -3
- package/dist/core/approval.js +61 -7
- package/dist/core/audit-analysis.d.ts +7 -1
- package/dist/core/audit-analysis.js +111 -0
- package/dist/core/audit-metrics.d.ts +7 -0
- package/dist/core/audit-metrics.js +24 -4
- package/dist/core/audit-query.d.ts +1 -0
- package/dist/core/audit-query.js +3 -0
- package/dist/core/audit-replay-context.d.ts +35 -0
- package/dist/core/audit-replay-context.js +88 -0
- package/dist/core/audit-types.d.ts +24 -1
- package/dist/core/audit-types.js +1 -1
- package/dist/core/capability/approval-state-mutation.d.ts +13 -0
- package/dist/core/capability/approval-state-mutation.js +92 -0
- package/dist/core/capability/approval-v3.d.ts +15 -0
- package/dist/core/capability/approval-v3.js +74 -0
- package/dist/core/capability/attestation.d.ts +13 -0
- package/dist/core/capability/attestation.js +55 -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 +7 -0
- package/dist/core/capability/boundary-driver-container.js +122 -0
- package/dist/core/capability/boundary-driver.d.ts +29 -0
- package/dist/core/capability/boundary-driver.js +43 -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 +13 -0
- package/dist/core/capability/boundary-grant-materialize.js +78 -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 +23 -0
- package/dist/core/capability/boundary-run.js +23 -0
- package/dist/core/capability/boundary-session.d.ts +57 -0
- package/dist/core/capability/boundary-session.js +129 -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 +108 -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-lease.d.ts +9 -0
- package/dist/core/capability/grant-lease.js +46 -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 +93 -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 +10 -1
- package/dist/core/capability/index.js +8 -0
- package/dist/core/capability/limits.d.ts +7 -0
- package/dist/core/capability/limits.js +55 -0
- package/dist/core/capability/paths.d.ts +2 -2
- package/dist/core/capability/paths.js +9 -9
- 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 +70 -0
- package/dist/core/capability/policy-engine.js +476 -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 +45 -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/capability/trusted-workspace-roots.d.ts +25 -0
- package/dist/core/capability/trusted-workspace-roots.js +149 -0
- package/dist/core/capability/types.d.ts +11 -1
- package/dist/core/capability-approval.d.ts +2 -1
- package/dist/core/capability-approval.js +100 -2
- package/dist/core/classify-subagent.d.ts +2 -1
- package/dist/core/classify-subagent.js +40 -22
- package/dist/core/classify-tool.js +157 -64
- package/dist/core/config.d.ts +32 -3
- package/dist/core/config.js +105 -3
- package/dist/core/gate-contract.d.ts +3 -0
- package/dist/core/gate-contract.js +3 -0
- package/dist/core/gate-engine.js +12 -7
- package/dist/core/harvest.d.ts +53 -0
- package/dist/core/harvest.js +276 -0
- package/dist/core/index.d.ts +9 -3
- package/dist/core/index.js +6 -2
- package/dist/core/judge-doctor.d.ts +3 -0
- package/dist/core/judge-doctor.js +90 -11
- package/dist/core/judge-fallback-hints.d.ts +13 -0
- package/dist/core/judge-fallback-hints.js +108 -0
- package/dist/core/path-utils.d.ts +12 -0
- package/dist/core/path-utils.js +60 -7
- package/dist/core/reclassify.d.ts +3 -0
- package/dist/core/reclassify.js +44 -14
- package/dist/core/recovery/capability.d.ts +3 -0
- package/dist/core/recovery/capability.js +22 -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 +3 -0
- package/dist/core/recovery/index.js +2 -0
- package/dist/core/recovery/types.d.ts +20 -0
- package/dist/core/recovery/types.js +1 -0
- package/dist/core/replay-scrub.d.ts +9 -0
- package/dist/core/replay-scrub.js +43 -0
- package/dist/core/shell-tokenizer.d.ts +2 -0
- package/dist/core/shell-tokenizer.js +82 -23
- package/dist/core/standing-allow.d.ts +50 -0
- package/dist/core/standing-allow.js +178 -0
- package/dist/core/transactional/diff-evaluator.js +4 -1
- package/dist/core/transactional/eligibility.js +4 -0
- package/dist/core/transactional/git-worktree.d.ts +10 -3
- package/dist/core/transactional/git-worktree.js +81 -11
- package/dist/core/transactional/reasons.d.ts +1 -0
- package/dist/core/transactional/reasons.js +4 -0
- package/dist/core/transactional/runner.js +22 -6
- package/dist/core/transactional/types.d.ts +4 -0
- package/dist/core/types.d.ts +36 -3
- package/dist/core/verdict/adapter.d.ts +3 -4
- package/dist/core/verdict/adapter.js +19 -6
- package/dist/core/verdict/containment.d.ts +4 -3
- package/dist/core/verdict/containment.js +17 -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.d.ts +1 -0
- package/dist/core/verdict/judge-audit.js +33 -1
- package/dist/core/verdict/judge-baseline.d.ts +20 -0
- package/dist/core/verdict/judge-baseline.js +49 -0
- package/dist/core/verdict/judge-broker-service.d.ts +42 -0
- package/dist/core/verdict/judge-broker-service.js +291 -0
- package/dist/core/verdict/judge-catalog.js +1 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +6 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +67 -0
- package/dist/core/verdict/judge-cli.d.ts +23 -0
- package/dist/core/verdict/judge-cli.js +139 -16
- package/dist/core/verdict/judge-factory.js +13 -4
- package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
- package/dist/core/verdict/judge-provider-matrix.js +66 -0
- package/dist/core/verdict/judge-runtime-config.d.ts +44 -0
- package/dist/core/verdict/judge-runtime-config.js +96 -0
- package/dist/core/verdict/judge-session-broker.d.ts +48 -0
- package/dist/core/verdict/judge-session-broker.js +200 -0
- package/dist/core/verdict/judge-session-guard.d.ts +27 -0
- package/dist/core/verdict/judge-session-guard.js +91 -0
- package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
- package/dist/core/verdict/judge-session-kill-switch.js +43 -0
- package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
- package/dist/core/verdict/judge-session-mutex.js +23 -0
- package/dist/core/verdict/judge-shadow.d.ts +24 -0
- package/dist/core/verdict/judge-shadow.js +116 -0
- package/dist/core/verdict/judge-transport.d.ts +44 -0
- package/dist/core/verdict/judge-transport.js +268 -0
- package/dist/core/verdict/judge.d.ts +14 -14
- package/dist/core/verdict/judge.js +5 -67
- package/dist/core/verdict/launcher-resolve.d.ts +2 -0
- package/dist/core/verdict/launcher-resolve.js +16 -0
- package/dist/core/verdict/parser.js +6 -1
- 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 +25 -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 +26 -1
- package/dist/core/verdict/verdict.js +300 -179
- package/dist/corpus/adversarial-probe.d.ts +86 -0
- package/dist/corpus/adversarial-probe.js +220 -0
- package/dist/corpus/evaluate.d.ts +45 -12
- package/dist/corpus/evaluate.js +61 -8
- package/dist/corpus/gate-latency-budget.d.ts +39 -0
- package/dist/corpus/gate-latency-budget.js +33 -0
- package/dist/corpus/gates.d.ts +35 -0
- package/dist/corpus/gates.js +81 -0
- package/dist/corpus/judge-accuracy.d.ts +8 -0
- package/dist/corpus/judge-accuracy.js +39 -0
- package/dist/corpus/must-allow-commands.d.ts +5 -0
- package/dist/corpus/must-allow-commands.js +13 -0
- package/dist/corpus/mutators.d.ts +28 -0
- package/dist/corpus/mutators.js +162 -0
- package/dist/corpus/ratchet.d.ts +42 -0
- package/dist/corpus/ratchet.js +116 -0
- package/dist/corpus/runtime-match.d.ts +19 -0
- package/dist/corpus/runtime-match.js +43 -0
- package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
- package/dist/corpus/standing-allow-catalog.generated.js +99 -0
- package/dist/corpus/types.d.ts +47 -0
- package/dist/corpus/types.js +157 -0
- package/dist/installer.js +4 -1
- package/dist/judge-broker-daemon.d.ts +1 -0
- package/dist/judge-broker-daemon.js +137 -0
- package/dist/services/sandbox-service.d.ts +5 -0
- package/dist/services/sandbox-service.js +30 -2
- package/dist/types.d.ts +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -3
- package/skills/belay/SKILL.md +7 -1
- package/skills/belay/belay-approve.md +17 -0
|
@@ -2,20 +2,48 @@ import { 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
|
-
import {
|
|
5
|
+
import { compactApprovals, createApprovalRecordWithEnvelope } from '../../core/approval.js';
|
|
6
|
+
import { buildReplayHint, buildRetryInstructionForConfig, getExecutionLeaseMs, replayShellCommand, validateReplayEnvelope, } from '../../core/approval-replay.js';
|
|
7
|
+
import { consumeApprovedAfterCliReplay, gateApprovalStoreFromDeps, recordApproval, } from '../../core/approval-service.js';
|
|
6
8
|
import { issueApprovalToken } from '../../core/approval-token.js';
|
|
7
|
-
import {
|
|
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 { resolveBoundaryDriverContext } from '../../core/capability/boundary-session.js';
|
|
14
|
+
import { hashCapabilityRequests } from '../../core/capability/capability-request-hash.js';
|
|
15
|
+
import { recordGateApprovalAsk, scheduleGateShadowAudit, } from '../../core/capability/gate-shadow-audit.js';
|
|
16
|
+
import { consumeGrantLease, findMatchingGrant, grantsFromApprovedState, } from '../../core/capability/grant-lease.js';
|
|
17
|
+
import { loadClassifierAuthorization } from '../../core/capability/grant-loader.js';
|
|
18
|
+
import { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload, fsScopeAllowlistPath, isCapabilityBrokerDemotionActive, loadFsScopeAllowlistSync, loadTrustedWorkspaceRootsSync, shouldSkipBrokerApprovedOnce, shouldSkipBrokerApprovedRecord, trustedWorkspaceRootsPath, validateTrustedWorkspaceRootCandidate, } from '../../core/capability/index.js';
|
|
8
19
|
import { resolveLayeredConfig, teamConfigPath } from '../../core/config-layers.js';
|
|
9
20
|
import { classifyResultToGateVerdict, unnormalizedGateVerdict, } from '../../core/gate-contract.js';
|
|
10
21
|
import { classifyGatedActionAsync, extractAgentAssessment, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from '../../core/gate-engine.js';
|
|
11
|
-
import {
|
|
22
|
+
import { approvalCommandMatch, approvedApprovalsFile, belayStateDir, canonicalStringify, classifierOptionsFromConfig, configuredControlPlaneDir, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, scrubValue, toolFingerprint, } from '../../core/index.js';
|
|
23
|
+
import { extractJudgeFallbackReason, formatJudgeInfrastructureDenyMessage, inferProviderIdFromFallbackReason, isJudgeInfrastructureFailure, } from '../../core/judge-fallback-hints.js';
|
|
12
24
|
import { notifyDeny } from '../../core/notify.js';
|
|
25
|
+
import { canonicalPath } from '../../core/path-utils.js';
|
|
26
|
+
import { recoveryFailClosedResult, recoveryFailReasonFromSkip, } from '../../core/recovery/fail-closed.js';
|
|
27
|
+
import { fingerprintReplayPayload } from '../../core/replay-scrub.js';
|
|
28
|
+
import { loadStandingAllow, resolveStandingAllowMatch, standingAllowFile, } from '../../core/standing-allow.js';
|
|
13
29
|
import { isTransactionalEligible, runTransactionalExecution, TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPROVAL_BYPASS_REASONS, } from '../../core/transactional/index.js';
|
|
30
|
+
import { egressStatus } from '../../services/egress-service.js';
|
|
14
31
|
import { protectedArtifactRoots } from '../layouts/protected-paths.js';
|
|
15
32
|
const EMPTY_APPROVALS = {
|
|
16
33
|
version: 1,
|
|
17
34
|
approvals: [],
|
|
18
35
|
};
|
|
36
|
+
function adapterIdFromContext(ctx) {
|
|
37
|
+
if (ctx.config.adapter === 'cursor' ||
|
|
38
|
+
ctx.config.adapter === 'claude' ||
|
|
39
|
+
ctx.config.adapter === 'codex') {
|
|
40
|
+
return ctx.config.adapter;
|
|
41
|
+
}
|
|
42
|
+
if (ctx.layout.name === 'cursor' || ctx.layout.name === 'claude' || ctx.layout.name === 'codex') {
|
|
43
|
+
return ctx.layout.name;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
19
47
|
async function loadJsonFile(filePath, fallback) {
|
|
20
48
|
try {
|
|
21
49
|
const raw = await readFile(filePath, 'utf8');
|
|
@@ -49,10 +77,12 @@ export function createDefaultGateRuntimeDeps() {
|
|
|
49
77
|
? pendingApprovalsFile(ctx.config, repoLocalStateDir)
|
|
50
78
|
: approvedApprovalsFile(ctx.config, repoLocalStateDir);
|
|
51
79
|
const loaded = await loadJsonFile(filePath, EMPTY_APPROVALS);
|
|
80
|
+
const version = loaded.version === 3 ? 3 : loaded.version === 2 ? 2 : 1;
|
|
52
81
|
return {
|
|
53
82
|
filePath,
|
|
54
83
|
state: {
|
|
55
|
-
version
|
|
84
|
+
version,
|
|
85
|
+
...(loaded.revision !== undefined ? { revision: loaded.revision } : {}),
|
|
56
86
|
approvals: Array.isArray(loaded.approvals) ? loaded.approvals : [],
|
|
57
87
|
},
|
|
58
88
|
};
|
|
@@ -90,11 +120,13 @@ export function repoShellClassifierOptions(config, repoRoot, layout, extras = {}
|
|
|
90
120
|
export function runtimeClassifierOptions(ctx, config) {
|
|
91
121
|
const repoLocalStateDir = ctx.layout.repoLocalStateDir(ctx.repoRoot);
|
|
92
122
|
const brokerFsScope = isCapabilityBrokerDemotionActive(config);
|
|
123
|
+
const trustedRoots = loadTrustedWorkspaceRootsSync(trustedWorkspaceRootsPath(config, repoLocalStateDir));
|
|
93
124
|
return repoShellClassifierOptions(config, ctx.repoRoot, ctx.layout, {
|
|
94
125
|
brokerFsScope,
|
|
95
126
|
fsScopeAllowlist: brokerFsScope
|
|
96
127
|
? loadFsScopeAllowlistSync(fsScopeAllowlistPath(config, repoLocalStateDir))
|
|
97
128
|
: undefined,
|
|
129
|
+
trustedWorkspaceRoots: trustedRoots.roots.map((entry) => entry.path),
|
|
98
130
|
});
|
|
99
131
|
}
|
|
100
132
|
function gateAuditEventName(kind) {
|
|
@@ -106,7 +138,7 @@ function gateAuditEventName(kind) {
|
|
|
106
138
|
}
|
|
107
139
|
return 'subagentGate';
|
|
108
140
|
}
|
|
109
|
-
async function ensurePendingApproval(ctx, deps, kind, result, approvalInput) {
|
|
141
|
+
async function ensurePendingApproval(ctx, deps, kind, result, approvalInput, scopeHint) {
|
|
110
142
|
const pending = await deps.loadApprovals(ctx, 'pending-approvals.json');
|
|
111
143
|
pending.state = compactApprovals(pending.state);
|
|
112
144
|
const existing = pending.state.approvals.find((approval) => approval.kind === kind &&
|
|
@@ -114,9 +146,9 @@ async function ensurePendingApproval(ctx, deps, kind, result, approvalInput) {
|
|
|
114
146
|
approval.repoRoot === ctx.repoRoot);
|
|
115
147
|
if (existing) {
|
|
116
148
|
await deps.writeApprovals(pending.filePath, pending.state);
|
|
117
|
-
return existing;
|
|
149
|
+
return { approval: existing, created: false };
|
|
118
150
|
}
|
|
119
|
-
const approval =
|
|
151
|
+
const approval = createApprovalRecordWithEnvelope({
|
|
120
152
|
kind,
|
|
121
153
|
fingerprint: result.fingerprint,
|
|
122
154
|
repoRoot: ctx.repoRoot,
|
|
@@ -124,35 +156,120 @@ async function ensurePendingApproval(ctx, deps, kind, result, approvalInput) {
|
|
|
124
156
|
summary: result.normalizedCommand ?? result.summary ?? '',
|
|
125
157
|
approvalTtlMinutes: ctx.config.approvalTtlMinutes,
|
|
126
158
|
approvalId: `belay_${randomUUID().replaceAll('-', '').slice(0, 12)}`,
|
|
127
|
-
|
|
128
|
-
|
|
159
|
+
approvalInput,
|
|
160
|
+
scopeHint,
|
|
161
|
+
capabilityRequests: result.capabilityRequests,
|
|
129
162
|
});
|
|
130
|
-
pending.state.version =
|
|
163
|
+
pending.state.version = APPROVAL_STATE_VERSION_V3;
|
|
164
|
+
pending.state.revision = (pending.state.revision ?? 0) + 1;
|
|
131
165
|
pending.state.approvals.push(approval);
|
|
132
166
|
await deps.writeApprovals(pending.filePath, pending.state);
|
|
133
|
-
return approval;
|
|
167
|
+
return { approval, created: true };
|
|
168
|
+
}
|
|
169
|
+
function extractShellCommandFromPayload(payload) {
|
|
170
|
+
const toolInput = payload.tool_input;
|
|
171
|
+
if (!toolInput || typeof toolInput !== 'object') {
|
|
172
|
+
return '';
|
|
173
|
+
}
|
|
174
|
+
const input = toolInput;
|
|
175
|
+
return typeof input.command === 'string' ? input.command : '';
|
|
176
|
+
}
|
|
177
|
+
function deriveWorkspaceRootScopeHint(params) {
|
|
178
|
+
if (params.result.reason !== 'outside_repo_mutation' &&
|
|
179
|
+
params.result.reason !== 'outside_repo_redirect') {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
const action = params.replayAction;
|
|
183
|
+
if (!action?.cwd) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
const outsidePaths = new Set();
|
|
187
|
+
if (action.kind === 'shell' && action.command) {
|
|
188
|
+
for (const resolved of collectOutsideRepoPaths(action.command, action.cwd, action.repoRoot, params.options.trustedWorkspaceRoots)) {
|
|
189
|
+
outsidePaths.add(resolved);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (action.kind === 'tool') {
|
|
193
|
+
const toolPayload = params.payloadForScopeHint ?? action.payload ?? {};
|
|
194
|
+
for (const resolved of collectOutsideRepoPathsFromToolPayload(toolPayload, action.cwd, action.repoRoot, params.options.trustedWorkspaceRoots)) {
|
|
195
|
+
outsidePaths.add(resolved);
|
|
196
|
+
}
|
|
197
|
+
const command = extractShellCommandFromPayload(toolPayload);
|
|
198
|
+
if (command) {
|
|
199
|
+
for (const resolved of collectOutsideRepoPaths(command, action.cwd, action.repoRoot, params.options.trustedWorkspaceRoots)) {
|
|
200
|
+
outsidePaths.add(resolved);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (outsidePaths.size !== 1) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
const targetPath = [...outsidePaths][0];
|
|
208
|
+
if (!targetPath) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
const candidateRoot = canonicalPath(path.dirname(targetPath));
|
|
212
|
+
const validation = validateTrustedWorkspaceRootCandidate({
|
|
213
|
+
candidatePath: candidateRoot,
|
|
214
|
+
repoRoot: action.repoRoot,
|
|
215
|
+
controlPlaneDir: params.options.controlPlaneDir ?? undefined,
|
|
216
|
+
protectedRoots: params.options.protectedArtifactRoots ?? [],
|
|
217
|
+
requireExistingDirectory: true,
|
|
218
|
+
requireNonGit: true,
|
|
219
|
+
});
|
|
220
|
+
if (!validation.ok) {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
return { scope: 'workspace-root', path: validation.normalizedPath };
|
|
134
224
|
}
|
|
135
225
|
async function consumeApprovedApproval(ctx, deps, kind, fingerprint) {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
const index =
|
|
226
|
+
const loaded = await deps.loadApprovals(ctx, 'approved-approvals.json');
|
|
227
|
+
loaded.state = compactApprovals(loaded.state);
|
|
228
|
+
const index = loaded.state.approvals.findIndex((approval) => approval.kind === kind &&
|
|
139
229
|
approval.fingerprint === fingerprint &&
|
|
140
230
|
approval.repoRoot === ctx.repoRoot);
|
|
141
231
|
if (index === -1) {
|
|
142
|
-
await deps.writeApprovals(
|
|
232
|
+
await deps.writeApprovals(loaded.filePath, loaded.state);
|
|
143
233
|
return null;
|
|
144
234
|
}
|
|
145
|
-
const approval =
|
|
235
|
+
const approval = loaded.state.approvals[index];
|
|
146
236
|
if (approval.executionLeaseExpiresAt) {
|
|
147
|
-
await deps.writeApprovals(
|
|
148
|
-
return approval;
|
|
237
|
+
await deps.writeApprovals(loaded.filePath, loaded.state);
|
|
238
|
+
return { approval, firstExecution: false };
|
|
149
239
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
240
|
+
const consumed = await mutateApprovalStateWithRetry({
|
|
241
|
+
load: () => deps.loadApprovals(ctx, 'approved-approvals.json'),
|
|
242
|
+
write: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
243
|
+
mutate: (state) => {
|
|
244
|
+
const compacted = compactApprovals(state);
|
|
245
|
+
const matchIndex = compacted.approvals.findIndex((approval) => approval.kind === kind &&
|
|
246
|
+
approval.fingerprint === fingerprint &&
|
|
247
|
+
approval.repoRoot === ctx.repoRoot);
|
|
248
|
+
if (matchIndex === -1) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const approval = compacted.approvals[matchIndex];
|
|
252
|
+
if (approval.grant && approval.grant.usesRemaining <= 0) {
|
|
253
|
+
compacted.approvals.splice(matchIndex, 1);
|
|
254
|
+
return { state: compacted, result: null };
|
|
255
|
+
}
|
|
256
|
+
compacted.approvals[matchIndex] = {
|
|
257
|
+
...approval,
|
|
258
|
+
executionLeaseExpiresAt: new Date(Date.now() + getExecutionLeaseMs(ctx.config)).toISOString(),
|
|
259
|
+
grant: approval.grant
|
|
260
|
+
? {
|
|
261
|
+
...approval.grant,
|
|
262
|
+
usesRemaining: Math.max(0, approval.grant.usesRemaining - 1),
|
|
263
|
+
}
|
|
264
|
+
: undefined,
|
|
265
|
+
};
|
|
266
|
+
return { state: compacted, result: approval };
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
if (!consumed) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
return { approval: consumed, firstExecution: true };
|
|
156
273
|
}
|
|
157
274
|
export async function evaluateGatedAction(ctx, deps, params) {
|
|
158
275
|
let action;
|
|
@@ -205,15 +322,42 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
205
322
|
});
|
|
206
323
|
}
|
|
207
324
|
const classifierOptions = runtimeClassifierOptions(ctx, ctx.config);
|
|
208
|
-
const
|
|
325
|
+
const approvedForAuth = await deps.loadApprovals(ctx, 'approved-approvals.json');
|
|
326
|
+
const authorization = await loadClassifierAuthorization({
|
|
327
|
+
repoRoot: ctx.repoRoot,
|
|
328
|
+
config: ctx.config,
|
|
329
|
+
approvedState: compactApprovals(approvedForAuth.state),
|
|
330
|
+
});
|
|
331
|
+
const egress = await egressStatus({ targetDir: ctx.repoRoot });
|
|
332
|
+
const egressProxyActive = isEgressProxyActive({
|
|
333
|
+
config: ctx.config,
|
|
334
|
+
running: egress.running,
|
|
335
|
+
foreignProxy: egress.foreignProxy,
|
|
336
|
+
repoRootMismatch: egress.repoRootMismatch,
|
|
337
|
+
});
|
|
338
|
+
const enrichedClassifierOptions = {
|
|
339
|
+
...classifierOptions,
|
|
340
|
+
...authorization,
|
|
341
|
+
egressProxyActive,
|
|
342
|
+
};
|
|
343
|
+
const predicted = await classifyGatedActionAsync(action, ctx.config, enrichedClassifierOptions);
|
|
209
344
|
let result = predicted;
|
|
210
345
|
let predictedAssessment;
|
|
211
346
|
let observedAssessment;
|
|
212
347
|
let transactionalLayer;
|
|
213
|
-
|
|
348
|
+
const recoveryCandidate = isTransactionalEligible(ctx.config, params.kind, predicted) &&
|
|
214
349
|
params.kind === 'shell' &&
|
|
215
|
-
params.command)
|
|
350
|
+
Boolean(params.command);
|
|
351
|
+
if (recoveryCandidate && params.command) {
|
|
216
352
|
const transactional = ctx.config.policy.transactional;
|
|
353
|
+
const boundaryDriverId = ctx.config.capability?.boundaryDriver ??
|
|
354
|
+
(ctx.config.sandbox.runtime === 'container' ? 'container' : 'host-integration');
|
|
355
|
+
const boundaryContext = await resolveBoundaryDriverContext({
|
|
356
|
+
repoRoot: ctx.repoRoot,
|
|
357
|
+
config: ctx.config,
|
|
358
|
+
driverId: boundaryDriverId,
|
|
359
|
+
egressProxyRunning: egressProxyActive,
|
|
360
|
+
});
|
|
217
361
|
const txResult = await runTransactionalExecution({
|
|
218
362
|
command: params.command,
|
|
219
363
|
cwd: params.cwd,
|
|
@@ -224,9 +368,11 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
224
368
|
diffContext: {
|
|
225
369
|
repoRoot: ctx.repoRoot,
|
|
226
370
|
sensitivePaths: ctx.config.classifier.sensitivePaths,
|
|
227
|
-
protectedRoots:
|
|
371
|
+
protectedRoots: enrichedClassifierOptions.protectedArtifactRoots ?? [],
|
|
228
372
|
maxDeletionCount: transactional.maxDeletionCount,
|
|
229
373
|
},
|
|
374
|
+
boundaryContext,
|
|
375
|
+
dirtyIgnoreRoots: protectedArtifactRoots(ctx.layout, ctx.repoRoot, ctx.config.controlPlane.enabled ? configuredControlPlaneDir(ctx.config) : null),
|
|
230
376
|
});
|
|
231
377
|
if (!txResult.skipped && txResult.observed) {
|
|
232
378
|
result = txResult.result;
|
|
@@ -240,13 +386,20 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
240
386
|
transactionalTimedOut: txResult.timedOut === true,
|
|
241
387
|
};
|
|
242
388
|
}
|
|
243
|
-
else
|
|
389
|
+
else {
|
|
390
|
+
const skipReason = txResult.skipReason ?? 'recovery_observation_failed';
|
|
391
|
+
result = recoveryFailClosedResult(predicted, recoveryFailReasonFromSkip(skipReason), [
|
|
392
|
+
skipReason,
|
|
393
|
+
]);
|
|
244
394
|
transactionalLayer = {
|
|
245
395
|
transactional: false,
|
|
246
|
-
transactionalSkipReason:
|
|
396
|
+
transactionalSkipReason: skipReason,
|
|
397
|
+
recoveryFailClosed: true,
|
|
247
398
|
};
|
|
248
399
|
}
|
|
249
400
|
}
|
|
401
|
+
const scrubOpts = scrubOptionsFromConfig(ctx.config);
|
|
402
|
+
const scrubbedPayload = fingerprintReplayPayload(params.kind, params.payload, scrubOpts);
|
|
250
403
|
return gateDecisionToVerdict(ctx, deps, params.kind, result, {
|
|
251
404
|
predictedAssessment,
|
|
252
405
|
observedAssessment,
|
|
@@ -255,24 +408,40 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
255
408
|
? {
|
|
256
409
|
input: params.command ?? result.normalizedCommand ?? result.summary ?? '',
|
|
257
410
|
inputKind: 'shell',
|
|
411
|
+
cwd: params.cwd,
|
|
258
412
|
}
|
|
259
413
|
: {
|
|
260
414
|
input: params.command ??
|
|
261
415
|
result.normalizedCommand ??
|
|
262
416
|
result.summary ??
|
|
263
|
-
canonicalStringify(
|
|
417
|
+
canonicalStringify(scrubbedPayload ?? {}),
|
|
264
418
|
inputKind: params.kind,
|
|
419
|
+
cwd: params.cwd,
|
|
420
|
+
toolName: params.toolName,
|
|
421
|
+
payload: scrubbedPayload,
|
|
265
422
|
},
|
|
423
|
+
replayAction: {
|
|
424
|
+
kind: params.kind,
|
|
425
|
+
cwd: params.cwd,
|
|
426
|
+
toolName: params.toolName,
|
|
427
|
+
command: params.command,
|
|
428
|
+
payload: scrubbedPayload,
|
|
429
|
+
fingerprint: result.fingerprint,
|
|
430
|
+
repoRoot: ctx.repoRoot,
|
|
431
|
+
},
|
|
432
|
+
classifierOptions: enrichedClassifierOptions,
|
|
433
|
+
scopeHintPayload: params.payload,
|
|
266
434
|
});
|
|
267
435
|
}
|
|
268
436
|
/** R39: unmapped Codex tools ask via pending approval — not hard deny without approval path. */
|
|
269
437
|
export async function gateUnmappedToolVerdict(ctx, deps, toolName, payload) {
|
|
270
|
-
const
|
|
438
|
+
const scrubOpts = scrubOptionsFromConfig(ctx.config);
|
|
439
|
+
const replayPayload = fingerprintReplayPayload('tool', payload, scrubOpts) ?? {};
|
|
271
440
|
const result = {
|
|
272
441
|
verdict: 'deny_pending_approval',
|
|
273
442
|
reason: 'unmapped_tool',
|
|
274
443
|
summary: toolName,
|
|
275
|
-
fingerprint: toolFingerprint(toolName,
|
|
444
|
+
fingerprint: toolFingerprint(toolName, replayPayload, ctx.repoRoot),
|
|
276
445
|
assessment: {
|
|
277
446
|
reversibility: 'irreversible',
|
|
278
447
|
external: false,
|
|
@@ -285,10 +454,61 @@ export async function gateUnmappedToolVerdict(ctx, deps, toolName, payload) {
|
|
|
285
454
|
approvalInput: {
|
|
286
455
|
input: toolName,
|
|
287
456
|
inputKind: 'tool',
|
|
457
|
+
cwd: ctx.repoRoot,
|
|
458
|
+
toolName,
|
|
459
|
+
payload: replayPayload,
|
|
460
|
+
},
|
|
461
|
+
replayAction: {
|
|
462
|
+
kind: 'tool',
|
|
463
|
+
cwd: ctx.repoRoot,
|
|
464
|
+
toolName,
|
|
465
|
+
payload: replayPayload,
|
|
466
|
+
fingerprint: result.fingerprint,
|
|
467
|
+
repoRoot: ctx.repoRoot,
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
async function consumeCapabilityGrantIfUsed(ctx, deps, result) {
|
|
472
|
+
if (result.reason !== 'capability_grant' &&
|
|
473
|
+
result.authorizationDecision?.matchedRule !== 'grant.exact') {
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
const request = result.capabilityRequests?.[0];
|
|
477
|
+
if (!request) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
const consumed = await mutateApprovalStateWithRetry({
|
|
481
|
+
load: () => deps.loadApprovals(ctx, 'approved-approvals.json'),
|
|
482
|
+
write: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
483
|
+
mutate: (state) => {
|
|
484
|
+
const compacted = compactApprovals(state);
|
|
485
|
+
const grant = findMatchingGrant(grantsFromApprovedState(compacted, ctx.repoRoot), request);
|
|
486
|
+
if (!grant) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
const lease = consumeGrantLease(compacted, grant.grantId);
|
|
490
|
+
if (!lease.consumed) {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
return { state: lease.state, result: true };
|
|
288
494
|
},
|
|
289
495
|
});
|
|
496
|
+
return consumed === true;
|
|
290
497
|
}
|
|
291
498
|
async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {}) {
|
|
499
|
+
const replayContext = buildAuditReplayContext(kind, result, auditExtras.replayAction);
|
|
500
|
+
const actionSnapshot = buildAuditActionSnapshot(kind, result, auditExtras.replayAction);
|
|
501
|
+
const providerId = String(ctx.config.judge?.providerId ?? 'cursor');
|
|
502
|
+
const stateDir = belayStateDir(ctx.config, ctx.layout.repoLocalStateDir(ctx.repoRoot));
|
|
503
|
+
const capabilityAudit = scheduleGateShadowAudit({
|
|
504
|
+
repoRoot: ctx.repoRoot,
|
|
505
|
+
config: ctx.config,
|
|
506
|
+
providerId,
|
|
507
|
+
result,
|
|
508
|
+
command: auditExtras.replayAction?.command ??
|
|
509
|
+
(kind === 'shell' ? auditExtras.approvalInput?.input : undefined),
|
|
510
|
+
stateDir,
|
|
511
|
+
});
|
|
292
512
|
const gateBase = {
|
|
293
513
|
event: gateAuditEventName(kind),
|
|
294
514
|
kind,
|
|
@@ -301,6 +521,9 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
301
521
|
schemaVersion: result.axes ? 2 : 1,
|
|
302
522
|
...(result.axes ?? {}),
|
|
303
523
|
...auditExtras.transactionalLayer,
|
|
524
|
+
...capabilityAudit,
|
|
525
|
+
...(replayContext ? { replayContext } : {}),
|
|
526
|
+
...(actionSnapshot ? { actionSnapshot } : {}),
|
|
304
527
|
};
|
|
305
528
|
if (result.reason === TRANSACTIONAL_ALREADY_APPLIED) {
|
|
306
529
|
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.';
|
|
@@ -321,6 +544,37 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
321
544
|
agent_message: agentMessage,
|
|
322
545
|
});
|
|
323
546
|
}
|
|
547
|
+
if (result.verdict === 'deny_pending_approval' &&
|
|
548
|
+
ctx.config.mode === 'enforce' &&
|
|
549
|
+
!TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason)) {
|
|
550
|
+
const standingAllowPath = standingAllowFile(ctx.config, ctx.layout.repoLocalStateDir(ctx.repoRoot));
|
|
551
|
+
const standingState = await loadStandingAllow(standingAllowPath);
|
|
552
|
+
const standingMatch = resolveStandingAllowMatch({
|
|
553
|
+
kind,
|
|
554
|
+
result,
|
|
555
|
+
repoRoot: ctx.repoRoot,
|
|
556
|
+
state: standingState,
|
|
557
|
+
});
|
|
558
|
+
if (standingMatch) {
|
|
559
|
+
await deps.appendAudit(ctx, {
|
|
560
|
+
...gateBase,
|
|
561
|
+
verdict: 'allow',
|
|
562
|
+
reason: 'standing_allow',
|
|
563
|
+
standingAllowSource: standingMatch.source,
|
|
564
|
+
...(standingMatch.catalogCommand
|
|
565
|
+
? { standingAllowCatalogCommand: standingMatch.catalogCommand }
|
|
566
|
+
: {}),
|
|
567
|
+
wouldBlock: false,
|
|
568
|
+
permission: 'allow',
|
|
569
|
+
});
|
|
570
|
+
return classifyResultToGateVerdict({
|
|
571
|
+
result: { ...result, verdict: 'allow', reason: 'standing_allow' },
|
|
572
|
+
mode: ctx.config.mode,
|
|
573
|
+
permission: 'allow',
|
|
574
|
+
wouldBlock: false,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}
|
|
324
578
|
const brokerActive = isCapabilityBrokerDemotionActive(ctx.config);
|
|
325
579
|
let approved = null;
|
|
326
580
|
if (!TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason) &&
|
|
@@ -331,15 +585,69 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
331
585
|
entry.fingerprint === result.fingerprint &&
|
|
332
586
|
entry.repoRoot === ctx.repoRoot);
|
|
333
587
|
if (!shouldSkipBrokerApprovedRecord(brokerActive, matchedApproval?.reason)) {
|
|
588
|
+
if (matchedApproval &&
|
|
589
|
+
matchedApproval.capabilityRequestHash &&
|
|
590
|
+
result.capabilityRequests?.length &&
|
|
591
|
+
matchedApproval.capabilityRequestHash !== hashCapabilityRequests(result.capabilityRequests)) {
|
|
592
|
+
await deps.appendAudit(ctx, {
|
|
593
|
+
...gateBase,
|
|
594
|
+
verdict: 'deny_pending_approval',
|
|
595
|
+
reason: 'approval_replay_mismatch',
|
|
596
|
+
approvalId: matchedApproval.approvalId,
|
|
597
|
+
wouldBlock: true,
|
|
598
|
+
permission: 'deny',
|
|
599
|
+
});
|
|
600
|
+
return classifyResultToGateVerdict({
|
|
601
|
+
result: {
|
|
602
|
+
...result,
|
|
603
|
+
verdict: 'deny_pending_approval',
|
|
604
|
+
reason: 'approval_replay_mismatch',
|
|
605
|
+
},
|
|
606
|
+
mode: ctx.config.mode,
|
|
607
|
+
permission: 'deny',
|
|
608
|
+
wouldBlock: true,
|
|
609
|
+
approvalId: matchedApproval.approvalId,
|
|
610
|
+
user_message: 'Belay denied this action because capability requests changed after approval. Re-approve the exact action or run belay explain.',
|
|
611
|
+
agent_message: 'Belay denied this action because capability requests changed after approval.',
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
if (matchedApproval &&
|
|
615
|
+
auditExtras.replayAction &&
|
|
616
|
+
!validateReplayEnvelope(matchedApproval, auditExtras.replayAction)) {
|
|
617
|
+
await deps.appendAudit(ctx, {
|
|
618
|
+
...gateBase,
|
|
619
|
+
verdict: 'deny_pending_approval',
|
|
620
|
+
reason: 'approval_replay_mismatch',
|
|
621
|
+
approvalId: matchedApproval.approvalId,
|
|
622
|
+
wouldBlock: true,
|
|
623
|
+
permission: 'deny',
|
|
624
|
+
});
|
|
625
|
+
return classifyResultToGateVerdict({
|
|
626
|
+
result: {
|
|
627
|
+
...result,
|
|
628
|
+
verdict: 'deny_pending_approval',
|
|
629
|
+
reason: 'approval_replay_mismatch',
|
|
630
|
+
},
|
|
631
|
+
mode: ctx.config.mode,
|
|
632
|
+
permission: 'deny',
|
|
633
|
+
wouldBlock: true,
|
|
634
|
+
approvalId: matchedApproval.approvalId,
|
|
635
|
+
user_message: 'Belay denied this action because it does not match the approved replay envelope. Re-approve the exact action or run belay explain.',
|
|
636
|
+
agent_message: 'Belay denied this action because cwd, tool, or payload changed after approval.',
|
|
637
|
+
});
|
|
638
|
+
}
|
|
334
639
|
approved = await consumeApprovedApproval(ctx, deps, kind, result.fingerprint);
|
|
335
640
|
}
|
|
336
641
|
}
|
|
337
642
|
if (approved) {
|
|
643
|
+
if (approved.firstExecution) {
|
|
644
|
+
await recordGateApprovalAsk(stateDir, result.reason, true);
|
|
645
|
+
}
|
|
338
646
|
await deps.appendAudit(ctx, {
|
|
339
647
|
...gateBase,
|
|
340
648
|
verdict: 'allow',
|
|
341
649
|
reason: 'approved_once',
|
|
342
|
-
approvalId: approved.approvalId,
|
|
650
|
+
approvalId: approved.approval.approvalId,
|
|
343
651
|
wouldBlock: false,
|
|
344
652
|
permission: 'allow',
|
|
345
653
|
});
|
|
@@ -348,10 +656,32 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
348
656
|
mode: ctx.config.mode,
|
|
349
657
|
permission: 'allow',
|
|
350
658
|
wouldBlock: false,
|
|
351
|
-
approvalId: approved.approvalId,
|
|
659
|
+
approvalId: approved.approval.approvalId,
|
|
352
660
|
});
|
|
353
661
|
}
|
|
354
662
|
if (result.verdict === 'allow' || result.verdict === 'allow_flagged') {
|
|
663
|
+
const grantConsumed = await consumeCapabilityGrantIfUsed(ctx, deps, result);
|
|
664
|
+
if (!grantConsumed) {
|
|
665
|
+
await deps.appendAudit(ctx, {
|
|
666
|
+
...gateBase,
|
|
667
|
+
verdict: 'deny_pending_approval',
|
|
668
|
+
reason: 'capability_grant_unavailable',
|
|
669
|
+
wouldBlock: true,
|
|
670
|
+
permission: 'deny',
|
|
671
|
+
});
|
|
672
|
+
return classifyResultToGateVerdict({
|
|
673
|
+
result: {
|
|
674
|
+
...result,
|
|
675
|
+
verdict: 'deny_pending_approval',
|
|
676
|
+
reason: 'capability_grant_unavailable',
|
|
677
|
+
},
|
|
678
|
+
mode: ctx.config.mode,
|
|
679
|
+
permission: 'deny',
|
|
680
|
+
wouldBlock: true,
|
|
681
|
+
user_message: 'Belay denied this action because the capability grant could not be consumed. Re-approve or run belay explain.',
|
|
682
|
+
agent_message: 'Belay denied this action because the capability grant was missing, expired, or already used.',
|
|
683
|
+
});
|
|
684
|
+
}
|
|
355
685
|
await deps.appendAudit(ctx, {
|
|
356
686
|
...gateBase,
|
|
357
687
|
verdict: result.verdict,
|
|
@@ -381,7 +711,44 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
381
711
|
wouldBlock: true,
|
|
382
712
|
});
|
|
383
713
|
}
|
|
384
|
-
|
|
714
|
+
if (isJudgeInfrastructureFailure(result)) {
|
|
715
|
+
const fallbackReason = extractJudgeFallbackReason(result);
|
|
716
|
+
const providerId = inferProviderIdFromFallbackReason(fallbackReason, String(ctx.config.judge.providerId ?? 'cursor'));
|
|
717
|
+
const denyMessages = formatJudgeInfrastructureDenyMessage({
|
|
718
|
+
providerId,
|
|
719
|
+
fallbackReason,
|
|
720
|
+
command: result.normalizedCommand ?? result.summary,
|
|
721
|
+
});
|
|
722
|
+
await deps.appendAudit(ctx, {
|
|
723
|
+
...gateBase,
|
|
724
|
+
verdict: result.verdict,
|
|
725
|
+
reason: 'judge_transport_unavailable',
|
|
726
|
+
judgeFallbackReason: fallbackReason,
|
|
727
|
+
wouldBlock: true,
|
|
728
|
+
permission: 'deny',
|
|
729
|
+
});
|
|
730
|
+
return classifyResultToGateVerdict({
|
|
731
|
+
result: {
|
|
732
|
+
...result,
|
|
733
|
+
verdict: 'deny_pending_approval',
|
|
734
|
+
reason: 'judge_transport_unavailable',
|
|
735
|
+
},
|
|
736
|
+
mode: ctx.config.mode,
|
|
737
|
+
permission: 'deny',
|
|
738
|
+
wouldBlock: true,
|
|
739
|
+
user_message: denyMessages.user_message,
|
|
740
|
+
agent_message: denyMessages.agent_message,
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
const { approval, created } = await ensurePendingApproval(ctx, deps, kind, result, auditExtras.approvalInput, deriveWorkspaceRootScopeHint({
|
|
744
|
+
result,
|
|
745
|
+
replayAction: auditExtras.replayAction,
|
|
746
|
+
payloadForScopeHint: auditExtras.scopeHintPayload,
|
|
747
|
+
options: auditExtras.classifierOptions ?? {},
|
|
748
|
+
}));
|
|
749
|
+
if (created) {
|
|
750
|
+
await recordGateApprovalAsk(stateDir, result.reason, false);
|
|
751
|
+
}
|
|
385
752
|
let approvalToken;
|
|
386
753
|
try {
|
|
387
754
|
approvalToken = await issueApprovalToken({
|
|
@@ -419,7 +786,7 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
419
786
|
permission: 'deny',
|
|
420
787
|
wouldBlock: true,
|
|
421
788
|
approvalId: approval.approvalId,
|
|
422
|
-
user_message: `Belay blocked this high-risk action. Approval ID: ${approval.approvalId}. ${
|
|
789
|
+
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.`,
|
|
423
790
|
agent_message: `Belay denied this action as ${result.reason}. Wait for approval, then retry the exact same action once.`,
|
|
424
791
|
});
|
|
425
792
|
}
|
|
@@ -428,6 +795,7 @@ export async function processApprovalPrompt(ctx, deps, prompt) {
|
|
|
428
795
|
if (!approvalId) {
|
|
429
796
|
return { continue: true };
|
|
430
797
|
}
|
|
798
|
+
const adapter = adapterIdFromContext(ctx);
|
|
431
799
|
if (ctx.config.approvalSigning.required) {
|
|
432
800
|
const message = `Signed approval token required for ${approvalId}. Editor prompt approval is disabled in this configuration. ` +
|
|
433
801
|
`Use belay approve --approval-id ${approvalId} --token <signed-token>.`;
|
|
@@ -448,12 +816,11 @@ export async function processApprovalPrompt(ctx, deps, prompt) {
|
|
|
448
816
|
approvalId,
|
|
449
817
|
config: ctx.config,
|
|
450
818
|
requireSignedToken: ctx.config.approvalSigning.required,
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
},
|
|
819
|
+
adapter,
|
|
820
|
+
store: gateApprovalStoreFromDeps({
|
|
821
|
+
loadApprovals: (fileName) => deps.loadApprovals(ctx, fileName),
|
|
822
|
+
writeApprovals: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
823
|
+
}),
|
|
457
824
|
});
|
|
458
825
|
await deps.appendAudit(ctx, {
|
|
459
826
|
event: 'approval',
|
|
@@ -469,9 +836,96 @@ export async function processApprovalPrompt(ctx, deps, prompt) {
|
|
|
469
836
|
user_message: recorded.message,
|
|
470
837
|
};
|
|
471
838
|
}
|
|
839
|
+
const replay = recorded.approval ? buildReplayHint(ctx.config, recorded.approval, adapter) : null;
|
|
840
|
+
const shouldFallbackReplay = adapter === 'cursor' &&
|
|
841
|
+
replay?.kind === 'shell' &&
|
|
842
|
+
replay.autoReplay === true &&
|
|
843
|
+
Boolean(recorded.approval?.input) &&
|
|
844
|
+
(!process.env.VITEST || process.env.BELAY_TEST_APPROVAL_REPLAY === '1');
|
|
845
|
+
if (shouldFallbackReplay && recorded.approval) {
|
|
846
|
+
const approvalStore = gateApprovalStoreFromDeps({
|
|
847
|
+
loadApprovals: (fileName) => deps.loadApprovals(ctx, fileName),
|
|
848
|
+
writeApprovals: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
849
|
+
});
|
|
850
|
+
let replayResult;
|
|
851
|
+
try {
|
|
852
|
+
replayResult = await replayShellCommand(recorded.approval.input ?? '', recorded.approval.cwd ?? ctx.repoRoot, getExecutionLeaseMs(ctx.config));
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
856
|
+
await deps.appendAudit(ctx, {
|
|
857
|
+
event: 'approval',
|
|
858
|
+
kind: 'approval',
|
|
859
|
+
verdict: 'allow',
|
|
860
|
+
approvalId,
|
|
861
|
+
reason: 'approval_replay_error',
|
|
862
|
+
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt,
|
|
863
|
+
});
|
|
864
|
+
return {
|
|
865
|
+
continue: false,
|
|
866
|
+
user_message: `Belay approval recorded for ${approvalId}, but one-step shell replay could not start (${detail}). ` +
|
|
867
|
+
'Approval remains active for one hook retry.',
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
const output = [replayResult.stdout, replayResult.stderr].filter(Boolean).join('\n').trim();
|
|
871
|
+
if (replayResult.exitCode === 0) {
|
|
872
|
+
try {
|
|
873
|
+
await consumeApprovedAfterCliReplay({
|
|
874
|
+
approvalId: approvalId,
|
|
875
|
+
store: approvalStore,
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
catch (error) {
|
|
879
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
880
|
+
await deps.appendAudit(ctx, {
|
|
881
|
+
event: 'approval',
|
|
882
|
+
kind: 'approval',
|
|
883
|
+
verdict: 'allow',
|
|
884
|
+
approvalId,
|
|
885
|
+
reason: 'approval_replay_consume_error',
|
|
886
|
+
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt,
|
|
887
|
+
});
|
|
888
|
+
return {
|
|
889
|
+
continue: false,
|
|
890
|
+
user_message: `Belay approval recorded for ${approvalId}, and one-step shell replay succeeded, but approval finalization failed (${detail}). ` +
|
|
891
|
+
'Approval remains active for one hook retry.' +
|
|
892
|
+
(output ? `\n${output}` : ''),
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
await deps.appendAudit(ctx, {
|
|
896
|
+
event: 'approval',
|
|
897
|
+
kind: 'approval',
|
|
898
|
+
verdict: 'allow',
|
|
899
|
+
approvalId,
|
|
900
|
+
reason: 'approval_replay_succeeded',
|
|
901
|
+
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt,
|
|
902
|
+
});
|
|
903
|
+
return {
|
|
904
|
+
continue: false,
|
|
905
|
+
user_message: `Belay approval recorded for ${approvalId}. One-step shell replay succeeded; no manual retry required.` +
|
|
906
|
+
(output ? `\n${output}` : ''),
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
await deps.appendAudit(ctx, {
|
|
910
|
+
event: 'approval',
|
|
911
|
+
kind: 'approval',
|
|
912
|
+
verdict: 'allow',
|
|
913
|
+
approvalId,
|
|
914
|
+
reason: 'approval_replay_failed',
|
|
915
|
+
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt,
|
|
916
|
+
});
|
|
917
|
+
const timeoutNote = replayResult.timedOut ? ' Replay timed out.' : '';
|
|
918
|
+
return {
|
|
919
|
+
continue: false,
|
|
920
|
+
user_message: `Belay approval recorded for ${approvalId}, but one-step shell replay failed (exit ${replayResult.exitCode}).${timeoutNote} ` +
|
|
921
|
+
`Approval remains active for one hook retry.` +
|
|
922
|
+
(output ? `\n${output}` : ''),
|
|
923
|
+
};
|
|
924
|
+
}
|
|
472
925
|
return {
|
|
473
926
|
continue: false,
|
|
474
927
|
user_message: recorded.message,
|
|
928
|
+
...(replay ? { replay } : {}),
|
|
475
929
|
};
|
|
476
930
|
}
|
|
477
931
|
export function gateVerdictToCursorResponse(verdict) {
|
|
@@ -504,6 +958,7 @@ export function gateVerdictToClaudeUserPromptResponse(verdict) {
|
|
|
504
958
|
hookEventName: 'UserPromptSubmit',
|
|
505
959
|
continue: false,
|
|
506
960
|
user_message: verdict.user_message,
|
|
961
|
+
...(verdict.replay ? { replay: verdict.replay } : {}),
|
|
507
962
|
},
|
|
508
963
|
};
|
|
509
964
|
}
|
|
@@ -520,6 +975,7 @@ export function gateVerdictToCodexUserPromptResponse(verdict) {
|
|
|
520
975
|
return {
|
|
521
976
|
decision: 'block',
|
|
522
977
|
reason: verdict.user_message,
|
|
978
|
+
...(verdict.replay ? { replay: verdict.replay } : {}),
|
|
523
979
|
};
|
|
524
980
|
}
|
|
525
981
|
export async function appendObservedAudit(ctx, deps, eventName, payload) {
|