@rynx-ai/runtime 0.1.11-beta.34 → 0.1.11-beta.36
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/native-hook-main.js +1 -0
- package/dist/codex-app-server/forwarder.js +7 -1
- package/dist/codex-app-server/mapping.js +34 -10
- package/dist/codex-home.d.ts +35 -3
- package/dist/codex-home.js +321 -14
- package/dist/host.d.ts +2 -24
- package/dist/host.js +99 -118
- package/dist/runner/child.d.ts +1 -0
- package/dist/runner/child.js +33 -7
- package/dist/terminal/registry.d.ts +3 -2
- package/dist/terminal/registry.js +6 -4
- package/dist/terminal/tmux.js +14 -6
- package/package.json +2 -2
|
@@ -312,6 +312,7 @@ function nativeVerdict(hookKind, payload, result, suggestions) {
|
|
|
312
312
|
...(behavior === "deny" && feedback ? { message: feedback } : {}),
|
|
313
313
|
...(behavior === "allow"
|
|
314
314
|
? {
|
|
315
|
+
updatedInput: toolInput,
|
|
315
316
|
updatedPermissions: [{
|
|
316
317
|
type: "setMode",
|
|
317
318
|
mode: resolution.actionId === "allow_auto" ? "auto" : "default",
|
|
@@ -13,6 +13,11 @@ function isSubagentThreadStarted(params) {
|
|
|
13
13
|
const source = params?.thread?.source?.subAgent?.thread_spawn;
|
|
14
14
|
return source !== null && typeof source === "object" && !Array.isArray(source);
|
|
15
15
|
}
|
|
16
|
+
/** Internal system threads are non-persistable and never replace the parent
|
|
17
|
+
* TUI thread, even though the app-server broadcasts `thread/started`. */
|
|
18
|
+
function isEphemeralThreadStarted(params) {
|
|
19
|
+
return params?.thread?.ephemeral === true;
|
|
20
|
+
}
|
|
16
21
|
function turnIdFrom(params) {
|
|
17
22
|
const p = params;
|
|
18
23
|
return p?.turnId ?? p?.turn?.id;
|
|
@@ -261,7 +266,8 @@ export class CodexSessionForwarder {
|
|
|
261
266
|
}
|
|
262
267
|
handle(method, params) {
|
|
263
268
|
if (method === "thread/started" || method === "thread.started") {
|
|
264
|
-
if (method === "thread/started" &&
|
|
269
|
+
if (method === "thread/started" &&
|
|
270
|
+
(isSubagentThreadStarted(params) || isEphemeralThreadStarted(params)))
|
|
265
271
|
return;
|
|
266
272
|
const tid = threadIdFrom(params);
|
|
267
273
|
if (tid && tid !== this.currentThreadIdValue) {
|
|
@@ -127,10 +127,12 @@ function fileChangeSummary(changes) {
|
|
|
127
127
|
for (const change of changes) {
|
|
128
128
|
if (!isRecord(change))
|
|
129
129
|
continue;
|
|
130
|
+
if (typeof change.path !== "string")
|
|
131
|
+
continue;
|
|
130
132
|
const kind = isRecord(change.kind) && typeof change.kind.type === "string" && change.kind.type
|
|
131
133
|
? change.kind.type
|
|
132
134
|
: "change";
|
|
133
|
-
lines.push(`${kind} ${
|
|
135
|
+
lines.push(`${kind} ${change.path}`);
|
|
134
136
|
}
|
|
135
137
|
return lines.join("\n");
|
|
136
138
|
}
|
|
@@ -147,20 +149,37 @@ export function mapCodexItem(method, item) {
|
|
|
147
149
|
switch (item.type) {
|
|
148
150
|
case "commandExecution": {
|
|
149
151
|
const commandItem = item;
|
|
152
|
+
const id = typeof commandItem.id === "string" ? commandItem.id : "";
|
|
153
|
+
const command = typeof commandItem.command === "string" ? commandItem.command : "";
|
|
154
|
+
if (!id || !command)
|
|
155
|
+
return { events };
|
|
156
|
+
const exitCode = typeof commandItem.exitCode === "number"
|
|
157
|
+
? commandItem.exitCode
|
|
158
|
+
: null;
|
|
159
|
+
const rawOutput = commandItem.aggregatedOutput ?? "";
|
|
160
|
+
const aggregatedOutput = exitCode !== null && exitCode !== 0
|
|
161
|
+
? `${rawOutput}${rawOutput ? "\n" : ""}[exit code: ${exitCode}]`
|
|
162
|
+
: rawOutput;
|
|
150
163
|
events.push({
|
|
151
164
|
type: "tool",
|
|
152
165
|
event: isStart ? "on_tool_start" : "on_tool_end",
|
|
153
|
-
name: "
|
|
154
|
-
input: isStart
|
|
155
|
-
? {
|
|
166
|
+
name: "shell",
|
|
167
|
+
input: isStart || isEnd
|
|
168
|
+
? {
|
|
169
|
+
id,
|
|
170
|
+
command,
|
|
171
|
+
...(typeof commandItem.cwd === "string" && commandItem.cwd
|
|
172
|
+
? { cwd: commandItem.cwd }
|
|
173
|
+
: {}),
|
|
174
|
+
}
|
|
156
175
|
: undefined,
|
|
157
176
|
output: isEnd
|
|
158
177
|
? {
|
|
159
|
-
id
|
|
160
|
-
command
|
|
178
|
+
id,
|
|
179
|
+
command,
|
|
161
180
|
status: commandItem.status,
|
|
162
|
-
aggregatedOutput
|
|
163
|
-
exitCode
|
|
181
|
+
aggregatedOutput,
|
|
182
|
+
exitCode,
|
|
164
183
|
}
|
|
165
184
|
: undefined,
|
|
166
185
|
data: { method, item },
|
|
@@ -170,14 +189,19 @@ export function mapCodexItem(method, item) {
|
|
|
170
189
|
case "fileChange": {
|
|
171
190
|
const fileChange = item;
|
|
172
191
|
const changes = Array.isArray(fileChange.changes) ? fileChange.changes : [];
|
|
192
|
+
const id = typeof item.id === "string" ? item.id : "";
|
|
193
|
+
if (!id ||
|
|
194
|
+
changes.length === 0 ||
|
|
195
|
+
!changes.some((change) => isRecord(change) && typeof change.path === "string"))
|
|
196
|
+
return { events };
|
|
173
197
|
events.push({
|
|
174
198
|
type: "tool",
|
|
175
199
|
event: isStart ? "on_tool_start" : "on_tool_end",
|
|
176
200
|
name: "apply_patch",
|
|
177
|
-
input: isStart || isEnd ? { id
|
|
201
|
+
input: isStart || isEnd ? { id, changes } : undefined,
|
|
178
202
|
output: isEnd
|
|
179
203
|
? {
|
|
180
|
-
id
|
|
204
|
+
id,
|
|
181
205
|
status: fileChange.status,
|
|
182
206
|
aggregatedOutput: fileChangeSummary(changes),
|
|
183
207
|
}
|
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";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { parse, stringify } from "smol-toml";
|
|
5
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,6 +276,145 @@ 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) {
|
package/dist/host.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ResolvedExecutionSnapshot, type
|
|
1
|
+
import { type ResolvedExecutionSnapshot, type RuntimeTurnOptions, type RuntimeUserInput, type SessionCollaborationMode, type LiveSessionFailure, type SessionWorkspaceSnapshot, type SessionInteractionResolution, type SessionEvent } from "@rynx-ai/core";
|
|
2
2
|
import { type AgentRuntimeId } from "@rynx-ai/core";
|
|
3
3
|
import { type AppConfig } from "@rynx-ai/core";
|
|
4
4
|
import { createCodexChildEnv } from "./codex-child-env.js";
|
|
@@ -107,14 +107,6 @@ export interface LiveSessionOpts {
|
|
|
107
107
|
execution: ResolvedExecutionSnapshot;
|
|
108
108
|
retargetMirror?: RetargetMirror;
|
|
109
109
|
}
|
|
110
|
-
/**
|
|
111
|
-
* The `OPENAI_*` retry env for a codex-lineage app-server spawn, or `undefined`
|
|
112
|
-
* when the budget declares no retry or the runtime is claude. claude applies
|
|
113
|
-
* its retry budget per-run in `buildOptions` (see the claude executor), so it
|
|
114
|
-
* never needs a dedicated app-server.
|
|
115
|
-
*/
|
|
116
|
-
export declare function codexRetryEnv(runtime: AgentRuntimeId, budget?: ResolvedExecutionBudget): Record<string, string> | undefined;
|
|
117
|
-
export declare function codexBackendKey(runtime: AgentRuntimeId, budget?: ResolvedExecutionBudget): string;
|
|
118
110
|
export declare class LocalAgentHost implements CodexCapabilities {
|
|
119
111
|
private readonly config;
|
|
120
112
|
private readonly sessionStore;
|
|
@@ -122,8 +114,6 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
122
114
|
private readonly defaultRuntime;
|
|
123
115
|
private readonly injectedCommandRunner?;
|
|
124
116
|
private readonly injectedAppServerClient?;
|
|
125
|
-
private readonly clock;
|
|
126
|
-
private readonly backendIdleTtlMs;
|
|
127
117
|
private readonly forwarderClientFactory;
|
|
128
118
|
private readonly preloadClientFactory;
|
|
129
119
|
/** Builds one short-lived message/interrupt client. */
|
|
@@ -149,16 +139,12 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
149
139
|
/** Short-lived dedupe for managed fork notifications delivered after the
|
|
150
140
|
* `thread/fork` response. Values are expected source Provider thread ids. */
|
|
151
141
|
private readonly managedForkThreadStarts;
|
|
152
|
-
constructor({ config, commandRunner, sessionStore, allowedRoots, appServerClient,
|
|
142
|
+
constructor({ config, commandRunner, sessionStore, allowedRoots, appServerClient, forwarderClientFactory, preloadClientFactory, injectionClientFactory, sessionId, runtimeHomeSessionId, }: {
|
|
153
143
|
config: AppConfig;
|
|
154
144
|
commandRunner?: CodexCommandRunner;
|
|
155
145
|
sessionStore?: CodexSessionStore;
|
|
156
146
|
allowedRoots?: string[];
|
|
157
147
|
appServerClient?: CodexAppServerClient | null;
|
|
158
|
-
/** Monotonic-ish clock for idle reaping (tests inject a controllable one). */
|
|
159
|
-
now?: () => number;
|
|
160
|
-
/** Idle TTL after which a per-budget composite backend is reaped (ms). */
|
|
161
|
-
backendIdleTtlMs?: number;
|
|
162
148
|
/** Override the live-session forwarder connection factory (tests inject a fake). */
|
|
163
149
|
forwarderClientFactory?: (appServerUrl: string) => CodexAppServerClient;
|
|
164
150
|
/** Override the short-lived known-thread preload connection factory. */
|
|
@@ -176,14 +162,6 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
176
162
|
runtimeHomeSessionId?: string;
|
|
177
163
|
});
|
|
178
164
|
private getBackend;
|
|
179
|
-
/**
|
|
180
|
-
* Stop + drop per-budget composite backends that have been idle longer than
|
|
181
|
-
* the TTL and have no in-flight run. Base per-runtime backends (key === the
|
|
182
|
-
* bare runtime id) are never reaped — they live for the process lifetime, as
|
|
183
|
-
* before. There are no per-conversation close hooks, so this lazy sweep (run
|
|
184
|
-
* on every `getBackend`) is how dedicated app-servers get reclaimed.
|
|
185
|
-
*/
|
|
186
|
-
private reapIdleBackends;
|
|
187
165
|
private createBackend;
|
|
188
166
|
/** This session's private native home, shared by its app-server + TUI. */
|
|
189
167
|
private runtimeHome;
|