@parall/codex-agent 1.44.0 → 1.46.0
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/app-server-process.d.ts +9 -0
- package/dist/app-server-process.d.ts.map +1 -0
- package/dist/app-server-process.js +58 -0
- package/dist/app-server-protocol.d.ts +19 -0
- package/dist/app-server-protocol.d.ts.map +1 -0
- package/dist/app-server-protocol.js +42 -0
- package/dist/dispatch.d.ts +88 -5
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +364 -196
- package/dist/index.js +27 -11
- package/dist/instructions-refresh.d.ts +136 -0
- package/dist/instructions-refresh.d.ts.map +1 -0
- package/dist/instructions-refresh.js +244 -0
- package/dist/jsonrpc-client.d.ts +49 -1
- package/dist/jsonrpc-client.d.ts.map +1 -1
- package/dist/jsonrpc-client.js +77 -5
- package/dist/legacy-workspace-config-migration.d.ts +112 -0
- package/dist/legacy-workspace-config-migration.d.ts.map +1 -0
- package/dist/legacy-workspace-config-migration.js +229 -0
- package/dist/server-requests.d.ts +10 -0
- package/dist/server-requests.d.ts.map +1 -0
- package/dist/server-requests.js +39 -0
- package/dist/session-manager.d.ts +12 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +53 -3
- package/dist/turn-sink.d.ts +26 -0
- package/dist/turn-sink.d.ts.map +1 -0
- package/dist/turn-sink.js +45 -0
- package/dist/workspace.d.ts +23 -25
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +138 -138
- package/package.json +5 -5
- package/src/app-server-process.ts +59 -0
- package/src/app-server-protocol.ts +46 -0
- package/src/dispatch.ts +426 -204
- package/src/index.ts +35 -10
- package/src/instructions-refresh.ts +367 -0
- package/src/jsonrpc-client.ts +109 -7
- package/src/legacy-workspace-config-migration.ts +296 -0
- package/src/server-requests.ts +40 -0
- package/src/session-manager.ts +74 -6
- package/src/turn-sink.ts +54 -0
- package/src/workspace.ts +155 -155
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RuntimeEvent } from '@parall/agent-core';
|
|
2
|
+
import { EventMapper } from './event-mapping.js';
|
|
3
|
+
export type TurnEventEnvelope = {
|
|
4
|
+
kind: 'runtime';
|
|
5
|
+
event: RuntimeEvent;
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'turn_end';
|
|
8
|
+
threadId?: string;
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'error';
|
|
11
|
+
message: string;
|
|
12
|
+
};
|
|
13
|
+
/** Per-turn buffered sink backed by an unbounded promise queue. */
|
|
14
|
+
export declare class TurnSink {
|
|
15
|
+
readonly mapper: EventMapper;
|
|
16
|
+
private readonly queue;
|
|
17
|
+
private resolver;
|
|
18
|
+
private closed;
|
|
19
|
+
push(envelope: TurnEventEnvelope): void;
|
|
20
|
+
next(): Promise<TurnEventEnvelope>;
|
|
21
|
+
close(): void;
|
|
22
|
+
/** True once close() ran — the sink's terminal state (queued envelopes may
|
|
23
|
+
* still drain via next()). */
|
|
24
|
+
get isClosed(): boolean;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=turn-sink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-sink.d.ts","sourceRoot":"","sources":["../src/turn-sink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC,mEAAmE;AACnE,qBAAa,QAAQ;IACnB,QAAQ,CAAC,MAAM,cAAqB;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA2B;IACjD,OAAO,CAAC,QAAQ,CAAqD;IACrE,OAAO,CAAC,MAAM,CAAS;IAEvB,IAAI,CAAC,QAAQ,EAAE,iBAAiB;IAWhC,IAAI,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAelC,KAAK;IAOL;kCAC8B;IAC9B,IAAI,QAAQ,IAAI,OAAO,CAEtB;CACF"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { EventMapper } from './event-mapping.js';
|
|
2
|
+
/** Per-turn buffered sink backed by an unbounded promise queue. */
|
|
3
|
+
export class TurnSink {
|
|
4
|
+
mapper = new EventMapper();
|
|
5
|
+
queue = [];
|
|
6
|
+
resolver = null;
|
|
7
|
+
closed = false;
|
|
8
|
+
push(envelope) {
|
|
9
|
+
if (this.closed)
|
|
10
|
+
return;
|
|
11
|
+
if (this.resolver) {
|
|
12
|
+
const r = this.resolver;
|
|
13
|
+
this.resolver = null;
|
|
14
|
+
r(envelope);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
this.queue.push(envelope);
|
|
18
|
+
}
|
|
19
|
+
next() {
|
|
20
|
+
// Drain any queued envelopes first, even after close(). Otherwise a final
|
|
21
|
+
// error envelope enqueued right before close() (e.g. by
|
|
22
|
+
// handleSubprocessClose) is silently dropped because the consumer would
|
|
23
|
+
// see turn_end before it.
|
|
24
|
+
const pending = this.queue.shift();
|
|
25
|
+
if (pending)
|
|
26
|
+
return Promise.resolve(pending);
|
|
27
|
+
if (this.closed) {
|
|
28
|
+
return Promise.resolve({ kind: 'turn_end' });
|
|
29
|
+
}
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
this.resolver = resolve;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
close() {
|
|
35
|
+
this.closed = true;
|
|
36
|
+
const r = this.resolver;
|
|
37
|
+
this.resolver = null;
|
|
38
|
+
r?.({ kind: 'turn_end' });
|
|
39
|
+
}
|
|
40
|
+
/** True once close() ran — the sink's terminal state (queued envelopes may
|
|
41
|
+
* still drain via next()). */
|
|
42
|
+
get isClosed() {
|
|
43
|
+
return this.closed;
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/workspace.d.ts
CHANGED
|
@@ -16,10 +16,10 @@ export declare const CONFIG_LOCK_TIMINGS: {
|
|
|
16
16
|
};
|
|
17
17
|
/**
|
|
18
18
|
* Advisory cross-process lock serializing `<codexHome>/config.toml`
|
|
19
|
-
* read-modify-write.
|
|
20
|
-
*
|
|
21
|
-
* together — without a lock,
|
|
22
|
-
*
|
|
19
|
+
* read-modify-write. Several bridge children can share one CODEX_HOME (an
|
|
20
|
+
* operator-set PRLL_CODEX_HOME reaches every child on the machine), and the
|
|
21
|
+
* daemon starts them together — without a lock, concurrent whole-file
|
|
22
|
+
* rewrites working from stale reads lose updates.
|
|
23
23
|
*
|
|
24
24
|
* Queue design (why not wx-create + steal): any scheme that renames or
|
|
25
25
|
* unlinks the SHARED lock path can, between its staleness check and the
|
|
@@ -62,11 +62,12 @@ export declare const CONFIG_LOCK_TIMINGS: {
|
|
|
62
62
|
* eviction and the process re-enqueues. On `waitMs` timeout
|
|
63
63
|
* the mutation proceeds without the lock (warn) — blocking would wedge
|
|
64
64
|
* bridge startup. The lock coordinates bridge processes only; codex itself
|
|
65
|
-
* does not observe it, which is why bridge writes to
|
|
66
|
-
* additionally kept rare
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
65
|
+
* does not observe it, which is why bridge writes to the global config are
|
|
66
|
+
* additionally kept rare — the only remaining mutation is the parall
|
|
67
|
+
* provider block (runtime_auth agents, which share the operator's ~/.codex,
|
|
68
|
+
* write nothing at all: platform instructions ride the app-server
|
|
69
|
+
* `developerInstructions` param) — and whole-file writes are atomic
|
|
70
|
+
* (temp + rename) so codex never reads a truncated file.
|
|
70
71
|
*
|
|
71
72
|
* Exported for tests.
|
|
72
73
|
*/
|
|
@@ -84,19 +85,6 @@ export declare const CONFIG_LOCK_TEST_HOOKS: {
|
|
|
84
85
|
beforeTicketPublish?: () => void;
|
|
85
86
|
beforeTicketEntry?: () => void;
|
|
86
87
|
};
|
|
87
|
-
/**
|
|
88
|
-
* Ensure the workspace directory is marked as trusted in the global Codex
|
|
89
|
-
* config so that project-level `developer_instructions` are loaded at
|
|
90
|
-
* app-server startup.
|
|
91
|
-
*
|
|
92
|
-
* Uses smol-toml for structured reads (avoids substring false-positives),
|
|
93
|
-
* but writes via raw text manipulation (preserves comments and formatting).
|
|
94
|
-
* Failures are swallowed (warn-only) so a trust write issue never blocks
|
|
95
|
-
* the bridge from starting.
|
|
96
|
-
*/
|
|
97
|
-
export declare function ensureWorkspaceTrusted(codexHome: string, workspaceDir: string, log?: {
|
|
98
|
-
warn: (msg: string) => void;
|
|
99
|
-
}): void;
|
|
100
88
|
/**
|
|
101
89
|
* Returns true when the agent is using the Parall LLM proxy (llm_source=parall)
|
|
102
90
|
* rather than a BYO custom provider or runtime_auth. Detected by checking
|
|
@@ -117,11 +105,21 @@ export declare function isParallProxyMode(env?: NodeJS.ProcessEnv): boolean;
|
|
|
117
105
|
*
|
|
118
106
|
* Provider-managed: overwritten on every boot (env vars are the SSOT).
|
|
119
107
|
*/
|
|
108
|
+
/**
|
|
109
|
+
* Escape a value as a TOML basic (double-quoted) string. Needed for Windows
|
|
110
|
+
* paths (backslashes) in the auth command; harmless hardening everywhere else.
|
|
111
|
+
*/
|
|
112
|
+
export declare function tomlBasicString(value: string): string;
|
|
113
|
+
/** Injectable auth-command inputs — production callers pass nothing. */
|
|
114
|
+
export interface ParallProviderAuthOptions {
|
|
115
|
+
platform?: NodeJS.Platform;
|
|
116
|
+
nodeBin?: string;
|
|
117
|
+
}
|
|
120
118
|
export declare function ensureParallProvider(codexHome: string, apiUrl: string, log?: {
|
|
121
119
|
warn: (msg: string) => void;
|
|
122
|
-
}): void;
|
|
123
|
-
export declare function writeCodexSystemPrompt(workspaceDir: string, agentIdentity?: AgentIdentity, capabilityFragments?: string[]):
|
|
120
|
+
}, opts?: ParallProviderAuthOptions): void;
|
|
121
|
+
export declare function writeCodexSystemPrompt(workspaceDir: string, agentIdentity?: AgentIdentity, capabilityFragments?: string[]): string;
|
|
124
122
|
export declare function ensureCodexWorkspace(workspaceDir: string, log?: {
|
|
125
123
|
warn: (msg: string) => void;
|
|
126
|
-
}, agentIdentity?: AgentIdentity, capabilityFragments?: string[]):
|
|
124
|
+
}, agentIdentity?: AgentIdentity, capabilityFragments?: string[]): string;
|
|
127
125
|
//# sourceMappingURL=workspace.d.ts.map
|
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":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAIxD;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB;;;;CAI/B,CAAC;AAUF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,GAAG,SAAS,EAChD,EAAE,EAAE,MAAM,IAAI,GACb,IAAI,CAkJN;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,EAAE;IACnC,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,mBAAmB,CAAC,EAAE,MAAM,IAAI,CAAC;IACjC,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B,CAAC;AAuJP;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAK/E;AAED;;;;;;;;;;;;;GAaG;AACH;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAerD;AAED,wEAAwE;AACxE,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA0BD,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,EACrC,IAAI,CAAC,EAAE,yBAAyB,GAC/B,IAAI,CAEN;AAiFD,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,aAAa,EAC7B,mBAAmB,CAAC,EAAE,MAAM,EAAE,GAC7B,MAAM,CAwBR;AAED,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,EAC7B,mBAAmB,CAAC,EAAE,MAAM,EAAE,GAC7B,MAAM,CA+BR"}
|
package/dist/workspace.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { parse as parseToml } from 'smol-toml';
|
|
4
3
|
import { BRIDGE_WORKSPACE_INSTRUCTIONS, PRLL_BEHAVIOR, PRLL_REFERENCE_GUIDE, buildIdentity, buildSkillReferences, writeSkillFiles, } from '@parall/agent-core';
|
|
4
|
+
import { runLegacyWorkspaceConfigMigration } from './legacy-workspace-config-migration.js';
|
|
5
5
|
/**
|
|
6
6
|
* Config-lock timings, exported for tests.
|
|
7
7
|
*
|
|
@@ -25,10 +25,10 @@ function sleepSync(ms) {
|
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
27
|
* Advisory cross-process lock serializing `<codexHome>/config.toml`
|
|
28
|
-
* read-modify-write.
|
|
29
|
-
*
|
|
30
|
-
* together — without a lock,
|
|
31
|
-
*
|
|
28
|
+
* read-modify-write. Several bridge children can share one CODEX_HOME (an
|
|
29
|
+
* operator-set PRLL_CODEX_HOME reaches every child on the machine), and the
|
|
30
|
+
* daemon starts them together — without a lock, concurrent whole-file
|
|
31
|
+
* rewrites working from stale reads lose updates.
|
|
32
32
|
*
|
|
33
33
|
* Queue design (why not wx-create + steal): any scheme that renames or
|
|
34
34
|
* unlinks the SHARED lock path can, between its staleness check and the
|
|
@@ -71,11 +71,12 @@ function sleepSync(ms) {
|
|
|
71
71
|
* eviction and the process re-enqueues. On `waitMs` timeout
|
|
72
72
|
* the mutation proceeds without the lock (warn) — blocking would wedge
|
|
73
73
|
* bridge startup. The lock coordinates bridge processes only; codex itself
|
|
74
|
-
* does not observe it, which is why bridge writes to
|
|
75
|
-
* additionally kept rare
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
74
|
+
* does not observe it, which is why bridge writes to the global config are
|
|
75
|
+
* additionally kept rare — the only remaining mutation is the parall
|
|
76
|
+
* provider block (runtime_auth agents, which share the operator's ~/.codex,
|
|
77
|
+
* write nothing at all: platform instructions ride the app-server
|
|
78
|
+
* `developerInstructions` param) — and whole-file writes are atomic
|
|
79
|
+
* (temp + rename) so codex never reads a truncated file.
|
|
79
80
|
*
|
|
80
81
|
* Exported for tests.
|
|
81
82
|
*/
|
|
@@ -314,9 +315,9 @@ function bakeryEnqueue(queueDir) {
|
|
|
314
315
|
/**
|
|
315
316
|
* Whole-file config writes go through temp + rename so a concurrent reader
|
|
316
317
|
* (including codex itself, which does not observe the advisory lock) never
|
|
317
|
-
* sees a truncated file.
|
|
318
|
-
*
|
|
319
|
-
*
|
|
318
|
+
* sees a truncated file. This is now the only way the bridge writes the global
|
|
319
|
+
* config: the append-based trust path it used to share this file with is gone
|
|
320
|
+
* with `ensureWorkspaceTrusted`, and the provider block is a whole-file rewrite.
|
|
320
321
|
*
|
|
321
322
|
* The rename targets the file's REAL path: `config.toml` managed by a
|
|
322
323
|
* dotfiles setup is often a symlink, and renaming onto the link path would
|
|
@@ -402,108 +403,6 @@ function resolveWriteTarget(filePath) {
|
|
|
402
403
|
}
|
|
403
404
|
throw new Error(`symlink chain deeper than 40 while resolving ${filePath}`);
|
|
404
405
|
}
|
|
405
|
-
/**
|
|
406
|
-
* Ensure the workspace directory is marked as trusted in the global Codex
|
|
407
|
-
* config so that project-level `developer_instructions` are loaded at
|
|
408
|
-
* app-server startup.
|
|
409
|
-
*
|
|
410
|
-
* Uses smol-toml for structured reads (avoids substring false-positives),
|
|
411
|
-
* but writes via raw text manipulation (preserves comments and formatting).
|
|
412
|
-
* Failures are swallowed (warn-only) so a trust write issue never blocks
|
|
413
|
-
* the bridge from starting.
|
|
414
|
-
*/
|
|
415
|
-
export function ensureWorkspaceTrusted(codexHome, workspaceDir, log) {
|
|
416
|
-
withConfigLock(codexHome, log, () => ensureWorkspaceTrustedLocked(codexHome, workspaceDir, log));
|
|
417
|
-
}
|
|
418
|
-
function ensureWorkspaceTrustedLocked(codexHome, workspaceDir, log) {
|
|
419
|
-
const configPath = path.join(codexHome, 'config.toml');
|
|
420
|
-
const normalizedPath = path.resolve(workspaceDir);
|
|
421
|
-
try {
|
|
422
|
-
let content = '';
|
|
423
|
-
try {
|
|
424
|
-
content = fs.readFileSync(configPath, 'utf8');
|
|
425
|
-
}
|
|
426
|
-
catch (err) {
|
|
427
|
-
if (err.code !== 'ENOENT')
|
|
428
|
-
throw err;
|
|
429
|
-
}
|
|
430
|
-
// Structured read — know exactly what state we're in.
|
|
431
|
-
let parsed;
|
|
432
|
-
if (content) {
|
|
433
|
-
try {
|
|
434
|
-
parsed = parseToml(content);
|
|
435
|
-
}
|
|
436
|
-
catch {
|
|
437
|
-
// File is already broken TOML — don't make it worse.
|
|
438
|
-
log?.warn(`Codex config.toml is not valid TOML; skipping trust write for ${normalizedPath}`);
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
const projects = parsed?.projects;
|
|
443
|
-
const existingTrust = projects?.[normalizedPath]?.trust_level;
|
|
444
|
-
if (existingTrust === 'trusted')
|
|
445
|
-
return;
|
|
446
|
-
if (existingTrust !== undefined) {
|
|
447
|
-
// An explicit non-trusted value is a human decision — on a local
|
|
448
|
-
// runtime_auth daemon this file IS the operator's own ~/.codex config,
|
|
449
|
-
// and a workspace they deliberately marked untrusted must never be
|
|
450
|
-
// silently flipped by an agent. Leave it and surface the consequence.
|
|
451
|
-
log?.warn(`Codex config marks ${normalizedPath} as trust_level=${JSON.stringify(existingTrust)}; respecting the explicit decision — project-level developer_instructions will not load for this workspace`);
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
// TOML basic-string keys require backslash and double-quote escaping.
|
|
455
|
-
const escapedPath = normalizedPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
456
|
-
const sectionHeader = `[projects."${escapedPath}"]`;
|
|
457
|
-
const trustLine = 'trust_level = "trusted"';
|
|
458
|
-
const headerIdx = content.indexOf(sectionHeader);
|
|
459
|
-
if (headerIdx !== -1) {
|
|
460
|
-
// Section exists but trust_level isn't "trusted". Scan all lines
|
|
461
|
-
// within the section (up to the next `[` header or EOF) for an
|
|
462
|
-
// existing trust_level key — it may not be the first line after
|
|
463
|
-
// the header if the user added comments or other keys.
|
|
464
|
-
const headerLineEnd = content.indexOf('\n', headerIdx);
|
|
465
|
-
if (headerLineEnd === -1) {
|
|
466
|
-
content = `${content}\n${trustLine}\n`;
|
|
467
|
-
}
|
|
468
|
-
else {
|
|
469
|
-
const afterHeader = headerLineEnd + 1;
|
|
470
|
-
const sectionEnd = findSectionEnd(content, afterHeader);
|
|
471
|
-
const sectionBody = content.substring(afterHeader, sectionEnd);
|
|
472
|
-
const trustMatch = sectionBody.match(/^[ \t]*trust_level[ \t]*=.*$/m);
|
|
473
|
-
if (trustMatch) {
|
|
474
|
-
const matchStart = afterHeader + trustMatch.index;
|
|
475
|
-
const matchEnd = matchStart + trustMatch[0].length;
|
|
476
|
-
content = content.substring(0, matchStart) + trustLine + content.substring(matchEnd);
|
|
477
|
-
}
|
|
478
|
-
else {
|
|
479
|
-
content =
|
|
480
|
-
content.substring(0, afterHeader) + trustLine + '\n' + content.substring(afterHeader);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
writeConfigAtomic(configPath, content);
|
|
484
|
-
}
|
|
485
|
-
else if (projects?.[normalizedPath] !== undefined) {
|
|
486
|
-
// smol-toml found the section but indexOf missed it — the header
|
|
487
|
-
// uses non-canonical TOML formatting. Appending would create a
|
|
488
|
-
// duplicate table. Skip rather than corrupt the file.
|
|
489
|
-
log?.warn(`Codex config.toml has non-canonical header for ${normalizedPath}; skipping trust write`);
|
|
490
|
-
}
|
|
491
|
-
else if (projects && !content.includes('[projects.')) {
|
|
492
|
-
// `projects` exists in parsed output but no `[projects.` table
|
|
493
|
-
// headers in the raw text — it's an inline table. Appending a
|
|
494
|
-
// standard table header would produce invalid TOML.
|
|
495
|
-
log?.warn(`Codex config.toml uses inline table for projects; skipping trust write for ${normalizedPath}`);
|
|
496
|
-
}
|
|
497
|
-
else {
|
|
498
|
-
// Section doesn't exist — append.
|
|
499
|
-
fs.mkdirSync(codexHome, { recursive: true });
|
|
500
|
-
fs.appendFileSync(configPath, `\n${sectionHeader}\n${trustLine}\n`, 'utf8');
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
catch (err) {
|
|
504
|
-
log?.warn(`failed to write Codex project trust for ${normalizedPath}: ${String(err)}`);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
406
|
/**
|
|
508
407
|
* Returns true when the agent is using the Parall LLM proxy (llm_source=parall)
|
|
509
408
|
* rather than a BYO custom provider or runtime_auth. Detected by checking
|
|
@@ -530,10 +429,61 @@ export function isParallProxyMode(env = process.env) {
|
|
|
530
429
|
*
|
|
531
430
|
* Provider-managed: overwritten on every boot (env vars are the SSOT).
|
|
532
431
|
*/
|
|
533
|
-
|
|
534
|
-
|
|
432
|
+
/**
|
|
433
|
+
* Escape a value as a TOML basic (double-quoted) string. Needed for Windows
|
|
434
|
+
* paths (backslashes) in the auth command; harmless hardening everywhere else.
|
|
435
|
+
*/
|
|
436
|
+
export function tomlBasicString(value) {
|
|
437
|
+
let out = '';
|
|
438
|
+
for (const ch of value) {
|
|
439
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
440
|
+
if (ch === '\\')
|
|
441
|
+
out += '\\\\';
|
|
442
|
+
else if (ch === '"')
|
|
443
|
+
out += '\\"';
|
|
444
|
+
else if (ch === '\b')
|
|
445
|
+
out += '\\b';
|
|
446
|
+
else if (ch === '\t')
|
|
447
|
+
out += '\\t';
|
|
448
|
+
else if (ch === '\n')
|
|
449
|
+
out += '\\n';
|
|
450
|
+
else if (ch === '\f')
|
|
451
|
+
out += '\\f';
|
|
452
|
+
else if (ch === '\r')
|
|
453
|
+
out += '\\r';
|
|
454
|
+
else if (code < 0x20 || code === 0x7f)
|
|
455
|
+
out += `\\u${code.toString(16).padStart(4, '0')}`;
|
|
456
|
+
else
|
|
457
|
+
out += ch;
|
|
458
|
+
}
|
|
459
|
+
return `"${out}"`;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* The `[model_providers.parall.auth]` body. auth.command is the only path
|
|
463
|
+
* that actually sends the Bearer header on custom providers (see
|
|
464
|
+
* codex-responses-api-proxy.md), so both platforms stay in that shape:
|
|
465
|
+
* - POSIX keeps the empirically validated `printenv OPENAI_API_KEY`,
|
|
466
|
+
* byte-for-byte — do not touch it.
|
|
467
|
+
* - Windows has no printenv; reuse the bridge's own Node runtime
|
|
468
|
+
* (process.execPath — on disk by construction, and the config is
|
|
469
|
+
* provider-managed / rewritten every boot, so a moved Node self-heals).
|
|
470
|
+
* The -e script mirrors printenv exactly: missing var → exit 1, value →
|
|
471
|
+
* stdout with no trailing newline. Keep the script free of `"` and `\`
|
|
472
|
+
* so it stays inert inside a TOML basic string.
|
|
473
|
+
*/
|
|
474
|
+
function parallAuthLines(opts) {
|
|
475
|
+
const platform = opts?.platform ?? process.platform;
|
|
476
|
+
if (platform !== 'win32') {
|
|
477
|
+
return ['command = "printenv"', 'args = ["OPENAI_API_KEY"]'];
|
|
478
|
+
}
|
|
479
|
+
const nodeBin = opts?.nodeBin ?? process.execPath;
|
|
480
|
+
const script = 'const v=process.env.OPENAI_API_KEY;if(v===undefined)process.exit(1);process.stdout.write(v)';
|
|
481
|
+
return [`command = ${tomlBasicString(nodeBin)}`, `args = ["-e", ${tomlBasicString(script)}]`];
|
|
482
|
+
}
|
|
483
|
+
export function ensureParallProvider(codexHome, apiUrl, log, opts) {
|
|
484
|
+
withConfigLock(codexHome, log, () => ensureParallProviderLocked(codexHome, apiUrl, opts));
|
|
535
485
|
}
|
|
536
|
-
function ensureParallProviderLocked(codexHome, apiUrl) {
|
|
486
|
+
function ensureParallProviderLocked(codexHome, apiUrl, opts) {
|
|
537
487
|
const configPath = path.join(codexHome, 'config.toml');
|
|
538
488
|
const baseUrl = apiUrl.replace(/\/$/, '') + '/api/llm/v1';
|
|
539
489
|
try {
|
|
@@ -550,14 +500,13 @@ function ensureParallProviderLocked(codexHome, apiUrl) {
|
|
|
550
500
|
const providerBlock = [
|
|
551
501
|
sectionHeader,
|
|
552
502
|
'name = "Parall Proxy"',
|
|
553
|
-
`base_url =
|
|
503
|
+
`base_url = ${tomlBasicString(baseUrl)}`,
|
|
554
504
|
'wire_api = "responses"',
|
|
555
505
|
'supports_websockets = false',
|
|
556
506
|
'requires_openai_auth = false',
|
|
557
507
|
'',
|
|
558
508
|
authHeader,
|
|
559
|
-
|
|
560
|
-
'args = ["OPENAI_API_KEY"]',
|
|
509
|
+
...parallAuthLines(opts),
|
|
561
510
|
].join('\n');
|
|
562
511
|
const headerIdx = content.indexOf(sectionHeader);
|
|
563
512
|
if (headerIdx !== -1) {
|
|
@@ -585,18 +534,27 @@ function findSectionEnd(content, fromIndex) {
|
|
|
585
534
|
const nextHeader = content.indexOf('\n[', fromIndex);
|
|
586
535
|
return nextHeader === -1 ? content.length : nextHeader;
|
|
587
536
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
537
|
+
/** Human-inspectable copy of the prompt; also the migration's authorship proof. */
|
|
538
|
+
function systemPromptCopyPath(workspaceDir) {
|
|
539
|
+
return path.join(workspaceDir, '.parall', 'system-prompt.md');
|
|
540
|
+
}
|
|
541
|
+
// writeCodexSystemPrompt (re)builds the platform prompt and refreshes its
|
|
542
|
+
// human-inspectable reference copy (.parall/system-prompt.md). Delivery to
|
|
543
|
+
// the CLI is the app-server per-thread `developerInstructions` param — NOT
|
|
544
|
+
// a workspace config file.
|
|
591
545
|
// capabilityFragments are platform-derived capability declarations
|
|
592
546
|
// (agents.capabilities[].fragment) — placed after the platform reference
|
|
593
547
|
// guide, before skill references; empty/absent = no capability section.
|
|
594
|
-
// Split out from ensureCodexWorkspace so the bridge can
|
|
595
|
-
// platform-config heat-update
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
//
|
|
599
|
-
//
|
|
548
|
+
// Split out from ensureCodexWorkspace so the bridge can rebuild the prompt on
|
|
549
|
+
// a platform-config heat-update and hand the new value to
|
|
550
|
+
// adapter.updateConfig. Delivery to threads is the adapter's job: baked into
|
|
551
|
+
// new threads at thread/start; converged onto the persisted main thread via
|
|
552
|
+
// the lazy restart's fresh-process thread/resume (canonical configuration)
|
|
553
|
+
// plus an explicit compaction (model-visible context) — see
|
|
554
|
+
// src/instructions-refresh.ts for the two-plane semantics.
|
|
555
|
+
// Throws on failure BY DESIGN — at boot the prompt is mandatory, so a write
|
|
556
|
+
// failure must fail startup loudly; the refresh hot path wraps this in
|
|
557
|
+
// try/catch and retries next refresh.
|
|
600
558
|
export function writeCodexSystemPrompt(workspaceDir, agentIdentity, capabilityFragments) {
|
|
601
559
|
const parts = [
|
|
602
560
|
buildIdentity(agentIdentity),
|
|
@@ -609,18 +567,60 @@ export function writeCodexSystemPrompt(workspaceDir, agentIdentity, capabilityFr
|
|
|
609
567
|
}
|
|
610
568
|
parts.push(buildSkillReferences(workspaceDir));
|
|
611
569
|
const systemPrompt = parts.join('\n\n');
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
570
|
+
// Reference copy the agent can read as a file. The prompt itself is
|
|
571
|
+
// delivered per-thread via the app-server `developerInstructions` param —
|
|
572
|
+
// deliberately NOT via workspace `.codex/config.toml`, which codex only
|
|
573
|
+
// loads for workspaces marked trusted in the operator's global config:
|
|
574
|
+
// that coupled the platform prompt to codex's interactive workspace-trust
|
|
575
|
+
// concept and forced the bridge to write trust entries into the
|
|
576
|
+
// operator's own ~/.codex on shared homes.
|
|
577
|
+
fs.mkdirSync(path.join(workspaceDir, '.parall'), { recursive: true });
|
|
578
|
+
fs.writeFileSync(systemPromptCopyPath(workspaceDir), systemPrompt, 'utf8');
|
|
579
|
+
return systemPrompt;
|
|
619
580
|
}
|
|
620
581
|
export function ensureCodexWorkspace(workspaceDir, log, agentIdentity, capabilityFragments) {
|
|
621
582
|
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
583
|
+
// The ONE ordering rule this bootstrap has to get right.
|
|
584
|
+
//
|
|
585
|
+
// `.parall/system-prompt.md` is the migration's authorship proof, and
|
|
586
|
+
// writeCodexSystemPrompt below overwrites it. It is therefore legacy-era
|
|
587
|
+
// evidence for exactly one boot, so the migration must take its one-way claim
|
|
588
|
+
// and consume the proof BEFORE that write. The migration refuses to delete
|
|
589
|
+
// anything once the claim exists, so no later boot can judge the legacy config
|
|
590
|
+
// against a prompt this bridge wrote itself. A claim that cannot be persisted
|
|
591
|
+
// throws from here — the boot dies with the evidence intact rather than
|
|
592
|
+
// manufacturing a prompt a later boot would mistake for it.
|
|
593
|
+
//
|
|
594
|
+
// The proof goes in as a THUNK: reading it warns when it exists but cannot be
|
|
595
|
+
// read, and a boot that already lost (or never had) the claim must stay
|
|
596
|
+
// silent. Handing over an evaluated value would fire that warning before the
|
|
597
|
+
// claim is even checked.
|
|
598
|
+
runLegacyWorkspaceConfigMigration(workspaceDir, () => readAuthorshipProof(workspaceDir, log), log);
|
|
622
599
|
// Boot: let a write failure PROPAGATE (fatal) — startup must not proceed
|
|
623
600
|
// without the platform prompt. The refresh path tolerates failure.
|
|
624
|
-
writeCodexSystemPrompt(workspaceDir, agentIdentity, capabilityFragments);
|
|
601
|
+
const systemPrompt = writeCodexSystemPrompt(workspaceDir, agentIdentity, capabilityFragments);
|
|
625
602
|
writeSkillFiles(path.join(workspaceDir, '.parall', 'skills'));
|
|
603
|
+
return systemPrompt;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* The previous boot's prompt copy — the migration's authorship proof.
|
|
607
|
+
*
|
|
608
|
+
* ENOENT is the ordinary first boot in a workspace: absent, no warning. Any
|
|
609
|
+
* other error means the proof EXISTS but could not be read, which is a
|
|
610
|
+
* different fact and must not be reported as "never bootstrapped here" — the
|
|
611
|
+
* migration would otherwise preserve a genuinely stale config while telling the
|
|
612
|
+
* operator the wrong reason. Both map to "don't delete"; only the diagnostic
|
|
613
|
+
* differs.
|
|
614
|
+
*/
|
|
615
|
+
function readAuthorshipProof(workspaceDir, log) {
|
|
616
|
+
const proofPath = systemPromptCopyPath(workspaceDir);
|
|
617
|
+
try {
|
|
618
|
+
return { kind: 'present', prompt: fs.readFileSync(proofPath, 'utf8') };
|
|
619
|
+
}
|
|
620
|
+
catch (err) {
|
|
621
|
+
if (err.code === 'ENOENT')
|
|
622
|
+
return { kind: 'absent' };
|
|
623
|
+
log?.warn(`could not read the authorship proof ${proofPath}: ${String(err)}`);
|
|
624
|
+
return { kind: 'unreadable' };
|
|
625
|
+
}
|
|
626
626
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/codex-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.46.0",
|
|
4
4
|
"description": "Codex CLI bridge runtime for self-hosted Parall agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
"src"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"
|
|
29
|
-
"@parall/
|
|
30
|
-
"@parall/
|
|
31
|
-
"@parall/sdk": "1.44.0"
|
|
28
|
+
"@parall/agent-core": "1.46.0",
|
|
29
|
+
"@parall/cli": "1.46.0",
|
|
30
|
+
"@parall/sdk": "1.46.0"
|
|
32
31
|
},
|
|
33
32
|
"devDependencies": {
|
|
34
33
|
"@types/node": "^22.0.0",
|
|
34
|
+
"smol-toml": "^1.6.1",
|
|
35
35
|
"typescript": "^5.7.0"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import { ensureLocalAttachmentGitExclude } from '@parall/agent-core/internal/attachment-input';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OS-level plumbing for the `codex app-server` child process: the cwd invariant
|
|
7
|
+
* it requires (a git repo), and the Windows spawn/kill quirks.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const IS_WIN32 = process.platform === 'win32';
|
|
11
|
+
|
|
12
|
+
export function quoteWin32Arg(arg: string): string {
|
|
13
|
+
if (!/[\s"&|^<>()]/.test(arg)) return arg;
|
|
14
|
+
return `"${arg.replace(/"/g, '""')}"`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function killWin32Tree(pid: number): boolean {
|
|
18
|
+
try {
|
|
19
|
+
execSync(`taskkill /T /F /PID ${pid}`, { windowsHide: true, stdio: 'ignore' });
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ensureGitRepo(workingDirectory: string): void {
|
|
27
|
+
fs.mkdirSync(workingDirectory, { recursive: true });
|
|
28
|
+
// Only `git init` if the workspace isn't already inside any git repo. A
|
|
29
|
+
// bare existsSync(.git) check would miss the common case of a user pointing
|
|
30
|
+
// PRLL_WORKSPACE_DIR at a subdirectory of their existing project,
|
|
31
|
+
// and silently creating a nested repo there would mangle their layout.
|
|
32
|
+
//
|
|
33
|
+
// The exclude write sits inside this try, so in principle its failure would
|
|
34
|
+
// read as "not a repo" and fall through to `git init` in the user's existing
|
|
35
|
+
// repository. It cannot: the helper swallows its own errors. Deliberately left
|
|
36
|
+
// as-is rather than fixed under review — latent, not live, and out of this
|
|
37
|
+
// PR's scope: docs/tech-debt/codex-ensure-git-repo-exclude-coupling.md.
|
|
38
|
+
try {
|
|
39
|
+
execSync('git rev-parse --is-inside-work-tree', { cwd: workingDirectory, stdio: 'pipe' });
|
|
40
|
+
ensureLocalAttachmentGitExclude(workingDirectory);
|
|
41
|
+
return;
|
|
42
|
+
} catch {
|
|
43
|
+
// Not inside a repo — fall through to init.
|
|
44
|
+
}
|
|
45
|
+
const env = {
|
|
46
|
+
...process.env,
|
|
47
|
+
GIT_AUTHOR_NAME: 'parall-codex-agent',
|
|
48
|
+
GIT_AUTHOR_EMAIL: 'agent@parall.local',
|
|
49
|
+
GIT_COMMITTER_NAME: 'parall-codex-agent',
|
|
50
|
+
GIT_COMMITTER_EMAIL: 'agent@parall.local',
|
|
51
|
+
};
|
|
52
|
+
try {
|
|
53
|
+
execSync('git init', { cwd: workingDirectory, stdio: 'pipe', env });
|
|
54
|
+
execSync('git commit --allow-empty -m init', { cwd: workingDirectory, stdio: 'pipe', env });
|
|
55
|
+
ensureLocalAttachmentGitExclude(workingDirectory);
|
|
56
|
+
} catch {
|
|
57
|
+
// Non-fatal: codex app-server may still accept a bare directory. Let it raise at turn time.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { PreparedLocalImage } from '@parall/agent-core/internal/attachment-input';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wire shapes of the `codex app-server` JSON-RPC payloads: what we send as turn
|
|
5
|
+
* input, and how we read ids back out of responses and notifications. The
|
|
6
|
+
* protocol has shifted between CLI versions, so the tolerant field probing lives
|
|
7
|
+
* here rather than being spread through the adapter.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type CodexTurnInput = { type: 'text'; text: string } | { type: 'localImage'; path: string };
|
|
11
|
+
|
|
12
|
+
export function buildTurnInput(body: string, images: PreparedLocalImage[]): CodexTurnInput[] {
|
|
13
|
+
return [
|
|
14
|
+
{ type: 'text', text: body },
|
|
15
|
+
...images.map((image) => ({ type: 'localImage' as const, path: image.localPath })),
|
|
16
|
+
];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function extractThreadId(result: unknown): string | undefined {
|
|
20
|
+
if (!result || typeof result !== 'object') return undefined;
|
|
21
|
+
const r = result as Record<string, unknown>;
|
|
22
|
+
if (typeof r.threadId === 'string') return r.threadId;
|
|
23
|
+
const thread = r.thread as Record<string, unknown> | undefined;
|
|
24
|
+
if (thread && typeof thread.id === 'string') return thread.id;
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function extractTurnId(result: unknown): string | undefined {
|
|
29
|
+
if (!result || typeof result !== 'object') return undefined;
|
|
30
|
+
const r = result as Record<string, unknown>;
|
|
31
|
+
if (typeof r.turnId === 'string') return r.turnId;
|
|
32
|
+
const turn = r.turn as Record<string, unknown> | undefined;
|
|
33
|
+
if (turn && typeof turn.id === 'string') return turn.id;
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function extractThreadIdFromNotification(params: unknown): string | undefined {
|
|
38
|
+
if (!params || typeof params !== 'object') return undefined;
|
|
39
|
+
const p = params as Record<string, unknown>;
|
|
40
|
+
if (typeof p.threadId === 'string') return p.threadId;
|
|
41
|
+
const thread = p.thread as Record<string, unknown> | undefined;
|
|
42
|
+
if (thread && typeof thread.id === 'string') return thread.id;
|
|
43
|
+
const meta = (p._meta ?? p.meta) as Record<string, unknown> | undefined;
|
|
44
|
+
if (meta && typeof meta.threadId === 'string') return meta.threadId;
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|