@parall/codex-agent 1.18.1 → 1.18.3

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/config.d.ts CHANGED
@@ -10,20 +10,42 @@ export type CodexAgentConfig = {
10
10
  workspaceDir: string;
11
11
  model?: string;
12
12
  reasoningEffort?: string;
13
- sandbox: string;
13
+ /**
14
+ * Only set when the user explicitly specified `PRLL_CODEX_SANDBOX`.
15
+ * When undefined, the bridge deliberately does NOT pass `sandbox` on
16
+ * `thread/{start,resume}` so codex reads whatever the user configured
17
+ * in `~/.codex/config.toml` (`sandbox_mode`). This matches the self-
18
+ * hosted philosophy: user's codex setup is authoritative; bridge only
19
+ * overrides when the user asks it to.
20
+ */
21
+ sandbox?: string;
22
+ /**
23
+ * Approval policy is forced to `"never"` by default (not configurable
24
+ * via config.toml override): Parall's HITL contract routes approvals
25
+ * through the app, not codex's own stdin-prompt mechanism, which the
26
+ * long-running app-server subprocess has no way to answer.
27
+ */
14
28
  approvalPolicy: string;
15
29
  runtimeKey?: string;
16
30
  };
17
31
  export declare function resolveCodexAgentConfig(env?: NodeJS.ProcessEnv): CodexAgentConfig;
18
32
  /**
19
- * Normalise a sandbox value (CLI docs use kebab-case like `workspace-write`,
20
- * but the app-server JSON-RPC protocol uses camelCase like `workspaceWrite`).
21
- * Accept both forms and emit the canonical protocol form.
33
+ * Normalise a sandbox value to the kebab-case form the app-server JSON-RPC
34
+ * enum expects (`read-only` / `workspace-write` / `danger-full-access`).
35
+ * The CLI-facing PRLL_CODEX_SANDBOX env var may come in as either kebab-case
36
+ * or camelCase (from docs that cite the Rust field names); accept both.
37
+ *
38
+ * Empirically verified: the app-server rejects `workspaceWrite` with
39
+ * "unknown variant workspaceWrite, expected one of read-only,
40
+ * workspace-write, danger-full-access"
41
+ * which is the Rust enum's serde kebab-case form.
22
42
  */
23
43
  export declare function normalizeSandbox(value: string): string;
24
44
  /**
25
- * Normalise an approval policy string to the camelCase form the app-server
26
- * JSON-RPC protocol uses (`never` / `onRequest` / `unlessTrusted`).
45
+ * Normalise an approval policy string to the kebab-case form the app-server
46
+ * JSON-RPC enum expects (`never` / `on-request` / `on-failure` /
47
+ * `unless-trusted`). Mirrors the sandbox enum — serde rename_all kebab-case
48
+ * applies across app-server protocol enums.
27
49
  */
28
50
  export declare function normalizeApprovalPolicy(value: string): string;
29
51
  export declare function resolveWsUrl(apiUrl: string, explicitWsUrl?: string, swimlaneName?: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAcF,wBAAgB,uBAAuB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,gBAAgB,CA6B9F;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUtD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAW7D;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAMlG;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAG3F;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAGrF"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAcF,wBAAgB,uBAAuB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,gBAAgB,CAwC9F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUtD;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAW7D;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAMlG;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAG3F;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAGrF"}
