@parall/codex-agent 1.25.0 → 1.26.1

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
@@ -11,14 +11,14 @@ export type CodexAgentConfig = {
11
11
  model?: string;
12
12
  reasoningEffort?: string;
13
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.
14
+ * Sandbox mode passed to codex on `thread/{start,resume}`. Defaults to
15
+ * `danger-full-access` same as the hosted container because codex's
16
+ * inner bubblewrap sandbox requires unprivileged user namespaces that
17
+ * most environments lack, and the agent needs unrestricted shell access
18
+ * (e.g. `parall messages send`). Users can still override via
19
+ * `PRLL_CODEX_SANDBOX` env var.
20
20
  */
21
- sandbox?: string;
21
+ sandbox: string;
22
22
  /**
23
23
  * Approval policy is forced to `"never"` by default (not configurable
24
24
  * via config.toml override): Parall's HITL contract routes approvals
@@ -51,5 +51,6 @@ export declare function normalizeApprovalPolicy(value: string): string;
51
51
  export declare function resolveWsUrl(apiUrl: string, explicitWsUrl?: string, swimlaneName?: string): string;
52
52
  export declare function buildCodexRuntimeKey(agentUserId: string): string;
53
53
  export declare function sessionStateFilePathForRuntime(stateDir: string, runtimeKey: string): string;
54
+ export declare function contextFilePathForSession(stateDir: string, sessionKey: string): string;
54
55
  export declare function stepIdFilePathForSession(stateDir: string, sessionKey: string): string;
55
56
  //# sourceMappingURL=config.d.ts.map
@@ -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;;;;;;;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"}
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,EAAE,MAAM,CAAC;IAChB;;;;;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,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAGtF;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAGrF"}
package/dist/config.js CHANGED
@@ -41,7 +41,7 @@ export function resolveCodexAgentConfig(env = process.env) {
41
41
  workspaceDir,
42
42
  model: env.PRLL_CODEX_MODEL?.trim() || undefined,
43
43
  reasoningEffort: env.PRLL_CODEX_REASONING_EFFORT?.trim() || undefined,
44
- sandbox: env.PRLL_CODEX_SANDBOX?.trim() || undefined,
44
+ sandbox: env.PRLL_CODEX_SANDBOX?.trim() || "danger-full-access",
45
45
  approvalPolicy: env.PRLL_CODEX_APPROVAL?.trim() || "never",
46
46
  runtimeKey: env.PRLL_CODEX_RUNTIME_KEY?.trim() || undefined,
47
47
  };
@@ -101,6 +101,10 @@ export function sessionStateFilePathForRuntime(stateDir, runtimeKey) {
101
101
  const fileName = Buffer.from(runtimeKey).toString("base64url");
102
102
  return path.join(stateDir, "threads", `${fileName}.json`);
103
103
  }
104
+ export function contextFilePathForSession(stateDir, sessionKey) {
105
+ const fileName = Buffer.from(sessionKey).toString("base64url");
106
+ return path.join(stateDir, "dispatch-context", `${fileName}.json`);
107
+ }
104
108
  export function stepIdFilePathForSession(stateDir, sessionKey) {
105
109
  const fileName = Buffer.from(sessionKey).toString("base64url");
106
110
  return path.join(stateDir, "step-ids", `${fileName}.txt`);
@@ -4,6 +4,7 @@ import type { CodexSessionManager } from "./session-manager.js";
4
4
  type CodexAppServerAdapterOptions = Pick<CodexAgentConfig, "approvalPolicy" | "codexBin" | "codexHome" | "model" | "reasoningEffort" | "sandbox" | "workspaceDir"> & {
5
5
  sessionManager: CodexSessionManager;
6
6
  log?: GatewayLogger;
7
+ contextFilePath?: string;
7
8
  };
8
9
  /**
9
10
  * Bridge driver backed by `codex app-server --listen stdio://`.
@@ -38,7 +39,12 @@ export declare class CodexAppServerAdapter implements DispatchAdapter {
38
39
  */
39
40
  private setActiveTurn;
40
41
  constructor(opts: CodexAppServerAdapterOptions);
42
+ updateConfig(config: {
43
+ model?: string | null;
44
+ reasoningEffort?: string | null;
45
+ }): void;
41
46
  dispatch({ event, bodyForAgent, sessionKey, context }: DispatchOpts): AsyncIterable<RuntimeEvent>;
47
+ getBranchPoint(_sessionKey: string): string | undefined;
42
48
  forkSession({ sessionKey: parentSessionKey }: ForkOpts): Promise<ForkSessionHandle | null>;
43
49
  cleanupFork({ fork }: CleanupForkOpts): void;
44
50
  private logForkFailure;
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAUA,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,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IAgLlG,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"}
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAUA,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;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,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;IAE/D,YAAY,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI;IAK/E,QAAQ,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IAgLxG,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMjD,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;IA8DrB,OAAO,CAAC,qBAAqB;YAiCf,UAAU;IAwCxB,OAAO,CAAC,iBAAiB;CA2B1B"}
package/dist/dispatch.js CHANGED
@@ -49,6 +49,12 @@ export class CodexAppServerAdapter {
49
49
  constructor(opts) {
50
50
  this.opts = opts;
51
51
  }
52
+ updateConfig(config) {
53
+ if (config.model !== undefined)
54
+ this.opts.model = config.model ?? undefined;
55
+ if (config.reasoningEffort !== undefined)
56
+ this.opts.reasoningEffort = config.reasoningEffort ?? undefined;
57
+ }
52
58
  async *dispatch({ event, bodyForAgent, sessionKey, context }) {
53
59
  await this.ensureStarted(context.log);
54
60
  // After ensureStarted resolves the subprocess could still die before we
@@ -226,6 +232,11 @@ export class CodexAppServerAdapter {
226
232
  }
227
233
  }
228
234
  }
235
+ getBranchPoint(_sessionKey) {
236
+ // Codex app-server does not expose a branch-point API; fork scope prefix
237
+ // provides the behavioral fallback for this runtime.
238
+ return undefined;
239
+ }
229
240
  async forkSession({ sessionKey: parentSessionKey }) {
230
241
  const client = this.client;
231
242
  if (!client)
@@ -304,6 +315,9 @@ export class CodexAppServerAdapter {
304
315
  FORCE_COLOR: "0",
305
316
  NO_COLOR: "1",
306
317
  };
318
+ if (this.opts.contextFilePath) {
319
+ env.PRLL_CONTEXT_FILE = this.opts.contextFilePath;
320
+ }
307
321
  const args = ["app-server", "--listen", "stdio://"];
308
322
  (log ?? this.opts.log)?.info?.(`codex-agent: spawning ${this.opts.codexBin} ${args.join(" ")}`);
309
323
  const proc = spawn(this.opts.codexBin, args, {
@@ -382,15 +396,7 @@ export class CodexAppServerAdapter {
382
396
  const commonParams = {
383
397
  approvalPolicy: normalizeApprovalPolicy(this.opts.approvalPolicy),
384
398
  };
385
- // Only override sandbox when the user explicitly set PRLL_CODEX_SANDBOX.
386
- // Otherwise let codex read `sandbox_mode` from ~/.codex/config.toml — the
387
- // user's own configuration is authoritative for a self-hosted bridge,
388
- // and codex 0.120.0's workspace-write mode has a write_stdin bug that
389
- // our previous `workspace-write` default tripped for users whose config
390
- // had danger-full-access.
391
- if (this.opts.sandbox) {
392
- commonParams.sandbox = normalizeSandbox(this.opts.sandbox);
393
- }
399
+ commonParams.sandbox = normalizeSandbox(this.opts.sandbox);
394
400
  if (this.opts.model)
395
401
  commonParams.model = this.opts.model;
396
402
  if (this.opts.reasoningEffort) {
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import * as os from "node:os";
3
- import { ParallAgentGateway, parseShutdownDeadlineMs } from "@parall/agent-core";
3
+ import { ParallAgentGateway, createPlatformConfigManager, isPlatformManagedProfile, parseShutdownDeadlineMs } from "@parall/agent-core";
4
4
  import { ApiError, ParallClient, ParallWs } from "@parall/sdk";
5
- import { buildCodexRuntimeKey, resolveCodexAgentConfig, resolveWsUrl, sessionStateFilePathForRuntime, stepIdFilePathForSession, } from "./config.js";
5
+ import { buildCodexRuntimeKey, contextFilePathForSession, resolveCodexAgentConfig, resolveWsUrl, sessionStateFilePathForRuntime, stepIdFilePathForSession, } from "./config.js";
6
6
  import { CodexAppServerAdapter } from "./dispatch.js";
7
7
  import { CodexSessionManager } from "./session-manager.js";
8
- import { ensureCodexWorkspace } from "./workspace.js";
8
+ import { ensureCodexWorkspace, ensureWorkspaceTrusted } from "./workspace.js";
9
9
  function createLogger(prefix) {
10
10
  return {
11
11
  info: (msg) => console.log(`[${prefix}] ${msg}`),
@@ -35,12 +35,14 @@ async function main() {
35
35
  });
36
36
  const me = await getAgentMeWithLegacyFallback(client, config.orgId);
37
37
  const agentUserId = me.id;
38
+ ensureWorkspaceTrusted(config.codexHome, config.workspaceDir, log);
38
39
  ensureCodexWorkspace(config.workspaceDir, log, {
39
40
  userId: agentUserId,
40
41
  displayName: me.display_name,
41
42
  description: me.agent_profile?.description ?? undefined,
42
43
  });
43
44
  const runtimeKey = config.runtimeKey || buildCodexRuntimeKey(agentUserId);
45
+ const mainContextFilePath = contextFilePathForSession(config.stateDir, runtimeKey);
44
46
  const sessionStateFilePath = sessionStateFilePathForRuntime(config.stateDir, runtimeKey);
45
47
  const sessionManager = new CodexSessionManager(runtimeKey, sessionStateFilePath, log);
46
48
  const resolvedWsUrl = resolveWsUrl(config.apiUrl, config.wsUrl, config.swimlaneName);
@@ -48,16 +50,26 @@ async function main() {
48
50
  getTicket: () => client.getWsTicket(),
49
51
  wsUrl: resolvedWsUrl,
50
52
  });
53
+ const configMgr = createPlatformConfigManager({ client, stateDir: config.stateDir, runtimeType: "codex", log });
54
+ const platformDefaults = await configMgr.fetch();
55
+ let platformManaged = isPlatformManagedProfile(me.agent_profile);
56
+ const resolvedModel = platformManaged ? (platformDefaults.model ?? config.model) : config.model;
57
+ const resolvedEffort = platformManaged ? (platformDefaults.thinkingEffort ?? config.reasoningEffort) : config.reasoningEffort;
58
+ if (platformDefaults.model)
59
+ log.info(`platform config: model=${platformDefaults.model} (${platformManaged ? "applied" : "ignored, model_management=self"})`);
60
+ if (platformDefaults.thinkingEffort)
61
+ log.info(`platform config: reasoning_effort=${platformDefaults.thinkingEffort} (${platformManaged ? "applied" : "ignored, model_management=self"})`);
51
62
  const adapter = new CodexAppServerAdapter({
52
63
  codexBin: config.codexBin,
53
64
  codexHome: config.codexHome,
54
65
  workspaceDir: config.workspaceDir,
55
- model: config.model,
56
- reasoningEffort: config.reasoningEffort,
66
+ model: resolvedModel,
67
+ reasoningEffort: resolvedEffort,
57
68
  sandbox: config.sandbox,
58
69
  approvalPolicy: config.approvalPolicy,
59
70
  sessionManager,
60
71
  log,
72
+ contextFilePath: mainContextFilePath,
61
73
  });
62
74
  const gateway = new ParallAgentGateway({
63
75
  accountId: agentUserId,
@@ -82,7 +94,26 @@ async function main() {
82
94
  dispatchAdapter: adapter,
83
95
  log,
84
96
  shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
97
+ contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
85
98
  stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
99
+ onConfigUpdate: async () => {
100
+ const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
101
+ platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
102
+ const updated = await configMgr.fetch();
103
+ adapter.updateConfig({
104
+ model: platformManaged ? (updated.model ?? config.model) : (config.model ?? null),
105
+ reasoningEffort: platformManaged ? (updated.thinkingEffort ?? config.reasoningEffort) : (config.reasoningEffort ?? null),
106
+ });
107
+ },
108
+ onSessionReady: async () => {
109
+ const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
110
+ platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
111
+ const updated = await configMgr.fetch();
112
+ adapter.updateConfig({
113
+ model: platformManaged ? (updated.model ?? config.model) : (config.model ?? null),
114
+ reasoningEffort: platformManaged ? (updated.thinkingEffort ?? config.reasoningEffort) : (config.reasoningEffort ?? null),
115
+ });
116
+ },
86
117
  });
87
118
  const abortController = new AbortController();
88
119
  const abort = () => abortController.abort();
@@ -1,4 +1,17 @@
1
1
  import type { AgentIdentity } from "@parall/agent-core";
2
+ /**
3
+ * Ensure the workspace directory is marked as trusted in the global Codex
4
+ * config so that project-level `developer_instructions` are loaded at
5
+ * app-server startup.
6
+ *
7
+ * Uses smol-toml for structured reads (avoids substring false-positives),
8
+ * but writes via raw text manipulation (preserves comments and formatting).
9
+ * Failures are swallowed (warn-only) so a trust write issue never blocks
10
+ * the bridge from starting.
11
+ */
12
+ export declare function ensureWorkspaceTrusted(codexHome: string, workspaceDir: string, log?: {
13
+ warn: (msg: string) => void;
14
+ }): void;
2
15
  export declare function ensureCodexWorkspace(workspaceDir: string, log?: {
3
16
  warn: (msg: string) => void;
4
17
  }, agentIdentity?: AgentIdentity): void;
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,EACpB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,EACrC,aAAa,CAAC,EAAE,aAAa,QA+B9B"}
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GACpC,IAAI,CAyEN;AASD,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,EACpB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,EACrC,aAAa,CAAC,EAAE,aAAa,GAC5B,IAAI,CAqBN"}
package/dist/workspace.js CHANGED
@@ -1,32 +1,117 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { BRIDGE_WORKSPACE_INSTRUCTIONS, PRLL_BEHAVIOR, PRLL_REFERENCE_GUIDE, buildIdentity, } from "@parall/agent-core";
3
+ import { parse as parseToml } from "smol-toml";
4
+ import { BRIDGE_WORKSPACE_INSTRUCTIONS, PRLL_BEHAVIOR, PRLL_REFERENCE_GUIDE, buildIdentity, buildSkillReferences, writeSkillFiles, } from "@parall/agent-core";
5
+ /**
6
+ * Ensure the workspace directory is marked as trusted in the global Codex
7
+ * config so that project-level `developer_instructions` are loaded at
8
+ * app-server startup.
9
+ *
10
+ * Uses smol-toml for structured reads (avoids substring false-positives),
11
+ * but writes via raw text manipulation (preserves comments and formatting).
12
+ * Failures are swallowed (warn-only) so a trust write issue never blocks
13
+ * the bridge from starting.
14
+ */
15
+ export function ensureWorkspaceTrusted(codexHome, workspaceDir, log) {
16
+ const configPath = path.join(codexHome, "config.toml");
17
+ const normalizedPath = path.resolve(workspaceDir);
18
+ try {
19
+ let content = "";
20
+ try {
21
+ content = fs.readFileSync(configPath, "utf8");
22
+ }
23
+ catch (err) {
24
+ if (err.code !== "ENOENT")
25
+ throw err;
26
+ }
27
+ // Structured read — know exactly what state we're in.
28
+ let parsed;
29
+ if (content) {
30
+ try {
31
+ parsed = parseToml(content);
32
+ }
33
+ catch {
34
+ // File is already broken TOML — don't make it worse.
35
+ log?.warn(`Codex config.toml is not valid TOML; skipping trust write for ${normalizedPath}`);
36
+ return;
37
+ }
38
+ }
39
+ const projects = parsed?.projects;
40
+ if (projects?.[normalizedPath]?.trust_level === "trusted")
41
+ return;
42
+ // TOML basic-string keys require backslash and double-quote escaping.
43
+ const escapedPath = normalizedPath.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
44
+ const sectionHeader = `[projects."${escapedPath}"]`;
45
+ const trustLine = 'trust_level = "trusted"';
46
+ const headerIdx = content.indexOf(sectionHeader);
47
+ if (headerIdx !== -1) {
48
+ // Section exists but trust_level isn't "trusted". Scan all lines
49
+ // within the section (up to the next `[` header or EOF) for an
50
+ // existing trust_level key — it may not be the first line after
51
+ // the header if the user added comments or other keys.
52
+ const headerLineEnd = content.indexOf("\n", headerIdx);
53
+ if (headerLineEnd === -1) {
54
+ content = `${content}\n${trustLine}\n`;
55
+ }
56
+ else {
57
+ const afterHeader = headerLineEnd + 1;
58
+ const sectionEnd = findSectionEnd(content, afterHeader);
59
+ const sectionBody = content.substring(afterHeader, sectionEnd);
60
+ const trustMatch = sectionBody.match(/^[ \t]*trust_level[ \t]*=.*$/m);
61
+ if (trustMatch) {
62
+ const matchStart = afterHeader + trustMatch.index;
63
+ const matchEnd = matchStart + trustMatch[0].length;
64
+ content = content.substring(0, matchStart) + trustLine + content.substring(matchEnd);
65
+ }
66
+ else {
67
+ content = content.substring(0, afterHeader) + trustLine + "\n" + content.substring(afterHeader);
68
+ }
69
+ }
70
+ fs.writeFileSync(configPath, content, "utf8");
71
+ }
72
+ else if (projects?.[normalizedPath] !== undefined) {
73
+ // smol-toml found the section but indexOf missed it — the header
74
+ // uses non-canonical TOML formatting. Appending would create a
75
+ // duplicate table. Skip rather than corrupt the file.
76
+ log?.warn(`Codex config.toml has non-canonical header for ${normalizedPath}; skipping trust write`);
77
+ }
78
+ else if (projects && !content.includes("[projects.")) {
79
+ // `projects` exists in parsed output but no `[projects.` table
80
+ // headers in the raw text — it's an inline table. Appending a
81
+ // standard table header would produce invalid TOML.
82
+ log?.warn(`Codex config.toml uses inline table for projects; skipping trust write for ${normalizedPath}`);
83
+ }
84
+ else {
85
+ // Section doesn't exist — append.
86
+ fs.mkdirSync(codexHome, { recursive: true });
87
+ fs.appendFileSync(configPath, `\n${sectionHeader}\n${trustLine}\n`, "utf8");
88
+ }
89
+ }
90
+ catch (err) {
91
+ log?.warn(`failed to write Codex project trust for ${normalizedPath}: ${String(err)}`);
92
+ }
93
+ }
94
+ function findSectionEnd(content, fromIndex) {
95
+ // A TOML section ends at the next `[` that starts a new table header.
96
+ // Match `[` at the beginning of a line (not inside a value).
97
+ const nextHeader = content.indexOf("\n[", fromIndex);
98
+ return nextHeader === -1 ? content.length : nextHeader;
99
+ }
4
100
  export function ensureCodexWorkspace(workspaceDir, log, agentIdentity) {
5
- const AGENTS_MD = [
101
+ const systemPrompt = [
6
102
  buildIdentity(agentIdentity),
7
103
  BRIDGE_WORKSPACE_INSTRUCTIONS,
8
104
  PRLL_BEHAVIOR,
9
105
  PRLL_REFERENCE_GUIDE,
106
+ buildSkillReferences(workspaceDir),
10
107
  ].join("\n\n");
11
108
  fs.mkdirSync(workspaceDir, { recursive: true });
12
- // AGENTS.md is bridge-managed: always overwrite so the workspace picks up
13
- // updated guardrails / command templates on upgrade. Operators must not
14
- // hand-edit this file — workspace customisations should go elsewhere in
15
- // the Codex home dir. Surface a warning when we replace divergent content
16
- // so an operator who did edit it locally sees a signal instead of silently
17
- // losing changes. Mirrors the Claude bridge behaviour.
18
- const agentsMdPath = path.join(workspaceDir, "AGENTS.md");
19
- if (log) {
20
- try {
21
- const existing = fs.readFileSync(agentsMdPath, "utf8");
22
- if (existing !== AGENTS_MD) {
23
- log.warn(`codex-agent: overwriting divergent ${agentsMdPath} with bridge-managed template ` +
24
- `(local edits to AGENTS.md are not preserved — customize other files in the workspace instead)`);
25
- }
26
- }
27
- catch {
28
- // file missing or unreadable — first-boot case, no warning needed
29
- }
30
- }
31
- fs.writeFileSync(agentsMdPath, AGENTS_MD, "utf8");
109
+ const parallDir = path.join(workspaceDir, ".parall");
110
+ fs.mkdirSync(parallDir, { recursive: true });
111
+ fs.writeFileSync(path.join(parallDir, "system-prompt.md"), systemPrompt, "utf8");
112
+ writeSkillFiles(path.join(parallDir, "skills"));
113
+ const codexConfigDir = path.join(workspaceDir, ".codex");
114
+ fs.mkdirSync(codexConfigDir, { recursive: true });
115
+ const toml = `developer_instructions = """\n${systemPrompt.replace(/\\/g, "\\\\").replace(/"""/g, '\\"""')}\n"""\n`;
116
+ fs.writeFileSync(path.join(codexConfigDir, "config.toml"), toml, "utf8");
32
117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/codex-agent",
3
- "version": "1.25.0",
3
+ "version": "1.26.1",
4
4
  "description": "Codex CLI bridge runtime for self-hosted Parall agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,9 +25,10 @@
25
25
  "src"
26
26
  ],
27
27
  "dependencies": {
28
- "@parall/agent-core": "1.25.0",
29
- "@parall/cli": "1.25.0",
30
- "@parall/sdk": "1.25.0"
28
+ "smol-toml": "^1.6.1",
29
+ "@parall/agent-core": "1.26.1",
30
+ "@parall/cli": "1.26.1",
31
+ "@parall/sdk": "1.26.1"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@types/node": "^22.0.0",
package/src/config.ts CHANGED
@@ -14,14 +14,14 @@ export type CodexAgentConfig = {
14
14
  model?: string;
15
15
  reasoningEffort?: string;
16
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.
17
+ * Sandbox mode passed to codex on `thread/{start,resume}`. Defaults to
18
+ * `danger-full-access` same as the hosted container because codex's
19
+ * inner bubblewrap sandbox requires unprivileged user namespaces that
20
+ * most environments lack, and the agent needs unrestricted shell access
21
+ * (e.g. `parall messages send`). Users can still override via
22
+ * `PRLL_CODEX_SANDBOX` env var.
23
23
  */
24
- sandbox?: string;
24
+ sandbox: string;
25
25
  /**
26
26
  * Approval policy is forced to `"never"` by default (not configurable
27
27
  * via config.toml override): Parall's HITL contract routes approvals
@@ -80,7 +80,7 @@ export function resolveCodexAgentConfig(env: NodeJS.ProcessEnv = process.env): C
80
80
  workspaceDir,
81
81
  model: env.PRLL_CODEX_MODEL?.trim() || undefined,
82
82
  reasoningEffort: env.PRLL_CODEX_REASONING_EFFORT?.trim() || undefined,
83
- sandbox: env.PRLL_CODEX_SANDBOX?.trim() || undefined,
83
+ sandbox: env.PRLL_CODEX_SANDBOX?.trim() || "danger-full-access",
84
84
  approvalPolicy: env.PRLL_CODEX_APPROVAL?.trim() || "never",
85
85
  runtimeKey: env.PRLL_CODEX_RUNTIME_KEY?.trim() || undefined,
86
86
  };
@@ -145,6 +145,11 @@ export function sessionStateFilePathForRuntime(stateDir: string, runtimeKey: str
145
145
  return path.join(stateDir, "threads", `${fileName}.json`);
146
146
  }
147
147
 
148
+ export function contextFilePathForSession(stateDir: string, sessionKey: string): string {
149
+ const fileName = Buffer.from(sessionKey).toString("base64url");
150
+ return path.join(stateDir, "dispatch-context", `${fileName}.json`);
151
+ }
152
+
148
153
  export function stepIdFilePathForSession(stateDir: string, sessionKey: string): string {
149
154
  const fileName = Buffer.from(sessionKey).toString("base64url");
150
155
  return path.join(stateDir, "step-ids", `${fileName}.txt`);
package/src/dispatch.ts CHANGED
@@ -35,6 +35,7 @@ type CodexAppServerAdapterOptions = Pick<
35
35
  > & {
36
36
  sessionManager: CodexSessionManager;
37
37
  log?: GatewayLogger;
38
+ contextFilePath?: string;
38
39
  };
39
40
 
40
41
  type TurnEventEnvelope =
@@ -87,6 +88,11 @@ export class CodexAppServerAdapter implements DispatchAdapter {
87
88
 
88
89
  constructor(private readonly opts: CodexAppServerAdapterOptions) {}
89
90
 
91
+ updateConfig(config: { model?: string | null; reasoningEffort?: string | null }): void {
92
+ if (config.model !== undefined) this.opts.model = config.model ?? undefined;
93
+ if (config.reasoningEffort !== undefined) this.opts.reasoningEffort = config.reasoningEffort ?? undefined;
94
+ }
95
+
90
96
  async *dispatch({ event, bodyForAgent, sessionKey, context }: DispatchOpts): AsyncIterable<RuntimeEvent> {
91
97
  await this.ensureStarted(context.log);
92
98
  // After ensureStarted resolves the subprocess could still die before we
@@ -263,6 +269,12 @@ export class CodexAppServerAdapter implements DispatchAdapter {
263
269
  }
264
270
  }
265
271
 
272
+ getBranchPoint(_sessionKey: string): string | undefined {
273
+ // Codex app-server does not expose a branch-point API; fork scope prefix
274
+ // provides the behavioral fallback for this runtime.
275
+ return undefined;
276
+ }
277
+
266
278
  async forkSession({ sessionKey: parentSessionKey }: ForkOpts): Promise<ForkSessionHandle | null> {
267
279
  const client = this.client;
268
280
  if (!client) return null;
@@ -334,12 +346,15 @@ export class CodexAppServerAdapter implements DispatchAdapter {
334
346
  // (git/ssh/npm/etc.). Earlier versions also rewrote HOME, which broke
335
347
  // tooling for users who set a custom CODEX_HOME pointing somewhere
336
348
  // other than their shell home.
337
- const env = {
349
+ const env: Record<string, string | undefined> = {
338
350
  ...process.env,
339
351
  CODEX_HOME: this.opts.codexHome,
340
352
  FORCE_COLOR: "0",
341
353
  NO_COLOR: "1",
342
354
  };
355
+ if (this.opts.contextFilePath) {
356
+ env.PRLL_CONTEXT_FILE = this.opts.contextFilePath;
357
+ }
343
358
  const args = ["app-server", "--listen", "stdio://"];
344
359
  (log ?? this.opts.log)?.info?.(`codex-agent: spawning ${this.opts.codexBin} ${args.join(" ")}`);
345
360
  const proc = spawn(this.opts.codexBin, args, {
@@ -430,15 +445,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
430
445
  const commonParams: Record<string, unknown> = {
431
446
  approvalPolicy: normalizeApprovalPolicy(this.opts.approvalPolicy),
432
447
  };
433
- // Only override sandbox when the user explicitly set PRLL_CODEX_SANDBOX.
434
- // Otherwise let codex read `sandbox_mode` from ~/.codex/config.toml — the
435
- // user's own configuration is authoritative for a self-hosted bridge,
436
- // and codex 0.120.0's workspace-write mode has a write_stdin bug that
437
- // our previous `workspace-write` default tripped for users whose config
438
- // had danger-full-access.
439
- if (this.opts.sandbox) {
440
- commonParams.sandbox = normalizeSandbox(this.opts.sandbox);
441
- }
448
+ commonParams.sandbox = normalizeSandbox(this.opts.sandbox);
442
449
  if (this.opts.model) commonParams.model = this.opts.model;
443
450
  if (this.opts.reasoningEffort) {
444
451
  // The app-server JSON-RPC surface uses camelCase for overrides —
package/src/index.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import * as os from "node:os";
4
- import { ParallAgentGateway, parseShutdownDeadlineMs } from "@parall/agent-core";
4
+ import { ParallAgentGateway, createPlatformConfigManager, isPlatformManagedProfile, parseShutdownDeadlineMs } from "@parall/agent-core";
5
5
  import { ApiError, ParallClient, ParallWs } from "@parall/sdk";
6
6
  import {
7
7
  buildCodexRuntimeKey,
8
+ contextFilePathForSession,
8
9
  resolveCodexAgentConfig,
9
10
  resolveWsUrl,
10
11
  sessionStateFilePathForRuntime,
@@ -12,7 +13,7 @@ import {
12
13
  } from "./config.js";
13
14
  import { CodexAppServerAdapter } from "./dispatch.js";
14
15
  import { CodexSessionManager } from "./session-manager.js";
15
- import { ensureCodexWorkspace } from "./workspace.js";
16
+ import { ensureCodexWorkspace, ensureWorkspaceTrusted } from "./workspace.js";
16
17
 
17
18
  function createLogger(prefix: string) {
18
19
  return {
@@ -46,12 +47,14 @@ async function main() {
46
47
 
47
48
  const me = await getAgentMeWithLegacyFallback(client, config.orgId);
48
49
  const agentUserId = me.id;
50
+ ensureWorkspaceTrusted(config.codexHome, config.workspaceDir, log);
49
51
  ensureCodexWorkspace(config.workspaceDir, log, {
50
52
  userId: agentUserId,
51
53
  displayName: me.display_name,
52
54
  description: me.agent_profile?.description ?? undefined,
53
55
  });
54
56
  const runtimeKey = config.runtimeKey || buildCodexRuntimeKey(agentUserId);
57
+ const mainContextFilePath = contextFilePathForSession(config.stateDir, runtimeKey);
55
58
  const sessionStateFilePath = sessionStateFilePathForRuntime(config.stateDir, runtimeKey);
56
59
  const sessionManager = new CodexSessionManager(
57
60
  runtimeKey,
@@ -65,16 +68,25 @@ async function main() {
65
68
  wsUrl: resolvedWsUrl,
66
69
  });
67
70
 
71
+ const configMgr = createPlatformConfigManager({ client, stateDir: config.stateDir, runtimeType: "codex", log });
72
+ const platformDefaults = await configMgr.fetch();
73
+ let platformManaged = isPlatformManagedProfile(me.agent_profile);
74
+ const resolvedModel = platformManaged ? (platformDefaults.model ?? config.model) : config.model;
75
+ const resolvedEffort = platformManaged ? (platformDefaults.thinkingEffort ?? config.reasoningEffort) : config.reasoningEffort;
76
+ if (platformDefaults.model) log.info(`platform config: model=${platformDefaults.model} (${platformManaged ? "applied" : "ignored, model_management=self"})`);
77
+ if (platformDefaults.thinkingEffort) log.info(`platform config: reasoning_effort=${platformDefaults.thinkingEffort} (${platformManaged ? "applied" : "ignored, model_management=self"})`);
78
+
68
79
  const adapter = new CodexAppServerAdapter({
69
80
  codexBin: config.codexBin,
70
81
  codexHome: config.codexHome,
71
82
  workspaceDir: config.workspaceDir,
72
- model: config.model,
73
- reasoningEffort: config.reasoningEffort,
83
+ model: resolvedModel,
84
+ reasoningEffort: resolvedEffort,
74
85
  sandbox: config.sandbox,
75
86
  approvalPolicy: config.approvalPolicy,
76
87
  sessionManager,
77
88
  log,
89
+ contextFilePath: mainContextFilePath,
78
90
  });
79
91
 
80
92
  const gateway = new ParallAgentGateway({
@@ -100,7 +112,26 @@ async function main() {
100
112
  dispatchAdapter: adapter,
101
113
  log,
102
114
  shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
115
+ contextFilePathForSession: (sessionKey) => contextFilePathForSession(config.stateDir, sessionKey),
103
116
  stepIdFilePathForSession: (sessionKey) => stepIdFilePathForSession(config.stateDir, sessionKey),
117
+ onConfigUpdate: async () => {
118
+ const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
119
+ platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
120
+ const updated = await configMgr.fetch();
121
+ adapter.updateConfig({
122
+ model: platformManaged ? (updated.model ?? config.model) : (config.model ?? null),
123
+ reasoningEffort: platformManaged ? (updated.thinkingEffort ?? config.reasoningEffort) : (config.reasoningEffort ?? null),
124
+ });
125
+ },
126
+ onSessionReady: async () => {
127
+ const refreshed = await getAgentMeWithLegacyFallback(client, config.orgId);
128
+ platformManaged = isPlatformManagedProfile(refreshed.agent_profile);
129
+ const updated = await configMgr.fetch();
130
+ adapter.updateConfig({
131
+ model: platformManaged ? (updated.model ?? config.model) : (config.model ?? null),
132
+ reasoningEffort: platformManaged ? (updated.thinkingEffort ?? config.reasoningEffort) : (config.reasoningEffort ?? null),
133
+ });
134
+ },
104
135
  });
105
136
 
106
137
  const abortController = new AbortController();
package/src/workspace.ts CHANGED
@@ -1,45 +1,135 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { parse as parseToml } from "smol-toml";
3
4
  import {
4
5
  BRIDGE_WORKSPACE_INSTRUCTIONS,
5
6
  PRLL_BEHAVIOR,
6
7
  PRLL_REFERENCE_GUIDE,
7
8
  buildIdentity,
9
+ buildSkillReferences,
10
+ writeSkillFiles,
8
11
  } from "@parall/agent-core";
9
12
  import type { AgentIdentity } from "@parall/agent-core";
10
13
 
14
+ /**
15
+ * Ensure the workspace directory is marked as trusted in the global Codex
16
+ * config so that project-level `developer_instructions` are loaded at
17
+ * app-server startup.
18
+ *
19
+ * Uses smol-toml for structured reads (avoids substring false-positives),
20
+ * but writes via raw text manipulation (preserves comments and formatting).
21
+ * Failures are swallowed (warn-only) so a trust write issue never blocks
22
+ * the bridge from starting.
23
+ */
24
+ export function ensureWorkspaceTrusted(
25
+ codexHome: string,
26
+ workspaceDir: string,
27
+ log?: { warn: (msg: string) => void },
28
+ ): void {
29
+ const configPath = path.join(codexHome, "config.toml");
30
+ const normalizedPath = path.resolve(workspaceDir);
31
+
32
+ try {
33
+ let content = "";
34
+ try {
35
+ content = fs.readFileSync(configPath, "utf8");
36
+ } catch (err) {
37
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
38
+ }
39
+
40
+ // Structured read — know exactly what state we're in.
41
+ let parsed: Record<string, unknown> | undefined;
42
+ if (content) {
43
+ try {
44
+ parsed = parseToml(content) as Record<string, unknown>;
45
+ } catch {
46
+ // File is already broken TOML — don't make it worse.
47
+ log?.warn(`Codex config.toml is not valid TOML; skipping trust write for ${normalizedPath}`);
48
+ return;
49
+ }
50
+ }
51
+
52
+ const projects = parsed?.projects as Record<string, Record<string, unknown>> | undefined;
53
+ if (projects?.[normalizedPath]?.trust_level === "trusted") return;
54
+
55
+ // TOML basic-string keys require backslash and double-quote escaping.
56
+ const escapedPath = normalizedPath.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
57
+ const sectionHeader = `[projects."${escapedPath}"]`;
58
+ const trustLine = 'trust_level = "trusted"';
59
+
60
+ const headerIdx = content.indexOf(sectionHeader);
61
+ if (headerIdx !== -1) {
62
+ // Section exists but trust_level isn't "trusted". Scan all lines
63
+ // within the section (up to the next `[` header or EOF) for an
64
+ // existing trust_level key — it may not be the first line after
65
+ // the header if the user added comments or other keys.
66
+ const headerLineEnd = content.indexOf("\n", headerIdx);
67
+ if (headerLineEnd === -1) {
68
+ content = `${content}\n${trustLine}\n`;
69
+ } else {
70
+ const afterHeader = headerLineEnd + 1;
71
+ const sectionEnd = findSectionEnd(content, afterHeader);
72
+ const sectionBody = content.substring(afterHeader, sectionEnd);
73
+ const trustMatch = sectionBody.match(/^[ \t]*trust_level[ \t]*=.*$/m);
74
+ if (trustMatch) {
75
+ const matchStart = afterHeader + trustMatch.index!;
76
+ const matchEnd = matchStart + trustMatch[0].length;
77
+ content = content.substring(0, matchStart) + trustLine + content.substring(matchEnd);
78
+ } else {
79
+ content = content.substring(0, afterHeader) + trustLine + "\n" + content.substring(afterHeader);
80
+ }
81
+ }
82
+ fs.writeFileSync(configPath, content, "utf8");
83
+ } else if (projects?.[normalizedPath] !== undefined) {
84
+ // smol-toml found the section but indexOf missed it — the header
85
+ // uses non-canonical TOML formatting. Appending would create a
86
+ // duplicate table. Skip rather than corrupt the file.
87
+ log?.warn(`Codex config.toml has non-canonical header for ${normalizedPath}; skipping trust write`);
88
+ } else if (projects && !content.includes("[projects.")) {
89
+ // `projects` exists in parsed output but no `[projects.` table
90
+ // headers in the raw text — it's an inline table. Appending a
91
+ // standard table header would produce invalid TOML.
92
+ log?.warn(`Codex config.toml uses inline table for projects; skipping trust write for ${normalizedPath}`);
93
+ } else {
94
+ // Section doesn't exist — append.
95
+ fs.mkdirSync(codexHome, { recursive: true });
96
+ fs.appendFileSync(configPath, `\n${sectionHeader}\n${trustLine}\n`, "utf8");
97
+ }
98
+ } catch (err) {
99
+ log?.warn(`failed to write Codex project trust for ${normalizedPath}: ${String(err)}`);
100
+ }
101
+ }
102
+
103
+ function findSectionEnd(content: string, fromIndex: number): number {
104
+ // A TOML section ends at the next `[` that starts a new table header.
105
+ // Match `[` at the beginning of a line (not inside a value).
106
+ const nextHeader = content.indexOf("\n[", fromIndex);
107
+ return nextHeader === -1 ? content.length : nextHeader;
108
+ }
109
+
11
110
  export function ensureCodexWorkspace(
12
111
  workspaceDir: string,
13
112
  log?: { warn: (msg: string) => void },
14
113
  agentIdentity?: AgentIdentity,
15
- ) {
16
- const AGENTS_MD = [
114
+ ): void {
115
+ const systemPrompt = [
17
116
  buildIdentity(agentIdentity),
18
117
  BRIDGE_WORKSPACE_INSTRUCTIONS,
19
118
  PRLL_BEHAVIOR,
20
119
  PRLL_REFERENCE_GUIDE,
120
+ buildSkillReferences(workspaceDir),
21
121
  ].join("\n\n");
122
+
22
123
  fs.mkdirSync(workspaceDir, { recursive: true });
23
124
 
24
- // AGENTS.md is bridge-managed: always overwrite so the workspace picks up
25
- // updated guardrails / command templates on upgrade. Operators must not
26
- // hand-edit this file — workspace customisations should go elsewhere in
27
- // the Codex home dir. Surface a warning when we replace divergent content
28
- // so an operator who did edit it locally sees a signal instead of silently
29
- // losing changes. Mirrors the Claude bridge behaviour.
30
- const agentsMdPath = path.join(workspaceDir, "AGENTS.md");
31
- if (log) {
32
- try {
33
- const existing = fs.readFileSync(agentsMdPath, "utf8");
34
- if (existing !== AGENTS_MD) {
35
- log.warn(
36
- `codex-agent: overwriting divergent ${agentsMdPath} with bridge-managed template ` +
37
- `(local edits to AGENTS.md are not preserved — customize other files in the workspace instead)`,
38
- );
39
- }
40
- } catch {
41
- // file missing or unreadable — first-boot case, no warning needed
42
- }
43
- }
44
- fs.writeFileSync(agentsMdPath, AGENTS_MD, "utf8");
125
+ const parallDir = path.join(workspaceDir, ".parall");
126
+ fs.mkdirSync(parallDir, { recursive: true });
127
+ fs.writeFileSync(path.join(parallDir, "system-prompt.md"), systemPrompt, "utf8");
128
+
129
+ writeSkillFiles(path.join(parallDir, "skills"));
130
+
131
+ const codexConfigDir = path.join(workspaceDir, ".codex");
132
+ fs.mkdirSync(codexConfigDir, { recursive: true });
133
+ const toml = `developer_instructions = """\n${systemPrompt.replace(/\\/g, "\\\\").replace(/"""/g, '\\"""')}\n"""\n`;
134
+ fs.writeFileSync(path.join(codexConfigDir, "config.toml"), toml, "utf8");
45
135
  }