@velum-labs/routekit-tool-codex 1.3.2 → 1.5.0
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 +5 -4
- package/dist/driver.js +14 -11
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/install.d.ts +26 -1
- package/dist/install.js +358 -30
- package/dist/launch.d.ts +5 -3
- package/dist/launch.js +69 -20
- package/dist/test/driver.test.js +2 -1
- package/dist/test/install.test.js +471 -1
- package/dist/test/launch.test.js +81 -5
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -21,7 +21,8 @@ import { codexTool } from "@velum-labs/routekit-tool-codex";
|
|
|
21
21
|
|
|
22
22
|
## Native client ownership
|
|
23
23
|
|
|
24
|
-
The launcher uses
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
history and commands
|
|
24
|
+
The launcher uses a RouteKit-owned `CODEX_HOME`, pins Codex authentication to
|
|
25
|
+
its private file store, applies gateway routing as process-scoped configuration,
|
|
26
|
+
and keeps gateway credentials in the environment. RouteKit does not observe,
|
|
27
|
+
store, resume, or delete Codex sessions; use Codex's native history and commands
|
|
28
|
+
directly.
|
package/dist/driver.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { rmSync } from "node:fs";
|
|
1
|
+
import { rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import { Codex } from "@openai/codex-sdk";
|
|
3
4
|
import { asHarnessError, buildChildEnv, createCachedHarnessDriver, HarnessError, nowIso, probeCliVersion, resolveDriverEnv, resumeStringField, SessionResourceRegistry, SingleFlightTurnController } from "@velum-labs/routekit-harness-core";
|
|
4
5
|
import { ResourceScope } from "@velum-labs/routekit-runtime/lifecycle";
|
|
@@ -81,9 +82,10 @@ function codexOptionsFor(config, context, isolatedHome) {
|
|
|
81
82
|
childEnv.CODEX_HOME = isolatedHome;
|
|
82
83
|
return {
|
|
83
84
|
codexPathOverride: config.command,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
config: {
|
|
86
|
+
cli_auth_credentials_store: "file",
|
|
87
|
+
...(config.provider.baseUrl !== undefined
|
|
88
|
+
? {
|
|
87
89
|
model_provider: "routekit",
|
|
88
90
|
model_providers: {
|
|
89
91
|
routekit: {
|
|
@@ -95,8 +97,8 @@ function codexOptionsFor(config, context, isolatedHome) {
|
|
|
95
97
|
}
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
: {})
|
|
101
|
+
},
|
|
100
102
|
...(apiKey !== undefined ? { apiKey } : {}),
|
|
101
103
|
env: childEnv
|
|
102
104
|
};
|
|
@@ -359,17 +361,18 @@ class CodexInstance {
|
|
|
359
361
|
return this.#status;
|
|
360
362
|
}
|
|
361
363
|
/**
|
|
362
|
-
*
|
|
363
|
-
* user's config and model cache from leaking into the
|
|
364
|
-
* explicitly supplied home remains borrowed and is never removed here.
|
|
364
|
+
* Every RouteKit Codex session uses an instance-owned `CODEX_HOME`, preventing
|
|
365
|
+
* the user's config, auth state, and model cache from leaking into the child.
|
|
366
|
+
* An explicitly supplied home remains borrowed and is never removed here.
|
|
365
367
|
*/
|
|
366
368
|
#createOwnedHome() {
|
|
367
|
-
if (this.#config.provider.baseUrl === undefined)
|
|
368
|
-
return undefined;
|
|
369
369
|
const env = resolveDriverEnv(this.#context);
|
|
370
370
|
if (env.CODEX_HOME !== undefined)
|
|
371
371
|
return undefined;
|
|
372
372
|
const home = createIsolatedCodexHome("routekit-codex-driver-", env);
|
|
373
|
+
writeFileSync(join(home, "config.toml"), 'cli_auth_credentials_store = "file"\n', {
|
|
374
|
+
mode: 0o600
|
|
375
|
+
});
|
|
373
376
|
this.#resources.defer(() => rmSync(home, { recursive: true, force: true }));
|
|
374
377
|
return home;
|
|
375
378
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ToolIntegration } from "@velum-labs/routekit-tools";
|
|
|
2
2
|
export declare const codexTool: ToolIntegration;
|
|
3
3
|
export type { CodexDriverConfig } from "./driver.js";
|
|
4
4
|
export { codexDriverConfigSchema, createCodexDriver } from "./driver.js";
|
|
5
|
-
export type { CodexInstallInput, CodexInstallOwner, CodexInstallProfile, CodexInstallResult } from "./install.js";
|
|
6
|
-
export { codexIntegrationBlock, codexIntegrationConfigPath, installCodexIntegration, uninstallCodexIntegration } from "./install.js";
|
|
5
|
+
export type { CodexConfigLockScope, CodexInstallInput, CodexInstallOwner, CodexInstallProfile, CodexInstallResult } from "./install.js";
|
|
6
|
+
export { acquireCodexConfigLock, codexIntegrationBlock, codexIntegrationConfigPath, installCodexIntegration, uninstallCodexIntegration, withCodexConfigLock } from "./install.js";
|
|
7
7
|
export type { CodexAgentRole, CodexModelPreset } from "./launch.js";
|
|
8
|
-
export { codexAgentRoles, codexAgentRoleToml, codexAuthPath, codexCatalogEntries, codexLaunchConfigToml, codexListedStockSlugs, codexModelCatalogJson, codexPersistentModelCatalogJson, codexProfileFiles, codexProfileFileToml, hasCodexLogin, isCodexConfigFailure, launchCodex, readCodexCatalogTemplate, readCodexHomeModelsCache, readCodexModelsCache } from "./launch.js";
|
|
8
|
+
export { codexAgentRoles, codexAgentRoleToml, codexAuthPath, codexCatalogEntries, codexLaunchConfigToml, codexListedStockSlugs, codexModelCatalogJson, codexPersistentModelCatalogJson, codexProfileFiles, codexProfileFileToml, hasCodexLogin, isCodexConfigFailure, launchCodex, readCodexCatalogTemplate, readCodexHomeModelsCache, readCodexModelsCache, resolveRouteKitCodexHome } from "./launch.js";
|
package/dist/index.js
CHANGED
|
@@ -36,5 +36,5 @@ export const codexTool = {
|
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
38
|
export { codexDriverConfigSchema, createCodexDriver } from "./driver.js";
|
|
39
|
-
export { codexIntegrationBlock, codexIntegrationConfigPath, installCodexIntegration, uninstallCodexIntegration } from "./install.js";
|
|
40
|
-
export { codexAgentRoles, codexAgentRoleToml, codexAuthPath, codexCatalogEntries, codexLaunchConfigToml, codexListedStockSlugs, codexModelCatalogJson, codexPersistentModelCatalogJson, codexProfileFiles, codexProfileFileToml, hasCodexLogin, isCodexConfigFailure, launchCodex, readCodexCatalogTemplate, readCodexHomeModelsCache, readCodexModelsCache } from "./launch.js";
|
|
39
|
+
export { acquireCodexConfigLock, codexIntegrationBlock, codexIntegrationConfigPath, installCodexIntegration, uninstallCodexIntegration, withCodexConfigLock } from "./install.js";
|
|
40
|
+
export { codexAgentRoles, codexAgentRoleToml, codexAuthPath, codexCatalogEntries, codexLaunchConfigToml, codexListedStockSlugs, codexModelCatalogJson, codexPersistentModelCatalogJson, codexProfileFiles, codexProfileFileToml, hasCodexLogin, isCodexConfigFailure, launchCodex, readCodexCatalogTemplate, readCodexHomeModelsCache, readCodexModelsCache, resolveRouteKitCodexHome } from "./launch.js";
|
package/dist/install.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { ModelReasoningCapabilities } from "@velum-labs/routekit-contracts";
|
|
1
|
+
import type { ModelReasoningCapabilities, ModelServiceTierCapabilities } from "@velum-labs/routekit-contracts";
|
|
2
2
|
export type CodexInstallProfile = {
|
|
3
3
|
modelId: string;
|
|
4
4
|
description?: string;
|
|
5
5
|
reasoning?: ModelReasoningCapabilities;
|
|
6
|
+
serviceTiers?: ModelServiceTierCapabilities;
|
|
6
7
|
};
|
|
7
8
|
export type CodexInstallOwner = {
|
|
8
9
|
id: string;
|
|
@@ -18,6 +19,11 @@ export type CodexInstallInput = {
|
|
|
18
19
|
models: readonly CodexInstallProfile[];
|
|
19
20
|
/** Model selected when the single RouteKit profile is first opened. */
|
|
20
21
|
defaultModel?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Repository policy context for this managed provider. Codex sends provider
|
|
24
|
+
* headers for every model, so explicit-model requests carry this context too.
|
|
25
|
+
*/
|
|
26
|
+
repositoryConnectionId?: string;
|
|
21
27
|
/** Safe selector for the one persistent RouteKit profile. */
|
|
22
28
|
profileId?: string;
|
|
23
29
|
/**
|
|
@@ -49,3 +55,22 @@ export declare function uninstallCodexIntegration(input: {
|
|
|
49
55
|
configPath: string;
|
|
50
56
|
removed: boolean;
|
|
51
57
|
};
|
|
58
|
+
export type CodexConfigLockScope = Readonly<{
|
|
59
|
+
install(input: CodexInstallInput): CodexInstallResult;
|
|
60
|
+
uninstall(input: {
|
|
61
|
+
ownerId: string;
|
|
62
|
+
codexHome?: string;
|
|
63
|
+
}): {
|
|
64
|
+
configPath: string;
|
|
65
|
+
removed: boolean;
|
|
66
|
+
};
|
|
67
|
+
/** Restore interrupted owner writes without beginning a new install or uninstall. */
|
|
68
|
+
recover(ownerId: string): void;
|
|
69
|
+
release(): void;
|
|
70
|
+
}>;
|
|
71
|
+
/**
|
|
72
|
+
* Hold the same owner-neutral home lock across a larger async CLI transaction.
|
|
73
|
+
* Scoped methods avoid reacquiring the non-reentrant synchronous lock.
|
|
74
|
+
*/
|
|
75
|
+
export declare function acquireCodexConfigLock(codexHome?: string): Promise<CodexConfigLockScope>;
|
|
76
|
+
export declare function withCodexConfigLock<A>(codexHome: string | undefined, callback: (scope: CodexConfigLockScope) => Promise<A> | A): Promise<A>;
|
package/dist/install.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, rmSync
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { parse as tomlParse, stringify as tomlStringify } from "smol-toml";
|
|
5
5
|
import { SUBSCRIPTIONS } from "@velum-labs/routekit-registry";
|
|
6
|
+
import { writeFileAtomic } from "@velum-labs/routekit-runtime/filesystem";
|
|
6
7
|
import { gatewayOpenAiBaseUrl } from "@velum-labs/routekit-runtime/network";
|
|
8
|
+
import { acquireLifecycleLock, acquireLifecycleLockSync } from "@velum-labs/routekit-runtime/service";
|
|
7
9
|
import { codexPersistentModelCatalogJson, codexProfileFileToml, readCodexHomeModelsCache } from "./launch.js";
|
|
8
10
|
export function codexIntegrationConfigPath(codexHome) {
|
|
9
11
|
if (codexHome !== undefined)
|
|
@@ -35,9 +37,214 @@ function profileFileName(selector) {
|
|
|
35
37
|
}
|
|
36
38
|
return `${selector}.config.toml`;
|
|
37
39
|
}
|
|
40
|
+
function pendingPath(codexHome, ownerId) {
|
|
41
|
+
catalogFileName(ownerId);
|
|
42
|
+
return join(codexHome, `.${ownerId}-install-pending.json`);
|
|
43
|
+
}
|
|
44
|
+
function uninstallPendingPath(codexHome, ownerId) {
|
|
45
|
+
catalogFileName(ownerId);
|
|
46
|
+
return join(codexHome, `.${ownerId}-uninstall-pending.json`);
|
|
47
|
+
}
|
|
48
|
+
function configLockPath(codexHome) {
|
|
49
|
+
return join(resolve(codexHome), ".routekit-codex-config.lock");
|
|
50
|
+
}
|
|
51
|
+
function snapshot(path) {
|
|
52
|
+
let entry;
|
|
53
|
+
try {
|
|
54
|
+
entry = lstatSync(path);
|
|
55
|
+
}
|
|
56
|
+
catch (cause) {
|
|
57
|
+
if (cause.code === "ENOENT") {
|
|
58
|
+
return { content: null, mode: null };
|
|
59
|
+
}
|
|
60
|
+
throw cause;
|
|
61
|
+
}
|
|
62
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
63
|
+
throw new Error(`Codex integration refuses a non-regular file: ${path}`);
|
|
64
|
+
}
|
|
65
|
+
return { content: readFileSync(path, "utf8"), mode: entry.mode & 0o777 };
|
|
66
|
+
}
|
|
67
|
+
function applySnapshot(path, value) {
|
|
68
|
+
if (value.content === null) {
|
|
69
|
+
rmSync(path, { force: true });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const mode = value.mode ?? 0o600;
|
|
73
|
+
writeFileAtomic(path, value.content, { mode });
|
|
74
|
+
}
|
|
75
|
+
function sameSnapshot(left, right) {
|
|
76
|
+
return left.content === right.content && left.mode === right.mode;
|
|
77
|
+
}
|
|
78
|
+
function validSnapshot(value) {
|
|
79
|
+
if (!isRecord(value))
|
|
80
|
+
return false;
|
|
81
|
+
if (!((typeof value.content === "string" && Number.isInteger(value.mode)) ||
|
|
82
|
+
(value.content === null && value.mode === null)))
|
|
83
|
+
return false;
|
|
84
|
+
return value.mode === null || (typeof value.mode === "number" && value.mode >= 0 && value.mode <= 0o777);
|
|
85
|
+
}
|
|
86
|
+
function readPending(path) {
|
|
87
|
+
const entry = (() => {
|
|
88
|
+
try {
|
|
89
|
+
return lstatSync(path);
|
|
90
|
+
}
|
|
91
|
+
catch (cause) {
|
|
92
|
+
if (cause.code === "ENOENT")
|
|
93
|
+
return undefined;
|
|
94
|
+
throw cause;
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
if (entry === undefined)
|
|
98
|
+
return;
|
|
99
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
100
|
+
throw new Error(`Codex pending integration metadata is not a regular file: ${path}`);
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new Error(`Codex pending integration metadata is corrupt: ${path}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function recoverFiles(codexHome, path, files, committed) {
|
|
110
|
+
if (committed) {
|
|
111
|
+
// A completed operation must never be rolled back merely because journal
|
|
112
|
+
// cleanup failed after its commit marker was atomically published.
|
|
113
|
+
rmSync(path);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Validate every current file before restoring any: an external edit must
|
|
117
|
+
// never be partially rolled back, even after a process crash.
|
|
118
|
+
for (const file of files) {
|
|
119
|
+
const current = snapshot(join(codexHome, file.name));
|
|
120
|
+
if (!sameSnapshot(current, file.before) && !sameSnapshot(current, file.target)) {
|
|
121
|
+
throw new Error(`Codex settings changed during interrupted integration (${file.name}); ` +
|
|
122
|
+
"RouteKit refused to overwrite the external edit");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const file of files)
|
|
126
|
+
applySnapshot(join(codexHome, file.name), file.before);
|
|
127
|
+
rmSync(path);
|
|
128
|
+
}
|
|
129
|
+
function completeJournal(path, journal) {
|
|
130
|
+
writeFileAtomic(path, `${JSON.stringify({ ...journal, phase: "committed" })}\n`, {
|
|
131
|
+
mode: 0o600
|
|
132
|
+
});
|
|
133
|
+
try {
|
|
134
|
+
rmSync(path);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// The committed marker is published. Retain it for the next locked operation
|
|
138
|
+
// to clean up; reporting a failed install here would misstate its outcome.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function recoverPendingInstall(codexHome, ownerId) {
|
|
142
|
+
const path = pendingPath(codexHome, ownerId);
|
|
143
|
+
const parsed = readPending(path);
|
|
144
|
+
if (parsed === undefined)
|
|
145
|
+
return;
|
|
146
|
+
if (!isRecord(parsed) || parsed.version !== 1 || parsed.ownerId !== ownerId ||
|
|
147
|
+
(parsed.phase !== undefined && parsed.phase !== "committed") ||
|
|
148
|
+
!Array.isArray(parsed.files) || parsed.files.length !== 3) {
|
|
149
|
+
throw new Error(`Codex pending installation metadata is corrupt: ${path}`);
|
|
150
|
+
}
|
|
151
|
+
const files = [];
|
|
152
|
+
for (const value of parsed.files) {
|
|
153
|
+
if (!isRecord(value) || typeof value.name !== "string" ||
|
|
154
|
+
!validSnapshot(value.before) || !validSnapshot(value.target) ||
|
|
155
|
+
value.target.content === null) {
|
|
156
|
+
throw new Error(`Codex pending installation metadata is corrupt: ${path}`);
|
|
157
|
+
}
|
|
158
|
+
files.push({ name: value.name, before: value.before, target: value.target });
|
|
159
|
+
}
|
|
160
|
+
const names = files.map((value) => value.name);
|
|
161
|
+
if (names[0] !== catalogFileName(ownerId) ||
|
|
162
|
+
names[1] === undefined || !names[1].endsWith(".config.toml") ||
|
|
163
|
+
names[1].includes("/") || names[1].includes("\\") ||
|
|
164
|
+
names[2] !== "config.toml" || new Set(names).size !== 3) {
|
|
165
|
+
throw new Error(`Codex pending installation metadata is corrupt: ${path}`);
|
|
166
|
+
}
|
|
167
|
+
recoverFiles(codexHome, path, files, parsed.phase === "committed");
|
|
168
|
+
}
|
|
169
|
+
function withoutManagedBlock(before, ownerId) {
|
|
170
|
+
const split = splitManagedBlock(before, ownerId);
|
|
171
|
+
if (split.managed === undefined)
|
|
172
|
+
return undefined;
|
|
173
|
+
const head = normalize(split.before);
|
|
174
|
+
const tail = normalize(split.after);
|
|
175
|
+
return `${head}${head.length > 0 && tail.length > 0 ? "\n" : ""}${tail}`;
|
|
176
|
+
}
|
|
177
|
+
function recoverPendingUninstall(codexHome, ownerId) {
|
|
178
|
+
const path = uninstallPendingPath(codexHome, ownerId);
|
|
179
|
+
const parsed = readPending(path);
|
|
180
|
+
if (parsed === undefined)
|
|
181
|
+
return;
|
|
182
|
+
if (!isRecord(parsed) || parsed.version !== 1 || parsed.ownerId !== ownerId ||
|
|
183
|
+
(parsed.phase !== undefined && parsed.phase !== "committed") ||
|
|
184
|
+
!Array.isArray(parsed.files) || parsed.files.length < 1) {
|
|
185
|
+
throw new Error(`Codex pending uninstallation metadata is corrupt: ${path}`);
|
|
186
|
+
}
|
|
187
|
+
const files = [];
|
|
188
|
+
for (const value of parsed.files) {
|
|
189
|
+
if (!isRecord(value) || typeof value.name !== "string" ||
|
|
190
|
+
!validSnapshot(value.before) || !validSnapshot(value.target)) {
|
|
191
|
+
throw new Error(`Codex pending uninstallation metadata is corrupt: ${path}`);
|
|
192
|
+
}
|
|
193
|
+
files.push({ name: value.name, before: value.before, target: value.target });
|
|
194
|
+
}
|
|
195
|
+
const config = files[0];
|
|
196
|
+
const desired = config.before.content === null
|
|
197
|
+
? undefined
|
|
198
|
+
: withoutManagedBlock(config.before.content, ownerId);
|
|
199
|
+
if (config.name !== "config.toml" || desired === undefined ||
|
|
200
|
+
config.target.content !== desired || config.target.mode !== config.before.mode) {
|
|
201
|
+
throw new Error(`Codex pending uninstallation metadata is corrupt: ${path}`);
|
|
202
|
+
}
|
|
203
|
+
const managed = splitManagedBlock(config.before.content, ownerId).managed;
|
|
204
|
+
const profileNames = new Set(ownedProfileFiles(managed, codexHome, ownerId).map((file) => basename(file)));
|
|
205
|
+
const catalog = ownedCatalogFile(managed, codexHome, ownerId);
|
|
206
|
+
const catalogName = catalog === undefined ? undefined : basename(catalog);
|
|
207
|
+
const names = new Set();
|
|
208
|
+
for (const file of files) {
|
|
209
|
+
if (names.has(file.name))
|
|
210
|
+
throw new Error(`Codex pending uninstallation metadata is corrupt: ${path}`);
|
|
211
|
+
names.add(file.name);
|
|
212
|
+
}
|
|
213
|
+
for (const file of files.slice(1)) {
|
|
214
|
+
const ownedProfile = profileNames.has(file.name) &&
|
|
215
|
+
file.before.content?.includes(`Managed by ${ownerId}`) === true;
|
|
216
|
+
if ((!ownedProfile && file.name !== catalogName) ||
|
|
217
|
+
file.before.content === null || file.target.content !== null || file.target.mode !== null) {
|
|
218
|
+
throw new Error(`Codex pending uninstallation metadata is corrupt: ${path}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
recoverFiles(codexHome, path, files, parsed.phase === "committed");
|
|
222
|
+
}
|
|
223
|
+
function recoverPendingChanges(codexHome, ownerId) {
|
|
224
|
+
recoverPendingInstall(codexHome, ownerId);
|
|
225
|
+
recoverPendingUninstall(codexHome, ownerId);
|
|
226
|
+
}
|
|
227
|
+
function reachedInstallBoundary(boundary) {
|
|
228
|
+
const testingGlobal = globalThis;
|
|
229
|
+
testingGlobal.__routekitCodexInstallFaultInjector?.(boundary);
|
|
230
|
+
}
|
|
231
|
+
function reachedUninstallBoundary(boundary) {
|
|
232
|
+
const testingGlobal = globalThis;
|
|
233
|
+
testingGlobal.__routekitCodexUninstallFaultInjector?.(boundary);
|
|
234
|
+
}
|
|
38
235
|
function selectedProfileId(input) {
|
|
39
236
|
return input.profileId ?? "routekit";
|
|
40
237
|
}
|
|
238
|
+
function repositoryConnectionHeader(connectionId) {
|
|
239
|
+
if (connectionId === undefined)
|
|
240
|
+
return undefined;
|
|
241
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(connectionId)) {
|
|
242
|
+
throw new Error("Codex repository connection id must be a UUID");
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
"X-RouteKit-Repository-Connection-Id": connectionId.toLowerCase()
|
|
246
|
+
};
|
|
247
|
+
}
|
|
41
248
|
function selectedDefaultModel(input) {
|
|
42
249
|
const model = input.defaultModel ?? input.models[0]?.modelId;
|
|
43
250
|
if (model === undefined)
|
|
@@ -56,6 +263,7 @@ function orderedCatalogProfiles(profiles, defaultModel) {
|
|
|
56
263
|
/** Serialize one additive, owner-marked Codex provider block. */
|
|
57
264
|
export function codexIntegrationBlock(input) {
|
|
58
265
|
const base = gatewayOpenAiBaseUrl(input.gatewayUrl);
|
|
266
|
+
const httpHeaders = repositoryConnectionHeader(input.repositoryConnectionId);
|
|
59
267
|
const begin = marker(input.owner.id, "begin");
|
|
60
268
|
const end = marker(input.owner.id, "end");
|
|
61
269
|
const filesComment = profileFilesComment(input.owner.id);
|
|
@@ -63,11 +271,13 @@ export function codexIntegrationBlock(input) {
|
|
|
63
271
|
const catalogFile = catalogFileName(input.owner.id);
|
|
64
272
|
const profileId = selectedProfileId(input);
|
|
65
273
|
const body = tomlStringify({
|
|
274
|
+
cli_auth_credentials_store: "file",
|
|
66
275
|
model_providers: {
|
|
67
276
|
[input.owner.providerId]: {
|
|
68
277
|
name: `${input.owner.displayName} gateway`,
|
|
69
278
|
base_url: base,
|
|
70
279
|
wire_api: "responses",
|
|
280
|
+
...(httpHeaders !== undefined ? { http_headers: httpHeaders } : {}),
|
|
71
281
|
...(input.auth !== undefined
|
|
72
282
|
? {
|
|
73
283
|
auth: {
|
|
@@ -167,18 +377,6 @@ function normalize(content) {
|
|
|
167
377
|
const trimmed = content.replace(/\s+$/, "");
|
|
168
378
|
return trimmed.length === 0 ? "" : `${trimmed}\n`;
|
|
169
379
|
}
|
|
170
|
-
function removeOwnedProfileFile(path, ownerId) {
|
|
171
|
-
try {
|
|
172
|
-
if (!existsSync(path))
|
|
173
|
-
return;
|
|
174
|
-
if (!readFileSync(path, "utf8").includes(`Managed by ${ownerId}`))
|
|
175
|
-
return;
|
|
176
|
-
rmSync(path);
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
// Best-effort cleanup; an orphaned profile does not alter the main config.
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
380
|
function assertProfileFileCanBeManaged(path, ownerId) {
|
|
183
381
|
if (!existsSync(path))
|
|
184
382
|
return;
|
|
@@ -187,13 +385,14 @@ function assertProfileFileCanBeManaged(path, ownerId) {
|
|
|
187
385
|
throw new Error(`refusing to overwrite an existing Codex profile: ${path}; ` +
|
|
188
386
|
`rename it, then rerun the RouteKit install`);
|
|
189
387
|
}
|
|
190
|
-
|
|
388
|
+
function installCodexIntegrationUnlocked(input) {
|
|
191
389
|
if (input.models.length === 0)
|
|
192
390
|
throw new Error("at least one Codex catalog model is required");
|
|
193
391
|
const defaultModel = selectedDefaultModel(input);
|
|
194
392
|
const profileId = selectedProfileId(input);
|
|
195
393
|
const configPath = codexIntegrationConfigPath(input.codexHome);
|
|
196
394
|
const codexHome = dirname(configPath);
|
|
395
|
+
recoverPendingChanges(codexHome, input.owner.id);
|
|
197
396
|
const existing = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
198
397
|
const { before, managed, after } = splitManagedBlock(existing, input.owner.id);
|
|
199
398
|
const outside = parseTomlOrThrow(`${normalize(before)}\n${normalize(after)}`, `your Codex config (${configPath})`);
|
|
@@ -215,12 +414,44 @@ export function installCodexIntegration(input) {
|
|
|
215
414
|
const persistentProfilePath = join(codexHome, profileFileName(profileId));
|
|
216
415
|
assertProfileFileCanBeManaged(persistentProfilePath, input.owner.id);
|
|
217
416
|
mkdirSync(codexHome, { recursive: true });
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
417
|
+
const files = [
|
|
418
|
+
{
|
|
419
|
+
name: catalogFileName(input.owner.id),
|
|
420
|
+
before: snapshot(catalogPath),
|
|
421
|
+
target: {
|
|
422
|
+
content: codexPersistentModelCatalogJson(orderedCatalogProfiles(input.models, defaultModel).map((profile) => ({
|
|
423
|
+
id: profile.modelId,
|
|
424
|
+
...(profile.reasoning !== undefined ? { reasoning: profile.reasoning } : {}),
|
|
425
|
+
...(profile.serviceTiers !== undefined ? { serviceTiers: profile.serviceTiers } : {})
|
|
426
|
+
})), readCodexHomeModelsCache(codexHome)[0]),
|
|
427
|
+
mode: 0o600
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
name: profileFileName(profileId),
|
|
432
|
+
before: snapshot(persistentProfilePath),
|
|
433
|
+
target: {
|
|
434
|
+
content: `# Managed by ${input.owner.id}\n${codexProfileFileToml(defaultModel, input.owner.providerId, catalogPath)}`,
|
|
435
|
+
mode: 0o600
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
name: "config.toml",
|
|
440
|
+
before: snapshot(configPath),
|
|
441
|
+
target: { content: next, mode: snapshot(configPath).mode ?? 0o600 }
|
|
442
|
+
}
|
|
443
|
+
];
|
|
444
|
+
const journal = pendingPath(codexHome, input.owner.id);
|
|
445
|
+
const pending = {
|
|
446
|
+
version: 1, ownerId: input.owner.id, files
|
|
447
|
+
};
|
|
448
|
+
writeFileAtomic(journal, `${JSON.stringify(pending)}\n`, { mode: 0o600 });
|
|
449
|
+
reachedInstallBoundary("pending");
|
|
450
|
+
for (const [index, file] of files.entries()) {
|
|
451
|
+
applySnapshot(join(codexHome, file.name), file.target);
|
|
452
|
+
reachedInstallBoundary(index === 0 ? "catalog" : index === 1 ? "profile" : "config");
|
|
453
|
+
}
|
|
454
|
+
completeJournal(journal, pending);
|
|
224
455
|
return {
|
|
225
456
|
configPath,
|
|
226
457
|
catalogPath,
|
|
@@ -228,22 +459,119 @@ export function installCodexIntegration(input) {
|
|
|
228
459
|
profiles: [profileId]
|
|
229
460
|
};
|
|
230
461
|
}
|
|
231
|
-
export function
|
|
462
|
+
export function installCodexIntegration(input) {
|
|
463
|
+
const lock = acquireLifecycleLockSync(configLockPath(dirname(codexIntegrationConfigPath(input.codexHome))));
|
|
464
|
+
try {
|
|
465
|
+
return installCodexIntegrationUnlocked(input);
|
|
466
|
+
}
|
|
467
|
+
finally {
|
|
468
|
+
lock.release();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function uninstallCodexIntegrationUnlocked(input) {
|
|
232
472
|
const configPath = codexIntegrationConfigPath(input.codexHome);
|
|
473
|
+
const codexHome = dirname(configPath);
|
|
474
|
+
recoverPendingChanges(codexHome, input.ownerId);
|
|
233
475
|
if (!existsSync(configPath))
|
|
234
476
|
return { configPath, removed: false };
|
|
235
477
|
const existing = readFileSync(configPath, "utf8");
|
|
236
478
|
const { before, managed, after } = splitManagedBlock(existing, input.ownerId);
|
|
237
479
|
if (managed === undefined)
|
|
238
480
|
return { configPath, removed: false };
|
|
239
|
-
|
|
240
|
-
|
|
481
|
+
const desired = withoutManagedBlock(existing, input.ownerId);
|
|
482
|
+
const config = snapshot(configPath);
|
|
483
|
+
const files = [
|
|
484
|
+
{
|
|
485
|
+
name: "config.toml",
|
|
486
|
+
before: config,
|
|
487
|
+
target: { content: desired, mode: config.mode }
|
|
488
|
+
}
|
|
489
|
+
];
|
|
490
|
+
for (const path of ownedProfileFiles(managed, codexHome, input.ownerId)) {
|
|
491
|
+
const before = snapshot(path);
|
|
492
|
+
if (before.content?.includes(`Managed by ${input.ownerId}`) === true) {
|
|
493
|
+
files.push({
|
|
494
|
+
name: basename(path),
|
|
495
|
+
before,
|
|
496
|
+
target: { content: null, mode: null }
|
|
497
|
+
});
|
|
498
|
+
}
|
|
241
499
|
}
|
|
242
|
-
const catalogPath = ownedCatalogFile(managed,
|
|
243
|
-
if (catalogPath !== undefined)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
500
|
+
const catalogPath = ownedCatalogFile(managed, codexHome, input.ownerId);
|
|
501
|
+
if (catalogPath !== undefined) {
|
|
502
|
+
const before = snapshot(catalogPath);
|
|
503
|
+
if (before.content !== null) {
|
|
504
|
+
files.push({
|
|
505
|
+
name: basename(catalogPath),
|
|
506
|
+
before,
|
|
507
|
+
target: { content: null, mode: null }
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const journal = uninstallPendingPath(codexHome, input.ownerId);
|
|
512
|
+
const pending = {
|
|
513
|
+
version: 1, ownerId: input.ownerId, files
|
|
514
|
+
};
|
|
515
|
+
writeFileAtomic(journal, `${JSON.stringify(pending)}\n`, { mode: 0o600 });
|
|
516
|
+
reachedUninstallBoundary("pending");
|
|
517
|
+
for (const [index, file] of files.entries()) {
|
|
518
|
+
applySnapshot(join(codexHome, file.name), file.target);
|
|
519
|
+
reachedUninstallBoundary(index === 0 ? "config" : file.name === catalogFileName(input.ownerId) ? "catalog" : "profile");
|
|
520
|
+
}
|
|
521
|
+
completeJournal(journal, pending);
|
|
248
522
|
return { configPath, removed: true };
|
|
249
523
|
}
|
|
524
|
+
export function uninstallCodexIntegration(input) {
|
|
525
|
+
const lock = acquireLifecycleLockSync(configLockPath(dirname(codexIntegrationConfigPath(input.codexHome))));
|
|
526
|
+
try {
|
|
527
|
+
return uninstallCodexIntegrationUnlocked(input);
|
|
528
|
+
}
|
|
529
|
+
finally {
|
|
530
|
+
lock.release();
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Hold the same owner-neutral home lock across a larger async CLI transaction.
|
|
535
|
+
* Scoped methods avoid reacquiring the non-reentrant synchronous lock.
|
|
536
|
+
*/
|
|
537
|
+
export async function acquireCodexConfigLock(codexHome) {
|
|
538
|
+
const configPath = resolve(codexIntegrationConfigPath(codexHome));
|
|
539
|
+
const lock = await acquireLifecycleLock(configLockPath(dirname(configPath)));
|
|
540
|
+
let released = false;
|
|
541
|
+
const assertScope = (inputHome) => {
|
|
542
|
+
if (released)
|
|
543
|
+
throw new Error("Codex configuration lock has already been released");
|
|
544
|
+
if (resolve(codexIntegrationConfigPath(inputHome)) !== configPath) {
|
|
545
|
+
throw new Error("Codex integration target does not match the locked home");
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
return {
|
|
549
|
+
install(input) {
|
|
550
|
+
assertScope(input.codexHome);
|
|
551
|
+
return installCodexIntegrationUnlocked(input);
|
|
552
|
+
},
|
|
553
|
+
uninstall(input) {
|
|
554
|
+
assertScope(input.codexHome);
|
|
555
|
+
return uninstallCodexIntegrationUnlocked(input);
|
|
556
|
+
},
|
|
557
|
+
recover(ownerId) {
|
|
558
|
+
assertScope(codexHome);
|
|
559
|
+
recoverPendingChanges(dirname(configPath), ownerId);
|
|
560
|
+
},
|
|
561
|
+
release() {
|
|
562
|
+
if (released)
|
|
563
|
+
return;
|
|
564
|
+
lock.release();
|
|
565
|
+
released = true;
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
export async function withCodexConfigLock(codexHome, callback) {
|
|
570
|
+
const scope = await acquireCodexConfigLock(codexHome);
|
|
571
|
+
try {
|
|
572
|
+
return await callback(scope);
|
|
573
|
+
}
|
|
574
|
+
finally {
|
|
575
|
+
scope.release();
|
|
576
|
+
}
|
|
577
|
+
}
|
package/dist/launch.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { type ModelReasoningCapabilities } from "@velum-labs/routekit-contracts";
|
|
2
|
+
import { type ModelReasoningCapabilities, type ModelServiceTierCapabilities } from "@velum-labs/routekit-contracts";
|
|
3
3
|
import type { AgentProfile, ToolLaunchContext, ToolLaunchSpec } from "@velum-labs/routekit-tools";
|
|
4
4
|
export type CodexModelPreset = Record<string, unknown>;
|
|
5
5
|
/** Metadata persisted beside a RouteKit-owned Codex profile. */
|
|
@@ -7,6 +7,7 @@ export type CodexPersistentCatalogModel = {
|
|
|
7
7
|
id: string;
|
|
8
8
|
label?: string;
|
|
9
9
|
reasoning?: ModelReasoningCapabilities;
|
|
10
|
+
serviceTiers?: ModelServiceTierCapabilities;
|
|
10
11
|
};
|
|
11
12
|
export declare function isCodexConfigFailure(code: number, stderr: string): boolean;
|
|
12
13
|
export declare function tomlKey(name: string): string;
|
|
@@ -23,6 +24,8 @@ export declare function hasCodexLogin(home?: string): boolean;
|
|
|
23
24
|
* cannot read or mutate the user's real Codex configuration.
|
|
24
25
|
*/
|
|
25
26
|
export declare function createIsolatedCodexHome(prefix: string, env?: Record<string, string | undefined>): string;
|
|
27
|
+
/** Stable RouteKit-owned Codex home used by interactive launchers. */
|
|
28
|
+
export declare function resolveRouteKitCodexHome(env?: Record<string, string | undefined>): string;
|
|
26
29
|
export declare function codexListedStockSlugs(home?: string): string[];
|
|
27
30
|
export declare function codexCatalogEntries(spec: Pick<ToolLaunchSpec, "defaultModel" | "modelSelection" | "models">, template: CodexModelPreset, stockModels?: readonly CodexModelPreset[], options?: {
|
|
28
31
|
appendUnlistedStock?: boolean;
|
|
@@ -48,6 +51,5 @@ export type CodexLaunchDependencies = {
|
|
|
48
51
|
spawnProcess?: typeof spawn;
|
|
49
52
|
env?: Record<string, string | undefined>;
|
|
50
53
|
};
|
|
51
|
-
|
|
52
|
-
/** Launch against the user's real Codex home; only generated catalog data is temporary. */
|
|
54
|
+
/** Launch against a RouteKit-owned Codex home with file-only auth persistence. */
|
|
53
55
|
export declare function launchCodex(ctx: ToolLaunchContext, deps?: CodexLaunchDependencies): Promise<number>;
|