process.exec: BEGIN-only awk prints output then times out, including stdin: ''

Abierto
#2,088 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
20/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
javascript, rust, wasm
Área
backend

Línea de trabajo

Start with crates/native-sidecar/src/language_execution.rs near line 1809, where attached stdin bytes are written but stdin is not closed, and compare it with the older noninteractive kernel exec path that closes stdin. The gawk wrapper in software/gawk/native/crates/awk/src/lib.rs supplies the input reader, and awk-rs's src/interpreter/mod.rs lines 238-276 and 307-308 show where BEGIN-only programs still reach read_line. Run the awk BEGIN row of repro.mjs; done when it returns succeeded with exit 0 under both Node and Bun, and the explicit-exit and /dev/null cases still pass.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Written by an AI agent (Codex) on behalf of @jasonkuhrt.

A finite BEGIN-only awk command prints its complete output but never finishes through vm.process.exec. It reaches the configured timeout under both Node and Bun, including when the supported stdin: '' option is supplied. In our noninteractive agent tool this consumes the full 120-second command deadline and returns timed_out instead of success.

Reproduction and results

Amazon Linux 2023 x86_64; @rivet-dev/agentos-core, runtime-core and native package 0.4.6; Node 24.22.1 and Bun 1.4.4. npm gitHead/main: c7462927ec452bbd5def5c0fc50a3fd74574d13a. Resolved guest command: /opt/agentos/pkgs/gawk/0.3.4/bin/awk, implemented by awk-rs 0.1.0.

Smallest failing call after the VM is ready

const result = await vm.process.exec(
  'awk \'BEGIN { print "AWK" }\'',
  {
    cwd: '/workspace',
    timeoutMs: 5_000,
    stdin: '',
    output: { capture: 'all' },
  },
);

Expected: succeeded, exit 0, stdout "AWK\n", empty stderr. Actual: stdout is correct, then timed_out, exit 137. The explicit-empty-stdin rows took 5,017 ms Node / 5,020 ms Bun. Omitting stdin has the same failure.

The independently rerun standalone script below produced:

Guest command Node 24.22.1 Bun 1.4.4
printf + file write/read control succeeded/0; exact output succeeded/0; exact output
awk 'BEGIN { print "AWK" }' output then timed_out/137, 5,012 ms output then timed_out/137, 5,009 ms
awk 'BEGIN { print "AWK"; exit }' succeeded/0; exact output succeeded/0; exact output
awk 'BEGIN { print "AWK" }' < /dev/null succeeded/0; exact output succeeded/0; exact output

The full 3,000-line write originally exposing this also timed out at 120,017 ms Node / 120,012 ms Bun. The same write with explicit exit or EOF completed with exact full file bytes/hash on both. This is not a Node-versus-Bun performance claim.

POSIX Special Patterns requires BEGIN-only programs without getline to exit without reading input; GNU's explanation agrees. A host GNU Awk 5.1.0 control exited 0 while its stdin pipe was deliberately kept open.

Runnable standalone script

Use an empty project with exact dependencies @rivet-dev/[email protected] and @aws-sdk/[email protected]. Start the local S3 emulator image ghcr.io/alchemy-run/floci:2.1.0-alchemy.3 on loopback port 4566. Save the following as repro.mjs, then run node repro.mjs or bun --no-env-file repro.mjs. AGENTOS_LOG=debug enables native logs.

The script imports no application code, uses only emulator test credentials and unique local resources, and verifies cleanup. It exits 0 when the diagnostic assertions, including the expected bad timed_out result, hold. This exact source was run under both runtimes; SHA256 69e17f4310799e113ed890c5c9d22c01167ddbedbf630cb5c1ed53aaba39420f.

Complete repro.mjs
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import path from 'node:path';
import os from 'node:os';
import fs from 'node:fs/promises';
import { randomUUID, createHash } from 'node:crypto';
const require = createRequire(import.meta.url);
const { AgentOs } = await import(require.resolve('@rivet-dev/agentos-core'));
const S3 = require('@aws-sdk/client-s3');
const runtime = process.versions.bun ? 'bun' : 'node';
assert.equal(process.versions.bun ?? process.version, runtime === 'bun' ? '1.4.4' : 'v24.22.1');
const endpoint = 'http://127.0.0.1:4566';
assert.equal(new URL(endpoint).hostname, '127.0.0.1');
const bucket = `shell-bare-${Date.now()}-${randomUUID().slice(0, 8)}`;
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'shell-bare-'));
const metadataPath = path.join(directory, 'metadata.sqlite');
const s3 = new S3.S3Client({ endpoint, region: 'us-east-1', forcePathStyle: true,
  credentials: { accessKeyId: 'test', secretAccessKey: 'test' }, maxAttempts: 2 });
const send = command => s3.send(command, { abortSignal: AbortSignal.timeout(10000) });
let vm, bucketCreated = false;
console.log(JSON.stringify({ event: 'provenance', runtime, version: process.versions.bun ?? process.version,
  executable: process.execPath, platform: process.platform, arch: process.arch, coreVersion: '0.4.6',
  bucket, endpoint, directory, nativeLogLevel: process.env.AGENTOS_LOG }));