package/dist/config.js CHANGED
@@ -14,11 +14,20 @@ export function resolveCodexAgentConfig(env = process.env) {
14
14
  const apiUrl = requireEnv(env, "PRLL_API_URL");
15
15
  const apiKey = requireEnv(env, "PRLL_API_KEY");
16
16
  const orgId = requireEnv(env, "PRLL_ORG_ID");
17
+ // CODEX_HOME is where codex looks for `auth.json` / `config.toml`. Its
18
+ // own default is `$HOME/.codex`; falling back to `$HOME` here was wrong
19
+ // — it made the spawned app-server subprocess look for
20
+ // `$HOME/auth.json`, which silently doesn't exist, so every
21
+ // `thread/start` failed with `401 Unauthorized` against the OpenAI
22
+ // WebSocket endpoint. Default to `$HOME/.codex` so `codex login`'s
23
+ // credentials are picked up transparently.
17
24
  const codexHome = resolvePath(env.PRLL_CODEX_HOME?.trim()
18
25
  || env.CODEX_HOME?.trim()
19
- || env.HOME
20
- || os.homedir());
21
- const stateDir = resolvePath(env.PRLL_CODEX_STATE_DIR?.trim() || path.join(codexHome, ".parall-agent"));
26
+ || path.join(env.HOME || os.homedir(), ".codex"));
27
+ // Bridge state lives outside CODEX_HOME to avoid polluting codex's own
28
+ // directory with non-codex files.
29
+ const stateDir = resolvePath(env.PRLL_CODEX_STATE_DIR?.trim()
30
+ || path.join(env.HOME || os.homedir(), ".parall-agent"));
22
31
  const workspaceDir = resolvePath(env.PRLL_CODEX_WORKSPACE_DIR?.trim() || path.join(stateDir, "workspace"));
23
32
  return {
24
33
  apiUrl,
@@ -32,40 +41,48 @@ export function resolveCodexAgentConfig(env = process.env) {
32
41
  workspaceDir,
33
42
  model: env.PRLL_CODEX_MODEL?.trim() || undefined,
34
43
  reasoningEffort: env.PRLL_CODEX_REASONING_EFFORT?.trim() || undefined,
35
- sandbox: env.PRLL_CODEX_SANDBOX?.trim() || "workspace-write",
44
+ sandbox: env.PRLL_CODEX_SANDBOX?.trim() || undefined,
36
45
  approvalPolicy: env.PRLL_CODEX_APPROVAL?.trim() || "never",
37
46
  runtimeKey: env.PRLL_CODEX_RUNTIME_KEY?.trim() || undefined,
38
47
  };
39
48
  }
40
49
  /**
41
- * Normalise a sandbox value (CLI docs use kebab-case like `workspace-write`,
42
- * but the app-server JSON-RPC protocol uses camelCase like `workspaceWrite`).
43
- * Accept both forms and emit the canonical protocol form.
50
+ * Normalise a sandbox value to the kebab-case form the app-server JSON-RPC
51
+ * enum expects (`read-only` / `workspace-write` / `danger-full-access`).
52
+ * The CLI-facing PRLL_CODEX_SANDBOX env var may come in as either kebab-case
53
+ * or camelCase (from docs that cite the Rust field names); accept both.
54
+ *
55
+ * Empirically verified: the app-server rejects `workspaceWrite` with
56
+ * "unknown variant workspaceWrite, expected one of read-only,
57
+ * workspace-write, danger-full-access"
58
+ * which is the Rust enum's serde kebab-case form.
44
59
  */
45
60
  export function normalizeSandbox(value) {
46
61
  const map = {
47
- "workspace-write": "workspaceWrite",
48
- "workspacewrite": "workspaceWrite",
49
- "danger-full-access": "dangerFullAccess",
50
- "dangerfullaccess": "dangerFullAccess",
51
- "read-only": "readOnly",
52
- "readonly": "readOnly",
62
+ "workspace-write": "workspace-write",
63
+ "workspacewrite": "workspace-write",
64
+ "danger-full-access": "danger-full-access",
65
+ "dangerfullaccess": "danger-full-access",
66
+ "read-only": "read-only",
67
+ "readonly": "read-only",
53
68
  };
54
69
  return map[value.toLowerCase()] ?? value;
55
70
  }
56
71
  /**
57
- * Normalise an approval policy string to the camelCase form the app-server
58
- * JSON-RPC protocol uses (`never` / `onRequest` / `unlessTrusted`).
72
+ * Normalise an approval policy string to the kebab-case form the app-server
73
+ * JSON-RPC enum expects (`never` / `on-request` / `on-failure` /
74
+ * `unless-trusted`). Mirrors the sandbox enum — serde rename_all kebab-case
75
+ * applies across app-server protocol enums.
59
76
  */
