@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 +8 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -1
- package/dist/dispatch.d.ts +6 -0
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +15 -9
- package/dist/index.js +36 -5
- package/dist/workspace.d.ts +13 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +107 -22
- package/package.json +5 -4
- package/src/config.ts +13 -8
- package/src/dispatch.ts +17 -10
- package/src/index.ts +35 -4
- package/src/workspace.ts +113 -23
package/dist/config.d.ts
CHANGED
|
@@ -11,14 +11,14 @@ export type CodexAgentConfig = {
|
|
|
11
11
|
model?: string;
|
|
12
12
|
reasoningEffort?: string;
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
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
|
package/dist/config.d.ts.map
CHANGED
|
@@ -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,
|
|
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() ||
|
|
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`);
|
package/dist/dispatch.d.ts
CHANGED
|
@@ -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;
|
package/dist/dispatch.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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:
|
|
56
|
-
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();
|
package/dist/workspace.d.ts
CHANGED
|
@@ -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;
|
package/dist/workspace.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"
|
|
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 {
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
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.
|
|
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
|
-
"
|
|
29
|
-
"@parall/
|
|
30
|
-
"@parall/
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
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() ||
|
|
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
|
-
|
|
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:
|
|
73
|
-
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
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
}
|