From 4a7d79c3308d0606a15b35adcad5c8b09d9d2ab0 Mon Sep 17 00:00:00 2001 From: dtzp555-max Date: Sun, 31 May 2026 22:44:15 +1000 Subject: [PATCH] fix: record OAuth-host verification + models.json SPOT for usage-probe/default (#112) (#119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three alignment/SPOT findings from the 2026-05-31 audit: 1. The OAuth token-refresh host (platform.claude.com/v1/oauth/token, a Class A surface) was introduced in the 2026-04-11 drift commit and had no verification record. Verified against the compiled cli.js (claude.exe v2.1.154) via `strings`: OAUTH_TOKEN_URL and OAUTH_CLIENT_ID both appear in the binary byte-for-byte (in the same `prod` config object), and the legacy host console.anthropic.com/v1/oauth is absent (0 hits). Recorded this as an inline ALIGNMENT citation comment above the constants. No live OAuth probe was run — a refresh-token grant would rotate the operator's real credentials; the strings-on-binary evidence is decisive. (cli.js: verified against compiled claude.exe v2.1.154, 2026-05-31.) 2. fetchUsageFromApi() hardcoded the haiku model ID; now derives from modelsConfig.aliases.haiku (ADR 0003 SPOT). Prevents a silent /usage break on a future haiku ID bump. 3. [P3] The default request model hardcoded the sonnet ID; now derives from modelsConfig.aliases.sonnet (ADR 0003 SPOT). Both SPOT values are byte-identical to the literals they replace today, so zero behavior change — only drift-resistance. The alignment.yml blacklist pin for the wrong-host variant was deliberately NOT included here: extending the blacklist is a governance-layer change (alignment.yml inline policy) and belongs in its own PR. ALIGNMENT.md: finding 1 IS the verification (cli.js citation recorded inline); findings 2-3 are SPOT hygiene that forward no new operation. No blacklisted tokens or port literals introduced; alignment.yml passes. Independent fresh-context reviewer (opus) INDEPENDENTLY re-ran `strings` on the binary and confirmed the host/client_id present and the legacy host absent — APPROVE (Iron Rule 10; alignment hard-requirement #3 satisfied). Closes #112. Co-authored-by: dtzp555 Co-authored-by: Claude Opus 4.8 --- CHANGELOG.md | 4 ++++ server.mjs | 10 ++++++++-- test-features.mjs | 21 +++++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9e580a..71d0a38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fix + +- **#112** — Recorded OAuth-host verification against compiled cli.js v2.1.154 (ALIGNMENT Class A); usage-probe and default request model now derive from `models.json` (ADR 0003 SPOT) instead of hardcoded IDs. + ### Security - **#109 P0** — `/health` no longer advertises `PROXY_ANONYMOUS_KEY` to remote callers by default. The `anonymousKey` field is now gated behind a new `PROXY_ADVERTISE_ANON_KEY=1` opt-in env var; localhost callers are always exempt. This prevents any LAN-reachable device from harvesting a working bearer credential from the unauthenticated `/health` endpoint. diff --git a/server.mjs b/server.mjs index da3fa7b..cdb732c 100644 --- a/server.mjs +++ b/server.mjs @@ -1222,6 +1222,12 @@ function streamStringAsSSE(res, id, model, content) { let usageCache = { data: null, fetchedAt: 0 }; const USAGE_CACHE_TTL = 5 * 60 * 1000; // 5 min +// ALIGNMENT (Class A — OAuth bearer machinery). Verified against the compiled cli.js +// (claude.exe v2.1.154) on 2026-05-31 via `strings`: both OAUTH_CLIENT_ID and +// OAUTH_TOKEN_URL appear in the binary byte-for-byte; the legacy host +// console.anthropic.com/v1/oauth is absent (0 hits). Re-verify on cli.js major bumps +// using the compiled-binary protocol (strings on the Mach-O/ELF; no live OAuth probe — +// a refresh-token grant would rotate the operator's real credentials). (issue #112) const OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"; const OAUTH_TOKEN_URL = "https://platform.claude.com/v1/oauth/token"; @@ -1329,7 +1335,7 @@ async function fetchUsageFromApi() { // Minimal /v1/messages request — we only need the response headers. // Mirrors Claude Code cli.js vE4: headers anthropic-ratelimit-unified-{5h,7d}-{utilization,reset}. const body = JSON.stringify({ - model: "claude-haiku-4-5-20251001", + model: modelsConfig.aliases.haiku, max_tokens: 1, messages: [{ role: "user", content: "." }], }); @@ -1670,7 +1676,7 @@ async function handleChatCompletions(req, res) { try { parsed = JSON.parse(body); } catch { return jsonResponse(res, 400, { error: "Invalid JSON" }); } const messages = parsed.messages || parsed.input || [{ role: "user", content: parsed.prompt || "" }]; - const model = parsed.model || "claude-sonnet-4-6"; + const model = parsed.model || modelsConfig.aliases.sonnet; const stream = parsed.stream; // Validate model against known models diff --git a/test-features.mjs b/test-features.mjs index a2ce6f6..ffc36e8 100644 --- a/test-features.mjs +++ b/test-features.mjs @@ -1654,6 +1654,27 @@ test("sanitizeError: multiple paths all stripped", () => { assert.ok(result.includes("[path]"), `expected [path] in: ${result}`); }); +// ── models.json SPOT wiring (issue #112) ──────────────────────────────────── +// Asserts that the alias values used by server.mjs (usage probe + default model) +// match the expected IDs. A future alias rename that silently breaks these +// code paths is caught here. +import { readFileSync as spotReadFileSync } from "node:fs"; +import { fileURLToPath as spotFileURLToPath } from "node:url"; +import { dirname as spotDirname, join as spotJoin } from "node:path"; + +console.log("\nmodels.json SPOT aliases (issue #112):"); + +const _spotDir = spotDirname(spotFileURLToPath(import.meta.url)); +const _spotModels = JSON.parse(spotReadFileSync(spotJoin(_spotDir, "models.json"), "utf8")); + +test("models.json aliases.haiku === 'claude-haiku-4-5-20251001' (usage-probe SPOT)", () => { + assert.equal(_spotModels.aliases.haiku, "claude-haiku-4-5-20251001"); +}); + +test("models.json aliases.sonnet === 'claude-sonnet-4-6' (default-request-model SPOT)", () => { + assert.equal(_spotModels.aliases.sonnet, "claude-sonnet-4-6"); +}); + // ── Cleanup ── closeDb();