60
77
  export function normalizeApprovalPolicy(value) {
61
78
  const map = {
62
79
  "never": "never",
63
- "on-request": "onRequest",
64
- "onrequest": "onRequest",
65
- "unless-trusted": "unlessTrusted",
66
- "unlesstrusted": "unlessTrusted",
67
- "on-failure": "onFailure",
68
- "onfailure": "onFailure",
80
+ "on-request": "on-request",
81
+ "onrequest": "on-request",
82
+ "unless-trusted": "unless-trusted",
83
+ "unlesstrusted": "unless-trusted",
84
+ "on-failure": "on-failure",
85
+ "onfailure": "on-failure",
69
86
  };
70
87
  return map[value.toLowerCase()] ?? value;
71
88
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,aAAa,EACb,YAAY,EACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,KAAK,4BAA4B,GAAG,IAAI,CACtC,gBAAgB,EACd,gBAAgB,GAChB,UAAU,GACV,WAAW,GACX,OAAO,GACP,iBAAiB,GACjB,SAAS,GACT,cAAc,CACjB,GAAG;IACF,cAAc,EAAE,mBAAmB,CAAC;IACpC,GAAG,CAAC,EAAE,aAAa,CAAC;CACrB,CAAC;AAOF;;;;;;;;;;;;;GAaG;AACH,qBAAa,qBAAsB,YAAW,eAAe;IA6B/C,OAAO,CAAC,QAAQ,CAAC,IAAI;IA5BjC,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,IAAI,CAA+C;IAC3D,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IACtD,OAAO,CAAC,QAAQ,CAAS;IAEzB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;gBAYQ,IAAI,EAAE,4BAA4B;IAExD,QAAQ,CAAC,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IA0J3F,WAAW,CAAC,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAuBhG,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAIrC,OAAO,CAAC,cAAc;IAKhB,IAAI;YAaI,aAAa;YAcb,OAAO;IA2DrB,OAAO,CAAC,qBAAqB;YAiCf,UAAU;IAuCxB,OAAO,CAAC,iBAAiB;CA2B1B"}
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,aAAa,EACb,YAAY,EACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,KAAK,4BAA4B,GAAG,IAAI,CACtC,gBAAgB,EACd,gBAAgB,GAChB,UAAU,GACV,WAAW,GACX,OAAO,GACP,iBAAiB,GACjB,SAAS,GACT,cAAc,CACjB,GAAG;IACF,cAAc,EAAE,mBAAmB,CAAC;IACpC,GAAG,CAAC,EAAE,aAAa,CAAC;CACrB,CAAC;AAOF;;;;;;;;;;;;;GAaG;AACH,qBAAa,qBAAsB,YAAW,eAAe;IA6B/C,OAAO,CAAC,QAAQ,CAAC,IAAI;IA5BjC,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,IAAI,CAA+C;IAC3D,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IACtD,OAAO,CAAC,QAAQ,CAAS;IAEzB;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;gBAYQ,IAAI,EAAE,4BAA4B;IAExD,QAAQ,CAAC,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IA0J3F,WAAW,CAAC,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAAE,QAAQ,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAuBhG,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAIrC,OAAO,CAAC,cAAc;IAKhB,IAAI;YAaI,aAAa;YAcb,OAAO;IA2DrB,OAAO,CAAC,qBAAqB;YAiCf,UAAU;IAgDxB,OAAO,CAAC,iBAAiB;CA2B1B"}
package/dist/dispatch.js CHANGED
@@ -353,13 +353,22 @@ export class CodexAppServerAdapter {
353
353
  // model, reasoningEffort). Passing them on resume lets the user change
354
354
  // PRLL_CODEX_* env vars and have the bridge pick them up on the next
355
355
  // restart instead of being stuck on the values baked into the persisted
356
- // thread. Sandbox / approval values are normalised to the camelCase enum
357
- // the protocol expects (`workspaceWrite`, `onRequest`, etc.), so users
358
- // can supply either CLI-style or protocol-style env values.
356
+ // thread. Sandbox / approval values are normalised to the kebab-case
357
+ // enum form the protocol expects (`workspace-write`, `on-request`, etc.
358
+ // serde rename_all kebab-case on the Rust enum); users can supply
359
+ // either kebab or camel and we always emit kebab.
359
360
  const commonParams = {
360
361
  approvalPolicy: normalizeApprovalPolicy(this.opts.approvalPolicy),
361
- sandbox: normalizeSandbox(this.opts.sandbox),
362
362
  };
363
+ // Only override sandbox when the user explicitly set PRLL_CODEX_SANDBOX.
364
+ // Otherwise let codex read `sandbox_mode` from ~/.codex/config.toml — the
365
+ // user's own configuration is authoritative for a self-hosted bridge,
366
+ // and codex 0.120.0's workspace-write mode has a write_stdin bug that
367
+ // our previous `workspace-write` default tripped for users whose config
368
+ // had danger-full-access.
369
+ if (this.opts.sandbox) {
370
+ commonParams.sandbox = normalizeSandbox(this.opts.sandbox);
371
+ }
363
372
  if (this.opts.model)
364
373
  commonParams.model = this.opts.model;
365
374
  if (this.opts.reasoningEffort) {
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import * as os from "node:os";
3
- import { ParallAgentGateway } from "@parall/agent-core";
3
+ import { ParallAgentGateway, parseShutdownDeadlineMs } from "@parall/agent-core";
4
4
  import { ParallClient, ParallWs } from "@parall/sdk";
5
5
  import { buildCodexRuntimeKey, resolveCodexAgentConfig, resolveWsUrl, sessionStateFilePathForRuntime, stepIdFilePathForSession, } from "./config.js";
6
6
  import { CodexAppServerAdapter } from "./dispatch.js";
@@ -65,6 +65,7 @@ async function main() {
65
65
  },
66
66
  dispatchAdapter: adapter,
67
67
  log,
68
+ shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
68
69
  stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
69
70
  });
70
71
  const abortController = new AbortController();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/codex-agent",
3
- "version": "1.18.1",
3
+ "version": "1.18.3",
4
4
  "description": "Codex CLI bridge runtime for self-hosted Parall agents",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/config.ts CHANGED
@@ -13,7 +13,21 @@ export type CodexAgentConfig = {
13
13
  workspaceDir: string;
14
14
  model?: string;
15
15
  reasoningEffort?: string;
16
- sandbox: string;
16
+ /**
17
+ * Only set when the user explicitly specified `PRLL_CODEX_SANDBOX`.
18
+ * When undefined, the bridge deliberately does NOT pass `sandbox` on
19
+ * `thread/{start,resume}` so codex reads whatever the user configured
20
+ * in `~/.codex/config.toml` (`sandbox_mode`). This matches the self-
21
+ * hosted philosophy: user's codex setup is authoritative; bridge only
22
+ * overrides when the user asks it to.
23
+ */
24
+ sandbox?: string;
25
+ /**
26
+ * Approval policy is forced to `"never"` by default (not configurable
27
+ * via config.toml override): Parall's HITL contract routes approvals
28
+ * through the app, not codex's own stdin-prompt mechanism, which the
29
+ * long-running app-server subprocess has no way to answer.
30
+ */
17
31
  approvalPolicy: string;
18
32
  runtimeKey?: string;
19
33
  };
@@ -34,13 +48,24 @@ export function resolveCodexAgentConfig(env: NodeJS.ProcessEnv = process.env): C
34
48
  const apiUrl = requireEnv(env, "PRLL_API_URL");
35
49
  const apiKey = requireEnv(env, "PRLL_API_KEY");
36
50
  const orgId = requireEnv(env, "PRLL_ORG_ID");
51
+ // CODEX_HOME is where codex looks for `auth.json` / `config.toml`. Its
52
+ // own default is `$HOME/.codex`; falling back to `$HOME` here was wrong
53
+ // — it made the spawned app-server subprocess look for
54
+ // `$HOME/auth.json`, which silently doesn't exist, so every
55
+ // `thread/start` failed with `401 Unauthorized` against the OpenAI
56
+ // WebSocket endpoint. Default to `$HOME/.codex` so `codex login`'s
57
+ // credentials are picked up transparently.
37
58
  const codexHome = resolvePath(
38
59
  env.PRLL_CODEX_HOME?.trim()
39
60
  || env.CODEX_HOME?.trim()
40
- || env.HOME
41
- || os.homedir(),
61
+ || path.join(env.HOME || os.homedir(), ".codex"),
62
+ );
63
+ // Bridge state lives outside CODEX_HOME to avoid polluting codex's own
64
+ // directory with non-codex files.
65
+ const stateDir = resolvePath(
66
+ env.PRLL_CODEX_STATE_DIR?.trim()
67
+ || path.join(env.HOME || os.homedir(), ".parall-agent"),
42
68
  );
43
- const stateDir = resolvePath(env.PRLL_CODEX_STATE_DIR?.trim() || path.join(codexHome, ".parall-agent"));
44
69
  const workspaceDir = resolvePath(env.PRLL_CODEX_WORKSPACE_DIR?.trim() || path.join(stateDir, "workspace"));
45
70
 
46
71
  return {
@@ -55,42 +80,50 @@ export function resolveCodexAgentConfig(env: NodeJS.ProcessEnv = process.env): C
55
80
  workspaceDir,
56
81
  model: env.PRLL_CODEX_MODEL?.trim() || undefined,
57
82
  reasoningEffort: env.PRLL_CODEX_REASONING_EFFORT?.trim() || undefined,
58
- sandbox: env.PRLL_CODEX_SANDBOX?.trim() || "workspace-write",
83
+ sandbox: env.PRLL_CODEX_SANDBOX?.trim() || undefined,
59
84
  approvalPolicy: env.PRLL_CODEX_APPROVAL?.trim() || "never",
60
85
  runtimeKey: env.PRLL_CODEX_RUNTIME_KEY?.trim() || undefined,
61
86
  };
62
87
  }
