@mikode13/harness 9.0.0 → 9.2.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 +11 -4
- package/dist/engines/domain/model/commandLoops.d.ts +59 -0
- package/dist/engines/domain/model/commandLoops.js +143 -0
- package/dist/engines/domain/model/commandLoops.js.map +1 -0
- package/dist/engines/domain/model/llmAgent.d.ts +6 -0
- package/dist/engines/domain/model/llmAgent.js +180 -50
- package/dist/engines/domain/model/llmAgent.js.map +1 -1
- package/dist/llm/infrastructure/claudeLLMClient.js +3 -3
- package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -1
- package/dist/recovery/domain/recoveryStore.d.ts +8 -0
- package/dist/recovery/domain/recoveryStore.js.map +1 -1
- package/dist/recovery/infrastructure/fileRecoveryStore.js +38 -2
- package/dist/recovery/infrastructure/fileRecoveryStore.js.map +1 -1
- package/dist/tools/domain/preparedCall.d.ts +26 -4
- package/dist/tools/domain/preparedCall.js +1 -1
- package/dist/tools/domain/preparedCall.js.map +1 -1
- package/dist/tools/infrastructure/commandOutput.d.ts +14 -0
- package/dist/tools/infrastructure/commandOutput.js +99 -0
- package/dist/tools/infrastructure/commandOutput.js.map +1 -0
- package/dist/tools/infrastructure/commandProcess.d.ts +3 -1
- package/dist/tools/infrastructure/commandProcess.js +3 -1
- package/dist/tools/infrastructure/commandProcess.js.map +1 -1
- package/dist/tools/infrastructure/commandTool.js +25 -11
- package/dist/tools/infrastructure/commandTool.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
3
|
+
// eslint-disable-next-line no-control-regex -- the escape that starts every terminal color
|
|
4
|
+
const colors = /\u001b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
5
|
+
const duration = String.raw `\d+(?:\.\d+)?\s?(?:ms|s|m|h)`;
|
|
6
|
+
const clock = String.raw `\d{1,2}:\d{2}:\d{2}(?:\.\d+)?(?:\s?[AP]M)?`;
|
|
7
|
+
const anyTime = new RegExp(String.raw `\b(?:${clock}|${duration})\b`, 'g');
|
|
8
|
+
/**
|
|
9
|
+
* The lines a known runner prints only to say when or for how long it ran, each with what of it
|
|
10
|
+
* is a time. Anything else keeps its times: in `expected 1s, received 2s` they are what failed.
|
|
11
|
+
*/
|
|
12
|
+
const runnerTimes = [
|
|
13
|
+
// Vitest's and Jest's summaries, and the last line of yarn, cargo or a build.
|
|
14
|
+
{ line: /^\s*(?:Start at|Duration|Time:|Done in|Finished|finished in)\s/, time: anyTime },
|
|
15
|
+
// cargo test's result, and pytest's summary between rules of `=`.
|
|
16
|
+
{ line: /^\s*test result:|^=+ .* =+$/, time: anyTime },
|
|
17
|
+
// Mocha's counts, such as "5 passing (12ms)".
|
|
18
|
+
{ line: /^\s*\d+ (?:passing|failing|pending) \(/, time: anyTime },
|
|
19
|
+
// A test or file reported with a mark or a status, such as Vitest's "✓ a.test.ts (3 tests)
|
|
20
|
+
// 12ms", Jest's "PASS a.test.ts (5.1 s)" or Go's "--- FAIL: TestA (0.00s)": the time at its end.
|
|
21
|
+
{
|
|
22
|
+
line: /^\s*(?:[✓✔√×✗✕✘↓]|PASS\b|FAIL\b|ok\b|not ok\b|--- (?:PASS|FAIL|SKIP):)/,
|
|
23
|
+
time: new RegExp(String.raw `\(?\b${duration}\)?\s*$`),
|
|
24
|
+
},
|
|
25
|
+
// Node's test runner, whose durations have no unit.
|
|
26
|
+
{ line: /^\s*(?:[#ℹ] duration_ms|duration_ms:)\s/, time: /\d+(?:\.\d+)?\s*$/ },
|
|
27
|
+
// `tsc --watch`, which starts each line with the time.
|
|
28
|
+
{ line: new RegExp(String.raw `^\[?${clock}\]?\s`), time: new RegExp(`^\\[?${clock}\\]?`) },
|
|
29
|
+
// `prettier --write`, such as "src/a.ts 12ms (unchanged)".
|
|
30
|
+
{
|
|
31
|
+
line: new RegExp(String.raw `^\S*[/.]\S*\s${duration}(?:\s\(unchanged\))?$`),
|
|
32
|
+
time: new RegExp(String.raw `\b${duration}\b`),
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
/** What a command printed without its colors, as the user is shown it. */
|
|
36
|
+
export function withoutColors(output) {
|
|
37
|
+
return output.replace(colors, '');
|
|
38
|
+
}
|
|
39
|
+
/** One line as two runs that ended alike print it: no colors, and no time a known runner adds. */
|
|
40
|
+
export function normalizedLine(line) {
|
|
41
|
+
const plain = withoutColors(line);
|
|
42
|
+
const rule = runnerTimes.find(({ line: pattern }) => pattern.test(plain));
|
|
43
|
+
return rule ? plain.replace(rule.time, '<time>') : plain;
|
|
44
|
+
}
|
|
45
|
+
// A line longer than this is hashed as printed, colors and all, whether it came whole or in
|
|
46
|
+
// chunks: no runner prints its time in one, and what is kept of it stays bounded.
|
|
47
|
+
const longestLine = 65_536;
|
|
48
|
+
/** One stream's lines, hashed as they arrive. */
|
|
49
|
+
class StreamFingerprint {
|
|
50
|
+
decoder = new StringDecoder('utf8');
|
|
51
|
+
hash = createHash('sha256');
|
|
52
|
+
partial = '';
|
|
53
|
+
// Part of the line being read was already hashed, as printed, since it ran past `longestLine`.
|
|
54
|
+
long = false;
|
|
55
|
+
push(chunk) {
|
|
56
|
+
this.add(this.decoder.write(chunk));
|
|
57
|
+
}
|
|
58
|
+
digest() {
|
|
59
|
+
this.add(this.decoder.end());
|
|
60
|
+
if (this.partial !== '' || this.long)
|
|
61
|
+
this.line(this.partial);
|
|
62
|
+
return this.hash.digest('hex');
|
|
63
|
+
}
|
|
64
|
+
add(text) {
|
|
65
|
+
const lines = (this.partial + text).split('\n');
|
|
66
|
+
this.partial = lines.pop() ?? '';
|
|
67
|
+
for (const line of lines)
|
|
68
|
+
this.line(line);
|
|
69
|
+
if (this.partial.length > longestLine) {
|
|
70
|
+
this.hash.update(this.partial);
|
|
71
|
+
this.partial = '';
|
|
72
|
+
this.long = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// A long line is hashed as it came, `\r` included, so its pieces add up to the whole line.
|
|
76
|
+
line(line) {
|
|
77
|
+
this.hash.update(this.long || line.length > longestLine
|
|
78
|
+
? `${line}\n`
|
|
79
|
+
: `${normalizedLine(line.replace(/\r$/, ''))}\n`);
|
|
80
|
+
this.long = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Everything a command printed, as one hash: every line, normalized, of its standard output and
|
|
85
|
+
* its error apart, since their order between them can differ from one run to the next. Not only
|
|
86
|
+
* the end its result keeps, so two failures that end alike but differ before are told apart.
|
|
87
|
+
*/
|
|
88
|
+
export class OutputFingerprint {
|
|
89
|
+
streams = { stdout: new StreamFingerprint(), stderr: new StreamFingerprint() };
|
|
90
|
+
push(stream, chunk) {
|
|
91
|
+
this.streams[stream].push(chunk);
|
|
92
|
+
}
|
|
93
|
+
digest() {
|
|
94
|
+
return createHash('sha256')
|
|
95
|
+
.update(`${this.streams.stdout.digest()}\n${this.streams.stderr.digest()}`)
|
|
96
|
+
.digest('hex');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=commandOutput.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commandOutput.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/commandOutput.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, type Hash } from 'node:crypto';\nimport { StringDecoder } from 'node:string_decoder';\n\n// eslint-disable-next-line no-control-regex -- the escape that starts every terminal color\nconst colors = /\\u001b\\[[0-9;?]*[ -/]*[@-~]/g;\n\nconst duration = String.raw`\\d+(?:\\.\\d+)?\\s?(?:ms|s|m|h)`;\nconst clock = String.raw`\\d{1,2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:\\s?[AP]M)?`;\nconst anyTime = new RegExp(String.raw`\\b(?:${clock}|${duration})\\b`, 'g');\n\n/**\n * The lines a known runner prints only to say when or for how long it ran, each with what of it\n * is a time. Anything else keeps its times: in `expected 1s, received 2s` they are what failed.\n */\nconst runnerTimes: { line: RegExp; time: RegExp }[] = [\n\t// Vitest's and Jest's summaries, and the last line of yarn, cargo or a build.\n\t{ line: /^\\s*(?:Start at|Duration|Time:|Done in|Finished|finished in)\\s/, time: anyTime },\n\t// cargo test's result, and pytest's summary between rules of `=`.\n\t{ line: /^\\s*test result:|^=+ .* =+$/, time: anyTime },\n\t// Mocha's counts, such as \"5 passing (12ms)\".\n\t{ line: /^\\s*\\d+ (?:passing|failing|pending) \\(/, time: anyTime },\n\t// A test or file reported with a mark or a status, such as Vitest's \"✓ a.test.ts (3 tests)\n\t// 12ms\", Jest's \"PASS a.test.ts (5.1 s)\" or Go's \"--- FAIL: TestA (0.00s)\": the time at its end.\n\t{\n\t\tline: /^\\s*(?:[✓✔√×✗✕✘↓]|PASS\\b|FAIL\\b|ok\\b|not ok\\b|--- (?:PASS|FAIL|SKIP):)/,\n\t\ttime: new RegExp(String.raw`\\(?\\b${duration}\\)?\\s*$`),\n\t},\n\t// Node's test runner, whose durations have no unit.\n\t{ line: /^\\s*(?:[#ℹ] duration_ms|duration_ms:)\\s/, time: /\\d+(?:\\.\\d+)?\\s*$/ },\n\t// `tsc --watch`, which starts each line with the time.\n\t{ line: new RegExp(String.raw`^\\[?${clock}\\]?\\s`), time: new RegExp(`^\\\\[?${clock}\\\\]?`) },\n\t// `prettier --write`, such as \"src/a.ts 12ms (unchanged)\".\n\t{\n\t\tline: new RegExp(String.raw`^\\S*[/.]\\S*\\s${duration}(?:\\s\\(unchanged\\))?$`),\n\t\ttime: new RegExp(String.raw`\\b${duration}\\b`),\n\t},\n];\n\n/** What a command printed without its colors, as the user is shown it. */\nexport function withoutColors(output: string): string {\n\treturn output.replace(colors, '');\n}\n\n/** One line as two runs that ended alike print it: no colors, and no time a known runner adds. */\nexport function normalizedLine(line: string): string {\n\tconst plain = withoutColors(line);\n\tconst rule = runnerTimes.find(({ line: pattern }) => pattern.test(plain));\n\treturn rule ? plain.replace(rule.time, '<time>') : plain;\n}\n\n// A line longer than this is hashed as printed, colors and all, whether it came whole or in\n// chunks: no runner prints its time in one, and what is kept of it stays bounded.\nconst longestLine = 65_536;\n\n/** One stream's lines, hashed as they arrive. */\nclass StreamFingerprint {\n\tprivate readonly decoder = new StringDecoder('utf8');\n\tprivate readonly hash: Hash = createHash('sha256');\n\tprivate partial = '';\n\t// Part of the line being read was already hashed, as printed, since it ran past `longestLine`.\n\tprivate long = false;\n\n\tpush(chunk: Buffer): void {\n\t\tthis.add(this.decoder.write(chunk));\n\t}\n\n\tdigest(): string {\n\t\tthis.add(this.decoder.end());\n\t\tif (this.partial !== '' || this.long) this.line(this.partial);\n\t\treturn this.hash.digest('hex');\n\t}\n\n\tprivate add(text: string): void {\n\t\tconst lines = (this.partial + text).split('\\n');\n\t\tthis.partial = lines.pop() ?? '';\n\t\tfor (const line of lines) this.line(line);\n\t\tif (this.partial.length > longestLine) {\n\t\t\tthis.hash.update(this.partial);\n\t\t\tthis.partial = '';\n\t\t\tthis.long = true;\n\t\t}\n\t}\n\n\t// A long line is hashed as it came, `\\r` included, so its pieces add up to the whole line.\n\tprivate line(line: string): void {\n\t\tthis.hash.update(\n\t\t\tthis.long || line.length > longestLine\n\t\t\t\t? `${line}\\n`\n\t\t\t\t: `${normalizedLine(line.replace(/\\r$/, ''))}\\n`,\n\t\t);\n\t\tthis.long = false;\n\t}\n}\n\n/**\n * Everything a command printed, as one hash: every line, normalized, of its standard output and\n * its error apart, since their order between them can differ from one run to the next. Not only\n * the end its result keeps, so two failures that end alike but differ before are told apart.\n */\nexport class OutputFingerprint {\n\tprivate readonly streams = { stdout: new StreamFingerprint(), stderr: new StreamFingerprint() };\n\n\tpush(stream: 'stdout' | 'stderr', chunk: Buffer): void {\n\t\tthis.streams[stream].push(chunk);\n\t}\n\n\tdigest(): string {\n\t\treturn createHash('sha256')\n\t\t\t.update(`${this.streams.stdout.digest()}\\n${this.streams.stderr.digest()}`)\n\t\t\t.digest('hex');\n\t}\n}\n"]}
|
|
@@ -30,7 +30,7 @@ export interface CommandProcessResult {
|
|
|
30
30
|
*
|
|
31
31
|
* @throws {Error} when the program cannot be started, such as when it is not found; nothing ran.
|
|
32
32
|
*/
|
|
33
|
-
export declare function runCommandProcess(executable: string, args: string[], { cwd, env, timeoutMs, outputBytes, signal, gracePeriodMs, killWaitMs, recheckMs, }: {
|
|
33
|
+
export declare function runCommandProcess(executable: string, args: string[], { cwd, env, timeoutMs, outputBytes, signal, gracePeriodMs, killWaitMs, recheckMs, onOutput, }: {
|
|
34
34
|
cwd: string;
|
|
35
35
|
env: NodeJS.ProcessEnv;
|
|
36
36
|
timeoutMs: number;
|
|
@@ -42,6 +42,8 @@ export declare function runCommandProcess(executable: string, args: string[], {
|
|
|
42
42
|
killWaitMs?: number;
|
|
43
43
|
/** How often an unconfirmed group is asked about again. Defaults to 1 s. */
|
|
44
44
|
recheckMs?: number;
|
|
45
|
+
/** Given every chunk it prints as it arrives, all of it, not only the end `output` keeps. */
|
|
46
|
+
onOutput?: (stream: 'stdout' | 'stderr', chunk: Buffer) => void;
|
|
45
47
|
}): Promise<CommandProcessResult>;
|
|
46
48
|
/** The groups of the commands still running, for tests. */
|
|
47
49
|
export declare function liveGroupIds(): number[];
|
|
@@ -18,7 +18,7 @@ const drainMs = 1_000;
|
|
|
18
18
|
*
|
|
19
19
|
* @throws {Error} when the program cannot be started, such as when it is not found; nothing ran.
|
|
20
20
|
*/
|
|
21
|
-
export function runCommandProcess(executable, args, { cwd, env, timeoutMs, outputBytes, signal, gracePeriodMs = defaultGracePeriodMs, killWaitMs = defaultKillWaitMs, recheckMs = defaultRecheckMs, }) {
|
|
21
|
+
export function runCommandProcess(executable, args, { cwd, env, timeoutMs, outputBytes, signal, gracePeriodMs = defaultGracePeriodMs, killWaitMs = defaultKillWaitMs, recheckMs = defaultRecheckMs, onOutput, }) {
|
|
22
22
|
// A rejection, not a throw: a caller awaiting the result must see it as one.
|
|
23
23
|
if (signal.aborted)
|
|
24
24
|
return Promise.reject(signal.reason);
|
|
@@ -42,9 +42,11 @@ export function runCommandProcess(executable, args, { cwd, env, timeoutMs, outpu
|
|
|
42
42
|
}))));
|
|
43
43
|
child.stdout.on('data', (chunk) => {
|
|
44
44
|
tail.push(chunk);
|
|
45
|
+
onOutput?.('stdout', chunk);
|
|
45
46
|
});
|
|
46
47
|
child.stderr.on('data', (chunk) => {
|
|
47
48
|
tail.push(chunk);
|
|
49
|
+
onOutput?.('stderr', chunk);
|
|
48
50
|
});
|
|
49
51
|
const stop = (why) => {
|
|
50
52
|
// The first reason stands: a timeout during a cancellation's grace period is still a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commandProcess.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/commandProcess.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport { setTimeout as delay } from 'node:timers/promises';\n\n// How long a group asked to stop gets before it is killed, and how long a kill gets to take.\nconst defaultGracePeriodMs = 5_000;\nconst defaultKillWaitMs = 5_000;\nconst pollMs = 25;\n// Output still in the pipes once every process is gone arrives at once; a process that left\n// the group and holds a pipe open must not hold up the result.\nconst drainMs = 1_000;\n\n/**\n * How a command ended:\n * - `exited`: it ended by itself, with `exitCode`, or killed by `signal` from outside.\n * - `timedOut`: it ran past its time and was stopped.\n * - `cancelled`: the run was cancelled while it ran, and it was stopped.\n */\nexport interface CommandProcessResult {\n\toutcome: 'exited' | 'timedOut' | 'cancelled';\n\texitCode: number | null;\n\tsignal: NodeJS.Signals | null;\n\t/** The end of what it printed, standard output and error together, as they arrived. */\n\toutput: string;\n\t/** Everything it printed, of which `output` is the end. */\n\tprintedBytes: number;\n\tdurationMs: number;\n\t/**\n\t * Every process of its group is gone. When false, something it started may still be\n\t * changing files, so nothing observed after it can be taken as its final effect.\n\t */\n\tstopped: boolean;\n}\n\n/**\n * Runs a configured command in a process group of its own, never through a shell, and returns\n * once that group is gone. A timeout or a cancellation stops the whole group: first asked with\n * `SIGTERM`, then killed. A command that ends by itself has whatever it left running in its\n * group stopped too, so nothing it started outlives it. A process that leaves the group, as a\n * daemon does, is out of reach; the docs say so.\n *\n * Only for POSIX: Windows has no process groups to stop.\n *\n * @throws {Error} when the program cannot be started, such as when it is not found; nothing ran.\n */\nexport function runCommandProcess(\n\texecutable: string,\n\targs: string[],\n\t{\n\t\tcwd,\n\t\tenv,\n\t\ttimeoutMs,\n\t\toutputBytes,\n\t\tsignal,\n\t\tgracePeriodMs = defaultGracePeriodMs,\n\t\tkillWaitMs = defaultKillWaitMs,\n\t\trecheckMs = defaultRecheckMs,\n\t}: {\n\t\tcwd: string;\n\t\tenv: NodeJS.ProcessEnv;\n\t\ttimeoutMs: number;\n\t\toutputBytes: number;\n\t\tsignal: AbortSignal;\n\t\t/** How long the group gets to stop on `SIGTERM` before it is killed. Defaults to 5 s. */\n\t\tgracePeriodMs?: number;\n\t\t/** How long a killed group gets to disappear before it counts as unconfirmed. 5 s. */\n\t\tkillWaitMs?: number;\n\t\t/** How often an unconfirmed group is asked about again. Defaults to 1 s. */\n\t\trecheckMs?: number;\n\t},\n): Promise<CommandProcessResult> {\n\t// A rejection, not a throw: a caller awaiting the result must see it as one.\n\tif (signal.aborted) return Promise.reject(signal.reason as Error);\n\tconst started = performance.now();\n\n\treturn new Promise((resolve, reject) => {\n\t\t// `detached` makes it the leader of a new process group, whose id is its pid.\n\t\tconst child = spawn(executable, args, {\n\t\t\tcwd,\n\t\t\tenv,\n\t\t\tdetached: true,\n\t\t\tstdio: ['ignore', 'pipe', 'pipe'],\n\t\t});\n\t\tif (child.pid !== undefined) watchForExit(child.pid);\n\t\tconst tail = new OutputTail(outputBytes);\n\t\tlet outcome: CommandProcessResult['outcome'] = 'exited';\n\t\tlet stopping: Promise<boolean> | undefined;\n\t\tlet exit: { code: number | null; signal: NodeJS.Signals | null } | undefined;\n\n\t\tconst streamsClosed = Promise.all(\n\t\t\t[child.stdout, child.stderr].map(\n\t\t\t\tstream =>\n\t\t\t\t\tnew Promise<void>(done =>\n\t\t\t\t\t\tstream.once('close', () => {\n\t\t\t\t\t\t\tdone();\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t),\n\t\t);\n\t\tchild.stdout.on('data', (chunk: Buffer) => {\n\t\t\ttail.push(chunk);\n\t\t});\n\t\tchild.stderr.on('data', (chunk: Buffer) => {\n\t\t\ttail.push(chunk);\n\t\t});\n\n\t\tconst stop = (why: 'timedOut' | 'cancelled'): void => {\n\t\t\t// The first reason stands: a timeout during a cancellation's grace period is still a\n\t\t\t// cancellation, and must end the run as one.\n\t\t\tif (exit || stopping || child.pid === undefined) return;\n\t\t\toutcome = why;\n\t\t\tdetach();\n\t\t\tstopping = stopGroup(child.pid, { gracePeriodMs, killWaitMs });\n\t\t\t// A leader that outlives even the kill never exits, so the result cannot wait for it.\n\t\t\tvoid stopping.then(gone => {\n\t\t\t\tif (!gone) void finish(false);\n\t\t\t});\n\t\t};\n\t\tconst timer = setTimeout(() => {\n\t\t\tstop('timedOut');\n\t\t}, timeoutMs);\n\t\tconst onAbort = () => {\n\t\t\tstop('cancelled');\n\t\t};\n\t\tsignal.addEventListener('abort', onAbort, { once: true });\n\t\tconst detach = () => {\n\t\t\tclearTimeout(timer);\n\t\t\tsignal.removeEventListener('abort', onAbort);\n\t\t};\n\n\t\tlet finished = false;\n\t\tconst finish = async (stopped: boolean) => {\n\t\t\tif (finished) return;\n\t\t\tfinished = true;\n\t\t\tdetach();\n\t\t\tif (child.pid !== undefined) {\n\t\t\t\tif (stopped) liveGroups.delete(child.pid);\n\t\t\t\telse forgetWhenGone(child.pid, recheckMs);\n\t\t\t}\n\t\t\tawait Promise.race([streamsClosed, delay(drainMs)]);\n\t\t\tchild.stdout.destroy();\n\t\t\tchild.stderr.destroy();\n\t\t\tresolve({\n\t\t\t\toutcome,\n\t\t\t\texitCode: exit?.code ?? null,\n\t\t\t\tsignal: exit?.signal ?? null,\n\t\t\t\toutput: tail.text(),\n\t\t\t\tprintedBytes: tail.total,\n\t\t\t\tdurationMs: Math.round(performance.now() - started),\n\t\t\t\tstopped,\n\t\t\t});\n\t\t};\n\n\t\tchild.once('error', error => {\n\t\t\t// Only a program that never started: nothing here signals the child through Node.\n\t\t\tif (child.pid !== undefined) return;\n\t\t\tdetach();\n\t\t\treject(error);\n\t\t});\n\t\tchild.once('exit', (code, exitSignal) => {\n\t\t\texit = { code, signal: exitSignal };\n\t\t\tdetach();\n\t\t\tconst pid = child.pid;\n\t\t\t// What it left in its group is stopped as well; an empty group is gone at once.\n\t\t\tvoid (\n\t\t\t\tstopping ??\n\t\t\t\t(pid === undefined ? Promise.resolve(true) : stopGroup(pid, { gracePeriodMs, killWaitMs }))\n\t\t\t).then(finish);\n\t\t});\n\t});\n}\n\n/**\n * The groups of the commands still running. If this process exits while one runs, as on an\n * uncaught exception, each is killed on the way out: nothing would be left to stop it or\n * record what it changed. A process killed by a signal gets no such chance, and its commands\n * then run on until they end.\n */\nconst liveGroups = new Set<number>();\nlet exitHookInstalled = false;\n\nconst defaultRecheckMs = 1_000;\n\n/** The groups of the commands still running, for tests. */\nexport function liveGroupIds(): number[] {\n\treturn [...liveGroups];\n}\n\n/**\n * Keeps an unconfirmed group in `liveGroups` until the system says it is gone, then forgets\n * it: once the group is empty its id can be given to another, which the exit hook must never\n * kill. It asks every second, so a reuse in between is the only gap; nothing short of a pidfd\n * closes it.\n */\nfunction forgetWhenGone(pgid: number, intervalMs: number): void {\n\tconst timer = setInterval(() => {\n\t\tif (signalGroup(pgid, 0)) return;\n\t\tliveGroups.delete(pgid);\n\t\tclearInterval(timer);\n\t}, intervalMs);\n\t// It must not keep this process alive on its own.\n\ttimer.unref();\n}\n\nfunction watchForExit(pgid: number): void {\n\tliveGroups.add(pgid);\n\tif (exitHookInstalled) return;\n\texitHookInstalled = true;\n\tprocess.once('exit', () => {\n\t\tfor (const group of liveGroups) signalGroup(group, 'SIGKILL');\n\t});\n}\n\n/**\n * Stops every process in the group `pgid` and waits until none is left. Whether it is gone is\n * asked of the system, never assumed from a signal being sent.\n *\n * @returns whether the group is confirmed gone.\n */\nasync function stopGroup(\n\tpgid: number,\n\t{ gracePeriodMs, killWaitMs }: { gracePeriodMs: number; killWaitMs: number },\n): Promise<boolean> {\n\tif (!signalGroup(pgid, 'SIGTERM')) return true;\n\tif (await goneWithin(pgid, gracePeriodMs)) return true;\n\tif (!signalGroup(pgid, 'SIGKILL')) return true;\n\treturn goneWithin(pgid, killWaitMs);\n}\n\n/** Sends `signal` to the group; false when the group no longer exists. */\nfunction signalGroup(pgid: number, signal: NodeJS.Signals | 0): boolean {\n\ttry {\n\t\tprocess.kill(-pgid, signal);\n\t\treturn true;\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === 'ESRCH') return false;\n\t\t// EPERM: a process of the group exists, but this one may not signal it.\n\t\treturn true;\n\t}\n}\n\nasync function goneWithin(pgid: number, ms: number): Promise<boolean> {\n\tconst deadline = performance.now() + ms;\n\twhile (performance.now() < deadline) {\n\t\tif (!signalGroup(pgid, 0)) return true;\n\t\tawait delay(pollMs);\n\t}\n\treturn !signalGroup(pgid, 0);\n}\n\n/** Keeps only the last `limit` bytes of what is pushed, and counts all of it. */\nclass OutputTail {\n\tprivate readonly limit: number;\n\tprivate chunks: Buffer[] = [];\n\tprivate kept = 0;\n\ttotal = 0;\n\n\tconstructor(limit: number) {\n\t\tthis.limit = limit;\n\t}\n\n\tpush(chunk: Buffer): void {\n\t\tthis.total += chunk.length;\n\t\tthis.chunks.push(chunk);\n\t\tthis.kept += chunk.length;\n\t\twhile (this.chunks.length > 1 && this.kept - (this.chunks[0]?.length ?? 0) >= this.limit) {\n\t\t\tthis.kept -= this.chunks.shift()?.length ?? 0;\n\t\t}\n\t}\n\n\ttext(): string {\n\t\tconst all = Buffer.concat(this.chunks);\n\t\tconst cut = all.length > this.limit;\n\t\tconst text = (cut ? all.subarray(all.length - this.limit) : all).toString('utf8');\n\t\t// A cut can land inside a character, which decodes as replacement characters.\n\t\treturn cut ? text.replace(/^�+/u, '') : text;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"commandProcess.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/commandProcess.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport { setTimeout as delay } from 'node:timers/promises';\n\n// How long a group asked to stop gets before it is killed, and how long a kill gets to take.\nconst defaultGracePeriodMs = 5_000;\nconst defaultKillWaitMs = 5_000;\nconst pollMs = 25;\n// Output still in the pipes once every process is gone arrives at once; a process that left\n// the group and holds a pipe open must not hold up the result.\nconst drainMs = 1_000;\n\n/**\n * How a command ended:\n * - `exited`: it ended by itself, with `exitCode`, or killed by `signal` from outside.\n * - `timedOut`: it ran past its time and was stopped.\n * - `cancelled`: the run was cancelled while it ran, and it was stopped.\n */\nexport interface CommandProcessResult {\n\toutcome: 'exited' | 'timedOut' | 'cancelled';\n\texitCode: number | null;\n\tsignal: NodeJS.Signals | null;\n\t/** The end of what it printed, standard output and error together, as they arrived. */\n\toutput: string;\n\t/** Everything it printed, of which `output` is the end. */\n\tprintedBytes: number;\n\tdurationMs: number;\n\t/**\n\t * Every process of its group is gone. When false, something it started may still be\n\t * changing files, so nothing observed after it can be taken as its final effect.\n\t */\n\tstopped: boolean;\n}\n\n/**\n * Runs a configured command in a process group of its own, never through a shell, and returns\n * once that group is gone. A timeout or a cancellation stops the whole group: first asked with\n * `SIGTERM`, then killed. A command that ends by itself has whatever it left running in its\n * group stopped too, so nothing it started outlives it. A process that leaves the group, as a\n * daemon does, is out of reach; the docs say so.\n *\n * Only for POSIX: Windows has no process groups to stop.\n *\n * @throws {Error} when the program cannot be started, such as when it is not found; nothing ran.\n */\nexport function runCommandProcess(\n\texecutable: string,\n\targs: string[],\n\t{\n\t\tcwd,\n\t\tenv,\n\t\ttimeoutMs,\n\t\toutputBytes,\n\t\tsignal,\n\t\tgracePeriodMs = defaultGracePeriodMs,\n\t\tkillWaitMs = defaultKillWaitMs,\n\t\trecheckMs = defaultRecheckMs,\n\t\tonOutput,\n\t}: {\n\t\tcwd: string;\n\t\tenv: NodeJS.ProcessEnv;\n\t\ttimeoutMs: number;\n\t\toutputBytes: number;\n\t\tsignal: AbortSignal;\n\t\t/** How long the group gets to stop on `SIGTERM` before it is killed. Defaults to 5 s. */\n\t\tgracePeriodMs?: number;\n\t\t/** How long a killed group gets to disappear before it counts as unconfirmed. 5 s. */\n\t\tkillWaitMs?: number;\n\t\t/** How often an unconfirmed group is asked about again. Defaults to 1 s. */\n\t\trecheckMs?: number;\n\t\t/** Given every chunk it prints as it arrives, all of it, not only the end `output` keeps. */\n\t\tonOutput?: (stream: 'stdout' | 'stderr', chunk: Buffer) => void;\n\t},\n): Promise<CommandProcessResult> {\n\t// A rejection, not a throw: a caller awaiting the result must see it as one.\n\tif (signal.aborted) return Promise.reject(signal.reason as Error);\n\tconst started = performance.now();\n\n\treturn new Promise((resolve, reject) => {\n\t\t// `detached` makes it the leader of a new process group, whose id is its pid.\n\t\tconst child = spawn(executable, args, {\n\t\t\tcwd,\n\t\t\tenv,\n\t\t\tdetached: true,\n\t\t\tstdio: ['ignore', 'pipe', 'pipe'],\n\t\t});\n\t\tif (child.pid !== undefined) watchForExit(child.pid);\n\t\tconst tail = new OutputTail(outputBytes);\n\t\tlet outcome: CommandProcessResult['outcome'] = 'exited';\n\t\tlet stopping: Promise<boolean> | undefined;\n\t\tlet exit: { code: number | null; signal: NodeJS.Signals | null } | undefined;\n\n\t\tconst streamsClosed = Promise.all(\n\t\t\t[child.stdout, child.stderr].map(\n\t\t\t\tstream =>\n\t\t\t\t\tnew Promise<void>(done =>\n\t\t\t\t\t\tstream.once('close', () => {\n\t\t\t\t\t\t\tdone();\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t),\n\t\t);\n\t\tchild.stdout.on('data', (chunk: Buffer) => {\n\t\t\ttail.push(chunk);\n\t\t\tonOutput?.('stdout', chunk);\n\t\t});\n\t\tchild.stderr.on('data', (chunk: Buffer) => {\n\t\t\ttail.push(chunk);\n\t\t\tonOutput?.('stderr', chunk);\n\t\t});\n\n\t\tconst stop = (why: 'timedOut' | 'cancelled'): void => {\n\t\t\t// The first reason stands: a timeout during a cancellation's grace period is still a\n\t\t\t// cancellation, and must end the run as one.\n\t\t\tif (exit || stopping || child.pid === undefined) return;\n\t\t\toutcome = why;\n\t\t\tdetach();\n\t\t\tstopping = stopGroup(child.pid, { gracePeriodMs, killWaitMs });\n\t\t\t// A leader that outlives even the kill never exits, so the result cannot wait for it.\n\t\t\tvoid stopping.then(gone => {\n\t\t\t\tif (!gone) void finish(false);\n\t\t\t});\n\t\t};\n\t\tconst timer = setTimeout(() => {\n\t\t\tstop('timedOut');\n\t\t}, timeoutMs);\n\t\tconst onAbort = () => {\n\t\t\tstop('cancelled');\n\t\t};\n\t\tsignal.addEventListener('abort', onAbort, { once: true });\n\t\tconst detach = () => {\n\t\t\tclearTimeout(timer);\n\t\t\tsignal.removeEventListener('abort', onAbort);\n\t\t};\n\n\t\tlet finished = false;\n\t\tconst finish = async (stopped: boolean) => {\n\t\t\tif (finished) return;\n\t\t\tfinished = true;\n\t\t\tdetach();\n\t\t\tif (child.pid !== undefined) {\n\t\t\t\tif (stopped) liveGroups.delete(child.pid);\n\t\t\t\telse forgetWhenGone(child.pid, recheckMs);\n\t\t\t}\n\t\t\tawait Promise.race([streamsClosed, delay(drainMs)]);\n\t\t\tchild.stdout.destroy();\n\t\t\tchild.stderr.destroy();\n\t\t\tresolve({\n\t\t\t\toutcome,\n\t\t\t\texitCode: exit?.code ?? null,\n\t\t\t\tsignal: exit?.signal ?? null,\n\t\t\t\toutput: tail.text(),\n\t\t\t\tprintedBytes: tail.total,\n\t\t\t\tdurationMs: Math.round(performance.now() - started),\n\t\t\t\tstopped,\n\t\t\t});\n\t\t};\n\n\t\tchild.once('error', error => {\n\t\t\t// Only a program that never started: nothing here signals the child through Node.\n\t\t\tif (child.pid !== undefined) return;\n\t\t\tdetach();\n\t\t\treject(error);\n\t\t});\n\t\tchild.once('exit', (code, exitSignal) => {\n\t\t\texit = { code, signal: exitSignal };\n\t\t\tdetach();\n\t\t\tconst pid = child.pid;\n\t\t\t// What it left in its group is stopped as well; an empty group is gone at once.\n\t\t\tvoid (\n\t\t\t\tstopping ??\n\t\t\t\t(pid === undefined ? Promise.resolve(true) : stopGroup(pid, { gracePeriodMs, killWaitMs }))\n\t\t\t).then(finish);\n\t\t});\n\t});\n}\n\n/**\n * The groups of the commands still running. If this process exits while one runs, as on an\n * uncaught exception, each is killed on the way out: nothing would be left to stop it or\n * record what it changed. A process killed by a signal gets no such chance, and its commands\n * then run on until they end.\n */\nconst liveGroups = new Set<number>();\nlet exitHookInstalled = false;\n\nconst defaultRecheckMs = 1_000;\n\n/** The groups of the commands still running, for tests. */\nexport function liveGroupIds(): number[] {\n\treturn [...liveGroups];\n}\n\n/**\n * Keeps an unconfirmed group in `liveGroups` until the system says it is gone, then forgets\n * it: once the group is empty its id can be given to another, which the exit hook must never\n * kill. It asks every second, so a reuse in between is the only gap; nothing short of a pidfd\n * closes it.\n */\nfunction forgetWhenGone(pgid: number, intervalMs: number): void {\n\tconst timer = setInterval(() => {\n\t\tif (signalGroup(pgid, 0)) return;\n\t\tliveGroups.delete(pgid);\n\t\tclearInterval(timer);\n\t}, intervalMs);\n\t// It must not keep this process alive on its own.\n\ttimer.unref();\n}\n\nfunction watchForExit(pgid: number): void {\n\tliveGroups.add(pgid);\n\tif (exitHookInstalled) return;\n\texitHookInstalled = true;\n\tprocess.once('exit', () => {\n\t\tfor (const group of liveGroups) signalGroup(group, 'SIGKILL');\n\t});\n}\n\n/**\n * Stops every process in the group `pgid` and waits until none is left. Whether it is gone is\n * asked of the system, never assumed from a signal being sent.\n *\n * @returns whether the group is confirmed gone.\n */\nasync function stopGroup(\n\tpgid: number,\n\t{ gracePeriodMs, killWaitMs }: { gracePeriodMs: number; killWaitMs: number },\n): Promise<boolean> {\n\tif (!signalGroup(pgid, 'SIGTERM')) return true;\n\tif (await goneWithin(pgid, gracePeriodMs)) return true;\n\tif (!signalGroup(pgid, 'SIGKILL')) return true;\n\treturn goneWithin(pgid, killWaitMs);\n}\n\n/** Sends `signal` to the group; false when the group no longer exists. */\nfunction signalGroup(pgid: number, signal: NodeJS.Signals | 0): boolean {\n\ttry {\n\t\tprocess.kill(-pgid, signal);\n\t\treturn true;\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === 'ESRCH') return false;\n\t\t// EPERM: a process of the group exists, but this one may not signal it.\n\t\treturn true;\n\t}\n}\n\nasync function goneWithin(pgid: number, ms: number): Promise<boolean> {\n\tconst deadline = performance.now() + ms;\n\twhile (performance.now() < deadline) {\n\t\tif (!signalGroup(pgid, 0)) return true;\n\t\tawait delay(pollMs);\n\t}\n\treturn !signalGroup(pgid, 0);\n}\n\n/** Keeps only the last `limit` bytes of what is pushed, and counts all of it. */\nclass OutputTail {\n\tprivate readonly limit: number;\n\tprivate chunks: Buffer[] = [];\n\tprivate kept = 0;\n\ttotal = 0;\n\n\tconstructor(limit: number) {\n\t\tthis.limit = limit;\n\t}\n\n\tpush(chunk: Buffer): void {\n\t\tthis.total += chunk.length;\n\t\tthis.chunks.push(chunk);\n\t\tthis.kept += chunk.length;\n\t\twhile (this.chunks.length > 1 && this.kept - (this.chunks[0]?.length ?? 0) >= this.limit) {\n\t\t\tthis.kept -= this.chunks.shift()?.length ?? 0;\n\t\t}\n\t}\n\n\ttext(): string {\n\t\tconst all = Buffer.concat(this.chunks);\n\t\tconst cut = all.length > this.limit;\n\t\tconst text = (cut ? all.subarray(all.length - this.limit) : all).toString('utf8');\n\t\t// A cut can land inside a character, which decodes as replacement characters.\n\t\treturn cut ? text.replace(/^�+/u, '') : text;\n\t}\n}\n"]}
|
|
@@ -5,8 +5,9 @@ import { displayPath } from '#src/recovery/domain/runChanges';
|
|
|
5
5
|
import { InvalidAgentConfigError } from '#src/shared/domain/errors';
|
|
6
6
|
import { buildArguments, defaultCommandOutputBytes, defaultCommandTimeoutMs, parametersOf, validateCommands, } from "../domain/commands.js";
|
|
7
7
|
import { EditRefusedError, EditUnconfirmedError } from "../domain/fileEdits.js";
|
|
8
|
-
import { ToolExecutionError } from "../domain/preparedCall.js";
|
|
8
|
+
import { ToolExecutionError, } from "../domain/preparedCall.js";
|
|
9
9
|
import { CommandCheckpoint } from "./commandCheckpoint.js";
|
|
10
|
+
import { OutputFingerprint, withoutColors } from "./commandOutput.js";
|
|
10
11
|
import { runCommandProcess } from "./commandProcess.js";
|
|
11
12
|
import { definePreparingTool } from "./defineTool.js";
|
|
12
13
|
// Only what a toolchain needs to find itself and its locale. Everything else, the provider
|
|
@@ -83,8 +84,8 @@ export async function createCommandTool({ commands, policy, writes, covered, bas
|
|
|
83
84
|
definition: command.definition,
|
|
84
85
|
context: run,
|
|
85
86
|
signal: runSignal,
|
|
86
|
-
ended:
|
|
87
|
-
ending =
|
|
87
|
+
ended: end => {
|
|
88
|
+
ending = end;
|
|
88
89
|
},
|
|
89
90
|
}),
|
|
90
91
|
};
|
|
@@ -104,6 +105,7 @@ export async function createCommandTool({ commands, policy, writes, covered, bas
|
|
|
104
105
|
}
|
|
105
106
|
let result;
|
|
106
107
|
let startFailure;
|
|
108
|
+
const fingerprint = new OutputFingerprint();
|
|
107
109
|
try {
|
|
108
110
|
result = await runProcess(line.executable, line.args, {
|
|
109
111
|
cwd: line.cwd,
|
|
@@ -111,6 +113,9 @@ export async function createCommandTool({ commands, policy, writes, covered, bas
|
|
|
111
113
|
timeoutMs: definition.timeoutMs ?? defaultCommandTimeoutMs,
|
|
112
114
|
outputBytes: definition.outputBytes ?? defaultCommandOutputBytes,
|
|
113
115
|
signal,
|
|
116
|
+
onOutput: (stream, chunk) => {
|
|
117
|
+
fingerprint.push(stream, chunk);
|
|
118
|
+
},
|
|
114
119
|
});
|
|
115
120
|
}
|
|
116
121
|
catch (error) {
|
|
@@ -137,14 +142,14 @@ export async function createCommandTool({ commands, policy, writes, covered, bas
|
|
|
137
142
|
// After its changes are recorded: a cancelled command is never reported as finished.
|
|
138
143
|
if (result.outcome === 'cancelled')
|
|
139
144
|
signal.throwIfAborted();
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
result
|
|
143
|
-
result.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
ended({
|
|
146
|
+
passed: result.outcome === 'exited' && result.exitCode === 0,
|
|
147
|
+
how: howItEnded(result),
|
|
148
|
+
output: withoutColors(result.output),
|
|
149
|
+
fingerprint: fingerprint.digest(),
|
|
150
|
+
...(observed.unrecorded.length === 0 ? { state: journal.contentState() } : {}),
|
|
151
|
+
changed: journal.appliedPaths.map(path => displayPath(base, path)),
|
|
152
|
+
});
|
|
148
153
|
if (result.outcome !== 'exited')
|
|
149
154
|
throw new ToolExecutionError(report);
|
|
150
155
|
if (result.exitCode !== 0) {
|
|
@@ -189,6 +194,15 @@ export async function createCommandTool({ commands, policy, writes, covered, bas
|
|
|
189
194
|
return lines.join('\n');
|
|
190
195
|
}
|
|
191
196
|
}
|
|
197
|
+
function howItEnded(result) {
|
|
198
|
+
if (result.outcome === 'timedOut')
|
|
199
|
+
return 'ran past its time limit';
|
|
200
|
+
if (result.outcome === 'cancelled')
|
|
201
|
+
return 'was cancelled';
|
|
202
|
+
if (result.exitCode !== null)
|
|
203
|
+
return `exited with code ${String(result.exitCode)}`;
|
|
204
|
+
return `was killed by ${result.signal ?? 'a signal'}`;
|
|
205
|
+
}
|
|
192
206
|
function kilobytes(bytes) {
|
|
193
207
|
return `${String(Math.ceil(bytes / 1024))} KB`;
|
|
194
208
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commandTool.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/commandTool.ts"],"names":[],"mappings":"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","sourcesContent":["import { realpath } from 'node:fs/promises';\nimport { isAbsolute, relative, resolve } from 'node:path';\nimport { z } from 'zod';\nimport type { CommandLine } from '#src/agent/domain/approval';\nimport type { RunContext } from '#src/agent/domain/runContext';\nimport { displayPath } from '#src/recovery/domain/runChanges';\nimport { InvalidAgentConfigError } from '#src/shared/domain/errors';\nimport type { AccessPolicy } from '../domain/accessPolicy.ts';\nimport {\n\tbuildArguments,\n\ttype CommandDefinition,\n\ttype CommandParameter,\n\ttype Commands,\n\tdefaultCommandOutputBytes,\n\tdefaultCommandTimeoutMs,\n\ttype ParameterValue,\n\tparametersOf,\n\tvalidateCommands,\n} from '../domain/commands.ts';\nimport { EditRefusedError, EditUnconfirmedError } from '../domain/fileEdits.ts';\nimport { ToolExecutionError, type PreparingTool } from '../domain/preparedCall.ts';\nimport { CommandCheckpoint, type CoveredFiles } from './commandCheckpoint.ts';\nimport { type CommandProcessResult, runCommandProcess } from './commandProcess.ts';\nimport { definePreparingTool } from './defineTool.ts';\nimport type { WorkspaceWrites } from './workspaceWrites.ts';\n\n// Only what a toolchain needs to find itself and its locale. Everything else, the provider\n// keys included, stays in this process.\nconst inheritedVariables = ['PATH', 'HOME', 'TMPDIR', 'USER', 'SHELL', 'LANG'];\n// Long enough for a slow disk, short enough not to hold up building an agent.\nconst cwdCheckMs = 10_000;\n\n/** A command ready to run: its definition, and where it runs as a real path. */\ninterface ResolvedCommand {\n\tdefinition: CommandDefinition;\n\tcwd: string;\n}\n\n/**\n * `runCommand`, which runs one of the commands the consumer configured, by name, with the\n * parameters each one declares. Every call asks the approver, showing the program exactly as\n * it will run; `autoApprove` skips that.\n *\n * Around each command, the covered files are captured before and compared after, so what it\n * changed is in the run's history and can be undone: see `CommandCheckpoint`. If the capture\n * fails, the command does not run. A timeout or a cancellation stops its whole process group,\n * and is never reported as a completed command. If the group cannot be confirmed stopped, what\n * changed so far is recorded, the observation stays open, and writing stops for the run.\n *\n * This is not a sandbox: a command runs the repository's code, with everything this user can\n * reach. See \"Configured commands are trusted, not sandboxed\" in decisions.md.\n *\n * @throws {InvalidAgentConfigError} for an invalid configuration, a working directory outside\n * every `write` root, or on Windows, where a process group cannot be stopped.\n */\nexport async function createCommandTool({\n\tcommands,\n\tpolicy,\n\twrites,\n\tcovered,\n\tbase,\n\tplatform = process.platform,\n\trunProcess = runCommandProcess,\n}: {\n\tcommands: Commands;\n\tpolicy: AccessPolicy;\n\twrites: WorkspaceWrites;\n\tcovered: CoveredFiles;\n\t/** The real path of the first root, where a relative `cwd` starts. */\n\tbase: string;\n\tplatform?: NodeJS.Platform;\n\t/** Runs the program. Replaced in tests, to reach what a real process rarely does. */\n\trunProcess?: typeof runCommandProcess;\n}): Promise<PreparingTool> {\n\tif (platform === 'win32') {\n\t\tthrow new InvalidAgentConfigError(\n\t\t\t'Configured commands are not supported on Windows yet: the harness cannot stop a command and everything it started there',\n\t\t);\n\t}\n\tvalidateCommands(commands);\n\tconst resolved = new Map<string, ResolvedCommand>();\n\tfor (const [name, definition] of Object.entries(commands)) {\n\t\tresolved.set(name, {\n\t\t\tdefinition,\n\t\t\tcwd: await resolveCwd(`Command \"${name}\"`, definition.cwd, policy, base),\n\t\t});\n\t}\n\n\treturn definePreparingTool({\n\t\tname: 'runCommand',\n\t\tdescription: describeTool(commands),\n\t\tinput: inputSchema(commands),\n\t\tsummarize: ({ command: { name, ...values } }) => {\n\t\t\tconst definition = commands[name];\n\t\t\t// The schema only permits configured names, but keep this defensive for callers that\n\t\t\t// use a tool outside LLMAgent. This description never uses the resolved cwd.\n\t\t\treturn definition ? visibleCommand(definition, values) : name;\n\t\t},\n\t\tprepare: async ({ command: { name, ...values } }, signal, { run }) => {\n\t\t\tconst command = resolved.get(name);\n\t\t\t// The schema allows only the configured names.\n\t\t\tif (!command) throw new Error(`There is no command named \"${name}\"`);\n\t\t\tconst paramValues: Record<string, ParameterValue> = {};\n\t\t\tfor (const parameter of parametersOf(command.definition)) {\n\t\t\t\tconst value =\n\t\t\t\t\t(values as Record<string, ParameterValue | undefined>)[parameter.name] ?? null;\n\t\t\t\tparamValues[parameter.name] =\n\t\t\t\t\tparameter.type === 'paths' && Array.isArray(value)\n\t\t\t\t\t\t? await resolvePaths(value, command.cwd, policy, signal)\n\t\t\t\t\t\t: value;\n\t\t\t}\n\t\t\tconst line: CommandLine = Object.freeze({\n\t\t\t\texecutable: command.definition.executable,\n\t\t\t\targs: Object.freeze(buildArguments(command.definition, paramValues)) as string[],\n\t\t\t\tcwd: command.cwd,\n\t\t\t});\n\n\t\t\tlet ending: string | undefined;\n\t\t\treturn {\n\t\t\t\t// A command can change anything its user can: it is always asked about.\n\t\t\t\trisk: 'destructive',\n\t\t\t\tcommand: line,\n\t\t\t\tending: () => ending,\n\t\t\t\trun: runSignal =>\n\t\t\t\t\trunCommand({\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tline,\n\t\t\t\t\t\tdefinition: command.definition,\n\t\t\t\t\t\tcontext: run,\n\t\t\t\t\t\tsignal: runSignal,\n\t\t\t\t\t\tended: text => {\n\t\t\t\t\t\t\tending = text;\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t};\n\t\t},\n\t});\n\n\tasync function runCommand({\n\t\tname,\n\t\tline,\n\t\tdefinition,\n\t\tcontext,\n\t\tsignal,\n\t\tended,\n\t}: {\n\t\tname: string;\n\t\tline: CommandLine;\n\t\tdefinition: CommandDefinition;\n\t\tcontext: RunContext;\n\t\tsignal: AbortSignal;\n\t\t/** Told how the command ended, once it ran to an end and everything it changed is recorded. */\n\t\tended: (ending: string) => void;\n\t}): Promise<string> {\n\t\tsignal.throwIfAborted();\n\t\tconst session = writes.sessionFor(context);\n\t\tconst journal = await session.journalForChange();\n\t\tlet checkpoint: CommandCheckpoint;\n\t\ttry {\n\t\t\tcheckpoint = await CommandCheckpoint.begin({ journal, covered, by: name, signal });\n\t\t} catch (error) {\n\t\t\tsignal.throwIfAborted();\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`The harness could not capture the files before \"${name}\", so it did not run`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\n\t\tlet result: CommandProcessResult | undefined;\n\t\tlet startFailure: unknown;\n\t\ttry {\n\t\t\tresult = await runProcess(line.executable, line.args, {\n\t\t\t\tcwd: line.cwd,\n\t\t\t\tenv: environmentFor(definition),\n\t\t\t\ttimeoutMs: definition.timeoutMs ?? defaultCommandTimeoutMs,\n\t\t\t\toutputBytes: definition.outputBytes ?? defaultCommandOutputBytes,\n\t\t\t\tsignal,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tstartFailure = error;\n\t\t}\n\n\t\tconst complete = result?.stopped ?? true;\n\t\tlet observed: Awaited<ReturnType<CommandCheckpoint['end']>>;\n\t\ttry {\n\t\t\tobserved = await checkpoint.end({ complete });\n\t\t} catch (error) {\n\t\t\tsession.stop('Writing stopped: the harness could not record what a command changed');\n\t\t\tthrow new EditUnconfirmedError(\n\t\t\t\t`\"${name}\" ran, but the harness could not record what it changed; writing has stopped for this run`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\n\t\tif (!result) {\n\t\t\tsignal.throwIfAborted();\n\t\t\tthrow new EditRefusedError(`\"${name}\" could not be started`, { cause: startFailure });\n\t\t}\n\t\tconst report = describeRun(name, definition, result, observed);\n\t\tif (!complete) {\n\t\t\tsession.stop('Writing stopped: a command could not be stopped');\n\t\t\tthrow new EditUnconfirmedError(\n\t\t\t\t`${report}\\n\"${name}\" could not be stopped: something it started may still be changing files, and the history may not hold all of it. Writing has stopped for this run.`,\n\t\t\t);\n\t\t}\n\t\t// After its changes are recorded: a cancelled command is never reported as finished.\n\t\tif (result.outcome === 'cancelled') signal.throwIfAborted();\n\t\t// Everything but the duration, which differs between two runs of the same check.\n\t\tended(\n\t\t\tJSON.stringify([\n\t\t\t\tresult.outcome,\n\t\t\t\tresult.exitCode,\n\t\t\t\tresult.signal,\n\t\t\t\tresult.printedBytes,\n\t\t\t\tresult.output,\n\t\t\t]),\n\t\t);\n\t\tif (result.outcome !== 'exited') throw new ToolExecutionError(report);\n\t\tif (result.exitCode !== 0) {\n\t\t\tthrow new ToolExecutionError(report, {\n\t\t\t\t...(result.exitCode === null ? {} : { exitCode: result.exitCode }),\n\t\t\t});\n\t\t}\n\t\treturn report;\n\t}\n\n\tfunction describeRun(\n\t\tname: string,\n\t\tdefinition: CommandDefinition,\n\t\tresult: CommandProcessResult,\n\t\t{ changes, unrecorded }: Awaited<ReturnType<CommandCheckpoint['end']>>,\n\t): string {\n\t\tconst seconds = (result.durationMs / 1000).toFixed(1);\n\t\tconst lines: string[] = [];\n\t\tif (result.outcome === 'timedOut') {\n\t\t\tconst limit = (definition.timeoutMs ?? defaultCommandTimeoutMs) / 1000;\n\t\t\tlines.push(`\"${name}\" ran past its limit of ${String(limit)} s and was stopped.`);\n\t\t} else if (result.outcome === 'cancelled') {\n\t\t\tlines.push(`\"${name}\" was stopped: the run was cancelled.`);\n\t\t} else if (result.exitCode !== null) {\n\t\t\tlines.push(`\"${name}\" exited with code ${String(result.exitCode)} after ${seconds} s.`);\n\t\t} else {\n\t\t\tlines.push(`\"${name}\" was killed by ${result.signal ?? 'a signal'} after ${seconds} s.`);\n\t\t}\n\n\t\tconst shown = (path: string) => displayPath(base, path);\n\t\tlines.push(\n\t\t\tchanges.length === 0\n\t\t\t\t? 'It changed no file the history covers.'\n\t\t\t\t: `Files it changed, recorded in the history: ${changes.map(change => shown(change.path)).join(', ')}.`,\n\t\t);\n\t\tif (unrecorded.length > 0) {\n\t\t\tlines.push(\n\t\t\t\t`Not recorded, because the history cannot copy them (a folder, a link, or a file over 64 MB): ${unrecorded.map(shown).join(', ')}.`,\n\t\t\t);\n\t\t}\n\n\t\tconst limit = definition.outputBytes ?? defaultCommandOutputBytes;\n\t\tif (result.printedBytes === 0) {\n\t\t\tlines.push('It printed nothing.');\n\t\t} else {\n\t\t\tlines.push(\n\t\t\t\tresult.printedBytes > limit\n\t\t\t\t\t? `Output, the last ${kilobytes(limit)} of ${kilobytes(result.printedBytes)}:`\n\t\t\t\t\t: 'Output:',\n\t\t\t\tresult.output,\n\t\t\t);\n\t\t}\n\t\treturn lines.join('\\n');\n\t}\n}\n\nfunction kilobytes(bytes: number): string {\n\treturn `${String(Math.ceil(bytes / 1024))} KB`;\n}\n\n/** The command line the model can see, before paths are resolved against the host workspace. */\nfunction visibleCommand(\n\tdefinition: CommandDefinition,\n\tvalues: Record<string, ParameterValue | undefined>,\n): string {\n\tconst args: string[] = [];\n\tfor (const argument of definition.args ?? []) {\n\t\tif (typeof argument === 'string') {\n\t\t\targs.push(argument);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst value = values[argument.name] ?? null;\n\t\tif (argument.type === 'paths') {\n\t\t\tif (Array.isArray(value)) args.push(...value);\n\t\t} else if (value !== null && typeof value === 'string') {\n\t\t\targs.push(value);\n\t\t}\n\t}\n\n\treturn [definition.executable, ...args].join(' ');\n}\n\n/**\n * Where a configured program runs, as a real path inside a `write` root. `program` names it in\n * the error, as `Command \"test\"`.\n */\nexport async function resolveCwd(\n\tprogram: string,\n\tcwd: string | undefined,\n\tpolicy: AccessPolicy,\n\tbase: string,\n): Promise<string> {\n\tconst named = resolve(base, cwd ?? '.');\n\tlet real: string;\n\ttry {\n\t\treal = await realpath(named);\n\t\tawait policy.check(real, 'write', AbortSignal.timeout(cwdCheckMs));\n\t} catch (error) {\n\t\tthrow new InvalidAgentConfigError(\n\t\t\t`${program} needs a cwd inside a write root of the workspace`,\n\t\t\t{ cause: error },\n\t\t);\n\t}\n\treturn real;\n}\n\n/**\n * Each path, once the policy lets the agent read it, relative to where the command runs. One\n * that would read as a flag is given as `./` and its name.\n */\nasync function resolvePaths(\n\tpaths: string[],\n\tcwd: string,\n\tpolicy: AccessPolicy,\n\tsignal: AbortSignal,\n): Promise<string[]> {\n\tconst resolved: string[] = [];\n\tfor (const path of paths) {\n\t\tconst { absolute } = await policy.check(path, 'read', signal);\n\t\tconst fromCwd = relative(cwd, absolute) || '.';\n\t\tresolved.push(fromCwd.startsWith('-') && !isAbsolute(fromCwd) ? `./${fromCwd}` : fromCwd);\n\t}\n\treturn resolved;\n}\n\n/** A small environment of its own: the inherited few, then what the consumer set. */\nexport function environmentFor(\n\tdefinition: Pick<CommandDefinition, 'env'>,\n\tenv: NodeJS.ProcessEnv = process.env,\n): NodeJS.ProcessEnv {\n\tconst inherited: NodeJS.ProcessEnv = {};\n\tfor (const [variable, value] of Object.entries(env)) {\n\t\tif (value === undefined) continue;\n\t\tif (inheritedVariables.includes(variable) || variable.startsWith('LC_')) {\n\t\t\tinherited[variable] = value;\n\t\t}\n\t}\n\treturn { ...inherited, ...definition.env };\n}\n\nfunction describeTool(commands: Commands): string {\n\tconst list = Object.entries(commands)\n\t\t.map(([name, { description }]) => `- ${name}: ${description}`)\n\t\t.join('\\n');\n\treturn [\n\t\t\"Runs one of the project's configured commands, by name, such as its tests or its formatter.\",\n\t\t'Files it changes are recorded in the history, so they can be undone. Its output is cut to its end.',\n\t\t'Commands:',\n\t\tlist,\n\t].join('\\n');\n}\n\n/**\n * One object per command, told apart by `name`, each with the parameters that command takes.\n * Strict mode needs every key present, so a parameter that may be left out takes `null`.\n */\nfunction inputSchema(commands: Commands) {\n\tconst variants = Object.entries(commands).map(([name, definition]) =>\n\t\tz.object({\n\t\t\tname: z.enum([name]),\n\t\t\t...Object.fromEntries(\n\t\t\t\tparametersOf(definition).map(parameter => [parameter.name, parameterSchema(parameter)]),\n\t\t\t),\n\t\t}),\n\t);\n\tconst [first, ...rest] = variants;\n\t// `validateCommands` refuses an empty configuration.\n\tif (!first) throw new InvalidAgentConfigError('No command is configured');\n\treturn z.object({\n\t\tcommand: (rest.length === 0 ? first : z.union([first, ...rest])).describe(\n\t\t\t'The command to run, and its parameters.',\n\t\t),\n\t});\n}\n\nfunction parameterSchema(parameter: CommandParameter): z.ZodType {\n\tswitch (parameter.type) {\n\t\tcase 'paths':\n\t\t\treturn z\n\t\t\t\t.array(z.string())\n\t\t\t\t.describe(`${parameter.description} Paths inside the workspace; [] for none.`);\n\t\tcase 'choice': {\n\t\t\tconst values = z.enum(parameter.values as [string, ...string[]]);\n\t\t\treturn parameter.required === true\n\t\t\t\t? values.describe(parameter.description)\n\t\t\t\t: values.nullable().describe(`${parameter.description} null to leave it out.`);\n\t\t}\n\t\tcase 'text': {\n\t\t\tconst text = z.string();\n\t\t\tconst description = `${parameter.description} Must match the regular expression ${parameter.pattern}, and not start with \"-\".`;\n\t\t\treturn parameter.required === true\n\t\t\t\t? text.describe(description)\n\t\t\t\t: text.nullable().describe(`${description} null to leave it out.`);\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"commandTool.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/commandTool.ts"],"names":[],"mappings":"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","sourcesContent":["import { realpath } from 'node:fs/promises';\nimport { isAbsolute, relative, resolve } from 'node:path';\nimport { z } from 'zod';\nimport type { CommandLine } from '#src/agent/domain/approval';\nimport type { RunContext } from '#src/agent/domain/runContext';\nimport { displayPath } from '#src/recovery/domain/runChanges';\nimport { InvalidAgentConfigError } from '#src/shared/domain/errors';\nimport type { AccessPolicy } from '../domain/accessPolicy.ts';\nimport {\n\tbuildArguments,\n\ttype CommandDefinition,\n\ttype CommandParameter,\n\ttype Commands,\n\tdefaultCommandOutputBytes,\n\tdefaultCommandTimeoutMs,\n\ttype ParameterValue,\n\tparametersOf,\n\tvalidateCommands,\n} from '../domain/commands.ts';\nimport { EditRefusedError, EditUnconfirmedError } from '../domain/fileEdits.ts';\nimport {\n\ttype CommandEnding,\n\tToolExecutionError,\n\ttype PreparingTool,\n} from '../domain/preparedCall.ts';\nimport { CommandCheckpoint, type CoveredFiles } from './commandCheckpoint.ts';\nimport { OutputFingerprint, withoutColors } from './commandOutput.ts';\nimport { type CommandProcessResult, runCommandProcess } from './commandProcess.ts';\nimport { definePreparingTool } from './defineTool.ts';\nimport type { WorkspaceWrites } from './workspaceWrites.ts';\n\n// Only what a toolchain needs to find itself and its locale. Everything else, the provider\n// keys included, stays in this process.\nconst inheritedVariables = ['PATH', 'HOME', 'TMPDIR', 'USER', 'SHELL', 'LANG'];\n// Long enough for a slow disk, short enough not to hold up building an agent.\nconst cwdCheckMs = 10_000;\n\n/** A command ready to run: its definition, and where it runs as a real path. */\ninterface ResolvedCommand {\n\tdefinition: CommandDefinition;\n\tcwd: string;\n}\n\n/**\n * `runCommand`, which runs one of the commands the consumer configured, by name, with the\n * parameters each one declares. Every call asks the approver, showing the program exactly as\n * it will run; `autoApprove` skips that.\n *\n * Around each command, the covered files are captured before and compared after, so what it\n * changed is in the run's history and can be undone: see `CommandCheckpoint`. If the capture\n * fails, the command does not run. A timeout or a cancellation stops its whole process group,\n * and is never reported as a completed command. If the group cannot be confirmed stopped, what\n * changed so far is recorded, the observation stays open, and writing stops for the run.\n *\n * This is not a sandbox: a command runs the repository's code, with everything this user can\n * reach. See \"Configured commands are trusted, not sandboxed\" in decisions.md.\n *\n * @throws {InvalidAgentConfigError} for an invalid configuration, a working directory outside\n * every `write` root, or on Windows, where a process group cannot be stopped.\n */\nexport async function createCommandTool({\n\tcommands,\n\tpolicy,\n\twrites,\n\tcovered,\n\tbase,\n\tplatform = process.platform,\n\trunProcess = runCommandProcess,\n}: {\n\tcommands: Commands;\n\tpolicy: AccessPolicy;\n\twrites: WorkspaceWrites;\n\tcovered: CoveredFiles;\n\t/** The real path of the first root, where a relative `cwd` starts. */\n\tbase: string;\n\tplatform?: NodeJS.Platform;\n\t/** Runs the program. Replaced in tests, to reach what a real process rarely does. */\n\trunProcess?: typeof runCommandProcess;\n}): Promise<PreparingTool> {\n\tif (platform === 'win32') {\n\t\tthrow new InvalidAgentConfigError(\n\t\t\t'Configured commands are not supported on Windows yet: the harness cannot stop a command and everything it started there',\n\t\t);\n\t}\n\tvalidateCommands(commands);\n\tconst resolved = new Map<string, ResolvedCommand>();\n\tfor (const [name, definition] of Object.entries(commands)) {\n\t\tresolved.set(name, {\n\t\t\tdefinition,\n\t\t\tcwd: await resolveCwd(`Command \"${name}\"`, definition.cwd, policy, base),\n\t\t});\n\t}\n\n\treturn definePreparingTool({\n\t\tname: 'runCommand',\n\t\tdescription: describeTool(commands),\n\t\tinput: inputSchema(commands),\n\t\tsummarize: ({ command: { name, ...values } }) => {\n\t\t\tconst definition = commands[name];\n\t\t\t// The schema only permits configured names, but keep this defensive for callers that\n\t\t\t// use a tool outside LLMAgent. This description never uses the resolved cwd.\n\t\t\treturn definition ? visibleCommand(definition, values) : name;\n\t\t},\n\t\tprepare: async ({ command: { name, ...values } }, signal, { run }) => {\n\t\t\tconst command = resolved.get(name);\n\t\t\t// The schema allows only the configured names.\n\t\t\tif (!command) throw new Error(`There is no command named \"${name}\"`);\n\t\t\tconst paramValues: Record<string, ParameterValue> = {};\n\t\t\tfor (const parameter of parametersOf(command.definition)) {\n\t\t\t\tconst value =\n\t\t\t\t\t(values as Record<string, ParameterValue | undefined>)[parameter.name] ?? null;\n\t\t\t\tparamValues[parameter.name] =\n\t\t\t\t\tparameter.type === 'paths' && Array.isArray(value)\n\t\t\t\t\t\t? await resolvePaths(value, command.cwd, policy, signal)\n\t\t\t\t\t\t: value;\n\t\t\t}\n\t\t\tconst line: CommandLine = Object.freeze({\n\t\t\t\texecutable: command.definition.executable,\n\t\t\t\targs: Object.freeze(buildArguments(command.definition, paramValues)) as string[],\n\t\t\t\tcwd: command.cwd,\n\t\t\t});\n\n\t\t\tlet ending: CommandEnding | undefined;\n\t\t\treturn {\n\t\t\t\t// A command can change anything its user can: it is always asked about.\n\t\t\t\trisk: 'destructive',\n\t\t\t\tcommand: line,\n\t\t\t\tending: () => ending,\n\t\t\t\trun: runSignal =>\n\t\t\t\t\trunCommand({\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tline,\n\t\t\t\t\t\tdefinition: command.definition,\n\t\t\t\t\t\tcontext: run,\n\t\t\t\t\t\tsignal: runSignal,\n\t\t\t\t\t\tended: end => {\n\t\t\t\t\t\t\tending = end;\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t};\n\t\t},\n\t});\n\n\tasync function runCommand({\n\t\tname,\n\t\tline,\n\t\tdefinition,\n\t\tcontext,\n\t\tsignal,\n\t\tended,\n\t}: {\n\t\tname: string;\n\t\tline: CommandLine;\n\t\tdefinition: CommandDefinition;\n\t\tcontext: RunContext;\n\t\tsignal: AbortSignal;\n\t\t/** Told how the command ended, once it ran to an end and everything it changed is recorded. */\n\t\tended: (ending: CommandEnding) => void;\n\t}): Promise<string> {\n\t\tsignal.throwIfAborted();\n\t\tconst session = writes.sessionFor(context);\n\t\tconst journal = await session.journalForChange();\n\t\tlet checkpoint: CommandCheckpoint;\n\t\ttry {\n\t\t\tcheckpoint = await CommandCheckpoint.begin({ journal, covered, by: name, signal });\n\t\t} catch (error) {\n\t\t\tsignal.throwIfAborted();\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`The harness could not capture the files before \"${name}\", so it did not run`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\n\t\tlet result: CommandProcessResult | undefined;\n\t\tlet startFailure: unknown;\n\t\tconst fingerprint = new OutputFingerprint();\n\t\ttry {\n\t\t\tresult = await runProcess(line.executable, line.args, {\n\t\t\t\tcwd: line.cwd,\n\t\t\t\tenv: environmentFor(definition),\n\t\t\t\ttimeoutMs: definition.timeoutMs ?? defaultCommandTimeoutMs,\n\t\t\t\toutputBytes: definition.outputBytes ?? defaultCommandOutputBytes,\n\t\t\t\tsignal,\n\t\t\t\tonOutput: (stream, chunk) => {\n\t\t\t\t\tfingerprint.push(stream, chunk);\n\t\t\t\t},\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tstartFailure = error;\n\t\t}\n\n\t\tconst complete = result?.stopped ?? true;\n\t\tlet observed: Awaited<ReturnType<CommandCheckpoint['end']>>;\n\t\ttry {\n\t\t\tobserved = await checkpoint.end({ complete });\n\t\t} catch (error) {\n\t\t\tsession.stop('Writing stopped: the harness could not record what a command changed');\n\t\t\tthrow new EditUnconfirmedError(\n\t\t\t\t`\"${name}\" ran, but the harness could not record what it changed; writing has stopped for this run`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\n\t\tif (!result) {\n\t\t\tsignal.throwIfAborted();\n\t\t\tthrow new EditRefusedError(`\"${name}\" could not be started`, { cause: startFailure });\n\t\t}\n\t\tconst report = describeRun(name, definition, result, observed);\n\t\tif (!complete) {\n\t\t\tsession.stop('Writing stopped: a command could not be stopped');\n\t\t\tthrow new EditUnconfirmedError(\n\t\t\t\t`${report}\\n\"${name}\" could not be stopped: something it started may still be changing files, and the history may not hold all of it. Writing has stopped for this run.`,\n\t\t\t);\n\t\t}\n\t\t// After its changes are recorded: a cancelled command is never reported as finished.\n\t\tif (result.outcome === 'cancelled') signal.throwIfAborted();\n\t\tended({\n\t\t\tpassed: result.outcome === 'exited' && result.exitCode === 0,\n\t\t\thow: howItEnded(result),\n\t\t\toutput: withoutColors(result.output),\n\t\t\tfingerprint: fingerprint.digest(),\n\t\t\t...(observed.unrecorded.length === 0 ? { state: journal.contentState() } : {}),\n\t\t\tchanged: journal.appliedPaths.map(path => displayPath(base, path)),\n\t\t});\n\t\tif (result.outcome !== 'exited') throw new ToolExecutionError(report);\n\t\tif (result.exitCode !== 0) {\n\t\t\tthrow new ToolExecutionError(report, {\n\t\t\t\t...(result.exitCode === null ? {} : { exitCode: result.exitCode }),\n\t\t\t});\n\t\t}\n\t\treturn report;\n\t}\n\n\tfunction describeRun(\n\t\tname: string,\n\t\tdefinition: CommandDefinition,\n\t\tresult: CommandProcessResult,\n\t\t{ changes, unrecorded }: Awaited<ReturnType<CommandCheckpoint['end']>>,\n\t): string {\n\t\tconst seconds = (result.durationMs / 1000).toFixed(1);\n\t\tconst lines: string[] = [];\n\t\tif (result.outcome === 'timedOut') {\n\t\t\tconst limit = (definition.timeoutMs ?? defaultCommandTimeoutMs) / 1000;\n\t\t\tlines.push(`\"${name}\" ran past its limit of ${String(limit)} s and was stopped.`);\n\t\t} else if (result.outcome === 'cancelled') {\n\t\t\tlines.push(`\"${name}\" was stopped: the run was cancelled.`);\n\t\t} else if (result.exitCode !== null) {\n\t\t\tlines.push(`\"${name}\" exited with code ${String(result.exitCode)} after ${seconds} s.`);\n\t\t} else {\n\t\t\tlines.push(`\"${name}\" was killed by ${result.signal ?? 'a signal'} after ${seconds} s.`);\n\t\t}\n\n\t\tconst shown = (path: string) => displayPath(base, path);\n\t\tlines.push(\n\t\t\tchanges.length === 0\n\t\t\t\t? 'It changed no file the history covers.'\n\t\t\t\t: `Files it changed, recorded in the history: ${changes.map(change => shown(change.path)).join(', ')}.`,\n\t\t);\n\t\tif (unrecorded.length > 0) {\n\t\t\tlines.push(\n\t\t\t\t`Not recorded, because the history cannot copy them (a folder, a link, or a file over 64 MB): ${unrecorded.map(shown).join(', ')}.`,\n\t\t\t);\n\t\t}\n\n\t\tconst limit = definition.outputBytes ?? defaultCommandOutputBytes;\n\t\tif (result.printedBytes === 0) {\n\t\t\tlines.push('It printed nothing.');\n\t\t} else {\n\t\t\tlines.push(\n\t\t\t\tresult.printedBytes > limit\n\t\t\t\t\t? `Output, the last ${kilobytes(limit)} of ${kilobytes(result.printedBytes)}:`\n\t\t\t\t\t: 'Output:',\n\t\t\t\tresult.output,\n\t\t\t);\n\t\t}\n\t\treturn lines.join('\\n');\n\t}\n}\n\nfunction howItEnded(result: CommandProcessResult): string {\n\tif (result.outcome === 'timedOut') return 'ran past its time limit';\n\tif (result.outcome === 'cancelled') return 'was cancelled';\n\tif (result.exitCode !== null) return `exited with code ${String(result.exitCode)}`;\n\treturn `was killed by ${result.signal ?? 'a signal'}`;\n}\n\nfunction kilobytes(bytes: number): string {\n\treturn `${String(Math.ceil(bytes / 1024))} KB`;\n}\n\n/** The command line the model can see, before paths are resolved against the host workspace. */\nfunction visibleCommand(\n\tdefinition: CommandDefinition,\n\tvalues: Record<string, ParameterValue | undefined>,\n): string {\n\tconst args: string[] = [];\n\tfor (const argument of definition.args ?? []) {\n\t\tif (typeof argument === 'string') {\n\t\t\targs.push(argument);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst value = values[argument.name] ?? null;\n\t\tif (argument.type === 'paths') {\n\t\t\tif (Array.isArray(value)) args.push(...value);\n\t\t} else if (value !== null && typeof value === 'string') {\n\t\t\targs.push(value);\n\t\t}\n\t}\n\n\treturn [definition.executable, ...args].join(' ');\n}\n\n/**\n * Where a configured program runs, as a real path inside a `write` root. `program` names it in\n * the error, as `Command \"test\"`.\n */\nexport async function resolveCwd(\n\tprogram: string,\n\tcwd: string | undefined,\n\tpolicy: AccessPolicy,\n\tbase: string,\n): Promise<string> {\n\tconst named = resolve(base, cwd ?? '.');\n\tlet real: string;\n\ttry {\n\t\treal = await realpath(named);\n\t\tawait policy.check(real, 'write', AbortSignal.timeout(cwdCheckMs));\n\t} catch (error) {\n\t\tthrow new InvalidAgentConfigError(\n\t\t\t`${program} needs a cwd inside a write root of the workspace`,\n\t\t\t{ cause: error },\n\t\t);\n\t}\n\treturn real;\n}\n\n/**\n * Each path, once the policy lets the agent read it, relative to where the command runs. One\n * that would read as a flag is given as `./` and its name.\n */\nasync function resolvePaths(\n\tpaths: string[],\n\tcwd: string,\n\tpolicy: AccessPolicy,\n\tsignal: AbortSignal,\n): Promise<string[]> {\n\tconst resolved: string[] = [];\n\tfor (const path of paths) {\n\t\tconst { absolute } = await policy.check(path, 'read', signal);\n\t\tconst fromCwd = relative(cwd, absolute) || '.';\n\t\tresolved.push(fromCwd.startsWith('-') && !isAbsolute(fromCwd) ? `./${fromCwd}` : fromCwd);\n\t}\n\treturn resolved;\n}\n\n/** A small environment of its own: the inherited few, then what the consumer set. */\nexport function environmentFor(\n\tdefinition: Pick<CommandDefinition, 'env'>,\n\tenv: NodeJS.ProcessEnv = process.env,\n): NodeJS.ProcessEnv {\n\tconst inherited: NodeJS.ProcessEnv = {};\n\tfor (const [variable, value] of Object.entries(env)) {\n\t\tif (value === undefined) continue;\n\t\tif (inheritedVariables.includes(variable) || variable.startsWith('LC_')) {\n\t\t\tinherited[variable] = value;\n\t\t}\n\t}\n\treturn { ...inherited, ...definition.env };\n}\n\nfunction describeTool(commands: Commands): string {\n\tconst list = Object.entries(commands)\n\t\t.map(([name, { description }]) => `- ${name}: ${description}`)\n\t\t.join('\\n');\n\treturn [\n\t\t\"Runs one of the project's configured commands, by name, such as its tests or its formatter.\",\n\t\t'Files it changes are recorded in the history, so they can be undone. Its output is cut to its end.',\n\t\t'Commands:',\n\t\tlist,\n\t].join('\\n');\n}\n\n/**\n * One object per command, told apart by `name`, each with the parameters that command takes.\n * Strict mode needs every key present, so a parameter that may be left out takes `null`.\n */\nfunction inputSchema(commands: Commands) {\n\tconst variants = Object.entries(commands).map(([name, definition]) =>\n\t\tz.object({\n\t\t\tname: z.enum([name]),\n\t\t\t...Object.fromEntries(\n\t\t\t\tparametersOf(definition).map(parameter => [parameter.name, parameterSchema(parameter)]),\n\t\t\t),\n\t\t}),\n\t);\n\tconst [first, ...rest] = variants;\n\t// `validateCommands` refuses an empty configuration.\n\tif (!first) throw new InvalidAgentConfigError('No command is configured');\n\treturn z.object({\n\t\tcommand: (rest.length === 0 ? first : z.union([first, ...rest])).describe(\n\t\t\t'The command to run, and its parameters.',\n\t\t),\n\t});\n}\n\nfunction parameterSchema(parameter: CommandParameter): z.ZodType {\n\tswitch (parameter.type) {\n\t\tcase 'paths':\n\t\t\treturn z\n\t\t\t\t.array(z.string())\n\t\t\t\t.describe(`${parameter.description} Paths inside the workspace; [] for none.`);\n\t\tcase 'choice': {\n\t\t\tconst values = z.enum(parameter.values as [string, ...string[]]);\n\t\t\treturn parameter.required === true\n\t\t\t\t? values.describe(parameter.description)\n\t\t\t\t: values.nullable().describe(`${parameter.description} null to leave it out.`);\n\t\t}\n\t\tcase 'text': {\n\t\t\tconst text = z.string();\n\t\t\tconst description = `${parameter.description} Must match the regular expression ${parameter.pattern}, and not start with \"-\".`;\n\t\t\treturn parameter.required === true\n\t\t\t\t? text.describe(description)\n\t\t\t\t: text.nullable().describe(`${description} null to leave it out.`);\n\t\t}\n\t}\n}\n"]}
|