super-dsh 0.1.11 → 0.1.12
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 +54 -6
- package/agent-agy/dist/agent.d.ts +4 -0
- package/agent-agy/dist/agent.js +41 -7
- package/agent-agy/dist/agy-cli-client.d.ts +12 -0
- package/agent-agy/dist/agy-cli-client.js +81 -13
- package/agent-agy/dist/index.js +13 -7
- package/agent-agy/package.json +5 -1
- package/agent-claude/dist/agent.js +67 -4
- package/agent-claude/dist/claude-client.js +46 -1
- package/agent-claude/dist/index.js +3 -0
- package/agent-claude/dist/sdk-runtime.js +2 -2
- package/agent-codex/dist/agent.js +33 -8
- package/agent-codex/dist/app-server-client.js +26 -3
- package/agent-codex/dist/app-server-protocol.js +6 -2
- package/agent-codex/dist/app-server-wire.js +23 -6
- package/agent-codex/dist/codex-native-config.js +6 -1
- package/agent-codex/dist/codex-runtime-fetch.js +293 -0
- package/agent-codex/dist/codex-runtime.js +12 -12
- package/agent-codex/dist/index.js +10 -4
- package/agent-codex/package.json +8 -3
- package/agent-codex-sdk/dist/sdk-runtime.js +1 -1
- package/agent-hermes/dist/hermes-client.js +44 -5
- package/agent-hermes/dist/index.js +10 -5
- package/agent-omp/dist/agent.js +36 -5
- package/agent-omp/dist/index.js +29 -6
- package/agent-omp/dist/lazy-rpc.js +11 -2
- package/agent-omp/dist/rpc-client.js +6 -1
- package/agent-pi/dist/agent.js +36 -9
- package/agent-pi/dist/index.js +6 -2
- package/agent-pi/dist/pi-client.js +13 -3
- package/cordis.patch.yml +10 -9
- package/package.json +6 -4
- package/superd-hub/dist/a2a-row.js +20 -3
- package/superd-hub/dist/connection-trust.d.ts +8 -0
- package/superd-hub/dist/connection-trust.js +16 -0
- package/superd-hub/dist/foreign-agy.d.ts +3 -0
- package/superd-hub/dist/foreign-agy.js +110 -0
- package/superd-hub/dist/foreign-claude.d.ts +4 -0
- package/superd-hub/dist/foreign-claude.js +160 -0
- package/superd-hub/dist/foreign-extension.d.ts +10 -0
- package/superd-hub/dist/foreign-extension.js +27 -0
- package/superd-hub/dist/foreign-hub.d.ts +27 -0
- package/superd-hub/dist/foreign-hub.js +243 -0
- package/superd-hub/dist/foreign-native.d.ts +4 -0
- package/superd-hub/dist/foreign-native.js +115 -0
- package/superd-hub/dist/web-trust.d.ts +14 -42
- package/superd-hub/dist/web-trust.js +18 -43
- package/superd-hub/package.json +14 -2
- package/agent-codex/README.md +0 -191
- 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
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codex-runtime-fetch.ts — the lazy, durable Codex runtime the app-server line
|
|
3
|
+
* falls back to when the operator has NO codex of their own.
|
|
4
|
+
*
|
|
5
|
+
* 2026-10-06 user ruling (amends the 2026-10-05 core-discovery order): the
|
|
6
|
+
* exact-pin `@openai/codex` package must NEVER ride the installation — it went
|
|
7
|
+
* out as a real `dependencies` entry in 0.1.11, so every registry consumer
|
|
8
|
+
* downloaded the pinned CLI binary package AND its six aliased platform
|
|
9
|
+
* binaries (linux-x64/arm64, darwin-x64/arm64, win32-x64/arm64) at install
|
|
10
|
+
* time. That is the omp-sdk/agy-sdk/codex-sdk rule violated one week after it
|
|
11
|
+
* was written: SDKs and binaries are second-class — anything a line needs, it
|
|
12
|
+
* fetches at RUNTIME into its world home, and installs stay clean (no
|
|
13
|
+
* postinstall, no allow-build, no dependency that ships a binary).
|
|
14
|
+
*
|
|
15
|
+
* So the discovery order in codex-runtime.ts is
|
|
16
|
+
* CODEX_BIN → PATH CLI → desktop core → world-home runtime (this module),
|
|
17
|
+
* and this module is the only supplier of that last tier: on the first real
|
|
18
|
+
* prompt, when override, CLI and desktop all missed, the pinned
|
|
19
|
+
* `@openai/codex@0.160.0` tree is fetched into
|
|
20
|
+
*
|
|
21
|
+
* <worldHome>/codex-runtime/
|
|
22
|
+
* package.json written by us (private, never published)
|
|
23
|
+
* node_modules/ `npm install --exact --ignore-scripts --omit=dev`
|
|
24
|
+
* node_modules/@openai/codex/bin/codex.js the entry discovery returns
|
|
25
|
+
* <worldHome>/codex-runtime.json marker — written LAST, after the smoke run
|
|
26
|
+
* <worldHome>/codex-runtime.lock atomic (mkdir) bootstrap lock
|
|
27
|
+
* <worldHome>/codex-runtime.error.log written ONLY by a failed fetch
|
|
28
|
+
*
|
|
29
|
+
* Why the world home rather than the package root: the package root is the
|
|
30
|
+
* installed tarball — read-only in every real consumer install, and shared by
|
|
31
|
+
* every world. The world home (`<dshHome>/agents/codex`) is per-runtime,
|
|
32
|
+
* writable, and already the home of this adapter's own DSH state.
|
|
33
|
+
*
|
|
34
|
+
* Readiness is the ENTRY FILE (checked on every discovery, zero network) plus
|
|
35
|
+
* the marker's package version: a pin bump in a future adapter must refetch,
|
|
36
|
+
* and the sync resolver alone would happily keep using the old tree.
|
|
37
|
+
*
|
|
38
|
+
* The npm invocation is deliberately `--ignore-scripts`: the codex packages
|
|
39
|
+
* ship their binaries inside the tarballs, so a fetch needs zero lifecycle
|
|
40
|
+
* scripts — exactly the property this ruling exists to protect.
|
|
41
|
+
*/
|
|
42
|
+
import { execFile } from "node:child_process";
|
|
43
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
44
|
+
import { dirname, join, resolve } from "node:path";
|
|
45
|
+
import { fileURLToPath } from "node:url";
|
|
46
|
+
import { promisify } from "node:util";
|
|
47
|
+
import { resolveCodexRuntime } from "./codex-runtime.js";
|
|
48
|
+
/** The pinned Codex CLI package this line fetches (ships the app-server binary). */
|
|
49
|
+
export const CODEX_RUNTIME_PACKAGE = "@openai/codex";
|
|
50
|
+
/** Pin of {@link CODEX_RUNTIME_PACKAGE}; bumping it must refetch the tree. */
|
|
51
|
+
export const CODEX_RUNTIME_VERSION = "0.160.0";
|
|
52
|
+
/** The entry `resolveCodexRuntime` returns, relative to the runtime dir. */
|
|
53
|
+
const CODEX_ENTRY_RELATIVE = ["node_modules", CODEX_RUNTIME_PACKAGE, "bin", "codex.js"];
|
|
54
|
+
export function codexRuntimeLayout(worldHome) {
|
|
55
|
+
const dir = join(worldHome, "codex-runtime");
|
|
56
|
+
return {
|
|
57
|
+
dir,
|
|
58
|
+
manifest: join(dir, "package.json"),
|
|
59
|
+
entry: join(dir, ...CODEX_ENTRY_RELATIVE),
|
|
60
|
+
marker: join(worldHome, "codex-runtime.json"),
|
|
61
|
+
lock: join(worldHome, "codex-runtime.lock"),
|
|
62
|
+
errorLog: join(worldHome, "codex-runtime.error.log"),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Read the install marker; undefined when absent or unreadable. */
|
|
66
|
+
export function readCodexRuntimeRecord(worldHome) {
|
|
67
|
+
const { marker } = codexRuntimeLayout(worldHome);
|
|
68
|
+
if (!existsSync(marker))
|
|
69
|
+
return undefined;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(readFileSync(marker, "utf8"));
|
|
72
|
+
return typeof parsed === "object" && parsed !== null ? parsed : undefined;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Fast path: a fetched runtime exists, matches `identity`, and its entry file
|
|
80
|
+
* is still on disk. Everything else must (re)fetch or (re)discover.
|
|
81
|
+
*/
|
|
82
|
+
export function codexRuntimeCurrent(worldHome, identity) {
|
|
83
|
+
const record = readCodexRuntimeRecord(worldHome);
|
|
84
|
+
if (record === undefined)
|
|
85
|
+
return undefined;
|
|
86
|
+
if (record.pluginVersion !== identity.pluginVersion)
|
|
87
|
+
return undefined;
|
|
88
|
+
if (record.packageVersion !== identity.packageVersion)
|
|
89
|
+
return undefined;
|
|
90
|
+
if (!existsSync(codexRuntimeLayout(worldHome).entry))
|
|
91
|
+
return undefined;
|
|
92
|
+
return record;
|
|
93
|
+
}
|
|
94
|
+
/** The npm argv the runtime tree is installed with (kept pure for the test). */
|
|
95
|
+
export function codexInstallArgv(packageVersion = CODEX_RUNTIME_VERSION) {
|
|
96
|
+
return [
|
|
97
|
+
"install",
|
|
98
|
+
"--save-exact",
|
|
99
|
+
"--ignore-scripts",
|
|
100
|
+
"--omit=dev",
|
|
101
|
+
`${CODEX_RUNTIME_PACKAGE}@${packageVersion}`,
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
/** A held lock older than this is treated as abandoned and reclaimed. */
|
|
105
|
+
const LOCK_STALE_MS = 30 * 60 * 1000;
|
|
106
|
+
/** The smoke run's wall-clock cap: `--version` must never wedge a turn. */
|
|
107
|
+
const SMOKE_TIMEOUT_MS = 60_000;
|
|
108
|
+
const execFileAsync = promisify(execFile);
|
|
109
|
+
/**
|
|
110
|
+
* The PRODUCTION {@link CodexRuntimeFetchDeps}: real npm/Node invocations plus
|
|
111
|
+
* the marker identity read from this package's own manifest. Kept IN THE
|
|
112
|
+
* MODULE so the manual pre-warm script and the first-prompt lazy fetch share
|
|
113
|
+
* ONE implementation — a second copy is exactly how the two paths would drift.
|
|
114
|
+
*/
|
|
115
|
+
export function createCodexRuntimeDeps(options = {}) {
|
|
116
|
+
const pluginDir = options.pluginDir ?? dirname(dirname(fileURLToPath(import.meta.url)));
|
|
117
|
+
return {
|
|
118
|
+
run: async (cmd, args, opts) => {
|
|
119
|
+
await execFileAsync(cmd, args, { cwd: opts?.cwd, env: options.env, maxBuffer: 64 * 1024 * 1024 });
|
|
120
|
+
},
|
|
121
|
+
smoke: async (cmd, args, opts) => {
|
|
122
|
+
const { stdout } = await execFileAsync(cmd, args, {
|
|
123
|
+
cwd: opts?.cwd,
|
|
124
|
+
env: options.env,
|
|
125
|
+
timeout: opts?.timeoutMs,
|
|
126
|
+
maxBuffer: 1024 * 1024,
|
|
127
|
+
});
|
|
128
|
+
return stdout;
|
|
129
|
+
},
|
|
130
|
+
pluginVersion: readPluginVersion(pluginDir),
|
|
131
|
+
log: (line) => process.stderr.write(`[agent-codex] ${line}\n`),
|
|
132
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** The version of the adapter package at `pluginDir` (the marker identity half). */
|
|
136
|
+
export function readPluginVersion(pluginDir) {
|
|
137
|
+
const raw = readFileSync(join(pluginDir, "package.json"), "utf8");
|
|
138
|
+
const parsed = JSON.parse(raw);
|
|
139
|
+
if (typeof parsed.version !== "string" || parsed.version === "") {
|
|
140
|
+
throw new Error(`codex: ${join(pluginDir, "package.json")} carries no version`);
|
|
141
|
+
}
|
|
142
|
+
return parsed.version;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Sync discovery across the whole ladder INCLUDING the fetched runtime; a
|
|
146
|
+
* miss throws (which is the fetch trigger, never surfaced to the user).
|
|
147
|
+
*/
|
|
148
|
+
function discoverWith(worldHome, deps) {
|
|
149
|
+
return resolveCodexRuntime({
|
|
150
|
+
...(deps.discovery ?? {}),
|
|
151
|
+
...(deps.env === undefined ? {} : { env: deps.env }),
|
|
152
|
+
packageEntryCandidates: [codexRuntimeLayout(worldHome).entry],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Return a usable Codex runtime for `<worldHome>`, fetching the pinned
|
|
157
|
+
* package first when the operator's own CODEX_BIN / PATH CLI / desktop core
|
|
158
|
+
* all miss.
|
|
159
|
+
*
|
|
160
|
+
* Idempotent: an operator-provided runtime short-circuits without touching
|
|
161
|
+
* the network, and so does a complete, matching fetched runtime. Concurrent
|
|
162
|
+
* callers: the bootstrap is guarded by a `mkdir` lock — a second caller that
|
|
163
|
+
* finds a FRESH lock throws a clear "fetch in progress" error, while an
|
|
164
|
+
* abandoned lock (older than {@link LOCK_STALE_MS}) is reclaimed so a crashed
|
|
165
|
+
* fetch cannot brick the line.
|
|
166
|
+
*
|
|
167
|
+
* Failures append to `<worldHome>/codex-runtime.error.log` and throw a single
|
|
168
|
+
* actionable error — never a partial runtime behind a success marker.
|
|
169
|
+
*/
|
|
170
|
+
export async function ensureCodexRuntime(worldHome, deps) {
|
|
171
|
+
const usable = (() => {
|
|
172
|
+
try {
|
|
173
|
+
return discoverWith(worldHome, deps);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
})();
|
|
179
|
+
// An operator runtime (override/CLI/desktop) is always taken as-is; a fetched
|
|
180
|
+
// runtime is taken only when its pin still matches — a bumped pin refetches.
|
|
181
|
+
if (usable !== undefined && usable.source !== "package")
|
|
182
|
+
return usable;
|
|
183
|
+
const identity = { pluginVersion: deps.pluginVersion, packageVersion: CODEX_RUNTIME_VERSION };
|
|
184
|
+
if (usable !== undefined && codexRuntimeCurrent(worldHome, identity) !== undefined)
|
|
185
|
+
return usable;
|
|
186
|
+
const layout = codexRuntimeLayout(worldHome);
|
|
187
|
+
mkdirSync(worldHome, { recursive: true });
|
|
188
|
+
acquireLock(layout, deps);
|
|
189
|
+
try {
|
|
190
|
+
// Re-check inside the lock: a peer may have completed the fetch in the
|
|
191
|
+
// window between our check and the lock, in which case we pay nothing.
|
|
192
|
+
const raced = (() => {
|
|
193
|
+
try {
|
|
194
|
+
return discoverWith(worldHome, deps);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
})();
|
|
200
|
+
if (raced !== undefined && (raced.source !== "package" || codexRuntimeCurrent(worldHome, identity) !== undefined))
|
|
201
|
+
return raced;
|
|
202
|
+
try {
|
|
203
|
+
await buildRuntime(layout, deps, identity);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
207
|
+
const stamp = new Date().toISOString();
|
|
208
|
+
try {
|
|
209
|
+
appendFileSync(layout.errorLog, `[${stamp}] codex runtime fetch failed: ${detail}\n`);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
/* the trace is best-effort; the throw below is the contract */
|
|
213
|
+
}
|
|
214
|
+
throw new Error(`codex: could not fetch the runtime (${CODEX_RUNTIME_PACKAGE}@${CODEX_RUNTIME_VERSION}) at ${layout.dir}: ${detail}. ` +
|
|
215
|
+
`Pre-warm it by hand with \`npm run fetch-codex-runtime\`, or install a Codex CLI and set CODEX_BIN. Trace: ${layout.errorLog}`);
|
|
216
|
+
}
|
|
217
|
+
const resolved = discoverWith(worldHome, deps);
|
|
218
|
+
deps.log(`codex: runtime ready at ${layout.dir} (${CODEX_RUNTIME_PACKAGE}@${CODEX_RUNTIME_VERSION})`);
|
|
219
|
+
return resolved;
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
rmSync(layout.lock, { recursive: true, force: true });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** The idempotent build steps; the marker is written last, after the smoke run. */
|
|
226
|
+
async function buildRuntime(layout, deps, identity) {
|
|
227
|
+
mkdirSync(layout.dir, { recursive: true });
|
|
228
|
+
// npm needs a manifest to own the tree; `private` + a throwaway version keep
|
|
229
|
+
// it unambiguously not-a-publishable-package, and an existing one is left
|
|
230
|
+
// alone so a later install can still be reproduced by hand.
|
|
231
|
+
if (!existsSync(layout.manifest)) {
|
|
232
|
+
writeFileSync(layout.manifest, JSON.stringify({ name: "codex-runtime", private: true, version: "0.0.0" }, null, 2) + "\n");
|
|
233
|
+
}
|
|
234
|
+
deps.log(`codex: fetching ${CODEX_RUNTIME_PACKAGE}@${CODEX_RUNTIME_VERSION} into ${layout.dir}`);
|
|
235
|
+
await deps.run("npm", codexInstallArgv(), { cwd: layout.dir });
|
|
236
|
+
if (!existsSync(layout.entry)) {
|
|
237
|
+
throw new Error(`npm install produced no ${layout.entry} (the package layout changed?)`);
|
|
238
|
+
}
|
|
239
|
+
// Smoke step: run the fetched entry's `--version`. This exercises the real
|
|
240
|
+
// platform-binary pick the entry performs at run time — a tree whose binary
|
|
241
|
+
// does not match this host fails HERE, at the fetch, with a trace on disk,
|
|
242
|
+
// instead of at the first prompt inside the agent's cold-start error path.
|
|
243
|
+
// The entry is resolved ABSOLUTE before the cwd switch — the smoke run
|
|
244
|
+
// changes the working directory, and a relative worldHome would otherwise
|
|
245
|
+
// re-resolve against it (same trap as the `-c model_catalog_json` lesson).
|
|
246
|
+
const smokeEntry = resolve(layout.entry);
|
|
247
|
+
deps.log(`codex: smoke-running ${CODEX_RUNTIME_PACKAGE}@${CODEX_RUNTIME_VERSION} --version`);
|
|
248
|
+
const stdout = await deps.smoke(process.execPath, [smokeEntry, "--version"], { cwd: layout.dir, timeoutMs: SMOKE_TIMEOUT_MS });
|
|
249
|
+
const smokeVersion = stdout.trim().split("\n")[0]?.trim() ?? "";
|
|
250
|
+
if (smokeVersion === "") {
|
|
251
|
+
throw new Error(`smoke run of ${layout.entry} --version produced no output`);
|
|
252
|
+
}
|
|
253
|
+
// Marker LAST — a crash before this point leaves an incomplete runtime that
|
|
254
|
+
// the next call heals, never a false success.
|
|
255
|
+
const record = {
|
|
256
|
+
...identity,
|
|
257
|
+
entryPath: layout.entry,
|
|
258
|
+
smokeVersion,
|
|
259
|
+
installedAt: new Date().toISOString(),
|
|
260
|
+
};
|
|
261
|
+
writeFileSync(layout.marker, JSON.stringify(record, null, 2) + "\n");
|
|
262
|
+
rmSync(layout.errorLog, { force: true });
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* mkdir-based mutex. Returns owning the lock; throws when a fresh lock is held
|
|
266
|
+
* by someone else; reclaims a lock older than {@link LOCK_STALE_MS}.
|
|
267
|
+
*/
|
|
268
|
+
function acquireLock(layout, deps) {
|
|
269
|
+
for (;;) {
|
|
270
|
+
try {
|
|
271
|
+
mkdirSync(layout.lock);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
if (error.code !== "EEXIST")
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
let ageMs;
|
|
279
|
+
try {
|
|
280
|
+
ageMs = Date.now() - statSync(layout.lock).mtimeMs;
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
continue; // released between the mkdir attempt and the stat — retry now
|
|
284
|
+
}
|
|
285
|
+
if (ageMs > LOCK_STALE_MS) {
|
|
286
|
+
deps.log(`codex: reclaiming stale runtime lock (age ${Math.round(ageMs / 1000)}s)`);
|
|
287
|
+
rmSync(layout.lock, { recursive: true, force: true });
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
throw new Error(`codex: another runtime fetch is already in progress (lock ${layout.lock}). ` +
|
|
291
|
+
`Retry once it finishes; if no fetch is running, remove that lock directory.`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** Resolve an installed Codex runtime at first use, without modifying its home. */
|
|
2
2
|
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
3
|
import { homedir } from "node:os";
|
|
5
4
|
import { posix, win32 } from "node:path";
|
|
6
5
|
function readableFile(path) {
|
|
@@ -66,6 +65,7 @@ export function resolveCodexRuntime(options = {}) {
|
|
|
66
65
|
env,
|
|
67
66
|
homeDir: options.homeDir ?? homedir(),
|
|
68
67
|
...(options.desktopCandidates === undefined ? {} : { desktopCandidates: options.desktopCandidates }),
|
|
68
|
+
...(options.packageEntryCandidates === undefined ? {} : { packageEntryCandidates: options.packageEntryCandidates }),
|
|
69
69
|
isFile,
|
|
70
70
|
isExecutable,
|
|
71
71
|
realpath: options.realpath ?? realpathSync,
|
|
@@ -84,7 +84,6 @@ export function resolveCodexRuntime(options = {}) {
|
|
|
84
84
|
}
|
|
85
85
|
return undefined;
|
|
86
86
|
}),
|
|
87
|
-
resolvePackageEntry: options.resolvePackageEntry ?? (() => createRequire(import.meta.url).resolve("@openai/codex/bin/codex.js")),
|
|
88
87
|
};
|
|
89
88
|
const usable = (command) => !shellWrapper(command) && isFile(command) && (isNodeScript(command, discovery) || isExecutable(command));
|
|
90
89
|
const selected = (command, source) => ({ command, nodeScript: isNodeScript(command, discovery), source });
|
|
@@ -97,22 +96,23 @@ export function resolveCodexRuntime(options = {}) {
|
|
|
97
96
|
throw new Error(`CODEX_BIN ${override} is not a readable JavaScript entry or executable Codex runtime. Fix CODEX_BIN or remove the override.`);
|
|
98
97
|
return selected(command, "override");
|
|
99
98
|
}
|
|
99
|
+
const cli = discovery.findCommand("codex");
|
|
100
|
+
if (cli !== undefined && usable(cli))
|
|
101
|
+
return selected(cli, "cli");
|
|
100
102
|
for (const candidate of desktopPaths(discovery)) {
|
|
101
103
|
if (!shellWrapper(candidate) && isFile(candidate) && isExecutable(candidate))
|
|
102
104
|
return selected(candidate, "desktop");
|
|
103
105
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (isFile(entry))
|
|
106
|
+
// Last resort only: the runtime the fetch layer materialized into the world
|
|
107
|
+
// home. It cannot pre-exist on a clean install — the fetch only runs when the
|
|
108
|
+
// override, CLI and desktop all missed, so checking it last is exact.
|
|
109
|
+
for (const entry of discovery.packageEntryCandidates ?? []) {
|
|
110
|
+
if (!shellWrapper(entry) && isFile(entry))
|
|
110
111
|
return { command: entry, nodeScript: true, source: "package" };
|
|
111
112
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
throw new Error("cannot locate a Codex runtime: install Codex desktop or a user CLI on PATH, restore the exact-pin @openai/codex package, or set CODEX_BIN to its executable or JavaScript entry.");
|
|
113
|
+
throw new Error("cannot locate a Codex runtime: install a Codex CLI (or the desktop app) on PATH, " +
|
|
114
|
+
"set CODEX_BIN to its executable or JavaScript entry, " +
|
|
115
|
+
"or let the adapter fetch the pinned @openai/codex package into the world home at first prompt.");
|
|
116
116
|
}
|
|
117
117
|
/** Compatibility interface for callers that need only the selected path. */
|
|
118
118
|
export function resolveCodexCommand(options = {}) {
|
|
@@ -20,6 +20,7 @@ import { OPENAI_OAUTH_ID, readEffectiveProviderStore } from "./codex-oauth.js";
|
|
|
20
20
|
* record and fails closed when the thread is unknown. The rollout scan, the
|
|
21
21
|
* adapter-owned session list and the transcript replay are gone.
|
|
22
22
|
*/
|
|
23
|
+
import { bindForeignHub } from '@pgmi-builds/superd-hub/foreign-hub';
|
|
23
24
|
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
24
25
|
import { join } from "node:path";
|
|
25
26
|
import { Service } from "@deepseek-ai/cordis";
|
|
@@ -411,6 +412,9 @@ export class CodexProvider extends Service {
|
|
|
411
412
|
this.routes.replace([...ids]);
|
|
412
413
|
trace(`routes replaced: ${ids.join(", ") || "none"}`);
|
|
413
414
|
}
|
|
415
|
+
hubFor(sessionId) {
|
|
416
|
+
return bindForeignHub('codex', sessionId, this.home, () => this.runtime.ctx.agents.get(sessionId));
|
|
417
|
+
}
|
|
414
418
|
async createAgent(ownerCtx, options) {
|
|
415
419
|
const loopCtx = this.runtime.ctx;
|
|
416
420
|
const id = options.sessionId;
|
|
@@ -444,7 +448,7 @@ export class CodexProvider extends Service {
|
|
|
444
448
|
// app-server child is NOT spawned here (lazy-spawn gate, rules §14-i) —
|
|
445
449
|
// create() is pure construction, and the process materializes on the first
|
|
446
450
|
// real prompt.
|
|
447
|
-
const client = CodexAppServerClient.create({ cwd: spawnCwd, approvalPolicy, sandboxMode });
|
|
451
|
+
const client = CodexAppServerClient.create({ cwd: spawnCwd, approvalPolicy, sandboxMode, worldHome: this.home, hub: this.hubFor(id) });
|
|
448
452
|
// The session's route is NOT pinned here any more (2026-10-04): the agent
|
|
449
453
|
// resolves it per turn from DSH's own model-selection state, and a launch
|
|
450
454
|
// costs nothing until the first real prompt. `setupAndPublish` still seeds a
|
|
@@ -499,7 +503,7 @@ export class CodexProvider extends Service {
|
|
|
499
503
|
if (record === undefined) {
|
|
500
504
|
throw new CodexResumeError("SESSION_MAP_MISS", `cannot resume session "${id}": no Codex thread is recorded for this Dash session id`);
|
|
501
505
|
}
|
|
502
|
-
if (record.threadId === null) {
|
|
506
|
+
if (record.threadId === null && (await readStoredEvents(persistence, id, options.signal)).some(event => event.type === "turn/start" || event.type === "user/message" || event.type === "assistant/message")) {
|
|
503
507
|
// C2: never auto-start a fresh thread — the session's history is in the
|
|
504
508
|
// (missing) native thread, not in a blank one.
|
|
505
509
|
throw new CodexResumeError("NATIVE_THREAD_NEVER_STARTED", `cannot resume session "${id}": its Codex thread was never started (no thread id recorded)`);
|
|
@@ -514,7 +518,7 @@ export class CodexProvider extends Service {
|
|
|
514
518
|
// `<worldHome>/.codex` home (never migrated) and pruned rollouts alike.
|
|
515
519
|
// Without this gate the failure would surface only at the first turn
|
|
516
520
|
// (resumeThread is zero-IO).
|
|
517
|
-
if (readRolloutHead(resolveCodexHome(), record.threadId) === undefined) {
|
|
521
|
+
if (record.threadId !== null && readRolloutHead(resolveCodexHome(), record.threadId) === undefined) {
|
|
518
522
|
throw new CodexResumeError("ROLLOUT_MISSING", `cannot resume session "${id}": no rollout for Codex thread ${record.threadId} exists in the current Codex home (${resolveCodexHome()})`);
|
|
519
523
|
}
|
|
520
524
|
// Spawn cwd comes from the recorded pairing — re-realpath and re-verify so
|
|
@@ -541,9 +545,11 @@ export class CodexProvider extends Service {
|
|
|
541
545
|
// rollout). No process is spawned here — lazy-spawn gate, rules §14-i.
|
|
542
546
|
const client = CodexAppServerClient.create({
|
|
543
547
|
cwd: spawnCwd,
|
|
544
|
-
resumeThreadId: record.threadId,
|
|
548
|
+
...(record.threadId === null ? {} : { resumeThreadId: record.threadId }),
|
|
545
549
|
approvalPolicy,
|
|
546
550
|
sandboxMode,
|
|
551
|
+
worldHome: this.home,
|
|
552
|
+
hub: this.hubFor(id),
|
|
547
553
|
});
|
|
548
554
|
let handle;
|
|
549
555
|
let stored;
|
package/agent-codex/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgmi-builds/agent-codex",
|
|
3
3
|
"version": "0.1.0",
|
|
4
|
-
"description": "Codex provider for DeepSeek Harness (dsh) — an AgentFactory that drives a `codex app-server` child process (stdio JSONL JSON-RPC) per session and bridges its thread/turn surface, approvals and permission policy into Dash Agent/Session contracts.",
|
|
4
|
+
"description": "Codex provider for DeepSeek Harness (dsh) — an AgentFactory that drives a `codex app-server` child process (stdio JSONL JSON-RPC) per session and bridges its thread/turn surface, approvals and permission policy into Dash Agent/Session contracts. Declares NO codex dependency and ships NO postinstall (2026-10-06 ruling): the operator's own CODEX_BIN / PATH CLI / desktop core is discovered at first prompt, and only when all three miss is the pinned `@openai/codex` package fetched at RUNTIME into the world home (src/codex-runtime-fetch.ts, `npm run fetch-codex-runtime`).",
|
|
5
|
+
|
|
5
6
|
"type": "module",
|
|
6
7
|
"exports": {
|
|
7
8
|
".": "./dist/index.js",
|
|
@@ -9,9 +10,11 @@
|
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"dist",
|
|
13
|
+
"scripts",
|
|
12
14
|
"cordis.patch.yml",
|
|
13
15
|
"README.md"
|
|
14
16
|
],
|
|
17
|
+
|
|
15
18
|
"dsh": {
|
|
16
19
|
"bundle": {
|
|
17
20
|
"patch": "./cordis.patch.yml"
|
|
@@ -19,12 +22,14 @@
|
|
|
19
22
|
},
|
|
20
23
|
"scripts": {
|
|
21
24
|
"build": "tsc -p tsconfig.json",
|
|
22
|
-
"test": "node --test test/*.test.mjs"
|
|
25
|
+
"test": "node --test test/*.test.mjs",
|
|
26
|
+
"fetch-codex-runtime": "node scripts/fetch-codex-runtime.mjs"
|
|
23
27
|
},
|
|
28
|
+
|
|
24
29
|
"dependencies": {
|
|
25
|
-
"@openai/codex": "0.160.0",
|
|
26
30
|
"zod": "4.4.3"
|
|
27
31
|
},
|
|
32
|
+
|
|
28
33
|
"peerDependencies": {
|
|
29
34
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
30
35
|
"@deepseek-ai/dsh-agent": ">=0.1.5-0 <0.3.0-0",
|
|
@@ -110,7 +110,7 @@ const LOCK_STALE_MS = 30 * 60 * 1000;
|
|
|
110
110
|
export function sdkInstallArgv(packageVersion = SDK_PACKAGE_VERSION) {
|
|
111
111
|
return [
|
|
112
112
|
"install",
|
|
113
|
-
"--exact",
|
|
113
|
+
"--save-exact",
|
|
114
114
|
"--ignore-scripts",
|
|
115
115
|
"--omit=dev",
|
|
116
116
|
`${SDK_PACKAGE}@${packageVersion}`,
|
|
@@ -60,10 +60,12 @@
|
|
|
60
60
|
* as are `HERMES_CWD`/`HERMES_TUI_GATEWAY_URL`/`HERMES_TUI_SIDECAR_URL`
|
|
61
61
|
* (keep the spawned stdio transport the only transport).
|
|
62
62
|
*/
|
|
63
|
-
import { spawn } from "node:child_process";
|
|
63
|
+
import { spawn, execFile } from "node:child_process";
|
|
64
64
|
import { existsSync, readFileSync } from "node:fs";
|
|
65
65
|
import { homedir } from "node:os";
|
|
66
|
-
import { delimiter, resolve } from "node:path";
|
|
66
|
+
import { delimiter, resolve, join } from "node:path";
|
|
67
|
+
import { promisify } from "node:util";
|
|
68
|
+
import { prepareHermesNativePlugin } from "@pgmi-builds/superd-hub/foreign-native";
|
|
67
69
|
import { createInterface } from "node:readline";
|
|
68
70
|
import { projectGatewayEvent } from "./hermes-events.js";
|
|
69
71
|
const READY_TIMEOUT_MS = 15_000; // TUI parity (STARTUP_TIMEOUT_MS)
|
|
@@ -217,13 +219,19 @@ export class HermesGatewayClient {
|
|
|
217
219
|
* materialized within the grace. Agent-held clients never arm it (ruling 4).
|
|
218
220
|
*/
|
|
219
221
|
#spawnGraceTimer = null;
|
|
222
|
+
#hubPreparation;
|
|
223
|
+
#hubSession;
|
|
220
224
|
constructor(opts) {
|
|
221
225
|
this.#requestTimeoutMs = opts.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
222
226
|
this.#approvalTimeoutMs = opts.approvalTimeoutMs ?? APPROVAL_TIMEOUT_MS;
|
|
223
227
|
this.#lazy = opts.lazy === true;
|
|
224
228
|
this.#spawnOpts = opts;
|
|
225
|
-
if (!this.#lazy)
|
|
226
|
-
|
|
229
|
+
if (!this.#lazy) {
|
|
230
|
+
if (opts.hub === undefined)
|
|
231
|
+
this.#materializeChild();
|
|
232
|
+
else
|
|
233
|
+
void this.ensureStarted().catch(() => { });
|
|
234
|
+
}
|
|
227
235
|
}
|
|
228
236
|
/** Captured spawn options for deferred materialization (lazy clients). */
|
|
229
237
|
#spawnOpts;
|
|
@@ -237,6 +245,10 @@ export class HermesGatewayClient {
|
|
|
237
245
|
const root = resolveHermesRoot();
|
|
238
246
|
const python = resolveHermesPython(root);
|
|
239
247
|
const env = { ...(opts.env ?? process.env) };
|
|
248
|
+
if (this.#hubSession !== undefined)
|
|
249
|
+
env.SUPERD_HUB_SESSION = this.#hubSession;
|
|
250
|
+
else
|
|
251
|
+
delete env.SUPERD_HUB_SESSION;
|
|
240
252
|
// Native `.env` fill (dotenv convention: existing env WINS). The omp and
|
|
241
253
|
// pi runtimes load their own native `.env`; the hermes gateway child does
|
|
242
254
|
// not, and the harness env carries no provider keys — without this fill
|
|
@@ -399,6 +411,23 @@ export class HermesGatewayClient {
|
|
|
399
411
|
* A lazy client materializes its python child HERE (full-lazy ruling:
|
|
400
412
|
* view-time resume spawns nothing). */
|
|
401
413
|
ensureStarted() {
|
|
414
|
+
const hub = this.#spawnOpts.hub;
|
|
415
|
+
if (hub !== undefined) {
|
|
416
|
+
if (this.#hubPreparation === undefined) {
|
|
417
|
+
this.#hubPreparation = (async () => {
|
|
418
|
+
if (this.#closed)
|
|
419
|
+
throw new Error('hermes gateway client closed');
|
|
420
|
+
const prepare = this.#spawnOpts.prepareHub ?? prepareHermesHub;
|
|
421
|
+
this.#hubSession = await prepare(hub);
|
|
422
|
+
if (this.#closed)
|
|
423
|
+
throw new Error('hermes gateway client closed');
|
|
424
|
+
})().catch(error => { this.#fail(error instanceof Error ? error : new Error(String(error))); throw error; });
|
|
425
|
+
}
|
|
426
|
+
return this.#hubPreparation.then(() => this.#startPrepared());
|
|
427
|
+
}
|
|
428
|
+
return this.#startPrepared();
|
|
429
|
+
}
|
|
430
|
+
#startPrepared() {
|
|
402
431
|
if (this.#readyPromise === null) {
|
|
403
432
|
if (this.#closed || this.#failure !== null) {
|
|
404
433
|
return Promise.reject(this.#failure ?? new Error("hermes gateway client closed"));
|
|
@@ -528,6 +557,7 @@ export class HermesGatewayClient {
|
|
|
528
557
|
if (this.#closed)
|
|
529
558
|
return;
|
|
530
559
|
this.#closed = true;
|
|
560
|
+
this.#spawnOpts.hub?.dispose();
|
|
531
561
|
this.#clearSpawnGrace();
|
|
532
562
|
// Fire-and-forget best effort: write the request directly (the RPC path
|
|
533
563
|
// would refuse under #closed and nothing will read the response anyway).
|
|
@@ -933,7 +963,7 @@ export class HermesGatewayClient {
|
|
|
933
963
|
clearTimeout(this.#readyTimer);
|
|
934
964
|
this.#readyTimer = null;
|
|
935
965
|
}
|
|
936
|
-
if (!this.#readySettled) {
|
|
966
|
+
if (!this.#readySettled && this.#readyPromise !== null) {
|
|
937
967
|
this.#readySettled = true;
|
|
938
968
|
this.#readyReject(error);
|
|
939
969
|
}
|
|
@@ -999,3 +1029,12 @@ export class HermesGatewayClient {
|
|
|
999
1029
|
}
|
|
1000
1030
|
}
|
|
1001
1031
|
}
|
|
1032
|
+
async function prepareHermesHub(binding) {
|
|
1033
|
+
const root = resolveHermesRoot();
|
|
1034
|
+
const python = resolveHermesPython(root);
|
|
1035
|
+
return prepareHermesNativePlugin(binding, join(homedir(), '.hermes'), async (key) => {
|
|
1036
|
+
const env = { ...process.env, PYTHONPATH: root };
|
|
1037
|
+
delete env.HERMES_HOME;
|
|
1038
|
+
await promisify(execFile)(python, ['-m', 'hermes_cli.main', 'plugins', 'enable', key], { cwd: root, env, timeout: 30_000 });
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
@@ -26,6 +26,7 @@ import { join } from "node:path";
|
|
|
26
26
|
import { Service } from "@deepseek-ai/cordis";
|
|
27
27
|
import { installModelSelection } from "@deepseek-ai/dsh-agent";
|
|
28
28
|
import { SessionLogOffset, SessionPreparation, interruptedTurnClosers, } from "@deepseek-ai/dsh-session";
|
|
29
|
+
import { bindForeignHub } from "@pgmi-builds/superd-hub/foreign-hub";
|
|
29
30
|
import { HermesGatewayClient, HERMES_SPAWN_GRACE_MS } from "./hermes-client.js";
|
|
30
31
|
import { HermesAgent } from "./agent.js";
|
|
31
32
|
import { HermesLlmAdapter, HERMES_PROVIDER_ID } from "./adapter.js";
|
|
@@ -296,6 +297,9 @@ export class HermesProvider extends Service {
|
|
|
296
297
|
this.runtime.ctx.logger.warn(`hermes-provider: model route swap failed: ${String(error)}`);
|
|
297
298
|
});
|
|
298
299
|
}
|
|
300
|
+
hubFor(sessionId) {
|
|
301
|
+
return bindForeignHub('hermes', sessionId, this.home, () => this.runtime.ctx.agents.get(sessionId));
|
|
302
|
+
}
|
|
299
303
|
async createAgent(ownerCtx, options) {
|
|
300
304
|
const loopCtx = this.runtime.ctx;
|
|
301
305
|
const id = options.sessionId;
|
|
@@ -316,7 +320,7 @@ export class HermesProvider extends Service {
|
|
|
316
320
|
// Agent-held client: the spawn-grace reaper is NEVER armed here (2026-09-17
|
|
317
321
|
// ruling 4 — the session may legitimately materialize far beyond any
|
|
318
322
|
// grace; disposal is the agent lifecycle's job, not a timer's).
|
|
319
|
-
const client = HermesGatewayClient.spawn({ cwd: spawnCwd, lazy: true }); // full-lazy ruling 2026-09-18: view/create-time spawns NOTHING;
|
|
323
|
+
const client = HermesGatewayClient.spawn({ cwd: spawnCwd, lazy: true, hub: this.hubFor(id) }); // full-lazy ruling 2026-09-18: view/create-time spawns NOTHING;
|
|
320
324
|
const preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
|
|
321
325
|
...(options.seed === undefined ? {} : { seed: options.seed }),
|
|
322
326
|
...(meta === undefined ? {} : { meta }),
|
|
@@ -360,7 +364,9 @@ export class HermesProvider extends Service {
|
|
|
360
364
|
if (record === undefined) {
|
|
361
365
|
throw new Error(`cannot resume session "${id}": no Hermes gateway session is recorded for this Dash session id`);
|
|
362
366
|
}
|
|
363
|
-
|
|
367
|
+
const persistence = this.requirePersistence("resume");
|
|
368
|
+
if ((record.gatewayStoredSessionId === null || record.gatewayStoredSessionId === undefined)
|
|
369
|
+
&& (await readStoredEvents(persistence, id, options.signal)).some(event => event.type === "turn/start" || event.type === "user/message" || event.type === "assistant/message")) {
|
|
364
370
|
throw new Error(`cannot resume session "${id}": no durable gateway session id is recorded (this record predates durable-id capture; re-create the session)`);
|
|
365
371
|
}
|
|
366
372
|
// Spawn cwd comes from the recorded pairing — re-realpath and re-verify.
|
|
@@ -368,7 +374,6 @@ export class HermesProvider extends Service {
|
|
|
368
374
|
if (spawnCwd === undefined) {
|
|
369
375
|
throw new Error(`cannot resume session "${id}": its recorded working directory no longer exists`);
|
|
370
376
|
}
|
|
371
|
-
const persistence = this.requirePersistence("resume");
|
|
372
377
|
// Preset (record-only): env override, else the recorded preset, else the
|
|
373
378
|
// persisted session log, else the permission service's default.
|
|
374
379
|
const envMode = envApprovalMode();
|
|
@@ -380,7 +385,7 @@ export class HermesProvider extends Service {
|
|
|
380
385
|
trace(`resume id=${id} spawnCwd=${spawnCwd} preset=${preset ?? "none"}`);
|
|
381
386
|
// Spawn the gateway child (no session bound); the FIRST prompt re-attaches
|
|
382
387
|
// via resumeSession(record.gatewayStoredSessionId) — the DURABLE key.
|
|
383
|
-
const client = HermesGatewayClient.spawn({ cwd: spawnCwd, lazy: true }); // full-lazy ruling 2026-09-18: view/create-time spawns NOTHING;
|
|
388
|
+
const client = HermesGatewayClient.spawn({ cwd: spawnCwd, lazy: true, hub: this.hubFor(id) }); // full-lazy ruling 2026-09-18: view/create-time spawns NOTHING;
|
|
384
389
|
let handle;
|
|
385
390
|
let stored;
|
|
386
391
|
let preparation;
|
|
@@ -403,7 +408,7 @@ export class HermesProvider extends Service {
|
|
|
403
408
|
this.followGatewaySessionId(id, client);
|
|
404
409
|
handedOff = true;
|
|
405
410
|
const agentOptions = await this.sessionAgentOptions(options.agentOptions);
|
|
406
|
-
return await setupAndPublish(loopCtx, ownerCtx, id, agentOptions, options.setup, preparation.session, client, options.parentAgent, "resume", { enterSession: true, preparation }, stored, this.agentRuntime(client, spawnCwd, record.gatewayStoredSessionId));
|
|
411
|
+
return await setupAndPublish(loopCtx, ownerCtx, id, agentOptions, options.setup, preparation.session, client, options.parentAgent, "resume", { enterSession: true, preparation }, stored, this.agentRuntime(client, spawnCwd, record.gatewayStoredSessionId ?? undefined));
|
|
407
412
|
}
|
|
408
413
|
catch (error) {
|
|
409
414
|
trace(`resume ${id} THREW: ${String(error)} | stack=${error instanceof Error ? error.stack?.slice(0, 400) : "n/a"}`);
|