63
88
 
64
89
  /**
65
- * Normalise a sandbox value (CLI docs use kebab-case like `workspace-write`,
66
- * but the app-server JSON-RPC protocol uses camelCase like `workspaceWrite`).
67
- * Accept both forms and emit the canonical protocol form.
90
+ * Normalise a sandbox value to the kebab-case form the app-server JSON-RPC
91
+ * enum expects (`read-only` / `workspace-write` / `danger-full-access`).
92
+ * The CLI-facing PRLL_CODEX_SANDBOX env var may come in as either kebab-case
93
+ * or camelCase (from docs that cite the Rust field names); accept both.
94
+ *
95
+ * Empirically verified: the app-server rejects `workspaceWrite` with
96
+ * "unknown variant workspaceWrite, expected one of read-only,
97
+ * workspace-write, danger-full-access"
98
+ * which is the Rust enum's serde kebab-case form.
68
99
  */
69
100
  export function normalizeSandbox(value: string): string {
70
101
  const map: Record<string, string> = {
71
- "workspace-write": "workspaceWrite",
72
- "workspacewrite": "workspaceWrite",
73
- "danger-full-access": "dangerFullAccess",
74
- "dangerfullaccess": "dangerFullAccess",
75
- "read-only": "readOnly",
76
- "readonly": "readOnly",
102
+ "workspace-write": "workspace-write",
103
+ "workspacewrite": "workspace-write",
104
+ "danger-full-access": "danger-full-access",
105
+ "dangerfullaccess": "danger-full-access",
106
+ "read-only": "read-only",
107
+ "readonly": "read-only",
77
108
  };
78
109
  return map[value.toLowerCase()] ?? value;
79
110
  }
