super-dsh 0.1.10-e → 0.1.11-a
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/README.md +38 -2
- package/agent-agy/dist/agent.js +19 -1
- package/agent-claude/dist/agent.js +23 -5
- package/agent-codex/dist/adapter.js +3 -2
- package/agent-codex/dist/agent.js +41 -7
- package/agent-codex/dist/app-server-client.js +9 -2
- package/agent-codex/dist/app-server-protocol.js +6 -2
- package/agent-codex/dist/app-server-wire.js +29 -24
- package/agent-codex/dist/codex-launch.js +10 -32
- package/agent-codex/dist/codex-native-config.js +9 -3
- package/agent-codex/dist/codex-oauth.js +153 -0
- package/agent-codex/dist/codex-provider-gateway.js +36 -38
- package/agent-codex/dist/codex-providers.js +20 -7
- package/agent-codex/dist/codex-runtime-fetch.js +293 -0
- package/agent-codex/dist/codex-runtime.js +120 -0
- package/agent-codex/dist/index.js +20 -12
- package/agent-codex/package.json +8 -3
- package/agent-codex-sdk/dist/adapter.js +3 -2
- package/agent-codex-sdk/dist/agent.js +35 -1
- package/agent-codex-sdk/dist/codex-native-config.js +9 -3
- package/agent-codex-sdk/dist/codex-oauth.js +153 -0
- package/agent-codex-sdk/dist/codex-provider-gateway.js +36 -38
- package/agent-codex-sdk/dist/codex-providers.js +20 -7
- package/agent-codex-sdk/dist/codex-sdk-launch.js +5 -38
- package/agent-codex-sdk/dist/index.js +18 -11
- package/agent-codex-sdk/dist/sdk-runtime.js +3 -3
- package/agent-hermes/dist/agent.js +46 -2
- package/agent-hermes/dist/hermes-client.js +49 -1
- package/agent-omp/dist/agent.js +17 -4
- package/agent-omp-sdk/dist/agent.d.ts +11 -0
- package/agent-omp-sdk/dist/agent.js +17 -4
- package/agent-pi/dist/agent.js +33 -1
- package/client/index.js +10 -7
- package/package.json +7 -5
- package/superd-hub/dist/client/agent-settings.d.ts +1 -1
- package/superd-hub/dist/client/agent-settings.js +2 -2
- package/superd-hub/dist/connection-trust.d.ts +8 -0
- package/superd-hub/dist/connection-trust.js +16 -0
- package/superd-hub/dist/web-trust.d.ts +14 -42
- package/superd-hub/dist/web-trust.js +18 -43
- package/superd-hub/dist/world-mount.d.ts +6 -5
- package/superd-hub/dist/world-mount.js +6 -6
- package/superd-hub/lib/client/index.js +10 -7
- package/superd-hub/package.json +5 -1
- package/agent-codex/README.md +0 -179
- package/agent-codex-sdk/README.md +0 -79
- package/agent-hermes/README.md +0 -145
- package/agent-omp/README.md +0 -94
- package/agent-omp-sdk/README.md +0 -103
- package/agent-pi/README.md +0 -67
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ the foreign store.
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
dsh plugin --profile <name> add super-dsh@0.1.
|
|
17
|
+
dsh plugin --profile <name> add super-dsh@0.1.11
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
That single package composes the whole line (`@pgmi-builds/superd-hub` plus the
|
|
@@ -22,17 +22,53 @@ six primary `@pgmi-builds/agent-*` packages and three OPTIONAL SDK halves).
|
|
|
22
22
|
Restart the profile and the
|
|
23
23
|
home under `<DSH_HOME>/agents/<label>/` on first boot.
|
|
24
24
|
|
|
25
|
+
## Remote web access
|
|
26
|
+
|
|
27
|
+
To use the same authenticated UI through a reverse proxy or LAN relay, enable
|
|
28
|
+
this profile patch:
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
- id: superd-web-trust
|
|
32
|
+
config:
|
|
33
|
+
trustAllHosts: true
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This skips Host/Origin restrictions and enables browser ownership for every
|
|
37
|
+
hostname. Native token-to-cookie login, cookie expiry, and authority binding
|
|
38
|
+
remain in effect. The default is `false`, preserving declared-authority mode.
|
|
39
|
+
It does not change the server's listening address: keep loopback behind Caddy
|
|
40
|
+
and a LAN relay. The test launcher enables this mode without `--host` or
|
|
41
|
+
`--trusted-host` and starts the relay on every test port, including 4999.
|
|
42
|
+
|
|
25
43
|
## Runtimes
|
|
26
44
|
|
|
27
45
|
| Label | Runtime | Integration |
|
|
28
46
|
|---|---|---|
|
|
29
47
|
| OMP | OMP (`~/.omp`) | host `omp` binary over RPC (`omp --mode rpc`, NDJSON v1), lazy per session |
|
|
30
|
-
| Codex | Codex
|
|
48
|
+
| Codex | Codex app-server (`~/.codex`) | desktop core first, CLI and bundled core fallback |
|
|
31
49
|
| Claude | Claude Code (`~/.claude`) | standalone adapter, joined by the hub |
|
|
32
50
|
| Pi | pi coding agent (`~/.pi`) | in-process SDK |
|
|
33
51
|
| Hermes | Hermes (`~/.hermes`) | gateway child |
|
|
34
52
|
| Antigravity | agy SDK | Python bridge (`bridge/agy_bridge.py`) |
|
|
35
53
|
|
|
54
|
+
### Codex in 0.1.11
|
|
55
|
+
|
|
56
|
+
Codex discovers the local OpenAI OAuth login as a read-only provider in the
|
|
57
|
+
model selector. Selecting it explicitly overrides a third-party provider in the
|
|
58
|
+
native config, while credentials and native configuration stay in the native
|
|
59
|
+
home. Third-party providers remain selectable per session.
|
|
60
|
+
|
|
61
|
+
The adapter prefers the installed desktop core, then the CLI, then the bundled
|
|
62
|
+
0.160.0 core (`CODEX_BIN` overrides discovery). Desktop host browser and computer
|
|
63
|
+
use services are separate and are not attached by this adapter.
|
|
64
|
+
|
|
65
|
+
**Provider compatibility:** each provider defines its own message, reasoning,
|
|
66
|
+
and encrypted-state formats. Selecting another provider does not guarantee
|
|
67
|
+
that it can consume the existing session history. Successful continuation
|
|
68
|
+
across endpoints is an observed convenience, not an adapter guarantee.
|
|
69
|
+
The adapter surfaces provider errors; if the selected provider rejects the
|
|
70
|
+
history, start a new session with that provider.
|
|
71
|
+
|
|
36
72
|
### Opt-in SDK halves
|
|
37
73
|
|
|
38
74
|
Three runtimes also ship a preserved in-process SDK transport beside the primary
|
package/agent-agy/dist/agent.js
CHANGED
|
@@ -129,9 +129,27 @@ export class AgyAgent {
|
|
|
129
129
|
this.closeStep();
|
|
130
130
|
this.commitStepStart();
|
|
131
131
|
const callId = ToolCallId(ev.id || `agy-${this.dashTurn}-${this.step}`);
|
|
132
|
+
const argumentsJson = ev.args || "{}";
|
|
133
|
+
// Advertisement-before-execution (validator invariant, 2026-10-04 dev3
|
|
134
|
+
// omp/hermes repair family): agy assistant frames carry only
|
|
135
|
+
// reasoning/text — never tool-call blocks — so every tool/call needs
|
|
136
|
+
// a minimal advertisement assistant frame first; arguments are copied
|
|
137
|
+
// verbatim into both frames (validator: lifecycle arguments ≡
|
|
138
|
+
// advertised, strict string equality).
|
|
139
|
+
const advertisement = createAssistantMessage({
|
|
140
|
+
content: [{ type: "tool-call", id: callId, name: ev.name, arguments: argumentsJson }],
|
|
141
|
+
source: {
|
|
142
|
+
provider: this.options.provider ?? "agy",
|
|
143
|
+
model: this.options.model ?? "",
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
this.session.append("assistant/message", {
|
|
147
|
+
turn: this.dashTurn, step: this.step,
|
|
148
|
+
message: advertisement, stream: [],
|
|
149
|
+
}, { surfaceOp: "append" });
|
|
132
150
|
this.session.append("tool/call", {
|
|
133
151
|
turn: this.dashTurn, step: this.step,
|
|
134
|
-
callId, name: ev.name, arguments:
|
|
152
|
+
callId, name: ev.name, arguments: argumentsJson,
|
|
135
153
|
});
|
|
136
154
|
this.unpairedCalls.push({ callId, name: ev.name });
|
|
137
155
|
}
|
|
@@ -969,8 +969,12 @@ export class ClaudeAgent {
|
|
|
969
969
|
* projection carried — so the `tool_start` flush lands a same-attempt
|
|
970
970
|
* sample and nothing ever stashes across turns.
|
|
971
971
|
*/
|
|
972
|
-
flushAssistant(usage) {
|
|
973
|
-
|
|
972
|
+
flushAssistant(usage, toolCall) {
|
|
973
|
+
// A toolCall rides the SAME frame (advertise+usage on one attempt): the
|
|
974
|
+
// token-meter fold requires every step's assistant frame to carry a usage
|
|
975
|
+
// sample, so a separate no-usage advertisement frame would turn the
|
|
976
|
+
// turn's whole usage disclosure undefined.
|
|
977
|
+
if (toolCall === undefined && this.reasoningBuffer === "" && this.textBuffer === "") {
|
|
974
978
|
if (usage !== undefined)
|
|
975
979
|
trace("flush had usage but no buffered content — sample dropped (no message carries it)");
|
|
976
980
|
return;
|
|
@@ -980,6 +984,9 @@ export class ClaudeAgent {
|
|
|
980
984
|
blocks.push({ type: "reasoning", text: this.reasoningBuffer });
|
|
981
985
|
if (this.textBuffer !== "")
|
|
982
986
|
blocks.push({ type: "text", text: this.textBuffer });
|
|
987
|
+
if (toolCall !== undefined) {
|
|
988
|
+
blocks.push({ type: "tool-call", id: ToolCallId(toolCall.callId), name: toolCall.name, arguments: toolCall.arguments });
|
|
989
|
+
}
|
|
983
990
|
this.reasoningBuffer = "";
|
|
984
991
|
this.textBuffer = "";
|
|
985
992
|
const effectiveUsage = usage ?? this.capturedUsage;
|
|
@@ -1006,13 +1013,23 @@ export class ClaudeAgent {
|
|
|
1006
1013
|
this.streamBridge = undefined;
|
|
1007
1014
|
}
|
|
1008
1015
|
appendToolCall(callId, name, args) {
|
|
1009
|
-
|
|
1016
|
+
// The tool-call block rides the SAME assistant/message as any pending
|
|
1017
|
+
// text (native advertise+usage shape): one attempt per step with the
|
|
1018
|
+
// usage sample AND the advertisement on one frame. A separate no-usage
|
|
1019
|
+
// advertisement frame would satisfy the format validator but break the
|
|
1020
|
+
// token-meter fold (every step's assistant frame needs a usage sample).
|
|
1021
|
+
const argumentsJson = JSON.stringify(args ?? {});
|
|
1022
|
+
this.flushAssistant(undefined, {
|
|
1023
|
+
callId,
|
|
1024
|
+
name,
|
|
1025
|
+
arguments: argumentsJson,
|
|
1026
|
+
});
|
|
1010
1027
|
this.session.append("tool/call", {
|
|
1011
1028
|
turn: this.dashTurn,
|
|
1012
1029
|
step: this.step,
|
|
1013
1030
|
callId: ToolCallId(callId),
|
|
1014
1031
|
name,
|
|
1015
|
-
arguments:
|
|
1032
|
+
arguments: argumentsJson,
|
|
1016
1033
|
});
|
|
1017
1034
|
}
|
|
1018
1035
|
appendToolResult(callId, content, isError) {
|
|
@@ -1193,7 +1210,8 @@ export class ClaudeAgent {
|
|
|
1193
1210
|
// RC-6: the mid-turn flush closes the attempt WITH the sample the
|
|
1194
1211
|
// message itself reported (captured above / on the assistant events).
|
|
1195
1212
|
this.captureProvenance(event);
|
|
1196
|
-
|
|
1213
|
+
// flushes pending text WITH the tool-call block as one assistant
|
|
1214
|
+
// message (advertise+usage on one attempt — see appendToolCall).
|
|
1197
1215
|
this.appendToolCall(callId, name, event.arguments);
|
|
1198
1216
|
break;
|
|
1199
1217
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readEffectiveProviderStore } from "./codex-oauth.js";
|
|
2
|
+
import { resolveCodexHome } from "./codex-store.js";
|
|
1
3
|
/**
|
|
2
4
|
* Codex-backed `LlmAdapter` registered on `ctx.llm` by the Codex provider.
|
|
3
5
|
*
|
|
@@ -23,7 +25,6 @@
|
|
|
23
25
|
* @module agent-codex/adapter
|
|
24
26
|
*/
|
|
25
27
|
import { LlmAdapter, LlmError, } from "@deepseek-ai/dsh-llm";
|
|
26
|
-
import { readProviderStore } from "./codex-providers.js";
|
|
27
28
|
/**
|
|
28
29
|
* The single route every Codex model was served under before the multi-provider
|
|
29
30
|
* rework. No longer registered (a session pinned to it falls back to whatever
|
|
@@ -41,7 +42,7 @@ export function routeIds(store) {
|
|
|
41
42
|
}
|
|
42
43
|
/** The provider entry a route id names, or `undefined`. */
|
|
43
44
|
export function providerFor(worldHome, providerId) {
|
|
44
|
-
return
|
|
45
|
+
return readEffectiveProviderStore(worldHome, resolveCodexHome()).providers.find((entry) => entry.id === providerId);
|
|
45
46
|
}
|
|
46
47
|
export class CodexLlmAdapter extends LlmAdapter {
|
|
47
48
|
worldHome;
|
|
@@ -289,6 +289,11 @@ export class CodexAgent {
|
|
|
289
289
|
*/
|
|
290
290
|
#remoteQueue = [];
|
|
291
291
|
#bridgedToolResults = new Set();
|
|
292
|
+
/** callIds already advertised by an appended assistant/message frame — the
|
|
293
|
+
* strict validator refuses any `tool/call` whose callId no assistant frame
|
|
294
|
+
* advertised, and requires the execution arguments to equal the advertised
|
|
295
|
+
* string (2026-10-04 dev3 omp/hermes repair: same write-side family). */
|
|
296
|
+
#advertisedToolCalls = new Set();
|
|
292
297
|
/**
|
|
293
298
|
* The open turn's FINAL assistant message, buffered from `message_end`
|
|
294
299
|
* until `turn_end` so the SAME turn's usage can land on it (the DSH
|
|
@@ -711,9 +716,14 @@ export class CodexAgent {
|
|
|
711
716
|
// An unknown provider id (a session pinned before the multi-provider rework)
|
|
712
717
|
// maps to "no injection": the user's own config.toml is the honest answer.
|
|
713
718
|
const launchSpec = launch ?? NATIVE_LAUNCH;
|
|
714
|
-
const providerId = launch === undefined ? null : route?.provider ?? null;
|
|
719
|
+
const providerId = launch === undefined ? null : launch.runtimeProvider ?? route?.provider ?? null;
|
|
715
720
|
const client = this.#client;
|
|
716
721
|
const key = `${providerId ?? "native"}/${route?.model ?? ""}`;
|
|
722
|
+
// Pin before ensureStarted: the first thread and turn otherwise inherit
|
|
723
|
+
// the native model captured at construction, even with a new provider.
|
|
724
|
+
if (route !== undefined) {
|
|
725
|
+
await client.setModel(route.provider, route.model);
|
|
726
|
+
}
|
|
717
727
|
if (!client.spawned && client.threadId === null) {
|
|
718
728
|
client.applyLaunch(launchSpec, providerId);
|
|
719
729
|
await client.ensureStarted();
|
|
@@ -726,11 +736,6 @@ export class CodexAgent {
|
|
|
726
736
|
else if (client.appliedProvider !== providerId) {
|
|
727
737
|
await client.reattach(launchSpec, providerId, route?.model);
|
|
728
738
|
}
|
|
729
|
-
else if (route !== undefined) {
|
|
730
|
-
await client.setModel(route.provider, route.model).catch((error) => {
|
|
731
|
-
trace(`set_model ${key} failed: ${String(error)}`);
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
739
|
this.#routeKey = key;
|
|
735
740
|
}
|
|
736
741
|
/**
|
|
@@ -1043,7 +1048,32 @@ export class CodexAgent {
|
|
|
1043
1048
|
case "tool_execution_start": {
|
|
1044
1049
|
const callId = String(event.toolCallId ?? "");
|
|
1045
1050
|
const name = String(event.toolName ?? "");
|
|
1051
|
+
const argumentsJson = String(event.argumentsJson ?? JSON.stringify(event.args ?? {}));
|
|
1046
1052
|
this.#commitStepStart();
|
|
1053
|
+
// Advertisement-before-execution (validator invariant, 2026-10-04 dev3
|
|
1054
|
+
// omp/hermes repair family): unless a real assistant frame advertised
|
|
1055
|
+
// this callId, bridge a minimal assistant/message carrying the
|
|
1056
|
+
// tool-call block — arguments copied verbatim into both frames. The
|
|
1057
|
+
// synthesized source mirrors #appendAssistantMessage's open-turn
|
|
1058
|
+
// fallback chain.
|
|
1059
|
+
if (!this.#advertisedToolCalls.has(callId)) {
|
|
1060
|
+
const advertisement = createAssistantMessage({
|
|
1061
|
+
content: [{ type: "tool-call", id: ToolCallId(callId), name, arguments: argumentsJson }],
|
|
1062
|
+
source: {
|
|
1063
|
+
provider: String(this.#turnRoute?.provider ?? this.options.provider ?? ""),
|
|
1064
|
+
model: String(this.#turnRoute?.model ?? this.options.model ?? ""),
|
|
1065
|
+
},
|
|
1066
|
+
});
|
|
1067
|
+
this.session.append("assistant/message", {
|
|
1068
|
+
turn: this.#dashTurn,
|
|
1069
|
+
step: this.#step,
|
|
1070
|
+
message: advertisement,
|
|
1071
|
+
stream: [],
|
|
1072
|
+
}, {
|
|
1073
|
+
surfaceOp: "append",
|
|
1074
|
+
});
|
|
1075
|
+
this.#advertisedToolCalls.add(callId);
|
|
1076
|
+
}
|
|
1047
1077
|
this.session.append("tool/call", {
|
|
1048
1078
|
turn: this.#dashTurn,
|
|
1049
1079
|
step: this.#step,
|
|
@@ -1052,7 +1082,7 @@ export class CodexAgent {
|
|
|
1052
1082
|
// The DSH payload requires the RAW JSON string the model produced;
|
|
1053
1083
|
// the wire carries it verbatim for MCP calls and renders it from the
|
|
1054
1084
|
// discrete fields for the other item shapes.
|
|
1055
|
-
arguments:
|
|
1085
|
+
arguments: argumentsJson,
|
|
1056
1086
|
});
|
|
1057
1087
|
break;
|
|
1058
1088
|
}
|
|
@@ -1164,6 +1194,10 @@ export class CodexAgent {
|
|
|
1164
1194
|
}
|
|
1165
1195
|
#appendAssistantMessage(message, usage) {
|
|
1166
1196
|
const content = convertContent(message.content);
|
|
1197
|
+
for (const block of content) {
|
|
1198
|
+
if (block.type === "tool-call")
|
|
1199
|
+
this.#advertisedToolCalls.add(block.id);
|
|
1200
|
+
}
|
|
1167
1201
|
const assistant = createAssistantMessage({
|
|
1168
1202
|
content,
|
|
1169
1203
|
source: {
|
|
@@ -440,11 +440,18 @@ export class CodexAppServerClient {
|
|
|
440
440
|
// crash still fails the client.
|
|
441
441
|
const generation = ++this.#transportGeneration;
|
|
442
442
|
const launch = this.#launch;
|
|
443
|
+
// This is an independent Super D consumer, including when its parent
|
|
444
|
+
// happens to be a desktop task. The desktop host's attribution and
|
|
445
|
+
// tools pipe belong to that host, not to this new app-server.
|
|
446
|
+
const childEnv = { ...process.env, ...launch.env };
|
|
447
|
+
delete childEnv.CODEX_INTERNAL_ORIGINATOR_OVERRIDE;
|
|
448
|
+
delete childEnv.CODEX_APP_TOOLS_PIPE_PATH;
|
|
443
449
|
transport = transportFactory({
|
|
444
450
|
...(this.#options.cwd === undefined ? {} : { cwd: this.#options.cwd }),
|
|
445
451
|
...(this.#options.requestTimeoutMs === undefined ? {} : { requestTimeoutMs: this.#options.requestTimeoutMs }),
|
|
452
|
+
...(this.#options.worldHome === undefined ? {} : { worldHome: this.#options.worldHome }),
|
|
446
453
|
args: ["app-server", ...launch.argv],
|
|
447
|
-
env:
|
|
454
|
+
env: childEnv,
|
|
448
455
|
onNotification: (notification) => this.#onNotification(notification),
|
|
449
456
|
onServerRequest: (request) => this.#onServerRequest(request),
|
|
450
457
|
onExit: (info) => {
|
|
@@ -456,7 +463,7 @@ export class CodexAppServerClient {
|
|
|
456
463
|
}
|
|
457
464
|
this.#transport = transport;
|
|
458
465
|
if (first) {
|
|
459
|
-
transport.start();
|
|
466
|
+
await transport.start();
|
|
460
467
|
const info = asRecord(await transport.request("initialize", {
|
|
461
468
|
clientInfo: { name: "superd-agent-codex", title: "Super D agent-codex", version: "0.1.0" },
|
|
462
469
|
capabilities: { experimentalApi: true, requestAttestation: false },
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Narrow hand-written types for the codex `app-server` JSONL JSON-RPC wire.
|
|
3
3
|
*
|
|
4
|
-
* DERIVED FROM: the `codex app-server generate-ts --out <dir>` output for
|
|
5
|
-
* `@openai/codex`
|
|
4
|
+
* DERIVED FROM: the `codex app-server generate-ts --out <dir>` output for
|
|
5
|
+
* `@openai/codex@0.154.0` (the SDK line's tested pair). The app-server line's
|
|
6
|
+
* runtime target is the exact-pin `@openai/codex@0.160.0`, lazily fetched into
|
|
7
|
+
* the world home (codex-runtime-fetch.ts) — never an install-time dependency.
|
|
8
|
+
* That tree is ~617 files;
|
|
9
|
+
|
|
6
10
|
* this module is a hand-written, deliberately narrow subset covering exactly
|
|
7
11
|
* the envelopes, requests, notifications and item shapes
|
|
8
12
|
* `src/app-server-client.ts` and `src/app-server-events.ts` consume. Each
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* `{"id":…,"result":…}` / `{"id":…,"error":{…}}`).
|
|
8
8
|
*
|
|
9
9
|
* This module owns exactly four things and nothing else:
|
|
10
|
-
* 1.
|
|
10
|
+
* 1. runtime selection (override → PATH CLI → desktop core → fetched world-home
|
|
11
|
+
* runtime — codex-runtime-fetch.ts; the pinned package is NEVER an
|
|
12
|
+
* install-time dependency, 2026-10-06 ruling),
|
|
13
|
+
|
|
11
14
|
* 2. process spawn + line framing + per-id request/response correlation,
|
|
12
15
|
* 3. dispatch of server-initiated notifications and server-requests,
|
|
13
16
|
* 4. teardown (process GROUP kill — the app-server may spawn stdio MCP
|
|
@@ -22,7 +25,9 @@
|
|
|
22
25
|
* process, and the client calls it on first real work.
|
|
23
26
|
*/
|
|
24
27
|
import { spawn } from "node:child_process";
|
|
25
|
-
import {
|
|
28
|
+
import { createCodexRuntimeDeps, ensureCodexRuntime } from "./codex-runtime-fetch.js";
|
|
29
|
+
import { resolveCodexRuntime } from "./codex-runtime.js";
|
|
30
|
+
export { resolveCodexCommand, resolveCodexRuntime } from "./codex-runtime.js";
|
|
26
31
|
/** Per-request protocol error surfaced by the server (JSON-RPC `error` object). */
|
|
27
32
|
export class CodexWireError extends Error {
|
|
28
33
|
code;
|
|
@@ -37,23 +42,6 @@ export class CodexWireError extends Error {
|
|
|
37
42
|
/** Server overload: the documented retryable backpressure code. */
|
|
38
43
|
export const SERVER_OVERLOADED_CODE = -32001;
|
|
39
44
|
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
40
|
-
/** The codex entry script this package pins (`@openai/codex/bin/codex.js`). */
|
|
41
|
-
export function resolveCodexCommand() {
|
|
42
|
-
const override = process.env.CODEX_BIN;
|
|
43
|
-
if (override !== undefined && override.trim() !== "")
|
|
44
|
-
return override;
|
|
45
|
-
const require = createRequire(import.meta.url);
|
|
46
|
-
try {
|
|
47
|
-
// Pin-faithful: the SAME `@openai/codex` this package depends on, so the
|
|
48
|
-
// wire protocol and the binary version can never drift apart. Set
|
|
49
|
-
// CODEX_BIN to override (tests).
|
|
50
|
-
return require.resolve("@openai/codex/bin/codex.js");
|
|
51
|
-
}
|
|
52
|
-
catch (error) {
|
|
53
|
-
throw new Error("cannot locate the pinned codex CLI (@openai/codex/bin/codex.js): "
|
|
54
|
-
+ `${error instanceof Error ? error.message : String(error)}. Install the package or set CODEX_BIN.`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
45
|
/** True for a plain JSON object (never arrays/null). */
|
|
58
46
|
function asRecord(value) {
|
|
59
47
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
@@ -85,22 +73,25 @@ export class CodexAppServerTransport {
|
|
|
85
73
|
}
|
|
86
74
|
/**
|
|
87
75
|
* Spawn the child exactly once. Idempotent.
|
|
88
|
-
* @throws when
|
|
76
|
+
* @throws when no runtime can be discovered or fetched, or the child cannot
|
|
77
|
+
* be spawned at all (bad executable).
|
|
89
78
|
*/
|
|
90
|
-
start() {
|
|
79
|
+
async start() {
|
|
91
80
|
if (this.#child !== undefined)
|
|
92
81
|
return;
|
|
93
|
-
const
|
|
82
|
+
const env = this.#options.env ?? { ...process.env };
|
|
83
|
+
const runtime = await this.#resolveRuntime(env);
|
|
94
84
|
const args = [...(this.#options.args ?? ["app-server"])];
|
|
95
85
|
// `detached` gives the child its own process group so teardown can kill
|
|
96
86
|
// the whole tree (stdio MCP descendants included), not just the root.
|
|
97
|
-
const child = spawn(process.execPath, [command, ...args], {
|
|
87
|
+
const child = spawn(runtime.nodeScript ? process.execPath : runtime.command, runtime.nodeScript ? [runtime.command, ...args] : args, {
|
|
98
88
|
cwd: this.#options.cwd,
|
|
99
|
-
env
|
|
89
|
+
env,
|
|
100
90
|
stdio: ["pipe", "pipe", "pipe"],
|
|
101
91
|
detached: true,
|
|
102
92
|
});
|
|
103
93
|
this.#child = child;
|
|
94
|
+
this.#options.onStderr?.(`codex runtime source=${runtime.source} command=${runtime.command} mode=${runtime.nodeScript ? "node" : "native"}\n`);
|
|
104
95
|
child.stdout?.setEncoding("utf8");
|
|
105
96
|
child.stderr?.setEncoding("utf8");
|
|
106
97
|
child.stdout?.on("data", (chunk) => this.#onStdout(chunk));
|
|
@@ -111,6 +102,20 @@ export class CodexAppServerTransport {
|
|
|
111
102
|
child.on("error", (error) => this.#onExit(null, null, error));
|
|
112
103
|
child.on("exit", (code, signal) => this.#onExit(code, signal));
|
|
113
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
|
|
107
|
+
* Resolve the child's runtime. An explicit `command` and world-home-less
|
|
108
|
+
* calls stay on the pure sync ladder; only a world home arms the lazy fetch
|
|
109
|
+
* of the pinned package (2026-10-06 ruling — never an install-time dep).
|
|
110
|
+
*/
|
|
111
|
+
async #resolveRuntime(env) {
|
|
112
|
+
if (this.#options.command !== undefined || this.#options.worldHome === undefined) {
|
|
113
|
+
return resolveCodexRuntime({
|
|
114
|
+
env: this.#options.command === undefined ? env : { ...env, CODEX_BIN: this.#options.command },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return ensureCodexRuntime(this.#options.worldHome, createCodexRuntimeDeps({ env }));
|
|
118
|
+
}
|
|
114
119
|
/** Send a request and await its result. Rejects on protocol error, timeout, or EOF. */
|
|
115
120
|
request(method, params, options = {}) {
|
|
116
121
|
if (this.#closed || this.#child === undefined) {
|
|
@@ -1,35 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* codex-launch.ts — how a provider reaches a `codex app-server` child
|
|
3
|
-
* (2026-10-04 on-the-fly rework).
|
|
4
|
-
*
|
|
5
|
-
* The retired design put the provider in force by REWRITING the user's
|
|
6
|
-
* `~/.codex/config.toml` before the child started. That made the user's native
|
|
7
|
-
* home the switchboard: every switch was a disk write, only ever affected the
|
|
8
|
-
* next child, and fought every other tool that owns the same file (cc-switch's
|
|
9
|
-
* whole-config rewrites, the user's own hand edits).
|
|
10
|
-
*
|
|
11
|
-
* This module replaces it with process arguments. `codex app-server -c
|
|
12
|
-
* <dotted.path>=<value>` overrides any `config.toml` value for THAT child
|
|
13
|
-
* alone (the flag's own help text: "Override a configuration value that would
|
|
14
|
-
* otherwise be loaded from `~/.codex/config.toml`. Use a dotted path."), and
|
|
15
|
-
* `ThreadStartParams`/`ThreadResumeParams` accept the same object as `config`.
|
|
16
|
-
* A provider therefore costs nothing on disk and is scoped to one process, which
|
|
17
|
-
* is exactly what a per-session provider choice needs.
|
|
18
|
-
*
|
|
19
|
-
* Measured constraints this module encodes (see
|
|
20
|
-
* `docs/test-reports/2026-10-03-codex-provider-model-switch-probe.md`):
|
|
21
|
-
*
|
|
22
|
-
* - a HYPHENATED id is fine in a dotted path (`model_providers.zhipu-cn.…`),
|
|
23
|
-
* so native table names survive verbatim as provider ids;
|
|
24
|
-
* - `model_catalog_json` is resolved against the process CWD when it arrives
|
|
25
|
-
* as an override, so the path is always absolute;
|
|
26
|
-
* - `env_key` is the one auth shape that never consults the auth manager, so a
|
|
27
|
-
* stored API key rides the CHILD's environment under a synthetic variable
|
|
28
|
-
* name. That is also what makes a keyed provider usable behind a ChatGPT
|
|
29
|
-
* login, where `requires_openai_auth` is silently shadowed.
|
|
30
|
-
*
|
|
31
|
-
* @module agent-codex/codex-launch
|
|
32
|
-
*/
|
|
1
|
+
import { officialCatalogForLaunch } from "./codex-oauth.js";
|
|
33
2
|
import { tomlString, writeProviderCatalog } from "./codex-native-config.js";
|
|
34
3
|
/** The environment variable prefix for a key this adapter injects itself. */
|
|
35
4
|
const SYNTHETIC_KEY_PREFIX = "SUPERD_CODEX_KEY_";
|
|
@@ -48,6 +17,15 @@ export function syntheticKeyName(providerId) {
|
|
|
48
17
|
* @returns the argv/env additions and the provider id they pin.
|
|
49
18
|
*/
|
|
50
19
|
export function providerLaunch(worldHome, codexHome, provider) {
|
|
20
|
+
if (provider.kind === "openai-oauth") {
|
|
21
|
+
const catalog = officialCatalogForLaunch(codexHome);
|
|
22
|
+
// Empty override is filtered to None by Codex, restoring its builtin
|
|
23
|
+
// endpoint without changing the user's native configuration.
|
|
24
|
+
const argv = ["-c", 'model_provider="openai"', "-c", 'openai_base_url=""'];
|
|
25
|
+
if (catalog !== undefined)
|
|
26
|
+
argv.push("-c", `model_catalog_json=${tomlString(catalog)}`);
|
|
27
|
+
return { argv, env: {}, runtimeProvider: "openai" };
|
|
28
|
+
}
|
|
51
29
|
const table = `model_providers.${provider.id}`;
|
|
52
30
|
const argv = [
|
|
53
31
|
"-c", `${table}.name=${tomlString(provider.label)}`,
|
|
@@ -95,9 +95,10 @@ function slugOf(entry) {
|
|
|
95
95
|
* that we do not already hold, then upserts `models` by slug. Each upserted
|
|
96
96
|
* entry is a CLONE of `template` — an existing entry for the same slug if there
|
|
97
97
|
* is one, else the caller's template, else the first entry either side supplied
|
|
98
|
-
* — with
|
|
99
|
-
*
|
|
100
|
-
*
|
|
98
|
+
* — with identity and declared context window overridden. The other metadata
|
|
99
|
+
* is copied through except `comp_hash`: this optional opaque compatibility
|
|
100
|
+
* identifier belongs to the template's model, not an unrelated provider model.
|
|
101
|
+
* See the plan's D5 for why we do not synthesize the remaining fields.
|
|
101
102
|
*/
|
|
102
103
|
export function mergeModelCatalog(existing, absorbed, models, template) {
|
|
103
104
|
// Both sides are a TEMPLATE POOL only — never output. What gets emitted is
|
|
@@ -132,6 +133,11 @@ export function mergeModelCatalog(existing, absorbed, models, template) {
|
|
|
132
133
|
slug: model.id,
|
|
133
134
|
display_name: model.displayName ?? model.id,
|
|
134
135
|
};
|
|
136
|
+
// A copied OpenAI hash falsely triggers previous-model compaction on
|
|
137
|
+
// provider switches in Codex 0.160. Our provider model declarations do not
|
|
138
|
+
// carry verified hashes; remove stale generated values too. Context-limit
|
|
139
|
+
// compaction and its window/threshold metadata remain unchanged.
|
|
140
|
+
delete entry["comp_hash"];
|
|
135
141
|
if (model.contextWindow !== undefined) {
|
|
136
142
|
// Codex reads context_window and caps against max_context_window; a stale
|
|
137
143
|
// template value must not silently cap a larger real window.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/** Native OAuth discovery. Tokens stay in Codex's own home; no network/spawn. */
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { readNativeSelection } from './codex-native-config.js';
|
|
5
|
+
import { readProviderStore } from './codex-providers.js';
|
|
6
|
+
export const OPENAI_OAUTH_ID = 'openai-oauth';
|
|
7
|
+
export function hasOpenAiOAuth(codexHome) {
|
|
8
|
+
try {
|
|
9
|
+
const value = JSON.parse(readFileSync(join(codexHome, 'auth.json'), 'utf8'));
|
|
10
|
+
const tokens = value?.tokens;
|
|
11
|
+
return value?.auth_mode === 'chatgpt' && tokens !== null && typeof tokens === 'object'
|
|
12
|
+
&& [tokens.access_token, tokens.refresh_token].some(token => typeof token === 'string' && token.trim() !== '');
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function isRecord(value) {
|
|
19
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
function nonEmpty(value) {
|
|
22
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
23
|
+
}
|
|
24
|
+
function oneOf(value, choices) {
|
|
25
|
+
return typeof value === 'string' && choices.includes(value);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The non-defaulted ModelInfo fields consumed by Codex, including its required
|
|
29
|
+
* nested metadata. Keep unknown native fields untouched; never repair or
|
|
30
|
+
* synthesize a catalog. Source: codex-rs/protocol/src/openai_models.rs (0.154 / 0.160).
|
|
31
|
+
*/
|
|
32
|
+
function usableModel(value) {
|
|
33
|
+
if (!isRecord(value) || !nonEmpty(value['slug']) || typeof value['display_name'] !== 'string')
|
|
34
|
+
return false;
|
|
35
|
+
const reasoning = value['supported_reasoning_levels'];
|
|
36
|
+
const truncation = value['truncation_policy'];
|
|
37
|
+
const messages = value['model_messages'];
|
|
38
|
+
if (typeof value['base_instructions'] !== 'string' && !(isRecord(messages) && typeof messages['instructions_template'] === 'string'))
|
|
39
|
+
return false;
|
|
40
|
+
return Array.isArray(reasoning) && reasoning.every(level => isRecord(level) && nonEmpty(level['effort']) && typeof level['description'] === 'string')
|
|
41
|
+
&& oneOf(value['shell_type'], ['unified_exec', 'default', 'local', 'shell_command', 'disabled'])
|
|
42
|
+
&& oneOf(value['visibility'], ['list', 'hide', 'none'])
|
|
43
|
+
&& typeof value['supported_in_api'] === 'boolean'
|
|
44
|
+
&& Number.isSafeInteger(value['priority']) && Number(value['priority']) >= -2147483648 && Number(value['priority']) <= 2147483647
|
|
45
|
+
&& typeof value['support_verbosity'] === 'boolean'
|
|
46
|
+
&& isRecord(truncation) && oneOf(truncation['mode'], ['tokens', 'bytes']) && Number.isSafeInteger(truncation['limit'])
|
|
47
|
+
&& Array.isArray(value['experimental_supported_tools']) && value['experimental_supported_tools'].every(tool => typeof tool === 'string');
|
|
48
|
+
}
|
|
49
|
+
/** One bad entry invalidates the file: Codex deserializes the whole catalog. */
|
|
50
|
+
function officialCatalogModels(codexHome) {
|
|
51
|
+
try {
|
|
52
|
+
const cache = JSON.parse(readFileSync(join(codexHome, 'models_cache.json'), 'utf8'));
|
|
53
|
+
if (!isRecord(cache))
|
|
54
|
+
return undefined;
|
|
55
|
+
const models = cache['models'];
|
|
56
|
+
if (Array.isArray(models) && models.length > 0 && models.every(usableModel))
|
|
57
|
+
return models;
|
|
58
|
+
}
|
|
59
|
+
catch { /* Missing/stale cache must not become a forced catalog override. */ }
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Conservatively detect configuration that would leak into an official child
|
|
64
|
+
* without a cache override. Inspect all tables: a profile can supply a catalog
|
|
65
|
+
* even when the root has none. Accept indentation, quoted and dotted keys. We
|
|
66
|
+
* only need to know whether a conflicting override exists, not parse its value.
|
|
67
|
+
*/
|
|
68
|
+
function nativeAssignments(codexHome) {
|
|
69
|
+
let text;
|
|
70
|
+
try {
|
|
71
|
+
text = readFileSync(join(codexHome, 'config.toml'), 'utf8');
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const assignments = [];
|
|
77
|
+
for (const line of text.split('\n')) {
|
|
78
|
+
const assignment = /^\s*((?:[\w-]+|"[^"\n]+"|'[^'\n]+')(?:\s*\.\s*(?:[\w-]+|"[^"\n]+"|'[^'\n]+'))*)\s*=\s*(.*)$/.exec(line);
|
|
79
|
+
if (assignment === null)
|
|
80
|
+
continue;
|
|
81
|
+
const key = assignment[1].split(/\s*\.\s*/).at(-1)?.replace(/^["']|["']$/g, '');
|
|
82
|
+
assignments.push({ key, value: assignment[2] });
|
|
83
|
+
}
|
|
84
|
+
return assignments;
|
|
85
|
+
}
|
|
86
|
+
function nativeCatalogConflict(codexHome) {
|
|
87
|
+
return nativeAssignments(codexHome).some(({ key, value }) => key === 'model_catalog_json' || key === 'profile'
|
|
88
|
+
|| (key === 'model_provider' && !/^(?:"openai"|'openai')\s*(?:#.*)?$/.test(value)));
|
|
89
|
+
}
|
|
90
|
+
/** Official runtime cache only: never consume a third-party custom catalog. */
|
|
91
|
+
export function readOpenAiModels(codexHome) {
|
|
92
|
+
const models = [];
|
|
93
|
+
for (const raw of officialCatalogModels(codexHome) ?? []) {
|
|
94
|
+
if (raw['visibility'] !== 'list')
|
|
95
|
+
continue;
|
|
96
|
+
const slug = raw['slug'];
|
|
97
|
+
if (models.some(model => model.id === slug))
|
|
98
|
+
continue;
|
|
99
|
+
const displayName = raw['display_name'];
|
|
100
|
+
const contextWindow = raw['context_window'];
|
|
101
|
+
models.push({ id: slug,
|
|
102
|
+
...(nonEmpty(displayName) ? { displayName } : {}),
|
|
103
|
+
...(Number.isSafeInteger(contextWindow) && Number(contextWindow) > 0 ? { contextWindow: Number(contextWindow) } : {}),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// A different binary can leave a valid, older catalog in the shared home.
|
|
107
|
+
// Preserve the user's official configured choice as an identity, without
|
|
108
|
+
// inventing metadata or claiming that this cache/binary supports that model.
|
|
109
|
+
if (!nativeCatalogConflict(codexHome)) {
|
|
110
|
+
const native = readNativeSelection(codexHome);
|
|
111
|
+
if ((native.providerId === undefined || native.providerId === 'openai') && nonEmpty(native.model)
|
|
112
|
+
&& !models.some(model => model.id === native.model))
|
|
113
|
+
models.push({ id: native.model });
|
|
114
|
+
}
|
|
115
|
+
return models;
|
|
116
|
+
}
|
|
117
|
+
/** Internal catalog shape; disk serialization stores just its native identity. */
|
|
118
|
+
export function openAiOAuthEntry(codexHome) {
|
|
119
|
+
return { id: OPENAI_OAUTH_ID, label: 'OpenAI OAuth', kind: 'openai-oauth', origin: 'native',
|
|
120
|
+
baseUrl: '', protocol: 'responses', auth: { kind: 'apiKey', value: '' }, models: readOpenAiModels(codexHome) };
|
|
121
|
+
}
|
|
122
|
+
export function readEffectiveProviderStore(worldHome, codexHome) {
|
|
123
|
+
const store = readProviderStore(worldHome);
|
|
124
|
+
const aliasEntries = store.providers.filter(entry => entry.id === OPENAI_OAUTH_ID);
|
|
125
|
+
const ambiguous = aliasEntries.length > 1;
|
|
126
|
+
const blocked = store.blockedNativeIds?.includes(OPENAI_OAUTH_ID) === true;
|
|
127
|
+
return { ...store, providers: store.providers.flatMap(entry => entry.id === OPENAI_OAUTH_ID && ambiguous ? [] : entry.kind === 'openai-oauth'
|
|
128
|
+
? !blocked && hasOpenAiOAuth(codexHome) ? [openAiOAuthEntry(codexHome)] : [] : [entry]) };
|
|
129
|
+
}
|
|
130
|
+
/** A usable runtime-owned catalog path, excluding malformed/empty cache files. */
|
|
131
|
+
export function openAiCatalogPath(codexHome) {
|
|
132
|
+
return officialCatalogModels(codexHome) === undefined ? undefined : join(codexHome, 'models_cache.json');
|
|
133
|
+
}
|
|
134
|
+
/** Do not let an official child inherit a third-party model_catalog_json. */
|
|
135
|
+
export function officialCatalogForLaunch(codexHome) {
|
|
136
|
+
if (!hasOpenAiOAuth(codexHome))
|
|
137
|
+
throw new Error('OpenAI OAuth authentication is unavailable; sign in using Codex');
|
|
138
|
+
if (nativeAssignments(codexHome).some(({ key, value }) => key === 'forced_login_method' && !/^(?:"chatgpt"|'chatgpt')\s*(?:#.*)?$/.test(value))) {
|
|
139
|
+
throw new Error('OpenAI OAuth cannot use a conflicting forced_login_method; choose ChatGPT login in your Codex configuration');
|
|
140
|
+
}
|
|
141
|
+
// A forced catalog is static in Codex and disables version/identity/TTL
|
|
142
|
+
// refresh. Let the selected binary own discovery when native config is safe;
|
|
143
|
+
// only override a conflicting native catalog with the validated official one.
|
|
144
|
+
if (!nativeCatalogConflict(codexHome))
|
|
145
|
+
return undefined;
|
|
146
|
+
const catalog = openAiCatalogPath(codexHome);
|
|
147
|
+
if (catalog !== undefined)
|
|
148
|
+
return catalog;
|
|
149
|
+
const native = readNativeSelection(codexHome);
|
|
150
|
+
if (nativeCatalogConflict(codexHome) || (native.providerId !== undefined && native.providerId !== 'openai'))
|
|
151
|
+
throw new Error('OpenAI OAuth model cache unavailable; open Codex with the official provider to refresh its models');
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|