@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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency terminal UI helpers for the interactive `belay config` wizard.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is built on Node's built-in `readline`/`tty` only — no runtime
|
|
5
|
+
* dependencies — so it stays aligned with belay's zero-dependency posture.
|
|
6
|
+
*
|
|
7
|
+
* This module is intentionally TTY-only. Callers are responsible for non-TTY
|
|
8
|
+
* fallback behavior.
|
|
9
|
+
*/
|
|
10
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
11
|
+
import readline from 'node:readline';
|
|
12
|
+
const isColorEnabled = () => isInteractiveTTY() && !process.env.NO_COLOR && process.env.TERM !== 'dumb';
|
|
13
|
+
const symbol = (pretty, plain) => (isColorEnabled() ? pretty : plain);
|
|
14
|
+
const wrap = (open, close) => (s) => isColorEnabled() ? `\x1b[${open}m${s}\x1b[${close}m` : s;
|
|
15
|
+
export const c = {
|
|
16
|
+
bold: wrap(1, 22),
|
|
17
|
+
dim: wrap(2, 22),
|
|
18
|
+
italic: wrap(3, 23),
|
|
19
|
+
underline: wrap(4, 24),
|
|
20
|
+
inverse: wrap(7, 27),
|
|
21
|
+
cyan: wrap(36, 39),
|
|
22
|
+
green: wrap(32, 39),
|
|
23
|
+
yellow: wrap(33, 39),
|
|
24
|
+
red: wrap(31, 39),
|
|
25
|
+
gray: wrap(90, 39),
|
|
26
|
+
};
|
|
27
|
+
/** A select wizard only makes sense when both stdin and stdout are real TTYs. */
|
|
28
|
+
export function isInteractiveTTY() {
|
|
29
|
+
return Boolean(input.isTTY && output.isTTY);
|
|
30
|
+
}
|
|
31
|
+
const symPointer = () => symbol('❯', '>');
|
|
32
|
+
const symCheck = () => symbol('✓', 'v');
|
|
33
|
+
const symBar = () => symbol('│', '|');
|
|
34
|
+
const symQuestion = () => '?';
|
|
35
|
+
/** Top banner shown once at the start of the wizard. */
|
|
36
|
+
export function intro(title, subtitle) {
|
|
37
|
+
output.write('\n');
|
|
38
|
+
output.write(`${c.green(symBar())} ${c.bold(title)}\n`);
|
|
39
|
+
if (subtitle) {
|
|
40
|
+
output.write(`${c.green(symBar())} ${c.dim(subtitle)}\n`);
|
|
41
|
+
}
|
|
42
|
+
output.write(`${c.green(symBar())}\n`);
|
|
43
|
+
}
|
|
44
|
+
/** Closing line shown once at the end of the wizard. */
|
|
45
|
+
export function outro(message) {
|
|
46
|
+
output.write(`${c.green(symBar())}\n`);
|
|
47
|
+
output.write(`${c.green(symCheck())} ${message}\n\n`);
|
|
48
|
+
}
|
|
49
|
+
/** Collapsed one-line summary of an answered question. */
|
|
50
|
+
export function summary(label, value) {
|
|
51
|
+
output.write(`${c.green(symCheck())} ${c.dim(label)} ${c.cyan(value)}\n`);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Interactive single-choice prompt driven by arrow keys (or j/k). Resolves with
|
|
55
|
+
* the selected choice value. Must only be called in an interactive TTY.
|
|
56
|
+
*/
|
|
57
|
+
export function selectPrompt(opts) {
|
|
58
|
+
if (!isInteractiveTTY()) {
|
|
59
|
+
throw new Error('selectPrompt requires an interactive TTY.');
|
|
60
|
+
}
|
|
61
|
+
const { message, choices } = opts;
|
|
62
|
+
if (choices.length === 0) {
|
|
63
|
+
throw new Error('selectPrompt requires at least one choice.');
|
|
64
|
+
}
|
|
65
|
+
const startIndex = Math.max(0, choices.findIndex((ch) => ch.value === opts.defaultValue));
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
let index = startIndex;
|
|
68
|
+
let rendered = 0;
|
|
69
|
+
const lineFor = (ch, active) => {
|
|
70
|
+
const label = ch.label ?? ch.value;
|
|
71
|
+
if (active) {
|
|
72
|
+
const hint = ch.hint ? ` ${c.dim(ch.hint)}` : '';
|
|
73
|
+
return `${c.cyan(symPointer())} ${c.cyan(c.underline(label))}${hint}`;
|
|
74
|
+
}
|
|
75
|
+
return ` ${c.dim(label)}`;
|
|
76
|
+
};
|
|
77
|
+
const clearRendered = () => {
|
|
78
|
+
if (rendered > 0) {
|
|
79
|
+
output.write(`\x1b[${rendered}A`);
|
|
80
|
+
}
|
|
81
|
+
output.write('\x1b[J');
|
|
82
|
+
};
|
|
83
|
+
const render = () => {
|
|
84
|
+
clearRendered();
|
|
85
|
+
const lines = [
|
|
86
|
+
`${c.green(symQuestion())} ${c.bold(message)}`,
|
|
87
|
+
...choices.map((ch, i) => lineFor(ch, i === index)),
|
|
88
|
+
];
|
|
89
|
+
output.write(`${lines.join('\n')}\n`);
|
|
90
|
+
rendered = lines.length;
|
|
91
|
+
};
|
|
92
|
+
const cleanup = () => {
|
|
93
|
+
input.off('keypress', onKey);
|
|
94
|
+
if (input.isTTY && typeof input.setRawMode === 'function') {
|
|
95
|
+
input.setRawMode(false);
|
|
96
|
+
}
|
|
97
|
+
input.pause();
|
|
98
|
+
output.write('\x1b[?25h'); // show cursor
|
|
99
|
+
};
|
|
100
|
+
const finish = () => {
|
|
101
|
+
clearRendered();
|
|
102
|
+
cleanup();
|
|
103
|
+
const chosen = choices[index];
|
|
104
|
+
summary(message, chosen.label ?? chosen.value);
|
|
105
|
+
resolve(chosen.value);
|
|
106
|
+
};
|
|
107
|
+
const onKey = (_str, key) => {
|
|
108
|
+
if (!key)
|
|
109
|
+
return;
|
|
110
|
+
if (key.ctrl && key.name === 'c') {
|
|
111
|
+
clearRendered();
|
|
112
|
+
cleanup();
|
|
113
|
+
output.write('\n');
|
|
114
|
+
reject(new Error('Cancelled.'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
118
|
+
index = (index - 1 + choices.length) % choices.length;
|
|
119
|
+
render();
|
|
120
|
+
}
|
|
121
|
+
else if (key.name === 'down' || key.name === 'j') {
|
|
122
|
+
index = (index + 1) % choices.length;
|
|
123
|
+
render();
|
|
124
|
+
}
|
|
125
|
+
else if (key.name === 'return' || key.name === 'enter') {
|
|
126
|
+
finish();
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
readline.emitKeypressEvents(input);
|
|
130
|
+
if (input.isTTY && typeof input.setRawMode === 'function') {
|
|
131
|
+
input.setRawMode(true);
|
|
132
|
+
}
|
|
133
|
+
input.resume();
|
|
134
|
+
output.write('\x1b[?25l'); // hide cursor
|
|
135
|
+
input.on('keypress', onKey);
|
|
136
|
+
render();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/** Interactive yes/no prompt rendered as a two-row arrow-key selection. */
|
|
140
|
+
export async function confirmPrompt(message, defaultValue) {
|
|
141
|
+
const value = await selectPrompt({
|
|
142
|
+
message,
|
|
143
|
+
defaultValue: defaultValue ? 'yes' : 'no',
|
|
144
|
+
choices: [
|
|
145
|
+
{ value: 'yes', label: 'Yes' },
|
|
146
|
+
{ value: 'no', label: 'No' },
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
return value === 'yes';
|
|
150
|
+
}
|
package/dist/config-io.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { chmod, copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { getAdapterLayout } from './adapters/layouts/index.js';
|
|
5
5
|
import { compactApprovals, isExpired, mergeApprovalStates } from './core/approval.js';
|
|
@@ -42,7 +42,7 @@ export function approvedApprovalsPath(repoRoot, config) {
|
|
|
42
42
|
export { belayStateDir };
|
|
43
43
|
export async function ensureBelayStateDir(config, repoRoot) {
|
|
44
44
|
const stateDir = belayStateDir(config, repoLocalStateDirFor(repoRoot, config));
|
|
45
|
-
await mkdir(stateDir, { recursive: true });
|
|
45
|
+
await mkdir(stateDir, { recursive: true, mode: 0o700 });
|
|
46
46
|
return stateDir;
|
|
47
47
|
}
|
|
48
48
|
const APPROVAL_STATE_FILES = ['pending-approvals.json', 'approved-approvals.json'];
|
|
@@ -75,11 +75,15 @@ async function readApprovalStateFile(filePath) {
|
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
async function writeApprovalStateFile(filePath, state) {
|
|
78
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
79
|
-
await writeFile(filePath, `${JSON.stringify(compactApprovals(state), null, 2)}\n`,
|
|
78
|
+
await mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
79
|
+
await writeFile(filePath, `${JSON.stringify(compactApprovals(state), null, 2)}\n`, {
|
|
80
|
+
encoding: 'utf8',
|
|
81
|
+
mode: 0o600,
|
|
82
|
+
});
|
|
83
|
+
await chmod(filePath, 0o600);
|
|
80
84
|
}
|
|
81
85
|
async function migrateApprovalFilesBetween(sourceDir, targetDir) {
|
|
82
|
-
await mkdir(targetDir, { recursive: true });
|
|
86
|
+
await mkdir(targetDir, { recursive: true, mode: 0o700 });
|
|
83
87
|
for (const fileName of APPROVAL_STATE_FILES) {
|
|
84
88
|
const from = path.join(sourceDir, fileName);
|
|
85
89
|
const to = path.join(targetDir, fileName);
|
|
@@ -88,6 +92,7 @@ async function migrateApprovalFilesBetween(sourceDir, targetDir) {
|
|
|
88
92
|
}
|
|
89
93
|
if (!existsSync(to)) {
|
|
90
94
|
await copyFile(from, to);
|
|
95
|
+
await chmod(to, 0o600);
|
|
91
96
|
continue;
|
|
92
97
|
}
|
|
93
98
|
const targetState = await readApprovalStateFile(to);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export const DEFAULT_CLI_REPLAY_TIMEOUT_MS = 300_000;
|
|
4
|
+
export async function replayShellCommand(command, cwd, timeoutMs = DEFAULT_CLI_REPLAY_TIMEOUT_MS) {
|
|
5
|
+
const resolvedCwd = path.resolve(cwd);
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const child = spawn(command, {
|
|
8
|
+
cwd: resolvedCwd,
|
|
9
|
+
shell: true,
|
|
10
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
11
|
+
env: { ...process.env },
|
|
12
|
+
});
|
|
13
|
+
const stdout = [];
|
|
14
|
+
const stderr = [];
|
|
15
|
+
let timedOut = false;
|
|
16
|
+
const timer = setTimeout(() => {
|
|
17
|
+
timedOut = true;
|
|
18
|
+
child.kill('SIGTERM');
|
|
19
|
+
}, timeoutMs);
|
|
20
|
+
child.stdout?.on('data', (chunk) => stdout.push(Buffer.from(chunk)));
|
|
21
|
+
child.stderr?.on('data', (chunk) => stderr.push(Buffer.from(chunk)));
|
|
22
|
+
child.on('error', (error) => {
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
reject(error);
|
|
25
|
+
});
|
|
26
|
+
child.on('close', (code) => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
resolve({
|
|
29
|
+
exitCode: timedOut ? 124 : (code ?? 1),
|
|
30
|
+
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
31
|
+
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
32
|
+
...(timedOut ? { timedOut: true } : {}),
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ApprovalFlow, BelayConfigV4 } from './config.js';
|
|
2
|
+
import type { ApprovalRecord } from './types.js';
|
|
3
|
+
export type { ApprovalFlow } from './config.js';
|
|
4
|
+
export type ReplayAdapterId = 'cursor' | 'claude' | 'codex';
|
|
5
|
+
export { DEFAULT_CLI_REPLAY_TIMEOUT_MS, replayShellCommand } from './approval-replay-cli.js';
|
|
6
|
+
export interface ApprovalReplayHint {
|
|
7
|
+
kind: 'shell' | 'tool' | 'subagent';
|
|
8
|
+
input: string;
|
|
9
|
+
cwd?: string;
|
|
10
|
+
toolName?: string;
|
|
11
|
+
fingerprint: string;
|
|
12
|
+
approvalId: string;
|
|
13
|
+
autoReplay: boolean;
|
|
14
|
+
fallbackToTwoStep: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface ReplayEnvelopeInput {
|
|
17
|
+
kind: ApprovalRecord['kind'];
|
|
18
|
+
cwd?: string;
|
|
19
|
+
toolName?: string;
|
|
20
|
+
command?: string;
|
|
21
|
+
input?: string;
|
|
22
|
+
inputKind?: 'shell' | 'tool' | 'subagent';
|
|
23
|
+
payload?: Record<string, unknown>;
|
|
24
|
+
fingerprint: string;
|
|
25
|
+
repoRoot: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ReplayActionContext {
|
|
28
|
+
kind: ApprovalRecord['kind'];
|
|
29
|
+
cwd?: string;
|
|
30
|
+
toolName?: string;
|
|
31
|
+
command?: string;
|
|
32
|
+
payload?: Record<string, unknown>;
|
|
33
|
+
fingerprint: string;
|
|
34
|
+
repoRoot: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function replayPayloadHash(kind: ApprovalRecord['kind'], payload: Record<string, unknown> | undefined, repoRoot: string): string | undefined;
|
|
37
|
+
export declare function getExecutionLeaseMs(config: BelayConfigV4): number;
|
|
38
|
+
export declare function approvalFlow(config: BelayConfigV4): ApprovalFlow;
|
|
39
|
+
export declare function buildReplayEnvelopeFields(input: ReplayEnvelopeInput): Partial<ApprovalRecord>;
|
|
40
|
+
export declare function hasReplayEnvelope(approval: ApprovalRecord): boolean;
|
|
41
|
+
export declare function validateReplayEnvelope(approval: ApprovalRecord, action: ReplayActionContext): boolean;
|
|
42
|
+
export declare function canAutoReplay(config: BelayConfigV4, kind: ApprovalRecord['kind'], adapter?: ReplayAdapterId): boolean;
|
|
43
|
+
export declare function buildRetryInstructionForConfig(config: BelayConfigV4, tokenPrefix: string, approvalId: string): string;
|
|
44
|
+
export declare function buildApprovalRecordedMessage(config: BelayConfigV4, approval: ApprovalRecord, adapter?: ReplayAdapterId): string;
|
|
45
|
+
export declare function buildReplayHint(config: BelayConfigV4, approval: ApprovalRecord, adapter?: ReplayAdapterId): ApprovalReplayHint | null;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { canonicalStringify, hashValue } from './fingerprint.js';
|
|
3
|
+
export { DEFAULT_CLI_REPLAY_TIMEOUT_MS, replayShellCommand } from './approval-replay-cli.js';
|
|
4
|
+
const MAX_PAYLOAD_JSON_BYTES = 16_384;
|
|
5
|
+
export function replayPayloadHash(kind, payload, repoRoot) {
|
|
6
|
+
if (!payload || Object.keys(payload).length === 0) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
const json = canonicalStringify(payload);
|
|
10
|
+
return hashValue(`payload:${kind}:${json}:${repoRoot}`);
|
|
11
|
+
}
|
|
12
|
+
const DEFAULT_REPLAY_CAPABILITIES = {
|
|
13
|
+
shell: true,
|
|
14
|
+
tool: false,
|
|
15
|
+
subagent: false,
|
|
16
|
+
};
|
|
17
|
+
const ADAPTER_REPLAY_CAPABLE = {
|
|
18
|
+
cursor: { ...DEFAULT_REPLAY_CAPABILITIES },
|
|
19
|
+
claude: { ...DEFAULT_REPLAY_CAPABILITIES },
|
|
20
|
+
codex: { ...DEFAULT_REPLAY_CAPABILITIES },
|
|
21
|
+
};
|
|
22
|
+
export function getExecutionLeaseMs(config) {
|
|
23
|
+
const ms = config.approval?.executionLeaseMs;
|
|
24
|
+
return typeof ms === 'number' && ms > 0 ? ms : 60_000;
|
|
25
|
+
}
|
|
26
|
+
export function approvalFlow(config) {
|
|
27
|
+
return config.approval?.flow === 'two_step' ? 'two_step' : 'one_step';
|
|
28
|
+
}
|
|
29
|
+
export function buildReplayEnvelopeFields(input) {
|
|
30
|
+
const fields = {};
|
|
31
|
+
const cmd = input.command ?? input.input ?? '';
|
|
32
|
+
if (cmd) {
|
|
33
|
+
fields.input = cmd;
|
|
34
|
+
fields.inputKind = input.inputKind ?? input.kind;
|
|
35
|
+
}
|
|
36
|
+
if (input.cwd) {
|
|
37
|
+
fields.cwd = path.resolve(input.cwd);
|
|
38
|
+
}
|
|
39
|
+
if (input.toolName) {
|
|
40
|
+
fields.toolName = input.toolName;
|
|
41
|
+
}
|
|
42
|
+
if (input.payload && Object.keys(input.payload).length > 0) {
|
|
43
|
+
const json = canonicalStringify(input.payload);
|
|
44
|
+
if (Buffer.byteLength(json, 'utf8') <= MAX_PAYLOAD_JSON_BYTES) {
|
|
45
|
+
fields.payloadJson = json;
|
|
46
|
+
}
|
|
47
|
+
fields.payloadHash = replayPayloadHash(input.kind, input.payload, input.repoRoot);
|
|
48
|
+
}
|
|
49
|
+
return fields;
|
|
50
|
+
}
|
|
51
|
+
export function hasReplayEnvelope(approval) {
|
|
52
|
+
return Boolean(approval.cwd || approval.toolName || approval.payloadHash);
|
|
53
|
+
}
|
|
54
|
+
export function validateReplayEnvelope(approval, action) {
|
|
55
|
+
if (!hasReplayEnvelope(approval)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (approval.kind !== action.kind) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (approval.fingerprint !== action.fingerprint) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (approval.repoRoot !== action.repoRoot) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (approval.cwd && path.resolve(approval.cwd) !== path.resolve(action.cwd ?? '')) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (approval.toolName && approval.toolName !== action.toolName) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (approval.payloadHash) {
|
|
74
|
+
const hash = replayPayloadHash(action.kind, action.payload, action.repoRoot);
|
|
75
|
+
if (!hash || approval.payloadHash !== hash) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
export function canAutoReplay(config, kind, adapter) {
|
|
82
|
+
if (config.approvalSigning?.required) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (approvalFlow(config) !== 'one_step') {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (kind !== 'shell' && kind !== 'tool' && kind !== 'subagent') {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const scopes = config.approval?.autoReplayScopes;
|
|
92
|
+
if (kind === 'shell' && scopes?.shell === false) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (kind === 'tool' && scopes?.tool !== true) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (kind === 'subagent' && scopes?.subagent !== true) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
if (adapter) {
|
|
102
|
+
return ADAPTER_REPLAY_CAPABLE[adapter][kind] === true;
|
|
103
|
+
}
|
|
104
|
+
return kind === 'shell' && scopes?.shell !== false;
|
|
105
|
+
}
|
|
106
|
+
export function buildRetryInstructionForConfig(config, tokenPrefix, approvalId) {
|
|
107
|
+
if (approvalFlow(config) === 'one_step') {
|
|
108
|
+
return `To allow this action once, send ${tokenPrefix} ${approvalId}. After approval, retry the same action unchanged.`;
|
|
109
|
+
}
|
|
110
|
+
return `To allow the next matching action once, send ${tokenPrefix} ${approvalId} and then retry the original action unchanged.`;
|
|
111
|
+
}
|
|
112
|
+
export function buildApprovalRecordedMessage(config, approval, adapter) {
|
|
113
|
+
if (!canAutoReplay(config, approval.kind, adapter)) {
|
|
114
|
+
return `Belay approval recorded for ${approval.approvalId}. Retry the original action once before it expires.`;
|
|
115
|
+
}
|
|
116
|
+
if (approval.kind === 'shell' && approval.input) {
|
|
117
|
+
return (`Belay approval recorded for ${approval.approvalId}. ` +
|
|
118
|
+
`Retry this shell command unchanged: ${approval.input}`);
|
|
119
|
+
}
|
|
120
|
+
return `Belay approval recorded for ${approval.approvalId}. Retry the original action once before it expires.`;
|
|
121
|
+
}
|
|
122
|
+
export function buildReplayHint(config, approval, adapter) {
|
|
123
|
+
if (approvalFlow(config) !== 'one_step') {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const input = approval.input ?? approval.summary;
|
|
127
|
+
if (!input) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const autoReplay = canAutoReplay(config, approval.kind, adapter);
|
|
131
|
+
return {
|
|
132
|
+
kind: approval.kind,
|
|
133
|
+
input,
|
|
134
|
+
cwd: approval.cwd,
|
|
135
|
+
toolName: approval.toolName,
|
|
136
|
+
fingerprint: approval.fingerprint,
|
|
137
|
+
approvalId: approval.approvalId,
|
|
138
|
+
autoReplay,
|
|
139
|
+
fallbackToTwoStep: !autoReplay,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReplayAdapterId } from './approval-replay.js';
|
|
1
2
|
import type { BelayConfigV3 } from './config.js';
|
|
2
3
|
import type { ApprovalStateFile } from './types.js';
|
|
3
4
|
export interface ApprovalStore {
|
|
@@ -19,8 +20,22 @@ export declare function recordApproval(params: {
|
|
|
19
20
|
token?: string;
|
|
20
21
|
/** When true, require a signed token (out-of-band CLI path). Editor prompts skip this. */
|
|
21
22
|
requireSignedToken?: boolean;
|
|
23
|
+
adapter?: ReplayAdapterId;
|
|
22
24
|
}): Promise<{
|
|
23
25
|
ok: boolean;
|
|
24
26
|
message: string;
|
|
25
27
|
approval?: ApprovalStateFile['approvals'][number];
|
|
26
28
|
}>;
|
|
29
|
+
/** CLI `--replay` already executed the shell command; drop the one-shot grant. */
|
|
30
|
+
export declare function consumeApprovedAfterCliReplay(params: {
|
|
31
|
+
approvalId: string;
|
|
32
|
+
store: ApprovalStore;
|
|
33
|
+
}): Promise<void>;
|
|
34
|
+
export declare function createGateApprovalStore(repoRoot: string, config: BelayConfigV3): ApprovalStore;
|
|
35
|
+
export declare function gateApprovalStoreFromDeps(deps: {
|
|
36
|
+
loadApprovals: (fileName: 'pending-approvals.json' | 'approved-approvals.json') => Promise<{
|
|
37
|
+
filePath: string;
|
|
38
|
+
state: ApprovalStateFile;
|
|
39
|
+
}>;
|
|
40
|
+
writeApprovals: (filePath: string, state: ApprovalStateFile) => Promise<void>;
|
|
41
|
+
}): ApprovalStore;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { approvedApprovalsPath, loadApprovalState, pendingApprovalsPath, saveApprovalState, } from '../config-io.js';
|
|
1
2
|
import { compactApprovals } from './approval.js';
|
|
3
|
+
import { buildApprovalRecordedMessage } from './approval-replay.js';
|
|
2
4
|
import { verifyApprovalToken } from './approval-token.js';
|
|
3
5
|
import { configuredControlPlaneDir } from './config.js';
|
|
4
6
|
export async function recordApproval(params) {
|
|
5
|
-
const { approvalId, config, store, token, requireSignedToken = false } = params;
|
|
7
|
+
const { approvalId, config, store, token, requireSignedToken = false, adapter } = params;
|
|
6
8
|
const pending = await store.loadPending();
|
|
7
9
|
pending.state = compactApprovals(pending.state);
|
|
8
10
|
const index = pending.state.approvals.findIndex((approval) => approval.approvalId === approvalId);
|
|
@@ -35,7 +37,50 @@ export async function recordApproval(params) {
|
|
|
35
37
|
await store.writeApproved(approved.filePath, approved.state);
|
|
36
38
|
return {
|
|
37
39
|
ok: true,
|
|
38
|
-
message:
|
|
40
|
+
message: buildApprovalRecordedMessage(config, approval, adapter),
|
|
39
41
|
approval,
|
|
40
42
|
};
|
|
41
43
|
}
|
|
44
|
+
/** CLI `--replay` already executed the shell command; drop the one-shot grant. */
|
|
45
|
+
export async function consumeApprovedAfterCliReplay(params) {
|
|
46
|
+
const approved = await params.store.loadApproved();
|
|
47
|
+
approved.state = compactApprovals(approved.state);
|
|
48
|
+
const remaining = approved.state.approvals.filter((approval) => approval.approvalId !== params.approvalId);
|
|
49
|
+
if (remaining.length === approved.state.approvals.length) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
approved.state.approvals = remaining;
|
|
53
|
+
await params.store.writeApproved(approved.filePath, approved.state);
|
|
54
|
+
}
|
|
55
|
+
export function createGateApprovalStore(repoRoot, config) {
|
|
56
|
+
return {
|
|
57
|
+
async loadPending() {
|
|
58
|
+
const filePath = pendingApprovalsPath(repoRoot, config);
|
|
59
|
+
return {
|
|
60
|
+
filePath,
|
|
61
|
+
state: await loadApprovalState(repoRoot, 'pending-approvals.json', config),
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
async loadApproved() {
|
|
65
|
+
const filePath = approvedApprovalsPath(repoRoot, config);
|
|
66
|
+
return {
|
|
67
|
+
filePath,
|
|
68
|
+
state: await loadApprovalState(repoRoot, 'approved-approvals.json', config),
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
async writePending(_filePath, state) {
|
|
72
|
+
await saveApprovalState(repoRoot, 'pending-approvals.json', state, config);
|
|
73
|
+
},
|
|
74
|
+
async writeApproved(_filePath, state) {
|
|
75
|
+
await saveApprovalState(repoRoot, 'approved-approvals.json', state, config);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function gateApprovalStoreFromDeps(deps) {
|
|
80
|
+
return {
|
|
81
|
+
loadPending: () => deps.loadApprovals('pending-approvals.json'),
|
|
82
|
+
loadApproved: () => deps.loadApprovals('approved-approvals.json'),
|
|
83
|
+
writePending: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
84
|
+
writeApproved: (filePath, state) => deps.writeApprovals(filePath, state),
|
|
85
|
+
};
|
|
86
|
+
}
|
package/dist/core/approval.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ApprovalRecord, ApprovalStateFile } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
export declare const APPROVAL_EXECUTION_LEASE_MS
|
|
2
|
+
/** @deprecated Use `DEFAULT_APPROVAL_CONFIG.executionLeaseMs` */
|
|
3
|
+
export declare const APPROVAL_EXECUTION_LEASE_MS: number;
|
|
4
4
|
export declare function nowIso(): string;
|
|
5
5
|
export declare function isExpired(approval: ApprovalRecord): boolean;
|
|
6
6
|
export declare function isExecutionLeaseExpired(approval: ApprovalRecord): boolean;
|
|
@@ -8,7 +8,23 @@ export declare function compactApprovals(state: ApprovalStateFile): ApprovalStat
|
|
|
8
8
|
export declare function mergeApprovalStates(target: ApprovalStateFile, source: ApprovalStateFile): ApprovalStateFile;
|
|
9
9
|
export declare function escapeRegex(value: string): string;
|
|
10
10
|
export declare function approvalCommandMatch(prompt: string, tokenPrefix: string): string | null;
|
|
11
|
-
export declare function
|
|
11
|
+
export declare function createApprovalRecordWithEnvelope(params: {
|
|
12
|
+
kind: ApprovalRecord['kind'];
|
|
13
|
+
fingerprint: string;
|
|
14
|
+
repoRoot: string;
|
|
15
|
+
reason: string;
|
|
16
|
+
summary: string;
|
|
17
|
+
approvalTtlMinutes: number;
|
|
18
|
+
approvalId: string;
|
|
19
|
+
approvalInput?: {
|
|
20
|
+
input: string;
|
|
21
|
+
inputKind: 'shell' | 'tool' | 'subagent';
|
|
22
|
+
cwd?: string;
|
|
23
|
+
toolName?: string;
|
|
24
|
+
payload?: Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
scopeHint?: ApprovalRecord['scopeHint'];
|
|
27
|
+
}): ApprovalRecord;
|
|
12
28
|
export declare function createApprovalRecord(params: {
|
|
13
29
|
kind: ApprovalRecord['kind'];
|
|
14
30
|
fingerprint: string;
|
|
@@ -19,4 +35,9 @@ export declare function createApprovalRecord(params: {
|
|
|
19
35
|
approvalId: string;
|
|
20
36
|
input?: string;
|
|
21
37
|
inputKind?: 'shell' | 'tool' | 'subagent';
|
|
38
|
+
cwd?: string;
|
|
39
|
+
toolName?: string;
|
|
40
|
+
payloadHash?: string;
|
|
41
|
+
payloadJson?: string;
|
|
42
|
+
scopeHint?: ApprovalRecord['scopeHint'];
|
|
22
43
|
}): ApprovalRecord;
|
package/dist/core/approval.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { buildReplayEnvelopeFields } from './approval-replay.js';
|
|
2
|
+
import { DEFAULT_APPROVAL_CONFIG } from './config.js';
|
|
3
|
+
/** @deprecated Use `DEFAULT_APPROVAL_CONFIG.executionLeaseMs` */
|
|
4
|
+
export const APPROVAL_EXECUTION_LEASE_MS = DEFAULT_APPROVAL_CONFIG.executionLeaseMs;
|
|
3
5
|
export function nowIso() {
|
|
4
6
|
return new Date().toISOString();
|
|
5
7
|
}
|
|
@@ -49,8 +51,34 @@ export function approvalCommandMatch(prompt, tokenPrefix) {
|
|
|
49
51
|
}
|
|
50
52
|
return null;
|
|
51
53
|
}
|
|
52
|
-
export function
|
|
53
|
-
|
|
54
|
+
export function createApprovalRecordWithEnvelope(params) {
|
|
55
|
+
const envelope = buildReplayEnvelopeFields({
|
|
56
|
+
kind: params.kind,
|
|
57
|
+
command: params.approvalInput?.input,
|
|
58
|
+
input: params.approvalInput?.input,
|
|
59
|
+
inputKind: params.approvalInput?.inputKind,
|
|
60
|
+
cwd: params.approvalInput?.cwd,
|
|
61
|
+
toolName: params.approvalInput?.toolName,
|
|
62
|
+
payload: params.approvalInput?.payload,
|
|
63
|
+
fingerprint: params.fingerprint,
|
|
64
|
+
repoRoot: params.repoRoot,
|
|
65
|
+
});
|
|
66
|
+
return createApprovalRecord({
|
|
67
|
+
kind: params.kind,
|
|
68
|
+
fingerprint: params.fingerprint,
|
|
69
|
+
repoRoot: params.repoRoot,
|
|
70
|
+
reason: params.reason,
|
|
71
|
+
summary: params.summary,
|
|
72
|
+
approvalTtlMinutes: params.approvalTtlMinutes,
|
|
73
|
+
approvalId: params.approvalId,
|
|
74
|
+
input: envelope.input ?? params.approvalInput?.input,
|
|
75
|
+
inputKind: envelope.inputKind ?? params.approvalInput?.inputKind,
|
|
76
|
+
cwd: envelope.cwd,
|
|
77
|
+
toolName: envelope.toolName ?? params.approvalInput?.toolName,
|
|
78
|
+
payloadHash: envelope.payloadHash,
|
|
79
|
+
payloadJson: envelope.payloadJson,
|
|
80
|
+
scopeHint: params.scopeHint,
|
|
81
|
+
});
|
|
54
82
|
}
|
|
55
83
|
export function createApprovalRecord(params) {
|
|
56
84
|
const createdAt = nowIso();
|
|
@@ -69,5 +97,20 @@ export function createApprovalRecord(params) {
|
|
|
69
97
|
record.input = params.input;
|
|
70
98
|
record.inputKind = params.inputKind ?? params.kind;
|
|
71
99
|
}
|
|
100
|
+
if (params.cwd) {
|
|
101
|
+
record.cwd = params.cwd;
|
|
102
|
+
}
|
|
103
|
+
if (params.toolName) {
|
|
104
|
+
record.toolName = params.toolName;
|
|
105
|
+
}
|
|
106
|
+
if (params.payloadHash) {
|
|
107
|
+
record.payloadHash = params.payloadHash;
|
|
108
|
+
}
|
|
109
|
+
if (params.payloadJson) {
|
|
110
|
+
record.payloadJson = params.payloadJson;
|
|
111
|
+
}
|
|
112
|
+
if (params.scopeHint) {
|
|
113
|
+
record.scopeHint = params.scopeHint;
|
|
114
|
+
}
|
|
72
115
|
return record;
|
|
73
116
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ApprovalRoundTrip, AuditRecord, BypassAttempt, NoisyRuleCandidate } from './audit-types.js';
|
|
1
|
+
import type { ApprovalRoundTrip, AuditRecord, AvailabilityAskCounts, BypassAttempt, NoisyRuleCandidate, ReasonApprovalRatio, RepeatedFingerprintAsk } from './audit-types.js';
|
|
2
2
|
export declare function detectBypassAttempts(records: AuditRecord[], windowMs?: number): BypassAttempt[];
|
|
3
3
|
export declare function detectNoisyRules(records: AuditRecord[], roundTrips: ApprovalRoundTrip[], minDenies?: number): NoisyRuleCandidate[];
|
|
4
4
|
export declare function computeApprovalLatencyStats(roundTrips: ApprovalRoundTrip[]): {
|
|
@@ -8,3 +8,9 @@ export declare function computeApprovalLatencyStats(roundTrips: ApprovalRoundTri
|
|
|
8
8
|
};
|
|
9
9
|
export declare function bucketGateEventsByDay(records: AuditRecord[]): Record<string, number>;
|
|
10
10
|
export declare function countVerdicts(records: AuditRecord[]): Record<string, number>;
|
|
11
|
+
export declare function computeWouldBlockByReason(records: AuditRecord[]): Record<string, number>;
|
|
12
|
+
/** Approval ratios are candidate signals from operator behavior — not corpus ground truth. */
|
|
13
|
+
export declare function computeApprovalRatioByReason(records: AuditRecord[], roundTrips: ApprovalRoundTrip[]): ReasonApprovalRatio[];
|
|
14
|
+
export declare function isAvailabilityCausedAsk(record: AuditRecord): boolean;
|
|
15
|
+
export declare function computeAvailabilityAskCounts(records: AuditRecord[]): AvailabilityAskCounts;
|
|
16
|
+
export declare function computeRepeatedFingerprintAsks(records: AuditRecord[], minCount?: number, limit?: number): RepeatedFingerprintAsk[];
|