Skip to content

Commit f032edd

Browse files
V48 Gate 3 (impl-only): Fix sandbox tsx path
Resolve tsx from pipeline-hosts workspace paths so Pipeliner deposit runs no longer hang on bare node --import tsx / runtime npm install. Write absolute stdout/stderr/exit/telemetry markers immediately so the host always gets exit-code or early telemetry.
1 parent f6b34fe commit f032edd

4 files changed

Lines changed: 149 additions & 24 deletions

File tree

containers/images/pipeliner/Dockerfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ RUN mkdir -p /opt/bitcode/.proofs/pipeline-host /opt/bitcode/pipeline \
5555
&& cp containers/images/pipeliner/dist/run-live-asset-pack-pipeline.mjs /opt/bitcode/.proofs/pipeline-host/ \
5656
&& cp containers/images/pipeliner/dist/run-pipeline.mjs /opt/bitcode/pipeline/run-pipeline.mjs
5757

58+
# tsx must be loadable for the live AssetPack runner (imports monorepo .ts).
59+
# pnpm keeps it under packages/pipeline-hosts; host plan resolves that path.
60+
# Assert at image build so a missing dep fails the image, not a stuck sandbox.
61+
RUN test -f packages/pipeline-hosts/node_modules/tsx/dist/loader.mjs \
62+
|| test -f node_modules/tsx/dist/loader.mjs \
63+
|| (echo "pipeliner image missing tsx loader (pipeline-hosts dep)" >&2 && exit 1)
64+
5865
ENV BITCODE_MONOREPO_ROOT=/opt/bitcode
5966
ENV BITCODE_PIPELINE_IMAGE_ENTRY=/opt/bitcode/pipeline/run-pipeline.mjs
6067
WORKDIR /vercel/sandbox

packages/pipeline-hosts/src/__tests__/asset-pack-host-plan.test.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,18 @@ describe('asset-pack sandbox host plan', () => {
107107
]);
108108
const run = plan.commands.find((c) => c.label === 'asset-pack-pipeline-run');
109109
const runScript = run?.args?.join(' ') ?? '';
110-
// Image path runs sandbox-uploaded runner with tsx (loads monorepo .ts packages).
110+
// Image path: workspace tsx loader (pnpm does not hoist to monorepo root),
111+
// absolute log/exit markers, cwd sandbox root — no runtime npm install hang.
112+
expect(run?.cwd).toBe('/vercel/sandbox');
113+
expect(run?.detached).toBe(true);
114+
expect(run?.exitCodePath).toBe('.proofs/pipeline-host/pipeline.exit-code');
111115
expect(runScript).toContain('run-live-asset-pack-pipeline.mjs');
112-
expect(runScript).toContain('tsx');
116+
expect(runScript).toContain('/opt/bitcode/packages/pipeline-hosts/node_modules/tsx/dist/loader.mjs');
117+
expect(runScript).toContain('/vercel/sandbox/.proofs/pipeline-host/pipeline.exit-code');
118+
expect(runScript).toContain('/vercel/sandbox/.proofs/pipeline-host/telemetry.jsonl');
119+
expect(runScript).toContain('pipeline-shell-start');
120+
expect(runScript).toContain('Refusing runtime npm install');
121+
expect(runScript).not.toContain('npm install -g tsx');
113122
expect(runScript).toContain('/opt/bitcode');
114123
});
115124

packages/pipeline-hosts/src/asset-pack-host-constants.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ export const PIPELINE_STDOUT_PATH = `${HOST_RUN_DIRECTORY}/pipeline.stdout.log`;
1515
export const PIPELINE_STDERR_PATH = `${HOST_RUN_DIRECTORY}/pipeline.stderr.log`;
1616
export const PIPELINE_EXIT_CODE_PATH = `${HOST_RUN_DIRECTORY}/pipeline.exit-code`;
1717
export const SANDBOX_WORKING_DIRECTORY = '/vercel/sandbox' as const;
18+
/** Absolute host-run dir inside the sandbox microVM (Vercel default mount). */
19+
export const SANDBOX_HOST_RUN_DIRECTORY =
20+
`${SANDBOX_WORKING_DIRECTORY}/${HOST_RUN_DIRECTORY}` as const;
21+
export const SANDBOX_PIPELINE_STDOUT_PATH =
22+
`${SANDBOX_HOST_RUN_DIRECTORY}/pipeline.stdout.log` as const;
23+
export const SANDBOX_PIPELINE_STDERR_PATH =
24+
`${SANDBOX_HOST_RUN_DIRECTORY}/pipeline.stderr.log` as const;
25+
export const SANDBOX_PIPELINE_EXIT_CODE_PATH =
26+
`${SANDBOX_HOST_RUN_DIRECTORY}/pipeline.exit-code` as const;
27+
export const SANDBOX_TELEMETRY_PATH =
28+
`${SANDBOX_HOST_RUN_DIRECTORY}/telemetry.jsonl` as const;
1829
export const DEFAULT_LONG_TIMEOUT_MS = 45 * 60 * 1000;
1930
export const SANDBOX_PNPM_VERSION = '10.33.0';
2031

