super-dsh 0.1.10 → 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/adapter.js +106 -31
- package/agent-claude/dist/agent.js +177 -22
- package/agent-claude/dist/claude-client.js +84 -43
- package/agent-claude/dist/claude-launch.js +101 -0
- package/agent-claude/dist/claude-native-config.js +169 -0
- package/agent-claude/dist/claude-provider-gateway.js +340 -0
- package/agent-claude/dist/claude-providers.js +391 -0
- package/agent-claude/dist/claude-route.js +152 -0
- package/agent-claude/dist/index.js +214 -37
- package/agent-claude/dist/models.js +14 -0
- package/agent-claude/dist/sdk-runtime.js +320 -0
- package/agent-claude/package.json +6 -4
- 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 +150 -58
- package/package.json +2 -3
- package/superd-hub/dist/client/agent-settings.d.ts +66 -12
- package/superd-hub/dist/client/agent-settings.js +85 -14
- package/superd-hub/dist/client/runtime-settings-api.js +5 -5
- 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 +150 -58
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sdk-runtime.ts — the lazy, durable Claude Agent SDK runtime (2026-10-04 spec §2.1).
|
|
3
|
+
*
|
|
4
|
+
* Zero declared dependency: the FIRST real prompt fetches
|
|
5
|
+
* `@anthropic-ai/claude-agent-sdk` into <worldHome>/sdk-runtime/ and it stays
|
|
6
|
+
* durable there. Two modes (user ruling 2026-10-04): host-binary (default,
|
|
7
|
+
* `--omit=optional` — the 206 MB platform payload is never downloaded; the CLI
|
|
8
|
+
* is the user's own binary) and bundled-payload (no local binary: the SDK's
|
|
9
|
+
* own platform payload IS the CLI). A mode flip re-installs with the other
|
|
10
|
+
* flag set (idempotent heal); a FAILED fetch writes sdk-runtime.error.log and
|
|
11
|
+
* throws — never a silent degradation.
|
|
12
|
+
*
|
|
13
|
+
* Layout (the marker/lock/error-log mechanism is, in shape, the one
|
|
14
|
+
* `agent-codex-sdk/src/sdk-runtime.ts` runs on):
|
|
15
|
+
*
|
|
16
|
+
* <worldHome>/sdk-runtime/ npm tree (`--ignore-scripts`)
|
|
17
|
+
* package.json the ANCHOR we write before install
|
|
18
|
+
* (private, never published): npm otherwise
|
|
19
|
+
* resolves the nearest ancestor project and
|
|
20
|
+
* may exit 0 having landed nothing here
|
|
21
|
+
* node_modules/@anthropic-ai/claude-agent-sdk/
|
|
22
|
+
* sdk.mjs the entry `loadClaudeQuery` imports
|
|
23
|
+
* <worldHome>/sdk-runtime.json marker — written LAST; a matching
|
|
24
|
+
* identity AND a live entry mean complete
|
|
25
|
+
* <worldHome>/sdk-runtime.lock atomic (mkdir) bootstrap lock
|
|
26
|
+
* <worldHome>/sdk-runtime.error.log written ONLY by a failed fetch
|
|
27
|
+
*
|
|
28
|
+
* Why the world home rather than the package root: the package root is the
|
|
29
|
+
* installed tarball — read-only in every real consumer install and shared by
|
|
30
|
+
* every world — while the world home is per-runtime, writable, and already the
|
|
31
|
+
* home of this adapter's own DSH state.
|
|
32
|
+
*
|
|
33
|
+
* Mode is re-derived on EVERY call: the host binary is resolved again (it may
|
|
34
|
+
* have appeared or vanished since the fetch), and a marker whose recorded mode
|
|
35
|
+
* no longer matches re-installs under the other flag set.
|
|
36
|
+
*
|
|
37
|
+
* Every path derives from the explicit `worldHome` argument — this module never
|
|
38
|
+
* reads the ambient `DSH_HOME` — and the SDK entry is imported BY RESOLVED PATH,
|
|
39
|
+
* never by a bare specifier: the runtime lives in the world home while this
|
|
40
|
+
* module lives in the plugin tarball, so a bare specifier could not resolve it
|
|
41
|
+
* and would quietly reintroduce the dependency this module exists to delete.
|
|
42
|
+
*/
|
|
43
|
+
import { execFile } from "node:child_process";
|
|
44
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
45
|
+
import { homedir } from "node:os";
|
|
46
|
+
import { join } from "node:path";
|
|
47
|
+
import { pathToFileURL } from "node:url";
|
|
48
|
+
import { promisify } from "node:util";
|
|
49
|
+
/** The coding-agent SDK this line drives (a plain npm package, never declared). */
|
|
50
|
+
export const SDK_PACKAGE = "@anthropic-ai/claude-agent-sdk";
|
|
51
|
+
/** The pin, written here rather than in `package.json` — the marker carries it. */
|
|
52
|
+
export const SDK_PACKAGE_VERSION = "0.3.263";
|
|
53
|
+
export function sdkRuntimeLayout(worldHome) {
|
|
54
|
+
return {
|
|
55
|
+
dir: join(worldHome, "sdk-runtime"),
|
|
56
|
+
marker: join(worldHome, "sdk-runtime.json"),
|
|
57
|
+
lock: join(worldHome, "sdk-runtime.lock"),
|
|
58
|
+
errorLog: join(worldHome, "sdk-runtime.error.log"),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** The entry `loadClaudeQuery` imports — the SDK's `exports["."]`. */
|
|
62
|
+
export function sdkEntryPath(worldHome) {
|
|
63
|
+
return join(sdkRuntimeLayout(worldHome).dir, "node_modules", SDK_PACKAGE, "sdk.mjs");
|
|
64
|
+
}
|
|
65
|
+
/** Read the install marker; undefined when absent, unreadable or not our shape. */
|
|
66
|
+
export function readSdkRuntimeRecord(worldHome) {
|
|
67
|
+
const { marker } = sdkRuntimeLayout(worldHome);
|
|
68
|
+
if (!existsSync(marker))
|
|
69
|
+
return undefined;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(readFileSync(marker, "utf8"));
|
|
72
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
73
|
+
return undefined;
|
|
74
|
+
const record = parsed;
|
|
75
|
+
if (record.sdkPackage !== SDK_PACKAGE)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (typeof record.sdkVersion !== "string")
|
|
78
|
+
return undefined;
|
|
79
|
+
if (record.mode !== "host-binary" && record.mode !== "bundled-payload")
|
|
80
|
+
return undefined;
|
|
81
|
+
if (typeof record.createdAt !== "number")
|
|
82
|
+
return undefined;
|
|
83
|
+
return record;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The fast path: the marker is present, is ours, carries the current pin, was
|
|
91
|
+
* written under `wantedMode`, AND the SDK entry it promises still exists.
|
|
92
|
+
*
|
|
93
|
+
* The marker is written LAST, after a successful install, so its identity match
|
|
94
|
+
* normally implies the tree underneath; but an identity match alone is not
|
|
95
|
+
* enough. `fetchRuntime`'s failure path deletes the tree, a user may delete it
|
|
96
|
+
* by hand, and a half-wiped install can leave the (separate) marker behind. The
|
|
97
|
+
* entry `existsSync` is therefore the completeness contract: without it a stale
|
|
98
|
+
* marker would hand back a record whose `sdk.mjs` is gone and every prompt would
|
|
99
|
+
* fail `ERR_MODULE_NOT_FOUND` until someone removed the marker by hand. One stat
|
|
100
|
+
* costs nothing next to a 206 MB re-download.
|
|
101
|
+
*/
|
|
102
|
+
function sdkRuntimeReady(worldHome, wantedMode) {
|
|
103
|
+
const record = readSdkRuntimeRecord(worldHome);
|
|
104
|
+
if (record === undefined)
|
|
105
|
+
return undefined;
|
|
106
|
+
if (record.sdkVersion !== SDK_PACKAGE_VERSION)
|
|
107
|
+
return undefined;
|
|
108
|
+
if (record.mode !== wantedMode)
|
|
109
|
+
return undefined;
|
|
110
|
+
if (!existsSync(sdkEntryPath(worldHome)))
|
|
111
|
+
return undefined;
|
|
112
|
+
return record;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolve the host Claude Code binary, most specific first:
|
|
116
|
+
* `CLAUDE_EXECUTABLE` (only when it exists) > `claude` on `PATH` >
|
|
117
|
+
* `~/.local/bin/claude` > `/usr/local/bin/claude`.
|
|
118
|
+
*
|
|
119
|
+
* `undefined` means "no local binary" and is the trigger for
|
|
120
|
+
* `bundled-payload` — never a bare `"claude"` fallback, which would pretend a
|
|
121
|
+
* host binary exists and make the SDK spawn a name that is not on `PATH`.
|
|
122
|
+
*/
|
|
123
|
+
export function resolveClaudeBinary() {
|
|
124
|
+
const override = process.env.CLAUDE_EXECUTABLE;
|
|
125
|
+
if (override && existsSync(override))
|
|
126
|
+
return override;
|
|
127
|
+
for (const dir of (process.env.PATH ?? "").split(":").filter(Boolean)) {
|
|
128
|
+
const candidate = join(dir, "claude");
|
|
129
|
+
if (existsSync(candidate))
|
|
130
|
+
return candidate;
|
|
131
|
+
}
|
|
132
|
+
for (const candidate of [join(homedir(), ".local/bin/claude"), "/usr/local/bin/claude"]) {
|
|
133
|
+
if (existsSync(candidate))
|
|
134
|
+
return candidate;
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
/** The npm argv the runtime tree is installed with (kept pure for the test). */
|
|
139
|
+
export function sdkInstallArgv(version, mode) {
|
|
140
|
+
const argv = [
|
|
141
|
+
"install",
|
|
142
|
+
"--exact",
|
|
143
|
+
"--ignore-scripts",
|
|
144
|
+
"--omit=dev",
|
|
145
|
+
"--no-audit",
|
|
146
|
+
"--no-fund",
|
|
147
|
+
`${SDK_PACKAGE}@${version}`,
|
|
148
|
+
];
|
|
149
|
+
if (mode === "host-binary")
|
|
150
|
+
argv.push("--omit=optional");
|
|
151
|
+
return argv;
|
|
152
|
+
}
|
|
153
|
+
/** A held lock older than this is treated as abandoned and reclaimed. */
|
|
154
|
+
const LOCK_STALE_MS = 10 * 60 * 1000;
|
|
155
|
+
const execFileAsync = promisify(execFile);
|
|
156
|
+
/**
|
|
157
|
+
* The production `install`: `SUPERD_SDK_NPM` (so the fetch can be pointed at
|
|
158
|
+
* pnpm/corepack) over an otherwise inherited environment, run in the runtime
|
|
159
|
+
* dir. `maxBuffer` is raised for npm's progress output.
|
|
160
|
+
*/
|
|
161
|
+
async function defaultInstall(cmd, args, opts) {
|
|
162
|
+
await execFileAsync(cmd, args, {
|
|
163
|
+
cwd: opts.cwd,
|
|
164
|
+
env: opts.env ?? process.env,
|
|
165
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Return the record for `<worldHome>`'s durable SDK runtime, fetching it first
|
|
170
|
+
* when it is absent, stale or was installed under the other mode.
|
|
171
|
+
*
|
|
172
|
+
* Idempotent: a complete, matching runtime short-circuits without touching the
|
|
173
|
+
* network. Concurrent callers are separated by a `mkdir` lock — a second caller
|
|
174
|
+
* that finds a FRESH lock fails with a clear "fetch in progress" error (the SDK
|
|
175
|
+
* handle is a per-process singleton; the real race is another dsh process on
|
|
176
|
+
* the same world home), while an abandoned lock (older than
|
|
177
|
+
* {@link LOCK_STALE_MS}) is reclaimed so a crashed fetch cannot brick the line.
|
|
178
|
+
*
|
|
179
|
+
* A failure appends to `<worldHome>/sdk-runtime.error.log`, drops the partial
|
|
180
|
+
* tree and throws one actionable error — never a marker, so the next call heals.
|
|
181
|
+
*/
|
|
182
|
+
export async function ensureSdkRuntime(worldHome, deps) {
|
|
183
|
+
const install = deps?.install ?? defaultInstall;
|
|
184
|
+
const now = deps?.now ?? Date.now;
|
|
185
|
+
const layout = sdkRuntimeLayout(worldHome);
|
|
186
|
+
const wantedMode = resolveClaudeBinary() !== undefined ? "host-binary" : "bundled-payload";
|
|
187
|
+
const ready = sdkRuntimeReady(worldHome, wantedMode);
|
|
188
|
+
if (ready !== undefined)
|
|
189
|
+
return ready;
|
|
190
|
+
mkdirSync(worldHome, { recursive: true });
|
|
191
|
+
acquireLock(layout);
|
|
192
|
+
try {
|
|
193
|
+
// Re-check inside the lock: a peer may have completed the fetch in the
|
|
194
|
+
// window between our check and the lock, in which case we pay nothing.
|
|
195
|
+
const done = sdkRuntimeReady(worldHome, wantedMode);
|
|
196
|
+
if (done !== undefined)
|
|
197
|
+
return done;
|
|
198
|
+
try {
|
|
199
|
+
return await fetchRuntime(worldHome, layout, wantedMode, install, now);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
203
|
+
try {
|
|
204
|
+
appendFileSync(layout.errorLog, `[${new Date().toISOString()}] agent-claude SDK runtime fetch failed: ${detail}\n`);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
/* the trace is best-effort; the throw below is the contract */
|
|
208
|
+
}
|
|
209
|
+
rmSync(layout.dir, { recursive: true, force: true });
|
|
210
|
+
throw new Error(`agent-claude: could not fetch the SDK runtime ` +
|
|
211
|
+
`(${SDK_PACKAGE}@${SDK_PACKAGE_VERSION}, ${wantedMode} mode) at ${layout.dir}: ${detail}. ` +
|
|
212
|
+
`Trace: ${layout.errorLog}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
rmSync(layout.lock, { recursive: true, force: true });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/** The private manifest that anchors npm to the runtime dir (never published). */
|
|
220
|
+
const RUNTIME_MANIFEST = '{"name":"superd-claude-sdk-runtime","private":true}\n';
|
|
221
|
+
/**
|
|
222
|
+
* The idempotent install step. npm is anchored to the runtime dir by a private
|
|
223
|
+
* manifest BEFORE install (without one, npm resolves the nearest ANCESTOR
|
|
224
|
+
* project and may exit 0 having landed nothing here), and the entry is
|
|
225
|
+
* verified on disk BEFORE the marker is written — exit 0 alone is not success.
|
|
226
|
+
*/
|
|
227
|
+
async function fetchRuntime(worldHome, layout, mode, install, now) {
|
|
228
|
+
mkdirSync(layout.dir, { recursive: true });
|
|
229
|
+
// ANCHOR — npm resolves a tree from the nearest ancestor holding a
|
|
230
|
+
// package.json; with none in the runtime dir it walks up (on the real
|
|
231
|
+
// machine: to the repo root), reports "up to date" against THAT project,
|
|
232
|
+
// exits 0 and lands no tree here — yet the marker used to be written anyway,
|
|
233
|
+
// so every later loadClaudeQuery failed and each "heal" re-hit the same
|
|
234
|
+
// no-op install (reproduced twice, 2026-10-04). The private manifest pins
|
|
235
|
+
// the install to THIS directory; written only when missing or not
|
|
236
|
+
// byte-exact, so a re-run never rewrites a manifest that is already correct.
|
|
237
|
+
const manifest = join(layout.dir, "package.json");
|
|
238
|
+
let manifestExact = false;
|
|
239
|
+
if (existsSync(manifest)) {
|
|
240
|
+
try {
|
|
241
|
+
manifestExact = readFileSync(manifest, "utf8") === RUNTIME_MANIFEST;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
manifestExact = false; // unreadable ⇒ treat as absent and re-anchor
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (!manifestExact)
|
|
248
|
+
writeFileSync(manifest, RUNTIME_MANIFEST);
|
|
249
|
+
const npm = process.env.SUPERD_SDK_NPM ?? "npm";
|
|
250
|
+
await install(npm, sdkInstallArgv(SDK_PACKAGE_VERSION, mode), { cwd: layout.dir });
|
|
251
|
+
// VERIFY — a zero exit is not success: an unanchored (or no-op) install
|
|
252
|
+
// resolves without producing the entry. Fail BEFORE the marker — it must
|
|
253
|
+
// only ever exist with the entry verifiably on disk, or the self-healing
|
|
254
|
+
// loop never converges. The catch in `ensureSdkRuntime` appends the trace
|
|
255
|
+
// (the note below names the cause) and wipes this partial tree.
|
|
256
|
+
const entry = sdkEntryPath(worldHome);
|
|
257
|
+
if (!existsSync(entry)) {
|
|
258
|
+
throw new Error(`npm install exited 0 but produced no entry at ${entry} — ` +
|
|
259
|
+
`the install was not anchored to the runtime dir ` +
|
|
260
|
+
`(npm likely resolved an ancestor project's tree instead of ${manifest})`);
|
|
261
|
+
}
|
|
262
|
+
// Marker LAST — a crash before this point leaves an incomplete runtime that
|
|
263
|
+
// the next call heals, never a false success.
|
|
264
|
+
const record = {
|
|
265
|
+
sdkPackage: SDK_PACKAGE,
|
|
266
|
+
sdkVersion: SDK_PACKAGE_VERSION,
|
|
267
|
+
mode,
|
|
268
|
+
createdAt: now(),
|
|
269
|
+
};
|
|
270
|
+
writeFileSync(layout.marker, JSON.stringify(record, null, 2) + "\n");
|
|
271
|
+
rmSync(layout.errorLog, { force: true });
|
|
272
|
+
return record;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* mkdir-based mutex: returns owning the lock, throws when a FRESH lock is held
|
|
276
|
+
* by someone else, reclaims a lock older than {@link LOCK_STALE_MS}.
|
|
277
|
+
*/
|
|
278
|
+
function acquireLock(layout) {
|
|
279
|
+
for (;;) {
|
|
280
|
+
try {
|
|
281
|
+
mkdirSync(layout.lock);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
if (error.code !== "EEXIST")
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
let ageMs;
|
|
289
|
+
try {
|
|
290
|
+
ageMs = Date.now() - statSync(layout.lock).mtimeMs;
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
continue; // released between the mkdir attempt and the stat — retry now
|
|
294
|
+
}
|
|
295
|
+
if (ageMs > LOCK_STALE_MS) {
|
|
296
|
+
rmSync(layout.lock, { recursive: true, force: true });
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
throw new Error(`agent-claude: another SDK runtime fetch is already in progress (lock ${layout.lock}). ` +
|
|
300
|
+
`Retry once it finishes; if no fetch is running, remove that lock directory.`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Ensure the durable runtime and return the installed SDK entry's `query`
|
|
305
|
+
* function — the ONE function the client's default factory drives, and the only
|
|
306
|
+
* place a real download/install happens on the lazy path.
|
|
307
|
+
*
|
|
308
|
+
* The entry is imported by resolved path (see the header) and its shape is
|
|
309
|
+
* checked here: a tree that installs but exposes no `query` fails at fetch
|
|
310
|
+
* time, loud and near the cause, rather than inside the agent's first prompt.
|
|
311
|
+
*/
|
|
312
|
+
export async function loadClaudeQuery(worldHome) {
|
|
313
|
+
await ensureSdkRuntime(worldHome);
|
|
314
|
+
const entry = sdkEntryPath(worldHome);
|
|
315
|
+
const module = (await import(pathToFileURL(entry).href));
|
|
316
|
+
if (typeof module.query !== "function") {
|
|
317
|
+
throw new Error(`agent-claude: ${entry} exposes no query function`);
|
|
318
|
+
}
|
|
319
|
+
return module.query;
|
|
320
|
+
}
|
|
@@ -18,10 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsc -p tsconfig.json",
|
|
21
|
-
"test": "node --test test/*.test.mjs"
|
|
22
|
-
|
|
23
|
-
"dependencies": {
|
|
24
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.263"
|
|
21
|
+
"test": "node --test test/*.test.mjs",
|
|
22
|
+
"fetch-sdk-runtime": "node scripts/fetch-sdk-runtime.mjs"
|
|
25
23
|
},
|
|
26
24
|
"peerDependencies": {
|
|
27
25
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
@@ -30,6 +28,7 @@
|
|
|
30
28
|
"@deepseek-ai/dsh-llm": ">=0.1.5-0 <0.3.0-0",
|
|
31
29
|
"@deepseek-ai/dsh-session": ">=0.1.5-0 <0.3.0-0",
|
|
32
30
|
"@deepseek-ai/dsh-session-persistence": ">=0.1.5-0 <0.3.0-0",
|
|
31
|
+
"@deepseek-ai/dsh-typert-protocol": ">=0.1.5-0 <0.3.0-0",
|
|
33
32
|
"@deepseek-ai/dsh-user-approval": ">=0.1.5-0 <0.3.0-0",
|
|
34
33
|
"@deepseek-ai/dsh-user-questions": ">=0.1.5-0 <0.3.0-0"
|
|
35
34
|
},
|
|
@@ -52,6 +51,9 @@
|
|
|
52
51
|
"@deepseek-ai/dsh-session-persistence": {
|
|
53
52
|
"optional": true
|
|
54
53
|
},
|
|
54
|
+
"@deepseek-ai/dsh-typert-protocol": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
55
57
|
"@deepseek-ai/dsh-user-approval": {
|
|
56
58
|
"optional": true
|
|
57
59
|
},
|
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)}`,
|