try {
  await send(new S3.CreateBucketCommand({ Bucket: bucket })); bucketCreated = true;
  vm = await AgentOs.create({ user: { uid: 0, gid: 0 }, permissions: { fs: 'allow',
    childProcess: 'allow', process: 'allow', env: 'deny', hostFunction: 'allow', network: 'deny' },
    mounts: [{ path: '/workspace', plugin: { id: 'chunked_s3', config: { bucket,
      prefix: 'shell/', region: 'us-east-1', endpoint, metadataPath,
      credentials: { accessKeyId: 'test', secretAccessKey: 'test' } } } }] });
  const children = (await fs.readFile(`/proc/${process.pid}/task/${process.pid}/children`, 'utf8')).trim().split(/\s+/).filter(Boolean);
  console.log(JSON.stringify({ event: 'native_children', children: await Promise.all(children.map(async pid => {
    const executable = await fs.realpath(`/proc/${pid}/exe`);
    return { pid, executable, sha256: createHash('sha256').update(await fs.readFile(executable)).digest('hex') };
  })) }));
  for (const [label, command, expectedOutcome, expectedOutput] of [
    ['printf-read', "printf 'BEGIN\\n'; printf 'owned\\n' > /workspace/owned.txt; cat /workspace/owned.txt; printf 'END\\n'", 'succeeded', 'BEGIN\nowned\nEND\n'],
    ['awk-begin-open-stdin', 'awk \'BEGIN { print "AWK" }\'', 'timed_out', 'AWK\n'],
    ['awk-begin-explicit-exit', 'awk \'BEGIN { print "AWK"; exit }\'', 'succeeded', 'AWK\n'],
    ['awk-begin-null-input', 'awk \'BEGIN { print "AWK" }\' < /dev/null', 'succeeded', 'AWK\n'],
  ]) {
    const start = Date.now(); console.log(JSON.stringify({ event: 'execution_begin', label, command, start }));
    const result = await vm.process.exec(command, { cwd: '/workspace', timeoutMs: 5000, output: { capture: 'all' } });
    console.log(JSON.stringify({ event: 'execution_end', label, command, elapsedMs: Date.now() - start, result }));
    assert.equal(result.outcome, expectedOutcome); assert.equal(result.stdout, expectedOutput);
    assert.equal(result.exitCode, expectedOutcome === 'succeeded' ? 0 : 137); assert.equal(result.stderr, '');
  }
  assert.equal(Buffer.from(await vm.filesystem.readFile('/workspace/owned.txt')).toString(), 'owned\n');
  console.log(JSON.stringify({ event: 'diagnosis', reproducedBeginOnlyInputWait: true, minimalShellPassed: true }));
} finally {
  await vm?.dispose();
  if (bucketCreated) {
    for (const object of (await send(new S3.ListObjectsV2Command({ Bucket: bucket }))).Contents ?? [])
      await send(new S3.DeleteObjectCommand({ Bucket: bucket, Key: object.Key }));
    await send(new S3.DeleteBucketCommand({ Bucket: bucket }));
    await assert.rejects(() => send(new S3.HeadBucketCommand({ Bucket: bucket })), error => error.$metadata?.httpStatusCode === 404);
  }
  await fs.rm(directory, { recursive: true, force: true });
  await assert.rejects(() => fs.stat(directory), error => error.code === 'ENOENT');
  s3.destroy();
  console.log(JSON.stringify({ event: 'cleanup', bucketAbsentVerified: bucketCreated, metadataDirectoryAbsentVerified: true }));
}
process.exit(0);

Source evidence and suspected cause

Two input-handling details interact here; this report does not claim they are two independently reproduced defects.

  • In native language_execution.rs around 1809, the attached execution path writes supplied stdin bytes but does not close stdin. The SDK serializes stdin: '' as an empty buffer, whereas omitted stdin is null. The older noninteractive kernel exec path explicitly closes stdin after optional bytes.
  • The gawk wrapper supplies an input reader. That alone does not prove it is read. The actual awk-rs 0.1.0 crate, SHA256 ed6041a9be45690d0003e8d2ea488cb0a578104e591944398d96d9b7d9a0fbf9, matches the lockfile. Its src/interpreter/mod.rs:238-276 executes BEGIN and then enters input processing unless should_exit was set; :307-308 calls read_line.
  • Native sidecar SHA256 in both runs: 1f28dd8d6bc95dfed8926c36432eac8e5f86a0a578b6a9afef96f380a0b6ccfd. Installed gawk.aospkg SHA256: 29577ba7506191790426118d5e64a3f09c1cc4be5dab5ca938ffd7b70ba1229d; embedded awk WASM: 6fa4fc4d8cc515a5febf4c6a1bee68455494a2e7487bc4e8c854b056b2f98992.

Related earlier stdin repairs #2043 and #2046 shipped before 0.4.6; the reproduction above uses the newer attached shell_execution path. No matching BEGIN-only issue or later release/fix was found in the current search.

We have retained process.exec rather than replacing its structured result/capture/deadline contract with a custom spawn adapter. The requested behavior is simply that this finite command returns success without a shell rewrite, explicit-exit workaround, or an application-owned process manager.

Lenguaje dominante
Rust
Estrellas
4.8k
Forks
270
Merge medio
13 h 37 min
PR fusionados (30 d)
48

Preparar el entorno

Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de rivet-dev/agentos

Todos los issues de rivet-dev/agentos

Issues similares

Más issues de Rust

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.