@velum-labs/routekit-tool-codex 1.4.0 → 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
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
2
3
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { syncBuiltinESMExports } from "node:module";
|
|
3
5
|
import { tmpdir } from "node:os";
|
|
4
6
|
import { join } from "node:path";
|
|
5
7
|
import { test } from "node:test";
|
|
6
|
-
import {
|
|
8
|
+
import { parse as tomlParse } from "smol-toml";
|
|
9
|
+
import { acquireLifecycleLockSync } from "@velum-labs/routekit-runtime/service";
|
|
10
|
+
import { acquireCodexConfigLock, installCodexIntegration, uninstallCodexIntegration, withCodexConfigLock } from "../install.js";
|
|
7
11
|
const OWNER = {
|
|
8
12
|
id: "example-host",
|
|
9
13
|
displayName: "Example Host",
|
|
@@ -12,6 +16,271 @@ const OWNER = {
|
|
|
12
16
|
uninstallCommand: "example uninstall codex",
|
|
13
17
|
startCommand: "example serve"
|
|
14
18
|
};
|
|
19
|
+
test("Codex install sets file modes before publishing, without fallible post-commit chmod", () => {
|
|
20
|
+
const home = mkdtempSync(join(tmpdir(), "routekit-codex-atomic-mode-"));
|
|
21
|
+
const originalChmod = fs.chmodSync;
|
|
22
|
+
const publishedPaths = new Set([
|
|
23
|
+
join(home, "config.toml"),
|
|
24
|
+
join(home, "routekit.config.toml"),
|
|
25
|
+
join(home, ".example-host-model-catalog.json"),
|
|
26
|
+
join(home, ".example-host-install-pending.json")
|
|
27
|
+
]);
|
|
28
|
+
try {
|
|
29
|
+
fs.chmodSync = (path, mode) => {
|
|
30
|
+
if (publishedPaths.has(String(path))) {
|
|
31
|
+
throw new Error(`chmod after publication: ${String(path)}`);
|
|
32
|
+
}
|
|
33
|
+
originalChmod(path, mode);
|
|
34
|
+
};
|
|
35
|
+
syncBuiltinESMExports();
|
|
36
|
+
installCodexIntegration({
|
|
37
|
+
gatewayUrl: "http://127.0.0.1:9999",
|
|
38
|
+
owner: OWNER,
|
|
39
|
+
models: [{ modelId: "provider/one" }],
|
|
40
|
+
codexHome: home
|
|
41
|
+
});
|
|
42
|
+
for (const path of publishedPaths) {
|
|
43
|
+
if (!existsSync(path))
|
|
44
|
+
continue;
|
|
45
|
+
assert.equal(fs.statSync(path).mode & 0o777, 0o600);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
fs.chmodSync = originalChmod;
|
|
50
|
+
syncBuiltinESMExports();
|
|
51
|
+
rmSync(home, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
test("Codex committed journals survive cleanup failures without rolling back an install or uninstall", async () => {
|
|
55
|
+
const home = mkdtempSync(join(tmpdir(), "routekit-codex-journal-cleanup-"));
|
|
56
|
+
const configPath = join(home, "config.toml");
|
|
57
|
+
const installJournal = join(home, ".example-host-install-pending.json");
|
|
58
|
+
const uninstallJournal = join(home, ".example-host-uninstall-pending.json");
|
|
59
|
+
const originalRemove = fs.rmSync;
|
|
60
|
+
const input = {
|
|
61
|
+
gatewayUrl: "http://127.0.0.1:9999",
|
|
62
|
+
owner: OWNER,
|
|
63
|
+
models: [{ modelId: "provider/one" }],
|
|
64
|
+
codexHome: home
|
|
65
|
+
};
|
|
66
|
+
const failJournalCleanup = (journal) => {
|
|
67
|
+
fs.rmSync = (path, options) => {
|
|
68
|
+
if (String(path) === journal)
|
|
69
|
+
throw new Error("journal cleanup unavailable");
|
|
70
|
+
originalRemove(path, options);
|
|
71
|
+
};
|
|
72
|
+
syncBuiltinESMExports();
|
|
73
|
+
};
|
|
74
|
+
const restoreRemove = () => {
|
|
75
|
+
fs.rmSync = originalRemove;
|
|
76
|
+
syncBuiltinESMExports();
|
|
77
|
+
};
|
|
78
|
+
writeFileSync(configPath, 'model = "user-default"\n');
|
|
79
|
+
try {
|
|
80
|
+
failJournalCleanup(installJournal);
|
|
81
|
+
assert.equal(installCodexIntegration(input).action, "installed");
|
|
82
|
+
restoreRemove();
|
|
83
|
+
assert.equal(JSON.parse(readFileSync(installJournal, "utf8")).phase, "committed");
|
|
84
|
+
const installed = readFileSync(configPath, "utf8");
|
|
85
|
+
const scope = await acquireCodexConfigLock(home);
|
|
86
|
+
try {
|
|
87
|
+
scope.recover(OWNER.id);
|
|
88
|
+
assert.equal(readFileSync(configPath, "utf8"), installed);
|
|
89
|
+
assert.equal(existsSync(installJournal), false);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
scope.release();
|
|
93
|
+
}
|
|
94
|
+
failJournalCleanup(uninstallJournal);
|
|
95
|
+
assert.equal(uninstallCodexIntegration({ ownerId: OWNER.id, codexHome: home }).removed, true);
|
|
96
|
+
restoreRemove();
|
|
97
|
+
assert.equal(JSON.parse(readFileSync(uninstallJournal, "utf8")).phase, "committed");
|
|
98
|
+
const uninstalled = readFileSync(configPath, "utf8");
|
|
99
|
+
assert.equal(uninstalled, 'model = "user-default"\n');
|
|
100
|
+
const nextScope = await acquireCodexConfigLock(home);
|
|
101
|
+
try {
|
|
102
|
+
nextScope.recover(OWNER.id);
|
|
103
|
+
assert.equal(readFileSync(configPath, "utf8"), uninstalled);
|
|
104
|
+
assert.equal(existsSync(join(home, "routekit.config.toml")), false);
|
|
105
|
+
assert.equal(existsSync(uninstallJournal), false);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
nextScope.release();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
restoreRemove();
|
|
113
|
+
rmSync(home, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
test("Codex config scope can retry release when lock unlink fails", async () => {
|
|
117
|
+
const home = mkdtempSync(join(tmpdir(), "routekit-codex-release-retry-"));
|
|
118
|
+
const lockPath = join(home, ".routekit-codex-config.lock");
|
|
119
|
+
const originalRemove = fs.rmSync;
|
|
120
|
+
try {
|
|
121
|
+
const scope = await acquireCodexConfigLock(home);
|
|
122
|
+
fs.rmSync = (path, options) => {
|
|
123
|
+
if (String(path) === lockPath)
|
|
124
|
+
throw new Error("lock unlink unavailable");
|
|
125
|
+
originalRemove(path, options);
|
|
126
|
+
};
|
|
127
|
+
syncBuiltinESMExports();
|
|
128
|
+
assert.throws(() => scope.release(), /lock unlink unavailable/);
|
|
129
|
+
assert.throws(() => acquireLifecycleLockSync(lockPath), /lock is busy/);
|
|
130
|
+
fs.rmSync = originalRemove;
|
|
131
|
+
syncBuiltinESMExports();
|
|
132
|
+
scope.release();
|
|
133
|
+
assert.equal(existsSync(lockPath), false);
|
|
134
|
+
assert.throws(() => scope.recover(OWNER.id), /already been released/);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
fs.rmSync = originalRemove;
|
|
138
|
+
syncBuiltinESMExports();
|
|
139
|
+
rmSync(home, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
test("Codex install and uninstall share an owner-neutral home lock and fail closed on contention", () => {
|
|
143
|
+
const home = mkdtempSync(join(tmpdir(), "routekit-codex-config-lock-"));
|
|
144
|
+
const path = join(home, ".routekit-codex-config.lock");
|
|
145
|
+
const configPath = join(home, "config.toml");
|
|
146
|
+
const original = 'model = "user-default"\n';
|
|
147
|
+
writeFileSync(configPath, original);
|
|
148
|
+
const input = {
|
|
149
|
+
gatewayUrl: "http://127.0.0.1:9999",
|
|
150
|
+
owner: OWNER,
|
|
151
|
+
models: [{ modelId: "provider/one" }],
|
|
152
|
+
codexHome: home
|
|
153
|
+
};
|
|
154
|
+
try {
|
|
155
|
+
const first = acquireLifecycleLockSync(path);
|
|
156
|
+
try {
|
|
157
|
+
assert.throws(() => installCodexIntegration(input), /lifecycle lock is busy/);
|
|
158
|
+
assert.throws(() => uninstallCodexIntegration({ ownerId: OWNER.id, codexHome: home }), /lifecycle lock is busy/);
|
|
159
|
+
assert.equal(readFileSync(configPath, "utf8"), original);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
first.release();
|
|
163
|
+
}
|
|
164
|
+
installCodexIntegration(input);
|
|
165
|
+
assert.equal(uninstallCodexIntegration({ ownerId: OWNER.id, codexHome: home }).removed, true);
|
|
166
|
+
assert.equal(readFileSync(configPath, "utf8"), original);
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
rmSync(home, { recursive: true, force: true });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
test("async Codex scope composes owner installs without reacquiring the sync lock", async () => {
|
|
173
|
+
const home = mkdtempSync(join(tmpdir(), "routekit-codex-async-lock-"));
|
|
174
|
+
const otherHome = mkdtempSync(join(tmpdir(), "routekit-codex-other-lock-"));
|
|
175
|
+
const configPath = join(home, "config.toml");
|
|
176
|
+
const cloudOwner = {
|
|
177
|
+
...OWNER, id: "example-cloud", providerId: "example_cloud"
|
|
178
|
+
};
|
|
179
|
+
const input = {
|
|
180
|
+
gatewayUrl: "http://127.0.0.1:9999",
|
|
181
|
+
models: [{ modelId: "provider/one" }],
|
|
182
|
+
codexHome: home
|
|
183
|
+
};
|
|
184
|
+
try {
|
|
185
|
+
const scope = await acquireCodexConfigLock(home);
|
|
186
|
+
try {
|
|
187
|
+
assert.throws(() => installCodexIntegration({ ...input, owner: OWNER }), /lock is busy/);
|
|
188
|
+
assert.throws(() => scope.install({
|
|
189
|
+
...input, owner: OWNER, codexHome: otherHome
|
|
190
|
+
}), /does not match the locked home/);
|
|
191
|
+
scope.install({ ...input, owner: OWNER });
|
|
192
|
+
scope.install({ ...input, owner: cloudOwner, profileId: "routekit-cloud" });
|
|
193
|
+
assert.match(readFileSync(configPath, "utf8"), /model_providers\.example_route/);
|
|
194
|
+
assert.match(readFileSync(configPath, "utf8"), /model_providers\.example_cloud/);
|
|
195
|
+
scope.uninstall({ ownerId: cloudOwner.id, codexHome: home });
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
scope.release();
|
|
199
|
+
}
|
|
200
|
+
assert.throws(() => scope.install({ ...input, owner: OWNER }), /already been released/);
|
|
201
|
+
assert.equal(uninstallCodexIntegration({ ownerId: OWNER.id, codexHome: home }).removed, true);
|
|
202
|
+
await assert.rejects(withCodexConfigLock(home, async (held) => {
|
|
203
|
+
held.install({ ...input, owner: OWNER });
|
|
204
|
+
throw new Error("callback failed");
|
|
205
|
+
}), /callback failed/);
|
|
206
|
+
assert.equal(uninstallCodexIntegration({ ownerId: OWNER.id, codexHome: home }).removed, true);
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
rmSync(home, { recursive: true, force: true });
|
|
210
|
+
rmSync(otherHome, { recursive: true, force: true });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
test("async Codex scope creates a missing home before acquiring its lock", async () => {
|
|
214
|
+
const root = mkdtempSync(join(tmpdir(), "routekit-codex-new-home-"));
|
|
215
|
+
const home = join(root, "codex");
|
|
216
|
+
try {
|
|
217
|
+
assert.equal(existsSync(home), false);
|
|
218
|
+
await withCodexConfigLock(home, (scope) => {
|
|
219
|
+
scope.install({
|
|
220
|
+
gatewayUrl: "http://127.0.0.1:9999",
|
|
221
|
+
models: [{ modelId: "provider/one" }],
|
|
222
|
+
owner: OWNER,
|
|
223
|
+
codexHome: home
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
assert.equal(existsSync(join(home, "config.toml")), true);
|
|
227
|
+
assert.equal(existsSync(join(home, ".routekit-codex-config.lock")), false);
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
rmSync(root, { recursive: true, force: true });
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
test("Codex scope recovers interrupted install and uninstall without starting another operation", async () => {
|
|
234
|
+
const testingGlobal = globalThis;
|
|
235
|
+
const home = mkdtempSync(join(tmpdir(), "routekit-codex-scoped-recovery-"));
|
|
236
|
+
const configPath = join(home, "config.toml");
|
|
237
|
+
const original = 'model = "user-default"\n';
|
|
238
|
+
const input = {
|
|
239
|
+
gatewayUrl: "http://127.0.0.1:9999",
|
|
240
|
+
models: [{ modelId: "provider/one" }],
|
|
241
|
+
owner: OWNER,
|
|
242
|
+
codexHome: home
|
|
243
|
+
};
|
|
244
|
+
writeFileSync(configPath, original);
|
|
245
|
+
try {
|
|
246
|
+
const scope = await acquireCodexConfigLock(home);
|
|
247
|
+
try {
|
|
248
|
+
testingGlobal.__routekitCodexInstallFaultInjector = (boundary) => {
|
|
249
|
+
if (boundary === "profile")
|
|
250
|
+
throw new Error("interrupted install");
|
|
251
|
+
};
|
|
252
|
+
assert.throws(() => scope.install(input), /interrupted install/);
|
|
253
|
+
delete testingGlobal.__routekitCodexInstallFaultInjector;
|
|
254
|
+
scope.recover(OWNER.id);
|
|
255
|
+
assert.equal(readFileSync(configPath, "utf8"), original);
|
|
256
|
+
assert.equal(existsSync(join(home, "routekit.config.toml")), false);
|
|
257
|
+
assert.equal(existsSync(join(home, ".example-host-model-catalog.json")), false);
|
|
258
|
+
assert.equal(existsSync(join(home, ".example-host-install-pending.json")), false);
|
|
259
|
+
scope.install(input);
|
|
260
|
+
const installed = readFileSync(configPath, "utf8");
|
|
261
|
+
testingGlobal.__routekitCodexUninstallFaultInjector = (boundary) => {
|
|
262
|
+
if (boundary === "catalog")
|
|
263
|
+
throw new Error("interrupted uninstall");
|
|
264
|
+
};
|
|
265
|
+
assert.throws(() => scope.uninstall({ ownerId: OWNER.id, codexHome: home }), /interrupted uninstall/);
|
|
266
|
+
delete testingGlobal.__routekitCodexUninstallFaultInjector;
|
|
267
|
+
scope.recover(OWNER.id);
|
|
268
|
+
assert.equal(readFileSync(configPath, "utf8"), installed);
|
|
269
|
+
assert.equal(existsSync(join(home, "routekit.config.toml")), true);
|
|
270
|
+
assert.equal(existsSync(join(home, ".example-host-model-catalog.json")), true);
|
|
271
|
+
assert.equal(existsSync(join(home, ".example-host-uninstall-pending.json")), false);
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
scope.release();
|
|
275
|
+
}
|
|
276
|
+
assert.throws(() => scope.recover(OWNER.id), /already been released/);
|
|
277
|
+
}
|
|
278
|
+
finally {
|
|
279
|
+
delete testingGlobal.__routekitCodexInstallFaultInjector;
|
|
280
|
+
delete testingGlobal.__routekitCodexUninstallFaultInjector;
|
|
281
|
+
rmSync(home, { recursive: true, force: true });
|
|
282
|
+
}
|
|
283
|
+
});
|
|
15
284
|
test("Codex managed install adds one picker-backed profile and removes only owner-marked config", () => {
|
|
16
285
|
const home = mkdtempSync(join(tmpdir(), "routekit-codex-install-"));
|
|
17
286
|
const configPath = join(home, "config.toml");
|
|
@@ -30,6 +299,7 @@ test("Codex managed install adds one picker-backed profile and removes only owne
|
|
|
30
299
|
assert.equal(installed.action, "installed");
|
|
31
300
|
assert.deepEqual(installed.profiles, ["routekit"]);
|
|
32
301
|
assert.match(readFileSync(configPath, "utf8"), /model = "user-default"/);
|
|
302
|
+
assert.match(readFileSync(configPath, "utf8"), /cli_auth_credentials_store = "file"/);
|
|
33
303
|
assert.match(readFileSync(configPath, "utf8"), /base_url = "http:\/\/127\.0\.0\.1:9999\/v1"/);
|
|
34
304
|
assert.match(readFileSync(configPath, "utf8"), /codex --profile routekit/);
|
|
35
305
|
assert.equal(existsSync(join(home, "routekit.config.toml")), true);
|
|
@@ -81,6 +351,55 @@ test("Codex managed install can use a command-backed bearer token", () => {
|
|
|
81
351
|
rmSync(home, { recursive: true, force: true });
|
|
82
352
|
}
|
|
83
353
|
});
|
|
354
|
+
test("Codex repository-bound provider sends only the validated context header", () => {
|
|
355
|
+
const home = mkdtempSync(join(tmpdir(), "routekit-codex-repository-header-"));
|
|
356
|
+
const connectionId = "A2B9B2DD-83D4-4F04-904B-49D5BEB7B431";
|
|
357
|
+
try {
|
|
358
|
+
const input = {
|
|
359
|
+
gatewayUrl: "http://127.0.0.1:9999",
|
|
360
|
+
owner: OWNER,
|
|
361
|
+
models: [{ modelId: "auto" }, { modelId: "provider/explicit" }],
|
|
362
|
+
defaultModel: "auto",
|
|
363
|
+
repositoryConnectionId: connectionId,
|
|
364
|
+
codexHome: home
|
|
365
|
+
};
|
|
366
|
+
installCodexIntegration(input);
|
|
367
|
+
const config = readFileSync(join(home, "config.toml"), "utf8");
|
|
368
|
+
const parsed = tomlParse(config);
|
|
369
|
+
assert.deepEqual(parsed.model_providers.example_route?.http_headers, { "X-RouteKit-Repository-Connection-Id": connectionId.toLowerCase() });
|
|
370
|
+
assert.match(config, /\[model_providers\.example_route\.http_headers\]/);
|
|
371
|
+
assert.match(readFileSync(join(home, "routekit.config.toml"), "utf8"), /model = "auto"/);
|
|
372
|
+
assert.doesNotMatch(config, /A2B9B2DD/);
|
|
373
|
+
installCodexIntegration({ ...input, repositoryConnectionId: undefined });
|
|
374
|
+
const unbound = tomlParse(readFileSync(join(home, "config.toml"), "utf8"));
|
|
375
|
+
assert.equal(unbound.model_providers.example_route?.http_headers, undefined);
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
rmSync(home, { recursive: true, force: true });
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
test("Codex repository binding rejects malformed IDs before publishing configuration", () => {
|
|
382
|
+
const home = mkdtempSync(join(tmpdir(), "routekit-codex-invalid-repository-header-"));
|
|
383
|
+
const configPath = join(home, "config.toml");
|
|
384
|
+
const original = 'model = "user-default"\n';
|
|
385
|
+
writeFileSync(configPath, original);
|
|
386
|
+
try {
|
|
387
|
+
for (const connectionId of ["", "not-a-uuid", "a\nX-Api-Key: secret", "00000000-0000-0000-0000-000000000000 "]) {
|
|
388
|
+
assert.throws(() => installCodexIntegration({
|
|
389
|
+
gatewayUrl: "http://127.0.0.1:9999",
|
|
390
|
+
owner: OWNER,
|
|
391
|
+
models: [{ modelId: "auto" }],
|
|
392
|
+
repositoryConnectionId: connectionId,
|
|
393
|
+
codexHome: home
|
|
394
|
+
}), /repository connection id must be a UUID/);
|
|
395
|
+
assert.equal(readFileSync(configPath, "utf8"), original);
|
|
396
|
+
assert.equal(existsSync(join(home, ".example-host-model-catalog.json")), false);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
finally {
|
|
400
|
+
rmSync(home, { recursive: true, force: true });
|
|
401
|
+
}
|
|
402
|
+
});
|
|
84
403
|
test("Codex's single persistent profile can use a safe custom selector", () => {
|
|
85
404
|
const home = mkdtempSync(join(tmpdir(), "routekit-codex-opaque-"));
|
|
86
405
|
try {
|
|
@@ -117,3 +436,154 @@ test("Codex install refuses to overwrite a user-owned routekit profile", () => {
|
|
|
117
436
|
rmSync(home, { recursive: true, force: true });
|
|
118
437
|
}
|
|
119
438
|
});
|
|
439
|
+
test("Codex install recovers each interrupted write boundary without losing user settings", () => {
|
|
440
|
+
const testingGlobal = globalThis;
|
|
441
|
+
for (const boundary of ["pending", "catalog", "profile", "config"]) {
|
|
442
|
+
const home = mkdtempSync(join(tmpdir(), `routekit-codex-interrupted-${boundary}-`));
|
|
443
|
+
const original = 'model = "user-default"\n';
|
|
444
|
+
writeFileSync(join(home, "config.toml"), original);
|
|
445
|
+
try {
|
|
446
|
+
testingGlobal.__routekitCodexInstallFaultInjector = (reached) => {
|
|
447
|
+
if (reached === boundary)
|
|
448
|
+
throw new Error(`interrupted at ${boundary}`);
|
|
449
|
+
};
|
|
450
|
+
assert.throws(() => installCodexIntegration({
|
|
451
|
+
gatewayUrl: "http://127.0.0.1:9999",
|
|
452
|
+
owner: OWNER,
|
|
453
|
+
models: [{ modelId: "provider/one" }],
|
|
454
|
+
codexHome: home
|
|
455
|
+
}), new RegExp(`interrupted at ${boundary}`));
|
|
456
|
+
delete testingGlobal.__routekitCodexInstallFaultInjector;
|
|
457
|
+
assert.equal(existsSync(join(home, ".example-host-install-pending.json")), true);
|
|
458
|
+
if (boundary === "config") {
|
|
459
|
+
assert.match(readFileSync(join(home, "config.toml"), "utf8"), /example_route/);
|
|
460
|
+
assert.match(readFileSync(join(home, "routekit.config.toml"), "utf8"), /provider\/one/);
|
|
461
|
+
}
|
|
462
|
+
const result = installCodexIntegration({
|
|
463
|
+
gatewayUrl: "http://127.0.0.1:9999",
|
|
464
|
+
owner: OWNER,
|
|
465
|
+
models: [{ modelId: "provider/two" }],
|
|
466
|
+
codexHome: home
|
|
467
|
+
});
|
|
468
|
+
assert.equal(result.action, "installed");
|
|
469
|
+
assert.match(readFileSync(join(home, "config.toml"), "utf8"), /user-default/);
|
|
470
|
+
assert.match(readFileSync(join(home, "routekit.config.toml"), "utf8"), /provider\/two/);
|
|
471
|
+
assert.equal(existsSync(join(home, ".example-host-install-pending.json")), false);
|
|
472
|
+
}
|
|
473
|
+
finally {
|
|
474
|
+
delete testingGlobal.__routekitCodexInstallFaultInjector;
|
|
475
|
+
rmSync(home, { recursive: true, force: true });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
test("Codex uninstall recovers each interrupted write boundary and preserves another owner", () => {
|
|
480
|
+
const testingGlobal = globalThis;
|
|
481
|
+
const cloudOwner = {
|
|
482
|
+
...OWNER, id: "example-cloud", providerId: "example_cloud"
|
|
483
|
+
};
|
|
484
|
+
for (const boundary of ["pending", "config", "profile", "catalog"]) {
|
|
485
|
+
const home = mkdtempSync(join(tmpdir(), `routekit-codex-uninstall-${boundary}-`));
|
|
486
|
+
const original = 'model = "user-default"\n';
|
|
487
|
+
const configPath = join(home, "config.toml");
|
|
488
|
+
writeFileSync(configPath, original);
|
|
489
|
+
try {
|
|
490
|
+
installCodexIntegration({
|
|
491
|
+
gatewayUrl: "http://127.0.0.1:9999",
|
|
492
|
+
owner: OWNER,
|
|
493
|
+
models: [{ modelId: "provider/one" }],
|
|
494
|
+
codexHome: home
|
|
495
|
+
});
|
|
496
|
+
installCodexIntegration({
|
|
497
|
+
gatewayUrl: "http://127.0.0.1:9999",
|
|
498
|
+
owner: cloudOwner,
|
|
499
|
+
profileId: "routekit-cloud",
|
|
500
|
+
models: [{ modelId: "provider/cloud" }],
|
|
501
|
+
codexHome: home
|
|
502
|
+
});
|
|
503
|
+
testingGlobal.__routekitCodexUninstallFaultInjector = (reached) => {
|
|
504
|
+
if (reached === boundary)
|
|
505
|
+
throw new Error(`uninstall interrupted at ${boundary}`);
|
|
506
|
+
};
|
|
507
|
+
assert.throws(() => uninstallCodexIntegration({ ownerId: OWNER.id, codexHome: home }), new RegExp(`uninstall interrupted at ${boundary}`));
|
|
508
|
+
delete testingGlobal.__routekitCodexUninstallFaultInjector;
|
|
509
|
+
assert.equal(existsSync(join(home, ".example-host-uninstall-pending.json")), true);
|
|
510
|
+
if (boundary === "catalog") {
|
|
511
|
+
assert.equal(existsSync(join(home, ".example-host-model-catalog.json")), false);
|
|
512
|
+
assert.doesNotMatch(readFileSync(configPath, "utf8"), /example_route/);
|
|
513
|
+
}
|
|
514
|
+
assert.equal(uninstallCodexIntegration({ ownerId: OWNER.id, codexHome: home }).removed, true);
|
|
515
|
+
assert.equal(existsSync(join(home, ".example-host-uninstall-pending.json")), false);
|
|
516
|
+
assert.equal(existsSync(join(home, "routekit.config.toml")), false);
|
|
517
|
+
assert.equal(existsSync(join(home, ".example-host-model-catalog.json")), false);
|
|
518
|
+
assert.match(readFileSync(configPath, "utf8"), /user-default/);
|
|
519
|
+
assert.doesNotMatch(readFileSync(configPath, "utf8"), /example_route/);
|
|
520
|
+
assert.match(readFileSync(configPath, "utf8"), /example_cloud/);
|
|
521
|
+
assert.match(readFileSync(join(home, "routekit-cloud.config.toml"), "utf8"), /provider\/cloud/);
|
|
522
|
+
}
|
|
523
|
+
finally {
|
|
524
|
+
delete testingGlobal.__routekitCodexUninstallFaultInjector;
|
|
525
|
+
rmSync(home, { recursive: true, force: true });
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
test("Codex interrupted-uninstall recovery refuses an intervening user edit", () => {
|
|
530
|
+
const testingGlobal = globalThis;
|
|
531
|
+
const home = mkdtempSync(join(tmpdir(), "routekit-codex-uninstall-edit-"));
|
|
532
|
+
const configPath = join(home, "config.toml");
|
|
533
|
+
try {
|
|
534
|
+
installCodexIntegration({
|
|
535
|
+
gatewayUrl: "http://127.0.0.1:9999",
|
|
536
|
+
owner: OWNER,
|
|
537
|
+
models: [{ modelId: "provider/one" }],
|
|
538
|
+
codexHome: home
|
|
539
|
+
});
|
|
540
|
+
testingGlobal.__routekitCodexUninstallFaultInjector = (boundary) => {
|
|
541
|
+
if (boundary === "config")
|
|
542
|
+
throw new Error("interrupted after config");
|
|
543
|
+
};
|
|
544
|
+
assert.throws(() => uninstallCodexIntegration({ ownerId: OWNER.id, codexHome: home }), /interrupted after config/);
|
|
545
|
+
delete testingGlobal.__routekitCodexUninstallFaultInjector;
|
|
546
|
+
const edit = 'model = "user-edited-after-crash"\n';
|
|
547
|
+
writeFileSync(configPath, edit);
|
|
548
|
+
assert.throws(() => uninstallCodexIntegration({ ownerId: OWNER.id, codexHome: home }), /refused to overwrite the external edit/);
|
|
549
|
+
assert.equal(readFileSync(configPath, "utf8"), edit);
|
|
550
|
+
assert.equal(existsSync(join(home, ".example-host-uninstall-pending.json")), true);
|
|
551
|
+
}
|
|
552
|
+
finally {
|
|
553
|
+
delete testingGlobal.__routekitCodexUninstallFaultInjector;
|
|
554
|
+
rmSync(home, { recursive: true, force: true });
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
test("Codex interrupted-install recovery refuses to overwrite an intervening user edit", () => {
|
|
558
|
+
const testingGlobal = globalThis;
|
|
559
|
+
const home = mkdtempSync(join(tmpdir(), "routekit-codex-interrupted-edit-"));
|
|
560
|
+
const configPath = join(home, "config.toml");
|
|
561
|
+
writeFileSync(configPath, 'model = "before"\n');
|
|
562
|
+
try {
|
|
563
|
+
testingGlobal.__routekitCodexInstallFaultInjector = (boundary) => {
|
|
564
|
+
if (boundary === "catalog")
|
|
565
|
+
throw new Error("interrupted after catalog");
|
|
566
|
+
};
|
|
567
|
+
assert.throws(() => installCodexIntegration({
|
|
568
|
+
gatewayUrl: "http://127.0.0.1:9999",
|
|
569
|
+
owner: OWNER,
|
|
570
|
+
models: [{ modelId: "provider/one" }],
|
|
571
|
+
codexHome: home
|
|
572
|
+
}), /interrupted after catalog/);
|
|
573
|
+
delete testingGlobal.__routekitCodexInstallFaultInjector;
|
|
574
|
+
const edit = 'model = "user-edited-after-crash"\n';
|
|
575
|
+
writeFileSync(configPath, edit);
|
|
576
|
+
assert.throws(() => installCodexIntegration({
|
|
577
|
+
gatewayUrl: "http://127.0.0.1:9999",
|
|
578
|
+
owner: OWNER,
|
|
579
|
+
models: [{ modelId: "provider/one" }],
|
|
580
|
+
codexHome: home
|
|
581
|
+
}), /refused to overwrite the external edit/);
|
|
582
|
+
assert.equal(readFileSync(configPath, "utf8"), edit);
|
|
583
|
+
assert.equal(existsSync(join(home, ".example-host-install-pending.json")), true);
|
|
584
|
+
}
|
|
585
|
+
finally {
|
|
586
|
+
delete testingGlobal.__routekitCodexInstallFaultInjector;
|
|
587
|
+
rmSync(home, { recursive: true, force: true });
|
|
588
|
+
}
|
|
589
|
+
});
|
package/dist/test/launch.test.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import {
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import { test } from "node:test";
|
|
6
|
-
import { codexAgentRoleToml, codexCatalogEntries, codexLaunchConfigToml, codexModelCatalogJson, codexPersistentModelCatalogJson, createIsolatedCodexHome, launchCodex,
|
|
7
|
+
import { codexAgentRoleToml, codexCatalogEntries, codexLaunchConfigToml, codexModelCatalogJson, codexPersistentModelCatalogJson, createIsolatedCodexHome, launchCodex, resolveRouteKitCodexHome } from "../launch.js";
|
|
7
8
|
const SPEC = {
|
|
8
9
|
gatewayUrl: "http://127.0.0.1:9999",
|
|
9
10
|
defaultModel: "opaque-primary",
|
|
@@ -157,6 +158,38 @@ test("Codex launcher neutralizes stock-model behavior for gateway-routed models"
|
|
|
157
158
|
assert.equal(entry.base_instructions, "You are a coding agent.");
|
|
158
159
|
assert.deepEqual(entry.model_messages, { instructions_template: "You are a coding agent." });
|
|
159
160
|
});
|
|
161
|
+
test("Codex exposes authoritative service tiers for gateway-routed models", () => {
|
|
162
|
+
const spec = {
|
|
163
|
+
gatewayUrl: "http://127.0.0.1:9999",
|
|
164
|
+
defaultModel: "codex/gpt-tiered",
|
|
165
|
+
models: [
|
|
166
|
+
{
|
|
167
|
+
id: "codex/gpt-tiered",
|
|
168
|
+
serviceTiers: {
|
|
169
|
+
tiers: [
|
|
170
|
+
{ id: "priority", name: "Fast", description: "1.5x speed, increased usage" },
|
|
171
|
+
{ id: "flex", name: "Flex" }
|
|
172
|
+
],
|
|
173
|
+
defaultTier: "priority",
|
|
174
|
+
wireShape: "openai-service-tier"
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
args: []
|
|
179
|
+
};
|
|
180
|
+
const [entry] = codexCatalogEntries(spec, {
|
|
181
|
+
slug: "stock",
|
|
182
|
+
additional_speed_tiers: [],
|
|
183
|
+
service_tiers: [],
|
|
184
|
+
default_service_tier: null
|
|
185
|
+
});
|
|
186
|
+
assert.deepEqual(entry?.additional_speed_tiers, ["fast"]);
|
|
187
|
+
assert.deepEqual(entry?.service_tiers, [
|
|
188
|
+
{ id: "priority", name: "Fast", description: "1.5x speed, increased usage" },
|
|
189
|
+
{ id: "flex", name: "Flex", description: "Flex service tier." }
|
|
190
|
+
]);
|
|
191
|
+
assert.equal(entry?.default_service_tier, "priority");
|
|
192
|
+
});
|
|
160
193
|
test("Codex keeps real stock ModelInfo only for Codex-native models", () => {
|
|
161
194
|
const spec = {
|
|
162
195
|
gatewayUrl: "http://127.0.0.1:9999",
|
|
@@ -177,6 +210,7 @@ test("Codex launcher serializes a gateway provider and generic agent profiles",
|
|
|
177
210
|
role
|
|
178
211
|
]);
|
|
179
212
|
assert.match(config, /model = "opaque-primary"/);
|
|
213
|
+
assert.match(config, /cli_auth_credentials_store = "file"/);
|
|
180
214
|
assert.match(config, /base_url = "http:\/\/127\.0\.0\.1:9999\/v1"/);
|
|
181
215
|
assert.match(config, /env_key = "ROUTEKIT_GATEWAY_TOKEN"/);
|
|
182
216
|
assert.match(config, /config_file = "\/tmp\/reviewer\.toml"/);
|
|
@@ -191,6 +225,10 @@ test("persistent Codex profiles carry matching model metadata", () => {
|
|
|
191
225
|
efforts: [{ id: "low" }, { id: "high" }],
|
|
192
226
|
defaultEffort: "low",
|
|
193
227
|
provenance: "provider"
|
|
228
|
+
},
|
|
229
|
+
serviceTiers: {
|
|
230
|
+
tiers: [{ id: "priority", name: "Fast", description: "Faster responses" }],
|
|
231
|
+
wireShape: "openai-service-tier"
|
|
194
232
|
}
|
|
195
233
|
},
|
|
196
234
|
{ id: "codex/gpt-5.6" }
|
|
@@ -204,10 +242,48 @@ test("persistent Codex profiles carry matching model metadata", () => {
|
|
|
204
242
|
{ effort: "high", description: "high" }
|
|
205
243
|
]);
|
|
206
244
|
assert.equal(catalog.models[0]?.supports_reasoning_summaries, true);
|
|
245
|
+
assert.deepEqual(catalog.models[0]?.additional_speed_tiers, ["fast"]);
|
|
246
|
+
assert.deepEqual(catalog.models[0]?.service_tiers, [
|
|
247
|
+
{ id: "priority", name: "Fast", description: "Faster responses" }
|
|
248
|
+
]);
|
|
249
|
+
});
|
|
250
|
+
test("Codex launch home is RouteKit-scoped unless an absolute CODEX_HOME is explicit", () => {
|
|
251
|
+
assert.equal(resolveRouteKitCodexHome({ CODEX_HOME: "/private/codex" }), "/private/codex");
|
|
252
|
+
assert.equal(resolveRouteKitCodexHome({ ROUTEKIT_HOME: "/private/routekit" }), "/private/routekit/codex");
|
|
253
|
+
assert.throws(() => resolveRouteKitCodexHome({ CODEX_HOME: "relative" }), /absolute path/);
|
|
207
254
|
});
|
|
208
|
-
test("Codex
|
|
209
|
-
|
|
210
|
-
|
|
255
|
+
test("Codex launch writes private file-auth config under RouteKit home", async () => {
|
|
256
|
+
const root = mkdtempSync(join(tmpdir(), "routekit-codex-launch-home-"));
|
|
257
|
+
const routekitHome = join(root, "routekit");
|
|
258
|
+
let spawnedEnv;
|
|
259
|
+
const child = new EventEmitter();
|
|
260
|
+
queueMicrotask(() => child.emit("exit", 0, null));
|
|
261
|
+
try {
|
|
262
|
+
const result = await launchCodex({
|
|
263
|
+
spec: SPEC,
|
|
264
|
+
log: () => { },
|
|
265
|
+
prepareForPassthrough: () => { },
|
|
266
|
+
registerPort: () => "",
|
|
267
|
+
unregisterPort: () => { },
|
|
268
|
+
registerDisposer: () => { }
|
|
269
|
+
}, {
|
|
270
|
+
env: { HOME: join(root, "home"), ROUTEKIT_HOME: routekitHome },
|
|
271
|
+
spawnProcess: ((_command, _args, options) => {
|
|
272
|
+
spawnedEnv = options?.env;
|
|
273
|
+
return child;
|
|
274
|
+
})
|
|
275
|
+
});
|
|
276
|
+
const codexHome = join(routekitHome, "codex");
|
|
277
|
+
const configPath = join(codexHome, "config.toml");
|
|
278
|
+
assert.equal(result, 0);
|
|
279
|
+
assert.equal(spawnedEnv?.CODEX_HOME, codexHome);
|
|
280
|
+
assert.match(readFileSync(configPath, "utf8"), /cli_auth_credentials_store = "file"/);
|
|
281
|
+
assert.equal(statSync(codexHome).mode & 0o777, 0o700);
|
|
282
|
+
assert.equal(statSync(configPath).mode & 0o777, 0o600);
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
rmSync(root, { recursive: true, force: true });
|
|
286
|
+
}
|
|
211
287
|
});
|
|
212
288
|
test("isolated Codex homes used by the harness live under the user cache", () => {
|
|
213
289
|
const root = mkdtempSync(join(tmpdir(), "routekit-codex-home-test-"));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velum-labs/routekit-tool-codex",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.5.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/velum-labs/routekit.git",
|
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@openai/codex-sdk": "0.145.0",
|
|
42
|
-
"smol-toml": "1.7.
|
|
42
|
+
"smol-toml": "1.7.1",
|
|
43
43
|
"zod": "4.4.3",
|
|
44
|
-
"@velum-labs/routekit-contracts": "1.
|
|
45
|
-
"@velum-labs/routekit-harness-core": "1.
|
|
46
|
-
"@velum-labs/routekit-registry": "1.
|
|
47
|
-
"@velum-labs/routekit-runtime": "1.
|
|
48
|
-
"@velum-labs/routekit-tools": "1.
|
|
44
|
+
"@velum-labs/routekit-contracts": "1.5.0",
|
|
45
|
+
"@velum-labs/routekit-harness-core": "1.5.0",
|
|
46
|
+
"@velum-labs/routekit-registry": "1.5.0",
|
|
47
|
+
"@velum-labs/routekit-runtime": "1.5.0",
|
|
48
|
+
"@velum-labs/routekit-tools": "1.5.0"
|
|
49
49
|
},
|
|
50
50
|
"keywords": [
|
|
51
51
|
"llm",
|