(args: HeadlessTaskArgs)
| 147 | } |
| 148 | |
| 149 | export async function runHeadlessTask(args: HeadlessTaskArgs): Promise<HeadlessTaskResult> { |
| 150 | const { command, cwd, env, timeoutMs, logger } = args; |
| 151 | const [argv0] = command; |
| 152 | if (!argv0) throw new Error('headless: empty command'); |
| 153 | |
| 154 | const start = Date.now(); |
| 155 | let exitCode: number | null = null; |
| 156 | let signal: NodeJS.Signals | null = null; |
| 157 | let killed = false; |
| 158 | let agentSessionId: string | null = null; |
| 159 | const outSink = makeTailSink(OUTPUT_TAIL_BYTES); |
| 160 | const errSink = makeTailSink(OUTPUT_TAIL_BYTES); |
| 161 | const scanner = args.extractSessionId |
| 162 | ? makeSessionIdScanner(args.extractSessionId, (id) => { |
| 163 | agentSessionId = id; |
| 164 | logger.info('headless.session_id_captured', { agentSessionId: id }); |
| 165 | args.onSessionId?.(id); |
| 166 | }) |
| 167 | : null; |
| 168 | // win32: resolve the bare CLI name against PATH × PATHEXT. Native-exe agents |
| 169 | // (claude.exe, codex.exe) resolve to a direct path and run headless fine. But |
| 170 | // npm-shim agents (opencode, pi → a `.cmd`) would have to run through cmd.exe, |
| 171 | // and the headless PROMPT is the trailing arg — routing it through cmd.exe |
| 172 | // re-parses shell metacharacters (CVE-2024-27980 territory), a real injection |
| 173 | // surface. So shim agents stay headless-unsupported on Windows; we fail with a |
| 174 | // clear, recorded reason instead of a silent ENOENT. (Interactive launch of |
| 175 | // the same agents works — see win-command.ts / persistent-session.ts.) |
| 176 | const resolved = resolveLaunchCommand(command, { env }); |
| 177 | if (resolved.viaShell) { |
| 178 | logger.error('headless.win32_shim_unsupported', { command: argv0 }); |
| 179 | return { |
| 180 | command, |
| 181 | cwd, |
| 182 | exitCode: -1, |
| 183 | signal: null, |
| 184 | killed: false, |
| 185 | durationMs: Date.now() - start, |
| 186 | stdoutTail: '', |
| 187 | stderrTail: |
| 188 | `win32: "${argv0}" is an npm .cmd shim; headless dispatch is unsupported ` + |
| 189 | `on Windows (routing the task prompt through cmd.exe is a shell-injection ` + |
| 190 | `surface). Native-exe agents (claude, codex) run headless; run shim agents ` + |
| 191 | `(opencode, pi) interactively instead.`, |
| 192 | agentSessionId: null, |
| 193 | }; |
| 194 | } |
| 195 | const [spawnFile, ...spawnArgs] = resolved.argv; |
| 196 | if (!spawnFile) throw new Error('headless: empty command after resolution'); |
| 197 | const outFile = args.stdoutFile ? await openLogStream(args.stdoutFile, logger, 'stdout') : null; |
| 198 | const errFile = args.stderrFile ? await openLogStream(args.stderrFile, logger, 'stderr') : null; |
| 199 | const child = spawn(spawnFile, spawnArgs, { |
| 200 | cwd, |
| 201 | env: env as NodeJS.ProcessEnv, |
| 202 | stdio: ['ignore', 'pipe', 'pipe'], |
| 203 | }); |
| 204 | child.stdout?.on('data', (d: Buffer) => { |
| 205 | outSink.push(d); |
| 206 | scanner?.push(d); |
no test coverage detected