Merge pull request #2899 from haelyra/fix/plugin-bootstrap-prefix-classification

fix(hooks): classify platform-dependent raw prefixes
This commit is contained in:
haelyra
2026-08-28 23:14:10 -04:00
committed by GitHub
2 changed files with 25 additions and 22 deletions
+7 -9
View File
@@ -7,7 +7,6 @@ const { spawnSync } = require('child_process');
const { ensureAgentDataHomeEnv } = require('../lib/agent-data-home');
const SHELL_PROBE_TIMEOUT_MS = 2000;
const STDOUT_PIPE_CAP_BYTES = 64 * 1024;
function readStdinRaw() {
try {
@@ -37,9 +36,8 @@ function isRawPassthrough(raw, stdout) {
const stdoutBytes = toBuffer(stdout);
if (rawBytes.length === 0 || stdoutBytes.length === 0) return false;
return (
stdoutBytes.equals(rawBytes) ||
(stdoutBytes.length === STDOUT_PIPE_CAP_BYTES &&
rawBytes.subarray(0, stdoutBytes.length).equals(stdoutBytes))
stdoutBytes.length <= rawBytes.length &&
rawBytes.subarray(0, stdoutBytes.length).equals(stdoutBytes)
);
}
@@ -58,11 +56,11 @@ function passthrough(result) {
// instead; the harness falls back to the tool_use's original result, the
// same path #2240 established for bash-hook-dispatcher.
//
// IMPORTANT: a strict `stdout === raw` check misses the common case where
// child processes' synchronous `process.stdout.write()` writes hit the
// ~64 KB Node.js pipe buffer and get truncated -- stdout is then exactly
// 65536 bytes and a strict prefix of raw. So we also detect that
// truncation sentinel.
// IMPORTANT: a strict `stdout === raw` check misses child processes whose
// synchronous `process.stdout.write()` is truncated before exit. Pipe
// capacity varies by platform and Node version (observed at 8, 16, and
// 64 KiB), so classify any non-empty byte-exact prefix of the raw hook
// event as passthrough instead of assuming one buffer size.
const raw = result?.comparisonInput;
const looksLikePassthrough = isRawPassthrough(raw, stdout);
if (looksLikePassthrough) {
@@ -242,7 +242,7 @@ if (
else failed++;
if (
test('64 KiB passthrough sentinel is measured in UTF-8 bytes', () => {
test('truncated UTF-8 prefix is classified by bytes', () => {
const fixturePath = path.join(FIXTURE_DIR, 'multibyte-prefix-fixture.js');
fs.writeFileSync(
fixturePath,
@@ -255,7 +255,7 @@ if (
});
assert.strictEqual(Buffer.byteLength(payload.slice(0, 32768), 'utf8'), 64 * 1024);
assert.strictEqual(result.status, 0, result.stderr);
assert.strictEqual(result.stdout, '', 'a 64 KiB UTF-8 prefix of raw input must be suppressed');
assert.strictEqual(result.stdout, '', 'a UTF-8 byte prefix of raw input must be suppressed');
assert.match(result.stderr, /returned raw input as stdout/);
} finally {
fs.unlinkSync(fixturePath);
@@ -266,18 +266,23 @@ if (
else failed++;
if (
test('byte boundary does not misclassify 64K multibyte characters', () => {
const byteBoundaryPrefix = 'é'.repeat(32768);
const characterBoundaryPrefix = 'é'.repeat(65536);
test('classifies platform-dependent pipe prefixes without accepting mismatches', () => {
const raw = Buffer.from(`${'a'.repeat(64 * 1024)}tail`, 'utf8');
assert.strictEqual(Buffer.byteLength(byteBoundaryPrefix, 'utf8'), 64 * 1024);
assert.strictEqual(Buffer.byteLength(characterBoundaryPrefix, 'utf8'), 128 * 1024);
assert.strictEqual(isRawPassthrough(`${byteBoundaryPrefix}tail`, byteBoundaryPrefix), true);
assert.strictEqual(
isRawPassthrough(`${characterBoundaryPrefix}tail`, characterBoundaryPrefix),
false,
'64K JavaScript characters must not be treated as a 64 KiB byte boundary'
);
for (const prefixLength of [8 * 1024, 16 * 1024, 64 * 1024]) {
assert.strictEqual(
isRawPassthrough(raw, raw.subarray(0, prefixLength)),
true,
`${prefixLength}-byte raw prefix must be classified as passthrough`
);
}
const mismatchLength = 8 * 1024;
const mismatchedPrefix = Buffer.concat([
raw.subarray(0, mismatchLength - 1),
Buffer.from([raw[mismatchLength - 1] ^ 1])
]);
assert.strictEqual(isRawPassthrough(raw, mismatchedPrefix), false);
})
)
passed++;