80
111
 
81
112
  /**
82
- * Normalise an approval policy string to the camelCase form the app-server
83
- * JSON-RPC protocol uses (`never` / `onRequest` / `unlessTrusted`).
113
+ * Normalise an approval policy string to the kebab-case form the app-server
114
+ * JSON-RPC enum expects (`never` / `on-request` / `on-failure` /
115
+ * `unless-trusted`). Mirrors the sandbox enum — serde rename_all kebab-case
116
+ * applies across app-server protocol enums.
84
117
  */
85
118
  export function normalizeApprovalPolicy(value: string): string {
86
119
  const map: Record<string, string> = {
87
120
  "never": "never",
88
- "on-request": "onRequest",
89
- "onrequest": "onRequest",
90
- "unless-trusted": "unlessTrusted",
91
- "unlesstrusted": "unlessTrusted",
92
- "on-failure": "onFailure",
93
- "onfailure": "onFailure",
121
+ "on-request": "on-request",
122
+ "onrequest": "on-request",
123
+ "unless-trusted": "unless-trusted",
124
+ "unlesstrusted": "unless-trusted",
125
+ "on-failure": "on-failure",
126
+ "onfailure": "on-failure",
94
127
  };
95
128
  return map[value.toLowerCase()] ?? value;
96
129
  }
