@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.
@@ -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 { installCodexIntegration, uninstallCodexIntegration } from "../install.js";
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
+ });
@@ -1,9 +1,10 @@
1
1
  import assert from "node:assert/strict";
2
- import { existsSync, mkdtempSync, rmSync } from "node:fs";
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, resolveCodexHome } from "../launch.js";
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 normal home honors absolute CODEX_HOME and rejects relative values", () => {
209
- assert.equal(resolveCodexHome({ CODEX_HOME: "/private/codex" }), "/private/codex");
210
- assert.throws(() => resolveCodexHome({ CODEX_HOME: "relative" }), /absolute path/);
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.3.2",
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.0",
42
+ "smol-toml": "1.7.1",
43
43
  "zod": "4.4.3",
44
- "@velum-labs/routekit-contracts": "1.3.2",
45
- "@velum-labs/routekit-registry": "1.3.2",
46
- "@velum-labs/routekit-runtime": "1.3.2",
47
- "@velum-labs/routekit-tools": "1.3.2",
48
- "@velum-labs/routekit-harness-core": "1.3.2"
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",