@parall/codex-agent 1.42.1 → 1.43.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.
@@ -7,6 +7,14 @@ type CodexAppServerAdapterOptions = Pick<CodexAgentConfig, 'approvalPolicy' | 'c
7
7
  contextFilePath?: string;
8
8
  contextDirPath?: string;
9
9
  useParallProvider?: boolean;
10
+ /**
11
+ * Channel-capability shim directory (agent-core's capabilityBinDir),
12
+ * prepended to the subprocess PATH unconditionally. The DIRECTORY is
13
+ * constant for the bridge's lifetime; its CONTENT tracks capability
14
+ * grants, so a grant or revocation reaches even a long-lived app-server
15
+ * on its next shell command — no respawn needed.
16
+ */
17
+ capabilityBinDir?: string;
10
18
  };
11
19
  export declare class CodexAppServerAdapter implements DispatchAdapter {
12
20
  private readonly opts;
@@ -41,6 +49,16 @@ export declare class CodexAppServerAdapter implements DispatchAdapter {
41
49
  forkSession({ sessionKey: parentSessionKey }: ForkOpts): Promise<ForkSessionHandle | null>;
42
50
  cleanupFork({ fork }: CleanupForkOpts): void;
43
51
  private logForkFailure;
52
+ /**
53
+ * Lazily restart the app-server before the NEXT turn: the workspace
54
+ * config.toml (developer_instructions, carrying capability fragments) is
55
+ * loaded at process start, so a changed fragment set needs a respawn to
56
+ * reach the prompt. Deferred to the next dispatch with no active turns —
57
+ * never kills an in-flight turn; thread state survives via thread/resume.
58
+ */
59
+ requestProcessRestart(): void;
60
+ private restartRequested;
61
+ private applyPendingRestart;
44
62
  stop(): Promise<void>;
45
63
  private ensureStarted;
46
64
  private doStart;
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AASA,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;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,CAAC;AAqCF,qBAAa,qBAAsB,YAAW,eAAe;IA+B/C,OAAO,CAAC,QAAQ,CAAC,IAAI;IA9BjC,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,aAAa,CAA6B;IAC3D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA6B;IAC/D,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;IAMhF,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAqB/E,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAUvC,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAI1C,QAAQ,CAAC,EACd,KAAK,EACL,YAAY,EACZ,UAAU,EACV,OAAO,GACR,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IA6P7C,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;IAgChG,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAIrC,OAAO,CAAC,cAAc;IAKhB,IAAI;YAiBI,aAAa;YA2Bb,OAAO;IAuFrB,OAAO,CAAC,qBAAqB;YAmCf,UAAU;IA6CxB,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;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAqCF,qBAAa,qBAAsB,YAAW,eAAe;IA+B/C,OAAO,CAAC,QAAQ,CAAC,IAAI;IA9BjC,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,aAAa,CAA6B;IAC3D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA6B;IAC/D,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;IAMhF,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAqB/E,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAUvC,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAI1C,QAAQ,CAAC,EACd,KAAK,EACL,YAAY,EACZ,UAAU,EACV,OAAO,GACR,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IA8P7C,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;IAgChG,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAIrC,OAAO,CAAC,cAAc;IAKtB;;;;;;OAMG;IACH,qBAAqB,IAAI,IAAI;IAI7B,OAAO,CAAC,gBAAgB,CAAS;YAEnB,mBAAmB;IAS3B,IAAI;YAuBI,aAAa;YA2Bb,OAAO;IAuGrB,OAAO,CAAC,qBAAqB;YAmCf,UAAU;IA6CxB,OAAO,CAAC,iBAAiB;CA2B1B"}
package/dist/dispatch.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { execSync, spawn } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
4
5
  import { appendPreparedLocalAttachmentRefs, ensureLocalAttachmentGitExclude, pinLocalAttachmentPaths, } from '@parall/agent-core/internal/attachment-input';
5
6
  import { normalizeApprovalPolicy, normalizeSandbox } from './config.js';
6
7
  import { EventMapper } from './event-mapping.js';
@@ -125,6 +126,7 @@ export class CodexAppServerAdapter {
125
126
  }
126
127
  (this.opts.log ?? context.log)?.warn?.(`pending steer invalidated (subprocess died); falling through to normal dispatch`);
127
128
  }
129
+ await this.applyPendingRestart(context.log);
128
130
  await this.ensureStarted(context.log);
129
131
  // After ensureStarted resolves the subprocess could still die before we
130
132
  // capture the client (handleSubprocessClose nulls this.client). Yield a
@@ -405,6 +407,24 @@ export class CodexAppServerAdapter {
405
407
  this.opts.log?.warn?.(`thread/fork failed: ${errToString(err)}`);
406
408
  return null;
407
409
  }
410
+ /**
411
+ * Lazily restart the app-server before the NEXT turn: the workspace
412
+ * config.toml (developer_instructions, carrying capability fragments) is
413
+ * loaded at process start, so a changed fragment set needs a respawn to
414
+ * reach the prompt. Deferred to the next dispatch with no active turns —
415
+ * never kills an in-flight turn; thread state survives via thread/resume.
416
+ */
417
+ requestProcessRestart() {
418
+ this.restartRequested = true;
419
+ }
420
+ restartRequested = false;
421
+ async applyPendingRestart(log) {
422
+ if (!this.restartRequested || this.activeTurns.size > 0)
423
+ return;
424
+ this.restartRequested = false;
425
+ (log ?? this.opts.log)?.info?.('restarting codex app-server to pick up updated developer_instructions');
426
+ await this.stop();
427
+ }
408
428
  async stop() {
409
429
  this.stopping = true;
410
430
  const proc = this.proc;
@@ -414,6 +434,12 @@ export class CodexAppServerAdapter {
414
434
  this.initialized = false;
415
435
  this.activeTurnIds.clear();
416
436
  this.pendingInjections.clear();
437
+ // Resume markers are process-local facts. Clear them HERE, synchronously:
438
+ // the old child's close callback sees the replaced refs and skips its
439
+ // cleanup as stale, so a marker surviving stop() would make the next
440
+ // dispatch treat the persisted thread as already resumed in the NEW
441
+ // process and lose conversation continuity when turn/start rejects.
442
+ this.resumedThreadIds.clear();
417
443
  if (client)
418
444
  client.dispose(new Error('adapter stopped'));
419
445
  if (proc && proc.exitCode === null && proc.signalCode === null) {
@@ -468,6 +494,22 @@ export class CodexAppServerAdapter {
468
494
  FORCE_COLOR: '0',
469
495
  NO_COLOR: '1',
470
496
  };
497
+ // Channel-capability shims resolve ahead of any globally-installed CLI of
498
+ // the same name (the shim owns auth; the real binary is found further
499
+ // down the PATH by the shim itself). On Windows env keys are
500
+ // case-insensitive and the host key is usually `Path`; after the
501
+ // plain-object spread a literal `PATH` write would SPLIT the key
502
+ // (undefined which wins in the child), so reuse the parent's casing. On
503
+ // POSIX keys are case-SENSITIVE — match `PATH` exactly.
504
+ if (this.opts.capabilityBinDir) {
505
+ const pathKey = IS_WIN32
506
+ ? (Object.keys(env).find((k) => k.toUpperCase() === 'PATH') ?? 'PATH')
507
+ : 'PATH';
508
+ const existing = env[pathKey];
509
+ env[pathKey] = existing
510
+ ? `${this.opts.capabilityBinDir}${path.delimiter}${existing}`
511
+ : this.opts.capabilityBinDir;
512
+ }
471
513
  if (this.opts.contextFilePath) {
472
514
  env.PRLL_CONTEXT_FILE = this.opts.contextFilePath;
473
515
  }
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, createPlatformConfigManager, createLogger, createOtelLogger, childLogger, deriveModelIsPin, resolveRuntimeModel, parseShutdownDeadlineMs, parseForkDeadlineMs, parseDispatchDeadlineMs, parseProviderConfig, clearAllProviderCreds, llmSource, initAgentTelemetry, } from '@parall/agent-core';
3
+ import { ParallAgentGateway, capabilityBinDir, createPlatformConfigManager, createLogger, createOtelLogger, childLogger, deriveModelIsPin, materializeChannelCapabilities, resolveRuntimeModel, parseShutdownDeadlineMs, parseForkDeadlineMs, parseDispatchDeadlineMs, parseProviderConfig, clearAllProviderCreds, llmSource, initAgentTelemetry, } from '@parall/agent-core';
4
4
  import { ApiError, ParallClient, ParallWs } from '@parall/sdk';
5
5
  import { buildCodexRuntimeKey, contextFilePathForSession, dispatchContextDirPath, 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, ensureParallProvider, ensureWorkspaceTrusted, isParallProxyMode, } from './workspace.js';
8
+ import { ensureCodexWorkspace, ensureParallProvider, ensureWorkspaceTrusted, isParallProxyMode, writeCodexSystemPrompt, } from './workspace.js';
9
9
  const log = createLogger('codex-agent');
10
10
  let activeLog = log;
11
11
  async function getAgentMeWithLegacyFallback(client, orgId) {
@@ -58,11 +58,11 @@ async function main() {
58
58
  ensureParallProvider(config.codexHome, config.apiUrl, agentLog);
59
59
  agentLog.info('parall custom provider configured (Responses API HTTP/SSE mode)');
60
60
  }
61
- ensureCodexWorkspace(config.workspaceDir, agentLog, {
61
+ const agentIdentity = {
62
62
  userId: agentUserId,
63
63
  displayName: me.display_name,
64
64
  description: me.agent_profile?.description ?? undefined,
65
- });
65
+ };
66
66
  const runtimeKey = config.runtimeKey || buildCodexRuntimeKey(agentUserId);
67
67
  const mainContextFilePath = contextFilePathForSession(config.stateDir, runtimeKey);
68
68
  const sessionStateFilePath = sessionStateFilePathForRuntime(config.stateDir, runtimeKey);
@@ -79,6 +79,21 @@ async function main() {
79
79
  log: agentLog,
80
80
  });
81
81
  const platformDefaults = await configMgr.fetch();
82
+ // Channel capabilities: ONE call materializes the local artifacts (the
83
+ // lark-cli auth shim under capabilityBinDir) and yields the system-prompt
84
+ // fragments, so declaration and shim can never diverge within a refresh.
85
+ // No freshness gate — a stale (LKG-cached) declaration fail-closes at the
86
+ // mint endpoint, which re-evaluates the grant on every token request.
87
+ const applyChannelCapabilities = () => {
88
+ const caps = configMgr.capabilities();
89
+ materializeChannelCapabilities(config.stateDir, caps, agentLog);
90
+ return caps.map((c) => c.fragment);
91
+ };
92
+ // Assemble the workspace AFTER the first config fetch so
93
+ // developer_instructions carries the capability declarations from boot.
94
+ const bootCapabilityFragments = applyChannelCapabilities();
95
+ let lastCapabilityFragments = bootCapabilityFragments.join('\n\n');
96
+ ensureCodexWorkspace(config.workspaceDir, agentLog, agentIdentity, bootCapabilityFragments);
82
97
  // Model precedence: operator PIN (override) > env > server FLOOR. The server
83
98
  // now says which it is via model_is_pin (deriveModelIsPin presence-gates the
84
99
  // dual-read: old servers omit it → fall back to model_management). A PIN beats
@@ -104,6 +119,7 @@ async function main() {
104
119
  contextFilePath: mainContextFilePath,
105
120
  contextDirPath: dispatchContextDirPath(config.stateDir),
106
121
  useParallProvider,
122
+ capabilityBinDir: capabilityBinDir(config.stateDir),
107
123
  });
108
124
  // Shared by onConfigUpdate + onSessionReady. /agents/me is only consumed as
109
125
  // deriveModelIsPin's legacy fallback when the server omits model_is_pin
@@ -129,6 +145,24 @@ async function main() {
129
145
  model: resolveRuntimeModel(isPin, updated.model, config.model) ?? null,
130
146
  reasoningEffort: updated.thinkingEffort ?? config.reasoningEffort ?? null,
131
147
  });
148
+ // Capability heat-update: re-materialize shims + rewrite the prompt
149
+ // surfaces. The write is refresh-tolerant (warn + retry next refresh);
150
+ // only a SUCCESSFUL write with a changed fragment set schedules the
151
+ // lazy app-server restart (developer_instructions loads at spawn).
152
+ const fragments = applyChannelCapabilities();
153
+ const joinedFragments = fragments.join('\n\n');
154
+ let promptWritten = true;
155
+ try {
156
+ writeCodexSystemPrompt(config.workspaceDir, agentIdentity, fragments);
157
+ }
158
+ catch (err) {
159
+ promptWritten = false;
160
+ agentLog.warn(`system prompt refresh write failed (retrying next refresh): ${String(err)}`);
161
+ }
162
+ if (promptWritten && joinedFragments !== lastCapabilityFragments) {
163
+ lastCapabilityFragments = joinedFragments;
164
+ adapter.requestProcessRestart();
165
+ }
132
166
  };
