super-dsh 0.1.10-e → 0.1.11
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 +18 -2
- package/agent-agy/dist/agent.js +19 -1
- package/agent-claude/dist/agent.js +23 -5
- package/agent-codex/README.md +18 -6
- package/agent-codex/dist/adapter.js +3 -2
- package/agent-codex/dist/agent.js +41 -7
- package/agent-codex/dist/app-server-client.js +7 -1
- package/agent-codex/dist/app-server-wire.js +10 -22
- 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.js +120 -0
- package/agent-codex/dist/index.js +18 -11
- package/agent-codex/package.json +1 -1
- 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 +2 -2
- 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 +2 -2
- 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/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/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
|
|
@@ -27,12 +27,28 @@ home under `<DSH_HOME>/agents/<label>/` on first boot.
|
|
|
27
27
|
| Label | Runtime | Integration |
|
|
28
28
|
|---|---|---|
|
|
29
29
|
| OMP | OMP (`~/.omp`) | host `omp` binary over RPC (`omp --mode rpc`, NDJSON v1), lazy per session |
|
|
30
|
-
| Codex | Codex
|
|
30
|
+
| Codex | Codex app-server (`~/.codex`) | desktop core first, CLI and bundled core fallback |
|
|
31
31
|
| Claude | Claude Code (`~/.claude`) | standalone adapter, joined by the hub |
|
|
32
32
|
| Pi | pi coding agent (`~/.pi`) | in-process SDK |
|
|
33
33
|
| Hermes | Hermes (`~/.hermes`) | gateway child |
|
|
34
34
|
| Antigravity | agy SDK | Python bridge (`bridge/agy_bridge.py`) |
|
|
35
35
|
|
|
36
|
+
### Codex in 0.1.11
|
|
37
|
+
|
|
38
|
+
Codex discovers the local OpenAI OAuth login as a read-only provider in the
|
|
39
|
+
model selector. Selecting it explicitly overrides a third-party provider in the
|
|
40
|
+
native config, while credentials and native configuration stay in the native
|
|
41
|
+
home. Third-party providers remain selectable per session.
|
|
42
|
+
|
|
43
|
+
The adapter prefers the installed desktop core, then the CLI, then the bundled
|
|
44
|
+
0.160.0 core (`CODEX_BIN` overrides discovery). Desktop host browser and computer
|
|
45
|
+
use services are separate and are not attached by this adapter.
|
|
46
|
+
|
|
47
|
+
**Known limitation:** returning a session with third-party reasoning history to
|
|
48
|
+
OpenAI OAuth can be rejected by the official endpoint. Use a separate official
|
|
49
|
+
session for now. Compaction before switching was proven in an isolated probe;
|
|
50
|
+
automatic compaction is not included in this release.
|
|
51
|
+
|
|
36
52
|
### Opt-in SDK halves
|
|
37
53
|
|
|
38
54
|
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
|
}
|
package/agent-codex/README.md
CHANGED
|
@@ -19,10 +19,10 @@ superd 外壳下的 **Codex adapter**(super-dsh 线):把 Codex CLI 经由
|
|
|
19
19
|
|
|
20
20
|
## 线别(2026-10-03 迁移)
|
|
21
21
|
|
|
22
|
-
**app-server
|
|
22
|
+
**app-server 线**是主路线(SDK 依赖已从本包与主 pack 移除)。SDK 另存为默认关闭的 `agent-codex-sdk`,拥有独立的按需 runtime 安装。
|
|
23
23
|
SDK 线在 adapter 里登记的一整排降级被 app-server 协议面直接消掉:
|
|
24
24
|
|
|
25
|
-
| 能力 | SDK
|
|
25
|
+
| 能力 | SDK 可选线 | app-server 主路线 |
|
|
26
26
|
|---|---|---|
|
|
27
27
|
| 打断 | 只有 AbortSignal(掐断本轮 stream) | **`turn/interrupt`** |
|
|
28
28
|
| steer | 无 → 排队 followUp 近似 | **`turn/steer`**(注入进行中的 turn) |
|
|
@@ -82,19 +82,31 @@ collab/subagent 项。
|
|
|
82
82
|
| 变量 | 作用 |
|
|
83
83
|
|---|---|
|
|
84
84
|
| `CODEX_TRACE=1` | provider/agent/app-server 追踪(stderr) |
|
|
85
|
-
| `CODEX_BIN` |
|
|
85
|
+
| `CODEX_BIN` | 显式覆盖 Codex 核心或 JS 入口;默认依序发现 desktop 核心、用户 CLI、包内 exact-pin fallback |
|
|
86
86
|
| `CODEX_HOME` | Codex 原生 home 重定向(**测试专用**;生产路径零重定向) |
|
|
87
87
|
| `CODEX_APPROVAL_MODE` | 覆盖审批档(`never`/`on-request`/`on-failure`/`untrusted`;非法值忽略) |
|
|
88
88
|
| `CODEX_APPROVAL_TIMEOUT_MS` | 审批应答超时;`0` 关闭;默认 300000 |
|
|
89
89
|
| `CODEX_IDLE_EXIT_MS` | 会话空闲后拆客户端;`0` 关闭;默认 600000 |
|
|
90
90
|
|
|
91
|
+
## Binary 与官方 OAuth
|
|
92
|
+
|
|
93
|
+
核心发现顺序是显式 `CODEX_BIN` → 已安装 desktop 核心 → 用户 CLI → 包内 `@openai/codex@0.160.0`。
|
|
94
|
+
发现和启动仅发生在真正 prompt 时。JS 入口经 Node 启动,原生核心直接执行;认证或生成失败不会静默换 provider。
|
|
95
|
+
desktop 优先选择核心版本,不代表复制 desktop 宿主:独立 app-server 使用 Super D 自有 clientInfo,
|
|
96
|
+
不借用父任务的 desktop 工具 pipe。browser/computer use 仍需要对应宿主服务与工具接线。
|
|
97
|
+
|
|
98
|
+
本地 `auth.json` 有 ChatGPT OAuth 时,模型选择器增加只读的 **OpenAI OAuth** provider。
|
|
99
|
+
adapter 正本仅存其 native 身份,token 留在 Codex home;选择该 route 时通过子进程配置覆盖 builtin `openai`,
|
|
100
|
+
不会改写用户的 config/auth。官方模型读取原生 `models_cache.json`,并保留原生配置的官方默认模型;第三方自定义目录不会当作官方目录。
|
|
101
|
+
没有原生目录冲突时不强制注入静态 catalog,让实际选中的 binary 按版本、身份与有效期维护 cache;有目录冲突时仅注入可消费的官方 cache,缺失时明确报错。原生默认模型的 ID 不代表当前 binary 的访问权限。OAuth logout 后显式选择必须失败,不能落到本地第三方默认配置。
|
|
102
|
+
|
|
91
103
|
## 测试
|
|
92
104
|
|
|
93
105
|
```bash
|
|
94
|
-
|
|
106
|
+
node super-dsh/scripts/pack.mjs # 仓根执行,唯一构建打包入口
|
|
95
107
|
DSH_HOME=<repo>/.tests \
|
|
96
|
-
SUPERD_DSH_ANCHOR=<repo>/upstream/deepseek-harness/package.json \
|
|
97
|
-
node --test test/*.test.mjs
|
|
108
|
+
SUPERD_DSH_ANCHOR=<repo>/upstream/deepseek-harness/apps/cli/package.json \
|
|
109
|
+
node --test super-dsh/agent-codex/test/*.test.mjs
|
|
98
110
|
```
|
|
99
111
|
|
|
100
112
|
单测全部是 fake:`test/fixtures/fake-app-server.mjs` 是一个**可编程的 app-server 替身**
|
|
@@ -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,17 @@ 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 }),
|
|
446
452
|
args: ["app-server", ...launch.argv],
|
|
447
|
-
env:
|
|
453
|
+
env: childEnv,
|
|
448
454
|
onNotification: (notification) => this.#onNotification(notification),
|
|
449
455
|
onServerRequest: (request) => this.#onServerRequest(request),
|
|
450
456
|
onExit: (info) => {
|
|
@@ -7,7 +7,7 @@
|
|
|
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 → installed desktop → CLI → pinned package),
|
|
11
11
|
* 2. process spawn + line framing + per-id request/response correlation,
|
|
12
12
|
* 3. dispatch of server-initiated notifications and server-requests,
|
|
13
13
|
* 4. teardown (process GROUP kill — the app-server may spawn stdio MCP
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
* process, and the client calls it on first real work.
|
|
23
23
|
*/
|
|
24
24
|
import { spawn } from "node:child_process";
|
|
25
|
-
import {
|
|
25
|
+
import { resolveCodexRuntime } from "./codex-runtime.js";
|
|
26
|
+
export { resolveCodexCommand, resolveCodexRuntime } from "./codex-runtime.js";
|
|
26
27
|
/** Per-request protocol error surfaced by the server (JSON-RPC `error` object). */
|
|
27
28
|
export class CodexWireError extends Error {
|
|
28
29
|
code;
|
|
@@ -37,23 +38,6 @@ export class CodexWireError extends Error {
|
|
|
37
38
|
/** Server overload: the documented retryable backpressure code. */
|
|
38
39
|
export const SERVER_OVERLOADED_CODE = -32001;
|
|
39
40
|
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
41
|
/** True for a plain JSON object (never arrays/null). */
|
|
58
42
|
function asRecord(value) {
|
|
59
43
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
@@ -90,17 +74,21 @@ export class CodexAppServerTransport {
|
|
|
90
74
|
start() {
|
|
91
75
|
if (this.#child !== undefined)
|
|
92
76
|
return;
|
|
93
|
-
const
|
|
77
|
+
const env = this.#options.env ?? { ...process.env };
|
|
78
|
+
const runtime = resolveCodexRuntime({
|
|
79
|
+
env: this.#options.command === undefined ? env : { ...env, CODEX_BIN: this.#options.command },
|
|
80
|
+
});
|
|
94
81
|
const args = [...(this.#options.args ?? ["app-server"])];
|
|
95
82
|
// `detached` gives the child its own process group so teardown can kill
|
|
96
83
|
// the whole tree (stdio MCP descendants included), not just the root.
|
|
97
|
-
const child = spawn(process.execPath, [command, ...args], {
|
|
84
|
+
const child = spawn(runtime.nodeScript ? process.execPath : runtime.command, runtime.nodeScript ? [runtime.command, ...args] : args, {
|
|
98
85
|
cwd: this.#options.cwd,
|
|
99
|
-
env
|
|
86
|
+
env,
|
|
100
87
|
stdio: ["pipe", "pipe", "pipe"],
|
|
101
88
|
detached: true,
|
|
102
89
|
});
|
|
103
90
|
this.#child = child;
|
|
91
|
+
this.#options.onStderr?.(`codex runtime source=${runtime.source} command=${runtime.command} mode=${runtime.nodeScript ? "node" : "native"}\n`);
|
|
104
92
|
child.stdout?.setEncoding("utf8");
|
|
105
93
|
child.stderr?.setEncoding("utf8");
|
|
106
94
|
child.stdout?.on("data", (chunk) => this.#onStdout(chunk));
|
|
@@ -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
|
+
}
|