@guilz-dev/belay 0.3.0 → 0.4.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/dist/adapters/cursor/runtime-entry.js +1 -0
- package/dist/adapters/shared/gate-runtime.d.ts +11 -14
- package/dist/adapters/shared/gate-runtime.js +277 -19
- package/dist/bundle/claude-runtime.mjs +3138 -699
- package/dist/bundle/codex-runtime.mjs +3162 -711
- package/dist/bundle/cursor-runtime.mjs +3140 -701
- package/dist/cli.js +137 -5
- package/dist/commands/approve.d.ts +1 -0
- package/dist/commands/approve.js +45 -26
- package/dist/commands/config.js +135 -52
- package/dist/commands/doctor.js +11 -1
- package/dist/commands/explain.js +6 -1
- 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 +21 -2
- 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/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 +10 -5
- 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 +47 -2
- package/dist/core/approval.d.ts +24 -3
- package/dist/core/approval.js +47 -4
- 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/index.d.ts +2 -1
- package/dist/core/capability/index.js +1 -0
- package/dist/core/capability/paths.d.ts +2 -2
- package/dist/core/capability/paths.js +9 -9
- 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.js +2 -20
- package/dist/core/classify-tool.js +32 -3
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +49 -0
- package/dist/core/gate-engine.js +6 -6
- 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 +54 -0
- package/dist/core/path-utils.d.ts +11 -0
- package/dist/core/path-utils.js +56 -6
- package/dist/core/reclassify.d.ts +3 -0
- package/dist/core/reclassify.js +44 -14
- 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 +175 -0
- package/dist/core/types.d.ts +15 -0
- package/dist/core/verdict/adapter.js +10 -1
- package/dist/core/verdict/containment.d.ts +4 -3
- package/dist/core/verdict/containment.js +17 -12
- package/dist/core/verdict/judge-audit.d.ts +1 -0
- package/dist/core/verdict/judge-audit.js +32 -1
- package/dist/core/verdict/judge-baseline.d.ts +20 -0
- package/dist/core/verdict/judge-baseline.js +48 -0
- package/dist/core/verdict/judge-broker-service.d.ts +42 -0
- package/dist/core/verdict/judge-broker-service.js +279 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +5 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +61 -0
- package/dist/core/verdict/judge-cli.d.ts +23 -0
- package/dist/core/verdict/judge-cli.js +137 -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 +42 -0
- package/dist/core/verdict/judge-runtime-config.js +92 -0
- package/dist/core/verdict/judge-session-broker.d.ts +48 -0
- package/dist/core/verdict/judge-session-broker.js +195 -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 +40 -0
- package/dist/core/verdict/judge-transport.js +258 -0
- package/dist/core/verdict/judge.d.ts +14 -1
- package/dist/core/verdict/judge.js +6 -5
- package/dist/core/verdict/parser.js +6 -1
- package/dist/core/verdict/types.d.ts +13 -0
- package/dist/core/verdict/verdict.js +7 -4
- 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 +62 -5
- 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 +151 -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 +45 -0
- package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
- package/dist/corpus/standing-allow-catalog.generated.js +79 -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 +123 -0
- package/dist/services/sandbox-service.d.ts +1 -0
- package/dist/services/sandbox-service.js +2 -0
- 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
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import { type ApprovalReplayHint } from '../../core/approval-replay.js';
|
|
1
2
|
import type { GatedActionKind } from '../../core/gate-contract.js';
|
|
2
3
|
import { type GatePermissionResponse, type GateVerdict } from '../../core/gate-contract.js';
|
|
3
4
|
import { GateNormalizationError } from '../../core/gate-engine.js';
|
|
4
|
-
import { type
|
|
5
|
-
import type { ClassifierOptions } from '../../core/types.js';
|
|
5
|
+
import { type BelayConfigV3 } from '../../core/index.js';
|
|
6
|
+
import type { ApprovalStateFile, ClassifierOptions } from '../../core/types.js';
|
|
6
7
|
import type { AdapterLayout } from '../layouts/types.js';
|
|
8
|
+
export interface ApprovalPromptResult {
|
|
9
|
+
continue: boolean;
|
|
10
|
+
user_message?: string;
|
|
11
|
+
replay?: ApprovalReplayHint;
|
|
12
|
+
}
|
|
7
13
|
export interface GateRuntimeContext {
|
|
8
14
|
layout: AdapterLayout;
|
|
9
15
|
repoRoot: string;
|
|
@@ -32,20 +38,11 @@ export declare function evaluateGatedAction(ctx: GateRuntimeContext, deps: GateR
|
|
|
32
38
|
}): Promise<GateVerdict>;
|
|
33
39
|
/** R39: unmapped Codex tools ask via pending approval — not hard deny without approval path. */
|
|
34
40
|
export declare function gateUnmappedToolVerdict(ctx: GateRuntimeContext, deps: GateRuntimeDeps, toolName: string, payload: Record<string, unknown>): Promise<GateVerdict>;
|
|
35
|
-
export declare function processApprovalPrompt(ctx: GateRuntimeContext, deps: GateRuntimeDeps, prompt: string): Promise<
|
|
36
|
-
continue: boolean;
|
|
37
|
-
user_message?: string;
|
|
38
|
-
}>;
|
|
41
|
+
export declare function processApprovalPrompt(ctx: GateRuntimeContext, deps: GateRuntimeDeps, prompt: string): Promise<ApprovalPromptResult>;
|
|
39
42
|
export declare function gateVerdictToCursorResponse(verdict: GateVerdict): GatePermissionResponse;
|
|
40
43
|
export declare function gateVerdictToClaudePreToolUseResponse(verdict: GateVerdict): Record<string, unknown>;
|
|
41
|
-
export declare function gateVerdictToClaudeUserPromptResponse(verdict:
|
|
42
|
-
continue: boolean;
|
|
43
|
-
user_message?: string;
|
|
44
|
-
}): Record<string, unknown>;
|
|
44
|
+
export declare function gateVerdictToClaudeUserPromptResponse(verdict: ApprovalPromptResult): Record<string, unknown>;
|
|
45
45
|
export declare function gateVerdictToCodexPreToolUseResponse(verdict: GateVerdict): Record<string, unknown>;
|
|
46
|
-
export declare function gateVerdictToCodexUserPromptResponse(verdict:
|
|
47
|
-
continue: boolean;
|
|
48
|
-
user_message?: string;
|
|
49
|
-
}): Record<string, unknown>;
|
|
46
|
+
export declare function gateVerdictToCodexUserPromptResponse(verdict: ApprovalPromptResult): Record<string, unknown>;
|
|
50
47
|
export declare function appendObservedAudit(ctx: GateRuntimeContext, deps: GateRuntimeDeps, eventName: string, payload: Record<string, unknown>): Promise<void>;
|
|
51
48
|
export { GateNormalizationError };
|
|
@@ -2,20 +2,37 @@ 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 { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload, fsScopeAllowlistPath, isCapabilityBrokerDemotionActive, loadFsScopeAllowlistSync, loadTrustedWorkspaceRootsSync, shouldSkipBrokerApprovedOnce, shouldSkipBrokerApprovedRecord, trustedWorkspaceRootsPath, validateTrustedWorkspaceRootCandidate, } from '../../core/capability/index.js';
|
|
8
11
|
import { resolveLayeredConfig, teamConfigPath } from '../../core/config-layers.js';
|
|
9
12
|
import { classifyResultToGateVerdict, unnormalizedGateVerdict, } from '../../core/gate-contract.js';
|
|
10
13
|
import { classifyGatedActionAsync, extractAgentAssessment, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from '../../core/gate-engine.js';
|
|
11
|
-
import {
|
|
14
|
+
import { approvalCommandMatch, approvedApprovalsFile, canonicalStringify, classifierOptionsFromConfig, configuredControlPlaneDir, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, scrubValue, toolFingerprint, } from '../../core/index.js';
|
|
12
15
|
import { notifyDeny } from '../../core/notify.js';
|
|
16
|
+
import { canonicalPath } from '../../core/path-utils.js';
|
|
17
|
+
import { fingerprintReplayPayload } from '../../core/replay-scrub.js';
|
|
18
|
+
import { loadStandingAllow, resolveStandingAllowMatch, standingAllowFile, } from '../../core/standing-allow.js';
|
|
13
19
|
import { isTransactionalEligible, runTransactionalExecution, TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPROVAL_BYPASS_REASONS, } from '../../core/transactional/index.js';
|
|
14
20
|
import { protectedArtifactRoots } from '../layouts/protected-paths.js';
|
|
15
21
|
const EMPTY_APPROVALS = {
|
|
16
22
|
version: 1,
|
|
17
23
|
approvals: [],
|
|
18
24
|
};
|
|
25
|
+
function adapterIdFromContext(ctx) {
|
|
26
|
+
if (ctx.config.adapter === 'cursor' ||
|
|
27
|
+
ctx.config.adapter === 'claude' ||
|
|
28
|
+
ctx.config.adapter === 'codex') {
|
|
29
|
+
return ctx.config.adapter;
|
|
30
|
+
}
|
|
31
|
+
if (ctx.layout.name === 'cursor' || ctx.layout.name === 'claude' || ctx.layout.name === 'codex') {
|
|
32
|
+
return ctx.layout.name;
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
19
36
|
async function loadJsonFile(filePath, fallback) {
|
|
20
37
|
try {
|
|
21
38
|
const raw = await readFile(filePath, 'utf8');
|
|
@@ -90,11 +107,13 @@ export function repoShellClassifierOptions(config, repoRoot, layout, extras = {}
|
|
|
90
107
|
export function runtimeClassifierOptions(ctx, config) {
|
|
91
108
|
const repoLocalStateDir = ctx.layout.repoLocalStateDir(ctx.repoRoot);
|
|
92
109
|
const brokerFsScope = isCapabilityBrokerDemotionActive(config);
|
|
110
|
+
const trustedRoots = loadTrustedWorkspaceRootsSync(trustedWorkspaceRootsPath(config, repoLocalStateDir));
|
|
93
111
|
return repoShellClassifierOptions(config, ctx.repoRoot, ctx.layout, {
|
|
94
112
|
brokerFsScope,
|
|
95
113
|
fsScopeAllowlist: brokerFsScope
|
|
96
114
|
? loadFsScopeAllowlistSync(fsScopeAllowlistPath(config, repoLocalStateDir))
|
|
97
115
|
: undefined,
|
|
116
|
+
trustedWorkspaceRoots: trustedRoots.roots.map((entry) => entry.path),
|
|
98
117
|
});
|
|
99
118
|
}
|
|
100
119
|
function gateAuditEventName(kind) {
|
|
@@ -106,7 +125,7 @@ function gateAuditEventName(kind) {
|
|
|
106
125
|
}
|
|
107
126
|
return 'subagentGate';
|
|
108
127
|
}
|
|
109
|
-
async function ensurePendingApproval(ctx, deps, kind, result, approvalInput) {
|
|
128
|
+
async function ensurePendingApproval(ctx, deps, kind, result, approvalInput, scopeHint) {
|
|
110
129
|
const pending = await deps.loadApprovals(ctx, 'pending-approvals.json');
|
|
111
130
|
pending.state = compactApprovals(pending.state);
|
|
112
131
|
const existing = pending.state.approvals.find((approval) => approval.kind === kind &&
|
|
@@ -116,7 +135,7 @@ async function ensurePendingApproval(ctx, deps, kind, result, approvalInput) {
|
|
|
116
135
|
await deps.writeApprovals(pending.filePath, pending.state);
|
|
117
136
|
return existing;
|
|
118
137
|
}
|
|
119
|
-
const approval =
|
|
138
|
+
const approval = createApprovalRecordWithEnvelope({
|
|
120
139
|
kind,
|
|
121
140
|
fingerprint: result.fingerprint,
|
|
122
141
|
repoRoot: ctx.repoRoot,
|
|
@@ -124,14 +143,70 @@ async function ensurePendingApproval(ctx, deps, kind, result, approvalInput) {
|
|
|
124
143
|
summary: result.normalizedCommand ?? result.summary ?? '',
|
|
125
144
|
approvalTtlMinutes: ctx.config.approvalTtlMinutes,
|
|
126
145
|
approvalId: `belay_${randomUUID().replaceAll('-', '').slice(0, 12)}`,
|
|
127
|
-
|
|
128
|
-
|
|
146
|
+
approvalInput,
|
|
147
|
+
scopeHint,
|
|
129
148
|
});
|
|
130
149
|
pending.state.version = 2;
|
|
131
150
|
pending.state.approvals.push(approval);
|
|
132
151
|
await deps.writeApprovals(pending.filePath, pending.state);
|
|
133
152
|
return approval;
|
|
134
153
|
}
|
|
154
|
+
function extractShellCommandFromPayload(payload) {
|
|
155
|
+
const toolInput = payload.tool_input;
|
|
156
|
+
if (!toolInput || typeof toolInput !== 'object') {
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
159
|
+
const input = toolInput;
|
|
160
|
+
return typeof input.command === 'string' ? input.command : '';
|
|
161
|
+
}
|
|
162
|
+
function deriveWorkspaceRootScopeHint(params) {
|
|
163
|
+
if (params.result.reason !== 'outside_repo_mutation' &&
|
|
164
|
+
params.result.reason !== 'outside_repo_redirect') {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
const action = params.replayAction;
|
|
168
|
+
if (!action?.cwd) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
const outsidePaths = new Set();
|
|
172
|
+
if (action.kind === 'shell' && action.command) {
|
|
173
|
+
for (const resolved of collectOutsideRepoPaths(action.command, action.cwd, action.repoRoot, params.options.trustedWorkspaceRoots)) {
|
|
174
|
+
outsidePaths.add(resolved);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else if (action.kind === 'tool') {
|
|
178
|
+
const toolPayload = params.payloadForScopeHint ?? action.payload ?? {};
|
|
179
|
+
for (const resolved of collectOutsideRepoPathsFromToolPayload(toolPayload, action.cwd, action.repoRoot, params.options.trustedWorkspaceRoots)) {
|
|
180
|
+
outsidePaths.add(resolved);
|
|
181
|
+
}
|
|
182
|
+
const command = extractShellCommandFromPayload(toolPayload);
|
|
183
|
+
if (command) {
|
|
184
|
+
for (const resolved of collectOutsideRepoPaths(command, action.cwd, action.repoRoot, params.options.trustedWorkspaceRoots)) {
|
|
185
|
+
outsidePaths.add(resolved);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (outsidePaths.size !== 1) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
const targetPath = [...outsidePaths][0];
|
|
193
|
+
if (!targetPath) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
const candidateRoot = canonicalPath(path.dirname(targetPath));
|
|
197
|
+
const validation = validateTrustedWorkspaceRootCandidate({
|
|
198
|
+
candidatePath: candidateRoot,
|
|
199
|
+
repoRoot: action.repoRoot,
|
|
200
|
+
controlPlaneDir: params.options.controlPlaneDir ?? undefined,
|
|
201
|
+
protectedRoots: params.options.protectedArtifactRoots ?? [],
|
|
202
|
+
requireExistingDirectory: true,
|
|
203
|
+
requireNonGit: true,
|
|
204
|
+
});
|
|
205
|
+
if (!validation.ok) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
return { scope: 'workspace-root', path: validation.normalizedPath };
|
|
209
|
+
}
|
|
135
210
|
async function consumeApprovedApproval(ctx, deps, kind, fingerprint) {
|
|
136
211
|
const approved = await deps.loadApprovals(ctx, 'approved-approvals.json');
|
|
137
212
|
approved.state = compactApprovals(approved.state);
|
|
@@ -149,7 +224,7 @@ async function consumeApprovedApproval(ctx, deps, kind, fingerprint) {
|
|
|
149
224
|
}
|
|
150
225
|
approved.state.approvals[index] = {
|
|
151
226
|
...approval,
|
|
152
|
-
executionLeaseExpiresAt: new Date(Date.now() +
|
|
227
|
+
executionLeaseExpiresAt: new Date(Date.now() + getExecutionLeaseMs(ctx.config)).toISOString(),
|
|
153
228
|
};
|
|
154
229
|
await deps.writeApprovals(approved.filePath, approved.state);
|
|
155
230
|
return approval;
|
|
@@ -247,6 +322,8 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
247
322
|
};
|
|
248
323
|
}
|
|
249
324
|
}
|
|
325
|
+
const scrubOpts = scrubOptionsFromConfig(ctx.config);
|
|
326
|
+
const scrubbedPayload = fingerprintReplayPayload(params.kind, params.payload, scrubOpts);
|
|
250
327
|
return gateDecisionToVerdict(ctx, deps, params.kind, result, {
|
|
251
328
|
predictedAssessment,
|
|
252
329
|
observedAssessment,
|
|
@@ -255,24 +332,40 @@ export async function evaluateGatedAction(ctx, deps, params) {
|
|
|
255
332
|
? {
|
|
256
333
|
input: params.command ?? result.normalizedCommand ?? result.summary ?? '',
|
|
257
334
|
inputKind: 'shell',
|
|
335
|
+
cwd: params.cwd,
|
|
258
336
|
}
|
|
259
337
|
: {
|
|
260
338
|
input: params.command ??
|
|
261
339
|
result.normalizedCommand ??
|
|
262
340
|
result.summary ??
|
|
263
|
-
canonicalStringify(
|
|
341
|
+
canonicalStringify(scrubbedPayload ?? {}),
|
|
264
342
|
inputKind: params.kind,
|
|
343
|
+
cwd: params.cwd,
|
|
344
|
+
toolName: params.toolName,
|
|
345
|
+
payload: scrubbedPayload,
|
|
265
346
|
},
|
|
347
|
+
replayAction: {
|
|
348
|
+
kind: params.kind,
|
|
349
|
+
cwd: params.cwd,
|
|
350
|
+
toolName: params.toolName,
|
|
351
|
+
command: params.command,
|
|
352
|
+
payload: scrubbedPayload,
|
|
353
|
+
fingerprint: result.fingerprint,
|
|
354
|
+
repoRoot: ctx.repoRoot,
|
|
355
|
+
},
|
|
356
|
+
classifierOptions,
|
|
357
|
+
scopeHintPayload: params.payload,
|
|
266
358
|
});
|
|
267
359
|
}
|
|
268
360
|
/** R39: unmapped Codex tools ask via pending approval — not hard deny without approval path. */
|
|
269
361
|
export async function gateUnmappedToolVerdict(ctx, deps, toolName, payload) {
|
|
270
|
-
const
|
|
362
|
+
const scrubOpts = scrubOptionsFromConfig(ctx.config);
|
|
363
|
+
const replayPayload = fingerprintReplayPayload('tool', payload, scrubOpts) ?? {};
|
|
271
364
|
const result = {
|
|
272
365
|
verdict: 'deny_pending_approval',
|
|
273
366
|
reason: 'unmapped_tool',
|
|
274
367
|
summary: toolName,
|
|
275
|
-
fingerprint: toolFingerprint(toolName,
|
|
368
|
+
fingerprint: toolFingerprint(toolName, replayPayload, ctx.repoRoot),
|
|
276
369
|
assessment: {
|
|
277
370
|
reversibility: 'irreversible',
|
|
278
371
|
external: false,
|
|
@@ -285,10 +378,23 @@ export async function gateUnmappedToolVerdict(ctx, deps, toolName, payload) {
|
|
|
285
378
|
approvalInput: {
|
|
286
379
|
input: toolName,
|
|
287
380
|
inputKind: 'tool',
|
|
381
|
+
cwd: ctx.repoRoot,
|
|
382
|
+
toolName,
|
|
383
|
+
payload: replayPayload,
|
|
384
|
+
},
|
|
385
|
+
replayAction: {
|
|
386
|
+
kind: 'tool',
|
|
387
|
+
cwd: ctx.repoRoot,
|
|
388
|
+
toolName,
|
|
389
|
+
payload: replayPayload,
|
|
390
|
+
fingerprint: result.fingerprint,
|
|
391
|
+
repoRoot: ctx.repoRoot,
|
|
288
392
|
},
|
|
289
393
|
});
|
|
290
394
|
}
|
|
291
395
|
async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {}) {
|
|
396
|
+
const replayContext = buildAuditReplayContext(kind, result, auditExtras.replayAction);
|
|
397
|
+
const actionSnapshot = buildAuditActionSnapshot(kind, result, auditExtras.replayAction);
|
|
292
398
|
const gateBase = {
|
|
293
399
|
event: gateAuditEventName(kind),
|
|
294
400
|
kind,
|
|
@@ -301,6 +407,8 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
301
407
|
schemaVersion: result.axes ? 2 : 1,
|
|
302
408
|
...(result.axes ?? {}),
|
|
303
409
|
...auditExtras.transactionalLayer,
|
|
410
|
+
...(replayContext ? { replayContext } : {}),
|
|
411
|
+
...(actionSnapshot ? { actionSnapshot } : {}),
|
|
304
412
|
};
|
|
305
413
|
if (result.reason === TRANSACTIONAL_ALREADY_APPLIED) {
|
|
306
414
|
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 +429,37 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
321
429
|
agent_message: agentMessage,
|
|
322
430
|
});
|
|
323
431
|
}
|
|
432
|
+
if (result.verdict === 'deny_pending_approval' &&
|
|
433
|
+
ctx.config.mode === 'enforce' &&
|
|
434
|
+
!TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason)) {
|
|
435
|
+
const standingAllowPath = standingAllowFile(ctx.config, ctx.layout.repoLocalStateDir(ctx.repoRoot));
|
|
436
|
+
const standingState = await loadStandingAllow(standingAllowPath);
|
|
437
|
+
const standingMatch = resolveStandingAllowMatch({
|
|
438
|
+
kind,
|
|
439
|
+
result,
|
|
440
|
+
repoRoot: ctx.repoRoot,
|
|
441
|
+
state: standingState,
|
|
442
|
+
});
|
|
443
|
+
if (standingMatch) {
|
|
444
|
+
await deps.appendAudit(ctx, {
|
|
445
|
+
...gateBase,
|
|
446
|
+
verdict: 'allow',
|
|
447
|
+
reason: 'standing_allow',
|
|
448
|
+
standingAllowSource: standingMatch.source,
|
|
449
|
+
...(standingMatch.catalogCommand
|
|
450
|
+
? { standingAllowCatalogCommand: standingMatch.catalogCommand }
|
|
451
|
+
: {}),
|
|
452
|
+
wouldBlock: false,
|
|
453
|
+
permission: 'allow',
|
|
454
|
+
});
|
|
455
|
+
return classifyResultToGateVerdict({
|
|
456
|
+
result: { ...result, verdict: 'allow', reason: 'standing_allow' },
|
|
457
|
+
mode: ctx.config.mode,
|
|
458
|
+
permission: 'allow',
|
|
459
|
+
wouldBlock: false,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
324
463
|
const brokerActive = isCapabilityBrokerDemotionActive(ctx.config);
|
|
325
464
|
let approved = null;
|
|
326
465
|
if (!TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason) &&
|
|
@@ -331,6 +470,31 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
331
470
|
entry.fingerprint === result.fingerprint &&
|
|
332
471
|
entry.repoRoot === ctx.repoRoot);
|
|
333
472
|
if (!shouldSkipBrokerApprovedRecord(brokerActive, matchedApproval?.reason)) {
|
|
473
|
+
if (matchedApproval &&
|
|
474
|
+
auditExtras.replayAction &&
|
|
475
|
+
!validateReplayEnvelope(matchedApproval, auditExtras.replayAction)) {
|
|
476
|
+
await deps.appendAudit(ctx, {
|
|
477
|
+
...gateBase,
|
|
478
|
+
verdict: 'deny_pending_approval',
|
|
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.',
|
|
496
|
+
});
|
|
497
|
+
}
|
|
334
498
|
approved = await consumeApprovedApproval(ctx, deps, kind, result.fingerprint);
|
|
335
499
|
}
|
|
336
500
|
}
|
|
@@ -381,7 +545,12 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
381
545
|
wouldBlock: true,
|
|
382
546
|
});
|
|
383
547
|
}
|
|
384
|
-
const approval = await ensurePendingApproval(ctx, deps, kind, result, auditExtras.approvalInput
|
|
548
|
+
const approval = await ensurePendingApproval(ctx, deps, kind, result, auditExtras.approvalInput, deriveWorkspaceRootScopeHint({
|
|
549
|
+
result,
|
|
550
|
+
replayAction: auditExtras.replayAction,
|
|
551
|
+
payloadForScopeHint: auditExtras.scopeHintPayload,
|
|
552
|
+
options: auditExtras.classifierOptions ?? {},
|
|
553
|
+
}));
|
|
385
554
|
let approvalToken;
|
|
386
555
|
try {
|
|
387
556
|
approvalToken = await issueApprovalToken({
|
|
@@ -419,7 +588,7 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
419
588
|
permission: 'deny',
|
|
420
589
|
wouldBlock: true,
|
|
421
590
|
approvalId: approval.approvalId,
|
|
422
|
-
user_message: `Belay blocked this high-risk action. Approval ID: ${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.`,
|
|
423
592
|
agent_message: `Belay denied this action as ${result.reason}. Wait for approval, then retry the exact same action once.`,
|
|
424
593
|
});
|
|
425
594
|
}
|
|
@@ -428,6 +597,7 @@ export async function processApprovalPrompt(ctx, deps, prompt) {
|
|
|
428
597
|
if (!approvalId) {
|
|
429
598
|
return { continue: true };
|
|
430
599
|
}
|
|
600
|
+
const adapter = adapterIdFromContext(ctx);
|
|
431
601
|
if (ctx.config.approvalSigning.required) {
|
|
432
602
|
const message = `Signed approval token required for ${approvalId}. Editor prompt approval is disabled in this configuration. ` +
|
|
433
603
|
`Use belay approve --approval-id ${approvalId} --token <signed-token>.`;
|
|
@@ -448,12 +618,11 @@ export async function processApprovalPrompt(ctx, deps, prompt) {
|
|
|
448
618
|
approvalId,
|
|
449
619
|
config: ctx.config,
|
|
450
620
|
requireSignedToken: ctx.config.approvalSigning.required,
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
},
|
|
621
|
+
adapter,
|
|
622
|
+
store: gateApprovalStoreFromDeps({
|
|
623
|
+
loadApprovals: (fileName) => deps.loadApprovals(ctx, fileName),
|
|
624
|
+
writeApprovals: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
625
|
+
}),
|
|
457
626
|
});
|
|
458
627
|
await deps.appendAudit(ctx, {
|
|
459
628
|
event: 'approval',
|
|
@@ -469,9 +638,96 @@ export async function processApprovalPrompt(ctx, deps, prompt) {
|
|
|
469
638
|
user_message: recorded.message,
|
|
470
639
|
};
|
|
471
640
|
}
|
|
641
|
+
const replay = recorded.approval ? buildReplayHint(ctx.config, recorded.approval, adapter) : null;
|
|
642
|
+
const shouldFallbackReplay = adapter === 'cursor' &&
|
|
643
|
+
replay?.kind === 'shell' &&
|
|
644
|
+
replay.autoReplay === true &&
|
|
645
|
+
Boolean(recorded.approval?.input) &&
|
|
646
|
+
(!process.env.VITEST || process.env.BELAY_TEST_APPROVAL_REPLAY === '1');
|
|
647
|
+
if (shouldFallbackReplay && recorded.approval) {
|
|
648
|
+
const approvalStore = gateApprovalStoreFromDeps({
|
|
649
|
+
loadApprovals: (fileName) => deps.loadApprovals(ctx, fileName),
|
|
650
|
+
writeApprovals: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
651
|
+
});
|
|
652
|
+
let replayResult;
|
|
653
|
+
try {
|
|
654
|
+
replayResult = await replayShellCommand(recorded.approval.input ?? '', recorded.approval.cwd ?? ctx.repoRoot, getExecutionLeaseMs(ctx.config));
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
658
|
+
await deps.appendAudit(ctx, {
|
|
659
|
+
event: 'approval',
|
|
660
|
+
kind: 'approval',
|
|
661
|
+
verdict: 'allow',
|
|
662
|
+
approvalId,
|
|
663
|
+
reason: 'approval_replay_error',
|
|
664
|
+
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt,
|
|
665
|
+
});
|
|
666
|
+
return {
|
|
667
|
+
continue: false,
|
|
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
|
+
});
|
|
690
|
+
return {
|
|
691
|
+
continue: false,
|
|
692
|
+
user_message: `Belay approval recorded for ${approvalId}, and one-step shell replay succeeded, but approval finalization failed (${detail}). ` +
|
|
693
|
+
'Approval remains active for one hook retry.' +
|
|
694
|
+
(output ? `\n${output}` : ''),
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
await deps.appendAudit(ctx, {
|
|
698
|
+
event: 'approval',
|
|
699
|
+
kind: 'approval',
|
|
700
|
+
verdict: 'allow',
|
|
701
|
+
approvalId,
|
|
702
|
+
reason: 'approval_replay_succeeded',
|
|
703
|
+
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt,
|
|
704
|
+
});
|
|
705
|
+
return {
|
|
706
|
+
continue: false,
|
|
707
|
+
user_message: `Belay approval recorded for ${approvalId}. One-step shell replay succeeded; no manual retry required.` +
|
|
708
|
+
(output ? `\n${output}` : ''),
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
await deps.appendAudit(ctx, {
|
|
712
|
+
event: 'approval',
|
|
713
|
+
kind: 'approval',
|
|
714
|
+
verdict: 'allow',
|
|
715
|
+
approvalId,
|
|
716
|
+
reason: 'approval_replay_failed',
|
|
717
|
+
summary: recorded.approval.input ?? recorded.approval.summary ?? prompt,
|
|
718
|
+
});
|
|
719
|
+
const timeoutNote = replayResult.timedOut ? ' Replay timed out.' : '';
|
|
720
|
+
return {
|
|
721
|
+
continue: false,
|
|
722
|
+
user_message: `Belay approval recorded for ${approvalId}, but one-step shell replay failed (exit ${replayResult.exitCode}).${timeoutNote} ` +
|
|
723
|
+
`Approval remains active for one hook retry.` +
|
|
724
|
+
(output ? `\n${output}` : ''),
|
|
725
|
+
};
|
|
726
|
+
}
|
|
472
727
|
return {
|
|
473
728
|
continue: false,
|
|
474
729
|
user_message: recorded.message,
|
|
730
|
+
...(replay ? { replay } : {}),
|
|
475
731
|
};
|
|
476
732
|
}
|
|
477
733
|
export function gateVerdictToCursorResponse(verdict) {
|
|
@@ -504,6 +760,7 @@ export function gateVerdictToClaudeUserPromptResponse(verdict) {
|
|
|
504
760
|
hookEventName: 'UserPromptSubmit',
|
|
505
761
|
continue: false,
|
|
506
762
|
user_message: verdict.user_message,
|
|
763
|
+
...(verdict.replay ? { replay: verdict.replay } : {}),
|
|
507
764
|
},
|
|
508
765
|
};
|
|
509
766
|
}
|
|
@@ -520,6 +777,7 @@ export function gateVerdictToCodexUserPromptResponse(verdict) {
|
|
|
520
777
|
return {
|
|
521
778
|
decision: 'block',
|
|
522
779
|
reason: verdict.user_message,
|
|
780
|
+
...(verdict.replay ? { replay: verdict.replay } : {}),
|
|
523
781
|
};
|
|
524
782
|
}
|
|
525
783
|
export async function appendObservedAudit(ctx, deps, eventName, payload) {
|