@rynx-ai/runtime 0.1.11-beta.4 → 0.1.11-beta.41
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/dist/claude/executor.d.ts +19 -5
- package/dist/claude/executor.js +56 -12
- package/dist/claude/models.d.ts +0 -5
- package/dist/claude/models.js +1 -7
- package/dist/claude/native-bridge.d.ts +103 -1
- package/dist/claude/native-bridge.js +445 -30
- package/dist/claude/native-hook-main.js +81 -1
- package/dist/claude/native-hooks.js +7 -0
- package/dist/claude/native-integration.d.ts +178 -26
- package/dist/claude/native-integration.js +1528 -170
- package/dist/claude/session-status.d.ts +39 -0
- package/dist/claude/session-status.js +163 -0
- package/dist/claude/transcript-clone.d.ts +18 -0
- package/dist/claude/transcript-clone.js +497 -0
- package/dist/claude/transcript.d.ts +27 -4
- package/dist/claude/transcript.js +158 -47
- package/dist/codex-app-server/client.d.ts +10 -6
- package/dist/codex-app-server/client.js +67 -15
- package/dist/codex-app-server/forwarder.d.ts +92 -3
- package/dist/codex-app-server/forwarder.js +532 -57
- package/dist/codex-app-server/mapping.d.ts +3 -6
- package/dist/codex-app-server/mapping.js +206 -36
- package/dist/codex-app-server/mcp-startup.d.ts +13 -0
- package/dist/codex-app-server/mcp-startup.js +63 -0
- package/dist/codex-app-server/process-registry.d.ts +36 -0
- package/dist/codex-app-server/process-registry.js +320 -0
- package/dist/codex-app-server/protocol.d.ts +64 -7
- package/dist/codex-app-server/ws-channel.d.ts +7 -0
- package/dist/codex-app-server/ws-channel.js +104 -28
- package/dist/codex-home.d.ts +35 -3
- package/dist/codex-home.js +323 -18
- package/dist/codex-session-store.d.ts +23 -0
- package/dist/codex-session-store.js +21 -0
- package/dist/host.d.ts +103 -46
- package/dist/host.js +1988 -634
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/input-resources.d.ts +4 -0
- package/dist/input-resources.js +21 -5
- package/dist/models-catalog.d.ts +2 -1
- package/dist/models-catalog.js +94 -6
- package/dist/runner/child.d.ts +97 -28
- package/dist/runner/child.js +1486 -100
- package/dist/runner/manager.d.ts +110 -29
- package/dist/runner/manager.js +1481 -246
- package/dist/runner/protocol.d.ts +212 -24
- package/dist/runner/protocol.js +5 -0
- package/dist/runner/startup-policy.d.ts +7 -0
- package/dist/runner/startup-policy.js +10 -0
- package/dist/runner/transport.d.ts +18 -2
- package/dist/runner/transport.js +82 -3
- package/dist/runner-main.js +8 -3
- package/dist/terminal/claude-tui.d.ts +3 -1
- package/dist/terminal/claude-tui.js +3 -1
- package/dist/terminal/codex-tui.d.ts +4 -0
- package/dist/terminal/codex-tui.js +5 -0
- package/dist/terminal/control-parser.d.ts +39 -0
- package/dist/terminal/control-parser.js +172 -0
- package/dist/terminal/registry.d.ts +18 -15
- package/dist/terminal/registry.js +44 -23
- package/dist/terminal/spool.d.ts +47 -0
- package/dist/terminal/spool.js +231 -0
- package/dist/terminal/tmux.d.ts +126 -74
- package/dist/terminal/tmux.js +807 -211
- package/package.json +4 -4
|
@@ -16,6 +16,7 @@ import { spawn } from "node:child_process";
|
|
|
16
16
|
import { createServer } from "node:net";
|
|
17
17
|
import { WebSocket } from "ws";
|
|
18
18
|
import { createCodexChildEnv } from "../codex-child-env.js";
|
|
19
|
+
import { newRuntimeProcessTag, reconcileRuntimeProcesses, registerRuntimeProcess, runtimeProcessArgv0, runtimeProcessOwnerIdentity, unregisterRuntimeProcess, withRuntimeProcessStateArg, } from "./process-registry.js";
|
|
19
20
|
/** Reserve a free loopback TCP port (best-effort; racy but fine for local use). */
|
|
20
21
|
async function freeLoopbackPort() {
|
|
21
22
|
return await new Promise((resolve, reject) => {
|
|
@@ -28,6 +29,7 @@ async function freeLoopbackPort() {
|
|
|
28
29
|
});
|
|
29
30
|
});
|
|
30
31
|
}
|
|
32
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
|
|
31
33
|
export class WsRpcChannel {
|
|
32
34
|
opts;
|
|
33
35
|
child = null;
|
|
@@ -35,12 +37,14 @@ export class WsRpcChannel {
|
|
|
35
37
|
lineCb = null;
|
|
36
38
|
closeCb = null;
|
|
37
39
|
closedEmitted = false;
|
|
40
|
+
processTag = null;
|
|
41
|
+
stopTask = null;
|
|
38
42
|
readyTimeoutMs;
|
|
39
43
|
/** The `ws://IP:PORT` the app-server listens on — pass to the TUI's `--remote`. */
|
|
40
44
|
url = "";
|
|
41
45
|
constructor(opts) {
|
|
42
46
|
this.opts = opts;
|
|
43
|
-
this.readyTimeoutMs = opts.readyTimeoutMs ??
|
|
47
|
+
this.readyTimeoutMs = opts.readyTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
44
48
|
}
|
|
45
49
|
onLine(cb) {
|
|
46
50
|
this.lineCb = cb;
|
|
@@ -52,16 +56,48 @@ export class WsRpcChannel {
|
|
|
52
56
|
return this.ws?.readyState === WebSocket.OPEN;
|
|
53
57
|
}
|
|
54
58
|
async start() {
|
|
59
|
+
this.stopTask = null;
|
|
60
|
+
this.closedEmitted = false;
|
|
61
|
+
if (this.opts.stateDir) {
|
|
62
|
+
reconcileRuntimeProcesses();
|
|
63
|
+
}
|
|
55
64
|
const port = await freeLoopbackPort();
|
|
56
65
|
this.url = `ws://127.0.0.1:${port}`;
|
|
57
|
-
const
|
|
66
|
+
const baseArgs = this.opts.stateDir
|
|
67
|
+
? withRuntimeProcessStateArg(this.opts.baseArgs, this.opts.stateDir)
|
|
68
|
+
: this.opts.baseArgs;
|
|
69
|
+
const args = [...baseArgs, "--listen", this.url];
|
|
70
|
+
const processTag = this.opts.stateDir ? newRuntimeProcessTag() : null;
|
|
71
|
+
this.processTag = processTag;
|
|
58
72
|
this.child = spawn(this.opts.cliPath, args, {
|
|
59
73
|
stdio: ["ignore", "ignore", "pipe"],
|
|
60
74
|
env: { ...createCodexChildEnv(process.env), ...(this.opts.extraEnv ?? {}) },
|
|
75
|
+
detached: process.platform !== "win32",
|
|
76
|
+
...(processTag ? { argv0: runtimeProcessArgv0(this.opts.cliPath, processTag) } : {}),
|
|
77
|
+
});
|
|
78
|
+
const childPid = this.child.pid;
|
|
79
|
+
if (processTag && this.opts.stateDir && childPid) {
|
|
80
|
+
registerRuntimeProcess({
|
|
81
|
+
pid: childPid,
|
|
82
|
+
pgid: childPid,
|
|
83
|
+
ownerPid: process.pid,
|
|
84
|
+
ownerIdentity: runtimeProcessOwnerIdentity(process.pid),
|
|
85
|
+
sessionTag: processTag,
|
|
86
|
+
stateDir: this.opts.stateDir,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
this.child.on("exit", (code, signal) => {
|
|
90
|
+
this.unregisterProcess();
|
|
91
|
+
this.emitClose(code, signal, null);
|
|
61
92
|
});
|
|
62
|
-
this.child.on("exit", (code, signal) => this.emitClose(code, signal, null));
|
|
63
93
|
this.child.on("error", (error) => this.emitClose(null, null, error));
|
|
64
|
-
|
|
94
|
+
try {
|
|
95
|
+
this.ws = await this.connectWithRetry(this.url);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
await this.stop();
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
65
101
|
this.ws.on("message", (data) => {
|
|
66
102
|
// One JSON-RPC object per frame (codex ws framing).
|
|
67
103
|
this.lineCb?.(data.toString());
|
|
@@ -85,6 +121,12 @@ export class WsRpcChannel {
|
|
|
85
121
|
});
|
|
86
122
|
}
|
|
87
123
|
async stop(signal = "SIGTERM") {
|
|
124
|
+
if (this.stopTask)
|
|
125
|
+
return await this.stopTask;
|
|
126
|
+
this.stopTask = this.stopOwnedProcess(signal);
|
|
127
|
+
return await this.stopTask;
|
|
128
|
+
}
|
|
129
|
+
async stopOwnedProcess(signal) {
|
|
88
130
|
try {
|
|
89
131
|
this.ws?.close();
|
|
90
132
|
}
|
|
@@ -92,14 +134,33 @@ export class WsRpcChannel {
|
|
|
92
134
|
/* already closing */
|
|
93
135
|
}
|
|
94
136
|
const child = this.child;
|
|
95
|
-
if (child) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
/* already gone */
|
|
137
|
+
if (child && child.exitCode === null && child.signalCode === null) {
|
|
138
|
+
this.signalChild(child, signal);
|
|
139
|
+
if (!await waitForExit(child, 5_000)) {
|
|
140
|
+
this.signalChild(child, "SIGKILL");
|
|
141
|
+
await waitForExit(child, 2_000);
|
|
101
142
|
}
|
|
102
143
|
}
|
|
144
|
+
this.unregisterProcess();
|
|
145
|
+
this.child = null;
|
|
146
|
+
this.ws = null;
|
|
147
|
+
}
|
|
148
|
+
signalChild(child, signal) {
|
|
149
|
+
try {
|
|
150
|
+
if (process.platform !== "win32" && child.pid)
|
|
151
|
+
process.kill(-child.pid, signal);
|
|
152
|
+
else
|
|
153
|
+
child.kill(signal);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Already gone.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
unregisterProcess() {
|
|
160
|
+
const tag = this.processTag;
|
|
161
|
+
this.processTag = null;
|
|
162
|
+
if (tag)
|
|
163
|
+
unregisterRuntimeProcess(tag);
|
|
103
164
|
}
|
|
104
165
|
emitClose(code, signal, error) {
|
|
105
166
|
if (this.closedEmitted)
|
|
@@ -114,7 +175,7 @@ export class WsRpcChannel {
|
|
|
114
175
|
// eslint-disable-next-line no-constant-condition
|
|
115
176
|
while (Date.now() < deadline) {
|
|
116
177
|
try {
|
|
117
|
-
return await this.tryConnect(url);
|
|
178
|
+
return await this.tryConnect(url, Math.max(1, deadline - Date.now()));
|
|
118
179
|
}
|
|
119
180
|
catch (error) {
|
|
120
181
|
lastError = error;
|
|
@@ -123,9 +184,9 @@ export class WsRpcChannel {
|
|
|
123
184
|
}
|
|
124
185
|
throw new Error(`codex app-server ws did not become ready at ${url}: ${lastError?.message ?? "timeout"}`);
|
|
125
186
|
}
|
|
126
|
-
tryConnect(url) {
|
|
187
|
+
tryConnect(url, handshakeTimeout) {
|
|
127
188
|
return new Promise((resolve, reject) => {
|
|
128
|
-
const ws = new WebSocket(url);
|
|
189
|
+
const ws = new WebSocket(url, { handshakeTimeout });
|
|
129
190
|
const onOpen = () => {
|
|
130
191
|
ws.off("error", onError);
|
|
131
192
|
resolve(ws);
|
|
@@ -145,6 +206,22 @@ export class WsRpcChannel {
|
|
|
145
206
|
});
|
|
146
207
|
}
|
|
147
208
|
}
|
|
209
|
+
function waitForExit(child, timeoutMs) {
|
|
210
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
211
|
+
return Promise.resolve(true);
|
|
212
|
+
return new Promise((resolve) => {
|
|
213
|
+
const timer = setTimeout(() => {
|
|
214
|
+
child.off("exit", onExit);
|
|
215
|
+
resolve(false);
|
|
216
|
+
}, timeoutMs);
|
|
217
|
+
timer.unref?.();
|
|
218
|
+
const onExit = () => {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
resolve(true);
|
|
221
|
+
};
|
|
222
|
+
child.once("exit", onExit);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
148
225
|
/**
|
|
149
226
|
* Connect-only {@link RpcChannel}: attaches an ADDITIONAL client to an app-server
|
|
150
227
|
* someone else already started (a {@link WsRpcChannel}'s `url`) — no spawn. This
|
|
@@ -166,7 +243,7 @@ export class ExternalWsChannel {
|
|
|
166
243
|
/** The `ws://IP:PORT` of the already-running app-server to attach to. */
|
|
167
244
|
url, opts = {}) {
|
|
168
245
|
this.url = url;
|
|
169
|
-
this.readyTimeoutMs = opts.readyTimeoutMs ??
|
|
246
|
+
this.readyTimeoutMs = opts.readyTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
170
247
|
}
|
|
171
248
|
onLine(cb) {
|
|
172
249
|
this.lineCb = cb;
|
|
@@ -178,20 +255,16 @@ export class ExternalWsChannel {
|
|
|
178
255
|
return this.ws?.readyState === WebSocket.OPEN;
|
|
179
256
|
}
|
|
180
257
|
async start() {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
catch (error) {
|
|
189
|
-
lastError = error;
|
|
190
|
-
await new Promise((r) => setTimeout(r, 150));
|
|
191
|
-
}
|
|
258
|
+
// A channel instance is reusable after an observer disconnect. Do not let
|
|
259
|
+
// the prior closed socket make a failed reconnect look successful, and arm
|
|
260
|
+
// close delivery for the newly connected socket.
|
|
261
|
+
this.ws = null;
|
|
262
|
+
this.closedEmitted = false;
|
|
263
|
+
try {
|
|
264
|
+
this.ws = await this.connect(this.url);
|
|
192
265
|
}
|
|
193
|
-
|
|
194
|
-
throw new Error(`could not attach to app-server ws ${this.url}: ${
|
|
266
|
+
catch (error) {
|
|
267
|
+
throw new Error(`could not attach to app-server ws ${this.url}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
195
268
|
}
|
|
196
269
|
this.ws.on("message", (data) => this.lineCb?.(data.toString()));
|
|
197
270
|
this.ws.on("close", (code) => this.emitClose(code ?? null, null, null));
|
|
@@ -227,7 +300,10 @@ export class ExternalWsChannel {
|
|
|
227
300
|
}
|
|
228
301
|
connect(url) {
|
|
229
302
|
return new Promise((resolve, reject) => {
|
|
230
|
-
|
|
303
|
+
// The app-server owner has already completed its readiness probe. Use one
|
|
304
|
+
// bounded attach, then reuse this exact connection as the forwarder
|
|
305
|
+
// instead of running a second startup retry stage.
|
|
306
|
+
const ws = new WebSocket(url, { handshakeTimeout: this.readyTimeoutMs });
|
|
231
307
|
const onOpen = () => {
|
|
232
308
|
ws.off("error", onError);
|
|
233
309
|
resolve(ws);
|
package/dist/codex-home.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
import { type AgentRuntimeId } from "@rynx-ai/core";
|
|
1
|
+
import { type AgentRuntimeId, type ResolvedRetryPolicy } from "@rynx-ai/core";
|
|
2
2
|
export type CodexLineageRuntime = Exclude<AgentRuntimeId, "claude">;
|
|
3
|
+
export type CodexAuthMode = "chatgpt" | "apikey" | "pat";
|
|
4
|
+
export type CodexConfigAuth = "provider-ready" | "provider-auth-missing" | "codex-login";
|
|
5
|
+
/** Read the credential mode from Codex's optional-field auth.json shape. */
|
|
6
|
+
export declare function codexAuthEffectiveMode(authPath: string): CodexAuthMode | null;
|
|
7
|
+
/** Mirror Codex's provider-specific auth decision without spawning the CLI. */
|
|
8
|
+
export declare function codexConfigEffectiveAuth(configPath: string, env?: NodeJS.ProcessEnv): CodexConfigAuth;
|
|
9
|
+
/** True only when this exact launch would defer to an absent Codex login. */
|
|
10
|
+
export declare function codexLaunchRequiresLogin(codexHome: string, env?: NodeJS.ProcessEnv): boolean;
|
|
11
|
+
/** Resolve the user's auth/config source without nesting a parent Session's
|
|
12
|
+
* private CODEX_HOME into a child Session. */
|
|
13
|
+
export declare function resolveCodexHomeConfigSource(candidate: string, defaultHome?: string): string;
|
|
3
14
|
/**
|
|
4
15
|
* The deterministic private CODEX_HOME path for a rynx session. PER-SESSION,
|
|
5
16
|
* under the daemon-owned RYNX_HOME runtime tree,
|
|
@@ -22,8 +33,8 @@ export declare function legacyCodexHomePath(): string;
|
|
|
22
33
|
* `_CODEX_HOME_COPY_FILES`.
|
|
23
34
|
*
|
|
24
35
|
* PER-SESSION: one private home per rynx session (matching reference implementation), so concurrent
|
|
25
|
-
* agents never share a `skills/` dir. Idempotent:
|
|
26
|
-
*
|
|
36
|
+
* agents never share a `skills/` dir. Idempotent: existing Session-owned entries stay
|
|
37
|
+
* unchanged; live symlinks still observe source credential updates. Returns the private home dir.
|
|
27
38
|
*/
|
|
28
39
|
export declare function prepareCodexHome(sessionId: string, realHome?: string): string;
|
|
29
40
|
/**
|
|
@@ -32,6 +43,27 @@ export declare function prepareCodexHome(sessionId: string, realHome?: string):
|
|
|
32
43
|
* real home and cannot wedge a managed app-server/TUI pair.
|
|
33
44
|
*/
|
|
34
45
|
export declare function prepareRuntimeHome(sessionId: string, runtime: CodexLineageRuntime, realHome?: string): string;
|
|
46
|
+
/** Trust a runner-selected workspace only in the Session's private config so
|
|
47
|
+
* Codex cannot stop a detached TUI at its project-trust prompt. */
|
|
48
|
+
export declare function trustCodexProject(codexHome: string, cwd: string): void;
|
|
49
|
+
/** Apply the Session retry budget through Codex's supported provider settings.
|
|
50
|
+
* A config with no provider definitions is left untouched. */
|
|
51
|
+
export declare function applyCodexProviderRetryPolicy(codexHome: string, retry?: ResolvedRetryPolicy | null): void;
|
|
52
|
+
/** Preserve the user's private-home base instructions and append only the raw
|
|
53
|
+
* Agent-authored text. Repeated launches replace the managed suffix instead of
|
|
54
|
+
* accumulating it. Invalid user TOML is intentionally left untouched. */
|
|
55
|
+
export declare function syncCodexDeveloperInstructions(codexHome: string, instructions?: string | null): void;
|
|
56
|
+
export type CodexDeveloperInstructionsState = {
|
|
57
|
+
state: "present";
|
|
58
|
+
value: string;
|
|
59
|
+
} | {
|
|
60
|
+
state: "absent";
|
|
61
|
+
} | {
|
|
62
|
+
state: "unreadable";
|
|
63
|
+
};
|
|
64
|
+
/** Read the active private-home instructions without conflating a definite
|
|
65
|
+
* absence with malformed or temporarily unreadable config. */
|
|
66
|
+
export declare function readCodexDeveloperInstructionsState(codexHome: string): CodexDeveloperInstructionsState;
|
|
35
67
|
/** Persist immutable skill copies in a native Provider home. Fork descendants
|
|
36
68
|
* share that home, so entries must outlive every runner's temporary sources. */
|
|
37
69
|
export declare function populateCodexSkills(codexHome: string, skills: {
|
package/dist/codex-home.js
CHANGED
|
@@ -1,26 +1,174 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { copyFileSync, cpSync, existsSync, mkdirSync, renameSync, rmSync, symlinkSync } from "node:fs";
|
|
2
|
+
import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import {
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { parse, stringify } from "smol-toml";
|
|
6
|
+
import { assertSkillPathComponent, getRuntimeProfile, resolveRuntimeHome, } from "@rynx-ai/core";
|
|
6
7
|
import { adoptLegacyRuntimeDirectory, legacyRuntimeStateRoot, runtimeSessionDigest, runtimeSessionStateDir, } from "./runtime-state-paths.js";
|
|
7
|
-
/**
|
|
8
|
-
const
|
|
8
|
+
/** Codex-owned files that must stay live across every private home. */
|
|
9
|
+
const CODEX_SYMLINK_FILES = [
|
|
10
|
+
"auth.json",
|
|
11
|
+
".credentials.json",
|
|
12
|
+
"memories_1.sqlite",
|
|
13
|
+
"AGENTS.md",
|
|
14
|
+
"AGENTS.override.md",
|
|
15
|
+
"hooks.json",
|
|
16
|
+
];
|
|
17
|
+
/** Large/shared Codex state and cross-process coordination directories. */
|
|
18
|
+
const CODEX_SYMLINK_DIRS = [
|
|
19
|
+
join("plugins", "cache"),
|
|
20
|
+
"mcp-oauth-locks",
|
|
21
|
+
"memories",
|
|
22
|
+
"rules",
|
|
23
|
+
];
|
|
9
24
|
/** Inherit the user's settings by snapshot copy (not the mutable NUX/update state). */
|
|
10
25
|
const COPY_FILES = ["config.toml"];
|
|
11
26
|
const RUNTIME_HOME_FILES = {
|
|
12
|
-
codex: {
|
|
27
|
+
codex: {
|
|
28
|
+
symlink: CODEX_SYMLINK_FILES,
|
|
29
|
+
symlinkDirs: CODEX_SYMLINK_DIRS,
|
|
30
|
+
copy: COPY_FILES,
|
|
31
|
+
},
|
|
13
32
|
traex: {
|
|
14
33
|
// Traex keeps credentials below `cli/`, while its user configuration lives
|
|
15
34
|
// at the TRAE_HOME root. Keep both the current TOML name and the legacy YAML
|
|
16
35
|
// name so existing installations remain usable without an implicit migrate.
|
|
17
36
|
symlink: ["cli/auth.json"],
|
|
37
|
+
symlinkDirs: [],
|
|
18
38
|
copy: ["traecli.toml", "traecli.yaml"],
|
|
19
39
|
},
|
|
20
40
|
};
|
|
41
|
+
const CODEX_BUILTIN_PROVIDERS = new Set([
|
|
42
|
+
"openai",
|
|
43
|
+
"amazon-bedrock",
|
|
44
|
+
"amazon-bedrock-runtime",
|
|
45
|
+
"ollama",
|
|
46
|
+
"lmstudio",
|
|
47
|
+
]);
|
|
48
|
+
function isTomlTable(value) {
|
|
49
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
50
|
+
}
|
|
51
|
+
/** Resolve the effective native Provider, honoring the selected profile first. */
|
|
52
|
+
function effectiveCodexModelProvider(config) {
|
|
53
|
+
let provider = config.model_provider;
|
|
54
|
+
const activeProfile = config.profile;
|
|
55
|
+
if (typeof activeProfile === "string" && activeProfile.trim() && isTomlTable(config.profiles)) {
|
|
56
|
+
const profile = config.profiles[activeProfile];
|
|
57
|
+
if (isTomlTable(profile) && typeof profile.model_provider === "string") {
|
|
58
|
+
provider = profile.model_provider;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return typeof provider === "string" && provider.trim() ? provider : null;
|
|
62
|
+
}
|
|
63
|
+
/** Read the credential mode from Codex's optional-field auth.json shape. */
|
|
64
|
+
export function codexAuthEffectiveMode(authPath) {
|
|
65
|
+
let data;
|
|
66
|
+
try {
|
|
67
|
+
data = JSON.parse(readFileSync(authPath, "utf8"));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
73
|
+
return null;
|
|
74
|
+
const auth = data;
|
|
75
|
+
const tokens = auth.tokens;
|
|
76
|
+
if (tokens && typeof tokens === "object" && !Array.isArray(tokens)) {
|
|
77
|
+
const tokenRecord = tokens;
|
|
78
|
+
for (const field of ["access_token", "refresh_token"]) {
|
|
79
|
+
const value = tokenRecord[field];
|
|
80
|
+
if (typeof value === "string" && value.trim())
|
|
81
|
+
return "chatgpt";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (typeof auth.OPENAI_API_KEY === "string" && auth.OPENAI_API_KEY.trim())
|
|
85
|
+
return "apikey";
|
|
86
|
+
if (typeof auth.personal_access_token === "string" &&
|
|
87
|
+
auth.personal_access_token.trim())
|
|
88
|
+
return "pat";
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
/** Mirror Codex's provider-specific auth decision without spawning the CLI. */
|
|
92
|
+
export function codexConfigEffectiveAuth(configPath, env = process.env) {
|
|
93
|
+
let config;
|
|
94
|
+
try {
|
|
95
|
+
config = parse(readFileSync(configPath, "utf8"));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return "codex-login";
|
|
99
|
+
}
|
|
100
|
+
const providerId = effectiveCodexModelProvider(config);
|
|
101
|
+
if (!providerId)
|
|
102
|
+
return "codex-login";
|
|
103
|
+
if (CODEX_BUILTIN_PROVIDERS.has(providerId)) {
|
|
104
|
+
return providerId === "openai" ? "codex-login" : "provider-ready";
|
|
105
|
+
}
|
|
106
|
+
const provider = isTomlTable(config.model_providers)
|
|
107
|
+
? config.model_providers[providerId]
|
|
108
|
+
: undefined;
|
|
109
|
+
if (!isTomlTable(provider))
|
|
110
|
+
return "provider-auth-missing";
|
|
111
|
+
if (provider.requires_openai_auth === true)
|
|
112
|
+
return "codex-login";
|
|
113
|
+
const envKey = provider.env_key;
|
|
114
|
+
if (typeof envKey !== "string" || !envKey.trim())
|
|
115
|
+
return "provider-ready";
|
|
116
|
+
const value = env[envKey.trim()];
|
|
117
|
+
return typeof value === "string" && value.trim()
|
|
118
|
+
? "provider-ready"
|
|
119
|
+
: "provider-auth-missing";
|
|
120
|
+
}
|
|
121
|
+
/** True only when this exact launch would defer to an absent Codex login. */
|
|
122
|
+
export function codexLaunchRequiresLogin(codexHome, env = process.env) {
|
|
123
|
+
return codexConfigEffectiveAuth(join(codexHome, "config.toml"), env) === "codex-login" &&
|
|
124
|
+
codexAuthEffectiveMode(join(codexHome, "auth.json")) === null;
|
|
125
|
+
}
|
|
126
|
+
function pathExistsOrIsSymlink(path) {
|
|
127
|
+
try {
|
|
128
|
+
lstatSync(path);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function isPrivateCodexHome(candidate) {
|
|
136
|
+
const normalized = resolve(candidate);
|
|
137
|
+
if (basename(normalized) !== "codex-home")
|
|
138
|
+
return false;
|
|
139
|
+
const ownerDir = dirname(normalized);
|
|
140
|
+
const namespace = dirname(ownerDir);
|
|
141
|
+
const root = dirname(namespace);
|
|
142
|
+
return (basename(namespace) === "sessions" && basename(root) === "runtime") || basename(namespace) === "codex-native";
|
|
143
|
+
}
|
|
144
|
+
function privateCodexHomeSource(candidate) {
|
|
145
|
+
const sourceDirs = new Set();
|
|
146
|
+
for (const name of ["auth.json", ".credentials.json", "memories_1.sqlite"]) {
|
|
147
|
+
const file = join(candidate, name);
|
|
148
|
+
try {
|
|
149
|
+
if (lstatSync(file).isSymbolicLink())
|
|
150
|
+
sourceDirs.add(dirname(realpathSync(file)));
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Missing or unreadable links do not identify a source home.
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return sourceDirs.size === 1 ? [...sourceDirs][0] : null;
|
|
157
|
+
}
|
|
158
|
+
/** Resolve the user's auth/config source without nesting a parent Session's
|
|
159
|
+
* private CODEX_HOME into a child Session. */
|
|
160
|
+
export function resolveCodexHomeConfigSource(candidate, defaultHome = join(homedir(), ".codex")) {
|
|
161
|
+
const source = resolve(candidate);
|
|
162
|
+
const fallback = resolve(defaultHome);
|
|
163
|
+
if (source !== fallback && isPrivateCodexHome(source)) {
|
|
164
|
+
return privateCodexHomeSource(source) ?? fallback;
|
|
165
|
+
}
|
|
166
|
+
return source;
|
|
167
|
+
}
|
|
21
168
|
/** The user's real CODEX_HOME (env override, else `~/.codex`). */
|
|
22
169
|
function realCodexHome() {
|
|
23
|
-
|
|
170
|
+
const fallback = join(homedir(), ".codex");
|
|
171
|
+
return resolveCodexHomeConfigSource(process.env.CODEX_HOME?.trim() || fallback, fallback);
|
|
24
172
|
}
|
|
25
173
|
/**
|
|
26
174
|
* The deterministic private CODEX_HOME path for a rynx session. PER-SESSION,
|
|
@@ -52,8 +200,8 @@ export function legacyCodexHomePath() {
|
|
|
52
200
|
* `_CODEX_HOME_COPY_FILES`.
|
|
53
201
|
*
|
|
54
202
|
* PER-SESSION: one private home per rynx session (matching reference implementation), so concurrent
|
|
55
|
-
* agents never share a `skills/` dir. Idempotent:
|
|
56
|
-
*
|
|
203
|
+
* agents never share a `skills/` dir. Idempotent: existing Session-owned entries stay
|
|
204
|
+
* unchanged; live symlinks still observe source credential updates. Returns the private home dir.
|
|
57
205
|
*/
|
|
58
206
|
export function prepareCodexHome(sessionId, realHome = realCodexHome()) {
|
|
59
207
|
return prepareRuntimeHome(sessionId, "codex", realHome);
|
|
@@ -77,13 +225,9 @@ export function prepareRuntimeHome(sessionId, runtime, realHome = runtime === "c
|
|
|
77
225
|
for (const name of files.symlink) {
|
|
78
226
|
const src = join(realHome, name);
|
|
79
227
|
const dst = join(dir, name);
|
|
228
|
+
if (pathExistsOrIsSymlink(dst))
|
|
229
|
+
continue;
|
|
80
230
|
mkdirSync(dirname(dst), { recursive: true, mode: 0o700 });
|
|
81
|
-
try {
|
|
82
|
-
rmSync(dst, { recursive: true, force: true });
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
// absent — fine
|
|
86
|
-
}
|
|
87
231
|
if (existsSync(src)) {
|
|
88
232
|
try {
|
|
89
233
|
symlinkSync(src, dst);
|
|
@@ -93,11 +237,35 @@ export function prepareRuntimeHome(sessionId, runtime, realHome = runtime === "c
|
|
|
93
237
|
}
|
|
94
238
|
}
|
|
95
239
|
}
|
|
240
|
+
for (const name of files.symlinkDirs) {
|
|
241
|
+
const src = join(realHome, name);
|
|
242
|
+
const dst = join(dir, name);
|
|
243
|
+
let sourceIsDirectory = false;
|
|
244
|
+
try {
|
|
245
|
+
sourceIsDirectory = statSync(src).isDirectory();
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// Missing/unreadable shared state is optional.
|
|
249
|
+
}
|
|
250
|
+
if (!sourceIsDirectory)
|
|
251
|
+
continue;
|
|
252
|
+
if (pathExistsOrIsSymlink(dst))
|
|
253
|
+
continue;
|
|
254
|
+
mkdirSync(dirname(dst), { recursive: true, mode: 0o700 });
|
|
255
|
+
try {
|
|
256
|
+
symlinkSync(src, dst, "dir");
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// Best-effort: Codex recreates absent cache/state in its private home.
|
|
260
|
+
}
|
|
261
|
+
}
|
|
96
262
|
for (const name of files.copy) {
|
|
97
263
|
const src = join(realHome, name);
|
|
98
264
|
if (existsSync(src)) {
|
|
99
265
|
try {
|
|
100
266
|
const dst = join(dir, name);
|
|
267
|
+
if (pathExistsOrIsSymlink(dst))
|
|
268
|
+
continue;
|
|
101
269
|
mkdirSync(dirname(dst), { recursive: true, mode: 0o700 });
|
|
102
270
|
copyFileSync(src, dst);
|
|
103
271
|
}
|
|
@@ -108,14 +276,151 @@ export function prepareRuntimeHome(sessionId, runtime, realHome = runtime === "c
|
|
|
108
276
|
}
|
|
109
277
|
return dir;
|
|
110
278
|
}
|
|
279
|
+
/** Trust a runner-selected workspace only in the Session's private config so
|
|
280
|
+
* Codex cannot stop a detached TUI at its project-trust prompt. */
|
|
281
|
+
export function trustCodexProject(codexHome, cwd) {
|
|
282
|
+
const configPath = join(codexHome, "config.toml");
|
|
283
|
+
let config = {};
|
|
284
|
+
if (existsSync(configPath))
|
|
285
|
+
config = parse(readFileSync(configPath, "utf8"));
|
|
286
|
+
const projects = isTomlTable(config.projects) ? config.projects : {};
|
|
287
|
+
config.projects = projects;
|
|
288
|
+
let projectPath;
|
|
289
|
+
try {
|
|
290
|
+
projectPath = realpathSync(cwd);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
projectPath = resolve(cwd);
|
|
294
|
+
}
|
|
295
|
+
const existingProject = projects[projectPath];
|
|
296
|
+
const project = isTomlTable(existingProject) ? existingProject : {};
|
|
297
|
+
project.trust_level = "trusted";
|
|
298
|
+
projects[projectPath] = project;
|
|
299
|
+
writeCodexConfig(configPath, config);
|
|
300
|
+
}
|
|
301
|
+
const DEFAULT_CODEX_MAX_RETRIES = 7;
|
|
302
|
+
const DEFAULT_CODEX_REQUEST_TIMEOUT_SECONDS = 120;
|
|
303
|
+
const DEVELOPER_INSTRUCTIONS_BASE_FILE = ".rynx-developer-instructions-base";
|
|
304
|
+
/** Apply the Session retry budget through Codex's supported provider settings.
|
|
305
|
+
* A config with no provider definitions is left untouched. */
|
|
306
|
+
export function applyCodexProviderRetryPolicy(codexHome, retry) {
|
|
307
|
+
const configPath = join(codexHome, "config.toml");
|
|
308
|
+
if (!existsSync(configPath) || lstatSync(configPath).isSymbolicLink())
|
|
309
|
+
return;
|
|
310
|
+
const config = parse(readFileSync(configPath, "utf8"));
|
|
311
|
+
if (!isTomlTable(config.model_providers))
|
|
312
|
+
return;
|
|
313
|
+
const maxRetries = retry?.maxRetries ?? DEFAULT_CODEX_MAX_RETRIES;
|
|
314
|
+
const timeoutSeconds = retry?.timeoutPerRequestSeconds ??
|
|
315
|
+
DEFAULT_CODEX_REQUEST_TIMEOUT_SECONDS;
|
|
316
|
+
let changed = false;
|
|
317
|
+
for (const provider of Object.values(config.model_providers)) {
|
|
318
|
+
if (!isTomlTable(provider))
|
|
319
|
+
continue;
|
|
320
|
+
provider.request_max_retries = maxRetries;
|
|
321
|
+
provider.stream_max_retries = maxRetries;
|
|
322
|
+
provider.stream_idle_timeout_ms = Math.trunc(timeoutSeconds * 1_000);
|
|
323
|
+
changed = true;
|
|
324
|
+
}
|
|
325
|
+
if (changed)
|
|
326
|
+
writeCodexConfig(configPath, config);
|
|
327
|
+
}
|
|
328
|
+
/** Preserve the user's private-home base instructions and append only the raw
|
|
329
|
+
* Agent-authored text. Repeated launches replace the managed suffix instead of
|
|
330
|
+
* accumulating it. Invalid user TOML is intentionally left untouched. */
|
|
331
|
+
export function syncCodexDeveloperInstructions(codexHome, instructions) {
|
|
332
|
+
mkdirSync(codexHome, { recursive: true, mode: 0o700 });
|
|
333
|
+
const configPath = join(codexHome, "config.toml");
|
|
334
|
+
if (pathExistsOrIsSymlink(configPath) && lstatSync(configPath).isSymbolicLink()) {
|
|
335
|
+
try {
|
|
336
|
+
const source = realpathSync(configPath);
|
|
337
|
+
const content = readFileSync(source, "utf8");
|
|
338
|
+
rmSync(configPath, { force: true });
|
|
339
|
+
writeFileSync(configPath, content, { encoding: "utf8", mode: 0o600 });
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
let config;
|
|
346
|
+
try {
|
|
347
|
+
config = existsSync(configPath) ? parse(readFileSync(configPath, "utf8")) : {};
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const current = config.developer_instructions;
|
|
353
|
+
if (current !== undefined && typeof current !== "string")
|
|
354
|
+
return;
|
|
355
|
+
const addition = instructions?.trim() ?? "";
|
|
356
|
+
const basePath = join(codexHome, DEVELOPER_INSTRUCTIONS_BASE_FILE);
|
|
357
|
+
let base;
|
|
358
|
+
if (existsSync(basePath)) {
|
|
359
|
+
base = readFileSync(basePath, "utf8");
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
base = typeof current === "string" ? current.trim() : "";
|
|
363
|
+
if (addition && base === addition) {
|
|
364
|
+
base = "";
|
|
365
|
+
}
|
|
366
|
+
else if (addition && base.endsWith(`\n\n${addition}`)) {
|
|
367
|
+
base = base.slice(0, -(`\n\n${addition}`).length).trimEnd();
|
|
368
|
+
}
|
|
369
|
+
writeFileSync(basePath, base, { encoding: "utf8", mode: 0o600 });
|
|
370
|
+
}
|
|
371
|
+
const active = base && addition ? `${base}\n\n${addition}` : base || addition;
|
|
372
|
+
if (active)
|
|
373
|
+
config.developer_instructions = active;
|
|
374
|
+
else
|
|
375
|
+
delete config.developer_instructions;
|
|
376
|
+
writeCodexConfig(configPath, config);
|
|
377
|
+
}
|
|
378
|
+
/** Read the active private-home instructions without conflating a definite
|
|
379
|
+
* absence with malformed or temporarily unreadable config. */
|
|
380
|
+
export function readCodexDeveloperInstructionsState(codexHome) {
|
|
381
|
+
const configPath = join(codexHome, "config.toml");
|
|
382
|
+
let source;
|
|
383
|
+
try {
|
|
384
|
+
source = readFileSync(configPath, "utf8");
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
const code = error.code;
|
|
388
|
+
return code === "ENOENT" || code === "ENOTDIR"
|
|
389
|
+
? { state: "absent" }
|
|
390
|
+
: { state: "unreadable" };
|
|
391
|
+
}
|
|
392
|
+
let config;
|
|
393
|
+
try {
|
|
394
|
+
config = parse(source);
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return { state: "unreadable" };
|
|
398
|
+
}
|
|
399
|
+
if (!Object.prototype.hasOwnProperty.call(config, "developer_instructions")) {
|
|
400
|
+
return { state: "absent" };
|
|
401
|
+
}
|
|
402
|
+
const instructions = config.developer_instructions;
|
|
403
|
+
return typeof instructions === "string" && instructions.trim()
|
|
404
|
+
? { state: "present", value: instructions }
|
|
405
|
+
: { state: "unreadable" };
|
|
406
|
+
}
|
|
407
|
+
function writeCodexConfig(configPath, config) {
|
|
408
|
+
mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
|
|
409
|
+
const staged = join(dirname(configPath), `.config.toml.${process.pid}.${randomUUID()}`);
|
|
410
|
+
try {
|
|
411
|
+
writeFileSync(staged, stringify(config), { encoding: "utf8", mode: 0o600 });
|
|
412
|
+
renameSync(staged, configPath);
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
rmSync(staged, { force: true });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
111
418
|
/** Persist immutable skill copies in a native Provider home. Fork descendants
|
|
112
419
|
* share that home, so entries must outlive every runner's temporary sources. */
|
|
113
420
|
export function populateCodexSkills(codexHome, skills) {
|
|
114
421
|
const skillsDir = join(codexHome, "skills");
|
|
115
422
|
for (const skill of skills) {
|
|
116
|
-
|
|
117
|
-
throw new Error(`unsafe skill name: ${skill.name}`);
|
|
118
|
-
}
|
|
423
|
+
assertSkillPathComponent(skill.name);
|
|
119
424
|
}
|
|
120
425
|
if (skills.length === 0)
|
|
121
426
|
return;
|