fix(harness): r127 review round, only a shell's own not-found line names a program (#1873, lane r127)
Review P3-1: a "No such file or directory" line from another program
(cat, ls) no longer counts. Only bash's "line N:", dash's "N:" and env's
quoted form do.
P3-2: a name with a run of 16 letters and digits looks like a key, so it
stays in the run's private stderr instead of the reply.
P3-3: a 16 KiB stderr window that starts mid-file drops its partial first
line, so a cut name ("aude") isn't reported.
P3-4: CRLF lines read the same as LF lines.
P3-5: tests for the name check (tokens without spaces, key-shaped names),
for a cat line, and for the window boundary through the runner.
This commit was merged in pull request #1964.
This commit is contained in:
1 parent
61371adb10
commit
387f73a7a4
2 files changed
+43
-10
No files matched your search
@@ -386,30 +386,39 @@ export function toolServer({ path, tools, client, local }) {
|
||||
|
||||
// #1873 r127: exit 127 is the shell's "command not found". The turn's stderr then holds the
|
||||
// shell's own line for it, the last one nearest the exit:
|
||||
// bash X: line N: CMD: No such file or directory | X: line N: [exec: ]CMD: [command ]not found
|
||||
// bash X: line N: [exec: ]CMD: No such file or directory | [command ]not found
|
||||
// dash X: N: [exec: ]CMD: not found
|
||||
// env /usr/bin/env: 'CMD': No such file or directory
|
||||
// env [/usr/bin/]env: 'CMD': No such file or directory
|
||||
// Only those shapes count, so another program's missing file (cat: F: No such file…) doesn't.
|
||||
// The answer is CMD's basename, which the person reads in the failure; never its host path, and
|
||||
// nothing that isn't a plain program name.
|
||||
const NOT_FOUND = /(?:^|: )(?:exec: )?['\u2018]?([^\s:'\u2019]+)['\u2019]?: (?:(?:command )?not found|No such file or directory)$/;
|
||||
// nothing that isn't a plain program name. A run of 16 letters and digits looks like a key, not a
|
||||
// program, so it stays in the run's private stderr.
|
||||
const NOT_FOUND = [
|
||||
/(?:^|: )line \d+: (?:exec: )?([^\s:]+): (?:(?:command )?not found|No such file or directory)$/,
|
||||
/: \d+: (?:exec: )?([^\s:]+): not found$/,
|
||||
/^(?:\S*\/)?env: ['\u2018]([^\s:'\u2019]+)['\u2019]: No such file or directory$/,
|
||||
];
|
||||
export function missingCommand(text) {
|
||||
let name = null;
|
||||
for (const line of String(text).split("\n")) {
|
||||
const m = NOT_FOUND.exec(line);
|
||||
for (const line of String(text).split(/\r?\n/)) {
|
||||
const m = NOT_FOUND.map((re) => re.exec(line)).find(Boolean);
|
||||
if (m) name = basename(m[1]);
|
||||
}
|
||||
return name && /^[A-Za-z0-9._+-]{1,64}$/.test(name) ? name : null;
|
||||
return name && /^[A-Za-z0-9._+-]{1,64}$/.test(name) && !/[A-Za-z0-9]{16}/.test(name) ? name : null;
|
||||
}
|
||||
|
||||
// The last 16 KiB of a turn's stderr file, or "" when it can't be read.
|
||||
// The last 16 KiB of a turn's stderr file from a line start, or "" when it can't be read.
|
||||
const TAIL = 16384;
|
||||
function stderrTail(path) {
|
||||
let fd;
|
||||
try {
|
||||
fd = openSync(path, "r");
|
||||
const size = fstatSync(fd).size, length = Math.min(size, 16384);
|
||||
const size = fstatSync(fd).size, length = Math.min(size, TAIL);
|
||||
const buf = Buffer.alloc(length);
|
||||
readSync(fd, buf, 0, length, size - length);
|
||||
return buf.toString("utf8");
|
||||
const text = buf.toString("utf8");
|
||||
// A window that starts mid-file starts mid-line; that partial line is dropped.
|
||||
return size > TAIL ? text.slice(text.indexOf("\n") + 1) : text;
|
||||
} catch {
|
||||
return "";
|
||||
} finally {
|
||||
|
||||
@@ -354,6 +354,30 @@ test("r127 missingCommand reads the shells' not-found lines and gives a safe bas
|
||||
// A name that isn't a plain program name is left out, never echoed.
|
||||
assert.equal(missingCommand("a: 1: exec: $(rm -rf x): not found\n"), null);
|
||||
assert.equal(missingCommand(`a: 1: exec: ${"x".repeat(80)}: not found\n`), null);
|
||||
// Review r127 P3-5: tokens without spaces reach the name check, and it refuses them.
|
||||
assert.equal(missingCommand("a: 1: exec: $(id): not found\n"), null);
|
||||
assert.equal(missingCommand("a: line 2: x;id: command not found\n"), null);
|
||||
assert.equal(missingCommand("a: line 2: `id`: command not found\n"), null);
|
||||
// P3-2: a long run of letters and digits looks like a key, not a program; it stays in stderr.
|
||||
assert.equal(missingCommand("adapter.sh: line 3: sk-proj-abcDEF1234567890xyz: command not found\n"), null);
|
||||
assert.equal(missingCommand("adapter.sh: line 3: 0123456789abcdef0123: command not found\n"), null);
|
||||
// P3-1: only a shell's or env's own line counts; other programs' missing files don't.
|
||||
assert.equal(missingCommand("cat: /nope/file: No such file or directory\n"), null);
|
||||
assert.equal(missingCommand("w.sh: line 1: /opt/x/claude: No such file or directory\ncat: /run/state.json: No such file or directory\n"), "claude");
|
||||
// P3-4: CRLF lines read the same.
|
||||
assert.equal(missingCommand("sh: line 1: pi: command not found\r\n"), "pi");
|
||||
});
|
||||
|
||||
test("r127 a not-found line cut by the 16 KiB stderr window is not read as a name", async t => {
|
||||
const ctx = await setup(t, { session: { harness: 'pi', turnEnd: 'exit' } });
|
||||
ctx.inbox = [];
|
||||
// 52 + 16351 + 1 bytes: the 16 KiB window starts inside "claude", at "aude: No such file…".
|
||||
writeFileSync(join(ctx.dir, 'adapter.sh'),
|
||||
`printf 'x: line 1: /opt/x/claude: No such file or directory\\n' >&2\nhead -c 16351 /dev/zero | tr '\\0' 'z' >&2\nprintf '\\n' >&2\nexit 127\n`);
|
||||
const r = startRunner(ctx.runDir, ctx.cap);
|
||||
t.after(async () => { r.child.kill('SIGTERM'); await r.exited; });
|
||||
const reply = await ask(ctx);
|
||||
assert.equal(reply.body, "The turn failed: adapter exited 127");
|
||||
});
|
||||
|
||||
// Claude's private stream reader owns model activity; final result stdout alone
|
||||
|
||||
Reference in new issue
Block a user