133
167
  const gateway = new ParallAgentGateway({
134
168
  accountId: agentUserId,
@@ -152,6 +186,9 @@ async function main() {
152
186
  },
153
187
  dispatchAdapter: adapter,
154
188
  log: agentLog,
189
+ // Live capability view for hint routing: the channel reply hint points
190
+ // at the vendor CLI only while the grant is active.
191
+ getCapabilityKeys: () => configMgr.capabilities().map((c) => c.key),
155
192
  shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
156
193
  forkDeadlineMs: parseForkDeadlineMs(process.env.PRLL_FORK_DEADLINE_MS),
157
194
  dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
@@ -120,7 +120,8 @@ export declare function isParallProxyMode(env?: NodeJS.ProcessEnv): boolean;
120
120
  export declare function ensureParallProvider(codexHome: string, apiUrl: string, log?: {
121
121
  warn: (msg: string) => void;
122
122
  }): void;
123
+ export declare function writeCodexSystemPrompt(workspaceDir: string, agentIdentity?: AgentIdentity, capabilityFragments?: string[]): void;
123
124
  export declare function ensureCodexWorkspace(workspaceDir: string, log?: {
124
125
  warn: (msg: string) => void;
125
- }, agentIdentity?: AgentIdentity): void;
126
+ }, agentIdentity?: AgentIdentity, capabilityFragments?: string[]): void;
126
127
  //# sourceMappingURL=workspace.d.ts.map
