@proagentstore/cli 0.4.114 → 0.4.116
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-runner/coding/engine-check.js +5 -37
- package/dist/browser-runner/coding/engine-env.js +6 -3
- package/dist/browser-runner/coding/engine-spawn.js +99 -0
- package/dist/browser-runner/coding/git-guard.js +146 -0
- package/dist/browser-runner/coding/headless.js +11 -4
- package/package.json +1 -1
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { statSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
4
|
+
import { join } from "node:path";
|
|
5
5
|
import { handlerFor } from "./handlers.js";
|
|
6
6
|
import { parseCommand } from "./headless.js";
|
|
7
|
-
|
|
8
|
-
try {
|
|
9
|
-
if (!statSync(p).isFile())
|
|
10
|
-
return false;
|
|
11
|
-
accessSync(p, constants.X_OK);
|
|
12
|
-
return true;
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
7
|
+
import { resolveEngineSpawn } from "./engine-spawn.js";
|
|
18
8
|
function defaultExists(p) {
|
|
19
9
|
try {
|
|
20
10
|
statSync(p);
|
|
@@ -29,28 +19,6 @@ function defaultKeychainItemStatus(service) {
|
|
|
29
19
|
const r = spawnSync("security", ["find-generic-password", "-s", service], { stdio: "ignore", timeout: 5_000 });
|
|
30
20
|
return r.error ? null : r.status;
|
|
31
21
|
}
|
|
32
|
-
/** The same resolution `spawn(bin)` does: a path is taken as-is, a bare name is searched on PATH. */
|
|
33
|
-
export function resolveBinary(bin, deps = {}) {
|
|
34
|
-
const isExec = deps.isExecutable ?? defaultIsExecutable;
|
|
35
|
-
if (!bin)
|
|
36
|
-
return null;
|
|
37
|
-
if (bin.includes("/") || isAbsolute(bin)) {
|
|
38
|
-
const p = resolve(bin);
|
|
39
|
-
return isExec(p) ? p : null;
|
|
40
|
-
}
|
|
41
|
-
const env = deps.env ?? process.env;
|
|
42
|
-
const exts = (deps.platform ?? process.platform) === "win32" ? (env.PATHEXT || ".EXE;.CMD;.BAT").split(";") : [""];
|
|
43
|
-
for (const dir of (env.PATH || "").split(delimiter)) {
|
|
44
|
-
if (!dir)
|
|
45
|
-
continue;
|
|
46
|
-
for (const ext of exts) {
|
|
47
|
-
const p = join(dir, bin + ext);
|
|
48
|
-
if (isExec(p))
|
|
49
|
-
return p;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
22
|
/** Does this engine have a stored interactive login on the machine? */
|
|
55
23
|
export function detectEngineLogin(engine, deps = {}) {
|
|
56
24
|
const env = deps.env ?? process.env;
|
|
@@ -100,14 +68,14 @@ export function checkEngine(input, deps = {}) {
|
|
|
100
68
|
const engine = (["claude", "gemini", "codex", "grok", "generic"].includes(input.clientType ?? "") ? input.clientType : "claude");
|
|
101
69
|
const parsed = parseCommand(input.command);
|
|
102
70
|
const bin = parsed.bin || parseCommand(handlerFor(engine).cliCommand).bin || engine;
|
|
103
|
-
const binaryPath =
|
|
71
|
+
const { binaryPath, via } = resolveEngineSpawn(bin, [], deps);
|
|
104
72
|
const login = input.credentialInjected ? { login: "found", where: "platform" } : detectEngineLogin(engine, deps);
|
|
105
73
|
return {
|
|
106
74
|
checked: true,
|
|
107
75
|
engine,
|
|
108
76
|
bin,
|
|
109
77
|
binaryFound: binaryPath !== null,
|
|
110
|
-
...(binaryPath ? { binaryPath } : {}),
|
|
78
|
+
...(binaryPath ? { binaryPath, binaryVia: via } : {}),
|
|
111
79
|
login: login.login,
|
|
112
80
|
...(login.where ? { loginWhere: login.where } : {}),
|
|
113
81
|
};
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* getting a raise. `engine-auth.ts` READS this env; this builds it.
|
|
8
8
|
*
|
|
9
9
|
* 1. {@link mergeEnv} — the platform's overlay over the machine's, where empty means REMOVE.
|
|
10
|
-
* 2. {@link engineSpawnEnv} — that, plus the `gh` guard ahead of the real binary on `PATH
|
|
10
|
+
* 2. {@link engineSpawnEnv} — that, plus the `gh` guard ahead of the real binary on `PATH`, and the
|
|
11
|
+
* `git` stash guard (#1021) ahead of both.
|
|
11
12
|
*/
|
|
12
13
|
import { ghGuardEnv } from "./gh-guard.js";
|
|
14
|
+
import { gitGuardEnv } from "./git-guard.js";
|
|
13
15
|
/**
|
|
14
16
|
* Merge the platform's resolved engine env over the machine's, where an EMPTY value means
|
|
15
17
|
* REMOVE rather than "set to empty".
|
|
@@ -41,6 +43,7 @@ export function mergeEnv(base, overlay) {
|
|
|
41
43
|
* `ghGuardEnv` returns its input untouched whenever the guard cannot be installed, so this is safe
|
|
42
44
|
* in front of every spawn: the failure mode is the machine's own `gh`, never a missing one.
|
|
43
45
|
*/
|
|
44
|
-
export function engineSpawnEnv(overlay, ghScope, ghGuardRoot) {
|
|
45
|
-
|
|
46
|
+
export function engineSpawnEnv(overlay, ghScope, ghGuardRoot, gitGuardRoot) {
|
|
47
|
+
// The git guard needs no scope: no session may restore or discard the stash by position (#1021).
|
|
48
|
+
return gitGuardEnv(ghGuardEnv(mergeEnv(process.env, overlay), ghScope, ghGuardRoot), gitGuardRoot);
|
|
46
49
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { accessSync, constants, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { posix, win32 } from "node:path";
|
|
3
|
+
function defaultIsExecutable(p) {
|
|
4
|
+
try {
|
|
5
|
+
if (!statSync(p).isFile())
|
|
6
|
+
return false;
|
|
7
|
+
accessSync(p, constants.X_OK);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function defaultReadText(p) {
|
|
15
|
+
try {
|
|
16
|
+
return readFileSync(p, "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Windows env names are case-insensitive, but a copied env object is not (`Path`, `ComSpec`). */
|
|
23
|
+
function envVar(env, name, windows) {
|
|
24
|
+
if (!windows)
|
|
25
|
+
return env[name];
|
|
26
|
+
const key = Object.keys(env).find((k) => k.toUpperCase() === name);
|
|
27
|
+
return key ? env[key] : undefined;
|
|
28
|
+
}
|
|
29
|
+
const isPathLike = (bin, windows) => bin.includes("/") || (windows ? bin.includes("\\") || win32.isAbsolute(bin) : posix.isAbsolute(bin));
|
|
30
|
+
/** The same resolution `spawn(bin)` does: a path is taken as-is, a bare name is searched on PATH (with PATHEXT on Windows). */
|
|
31
|
+
export function resolveBinary(bin, deps = {}) {
|
|
32
|
+
const isExec = deps.isExecutable ?? defaultIsExecutable;
|
|
33
|
+
if (!bin)
|
|
34
|
+
return null;
|
|
35
|
+
const windows = (deps.platform ?? process.platform) === "win32";
|
|
36
|
+
const path = windows ? win32 : posix;
|
|
37
|
+
if (isPathLike(bin, windows)) {
|
|
38
|
+
const p = path.resolve(bin);
|
|
39
|
+
return isExec(p) ? p : null;
|
|
40
|
+
}
|
|
41
|
+
const env = deps.env ?? process.env;
|
|
42
|
+
const exts = windows ? (envVar(env, "PATHEXT", true) || ".EXE;.CMD;.BAT").split(";") : [""];
|
|
43
|
+
for (const dir of (envVar(env, "PATH", windows) || "").split(path.delimiter)) {
|
|
44
|
+
if (!dir)
|
|
45
|
+
continue;
|
|
46
|
+
for (const ext of exts) {
|
|
47
|
+
const p = path.join(dir, bin + ext);
|
|
48
|
+
if (isExec(p))
|
|
49
|
+
return p;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/** The first `%dp0%\node_modules\…` target an npm (cmd-shim) `.cmd` launches, absolute. */
|
|
55
|
+
export function shimTarget(shimPath, text) {
|
|
56
|
+
const m = /"%~?dp0%?\\(node_modules\\[^"]+)"/i.exec(text);
|
|
57
|
+
return m ? win32.join(win32.dirname(shimPath), m[1]) : null;
|
|
58
|
+
}
|
|
59
|
+
// cmd.exe metacharacters (cross-spawn's set). A caret makes the next one literal for one parse.
|
|
60
|
+
const CMD_META = /([()\][%!^"`<>&|;, *?])/g;
|
|
61
|
+
/**
|
|
62
|
+
* One argument for `cmd.exe /s /c "<shim> <args>"`: quoted for the CRT's argv parser, then
|
|
63
|
+
* caret-escaped twice — once for cmd's own parse, once more for the shim's `%*` re-parse.
|
|
64
|
+
*/
|
|
65
|
+
export function quoteCmdArg(arg) {
|
|
66
|
+
let a = arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1");
|
|
67
|
+
a = `"${a}"`;
|
|
68
|
+
return a.replace(CMD_META, "^$1").replace(CMD_META, "^$1");
|
|
69
|
+
}
|
|
70
|
+
export function resolveEngineSpawn(bin, args, deps = {}) {
|
|
71
|
+
const windows = (deps.platform ?? process.platform) === "win32";
|
|
72
|
+
if (!windows)
|
|
73
|
+
return { command: bin, args, binaryPath: resolveBinary(bin, deps), via: "direct" };
|
|
74
|
+
const env = deps.env ?? process.env;
|
|
75
|
+
// Real executables first: PATHEXT order would otherwise pick `.CMD` over a later `.EXE` in no order we chose.
|
|
76
|
+
const exe = resolveBinary(bin, { ...deps, env: { ...env, PATHEXT: ".exe;.com" } });
|
|
77
|
+
const found = exe ?? resolveBinary(bin, { ...deps, env: { ...env, PATHEXT: ".cmd;.bat" } });
|
|
78
|
+
if (!found)
|
|
79
|
+
return { command: bin, args, binaryPath: null, via: "direct" };
|
|
80
|
+
if (!/\.(cmd|bat)$/i.test(found))
|
|
81
|
+
return { command: found, args, binaryPath: found, via: "direct" };
|
|
82
|
+
const isExec = deps.isExecutable ?? defaultIsExecutable;
|
|
83
|
+
const text = (deps.readText ?? defaultReadText)(found);
|
|
84
|
+
const target = text ? shimTarget(found, text) : null;
|
|
85
|
+
if (target && isExec(target)) {
|
|
86
|
+
if (/\.(exe|com)$/i.test(target))
|
|
87
|
+
return { command: target, args, binaryPath: target, via: "direct" };
|
|
88
|
+
if (/\.[cm]?js$/i.test(target))
|
|
89
|
+
return { command: deps.nodePath ?? process.execPath, args: [target, ...args], binaryPath: target, via: "node" };
|
|
90
|
+
}
|
|
91
|
+
const line = [found.replace(CMD_META, "^$1"), ...args.map(quoteCmdArg)].join(" ");
|
|
92
|
+
return {
|
|
93
|
+
command: envVar(env, "COMSPEC", true) || "cmd.exe",
|
|
94
|
+
args: ["/d", "/s", "/c", `"${line}"`],
|
|
95
|
+
windowsVerbatimArguments: true,
|
|
96
|
+
binaryPath: found,
|
|
97
|
+
via: "cmd",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Engine may not restore or discard the repo-global stash by POSITION (#1021).
|
|
3
|
+
*
|
|
4
|
+
* ── The incident
|
|
5
|
+
*
|
|
6
|
+
* Setting up a fail-before comparison, an Engine composed `SRC="a.ts b.ts"; git stash push -- $SRC;
|
|
7
|
+
* …; git stash pop`. zsh passed `$SRC` as ONE pathspec, so the save failed and created nothing — and
|
|
8
|
+
* the chained `pop` restored the top of the stack anyway: someone else's retained recovery stash,
|
|
9
|
+
* applied into the active checkout as merge conflicts. The stash is repo-global; `pop`, `drop` and
|
|
10
|
+
* `stash@{n}` name a position in everyone's stack, not the thing this command saved.
|
|
11
|
+
*
|
|
12
|
+
* The Pilot's `no_stash_restore` refusal (`workers/api/src/lib/coding-scope-constraints.ts`) only
|
|
13
|
+
* screens what the PILOT sends. The incident command was composed by the Engine itself, which
|
|
14
|
+
* runs with `--dangerously-skip-permissions`, so the only enforcement point it cannot talk its way
|
|
15
|
+
* past is the `git` it executes. This is that point.
|
|
16
|
+
*
|
|
17
|
+
* ── What the shim does
|
|
18
|
+
*
|
|
19
|
+
* A POSIX `sh` script named `git`, first on every Engine's `PATH` (all engines, every session —
|
|
20
|
+
* unlike the `gh` guard it needs no scope). For `git [global options] stash <action> …`:
|
|
21
|
+
*
|
|
22
|
+
* pop, drop, clear refused — each consumes or discards by position, and there is no form of
|
|
23
|
+
* them that names an exact object.
|
|
24
|
+
* apply, branch allowed only with an exact object id (40 or 64 hex) — what `git stash
|
|
25
|
+
* create` printed — so a restore is tied to an object this command made.
|
|
26
|
+
*
|
|
27
|
+
* Refusal exits {@link GIT_REFUSED_EXIT} BEFORE git runs, with {@link GIT_REFUSED_MARKER} on stderr.
|
|
28
|
+
* Everything else — including `git stash push`, `list`, `show`, `create` — is `exec`'d to the real
|
|
29
|
+
* git with its own exit status. Its tests run THIS text against real temporary repositories.
|
|
30
|
+
*
|
|
31
|
+
* ── What it does not stop, stated rather than hidden
|
|
32
|
+
*
|
|
33
|
+
* {@link GIT_GUARD_GAPS}: a PATH shim is bypassed by an absolute `/usr/bin/git` or a rewritten PATH,
|
|
34
|
+
* and a git alias that expands to `stash pop` is resolved inside git, past the shim.
|
|
35
|
+
*/
|
|
36
|
+
import { createHash } from "node:crypto";
|
|
37
|
+
import { accessSync, constants, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
38
|
+
import { homedir } from "node:os";
|
|
39
|
+
import { join } from "node:path";
|
|
40
|
+
/** Distinct from git's own 1/128/129, so a refusal is recognisable from its status alone. */
|
|
41
|
+
export const GIT_REFUSED_EXIT = 3;
|
|
42
|
+
export const GIT_REFUSED_MARKER = "pags: refused";
|
|
43
|
+
export const GIT_GUARD_GAPS = [
|
|
44
|
+
"A PATH shim is bypassable: invoking git by absolute path (/usr/bin/git), or rewriting PATH, skips it.",
|
|
45
|
+
"A git alias that expands to `stash pop`/`drop`/`clear` is expanded inside git, past the shim.",
|
|
46
|
+
];
|
|
47
|
+
export function gitGuardRoot() {
|
|
48
|
+
return join(homedir(), ".config", "proagentstore", "git-guard");
|
|
49
|
+
}
|
|
50
|
+
/** The first executable `git` on `path` that is not one of our own shims. */
|
|
51
|
+
export function findRealGit(path, guardRoot) {
|
|
52
|
+
for (const dir of (path ?? "").split(":")) {
|
|
53
|
+
if (!dir || dir.startsWith(guardRoot))
|
|
54
|
+
continue;
|
|
55
|
+
const candidate = join(dir, "git");
|
|
56
|
+
try {
|
|
57
|
+
accessSync(candidate, constants.X_OK);
|
|
58
|
+
return candidate;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* not here — keep looking */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const sq = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
67
|
+
export function renderGitShim(realGit) {
|
|
68
|
+
return `#!/bin/sh
|
|
69
|
+
# ProAgentStore git stash guard (#1021) — GENERATED. Source of truth:
|
|
70
|
+
# packages/browser-runner/src/coding/git-guard.ts. Editing this file lets an agent restore or discard
|
|
71
|
+
# someone else's stash; that is a bypass, not a configuration.
|
|
72
|
+
REAL=${sq(realGit)}
|
|
73
|
+
|
|
74
|
+
state=global; skip=0; sub=""; action=""; ref1=""; ref2=""
|
|
75
|
+
for a in "$@"; do
|
|
76
|
+
if [ "$skip" = 1 ]; then skip=0; continue; fi
|
|
77
|
+
if [ "$state" = global ]; then
|
|
78
|
+
case "$a" in
|
|
79
|
+
-C|-c|--git-dir|--work-tree|--namespace|--config-env|--super-prefix) skip=1 ;;
|
|
80
|
+
-*) : ;;
|
|
81
|
+
*) sub="$a"; state=args ;;
|
|
82
|
+
esac
|
|
83
|
+
continue
|
|
84
|
+
fi
|
|
85
|
+
[ "$sub" = stash ] || break
|
|
86
|
+
case "$a" in
|
|
87
|
+
-*) : ;;
|
|
88
|
+
*) if [ -z "$action" ]; then action="$a"; elif [ -z "$ref1" ]; then ref1="$a"; elif [ -z "$ref2" ]; then ref2="$a"; fi ;;
|
|
89
|
+
esac
|
|
90
|
+
done
|
|
91
|
+
|
|
92
|
+
refuse() {
|
|
93
|
+
echo "${GIT_REFUSED_MARKER}: \\\`git stash $action\\\` $1 The stash is shared by everyone working in this repository, so a position in it is not what you saved." >&2
|
|
94
|
+
echo "Restore only an exact object you just created: S=\\$(git stash create) && [ -n \\"\\$S\\" ] && … && git stash apply \\"\\$S\\" — or compare in an isolated \\\`git worktree add --detach\\\` checkout, which never touches the stash." >&2
|
|
95
|
+
exit ${GIT_REFUSED_EXIT}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
exact_object() {
|
|
99
|
+
case "$1" in ""|*[!0-9a-f]*) return 1 ;; esac
|
|
100
|
+
[ \${#1} -eq 40 ] || [ \${#1} -eq 64 ]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if [ "$sub" = stash ]; then
|
|
104
|
+
case "$action" in
|
|
105
|
+
pop) refuse "restores and DROPS the top of the stack, whoever saved it." ;;
|
|
106
|
+
drop) refuse "discards a stack entry by position." ;;
|
|
107
|
+
clear) refuse "discards every entry, including other people's recovery work." ;;
|
|
108
|
+
apply) exact_object "$ref1" || refuse "needs an exact object id, not a stack position (got '\${ref1:-top of stack}')." ;;
|
|
109
|
+
branch) exact_object "$ref2" || refuse "needs an exact object id, not a stack position (got '\${ref2:-top of stack}')." ;;
|
|
110
|
+
esac
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
exec "$REAL" "$@"
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
const installed = new Map();
|
|
117
|
+
/** Install (idempotently) the shim and return its directory, or null when it cannot be. */
|
|
118
|
+
export function installGitGuard(env, root = gitGuardRoot()) {
|
|
119
|
+
const realGit = findRealGit(env.PATH, root);
|
|
120
|
+
if (!realGit)
|
|
121
|
+
return null;
|
|
122
|
+
const script = renderGitShim(realGit);
|
|
123
|
+
const key = createHash("sha256").update(`${root} ${script}`).digest("hex").slice(0, 16);
|
|
124
|
+
const cached = installed.get(key);
|
|
125
|
+
if (cached)
|
|
126
|
+
return cached;
|
|
127
|
+
const dir = join(root, key);
|
|
128
|
+
try {
|
|
129
|
+
mkdirSync(dir, { recursive: true });
|
|
130
|
+
const shim = join(dir, "git");
|
|
131
|
+
if (!existsSync(shim))
|
|
132
|
+
writeFileSync(shim, script, { mode: 0o755 });
|
|
133
|
+
installed.set(key, dir);
|
|
134
|
+
return dir;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// The gh guard's rule: a runner that cannot write its own config dir still runs the Engine,
|
|
138
|
+
// with the machine's git — never with a `git` that does not exist.
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** `env` with the guard first on PATH; untouched when it could not be installed. */
|
|
143
|
+
export function gitGuardEnv(env, root) {
|
|
144
|
+
const dir = installGitGuard(env, root);
|
|
145
|
+
return dir ? { ...env, PATH: `${dir}:${env.PATH ?? ""}` } : env;
|
|
146
|
+
}
|
|
@@ -7,6 +7,7 @@ import { appendTurnTail, turnReportFromExit, turnReportFromResult } from "./engi
|
|
|
7
7
|
import { renderToolResult, shortInput, stripAnsi } from "./transcript-lines.js";
|
|
8
8
|
import { authoredTurn, authorTag } from "./turn-author.js";
|
|
9
9
|
import { engineSpawnEnv, mergeEnv } from "./engine-env.js";
|
|
10
|
+
import { resolveEngineSpawn } from "./engine-spawn.js";
|
|
10
11
|
import { ghGuardStatus } from "./gh-guard.js";
|
|
11
12
|
import { handlerFor } from "./handlers.js";
|
|
12
13
|
import { resolveEngineAuth } from "./engine-auth.js";
|
|
@@ -426,10 +427,13 @@ export class HeadlessSession {
|
|
|
426
427
|
this.inputs.newEngine();
|
|
427
428
|
this.buf = "";
|
|
428
429
|
const args = this.adapter.buildLaunchArgs(this.cmdArgs, this.claudeSessionId);
|
|
429
|
-
const
|
|
430
|
+
const env = this.spawnEnv;
|
|
431
|
+
const launch = resolveEngineSpawn(this.cmdBin, args, { env });
|
|
432
|
+
const proc = spawn(launch.command, launch.args, {
|
|
430
433
|
cwd: this.config.workDir,
|
|
431
|
-
env
|
|
434
|
+
env,
|
|
432
435
|
stdio: ["pipe", "pipe", "pipe"],
|
|
436
|
+
windowsVerbatimArguments: launch.windowsVerbatimArguments,
|
|
433
437
|
});
|
|
434
438
|
this.proc = proc;
|
|
435
439
|
this.run = "idle";
|
|
@@ -582,10 +586,13 @@ export class HeadlessSession {
|
|
|
582
586
|
const turnArgs = resumedCodexThreadId
|
|
583
587
|
? buildCodexResumeArgs(this.cmdArgs, resumedCodexThreadId, text)
|
|
584
588
|
: this.adapter.buildTurnArgs(this.cmdArgs, text);
|
|
585
|
-
const
|
|
589
|
+
const env = this.spawnEnv;
|
|
590
|
+
const launch = resolveEngineSpawn(this.cmdBin, turnArgs, { env });
|
|
591
|
+
const proc = spawn(launch.command, launch.args, {
|
|
586
592
|
cwd: this.config.workDir,
|
|
587
|
-
env
|
|
593
|
+
env,
|
|
588
594
|
stdio: ["ignore", "pipe", "pipe"],
|
|
595
|
+
windowsVerbatimArguments: launch.windowsVerbatimArguments,
|
|
589
596
|
});
|
|
590
597
|
// A turn already running is aborted before its replacement starts: `input()` accepts a turn
|
|
591
598
|
// at any time — a human typing in the console, a takeover, a Loop that stopped waiting —
|