From b8dcf19949cb4b03f645127601d3356acab6a746 Mon Sep 17 00:00:00 2001 From: Miya Date: Wed, 21 Oct 2026 06:58:38 +0200 Subject: [PATCH 1/2] perf(sdk): probe and hash the authored Node once per CLI process An authored run resolved the Node authority twice on the startup path: assertAuthoredRuntimeAvailable before root admission and runAuthoredInNode before spawning the body. Each call spawned a node probe and read and SHA-256 hashed the whole node executable (~110 MB). Reuse the previous authority for the same FLOWS_AUTHORED_NODE/PATH while a stat fingerprint (dev, ino, size, mtime, ctime) of the resolved executable is unchanged. The child still hashes the executable it runs and the parent still compares that report, so a binary swapped after the probe is refused. Measured with an instrumented darwin-arm64 standalone build: the second authority call drops from 64-76 ms to 0 ms per run. Co-Authored-By: Claude Opus 5.5 --- packages/sdk/src/authored-node-runner.ts | 36 ++++++++++- .../sdk/tests/authored-node-authority.test.ts | 63 +++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 packages/sdk/tests/authored-node-authority.test.ts diff --git a/packages/sdk/src/authored-node-runner.ts b/packages/sdk/src/authored-node-runner.ts index 957f7f6c..02dcc187 100644 --- a/packages/sdk/src/authored-node-runner.ts +++ b/packages/sdk/src/authored-node-runner.ts @@ -3,7 +3,7 @@ import { FLOW_READ_BUDGET_MS } from './journal-read-policy.js'; import { spawn, spawnSync } from 'node:child_process'; import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto'; import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; -import { readFileSync, realpathSync } from 'node:fs'; +import { readFileSync, realpathSync, statSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { isAbsolute, join } from 'node:path'; import type { Readable } from 'node:stream'; @@ -35,10 +35,33 @@ const refusal = (): AuthoredFlowExecutionError => new AuthoredFlowExecutionError 'authored execution requires the embedded Node runner and Node >=22.14 on PATH (or an absolute FLOWS_AUTHORED_NODE); no workflow body was executed', ); interface NodeAuthority { path: string; version: string; executableSha256: string } +/** + * One authored run asks for the Node authority before root admission and + * again before spawning the body. Each probe spawns Node and hashes its whole + * executable (~100 MB), which a cold sandbox pays on the startup path twice. + * Reuse the previous answer only for the same candidate selection and an + * unchanged executable file. The spawned child still hashes the executable it + * actually runs and the parent compares that report with this authority, so + * a binary replaced after the probe is refused, never trusted. + */ +let probedAuthority: { key: string; fingerprint: string; authority: NodeAuthority } | undefined; +function executableFingerprint(path: string): string { + const stat = statSync(path, { bigint: true }); + return [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(':'); +} function nodeAuthority(): NodeAuthority { if (embeddedSource === undefined) throw refusal(); const override = process.env['FLOWS_AUTHORED_NODE']; if (override !== undefined && !isAbsolute(override)) throw refusal(); + const key = `${override ?? ''}\0${process.env['PATH'] ?? ''}`; + if (probedAuthority?.key === key) { + try { + if (executableFingerprint(probedAuthority.authority.path) === probedAuthority.fingerprint) { + return probedAuthority.authority; + } + } catch { /* A vanished executable is re-probed, and refused there. */ } + } + probedAuthority = undefined; const probe = spawnSync(override ?? 'node', ['--input-type=module', '--eval', ` import {createHook} from 'node:async_hooks'; import {stripTypeScriptTypes,register} from 'node:module'; @@ -56,10 +79,19 @@ process.stdout.write(JSON.stringify({path:process.execPath,version:process.versi const [major, minor] = value.version.split('.').map(Number); if (major! < 22 || (major === 22 && minor! < 14)) throw refusal(); const path = realpathSync(value.path); - return { path, version: value.version, executableSha256: hash(readFileSync(path)) }; + const fingerprint = executableFingerprint(path); + const authority = { path, version: value.version, executableSha256: hash(readFileSync(path)) }; + // A file rewritten while it was being hashed is not reused. + if (executableFingerprint(path) === fingerprint) probedAuthority = { key, fingerprint, authority }; + return authority; } catch { throw refusal(); } } +/** Test seam: resolve the authored Node authority exactly as the runner does. */ +export function authoredNodeAuthorityForTest(): Readonly { + return nodeAuthority(); +} + /** Called before root admission/resume; declarative commands never reach it. */ export function assertAuthoredRuntimeAvailable(): void { if (process.versions['bun'] === undefined) assertAuthoredPromiseHooks(); diff --git a/packages/sdk/tests/authored-node-authority.test.ts b/packages/sdk/tests/authored-node-authority.test.ts new file mode 100644 index 00000000..3e88145f --- /dev/null +++ b/packages/sdk/tests/authored-node-authority.test.ts @@ -0,0 +1,63 @@ +import { chmodSync, mkdtempSync, readFileSync, realpathSync, rmSync, utimesSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { authoredNodeAuthorityForTest, installAuthoredNodeSource } from '../src/authored-node-runner.js'; + +// The probe spawns the Node candidate and hashes its whole executable. One CLI +// run used to do both twice on the startup path (before root admission and +// again before spawning the body). The candidate here is a stand-in that +// answers the probe and records each invocation; its own file is the +// executable the authority fingerprints and hashes. +let directory: string, candidate: string, probes: string, previousOverride: string | undefined; + +beforeAll(() => { + directory = mkdtempSync(join(tmpdir(), 'authored-node-authority-')); + probes = join(directory, 'probes'); + writeFileSync(probes, ''); + candidate = join(directory, 'node-candidate'); + writeFileSync(candidate, `#!/bin/sh\necho probe >> ${JSON.stringify(probes)}\nprintf '%s' '${JSON.stringify({ path: join(directory, 'node-candidate'), version: '22.14.0' })}'\n`); + chmodSync(candidate, 0o755); + previousOverride = process.env['FLOWS_AUTHORED_NODE']; + process.env['FLOWS_AUTHORED_NODE'] = candidate; + installAuthoredNodeSource('export {};\n'); +}); + +afterAll(() => { + if (previousOverride === undefined) delete process.env['FLOWS_AUTHORED_NODE']; + else process.env['FLOWS_AUTHORED_NODE'] = previousOverride; + rmSync(directory, { recursive: true, force: true }); +}); + +const probeCount = (): number => readFileSync(probes, 'utf8').split('\n').filter(Boolean).length; + +describe('authored Node authority', () => { + it('probes and hashes the executable once per process while it is unchanged', () => { + const first = authoredNodeAuthorityForTest(); + const second = authoredNodeAuthorityForTest(); + expect(probeCount()).toBe(1); + expect(second).toEqual(first); + expect(first).toMatchObject({ path: realpathSync(candidate), version: '22.14.0' }); + expect(first.executableSha256).toMatch(/^[a-f0-9]{64}$/); + }); + + it('re-probes and re-hashes when the executable file changes', () => { + const before = authoredNodeAuthorityForTest(); + const count = probeCount(); + writeFileSync(candidate, readFileSync(candidate, 'utf8') + '# replaced\n'); + const later = new Date(Date.now() + 60_000); + utimesSync(candidate, later, later); + const after = authoredNodeAuthorityForTest(); + expect(probeCount()).toBe(count + 1); + expect(after.executableSha256).not.toBe(before.executableSha256); + }); + + it('re-probes when the candidate selection changes', () => { + authoredNodeAuthorityForTest(); + const count = probeCount(); + const path = process.env['PATH']; + process.env['PATH'] = `${directory}:${path ?? ''}`; + try { authoredNodeAuthorityForTest(); } finally { process.env['PATH'] = path; } + expect(probeCount()).toBe(count + 1); + }); +}); From baf849feeecda559b4bec0bd3926142e2ead51d1 Mon Sep 17 00:00:00 2001 From: Miya Date: Wed, 21 Oct 2026 08:53:02 +0200 Subject: [PATCH 2/2] fix(sdk): key the Node authority memo on the selected executable Review (#681): the memo keyed on FLOWS_AUTHORED_NODE/PATH and fingerprinted the previously resolved file, so a selection symlink retargeted while the old binary remained kept returning the old authority. Key reuse on the candidate spawnSync would run now (the override, or the first node on an all-absolute PATH) by both its path and its realpath. Any change re-probes; a PATH with a relative entry is never reused. Tests cover an override symlink retarget and a PATH node symlink retarget. Co-Authored-By: Claude Opus 5.5 --- packages/sdk/src/authored-node-runner.ts | 43 ++++++++---- .../sdk/tests/authored-node-authority.test.ts | 66 ++++++++++++++++--- 2 files changed, 90 insertions(+), 19 deletions(-) diff --git a/packages/sdk/src/authored-node-runner.ts b/packages/sdk/src/authored-node-runner.ts index 02dcc187..281ab63a 100644 --- a/packages/sdk/src/authored-node-runner.ts +++ b/packages/sdk/src/authored-node-runner.ts @@ -3,9 +3,9 @@ import { FLOW_READ_BUDGET_MS } from './journal-read-policy.js'; import { spawn, spawnSync } from 'node:child_process'; import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto'; import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; -import { readFileSync, realpathSync, statSync } from 'node:fs'; +import { accessSync, constants as fsConstants, readFileSync, realpathSync, statSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { isAbsolute, join } from 'node:path'; +import { delimiter, isAbsolute, join } from 'node:path'; import type { Readable } from 'node:stream'; import type { AuthoredRootMetadata } from './authored-root.js'; import type { AuthoredExecutionRuntime, AuthoredFlowExecutionResult, ExecuteAuthoredFlowOptions } from './authored-flow-executor.js'; @@ -39,22 +39,40 @@ interface NodeAuthority { path: string; version: string; executableSha256: strin * One authored run asks for the Node authority before root admission and * again before spawning the body. Each probe spawns Node and hashes its whole * executable (~100 MB), which a cold sandbox pays on the startup path twice. - * Reuse the previous answer only for the same candidate selection and an - * unchanged executable file. The spawned child still hashes the executable it - * actually runs and the parent compares that report with this authority, so - * a binary replaced after the probe is refused, never trusted. + * Reuse the previous answer only while the same candidate is selected (the + * override or the first `node` on PATH, by path and by realpath, so a + * retargeted symlink re-probes) and the executable it resolved is unchanged. + * The spawned child still hashes the executable it actually runs and the + * parent compares that report with this authority, so a binary replaced + * after the probe is refused, never trusted. */ -let probedAuthority: { key: string; fingerprint: string; authority: NodeAuthority } | undefined; +let probedAuthority: { selection: string; fingerprint: string; authority: NodeAuthority } | undefined; function executableFingerprint(path: string): string { const stat = statSync(path, { bigint: true }); return [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(':'); } +/** The candidate `spawnSync(override ?? 'node')` would run, by path and realpath. */ +function selectedCandidate(override: string | undefined): string | undefined { + const directories = (process.env['PATH'] ?? '').split(delimiter); + // A relative PATH entry resolves against the working directory; do not + // guess which candidate the spawn would pick, just probe every time. + if (override === undefined && directories.some(directory => !isAbsolute(directory))) return undefined; + const candidates = override !== undefined ? [override] : directories.map(directory => join(directory, 'node')); + for (const candidate of candidates) { + try { + if (!statSync(candidate).isFile()) continue; + accessSync(candidate, fsConstants.X_OK); + return `${candidate}\0${realpathSync(candidate)}`; + } catch { /* Not this candidate. */ } + } + return undefined; +} function nodeAuthority(): NodeAuthority { if (embeddedSource === undefined) throw refusal(); const override = process.env['FLOWS_AUTHORED_NODE']; if (override !== undefined && !isAbsolute(override)) throw refusal(); - const key = `${override ?? ''}\0${process.env['PATH'] ?? ''}`; - if (probedAuthority?.key === key) { + const selection = selectedCandidate(override); + if (selection !== undefined && probedAuthority?.selection === selection) { try { if (executableFingerprint(probedAuthority.authority.path) === probedAuthority.fingerprint) { return probedAuthority.authority; @@ -81,8 +99,11 @@ process.stdout.write(JSON.stringify({path:process.execPath,version:process.versi const path = realpathSync(value.path); const fingerprint = executableFingerprint(path); const authority = { path, version: value.version, executableSha256: hash(readFileSync(path)) }; - // A file rewritten while it was being hashed is not reused. - if (executableFingerprint(path) === fingerprint) probedAuthority = { key, fingerprint, authority }; + // Reuse only an answer for a candidate that could be identified, and not + // one whose file was rewritten while it was being hashed. + if (selection !== undefined && executableFingerprint(path) === fingerprint) { + probedAuthority = { selection, fingerprint, authority }; + } return authority; } catch { throw refusal(); } } diff --git a/packages/sdk/tests/authored-node-authority.test.ts b/packages/sdk/tests/authored-node-authority.test.ts index 3e88145f..c3db4f60 100644 --- a/packages/sdk/tests/authored-node-authority.test.ts +++ b/packages/sdk/tests/authored-node-authority.test.ts @@ -1,4 +1,4 @@ -import { chmodSync, mkdtempSync, readFileSync, realpathSync, rmSync, utimesSync, writeFileSync } from 'node:fs'; +import { chmodSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, unlinkSync, utimesSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; @@ -15,9 +15,7 @@ beforeAll(() => { directory = mkdtempSync(join(tmpdir(), 'authored-node-authority-')); probes = join(directory, 'probes'); writeFileSync(probes, ''); - candidate = join(directory, 'node-candidate'); - writeFileSync(candidate, `#!/bin/sh\necho probe >> ${JSON.stringify(probes)}\nprintf '%s' '${JSON.stringify({ path: join(directory, 'node-candidate'), version: '22.14.0' })}'\n`); - chmodSync(candidate, 0o755); + candidate = writeCandidate('node-candidate'); previousOverride = process.env['FLOWS_AUTHORED_NODE']; process.env['FLOWS_AUTHORED_NODE'] = candidate; installAuthoredNodeSource('export {};\n'); @@ -31,6 +29,14 @@ afterAll(() => { const probeCount = (): number => readFileSync(probes, 'utf8').split('\n').filter(Boolean).length; +/** A stand-in Node that answers the probe with its own path and records the call. */ +function writeCandidate(name: string): string { + const path = join(directory, name); + writeFileSync(path, `#!/bin/sh\necho probe >> ${JSON.stringify(probes)}\nprintf '%s' '${JSON.stringify({ path, version: '22.14.0' })}'\n`); + chmodSync(path, 0o755); + return path; +} + describe('authored Node authority', () => { it('probes and hashes the executable once per process while it is unchanged', () => { const first = authoredNodeAuthorityForTest(); @@ -52,12 +58,56 @@ describe('authored Node authority', () => { expect(after.executableSha256).not.toBe(before.executableSha256); }); - it('re-probes when the candidate selection changes', () => { + it('re-probes when the override selects a different executable', () => { authoredNodeAuthorityForTest(); const count = probeCount(); - const path = process.env['PATH']; - process.env['PATH'] = `${directory}:${path ?? ''}`; - try { authoredNodeAuthorityForTest(); } finally { process.env['PATH'] = path; } + const other = writeCandidate('other-candidate'); + process.env['FLOWS_AUTHORED_NODE'] = other; + try { + expect(authoredNodeAuthorityForTest().path).toBe(realpathSync(other)); + } finally { process.env['FLOWS_AUTHORED_NODE'] = candidate; } expect(probeCount()).toBe(count + 1); }); + + it('re-probes when the selected override symlink is retargeted while the old binary remains', () => { + const first = writeCandidate('symlink-target-a'); + const second = writeCandidate('symlink-target-b'); + const selected = join(directory, 'selected-node'); + symlinkSync(first, selected); + process.env['FLOWS_AUTHORED_NODE'] = selected; + try { + expect(authoredNodeAuthorityForTest().path).toBe(realpathSync(first)); + const count = probeCount(); + expect(authoredNodeAuthorityForTest().path).toBe(realpathSync(first)); + expect(probeCount()).toBe(count); + unlinkSync(selected); + symlinkSync(second, selected); + expect(authoredNodeAuthorityForTest().path).toBe(realpathSync(second)); + expect(probeCount()).toBe(count + 1); + } finally { process.env['FLOWS_AUTHORED_NODE'] = candidate; } + }); + + it('re-probes when the `node` found on PATH is retargeted', () => { + const first = writeCandidate('path-target-a'); + const second = writeCandidate('path-target-b'); + const bin = mkdtempSync(join(directory, 'bin-')); + const node = join(bin, 'node'); + symlinkSync(first, node); + const path = process.env['PATH']; + delete process.env['FLOWS_AUTHORED_NODE']; + process.env['PATH'] = `${bin}:/usr/bin:/bin`; + try { + expect(authoredNodeAuthorityForTest().path).toBe(realpathSync(first)); + const count = probeCount(); + expect(authoredNodeAuthorityForTest().path).toBe(realpathSync(first)); + expect(probeCount()).toBe(count); + unlinkSync(node); + symlinkSync(second, node); + expect(authoredNodeAuthorityForTest().path).toBe(realpathSync(second)); + expect(probeCount()).toBe(count + 1); + } finally { + process.env['PATH'] = path; + process.env['FLOWS_AUTHORED_NODE'] = candidate; + } + }); });