troxy-cli 1.29.9 → 1.29.10

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/bin/troxy.js CHANGED
@@ -573,7 +573,7 @@ switch (command) {
573
573
  // is its picture of which checkpoints exist, and that lives on the server.
574
574
  // This is a no-op on a machine with an MCP client, which gets a fresh tool
575
575
  // list from the MCP handshake every session anyway.
576
- const { refreshHostedInstructions, reprovisionKeyConsumers } = await import('../src/init.js');
576
+ const { refreshHostedInstructions } = await import('../src/init.js');
577
577
  const refresh = async () => {
578
578
  try {
579
579
  return await refreshHostedInstructions((loadConfig() || {}).apiKey);
@@ -614,10 +614,38 @@ switch (command) {
614
614
  // until someone thought to re-run `init` by hand. Best-effort: a
615
615
  // failure here must not make `update` itself look like it failed when
616
616
  // the actual package upgrade already succeeded.
617
+ //
618
+ // Found live 2026-09-13: this used to call reprovisionKeyConsumers()
619
+ // in-process via a dynamic import of ../src/init.js. That import is
620
+ // resolved by Node's ES module cache, which is keyed by file URL, not
621
+ // re-checked against disk - and this same process already loaded
622
+ // ../src/init.js (transitively, via bin/troxy.js's own top-of-file
623
+ // static imports) back when it STARTED, i.e. running the version
624
+ // being upgraded FROM. The npm install above only changes what's on
625
+ // disk; it can't evict this process's already-cached module graph.
626
+ // So every "provision the just-installed version" step here was
627
+ // actually running the PREVIOUS version's code - invisible unless
628
+ // that code path itself changed between versions, which is exactly
629
+ // what happened with the gui/user launchd domain fix (1.29.9): update
630
+ // correctly installed 1.29.9's files, then reprovisioned using
631
+ // 1.29.8's stale in-memory launchdUserDomain(), reintroducing the
632
+ // bug `troxy update` had just shipped the fix for. A dynamic import,
633
+ // no matter where or how it's written, can't fix this - it needs an
634
+ // actual new OS process, which loads ../src/init.js fresh from the
635
+ // now-updated disk. Hence shelling out to the just-installed binary's
636
+ // own hidden `_reprovision` command instead of calling the function
637
+ // directly.
617
638
  const savedConfig = loadConfig();
618
639
  if (savedConfig?.apiKey) {
619
640
  try {
620
- await reprovisionKeyConsumers(savedConfig.apiKey, savedConfig.agentName);
641
+ const { execFileSync } = await import('child_process');
642
+ const globalRoot = execSync('npm root -g', { stdio: 'pipe' }).toString().trim();
643
+ const freshBin = `${globalRoot}/troxy-cli/bin/troxy.js`;
644
+ execFileSync(process.execPath, [
645
+ freshBin, '_reprovision',
646
+ '--key', savedConfig.apiKey,
647
+ '--name', savedConfig.agentName || 'my-agent',
648
+ ], { stdio: 'inherit' });
621
649
  } catch (err) {
622
650
  console.error(`\n Could not refresh MCP client config: ${err.message}`);
623
651
  console.error(' Run "troxy init --key <your-key>" by hand to finish updating.\n');
@@ -639,6 +667,18 @@ switch (command) {
639
667
  break;
640
668
  }
641
669
 
670
+ // Internal, undocumented - not for direct use. `update` shells out to
671
+ // this in a brand new process (see the comment at its call site) purely
672
+ // to force a fresh, on-disk read of ../src/init.js after an in-place
673
+ // npm upgrade, instead of reusing this same process's already-cached
674
+ // module graph from the pre-upgrade version.
675
+ case '_reprovision': {
676
+ if (!flags.key) process.exit(1);
677
+ const { reprovisionKeyConsumers } = await import('../src/init.js');
678
+ await reprovisionKeyConsumers(flags.key, flags.name || 'my-agent');
679
+ break;
680
+ }
681
+
642
682
  // ── Help / default ────────────────────────────────────────────
643
683
  default:
644
684
  if (command) console.error(` Unknown command: ${command}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "troxy-cli",
3
- "version": "1.29.9",
3
+ "version": "1.29.10",
4
4
  "description": "A secure control layer for AI agents: policies across payments, messages, logins, destructive actions, model usage, and secrets, all enforceable from the CLI",
5
5
  "homepage": "https://troxy.io",
6
6
  "bugs": {
@@ -0,0 +1,82 @@
1
+ // Found live 2026-09-13: `troxy update`'s post-install reprovisioning step
2
+ // used to call reprovisionKeyConsumers() in-process via a dynamic import of
3
+ // ../src/init.js. Node's ES module cache is keyed by file URL and is never
4
+ // re-checked against disk - bin/troxy.js's own top-of-file static imports
5
+ // already pull in ../src/init.js the moment the process starts, i.e. running
6
+ // whatever version is being upgraded FROM. `npm install -g` only changes
7
+ // files on disk; it can't evict this process's already-cached module graph.
8
+ // So the "provision the just-installed version" step was silently always
9
+ // running the PREVIOUS version's code - invisible unless that exact code
10
+ // path changed between versions, which is exactly what happened with the
11
+ // gui/user launchd domain fix (1.29.9): update correctly installed 1.29.9's
12
+ // files, then reprovisioned using 1.29.8's stale in-memory
13
+ // launchdUserDomain(), reintroducing the very bug that release shipped the
14
+ // fix for. The fix is bin/troxy.js's `update` case shelling out (via
15
+ // execFileSync, a real new OS process) to the just-installed binary's own
16
+ // hidden `_reprovision` command, which loads ../src/init.js fresh from the
17
+ // now-updated disk - a dynamic import from the still-running update process
18
+ // can't do this no matter where it's written.
19
+
20
+ import { describe, it } from 'node:test';
21
+ import assert from 'node:assert/strict';
22
+ import { readFileSync } from 'node:fs';
23
+ import { fileURLToPath } from 'node:url';
24
+ import { dirname, join } from 'node:path';
25
+
26
+ const __dirname = dirname(fileURLToPath(import.meta.url));
27
+ const troxySrc = readFileSync(join(__dirname, '..', '..', 'bin', 'troxy.js'), 'utf8');
28
+
29
+ function updateCaseBody(src) {
30
+ const start = src.indexOf("case 'update':");
31
+ assert.ok(start !== -1, "case 'update' not found");
32
+ const end = src.indexOf("case '_reprovision':", start);
33
+ assert.ok(end !== -1, "case '_reprovision' not found after 'update'");
34
+ return src.slice(start, end);
35
+ }
36
+
37
+ describe("update case: reprovisions via a fresh subprocess, not an in-process import", () => {
38
+ it('does not call reprovisionKeyConsumers directly inside the update case', () => {
39
+ const body = updateCaseBody(troxySrc);
40
+ assert.doesNotMatch(body, /await reprovisionKeyConsumers\(/,
41
+ 'must not call reprovisionKeyConsumers() in-process - that reuses this process\'s pre-upgrade module cache');
42
+ });
43
+
44
+ it('no longer destructure-imports reprovisionKeyConsumers into the update case', () => {
45
+ const body = updateCaseBody(troxySrc);
46
+ assert.doesNotMatch(body, /\{[^}]*reprovisionKeyConsumers[^}]*\} = await import/,
47
+ 'the whole point of the fix is that update never binds the in-process function');
48
+ });
49
+
50
+ it('still imports refreshHostedInstructions in-process (unaffected by this bug)', () => {
51
+ const body = updateCaseBody(troxySrc);
52
+ assert.match(body, /const \{ refreshHostedInstructions \} = await import\('\.\.\/src\/init\.js'\);/);
53
+ });
54
+
55
+ it('shells out to a freshly-resolved binary path via execFileSync, not the running process\'s own code', () => {
56
+ const body = updateCaseBody(troxySrc);
57
+ assert.match(body, /execFileSync\(/, 'must spawn a real new process to force a fresh disk read');
58
+ assert.match(body, /npm root -g/, 'must resolve the just-installed package location, not assume one');
59
+ assert.match(body, /_reprovision/, 'must invoke the hidden _reprovision entry point');
60
+ });
61
+
62
+ it('passes the API key as a separate argv entry, not interpolated into a shell string', () => {
63
+ const body = updateCaseBody(troxySrc);
64
+ const callIdx = body.indexOf('execFileSync(process.execPath, [');
65
+ assert.ok(callIdx !== -1, 'expected execFileSync(process.execPath, [...]) call shape');
66
+ const callBlock = body.slice(callIdx, body.indexOf(');', callIdx));
67
+ assert.doesNotMatch(callBlock, /`[^`]*\$\{savedConfig\.apiKey\}/,
68
+ 'the key must not be template-interpolated into a shell-executed string');
69
+ assert.match(callBlock, /'--key',\s*savedConfig\.apiKey/);
70
+ });
71
+ });
72
+
73
+ describe("'_reprovision': hidden entry point the update subprocess invokes", () => {
74
+ it('exists, requires --key, and calls reprovisionKeyConsumers with fresh flags', () => {
75
+ const start = troxySrc.indexOf("case '_reprovision':");
76
+ assert.ok(start !== -1, "case '_reprovision' not found");
77
+ const block = troxySrc.slice(start, start + 500);
78
+ assert.match(block, /if \(!flags\.key\) process\.exit\(1\);/);
79
+ assert.match(block, /await import\('\.\.\/src\/init\.js'\)/);
80
+ assert.match(block, /reprovisionKeyConsumers\(flags\.key, flags\.name \|\| 'my-agent'\)/);
81
+ });
82
+ });