package/src/dispatch.ts CHANGED
@@ -396,13 +396,22 @@ export class CodexAppServerAdapter implements DispatchAdapter {
396
396
  // model, reasoningEffort). Passing them on resume lets the user change
397
397
  // PRLL_CODEX_* env vars and have the bridge pick them up on the next
398
398
  // restart instead of being stuck on the values baked into the persisted
399
- // thread. Sandbox / approval values are normalised to the camelCase enum
400
- // the protocol expects (`workspaceWrite`, `onRequest`, etc.), so users
401
- // can supply either CLI-style or protocol-style env values.
399
+ // thread. Sandbox / approval values are normalised to the kebab-case
400
+ // enum form the protocol expects (`workspace-write`, `on-request`, etc.
401
+ // serde rename_all kebab-case on the Rust enum); users can supply
402
+ // either kebab or camel and we always emit kebab.
402
403
  const commonParams: Record<string, unknown> = {
403
404
  approvalPolicy: normalizeApprovalPolicy(this.opts.approvalPolicy),
404
- sandbox: normalizeSandbox(this.opts.sandbox),
405
405
  };
406
+ // Only override sandbox when the user explicitly set PRLL_CODEX_SANDBOX.
407
+ // Otherwise let codex read `sandbox_mode` from ~/.codex/config.toml — the
408
+ // user's own configuration is authoritative for a self-hosted bridge,
409
+ // and codex 0.120.0's workspace-write mode has a write_stdin bug that
410
+ // our previous `workspace-write` default tripped for users whose config
411
+ // had danger-full-access.
412
+ if (this.opts.sandbox) {
413
+ commonParams.sandbox = normalizeSandbox(this.opts.sandbox);
414
+ }
406
415
  if (this.opts.model) commonParams.model = this.opts.model;
407
416
  if (this.opts.reasoningEffort) {
408
417
  // The app-server JSON-RPC surface uses camelCase for overrides —
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import * as os from "node:os";
4
- import { ParallAgentGateway } from "@parall/agent-core";
4
+ import { ParallAgentGateway, parseShutdownDeadlineMs } from "@parall/agent-core";
5
5
  import { ParallClient, ParallWs } from "@parall/sdk";
6
6
  import {
7
7
  buildCodexRuntimeKey,
@@ -83,6 +83,7 @@ async function main() {
83
83
  },
84
84
  dispatchAdapter: adapter,
85
85
  log,
86
+ shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
86
87
  stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
87
88
  });
88
89