@guilz-dev/belay 0.2.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/README.md +39 -12
- 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 +289 -23
- package/dist/bundle/claude-runtime.mjs +4435 -708
- package/dist/bundle/codex-runtime.mjs +4448 -710
- package/dist/bundle/cursor-runtime.mjs +4437 -710
- package/dist/cli.js +333 -11
- package/dist/commands/approve.d.ts +1 -0
- package/dist/commands/approve.js +56 -26
- package/dist/commands/config.d.ts +70 -0
- package/dist/commands/config.js +577 -0
- package/dist/commands/doctor.js +13 -2
- 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/health-snapshot.d.ts +6 -0
- package/dist/commands/health-snapshot.js +34 -0
- package/dist/commands/judge.d.ts +100 -0
- package/dist/commands/judge.js +304 -0
- 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/status.js +2 -2
- package/dist/commands/stdin-key.d.ts +1 -0
- package/dist/commands/stdin-key.js +8 -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/conformance/guarantee-table.js +12 -0
- package/dist/conformance/types.d.ts +2 -0
- 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-io.d.ts +2 -0
- package/dist/core/audit-io.js +14 -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 +4 -3
- package/dist/core/capability/index.js +3 -2
- package/dist/core/capability/paths.d.ts +2 -1
- package/dist/core/capability/paths.js +76 -8
- package/dist/core/capability/reasons.d.ts +3 -0
- package/dist/core/capability/reasons.js +8 -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.js +2 -20
- package/dist/core/classify-tool.js +129 -30
- package/dist/core/config-layers.js +2 -1
- package/dist/core/config.d.ts +40 -2
- package/dist/core/config.js +159 -11
- package/dist/core/credential-store.d.ts +11 -0
- package/dist/core/credential-store.js +60 -0
- package/dist/core/gate-engine.js +104 -13
- 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/integrity.d.ts +2 -0
- package/dist/core/integrity.js +13 -0
- package/dist/core/judge-api-key.d.ts +19 -0
- package/dist/core/judge-api-key.js +74 -0
- package/dist/core/judge-cloud-consent.d.ts +13 -0
- package/dist/core/judge-cloud-consent.js +38 -0
- package/dist/core/judge-config.d.ts +41 -4
- package/dist/core/judge-config.js +263 -57
- package/dist/core/judge-doctor.d.ts +9 -1
- package/dist/core/judge-doctor.js +199 -94
- package/dist/core/judge-model-discovery.d.ts +24 -0
- package/dist/core/judge-model-discovery.js +168 -0
- package/dist/core/judge-model-policy.d.ts +5 -0
- package/dist/core/judge-model-policy.js +21 -0
- package/dist/core/judge-runtime-detection.d.ts +9 -0
- package/dist/core/judge-runtime-detection.js +68 -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/transactional/diff-evaluator.js +1 -19
- package/dist/core/types.d.ts +17 -0
- package/dist/core/verdict/adapter.d.ts +1 -0
- package/dist/core/verdict/adapter.js +17 -2
- package/dist/core/verdict/containment.d.ts +8 -2
- package/dist/core/verdict/containment.js +45 -10
- 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-catalog.d.ts +40 -0
- package/dist/core/verdict/judge-catalog.js +148 -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 +46 -0
- package/dist/core/verdict/judge-cli.js +401 -0
- package/dist/core/verdict/judge-factory.d.ts +15 -4
- package/dist/core/verdict/judge-factory.js +127 -15
- 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 +33 -1
- package/dist/core/verdict/judge.js +84 -14
- package/dist/core/verdict/parser.js +6 -1
- package/dist/core/verdict/persistent-paths.d.ts +8 -0
- package/dist/core/verdict/persistent-paths.js +52 -0
- package/dist/core/verdict/types.d.ts +19 -2
- package/dist/core/verdict/verdict.js +166 -49
- 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 +69 -15
- 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/types.d.ts +7 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -3
- package/skills/belay/SKILL.md +15 -8
- package/skills/belay/belay-approve.md +17 -0
- package/dist/commands/init-wizard.d.ts +0 -21
- package/dist/commands/init-wizard.js +0 -63
package/README.md
CHANGED
|
@@ -34,8 +34,8 @@ policy to maintain.
|
|
|
34
34
|
| **Claude Code** | Supported | `.claude/settings.json` | `.claude/belay.config.json` |
|
|
35
35
|
| **Codex** | Experimental | `.codex/config.toml` | `.codex/belay.config.json` |
|
|
36
36
|
|
|
37
|
-
Pick the adapter at install time with `--adapter cursor|claude|codex` (or
|
|
38
|
-
`
|
|
37
|
+
Pick the adapter at install time with `--adapter cursor|claude|codex` (or use
|
|
38
|
+
`belay config` interactively). Hosts use different hook event names, but Belay registers
|
|
39
39
|
the same runners (`belay-tool-gate`, `belay-before-submit`, `belay-audit`) at
|
|
40
40
|
equivalent lifecycle points:
|
|
41
41
|
|
|
@@ -68,8 +68,8 @@ audit instead of guessing.
|
|
|
68
68
|
## Quick start
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
|
-
# Interactive setup (
|
|
72
|
-
|
|
71
|
+
# Interactive setup (adapter, scope, skill, judge provider, credentials)
|
|
72
|
+
belay config
|
|
73
73
|
|
|
74
74
|
# Or non-interactive
|
|
75
75
|
npx @guilz-dev/belay init --adapter claude # Claude Code
|
|
@@ -131,6 +131,9 @@ Belay is a layered hook gate, not a static denylist. Higher layers are opt-in.
|
|
|
131
131
|
- L3 command lists are **not security boundaries** by themselves — see
|
|
132
132
|
[docs/ops/semver-policy.md](./docs/ops/semver-policy.md) and
|
|
133
133
|
[docs/guarantee-table.md](./docs/guarantee-table.md).
|
|
134
|
+
- At default L3, **local-recoverable mutations outside the repo** pass after Tier1
|
|
135
|
+
(e.g. IDE plan files). To deny repo-external writes at the OS boundary, enable L1-full
|
|
136
|
+
(`sandbox.runtime` ≠ `none`). Tier1 requires a working judge (local Ollama by default).
|
|
134
137
|
- Adversarial resistance requires the full L1 stack:
|
|
135
138
|
`belay init --preset l1-full-recommended`, verified with `belay sandbox status`.
|
|
136
139
|
|
|
@@ -236,20 +239,37 @@ Notable settings:
|
|
|
236
239
|
current OS user. `upgrade` migrates repo-local approvals in; disabling merges
|
|
237
240
|
them back. File-mutation tools and shell redirects cannot write control-plane
|
|
238
241
|
paths while it is enabled.
|
|
239
|
-
- **Cloud judge** —
|
|
240
|
-
`
|
|
241
|
-
|
|
242
|
-
`
|
|
243
|
-
|
|
242
|
+
- **Cloud judge** — configure with `belay config` (interactive) or `belay config set judge.providerId <id>`.
|
|
243
|
+
Providers: `ollama`, `codex`, `claude`, `cursor`. **Provider** is the vendor/service
|
|
244
|
+
(`judge.providerId`); **driver** is the API compatibility layer (`judge.provider`:
|
|
245
|
+
`ollama`, `openai-compatible`, or `anthropic`); **host** is where hooks install
|
|
246
|
+
(`config.adapter`: `cursor`, `claude`, `codex`). Set `judge.endpoint` when needed;
|
|
247
|
+
credentials via `belay config credential mode project|apiKey` or env vars. Record egress
|
|
248
|
+
consent during `belay config` or via `belay judge consent` → `belay approve` →
|
|
249
|
+
`belay judge use … --cloud-consent-approval-id`. Cloud providers can use native CLI
|
|
250
|
+
transport without `judge.endpoint` when the host CLI is available (`codex-cli`,
|
|
251
|
+
`cursor-cli`, `claude-cli`); HTTP transport still requires endpoint and recorded consent.
|
|
252
|
+
Fresh installs default to the
|
|
253
|
+
host-matched provider (`cursor` → `cursor`, etc.). `belay judge use` remains available
|
|
254
|
+
as a secondary path.
|
|
255
|
+
|
|
256
|
+
Legacy `judge.model: auto` in config files is normalized to the provider catalog default on load
|
|
257
|
+
(with a warning); new `auto` values are rejected on CLI, `belay config set`, and `belay judge use`.
|
|
258
|
+
On an installed repo, interactive `belay config` defaults to judge-only setup without re-running
|
|
259
|
+
`init`. Model discovery is covered by unit tests with mocks; set `BELAY_LIVE_CLI_DISCOVERY=1` locally
|
|
260
|
+
for optional live CLI probes. Cloud egress consent is enforced for HTTP transport; native CLI
|
|
261
|
+
transport uses the host session and does not require `judge.endpoint`. `BELAY_JUDGE_MODEL_RESOLVED`
|
|
262
|
+
applies only under Vitest (test overrides).
|
|
244
263
|
|
|
245
264
|
## Command reference
|
|
246
265
|
|
|
247
266
|
```bash
|
|
248
267
|
belay init [--adapter cursor|claude|codex] [--scope project|global]
|
|
249
268
|
[--preset strict|standard|audit-first|l1-full-recommended]
|
|
250
|
-
[--with-skill] [--dogfood]
|
|
251
|
-
belay
|
|
252
|
-
belay
|
|
269
|
+
[--migrate-judge-default] [--with-skill] [--dogfood]
|
|
270
|
+
belay config # interactive setup (primary)
|
|
271
|
+
belay config list|get|set|unset|judge|credential …
|
|
272
|
+
belay upgrade [--migrate-judge-default] # refresh hooks + runtime, migrate config
|
|
253
273
|
belay dogfood [--enforce] # toggle audit / enforce mode
|
|
254
274
|
belay doctor [--fix] # check (and repair) floor health
|
|
255
275
|
belay status # show install scope / skill-only state
|
|
@@ -263,6 +283,13 @@ belay egress <start|stop|status|env>
|
|
|
263
283
|
belay sandbox status
|
|
264
284
|
belay approve <approval-id> [--scope once|domain|path]
|
|
265
285
|
belay revoke <approval-id>
|
|
286
|
+
belay judge status
|
|
287
|
+
belay judge list
|
|
288
|
+
belay judge use <ollama|codex|claude|cursor> [--model <id>] [--endpoint <url>]
|
|
289
|
+
[--accept-cloud] [--cloud-consent-approval-id <id>]
|
|
290
|
+
[--credential project|apiKey] [--key-stdin] [--key-env <NAME>]
|
|
291
|
+
belay judge test
|
|
292
|
+
belay judge consent <provider-id> [--endpoint <url>]
|
|
266
293
|
```
|
|
267
294
|
|
|
268
295
|
## Coexisting with existing hooks
|
|
@@ -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,11 +429,75 @@ 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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
464
|
+
let approved = null;
|
|
465
|
+
if (!TRANSACTIONAL_APPROVAL_BYPASS_REASONS.has(result.reason) &&
|
|
466
|
+
!shouldSkipBrokerApprovedOnce(brokerActive, result.reason)) {
|
|
467
|
+
const approvedState = await deps.loadApprovals(ctx, 'approved-approvals.json');
|
|
468
|
+
approvedState.state = compactApprovals(approvedState.state);
|
|
469
|
+
const matchedApproval = approvedState.state.approvals.find((entry) => entry.kind === kind &&
|
|
470
|
+
entry.fingerprint === result.fingerprint &&
|
|
471
|
+
entry.repoRoot === ctx.repoRoot);
|
|
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
|
+
}
|
|
498
|
+
approved = await consumeApprovedApproval(ctx, deps, kind, result.fingerprint);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
329
501
|
if (approved) {
|
|
330
502
|
await deps.appendAudit(ctx, {
|
|
331
503
|
...gateBase,
|
|
@@ -373,7 +545,12 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
373
545
|
wouldBlock: true,
|
|
374
546
|
});
|
|
375
547
|
}
|
|
376
|
-
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
|
+
}));
|
|
377
554
|
let approvalToken;
|
|
378
555
|
try {
|
|
379
556
|
approvalToken = await issueApprovalToken({
|
|
@@ -411,7 +588,7 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
|
|
|
411
588
|
permission: 'deny',
|
|
412
589
|
wouldBlock: true,
|
|
413
590
|
approvalId: approval.approvalId,
|
|
414
|
-
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.`,
|
|
415
592
|
agent_message: `Belay denied this action as ${result.reason}. Wait for approval, then retry the exact same action once.`,
|
|
416
593
|
});
|
|
417
594
|
}
|
|
@@ -420,6 +597,7 @@ export async function processApprovalPrompt(ctx, deps, prompt) {
|
|
|
420
597
|
if (!approvalId) {
|
|
421
598
|
return { continue: true };
|
|
422
599
|
}
|
|
600
|
+
const adapter = adapterIdFromContext(ctx);
|
|
423
601
|
if (ctx.config.approvalSigning.required) {
|
|
424
602
|
const message = `Signed approval token required for ${approvalId}. Editor prompt approval is disabled in this configuration. ` +
|
|
425
603
|
`Use belay approve --approval-id ${approvalId} --token <signed-token>.`;
|
|
@@ -440,12 +618,11 @@ export async function processApprovalPrompt(ctx, deps, prompt) {
|
|
|
440
618
|
approvalId,
|
|
441
619
|
config: ctx.config,
|
|
442
620
|
requireSignedToken: ctx.config.approvalSigning.required,
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
},
|
|
621
|
+
adapter,
|
|
622
|
+
store: gateApprovalStoreFromDeps({
|
|
623
|
+
loadApprovals: (fileName) => deps.loadApprovals(ctx, fileName),
|
|
624
|
+
writeApprovals: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
625
|
+
}),
|
|
449
626
|
});
|
|
450
627
|
await deps.appendAudit(ctx, {
|
|
451
628
|
event: 'approval',
|
|
@@ -461,9 +638,96 @@ export async function processApprovalPrompt(ctx, deps, prompt) {
|
|
|
461
638
|
user_message: recorded.message,
|
|
462
639
|
};
|
|
463
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
|
+
}
|
|
464
727
|
return {
|
|
465
728
|
continue: false,
|
|
466
729
|
user_message: recorded.message,
|
|
730
|
+
...(replay ? { replay } : {}),
|
|
467
731
|
};
|
|
468
732
|
}
|
|
469
733
|
export function gateVerdictToCursorResponse(verdict) {
|
|
@@ -496,6 +760,7 @@ export function gateVerdictToClaudeUserPromptResponse(verdict) {
|
|
|
496
760
|
hookEventName: 'UserPromptSubmit',
|
|
497
761
|
continue: false,
|
|
498
762
|
user_message: verdict.user_message,
|
|
763
|
+
...(verdict.replay ? { replay: verdict.replay } : {}),
|
|
499
764
|
},
|
|
500
765
|
};
|
|
501
766
|
}
|
|
@@ -512,6 +777,7 @@ export function gateVerdictToCodexUserPromptResponse(verdict) {
|
|
|
512
777
|
return {
|
|
513
778
|
decision: 'block',
|
|
514
779
|
reason: verdict.user_message,
|
|
780
|
+
...(verdict.replay ? { replay: verdict.replay } : {}),
|
|
515
781
|
};
|
|
516
782
|
}
|
|
517
783
|
export async function appendObservedAudit(ctx, deps, eventName, payload) {
|