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 +42 -2
- package/package.json +1 -1
- package/src/tests/update-reprovision.test.js +82 -0
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
|
|
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
|
|
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.
|
|
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
|
+
});
|