@@ -33,6 +44,22 @@ export const PIPELINER_IMAGE_DEFAULT = `${PIPELINER_VCR_REPOSITORY}:latest`;
3344
export const PIPELINE_IMAGE_ENTRY_DEFAULT = '/opt/bitcode/pipeline/run-pipeline.mjs';
3445
export const PIPELINE_IMAGE_MONOREPO_ROOT_DEFAULT = '/opt/bitcode';
3546

47+
/**
48+
* Absolute tsx ESM loader paths inside the Pipeliner monorepo install.
49+
* pnpm does not hoist `tsx` to `/opt/bitcode/node_modules`, so bare
50+
* `node --import tsx` fails with ERR_MODULE_NOT_FOUND from monorepo root.
51+
* Prefer the pipeline-hosts workspace install (declared dependency).
52+
*/
53+
export const PIPELINE_IMAGE_TSX_LOADER_CANDIDATES = [
54+
`${PIPELINE_IMAGE_MONOREPO_ROOT_DEFAULT}/packages/pipeline-hosts/node_modules/tsx/dist/loader.mjs`,
55+
`${PIPELINE_IMAGE_MONOREPO_ROOT_DEFAULT}/node_modules/tsx/dist/loader.mjs`,
56+
`${PIPELINE_IMAGE_MONOREPO_ROOT_DEFAULT}/node_modules/.pnpm/node_modules/tsx/dist/loader.mjs`,
57+
] as const;
58+
export const PIPELINE_IMAGE_TSX_CLI_CANDIDATES = [
59+
`${PIPELINE_IMAGE_MONOREPO_ROOT_DEFAULT}/packages/pipeline-hosts/node_modules/tsx/dist/cli.mjs`,
60+
`${PIPELINE_IMAGE_MONOREPO_ROOT_DEFAULT}/node_modules/tsx/dist/cli.mjs`,
61+
] as const;
62+
3663
/** Env: when set, host plans use Sandbox.create({ image }) instead of runtime. */
3764
export const PIPELINE_SANDBOX_IMAGE_ENV = 'BITCODE_PIPELINE_SANDBOX_IMAGE';
3865
export const PIPELINE_IMAGE_ENTRY_ENV = 'BITCODE_PIPELINE_IMAGE_ENTRY';

packages/pipeline-hosts/src/asset-pack-host-plan.ts

