@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
package/dist/installer.js
CHANGED
|
@@ -11,6 +11,7 @@ import { appendCliAuditEvent } from './core/audit-io.js';
|
|
|
11
11
|
import { isFreshConfigInput, mergeConfig, normalizeConfig, } from './core/config.js';
|
|
12
12
|
import { runtimeIntegrityFiles, writeIntegrityManifest } from './core/integrity.js';
|
|
13
13
|
import { hasValidCloudConsent, isCloudJudgeConfig, migrateImplicitLocalJudgeIfNeeded, resolveInitJudgeConfig, } from './core/judge-config.js';
|
|
14
|
+
import { resolveJudgeTransport } from './core/judge-runtime-detection.js';
|
|
14
15
|
import { bootstrapStateFiles, writeSkillArtifacts } from './installer/bootstrap.js';
|
|
15
16
|
import { writeRuntimeArtifacts } from './installer/runtime-artifacts.js';
|
|
16
17
|
import { applyInstallScope, resolveOperationScope } from './installer/scope-config.js';
|
|
@@ -154,7 +155,9 @@ async function applyInitJudgeConfig(repoRoot, adapterName, options, isFreshBefor
|
|
|
154
155
|
};
|
|
155
156
|
}
|
|
156
157
|
const configWithJudge = normalizeConfig({ ...mergedConfig, version: 4, judge: finalJudge });
|
|
157
|
-
if (isCloudJudgeConfig(configWithJudge.judge) &&
|
|
158
|
+
if (isCloudJudgeConfig(configWithJudge.judge) &&
|
|
159
|
+
resolveJudgeTransport(configWithJudge.judge) === 'http' &&
|
|
160
|
+
!hasValidCloudConsent(configWithJudge.judge)) {
|
|
158
161
|
process.stderr.write('Warning: Cloud judge saved without recorded consent. Tier1 cloud judge will fail closed until consent is granted (belay judge consent + belay approve, or TTY --accept-cloud-judge).\n');
|
|
159
162
|
}
|
|
160
163
|
await writeConfigFile(repoRoot, configWithJudge, adapterName);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { createServer } from 'node:net';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { cleanupJudgeBrokerArtifacts, JUDGE_BROKER_SESSION, judgeBrokerPaths, } from './core/verdict/judge-broker-service.js';
|
|
5
|
+
import { runCliJsonWithTimeouts } from './core/verdict/judge-cli.js';
|
|
6
|
+
import { DEFAULT_JUDGE_SESSION_CONFIG, normalizeJudgeSessionConfig, resolveSessionEvalTimeoutMs, } from './core/verdict/judge-runtime-config.js';
|
|
7
|
+
import { JudgeSessionBroker, } from './core/verdict/judge-session-broker.js';
|
|
8
|
+
async function loadBrokerSessionConfig(stateDir) {
|
|
9
|
+
const sessionConfigPath = path.join(stateDir, JUDGE_BROKER_SESSION);
|
|
10
|
+
try {
|
|
11
|
+
const raw = JSON.parse(await readFile(sessionConfigPath, 'utf8'));
|
|
12
|
+
return normalizeJudgeSessionConfig({ ...raw, enabled: true });
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return normalizeJudgeSessionConfig({ ...DEFAULT_JUDGE_SESSION_CONFIG, enabled: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function main() {
|
|
19
|
+
const repoRoot = process.env.BELAY_JUDGE_BROKER_REPO_ROOT;
|
|
20
|
+
const stateDir = process.env.BELAY_JUDGE_BROKER_STATE_DIR;
|
|
21
|
+
if (!repoRoot || !stateDir) {
|
|
22
|
+
process.stderr.write('BELAY_JUDGE_BROKER_REPO_ROOT and BELAY_JUDGE_BROKER_STATE_DIR are required.\n');
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const paths = judgeBrokerPaths(stateDir);
|
|
27
|
+
await mkdir(stateDir, { recursive: true, mode: 0o700 });
|
|
28
|
+
const sessionConfig = await loadBrokerSessionConfig(stateDir);
|
|
29
|
+
const defaultEvalTimeoutMs = resolveSessionEvalTimeoutMs(sessionConfig, 25_000);
|
|
30
|
+
const broker = new JudgeSessionBroker({
|
|
31
|
+
config: sessionConfig,
|
|
32
|
+
runCommand: (invocation, timeoutMs) => runCliJsonWithTimeouts(invocation, { evalTimeoutMs: timeoutMs }),
|
|
33
|
+
});
|
|
34
|
+
let idleTimer = null;
|
|
35
|
+
let server;
|
|
36
|
+
const scheduleIdleShutdown = () => {
|
|
37
|
+
if (idleTimer) {
|
|
38
|
+
clearTimeout(idleTimer);
|
|
39
|
+
}
|
|
40
|
+
idleTimer = setTimeout(() => {
|
|
41
|
+
void shutdown('idle_timeout');
|
|
42
|
+
}, sessionConfig.maxIdleMs);
|
|
43
|
+
};
|
|
44
|
+
const shutdown = async (reason) => {
|
|
45
|
+
broker.stopAll('manual_stop');
|
|
46
|
+
server?.close();
|
|
47
|
+
await cleanupJudgeBrokerArtifacts(paths);
|
|
48
|
+
process.stderr.write(`judge broker stopped (${reason})\n`);
|
|
49
|
+
process.exit(0);
|
|
50
|
+
};
|
|
51
|
+
server = createServer((socket) => {
|
|
52
|
+
scheduleIdleShutdown();
|
|
53
|
+
let buffer = '';
|
|
54
|
+
socket.on('data', (chunk) => {
|
|
55
|
+
buffer += String(chunk);
|
|
56
|
+
const lines = buffer.split('\n');
|
|
57
|
+
buffer = lines.pop() ?? '';
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
if (!line.trim()) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
void handleLine(line, socket, broker, defaultEvalTimeoutMs).catch((error) => {
|
|
63
|
+
const id = safeMessageId(line);
|
|
64
|
+
socket.write(`${JSON.stringify({
|
|
65
|
+
type: 'error',
|
|
66
|
+
id,
|
|
67
|
+
error: error instanceof Error ? error.message : 'judge broker handler failed',
|
|
68
|
+
})}\n`);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
await unlink(paths.socketPath).catch(() => undefined);
|
|
74
|
+
await new Promise((resolve, reject) => {
|
|
75
|
+
server.once('error', reject);
|
|
76
|
+
server.listen(paths.socketPath, () => resolve());
|
|
77
|
+
});
|
|
78
|
+
const status = {
|
|
79
|
+
pid: process.pid,
|
|
80
|
+
repoRoot: path.resolve(repoRoot),
|
|
81
|
+
socketPath: paths.socketPath,
|
|
82
|
+
startedAt: new Date().toISOString(),
|
|
83
|
+
};
|
|
84
|
+
await writeFile(paths.statusPath, JSON.stringify(status), { encoding: 'utf8', mode: 0o600 });
|
|
85
|
+
await writeFile(paths.pidPath, String(process.pid), { encoding: 'utf8', mode: 0o600 });
|
|
86
|
+
process.on('SIGTERM', () => {
|
|
87
|
+
void shutdown('sigterm');
|
|
88
|
+
});
|
|
89
|
+
process.on('SIGINT', () => {
|
|
90
|
+
void shutdown('sigint');
|
|
91
|
+
});
|
|
92
|
+
scheduleIdleShutdown();
|
|
93
|
+
process.stdout.write(`judge broker listening on ${paths.socketPath} for ${repoRoot}\n`);
|
|
94
|
+
}
|
|
95
|
+
function safeMessageId(line) {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(line);
|
|
98
|
+
return parsed.id ?? 'unknown';
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return 'unknown';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function handleLine(line, socket, broker, defaultEvalTimeoutMs) {
|
|
105
|
+
const message = JSON.parse(line);
|
|
106
|
+
if (message.type === 'stop') {
|
|
107
|
+
socket.write(`${JSON.stringify({ type: 'stopped', id: message.id })}\n`);
|
|
108
|
+
broker.stopAll('manual_stop');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (message.type === 'invalidate' && typeof message.sessionKey === 'string') {
|
|
112
|
+
broker.invalidateSession(message.sessionKey, message.reason ?? 'parse_failure');
|
|
113
|
+
socket.write(`${JSON.stringify({ type: 'invalidated', id: message.id })}\n`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (message.type !== 'evaluate' || !message.request) {
|
|
117
|
+
socket.write(`${JSON.stringify({ type: 'error', id: message.id, error: 'unsupported message' })}\n`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const result = await broker.evaluate(message.request, message.timeoutMs ?? defaultEvalTimeoutMs);
|
|
121
|
+
socket.write(`${JSON.stringify({ type: 'result', id: message.id, result })}\n`);
|
|
122
|
+
}
|
|
123
|
+
await main();
|
|
@@ -24,6 +24,7 @@ export interface SandboxStatusReport {
|
|
|
24
24
|
export declare function sandboxStatus(options?: SandboxServiceOptions): Promise<SandboxStatusReport>;
|
|
25
25
|
export declare function createCapabilityApprovalStore(repoRoot: string, config: Awaited<ReturnType<typeof loadConfigFile>>): {
|
|
26
26
|
allowlistPath: string;
|
|
27
|
+
trustedRootsPath: string;
|
|
27
28
|
loadPending(): Promise<{
|
|
28
29
|
filePath: string;
|
|
29
30
|
state: import("../types.js").ApprovalStateFile;
|
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { loadConfigFile, repoLocalStateDirFor } from '../config-io.js';
|
|
3
3
|
import { fsScopeAllowlistPath, loadFsScopeAllowlist } from '../core/capability/allowlist.js';
|
|
4
4
|
import { evaluateL1FullStatus, isCapabilityBrokerDemotionActive, } from '../core/capability/broker.js';
|
|
5
|
+
import { trustedWorkspaceRootsPath } from '../core/capability/trusted-workspace-roots.js';
|
|
5
6
|
import { configuredControlPlaneDir } from '../core/config.js';
|
|
6
7
|
import { verifyControlPlaneIsolation } from '../core/control-plane-isolation.js';
|
|
7
8
|
import { egressStatus } from './egress-service.js';
|
|
@@ -48,6 +49,7 @@ export function createCapabilityApprovalStore(repoRoot, config) {
|
|
|
48
49
|
const repoLocalDir = repoLocalStateDirFor(repoRoot, config);
|
|
49
50
|
return {
|
|
50
51
|
allowlistPath: fsScopeAllowlistPath(config, repoLocalDir),
|
|
52
|
+
trustedRootsPath: trustedWorkspaceRootsPath(config, repoLocalDir),
|
|
51
53
|
async loadPending() {
|
|
52
54
|
const { loadApprovalState, pendingApprovalsPath } = await import('../config-io.js');
|
|
53
55
|
const filePath = pendingApprovalsPath(repoRoot, config);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PACKAGE_VERSION = "0.
|
|
1
|
+
export declare const PACKAGE_VERSION = "0.4.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/sync-version.mjs — do not edit.
|
|
2
|
-
export const PACKAGE_VERSION = '0.
|
|
2
|
+
export const PACKAGE_VERSION = '0.4.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guilz-dev/belay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Belay-style approval and audit gating for agent runtimes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,15 +48,19 @@
|
|
|
48
48
|
"agent-belay-logo.png"
|
|
49
49
|
],
|
|
50
50
|
"scripts": {
|
|
51
|
-
"build": "rm -rf dist && node scripts/sync-version.mjs && tsc -p tsconfig.build.json && node scripts/build-runtime.mjs",
|
|
51
|
+
"build": "rm -rf dist && node scripts/sync-version.mjs && tsc -p tsconfig.build.json && node scripts/generate-standing-allow-catalog.mjs && tsc -p tsconfig.build.json && node scripts/build-runtime.mjs",
|
|
52
52
|
"check:version": "node scripts/check-cli-version.mjs",
|
|
53
|
+
"dogfood": "./scripts/dev-dogfood.sh",
|
|
54
|
+
"dev-refresh": "./scripts/dev-refresh.sh",
|
|
53
55
|
"prepublishOnly": "pnpm build && node scripts/check-cli-version.mjs",
|
|
54
56
|
"lint": "biome check src package.json README.md CHANGELOG.md CONTRIBUTING.md SECURITY.md tsconfig.json tsconfig.build.json vitest.config.ts scripts docs",
|
|
55
57
|
"typecheck": "tsc --noEmit",
|
|
56
58
|
"test": "pnpm build && vitest run",
|
|
57
59
|
"test:structural": "pnpm build && vitest run src/__tests__/verdict/structural-suite.test.ts",
|
|
58
60
|
"test:stable": "pnpm build && vitest run && vitest run && vitest run",
|
|
59
|
-
"corpus": "pnpm build && node scripts/corpus.mjs"
|
|
61
|
+
"corpus": "pnpm build && node scripts/corpus.mjs",
|
|
62
|
+
"probe:adversarial": "pnpm build && node scripts/adversarial-probe.mjs",
|
|
63
|
+
"corpus:ratchet": "pnpm build && node scripts/corpus-ratchet.mjs"
|
|
60
64
|
},
|
|
61
65
|
"devDependencies": {
|
|
62
66
|
"@biomejs/biome": "2.4.15",
|
package/skills/belay/SKILL.md
CHANGED
|
@@ -38,7 +38,13 @@ setup (or `npx @guilz-dev/belay init` for non-interactive install). Run
|
|
|
38
38
|
|
|
39
39
|
1. Read the approval ID in the deny message.
|
|
40
40
|
2. Approve once with `/belay-approve <approval-id>` or `belay approve <approval-id>`.
|
|
41
|
-
3. Retry the original action unchanged.
|
|
41
|
+
3. Retry the original action unchanged (default `approval.flow` is `one_step` for shell — the
|
|
42
|
+
approval response includes a replay hint when auto-replay is enabled).
|
|
43
|
+
|
|
44
|
+
For shell-only CLI replay after approval: `belay approve <approval-id> --replay`.
|
|
45
|
+
|
|
46
|
+
To restore the legacy two-step UX (approve, then always retry manually), set
|
|
47
|
+
`approval.flow` to `two_step` in `belay.config.json`.
|
|
42
48
|
|
|
43
49
|
For why it was blocked, use `/belay why <command>` or `belay explain --command "<command>"`.
|
|
44
50
|
For the latest pending ask, use `/belay explain` or `belay explain`.
|
|
@@ -5,3 +5,20 @@ belay approve <approval-id>
|
|
|
5
5
|
```
|
|
6
6
|
|
|
7
7
|
Then retry the original action unchanged.
|
|
8
|
+
|
|
9
|
+
With default `approval.flow: one_step`, shell approvals return a replay hint from editor
|
|
10
|
+
hooks. Tool and subagent approvals still require a manual retry.
|
|
11
|
+
|
|
12
|
+
To run a denied shell command from the CLI after approval (explicit opt-in; do not also retry via hooks):
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
belay approve <approval-id> --replay
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Successful CLI replay consumes the one-shot grant. On failure, the approval stays active for one hook retry.
|
|
19
|
+
|
|
20
|
+
Restore legacy two-step UX in `belay.config.json`:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{ "approval": { "flow": "two_step" } }
|
|
24
|
+
```
|