Files
agentic-dev/apps/runner/src/exec.ts
Danijel Martinek 750ab44379 feat(runner): clone stage with ephemeral credential helper
Clone stage per tech spec §6, verbatim mechanics: every git invocation
carries a BLANK credential.helper first (suppresses OS keychain
helpers) then the inline veect helper; the PAT reaches git via child
env only — never argv, URLs, logs, or .git/config. GIT_TERMINAL_PROMPT
and GIT_ASKPASS are pinned so a headless clone can never hang on a TTY
or ambient IDE askpass. Staged status events (start/heartbeat/final) +
ready on success; named failures: invalid-git-url, auth-failed,
clone-failed (daemon's 'repository not exported' maps to bad-URL, not
auth). Integration suite clones the daemon-served vite-kitchen and an
authenticated dumb-HTTP remote that asserts the exact Basic credential
git presented, plus leak assertions over logs/argv/.git.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
2026-07-12 22:50:41 +02:00

56 lines
1.6 KiB
TypeScript

import { execFile } from "node:child_process";
/**
* Minimal child-process runner shared by the stages. Array-args only —
* never a shell — so URLs and paths cannot be interpreted, and secrets
* can only travel via `env` (spec §6/§10).
*/
export interface CommandResult {
exitCode: number;
stdout: string;
stderr: string;
}
export type RunCommand = (
command: string,
args: string[],
options: { cwd: string; env?: Record<string, string> },
) => Promise<CommandResult>;
const MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
/** Ten minutes: covers a cold `npm install` of a real repo; prevents hangs. */
const COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
export const runCommand: RunCommand = (command, args, options) =>
new Promise((resolve, reject) => {
execFile(
command,
args,
{
cwd: options.cwd,
env: { ...process.env, ...options.env },
maxBuffer: MAX_OUTPUT_BYTES,
timeout: COMMAND_TIMEOUT_MS,
},
(error, stdout, stderr) => {
if (error === null) {
resolve({ exitCode: 0, stdout, stderr });
return;
}
// Non-zero exit is a result, not an exception — stages map it to
// named protocol errors. Spawn-level failures still reject.
const exitCode = typeof error.code === "number" ? error.code : null;
if (exitCode !== null) {
resolve({ exitCode, stdout, stderr });
return;
}
if (error.killed === true) {
resolve({ exitCode: 124, stdout, stderr: `${stderr}\n(timed out)` });
return;
}
reject(error);
},
);
});