@@ -1 +1 @@
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,eAAO,MAAM,mBAAmB;;;;CAI/B,CAAC;AAUF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;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;;;;;;;;;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,CAEN;AAqGD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAK/E;AAED;;;;;;;;;;;;;GAaG;AACH,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,GACpC,IAAI,CAEN;AAwDD,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"}
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,eAAO,MAAM,mBAAmB;;;;CAI/B,CAAC;AAUF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;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;;;;;;;;;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,CAEN;AAqGD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAK/E;AAED;;;;;;;;;;;;;GAaG;AACH,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,GACpC,IAAI,CAEN;AAoED,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,aAAa,EAC7B,mBAAmB,CAAC,EAAE,MAAM,EAAE,GAC7B,IAAI,CAqBN;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,IAAI,CAQN"}
package/dist/workspace.js CHANGED
@@ -585,21 +585,42 @@ function findSectionEnd(content, fromIndex) {
585
585
  const nextHeader = content.indexOf('\n[', fromIndex);
586
586
  return nextHeader === -1 ? content.length : nextHeader;
587
587
  }
588
- export function ensureCodexWorkspace(workspaceDir, log, agentIdentity) {
589
- const systemPrompt = [
588
+ // writeCodexSystemPrompt (re)writes the standing platform prompt surfaces:
589
+ // .parall/system-prompt.md (human-inspectable copy) and the workspace
590
+ // .codex/config.toml developer_instructions (what the CLI actually loads).
591
+ // capabilityFragments are platform-derived capability declarations
592
+ // (agents.capabilities[].fragment) — placed after the platform reference
593
+ // guide, before skill references; empty/absent = no capability section.
594
+ // Split out from ensureCodexWorkspace so the bridge can rewrite on
595
+ // platform-config heat-update; the app-server reads workspace config at
596
+ // process start, so the caller pairs a CHANGED fragment set with
597
+ // adapter.requestProcessRestart(). Throws on failure BY DESIGN — at boot the
598
+ // prompt is mandatory, so a write failure must fail startup loudly; the
599
+ // refresh hot path wraps this in try/catch and retries next refresh.
600
+ export function writeCodexSystemPrompt(workspaceDir, agentIdentity, capabilityFragments) {
601
+ const parts = [
590
602
  buildIdentity(agentIdentity),
591
603
  BRIDGE_WORKSPACE_INSTRUCTIONS,
592
604
  PRLL_BEHAVIOR,
593
605
  PRLL_REFERENCE_GUIDE,
594
- buildSkillReferences(workspaceDir),
595
- ].join('\n\n');
596
- fs.mkdirSync(workspaceDir, { recursive: true });
606
+ ];
607
+ if (capabilityFragments && capabilityFragments.length > 0) {
608
+ parts.push(capabilityFragments.join('\n\n'));
609
+ }
610
+ parts.push(buildSkillReferences(workspaceDir));
611
+ const systemPrompt = parts.join('\n\n');
597
612
  const parallDir = path.join(workspaceDir, '.parall');
598
613
  fs.mkdirSync(parallDir, { recursive: true });
599
614
  fs.writeFileSync(path.join(parallDir, 'system-prompt.md'), systemPrompt, 'utf8');
600
- writeSkillFiles(path.join(parallDir, 'skills'));
601
615
  const codexConfigDir = path.join(workspaceDir, '.codex');
602
616
  fs.mkdirSync(codexConfigDir, { recursive: true });
603
617
  const toml = `developer_instructions = """\n${systemPrompt.replace(/\\/g, '\\\\').replace(/"""/g, '\\"""')}\n"""\n`;
604
618
  fs.writeFileSync(path.join(codexConfigDir, 'config.toml'), toml, 'utf8');
605
619
  }
620
+ export function ensureCodexWorkspace(workspaceDir, log, agentIdentity, capabilityFragments) {
621
+ fs.mkdirSync(workspaceDir, { recursive: true });
622
+ // Boot: let a write failure PROPAGATE (fatal) — startup must not proceed
623
+ // without the platform prompt. The refresh path tolerates failure.
624
+ writeCodexSystemPrompt(workspaceDir, agentIdentity, capabilityFragments);
625
+ writeSkillFiles(path.join(workspaceDir, '.parall', 'skills'));
626
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/codex-agent",
3
- "version": "1.42.1",
3
+ "version": "1.43.0",
4
4
  "description": "Codex CLI bridge runtime for self-hosted Parall agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,9 +26,9 @@
26
26
  ],
27
27
  "dependencies": {
28
28
  "smol-toml": "^1.6.1",
29
- "@parall/agent-core": "1.42.1",
30
- "@parall/cli": "1.42.1",
31
- "@parall/sdk": "1.42.1"
29
+ "@parall/agent-core": "1.43.0",
30
+ "@parall/cli": "1.43.0",
31
+ "@parall/sdk": "1.43.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^22.0.0",
package/src/dispatch.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { execSync, spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
4
5
  import {
5
6
  appendPreparedLocalAttachmentRefs,
6
7
  ensureLocalAttachmentGitExclude,
@@ -37,6 +38,14 @@ type CodexAppServerAdapterOptions = Pick<
37
38
  contextFilePath?: string;
38
39
  contextDirPath?: string;
39
40
  useParallProvider?: boolean;
41
+ /**
42
+ * Channel-capability shim directory (agent-core's capabilityBinDir),
43
+ * prepended to the subprocess PATH unconditionally. The DIRECTORY is
44
+ * constant for the bridge's lifetime; its CONTENT tracks capability
45
+ * grants, so a grant or revocation reaches even a long-lived app-server
46
+ * on its next shell command — no respawn needed.
47
+ */
48
+ capabilityBinDir?: string;
40
49
  };
41
50
 
42
51
  type TurnEventEnvelope =
@@ -174,6 +183,7 @@ export class CodexAppServerAdapter implements DispatchAdapter {
174
183
  );
175
184
  }
176
185
 
186
+ await this.applyPendingRestart(context.log);
177
187
  await this.ensureStarted(context.log);
178
188
  // After ensureStarted resolves the subprocess could still die before we
179
189
  // capture the client (handleSubprocessClose nulls this.client). Yield a
@@ -453,6 +463,28 @@ export class CodexAppServerAdapter implements DispatchAdapter {
453
463
  return null;
454
464
  }
455
465
 
466
+ /**
467
+ * Lazily restart the app-server before the NEXT turn: the workspace
468
+ * config.toml (developer_instructions, carrying capability fragments) is
469
+ * loaded at process start, so a changed fragment set needs a respawn to
470
+ * reach the prompt. Deferred to the next dispatch with no active turns —
471
+ * never kills an in-flight turn; thread state survives via thread/resume.
472
+ */
473
+ requestProcessRestart(): void {
474
+ this.restartRequested = true;
475
+ }
476
+
477
+ private restartRequested = false;
478
+
479
+ private async applyPendingRestart(log?: GatewayLogger): Promise<void> {
480
+ if (!this.restartRequested || this.activeTurns.size > 0) return;
481
+ this.restartRequested = false;
482
+ (log ?? this.opts.log)?.info?.(
483
+ 'restarting codex app-server to pick up updated developer_instructions',
484
+ );
485
+ await this.stop();
486
+ }
487
+
456
488
  async stop() {
457
489
  this.stopping = true;
458
490
  const proc = this.proc;
@@ -462,6 +494,12 @@ export class CodexAppServerAdapter implements DispatchAdapter {
462
494
  this.initialized = false;
463
495
  this.activeTurnIds.clear();
464
496
  this.pendingInjections.clear();
497
+ // Resume markers are process-local facts. Clear them HERE, synchronously:
498
+ // the old child's close callback sees the replaced refs and skips its
499
+ // cleanup as stale, so a marker surviving stop() would make the next
500
+ // dispatch treat the persisted thread as already resumed in the NEW
501
+ // process and lose conversation continuity when turn/start rejects.
502
+ this.resumedThreadIds.clear();
465
503
  if (client) client.dispose(new Error('adapter stopped'));
466
504
  if (proc && proc.exitCode === null && proc.signalCode === null) {
467
505
  if (!IS_WIN32 || !proc.pid || !killWin32Tree(proc.pid)) {
@@ -515,6 +553,22 @@ export class CodexAppServerAdapter implements DispatchAdapter {
515
553
  FORCE_COLOR: '0',
516
554
  NO_COLOR: '1',
517
555
  };
556
+ // Channel-capability shims resolve ahead of any globally-installed CLI of
557
+ // the same name (the shim owns auth; the real binary is found further
558
+ // down the PATH by the shim itself). On Windows env keys are
559
+ // case-insensitive and the host key is usually `Path`; after the
560
+ // plain-object spread a literal `PATH` write would SPLIT the key
561
+ // (undefined which wins in the child), so reuse the parent's casing. On
562
+ // POSIX keys are case-SENSITIVE — match `PATH` exactly.
563
+ if (this.opts.capabilityBinDir) {
564
+ const pathKey = IS_WIN32
565
+ ? (Object.keys(env).find((k) => k.toUpperCase() === 'PATH') ?? 'PATH')
566
+ : 'PATH';
567
+ const existing = env[pathKey];
568
+ env[pathKey] = existing
569
+ ? `${this.opts.capabilityBinDir}${path.delimiter}${existing}`
570
+ : this.opts.capabilityBinDir;
571
+ }
518
572
  if (this.opts.contextFilePath) {
519
573
  env.PRLL_CONTEXT_FILE = this.opts.contextFilePath;
520
574
  }
package/src/index.ts CHANGED
@@ -3,11 +3,13 @@
3
3
  import * as os from 'node:os';
4
4
  import {
5
5
  ParallAgentGateway,
6
+ capabilityBinDir,
6
7
  createPlatformConfigManager,
7
8
  createLogger,
8
9
  createOtelLogger,
9
10
  childLogger,
10
11
  deriveModelIsPin,
12
+ materializeChannelCapabilities,
11
13
  resolveRuntimeModel,
12
14
  parseShutdownDeadlineMs,
13
15
  parseForkDeadlineMs,
@@ -34,6 +36,7 @@ import {
34
36
  ensureParallProvider,
35
37
  ensureWorkspaceTrusted,
36
38
  isParallProxyMode,
39
+ writeCodexSystemPrompt,
37
40
  } from './workspace.js';
38
41
 
39
42
  const log = createLogger('codex-agent');
@@ -88,11 +91,11 @@ async function main() {
88
91
  ensureParallProvider(config.codexHome, config.apiUrl, agentLog);
89
92
  agentLog.info('parall custom provider configured (Responses API HTTP/SSE mode)');
90
93
  }
91
- ensureCodexWorkspace(config.workspaceDir, agentLog, {
94
+ const agentIdentity = {
92
95
  userId: agentUserId,
93
96
  displayName: me.display_name,
94
97
  description: me.agent_profile?.description ?? undefined,
95
- });
98
+ };
96
99
  const runtimeKey = config.runtimeKey || buildCodexRuntimeKey(agentUserId);
97
100
  const mainContextFilePath = contextFilePathForSession(config.stateDir, runtimeKey);
98
101
  const sessionStateFilePath = sessionStateFilePathForRuntime(config.stateDir, runtimeKey);
@@ -111,6 +114,21 @@ async function main() {
111
114
  log: agentLog,
112
115
  });
113
116
  const platformDefaults = await configMgr.fetch();
117
+ // Channel capabilities: ONE call materializes the local artifacts (the
118
+ // lark-cli auth shim under capabilityBinDir) and yields the system-prompt
119
+ // fragments, so declaration and shim can never diverge within a refresh.
120
+ // No freshness gate — a stale (LKG-cached) declaration fail-closes at the
121
+ // mint endpoint, which re-evaluates the grant on every token request.
122
+ const applyChannelCapabilities = () => {
123
+ const caps = configMgr.capabilities();
124
+ materializeChannelCapabilities(config.stateDir, caps, agentLog);
125
+ return caps.map((c) => c.fragment);
126
+ };
127
+ // Assemble the workspace AFTER the first config fetch so
128
+ // developer_instructions carries the capability declarations from boot.
129
+ const bootCapabilityFragments = applyChannelCapabilities();
130
+ let lastCapabilityFragments = bootCapabilityFragments.join('\n\n');
131
+ ensureCodexWorkspace(config.workspaceDir, agentLog, agentIdentity, bootCapabilityFragments);
114
132
  // Model precedence: operator PIN (override) > env > server FLOOR. The server
115
133
  // now says which it is via model_is_pin (deriveModelIsPin presence-gates the
116
134
  // dual-read: old servers omit it → fall back to model_management). A PIN beats
@@ -143,6 +161,7 @@ async function main() {
143
161
  contextFilePath: mainContextFilePath,
144
162
  contextDirPath: dispatchContextDirPath(config.stateDir),
145
163
  useParallProvider,
164
+ capabilityBinDir: capabilityBinDir(config.stateDir),
146
165
  });
147
166
 
148
167
  // Shared by onConfigUpdate + onSessionReady. /agents/me is only consumed as
@@ -172,6 +191,23 @@ async function main() {
172
191
  model: resolveRuntimeModel(isPin, updated.model, config.model) ?? null,
173
192
  reasoningEffort: updated.thinkingEffort ?? config.reasoningEffort ?? null,
174
193
  });
194
+ // Capability heat-update: re-materialize shims + rewrite the prompt
195
+ // surfaces. The write is refresh-tolerant (warn + retry next refresh);
196
+ // only a SUCCESSFUL write with a changed fragment set schedules the
197
+ // lazy app-server restart (developer_instructions loads at spawn).
198
+ const fragments = applyChannelCapabilities();
199
+ const joinedFragments = fragments.join('\n\n');
200
+ let promptWritten = true;
201
+ try {
202
+ writeCodexSystemPrompt(config.workspaceDir, agentIdentity, fragments);
203
+ } catch (err) {
204
+ promptWritten = false;
205
+ agentLog.warn(`system prompt refresh write failed (retrying next refresh): ${String(err)}`);
206
+ }
207
+ if (promptWritten && joinedFragments !== lastCapabilityFragments) {
208
+ lastCapabilityFragments = joinedFragments;
209
+ adapter.requestProcessRestart();
210
+ }
175
211
  };
176
212
 
177
213
  const gateway = new ParallAgentGateway({
@@ -196,6 +232,9 @@ async function main() {
196
232
  },
197
233
  dispatchAdapter: adapter,
198
234
  log: agentLog,
235
+ // Live capability view for hint routing: the channel reply hint points
236
+ // at the vendor CLI only while the grant is active.
237
+ getCapabilityKeys: () => configMgr.capabilities().map((c) => c.key),
199
238
  shutdownDeadlineMs: parseShutdownDeadlineMs(process.env.PRLL_SHUTDOWN_DEADLINE_MS),
200
239
  forkDeadlineMs: parseForkDeadlineMs(process.env.PRLL_FORK_DEADLINE_MS),
201
240
  dispatchDeadlineMs: parseDispatchDeadlineMs(process.env.PRLL_DISPATCH_DEADLINE_MS),
package/src/workspace.ts CHANGED
@@ -609,29 +609,56 @@ function findSectionEnd(content: string, fromIndex: number): number {
609
609
  return nextHeader === -1 ? content.length : nextHeader;
610
610
  }
611
611
 
612
- export function ensureCodexWorkspace(
612
+ // writeCodexSystemPrompt (re)writes the standing platform prompt surfaces:
613
+ // .parall/system-prompt.md (human-inspectable copy) and the workspace
614
+ // .codex/config.toml developer_instructions (what the CLI actually loads).
615
+ // capabilityFragments are platform-derived capability declarations
616
+ // (agents.capabilities[].fragment) — placed after the platform reference
617
+ // guide, before skill references; empty/absent = no capability section.
618
+ // Split out from ensureCodexWorkspace so the bridge can rewrite on
619
+ // platform-config heat-update; the app-server reads workspace config at
620
+ // process start, so the caller pairs a CHANGED fragment set with
621
+ // adapter.requestProcessRestart(). Throws on failure BY DESIGN — at boot the
622
+ // prompt is mandatory, so a write failure must fail startup loudly; the
623
+ // refresh hot path wraps this in try/catch and retries next refresh.
624
+ export function writeCodexSystemPrompt(
613
625
  workspaceDir: string,
614
- log?: { warn: (msg: string) => void },
615
626
  agentIdentity?: AgentIdentity,
627
+ capabilityFragments?: string[],
616
628
  ): void {
617
- const systemPrompt = [
629
+ const parts = [
618
630
  buildIdentity(agentIdentity),
619
631
  BRIDGE_WORKSPACE_INSTRUCTIONS,
620
632
  PRLL_BEHAVIOR,
621
633
  PRLL_REFERENCE_GUIDE,
622
- buildSkillReferences(workspaceDir),
623
- ].join('\n\n');
624
-
625
- fs.mkdirSync(workspaceDir, { recursive: true });
634
+ ];
635
+ if (capabilityFragments && capabilityFragments.length > 0) {
636
+ parts.push(capabilityFragments.join('\n\n'));
637
+ }
638
+ parts.push(buildSkillReferences(workspaceDir));
639
+ const systemPrompt = parts.join('\n\n');
626
640
 
627
641
  const parallDir = path.join(workspaceDir, '.parall');
628
642
  fs.mkdirSync(parallDir, { recursive: true });
629
643
  fs.writeFileSync(path.join(parallDir, 'system-prompt.md'), systemPrompt, 'utf8');
630
644
 
631
- writeSkillFiles(path.join(parallDir, 'skills'));
632
-
633
645
  const codexConfigDir = path.join(workspaceDir, '.codex');
634
646
  fs.mkdirSync(codexConfigDir, { recursive: true });
635
647
  const toml = `developer_instructions = """\n${systemPrompt.replace(/\\/g, '\\\\').replace(/"""/g, '\\"""')}\n"""\n`;
636
648
  fs.writeFileSync(path.join(codexConfigDir, 'config.toml'), toml, 'utf8');
637
649
  }
650
+
651
+ export function ensureCodexWorkspace(
652
+ workspaceDir: string,
653
+ log?: { warn: (msg: string) => void },
654
+ agentIdentity?: AgentIdentity,
655
+ capabilityFragments?: string[],
656
+ ): void {
657
+ fs.mkdirSync(workspaceDir, { recursive: true });
658
+
659
+ // Boot: let a write failure PROPAGATE (fatal) — startup must not proceed
660
+ // without the platform prompt. The refresh path tolerates failure.
661
+ writeCodexSystemPrompt(workspaceDir, agentIdentity, capabilityFragments);
662
+
663
+ writeSkillFiles(path.join(workspaceDir, '.parall', 'skills'));
664
+ }