refactor: route custom Anthropic endpoint through OneCLI vault
The original approach passed ANTHROPIC_AUTH_TOKEN into the container
as an env var and disabled the proxy for the custom host (NO_PROXY) —
which works, but bypasses OneCLI entirely for that credential. The
container holds the raw secret, the gateway loses audit/rotation, and
we lose the rest of the vault's protections for this cohort.
OneCLI-native version: store the token as a generic secret with header
injection (--header-name Authorization --value-format 'Bearer {value}'
+ host-pattern matching the base URL hostname). The container only
needs ANTHROPIC_BASE_URL plus a placeholder ANTHROPIC_AUTH_TOKEN — the
proxy rewrites the Authorization header on the wire.
setup/lib/setup-config.ts — adds --anthropic-auth-token alongside the
existing --anthropic-base-url.
setup/auto.ts — runAuthStep short-circuits the auth-method prompt when
both NANOCLAW_ANTHROPIC_BASE_URL and NANOCLAW_ANTHROPIC_AUTH_TOKEN are
set: creates the OneCLI generic secret, writes ANTHROPIC_BASE_URL to
.env (so the runtime reads it), and appends `import './claude.js';` to
src/providers/index.ts (so the provider only registers when the user
has configured a custom endpoint — no branching for everyone else).
src/providers/claude.ts — drops ANTHROPIC_AUTH_TOKEN/NO_PROXY
passthrough. Reads ANTHROPIC_BASE_URL from .env, sets a placeholder
ANTHROPIC_AUTH_TOKEN in container env so the SDK includes an
Authorization header for OneCLI to overwrite.
src/providers/index.ts — removes the unconditional import; setup
appends it on demand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,8 @@
|
|||||||
* headless `claude -p` call for IANA-zone resolution.
|
* headless `claude -p` call for IANA-zone resolution.
|
||||||
*/
|
*/
|
||||||
import { spawn, spawnSync } from 'child_process';
|
import { spawn, spawnSync } from 'child_process';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
import * as p from '@clack/prompts';
|
import * as p from '@clack/prompts';
|
||||||
import k from 'kleur';
|
import k from 'kleur';
|
||||||
@@ -636,6 +638,16 @@ async function runAuthStep(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom Anthropic-compatible endpoint flow. Both URL and token must be set;
|
||||||
|
// OneCLI stores the token as a generic Bearer secret keyed to the URL host,
|
||||||
|
// so the container only ever sees ANTHROPIC_BASE_URL + a placeholder.
|
||||||
|
const customBaseUrl = process.env.NANOCLAW_ANTHROPIC_BASE_URL?.trim();
|
||||||
|
const customAuthToken = process.env.NANOCLAW_ANTHROPIC_AUTH_TOKEN?.trim();
|
||||||
|
if (customBaseUrl && customAuthToken) {
|
||||||
|
await runCustomEndpointAuth(customBaseUrl, customAuthToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const method = ensureAnswer(
|
const method = ensureAnswer(
|
||||||
await brightSelect({
|
await brightSelect({
|
||||||
message: 'How would you like to connect to Claude?',
|
message: 'How would you like to connect to Claude?',
|
||||||
@@ -741,6 +753,92 @@ async function runPasteAuth(method: 'oauth' | 'api'): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up Anthropic auth for a custom endpoint. The token is stored as a
|
||||||
|
* OneCLI generic secret with header injection so the proxy rewrites the
|
||||||
|
* Authorization header on the wire — the container only ever sees
|
||||||
|
* ANTHROPIC_BASE_URL + a placeholder bearer.
|
||||||
|
*/
|
||||||
|
async function runCustomEndpointAuth(
|
||||||
|
baseUrl: string,
|
||||||
|
token: string,
|
||||||
|
): Promise<void> {
|
||||||
|
let host: string;
|
||||||
|
try {
|
||||||
|
host = new URL(baseUrl).hostname;
|
||||||
|
} catch {
|
||||||
|
await fail(
|
||||||
|
'auth',
|
||||||
|
`Invalid Anthropic base URL: ${baseUrl}`,
|
||||||
|
'Check --anthropic-base-url and retry.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await runQuietChild(
|
||||||
|
'auth',
|
||||||
|
'onecli',
|
||||||
|
[
|
||||||
|
'secrets',
|
||||||
|
'create',
|
||||||
|
'--name',
|
||||||
|
'Anthropic',
|
||||||
|
'--type',
|
||||||
|
'generic',
|
||||||
|
'--value',
|
||||||
|
token,
|
||||||
|
'--host-pattern',
|
||||||
|
host,
|
||||||
|
'--header-name',
|
||||||
|
'Authorization',
|
||||||
|
'--value-format',
|
||||||
|
'Bearer {value}',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
running: `Saving your Anthropic auth token to your OneCLI vault…`,
|
||||||
|
done: 'Claude account connected.',
|
||||||
|
},
|
||||||
|
{ extraFields: { METHOD: 'custom-endpoint', HOST: host } },
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
await fail(
|
||||||
|
'auth',
|
||||||
|
`Couldn't save your Anthropic auth token to the vault.`,
|
||||||
|
'Make sure OneCLI is running (`onecli version`), then retry.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ANTHROPIC_BASE_URL has to be in .env so the runtime provider config
|
||||||
|
// reads it when building container env. The token is *not* written —
|
||||||
|
// OneCLI holds it.
|
||||||
|
writeEnvLine('ANTHROPIC_BASE_URL', baseUrl);
|
||||||
|
|
||||||
|
// Register the claude provider so the runtime passes ANTHROPIC_BASE_URL
|
||||||
|
// and the placeholder bearer into the container. Only appended when the
|
||||||
|
// user has configured a custom endpoint; standard installs don't load
|
||||||
|
// the file at all.
|
||||||
|
appendProviderImport('./claude.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeEnvLine(key: string, value: string): void {
|
||||||
|
const envFile = path.join(process.cwd(), '.env');
|
||||||
|
const content = fs.existsSync(envFile) ? fs.readFileSync(envFile, 'utf-8') : '';
|
||||||
|
const re = new RegExp(`^${key}=.*$`, 'm');
|
||||||
|
const next = re.test(content)
|
||||||
|
? content.replace(re, `${key}=${value}`)
|
||||||
|
: content.trimEnd() + (content ? '\n' : '') + `${key}=${value}\n`;
|
||||||
|
fs.writeFileSync(envFile, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendProviderImport(modulePath: string): void {
|
||||||
|
const file = path.join(process.cwd(), 'src', 'providers', 'index.ts');
|
||||||
|
const content = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : '';
|
||||||
|
const line = `import '${modulePath}';`;
|
||||||
|
if (content.includes(line)) return;
|
||||||
|
const sep = content && !content.endsWith('\n') ? '\n' : '';
|
||||||
|
fs.writeFileSync(file, content + sep + line + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
// ─── timezone step ─────────────────────────────────────────────────────
|
// ─── timezone step ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -95,6 +95,16 @@ export const CONFIG: Entry[] = [
|
|||||||
placeholder: 'https://api.anthropic.com',
|
placeholder: 'https://api.anthropic.com',
|
||||||
validate: httpUrl,
|
validate: httpUrl,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'anthropicAuthToken',
|
||||||
|
label: 'Anthropic auth token',
|
||||||
|
help: 'Bearer token for the custom Anthropic endpoint. Used together with --anthropic-base-url.',
|
||||||
|
surface: 'flag+ui',
|
||||||
|
group: 'Anthropic',
|
||||||
|
type: 'string',
|
||||||
|
secret: true,
|
||||||
|
validate: (v) => (v.trim() ? undefined : 'Required'),
|
||||||
|
},
|
||||||
|
|
||||||
// Existing env-var knobs — flag-only so they don't clutter the UI screen.
|
// Existing env-var knobs — flag-only so they don't clutter the UI screen.
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Claude provider container config — only registered when the user has
|
||||||
|
* configured a custom Anthropic-compatible endpoint via setup. Setup
|
||||||
|
* appends `import './claude.js'` to providers/index.ts at that point;
|
||||||
|
* standard installs hitting api.anthropic.com don't need this file
|
||||||
|
* loaded.
|
||||||
|
*
|
||||||
|
* The real auth token never enters the container. Setup creates an
|
||||||
|
* OneCLI generic secret (host-pattern = base URL hostname, header-name
|
||||||
|
* = Authorization, value-format = "Bearer {value}") so the proxy
|
||||||
|
* rewrites the Authorization header on the wire. The container only
|
||||||
|
* needs:
|
||||||
|
* - ANTHROPIC_BASE_URL — so the SDK knows where to call
|
||||||
|
* - ANTHROPIC_AUTH_TOKEN=placeholder — so the SDK adds an
|
||||||
|
* Authorization: Bearer header for OneCLI to overwrite
|
||||||
|
*/
|
||||||
import { readEnvFile } from '../env.js';
|
import { readEnvFile } from '../env.js';
|
||||||
import { registerProviderContainerConfig } from './provider-container-registry.js';
|
import { registerProviderContainerConfig } from './provider-container-registry.js';
|
||||||
|
|
||||||
registerProviderContainerConfig('claude', () => {
|
registerProviderContainerConfig('claude', () => {
|
||||||
const dotenv = readEnvFile(['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC']);
|
const dotenv = readEnvFile(['ANTHROPIC_BASE_URL']);
|
||||||
const env: Record<string, string> = {};
|
const env: Record<string, string> = {};
|
||||||
if (dotenv.ANTHROPIC_BASE_URL) {
|
if (dotenv.ANTHROPIC_BASE_URL) {
|
||||||
env.ANTHROPIC_BASE_URL = dotenv.ANTHROPIC_BASE_URL;
|
env.ANTHROPIC_BASE_URL = dotenv.ANTHROPIC_BASE_URL;
|
||||||
const host = new URL(dotenv.ANTHROPIC_BASE_URL).hostname;
|
env.ANTHROPIC_AUTH_TOKEN = 'placeholder';
|
||||||
env.NO_PROXY = host;
|
|
||||||
env.no_proxy = host;
|
|
||||||
}
|
}
|
||||||
if (dotenv.ANTHROPIC_AUTH_TOKEN) env.ANTHROPIC_AUTH_TOKEN = dotenv.ANTHROPIC_AUTH_TOKEN;
|
|
||||||
if (dotenv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC) env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = dotenv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
|
|
||||||
return { env };
|
return { env };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,4 +4,3 @@
|
|||||||
// needs (claude, mock) don't appear here.
|
// needs (claude, mock) don't appear here.
|
||||||
//
|
//
|
||||||
// Skills add a new provider by appending one import line below.
|
// Skills add a new provider by appending one import line below.
|
||||||
import './claude.js';
|
|
||||||
|
|||||||
Reference in New Issue
Block a user