troxy-cli 1.29.0 → 1.29.1
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/package.json +1 -1
- package/src/init.js +18 -7
- package/src/tests/claude-code-proxy.test.js +34 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "troxy-cli",
|
|
3
|
-
"version": "1.29.
|
|
3
|
+
"version": "1.29.1",
|
|
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": {
|
package/src/init.js
CHANGED
|
@@ -100,7 +100,7 @@ export async function reprovisionKeyConsumers(key, agentName, proxyOptIn = null)
|
|
|
100
100
|
} catch (err) {
|
|
101
101
|
console.log(` • Claude Code usage capture ✗ (${err.message})`);
|
|
102
102
|
}
|
|
103
|
-
await maybeEnableClaudeCodeProxy(proxyOptIn);
|
|
103
|
+
await maybeEnableClaudeCodeProxy(proxyOptIn, key);
|
|
104
104
|
}
|
|
105
105
|
console.log('\n Restart your MCP client to activate Troxy.');
|
|
106
106
|
}
|
|
@@ -583,12 +583,24 @@ export function troxyModelProxyBaseUrl() {
|
|
|
583
583
|
// 2. Remote Control is disabled while the base URL points elsewhere
|
|
584
584
|
// (Claude Code v2.1.196+) - nothing to configure around this one, it's
|
|
585
585
|
// just true while the proxy is active, hence the up-front warning.
|
|
586
|
-
export function patchClaudeCodeProxy(configPath, baseUrl = troxyModelProxyBaseUrl()) {
|
|
586
|
+
export function patchClaudeCodeProxy(configPath, troxyKey, baseUrl = troxyModelProxyBaseUrl()) {
|
|
587
587
|
let config = {};
|
|
588
588
|
try { config = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch {}
|
|
589
589
|
if (!config.env) config.env = {};
|
|
590
590
|
config.env.ANTHROPIC_BASE_URL = baseUrl;
|
|
591
591
|
config.env.ENABLE_TOOL_SEARCH = 'true';
|
|
592
|
+
// Found live 2026-09-04: without this, Claude Code still authenticates to
|
|
593
|
+
// whatever base_url it's pointed at using its own Anthropic credential (an
|
|
594
|
+
// API key sent as x-api-key, or a Claude.ai OAuth session token sent as
|
|
595
|
+
// Authorization) - neither is a Troxy key, so the model-proxy's
|
|
596
|
+
// authenticate_api_key(troxy_key) check (reads the Authorization header)
|
|
597
|
+
// rejected every real request as "invalid or revoked Troxy API key",
|
|
598
|
+
// silently making the whole proxy path a no-op regardless of base URL.
|
|
599
|
+
// ANTHROPIC_AUTH_TOKEN is the one Claude Code env var that maps directly
|
|
600
|
+
// onto Authorization: Bearer <value> (same docs page as above) - setting
|
|
601
|
+
// it to the Troxy key is what the model-proxy actually needs to identify
|
|
602
|
+
// the caller.
|
|
603
|
+
if (troxyKey) config.env.ANTHROPIC_AUTH_TOKEN = troxyKey;
|
|
592
604
|
|
|
593
605
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
594
606
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
@@ -601,7 +613,7 @@ export function patchClaudeCodeProxy(configPath, baseUrl = troxyModelProxyBaseUr
|
|
|
601
613
|
// proxyOptIn === null -> not passed; ask interactively if there's a TTY,
|
|
602
614
|
// otherwise skip (a scripted/CI init must never hang
|
|
603
615
|
// on a prompt, and silence must never mean yes)
|
|
604
|
-
async function maybeEnableClaudeCodeProxy(proxyOptIn) {
|
|
616
|
+
async function maybeEnableClaudeCodeProxy(proxyOptIn, key) {
|
|
605
617
|
let enable = proxyOptIn === true;
|
|
606
618
|
if (proxyOptIn === null && process.stdin.isTTY) {
|
|
607
619
|
console.log("\n Route Claude Code's model calls through Troxy for automatic cost");
|
|
@@ -609,15 +621,14 @@ async function maybeEnableClaudeCodeProxy(proxyOptIn) {
|
|
|
609
621
|
console.log(' opportunity, not just suggest one.');
|
|
610
622
|
console.log(" Trade-off: while this is on, Claude Code's Remote Control feature is");
|
|
611
623
|
console.log(' disabled (Anthropic disables it whenever the API base URL points');
|
|
612
|
-
console.log(' anywhere other than api.anthropic.com).
|
|
613
|
-
console.log('
|
|
614
|
-
console.log(' only the cost-saving model swap is live.');
|
|
624
|
+
console.log(' anywhere other than api.anthropic.com). Model policies (e.g. a BLOCK');
|
|
625
|
+
console.log(' rule on a specific model) ARE enforced on this path.');
|
|
615
626
|
const answer = await prompt(' Enable? (y/N): ');
|
|
616
627
|
enable = /^y(es)?$/i.test(answer);
|
|
617
628
|
}
|
|
618
629
|
if (!enable) return;
|
|
619
630
|
try {
|
|
620
|
-
patchClaudeCodeProxy(claudeCodeSettingsPath());
|
|
631
|
+
patchClaudeCodeProxy(claudeCodeSettingsPath(), key);
|
|
621
632
|
console.log(` • Claude Code model proxy (cost optimization) ✓`);
|
|
622
633
|
} catch (err) {
|
|
623
634
|
console.log(` • Claude Code model proxy ✗ (${err.message})`);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// Checklist #17 ("one-command proxy setup"): patchClaudeCodeProxy writes
|
|
2
|
-
// ANTHROPIC_BASE_URL + ENABLE_TOOL_SEARCH into Claude
|
|
3
|
-
// settings.json `env` block, so its own model calls route
|
|
4
|
-
// proxy.troxy.io instead of straight to Anthropic
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
2
|
+
// ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN + ENABLE_TOOL_SEARCH into Claude
|
|
3
|
+
// Code's global settings.json `env` block, so its own model calls route
|
|
4
|
+
// through proxy.troxy.io instead of straight to Anthropic, AND authenticate
|
|
5
|
+
// as this Troxy user once they get there. This is opt-in only (Gilad,
|
|
6
|
+
// 2026-08-29) - the prompt/flag gating that decides WHETHER to call this
|
|
7
|
+
// lives in reprovisionKeyConsumers/maybeEnableClaudeCodeProxy, not tested
|
|
8
|
+
// here; this file covers what actually gets written once it's called, same
|
|
9
|
+
// split as claude-code-hook.test.js does for the hooks patch.
|
|
9
10
|
|
|
10
11
|
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
11
12
|
import assert from 'node:assert/strict';
|
|
@@ -22,28 +23,45 @@ afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
|
|
|
22
23
|
describe('patchClaudeCodeProxy', () => {
|
|
23
24
|
const configPath = () => path.join(dir, 'settings.json');
|
|
24
25
|
const read = () => JSON.parse(fs.readFileSync(configPath(), 'utf8'));
|
|
26
|
+
const KEY = 'txy-test-key-123';
|
|
25
27
|
|
|
26
28
|
it('creates the env block with ANTHROPIC_BASE_URL when the file does not exist', () => {
|
|
27
|
-
patchClaudeCodeProxy(configPath());
|
|
29
|
+
patchClaudeCodeProxy(configPath(), KEY);
|
|
28
30
|
const cfg = read();
|
|
29
31
|
assert.equal(cfg.env.ANTHROPIC_BASE_URL, troxyModelProxyBaseUrl());
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
it('sets ENABLE_TOOL_SEARCH so the model-proxy\'s forwarded tool_reference blocks still work', () => {
|
|
33
|
-
patchClaudeCodeProxy(configPath());
|
|
35
|
+
patchClaudeCodeProxy(configPath(), KEY);
|
|
34
36
|
const cfg = read();
|
|
35
37
|
assert.equal(cfg.env.ENABLE_TOOL_SEARCH, 'true');
|
|
36
38
|
});
|
|
37
39
|
|
|
40
|
+
it('sets ANTHROPIC_AUTH_TOKEN to the Troxy key - found live 2026-09-04: without this, ' +
|
|
41
|
+
'Claude Code sends its own Anthropic credential as Authorization, and the model-proxy ' +
|
|
42
|
+
'rejects every real request as an invalid Troxy key, making the whole proxy path a no-op',
|
|
43
|
+
() => {
|
|
44
|
+
patchClaudeCodeProxy(configPath(), KEY);
|
|
45
|
+
const cfg = read();
|
|
46
|
+
assert.equal(cfg.env.ANTHROPIC_AUTH_TOKEN, KEY);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('does not write ANTHROPIC_AUTH_TOKEN at all when no key is given, rather than writing "undefined"', () => {
|
|
50
|
+
patchClaudeCodeProxy(configPath());
|
|
51
|
+
const cfg = read();
|
|
52
|
+
assert.equal(cfg.env.ANTHROPIC_AUTH_TOKEN, undefined);
|
|
53
|
+
});
|
|
54
|
+
|
|
38
55
|
it('accepts a custom base URL for testing/overrides instead of hardcoding proxy.troxy.io', () => {
|
|
39
|
-
patchClaudeCodeProxy(configPath(), 'https://staging.example.com');
|
|
56
|
+
patchClaudeCodeProxy(configPath(), KEY, 'https://staging.example.com');
|
|
40
57
|
const cfg = read();
|
|
41
58
|
assert.equal(cfg.env.ANTHROPIC_BASE_URL, 'https://staging.example.com');
|
|
59
|
+
assert.equal(cfg.env.ANTHROPIC_AUTH_TOKEN, KEY);
|
|
42
60
|
});
|
|
43
61
|
|
|
44
62
|
it('preserves unrelated existing env keys instead of replacing the whole block', () => {
|
|
45
63
|
fs.writeFileSync(configPath(), JSON.stringify({ env: { SOME_OTHER_VAR: 'keep-me' } }));
|
|
46
|
-
patchClaudeCodeProxy(configPath());
|
|
64
|
+
patchClaudeCodeProxy(configPath(), KEY);
|
|
47
65
|
const cfg = read();
|
|
48
66
|
assert.equal(cfg.env.SOME_OTHER_VAR, 'keep-me');
|
|
49
67
|
assert.equal(cfg.env.ANTHROPIC_BASE_URL, troxyModelProxyBaseUrl());
|
|
@@ -51,21 +69,22 @@ describe('patchClaudeCodeProxy', () => {
|
|
|
51
69
|
|
|
52
70
|
it('preserves unrelated top-level keys (e.g. hooks already written by patchClaudeCodeHooks)', () => {
|
|
53
71
|
fs.writeFileSync(configPath(), JSON.stringify({ hooks: { Stop: [{ hooks: [{ command: 'x' }] }] } }));
|
|
54
|
-
patchClaudeCodeProxy(configPath());
|
|
72
|
+
patchClaudeCodeProxy(configPath(), KEY);
|
|
55
73
|
const cfg = read();
|
|
56
74
|
assert.ok(cfg.hooks.Stop.length === 1, 'existing hooks block was dropped');
|
|
57
75
|
assert.equal(cfg.env.ANTHROPIC_BASE_URL, troxyModelProxyBaseUrl());
|
|
58
76
|
});
|
|
59
77
|
|
|
60
78
|
it('re-running is idempotent - overwrites rather than duplicating or erroring', () => {
|
|
61
|
-
patchClaudeCodeProxy(configPath());
|
|
62
|
-
patchClaudeCodeProxy(configPath());
|
|
79
|
+
patchClaudeCodeProxy(configPath(), KEY);
|
|
80
|
+
patchClaudeCodeProxy(configPath(), KEY);
|
|
63
81
|
const cfg = read();
|
|
64
82
|
assert.equal(cfg.env.ANTHROPIC_BASE_URL, troxyModelProxyBaseUrl());
|
|
83
|
+
assert.equal(cfg.env.ANTHROPIC_AUTH_TOKEN, KEY);
|
|
65
84
|
});
|
|
66
85
|
|
|
67
86
|
it('does not throw when the existing file is corrupted JSON', () => {
|
|
68
87
|
fs.writeFileSync(configPath(), '{ not json');
|
|
69
|
-
assert.doesNotThrow(() => patchClaudeCodeProxy(configPath()));
|
|
88
|
+
assert.doesNotThrow(() => patchClaudeCodeProxy(configPath(), KEY));
|
|
70
89
|
});
|
|
71
90
|
});
|