From 26fc3ff3228c956473911a2ae62404c9d3870aab Mon Sep 17 00:00:00 2001 From: KeXin95 Date: Sat, 25 Apr 2026 22:12:09 -0700 Subject: [PATCH 1/2] feat: pass ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN into agent containers Users with a custom Anthropic-compatible endpoint (ANTHROPIC_BASE_URL) were getting 401s because the OneCLI proxy injects ANTHROPIC_API_KEY=placeholder and forwards to api.anthropic.com, overriding the custom endpoint and key. Add a claude provider host config that reads ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, and CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC from .env and passes them into the container. Also sets NO_PROXY for the custom host so the OneCLI proxy doesn't intercept those requests. Co-Authored-By: Claude Sonnet 4.6 --- src/providers/claude.ts | 16 ++++++++++++++++ src/providers/index.ts | 1 + 2 files changed, 17 insertions(+) create mode 100644 src/providers/claude.ts diff --git a/src/providers/claude.ts b/src/providers/claude.ts new file mode 100644 index 0000000..7252da8 --- /dev/null +++ b/src/providers/claude.ts @@ -0,0 +1,16 @@ +import { readEnvFile } from '../env.js'; +import { registerProviderContainerConfig } from './provider-container-registry.js'; + +registerProviderContainerConfig('claude', () => { + const dotenv = readEnvFile(['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC']); + const env: Record = {}; + if (dotenv.ANTHROPIC_BASE_URL) { + env.ANTHROPIC_BASE_URL = dotenv.ANTHROPIC_BASE_URL; + const host = new URL(dotenv.ANTHROPIC_BASE_URL).hostname; + 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 }; +}); diff --git a/src/providers/index.ts b/src/providers/index.ts index 3ec9512..1a3a638 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -4,3 +4,4 @@ // needs (claude, mock) don't appear here. // // Skills add a new provider by appending one import line below. +import './claude.js'; From 6591062fbb9e2bff5fefb57fbfe3ab4df48ae150 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Mon, 27 Apr 2026 00:34:31 +0300 Subject: [PATCH 2/2] refactor: route custom Anthropic endpoint through OneCLI vault MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- setup/auto.ts | 98 +++++++++++++++++++++++++++++++++++++++ setup/lib/setup-config.ts | 10 ++++ src/providers/claude.ts | 24 +++++++--- src/providers/index.ts | 1 - 4 files changed, 126 insertions(+), 7 deletions(-) diff --git a/setup/auto.ts b/setup/auto.ts index 91f9bc1..8c78d64 100644 --- a/setup/auto.ts +++ b/setup/auto.ts @@ -22,6 +22,8 @@ * headless `claude -p` call for IANA-zone resolution. */ import { spawn, spawnSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; import * as p from '@clack/prompts'; import k from 'kleur'; @@ -636,6 +638,16 @@ async function runAuthStep(): Promise { 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( await brightSelect({ message: 'How would you like to connect to Claude?', @@ -741,6 +753,92 @@ async function runPasteAuth(method: 'oauth' | 'api'): Promise { } } +/** + * 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 { + 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 ───────────────────────────────────────────────────── /** diff --git a/setup/lib/setup-config.ts b/setup/lib/setup-config.ts index 0a59731..1fa6ad4 100644 --- a/setup/lib/setup-config.ts +++ b/setup/lib/setup-config.ts @@ -95,6 +95,16 @@ export const CONFIG: Entry[] = [ placeholder: 'https://api.anthropic.com', 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. { diff --git a/src/providers/claude.ts b/src/providers/claude.ts index 7252da8..e61d721 100644 --- a/src/providers/claude.ts +++ b/src/providers/claude.ts @@ -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 { registerProviderContainerConfig } from './provider-container-registry.js'; 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 = {}; if (dotenv.ANTHROPIC_BASE_URL) { env.ANTHROPIC_BASE_URL = dotenv.ANTHROPIC_BASE_URL; - const host = new URL(dotenv.ANTHROPIC_BASE_URL).hostname; - env.NO_PROXY = host; - env.no_proxy = host; + env.ANTHROPIC_AUTH_TOKEN = 'placeholder'; } - 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 }; }); diff --git a/src/providers/index.ts b/src/providers/index.ts index 1a3a638..3ec9512 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -4,4 +4,3 @@ // needs (claude, mock) don't appear here. // // Skills add a new provider by appending one import line below. -import './claude.js';