Lines changed: 104 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,16 @@ import {
3232
PIPELINE_IMAGE_ENTRY_DEFAULT,
3333
PIPELINE_IMAGE_ENTRY_ENV,
3434
PIPELINE_IMAGE_MONOREPO_ROOT_DEFAULT,
35+
PIPELINE_IMAGE_TSX_CLI_CANDIDATES,
36+
PIPELINE_IMAGE_TSX_LOADER_CANDIDATES,
3537
PIPELINE_SANDBOX_IMAGE_ENV,
3638
PIPELINE_STDERR_PATH,
3739
PIPELINE_STDOUT_PATH,
40+
SANDBOX_PIPELINE_EXIT_CODE_PATH,
41+
SANDBOX_PIPELINE_STDERR_PATH,
42+
SANDBOX_PIPELINE_STDOUT_PATH,
3843
SANDBOX_PNPM_VERSION,
44+
SANDBOX_TELEMETRY_PATH,
3945
SANDBOX_WORKING_DIRECTORY,
4046
SOURCE_OVERLAY_PATCH_PATH,
4147
TELEMETRY_PATH,
@@ -293,33 +299,18 @@ function buildPipelineImageCommands(
293299

294300
if (mode === 'asset_pack_pipeline') {
295301
// Prefer sandbox-uploaded runner (hot-fixed) over image-baked copy.
296-
// tsx loads monorepo .ts package sources under BITCODE_MONOREPO_ROOT.
297-
const sandboxLiveRunner = `${SANDBOX_WORKING_DIRECTORY}/${HOST_RUN_DIRECTORY}/run-live-asset-pack-pipeline.mjs`;
298-
const monorepoRoot = PIPELINE_IMAGE_MONOREPO_ROOT_DEFAULT;
299-
const pipelineRunScript = [
300-
`cd ${shellQuote(monorepoRoot)}`,
301-
// tsx loads monorepo .ts package sources (plain node cannot).
302-
`if ! node --import tsx -e "process.exit(0)" >/dev/null 2>&1; then`,
303-
` npm install -g tsx@4.19.3 || npm install --no-save --prefix ${shellQuote(monorepoRoot)} tsx@4.19.3`,
304-
`fi`,
305-
`RUNNER=${shellQuote(sandboxLiveRunner)}`,
306-
`if [ ! -f "$RUNNER" ]; then RUNNER=${shellQuote(`${monorepoRoot}/.proofs/pipeline-host/run-live-asset-pack-pipeline.mjs`)}; fi`,
307-
`if [ ! -f "$RUNNER" ]; then echo "live runner missing; falling back to image dispatcher" >&2; node ${shellQuote(pipelineImageEntry)}; else node --import tsx "$RUNNER"; fi`,
308-
].join(' && ');
302+
// Live runner imports monorepo .ts packages — needs tsx, resolved from the
303+
// image workspace (pnpm does not hoist to monorepo root). Never hang on
304+
// runtime npm install; fail closed with logs + exit-code for host poll.
305+
const pipelineRunScript = buildPipelineImageRunShellScript(pipelineImageEntry);
309306
commands.push({
310307
label: 'asset-pack-pipeline-run',
311308
cmd: 'sh',
312-
args: [
313-
'-lc',
314-
[
315-
`( ${pipelineRunScript} ) > ${shellQuote(PIPELINE_STDOUT_PATH)} 2> ${shellQuote(PIPELINE_STDERR_PATH)}`,
316-
'code=$?',
317-
`printf "%s" "$code" > ${shellQuote(PIPELINE_EXIT_CODE_PATH)}`,
318-
'exit "$code"',
319-
].join('; '),
320-
],
309+
args: ['-lc', pipelineRunScript],
310+
cwd: SANDBOX_WORKING_DIRECTORY,
321311
env: commandEnvironment,
322312
detached: true,
313+
// Relative paths: Sandbox readFile is rooted at /vercel/sandbox.
323314
exitCodePath: PIPELINE_EXIT_CODE_PATH,
324315
stdoutPath: PIPELINE_STDOUT_PATH,
325316
stderrPath: PIPELINE_STDERR_PATH,
@@ -456,6 +447,7 @@ function buildCommands(
456447
'exit "$code"',
457448
].join('; '),
458449
],
450+
cwd: SANDBOX_WORKING_DIRECTORY,
459451
env: commandEnvironment,
460452
detached: true,
461453
exitCodePath: PIPELINE_EXIT_CODE_PATH,
@@ -480,6 +472,96 @@ function buildCommands(
480472
return commands;
481473
}
482474

475+
/**
476+
* Detached in-box shell for Pipeliner image mode.
477+
*
478+
* Guarantees (for host poll + operator debug):
479+
* - Absolute stdout/stderr/exit-code under /vercel/sandbox/.proofs/pipeline-host
480+
* - Early telemetry.jsonl line so UI is not stuck on bare command-started
481+
* - tsx resolved from workspace install paths (no runtime npm install hang)
482+
* - Always writes pipeline.exit-code so waitForDetachedCommand terminates
483+
*/
484+
export function buildPipelineImageRunShellScript(pipelineImageEntry: string): string {
485+
const monorepoRoot = PIPELINE_IMAGE_MONOREPO_ROOT_DEFAULT;
486+
const sandboxLiveRunner = `${SANDBOX_WORKING_DIRECTORY}/${HOST_RUN_DIRECTORY}/run-live-asset-pack-pipeline.mjs`;
487+
const imageLiveRunner = `${monorepoRoot}/.proofs/pipeline-host/run-live-asset-pack-pipeline.mjs`;
488+
const stdoutPath = SANDBOX_PIPELINE_STDOUT_PATH;
489+
const stderrPath = SANDBOX_PIPELINE_STDERR_PATH;
490+
const exitCodePath = SANDBOX_PIPELINE_EXIT_CODE_PATH;
491+
const telemetryPath = SANDBOX_TELEMETRY_PATH;
492+
493+
const loaderCandidates = PIPELINE_IMAGE_TSX_LOADER_CANDIDATES.map(
494+
(p) => shellQuote(p),
495+
).join(' ');
496+
const cliCandidates = PIPELINE_IMAGE_TSX_CLI_CANDIDATES.map((p) => shellQuote(p)).join(' ');
497+
498+
// Single shell program: markers first, then resolve tsx, then run.
499+
// Redirects use absolute paths so cwd surprises cannot hide artifacts.
500+
return [
501+
'set +e',
502+
`HOST_DIR=${shellQuote(SANDBOX_WORKING_DIRECTORY + '/' + HOST_RUN_DIRECTORY)}`,
503+
'mkdir -p "$HOST_DIR"',
504+
`STDOUT=${shellQuote(stdoutPath)}`,
505+
`STDERR=${shellQuote(stderrPath)}`,
506+
`EXITF=${shellQuote(exitCodePath)}`,
507+
`TELEM=${shellQuote(telemetryPath)}`,
508+
// Truncate / create markers immediately (host + interactive debug).
509+
': > "$STDOUT"',
510+
': > "$STDERR"',
511+
'TS=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo unknown)',
512+
'echo "pipeline-shell: start ts=$TS monorepo=' +
513+
monorepoRoot +
514+
' node=$(command -v node 2>/dev/null)" >> "$STDERR"',
515+
'printf "%s\\n" "{\\"type\\":\\"pipeline-shell-start\\",\\"timestamp\\":\\"$TS\\",\\"cwd\\":\\"$(pwd)\\"}" >> "$TELEM"',
516+
'(',
517+
` cd ${shellQuote(monorepoRoot)} || { echo "pipeline-shell: cd monorepo failed" >&2; exit 127; }`,
518+
' TSX_LOADER=""',
519+
` for c in ${loaderCandidates}; do`,
520+
' if [ -f "$c" ]; then TSX_LOADER=$c; break; fi',
521+
' done',
522+
' TSX_CLI=""',
523+
` for c in ${cliCandidates}; do`,
524+
' if [ -f "$c" ]; then TSX_CLI=$c; break; fi',
525+
' done',
526+
' if [ -z "$TSX_LOADER" ] && [ -z "$TSX_CLI" ]; then',
527+
' if node --import tsx -e "process.exit(0)" >/dev/null 2>&1; then',
528+
' TSX_MODE=package',
529+
' else',
530+
' echo "pipeline-shell: tsx not found under monorepo (loader/cli/package). Refusing runtime npm install." >&2',
531+
' echo "pipeline-shell: candidates checked; rebuild Pipeliner with pipeline-hosts tsx dep." >&2',
532+
' exit 127',
533+
' fi',
534+
' else',
535+
' TSX_MODE=path',
536+
' fi',
537+
` RUNNER=${shellQuote(sandboxLiveRunner)}`,
538+
` if [ ! -f "$RUNNER" ]; then RUNNER=${shellQuote(imageLiveRunner)}; fi`,
539+
' echo "pipeline-shell: tsx_mode=$TSX_MODE loader=${TSX_LOADER:-none} cli=${TSX_CLI:-none} runner=$RUNNER" >&2',
540+
` if [ ! -f "$RUNNER" ]; then`,
541+
` echo "pipeline-shell: live runner missing; falling back to image dispatcher" >&2`,
542+
` node ${shellQuote(pipelineImageEntry)}`,
543+
' exit $?',
544+
' fi',
545+
' if [ -n "$TSX_LOADER" ]; then',
546+
' node --import "$TSX_LOADER" "$RUNNER"',
547+
' exit $?',
548+
' fi',
549+
' if [ -n "$TSX_CLI" ]; then',
550+
' node "$TSX_CLI" "$RUNNER"',
551+
' exit $?',
552+
' fi',
553+
' node --import tsx "$RUNNER"',
554+
' exit $?',
555+
') > "$STDOUT" 2>> "$STDERR"',
556+
'code=$?',
557+
'TS2=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo unknown)',
558+
'echo "pipeline-shell: finish ts=$TS2 exit=$code" >> "$STDERR"',
559+
'printf "%s\\n" "{\\"type\\":\\"pipeline-shell-finish\\",\\"timestamp\\":\\"$TS2\\",\\"exitCode\\":$code}" >> "$TELEM"',
560+
'printf "%s" "$code" > "$EXITF"',
561+
'exit "$code"',
562+
].join('\n');
563+
}
564+
483565
function shellQuote(value: string): string {
484566
return `'${value.replace(/'/g, `'\\''`)}'`;
485567
}

0 commit comments

Comments
 (0)