From 1605400052a3f79995eb41b3e41b44719ac6e0b5 Mon Sep 17 00:00:00 2001 From: dtzp555-max Date: Tue, 26 May 2026 16:30:09 +1000 Subject: [PATCH] =?UTF-8?q?docs:=20D79=20=E2=80=94=20Phase=205=20constitut?= =?UTF-8?q?ional=20layer=20(ADR=200012=20+=20ADR=200002=20Amendment=208=20?= =?UTF-8?q?+=20ADR=200013)=20(#50)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: D79 — Phase 5 constitutional layer (ADR 0012 + ADR 0002 Amendment 8 + ADR 0013) Three coupled governance documents land together as the Phase 5 constitutional layer (Iron Rule 11 IDR — reviewing them separately cannot verify consumer-producer alignment). Phase 5 opens 2026-05-26; D79 is governance-only, no code changes. - ADR 0012 (Phase 5 charter) — port OCP's plan-usage probe to `lib/providers/anthropic.mjs:quotaStatus()` (D80) + extend `/v0/management/dashboard-data` for new shape (D81) + Claude.ai-style dashboard restructure with 1-min auto-refresh + manual refresh (D82) + Suite 38/39 tests (D83) + optional mistral probe at D84 (codex skipped — no public API) + v0.5.0 close (maintainer-triggered). ~6 D-days. - ADR 0002 Amendment 8 (direct-API READ-ONLY exemption) — plugin contract amendment permitting quotaStatus() to call provider HTTP APIs directly, subject to three constraints: READ-ONLY (no mutating calls), subscription-scope (reuses spawn-path credentials), idempotent failure (returns null on any error, never throws). No other contract method gains this permission. - ADR 0013 (OAuth READ-ONLY consumption + schema-drift mitigation) — implementation discipline for ADR 0002 Amendment 8. Seven rules: (1) credential reuse via plugin's readAuthArtifact(), (2) READ-ONLY at wire (max_tokens:1, headers-only parse, body discarded), (3) cache TTL 5min + 60s-3600s exponential refresh backoff + stale-cache-on-failure, (4) opt-in via `~/.olp/config.json providers..quota_probe_enabled` (default false), (5) schema-drift mitigation via dual-path verification (compiled-binary `strings` + live API probe diff), (6) failure transparency through `olp doctor` + dashboard staleness markers, (7) explicit out-of-scope clarifications. Pre-flight institutional-knowledge audit (Iron Rule 12 prior-art search) captured at `~/.cc-rules/memory/learnings/anthropic_plan_usage_probe_schema_2026_05_26.md` (cross-machine git-sync). Findings: - OCP probe (server.mjs:842-1109) still works against current api.anthropic.com — tested live from PI231 OAuth credentials 2026-05-26. - 13 `anthropic-ratelimit-unified-*` response headers confirmed (3 new since OCP 2026-04 capture: 5h-status, 7d-status, overage-reset; no removals or renames). - Claude Code v2.1.x is now distributed as compiled native binary (Mach-O on macOS, ELF on Linux) — OCP's "grep cli.js" verification is no longer applicable. ADR 0013 Rule 5 replaces with dual-path verification (`strings` over the binary + live API probe diff). - OAuth refresh path (platform.claude.com/v1/oauth/token + client_id 9d1c250a-...) all unchanged. Authority: - ALIGNMENT.md Rule 1 (citation): audit memory + OCP server.mjs:842-1109 + live `/v1/messages` probe transcript 2026-05-26. - ALIGNMENT.md Rule 2 (provider-CLI-as-authority): Amendment 8 documents the exemption; the probe mirrors observed CLI behaviour. - ALIGNMENT.md Rule 5 (CI alignment.yml): not triggered (docs/ excluded by workflow `paths:` filter); blacklisted `/api/oauth/usage` token referenced only as meta-references ("must continue to blacklist"). - CLAUDE.md release_kit overlay: Phase 5 open; D-day commits stay under "Unreleased" until maintainer-triggered v0.5.0 close. Iron Rule 10: fresh-context reviewer required before merge per CLAUDE.md hard requirement #3. Co-Authored-By: Claude Opus 4.7 * docs: D79 fold-in — 6 in-PR nits from fresh-context reviewer (PR #50) Reviewer verdict: APPROVE_WITH_MINOR (0 blocking, 9 nits — 6 in-PR, 3 outside-PR). Folding in the 6 in-PR nits here; the 3 outside-PR ones (audit-memory chronology, audit-memory D80/D81 mapping, ALIGNMENT.md cross-ref to Amendment 8) are deferred. Folded-in nits: 1. ADR 0002 Amendment 8: added a 5th "does NOT permit" bullet making per-endpoint containment explicit. Amendment 8 permits the kind of call; ADR 0013 Rule 2 enumerates which specific endpoint. Re-opening per-endpoint scope requires an ADR 0013 amendment, not a Amendment-8-only interpretation. 2. ADR 0013 Rule 5: added "Path A prerequisites" paragraph documenting that strings (GNU/BSD binutils/coreutils) + Claude Code v2.1.x install are required for compiled-binary verification. Windows reviewers need WSL or binutils-mingw. 3. ADR 0013 Rule 5: added "Trigger for re-running the diff" paragraph naming three explicit hooks for major-version-bump detection: Annual Alignment Audit (14 May), olp doctor anthropic.quota_probe_reachable failure, manual maintainer attention. Documented graceful-degradation failure mode. 4. ADR 0012 D80 estimate: 1.5d → 2d. Reviewer flagged 1.5d as optimistic compared to D61-D63 (2.5d for narrower SSE heartbeat scope). Aligning. 5. ADR 0012 D84: moved Mistral GO/NO-GO spike to D79 close (before D80 starts), not mid-phase. Reduces mid-phase scope drift risk. Outcome will be amended into this charter as a D79-close amendment. 6. ADR 0012 Authority + cross-references: replaced "Claude Code § OAuth bearer + ratelimit headers" with "compiled-binary strings evidence per audit memory § Path A". Claude Code v2.1.x has no traditional section structure because it is a Mach-O / ELF compiled binary. Deferred (outside-PR) nits documented in PR review thread: - Audit memory historical-table chronology error (cb6c2a8 placed last; was second chronologically — narrative arc still holds, dates need correction). - Audit memory D80/D81 mapping mismatch (memory says D81 adds new fields; ADR 0012 says D80 parses all 13). - ALIGNMENT.md cross-reference to Amendment 8 (Class-specific Exceptions subsection should name Amendment 8 explicitly). All three outside-PR items are docs-only and not load-bearing for D80 implementation. Will fold in either at D80 commit (audit-memory updates) or as a tiny constitutional cleanup PR (ALIGNMENT.md cross-ref). Iron Rule 10: reviewer was a fresh-context opus subagent; their full review is recorded in PR #50 thread. Co-Authored-By: Claude Opus 4.7 --------- Co-authored-by: dtzp555 Co-authored-by: Claude Opus 4.7 --- CHANGELOG.md | 18 +- docs/adr/0002-plugin-architecture.md | 24 +++ ...-phase-5-charter-quota-probes-dashboard.md | 127 ++++++++++++ ...-read-only-consumption-and-schema-drift.md | 186 ++++++++++++++++++ docs/adr/README.md | 2 + 5 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 docs/adr/0012-phase-5-charter-quota-probes-dashboard.md create mode 100644 docs/adr/0013-oauth-read-only-consumption-and-schema-drift.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f6972c8..8cdca99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,23 @@ All notable changes to OLP land here. Per `CLAUDE.md` release_kit overlay, this ## Unreleased -(empty — Phase 5 entries land here once Phase 5 opens) +### D79 — Phase 5 constitutional layer: charter ADR 0012 + ADR 0002 Amendment 8 + ADR 0013 + +Three coupled governance documents land together as the D79 constitutional layer (Iron Rule 11 IDR — reviewing them separately cannot verify consumer-producer alignment). Phase 5 opens 2026-05-26; no code changes yet. + +- **ADR 0012 (Phase 5 charter).** Scope: port OCP's plan-usage probe to `lib/providers/anthropic.mjs:quotaStatus()` (D80) + extend `/v0/management/dashboard-data` for the new shape (D81) + Claude.ai-style dashboard restructure with 1-min auto-refresh + manual refresh button (D82) + Suite 38/39 quota-probe + dashboard tests (D83) + optional mistral probe at D84 (codex skipped — no public API) + v0.5.0 close (maintainer-triggered). D-day plan covers ~6 D-days. +- **ADR 0002 Amendment 8 (direct-API READ-ONLY exemption).** Plugin contract amendment permitting `quotaStatus()` to call provider HTTP APIs directly, subject to three constraints: READ-ONLY (no mutating calls), subscription-scope (reuses spawn-path credentials), idempotent failure (returns `null` on any error, never throws). No other contract method gains this permission. Cites OCP `server.mjs:842-1109` as the port reference + `~/.cc-rules/memory/learnings/anthropic_plan_usage_probe_schema_2026_05_26.md` as the live schema pin. +- **ADR 0013 (OAuth READ-ONLY consumption + schema-drift mitigation).** Implementation discipline for ADR 0002 Amendment 8. Seven rules: (1) credential reuse via plugin's `readAuthArtifact()` — no new OAuth grant, (2) READ-ONLY at the wire — one probe per cache miss with `max_tokens:1`, headers-only parse, body discarded, (3) cache TTL 5min + 60s-3600s exponential refresh backoff + stale-cache-on-failure, (4) opt-in via `~/.olp/config.json providers..quota_probe_enabled` (default false), (5) schema-drift mitigation via dual-path verification (compiled-binary `strings` + live API probe diff), (6) failure transparency through `olp doctor` + dashboard staleness markers, (7) explicit out-of-scope clarifications. + +**Pre-flight institutional-knowledge audit.** Phase 5 was unblocked by a 2026-05-26 audit per Iron Rule 12 (prior-art search before brainstorming). The audit verified: +- OCP's plan-usage probe (`server.mjs:842-1109`) still works against current `api.anthropic.com` — tested live from PI231 OAuth credentials. +- 13 `anthropic-ratelimit-unified-*` response headers confirmed (3 new fields since OCP's 2026-04 capture: `5h-status`, `7d-status`, `overage-reset`; no removals or renames). +- Claude Code v2.1.x is now distributed as compiled native binary (Mach-O / ELF) — OCP's "grep cli.js" verification is no longer applicable. ADR 0013 Rule 5 replaces it with dual-path verification (compiled-binary `strings` + live API probe diff). +- OAuth refresh path (`platform.claude.com/v1/oauth/token`, client_id `9d1c250a-...`, 60s-3600s exponential backoff) all unchanged. + +Audit memory persists at `~/.cc-rules/memory/learnings/anthropic_plan_usage_probe_schema_2026_05_26.md` (cross-machine git-sync). + +**Authority cited at D79 commit body:** ALIGNMENT.md Rules 1 + 2 + 5; CLAUDE.md release_kit (Phase 5 open); audit memory; OCP server.mjs:842-1109 as port reference; live `/v1/messages` probe transcript from 2026-05-26. ## v0.4.4 — 2026-05-26 diff --git a/docs/adr/0002-plugin-architecture.md b/docs/adr/0002-plugin-architecture.md index 54cba08..af71f60 100644 --- a/docs/adr/0002-plugin-architecture.md +++ b/docs/adr/0002-plugin-architecture.md @@ -9,6 +9,30 @@ > **Note on numbering.** Sequence is 1, 3, 4, 5, 6, 7 — Amendment 2 was never written. The reserved slot was originally planned for a separate `maxConcurrent` ratification, but that content was folded into Amendment 1 (the retroactive contract-sync amendment) at filing time and the gap was not backfilled. The gap is intentional and load-bearing — no missing content; do not renumber Amendments 3+ to close it (cross-references to Amendment N from other docs would silently break). +### Amendment 8 — 2026-05-26: Permit `quotaStatus()` direct-API access (READ-ONLY exemption) for plan-usage probes (D79–D80 — Phase 5) + +- **Context:** ADR 0012 (Phase 5 charter) opens 2026-05-26 to port OCP's plan-usage probe (`ocp/server.mjs:842-1109`) into `lib/providers/anthropic.mjs:quotaStatus()`. The probe calls `POST https://api.anthropic.com/v1/messages` directly with an OAuth bearer and parses `anthropic-ratelimit-unified-*` response headers. This violates the plugin contract's implicit assumption that ALL provider interaction goes through `spawn` (the binary CLI). `ALIGNMENT.md` Rule 2 (provider-CLI-as-authority) further constrains plugins to operations the provider CLI itself performs. The OCP-derived plan-usage probe satisfies neither of these — it bypasses `claude -p` and hits the public API directly. **Without an explicit exemption Amendment, D80 is unalignable.** +- **Why the exemption is sound:** The probe is strictly **READ-ONLY** (one `POST /v1/messages` with `max_tokens: 1`; the response body is discarded; only response headers are parsed) AND **subscription-scope** (the OAuth bearer is the same one Claude Code uses for `claude -p`; no extra grant is requested) AND **idempotent** (probe failure returns `null`, never throws to a caller). The "what authority backs this?" answer is: Anthropic's CLI internally makes the same `/v1/messages` call (verified 2026-05-26 by `strings` on the compiled binary — see `~/.cc-rules/memory/learnings/anthropic_plan_usage_probe_schema_2026_05_26.md`); the probe is mirroring an established CLI behaviour rather than introducing a new wire format. Under `ALIGNMENT.md` Rule 2, mirroring observed CLI behaviour is permitted; the Rule's intent is "don't invent wire formats Anthropic's CLI does not perform", which the probe respects. +- **Change — extend the Provider contract description:** + - `quotaStatus(authContext): { quotaInfo }` is now permitted to call provider HTTP APIs directly, subject to **all three** constraints: + 1. **READ-ONLY** — the API call must not mutate provider-side state. POST is acceptable when the response is what's needed (Anthropic returns ratelimit headers on `POST /v1/messages`); the request body MUST minimise side-effects (`max_tokens: 1`, dummy `messages`). + 2. **Subscription-scope reuse** — the credentials used MUST be the same auth artifact the spawn path already reads via `readAuthArtifact()`. No new OAuth grant, no new API-key registration, no separate scopes. + 3. **Idempotent failure** — if the probe fails for any reason (network error, 401, 429, schema parse failure), the function returns `null` rather than throwing. The caller (server.mjs / dashboard / `olp usage` CLI) interprets `null` as "quota data unavailable" and continues gracefully. + - `healthCheck()` and other contract methods are NOT extended by this Amendment. Only `quotaStatus()` may make direct API calls. A plugin that wants live data for any other contract method must continue to use `spawn` or `readAuthArtifact`. + - The probe MUST cache its result. Recommended TTL: 5 minutes (mirrors OCP `USAGE_CACHE_TTL`). Tighter TTLs (e.g. dashboard's 1-minute refresh) are served from the cached value if fresh; cache miss triggers a real probe. + - The probe MUST implement exponential backoff on refresh failures: minimum 60s, maximum 3600s (mirrors OCP `OAUTH_REFRESH_MIN_BACKOFF` / `OAUTH_REFRESH_MAX_BACKOFF`). Tight loop on failure has historically burned through Anthropic's rate limit in seconds (OCP institutional lesson 2026-04). + - The probe MUST be opt-in via `~/.olp/config.json` (`providers..quota_probe_enabled: true`; default `false`). Reasoning: a fresh OLP install on a machine without OAuth credentials should not bombard `api.anthropic.com` with 401-bound probes; the operator opts in once the credentials are configured. +- **What this Amendment does NOT permit:** + - Mutating API calls (e.g. POST/PATCH/DELETE that change provider-side state). Still forbidden. + - API calls for any contract method other than `quotaStatus()`. `spawn` / `healthCheck` / `doctorChecks` / `estimateCost` / `models` / `hints` / `name` / `displayName` / `auth` remain spawn-and-filesystem-only. + - Per-provider new auth grants. The probe uses the spawn path's existing credentials. + - Bypassing the alignment.yml blacklist. The hallucinated `/api/oauth/usage` token stays blacklisted; the probe uses `/v1/messages` (real endpoint). + - **API calls to endpoints not explicitly enumerated by the companion ADR 0013 § Rule 2.** Amendment 8 permits the *kind* of call (READ-ONLY direct API for quota probing); ADR 0013 Rule 2 enumerates *which specific endpoint* is permitted. A future reader of Amendment 8 alone should NOT infer that any READ-ONLY/idempotent endpoint is fair game — the per-endpoint containment is locked to ADR 0013. Re-opening per-endpoint scope requires an ADR 0013 amendment, not a new plugin-level interpretation of Amendment 8. +- **Backwards compatibility:** Plugins whose `quotaStatus()` still returns `null` (mistral at v0.5.0 pending D84 audit, codex permanently per Phase 5 charter) are NOT affected. No existing behaviour changes for them. +- **Authority cited at the implementation:** D80 commit cites this Amendment + `~/.cc-rules/memory/learnings/anthropic_plan_usage_probe_schema_2026_05_26.md` + `Claude Code v2.1.x § OAuth bearer + ratelimit-unified headers` + live-probe transcript from 2026-05-26 in the commit body. ALIGNMENT.md Rule 1 + Rule 5 (CI) both satisfied. +- **Tests:** Suite 38 (Phase 5 D83) covers the probe: mock HTTP server returning all 13 `anthropic-ratelimit-unified-*` headers; assert parse correctness for each; assert 5min cache; assert 60s-3600s exponential backoff on simulated 429; assert stale-cache-on-failure (`null` is returned only when no cache entry exists; if a stale entry exists it's returned with a `stale: true` marker). +- **Procedural mechanism:** Iron Rule 11 (IDR) — this Amendment, ADR 0012 (Phase 5 charter), and ADR 0013 (OAuth READ-ONLY consumption rules) land together at D79 as a single coupled commit. Reviewing them separately cannot verify consumer-producer alignment. Iron Rule 10 fresh-context reviewer per CLAUDE.md hard requirement #3. + ### Amendment 7 — 2026-05-26: Add OPTIONAL `doctorChecks()` to the Provider contract (D67 — Phase 4 operator UX) - **Context:** ADR 0010 § Phase 4 D64-D67 ships `bin/olp.mjs` operator CLI + `olp doctor` framework. `olp doctor` runs a set of `Check` objects (id / category / async `run()` returning `{ status, message, evidence? }`) and discriminates the next remediation step via a `kind` field (`noop` / `fix_server` / `fix_oauth` / `fix_provider` / `fresh_install`). The framework needs per-provider checks so a user with a broken `claude` install gets a different fix recipe than a user with a broken `vibe` install. Hardcoding the recipes in `bin/olp.mjs` would re-introduce the kind of per-provider knowledge drift that ADR 0002 § Decision exists to prevent — when a new provider plugin lands, the operator CLI would have to be edited too. diff --git a/docs/adr/0012-phase-5-charter-quota-probes-dashboard.md b/docs/adr/0012-phase-5-charter-quota-probes-dashboard.md new file mode 100644 index 0000000..a7e3e8a --- /dev/null +++ b/docs/adr/0012-phase-5-charter-quota-probes-dashboard.md @@ -0,0 +1,127 @@ +# ADR 0012 — Phase 5 Charter: Provider Quota Probes + Dashboard Enrichment + +**Status:** Accepted (Phase 5 open as of 2026-05-26) +**Date:** 2026-05-26 +**D-day:** D79 (charter + ADR 0002 Amendment 8 + ADR 0013 land together as the constitutional layer of Phase 5) + +--- + +## Context + +Phase 4 (ADR 0010) shipped OLP's operator + client UX layer — `bin/olp` operator CLI, `olp doctor` framework, `olp-connect` zero-config IDE wiring, OpenClaw `/olp` slash commands, anonymous-key deployment-context limits, SSE heartbeat. v0.4.4 is the current shipped state. Phase 4 closed every gap on the OCP-feature-parity matrix EXCEPT one: **live quota / plan-usage surfacing**. + +Today `lib/providers/anthropic.mjs:445` has a stub `quotaStatus()` returning `null` (D4 placeholder). The OLP dashboard's quota panel renders "—" for all providers. OCP, in contrast, exposes a live "39% session / 30% weekly" panel — the maintainer uses this multiple times per day to decide when to throttle voluntary `claude -p` traffic away from interactive sessions. OLP cannot become an OCP successor in practice (vs. just feature-parity-on-paper) until quota surfacing works. + +A pre-flight institutional-knowledge audit (2026-05-26 — see `~/.cc-rules/memory/learnings/anthropic_plan_usage_probe_schema_2026_05_26.md`) confirmed: + +1. **The OCP probe still works today** — Anthropic returns the same `anthropic-ratelimit-unified-*` headers on every `POST /v1/messages` call. Tested live 2026-05-26 from PI231 OAuth credentials. +2. **Schema added 3 fields since OCP's 2026-04 capture** — `5h-status`, `7d-status` (per-window status), `overage-reset` (only on active overage). No fields removed or renamed. +3. **Verification protocol has shifted** — Claude Code v2.1.x is now a **compiled binary** (Mach-O / ELF), not bundled JS. OCP's "grep cli.js" approach no longer applies; the replacement protocol is `strings` against the binary + periodic live probe diff. +4. **OAuth refresh path unchanged** — `platform.claude.com/v1/oauth/token` + `9d1c250a-...` client_id + 60s-3600s exponential backoff. + +The audit makes Phase 5 implementation low-risk: this is a port of a working OCP function, not a re-derivation. The work is mechanical + adapter-layer plumbing into OLP's plugin contract. + +A parallel maintainer request (2026-05-26, with reference screenshot of claude.ai/settings/usage) asked for Claude.ai-style dashboard enrichment: per-row utilization bars, reset countdown, 1-minute auto-refresh, manual refresh button. P5-1 (probe) + P5-2 (dashboard) together unlock both: the data plus the surface. v1.x roadmap #8 is closed by P5-2. + +--- + +## Decision + +Phase 5 scope is **Provider quota probes + dashboard enrichment**. The phase opens 2026-05-26 with D79 (this charter + ADR 0002 Amendment 8 + ADR 0013 OAuth READ-ONLY consumption rules). Phase 5 close ships v0.5.0; per `CLAUDE.md release_kit.phase_rolling_mode`, the close PR is maintainer-triggered. + +### In scope — Phase 5 D-day plan (~6 D-days) + +| D-day | Deliverable | Authority | Estimate | +|---|---|---|---| +| **D79** | This charter ADR 0012 + ADR 0002 Amendment 8 (direct-API READ-ONLY) + ADR 0013 (OAuth READ-ONLY consumption rules + schema-drift mitigation) + `package.json` `current_pre_release_identifier` → `0.5.0-phase5` + `CLAUDE.md release_kit.phase_rolling_mode.current_phase` → Phase 5 | This charter + audit memory | 0.5d | +| **D80** | `lib/providers/anthropic.mjs:quotaStatus()` ported from OCP `server.mjs:842-1109` — full probe with macOS-keychain auth read added (existing OLP reader only handles env + `.credentials.json`) + 5min cache + 60s-3600s refresh backoff + stale-cache-on-429 + all 13 headers parsed (including new 5h-status / 7d-status / overage-reset) | Port OCP probe + ALIGNMENT.md Rule 2 exemption per ADR 0002 Amendment 8 + audit memory | 2d | +| **D81** | `lib/audit-query.mjs` + `/v0/management/dashboard-data` extended to surface the new quota shape per provider (utilization, reset, representative-claim, fallback-percentage, overage-status). Audit-query stays in-memory scan per ADR 0008 Lane 2 = A (no SQLite). Schema migration documented in ADR 0008 § Amendment | ADR 0008 + this charter | 1d | +| **D82** | `dashboard.html` Claude.ai-style restructure — per-provider rows replace the current single Quota panel; each row: provider badge, model placeholder, utilization bar (5h + 7d), reset countdown ("Your limit will reset at HH:MM AM/PM" format from the user-shared claude.ai screenshot), status badge, representative-claim hint. 1-minute auto-refresh via `setInterval` with `document.visibilityState` guard. Manual refresh button calls `/v0/management/dashboard-data` directly | v1.x roadmap #8 + maintainer reference screenshot | 1.5d | +| **D83** | Test coverage — Suite 38 quota-probe unit tests (mock HTTP server returning the 13 headers; assert parse + cache + backoff + stale-on-429); Suite 39 dashboard rendering smoke (curl `/dashboard` after pre-seeding mock quota cache; assert HTML contains expected utilization strings); update Suite 33 doctor checks for new `anthropic.quota_probe_reachable` check | Test convention from existing suites | 1d | +| **D84** | (optional) `lib/providers/mistral.mjs:quotaStatus()` — Mistral GO/NO-GO decided at D79 close (30-min docs spike before D80 starts), NOT mid-phase. If Mistral exposes `/v1/usage` or equivalent → D84 ports the probe with a parallel ADR 0002 Amendment 8 invocation. If NOT exposed → D84 is dropped from the plan and mistral row shows "spend tracking only" badge sourced from audit-query. Codex explicitly skipped — `openai/codex` CLI has no public quota API. GO/NO-GO outcome documented in this charter as a D79-close amendment | Live audit at D79 close (~30 min spike) | 0.5d if GO; 0d if NO-GO | +| **close** | v0.5.0 release PR — `package.json` `0.4.4 → 0.5.0`, CHANGELOG promotion, `release_kit.phase_rolling_mode.current_pre_release_identifier` advance to Phase 6 token | `CLAUDE.md release_kit overlay` | maintainer-triggered | + +### Out of Phase 5 scope (with explicit triggers) + +#### `X-OLP-Cost-USD` per-request response header + +**Status:** Deferred to Phase 6. Was listed in ADR 0010 § Out-of-scope as "Phase 5 prerequisite". The prerequisite (provider-cost weights table) is non-trivial — needs per-(provider, model) `input_cost_per_1k_tokens` / `output_cost_per_1k_tokens` / `cache_read_discount` data sourced from each provider's published pricing page. Phase 5 already pulls in two new ADRs; adding a third data-onboarding ADR is scope creep. + +**Re-open condition.** Phase 6 unless a maintainer reports a cost-attribution debugging need that warrants pulling forward. + +#### `context_window_exceeded` fallback trigger (LiteLLM prior-art) + +**Status:** Deferred. ADR 0010 listed this as opportunistic-in-Phase-5 unless the trigger fires sooner. The trigger has not fired in Phase 4 production traffic. Continue to defer. + +#### per-(provider, model) live stats Map (replacing audit-query scan) + +**Status:** Deferred. Current scan latency is ~20ms at 7-day depth. Acceptable until volume grows (>100k requests/day). Re-evaluate at Phase 6 if dashboard latency degrades. + +#### Anthropic interactive-mode P0 (ADR 0009) + +**Status:** Still trigger-gated on Anthropic's 2026-06-15 billing-split rollout. Phase 5 does NOT depend on P0 — the quota probe reads `anthropic-ratelimit-unified-*` headers regardless of which billing pool the spawn path consumes. If P0 succeeds Phase 7+ Phase 5's probe code remains unchanged; if P0 fails Phase 5's probe code remains unchanged. The probe is billing-pool-agnostic because the headers are subscription-pool metadata, not Agent-SDK-Credit metadata. + +#### `/v1/messages` Anthropic-shape entry surface + +**Status:** Still deferred per ADR 0010 § Out-of-scope. No change in Phase 5. + +#### v1.x roadmap #3 / #5 / #6 + +**Status:** Still trigger-gated per `docs/v1x-roadmap.md`. None has fired. Continue to defer. + +### Opportunistic Phase 5 micro-additions (not blocking) + +Items small enough to land alongside a planned D-day without scope creep, if encountered: + +- README § Dashboard screenshot update (post-P5-2 enrichment) — capture from MacBook test path per `~/.cc-rules/memory/feedback/mac_mini_never_for_testing.md`. +- `olp usage` CLI subcommand (bin/olp.mjs) surfaces the parsed quota shape in terminal form. Already partially exists (cmdUsage in bin/olp.mjs); confirm payload alignment after D80. +- Add `claude_code_oauth_client_id` config override in `~/.olp/config.json` so power users can override the hardcoded `9d1c250a-...` UUID without env-var fiddling. Mirrors compiled binary's `CLAUDE_CODE_OAUTH_CLIENT_ID` env support. +- `docs/provider-audits/anthropic.md` re-capture with current `claude --version` (v2.1.142 MacBook / v2.1.150 PI231) + binary distribution layout note. + +### Exit gate — v0.5.0 close criteria + +1. D79 — D84 all merged with fresh-context opus reviewer APPROVE per Iron Rule 10. +2. CI green on every D-day merge commit and on the v0.5.0 release commit head. `alignment.yml` blacklist re-confirmed (no new hallucinated tokens introduced). +3. README § Quota / Plan Usage section present with screenshot of the enriched dashboard. README § Supported Providers table updated to note "quota probe: anthropic ✅, mistral ⚠️/✅ (D84 outcome), codex ❌ (no public API)". +4. ADR 0012 (this charter) + ADR 0002 Amendment 8 + ADR 0013 (OAuth READ-ONLY consumption) on disk. +5. `CHANGELOG.md "Unreleased"` promoted to `"## v0.5.0 — "` with D79 — D84 entries. +6. `package.json` bumped to `0.5.0`. +7. `CLAUDE.md release_kit.phase_rolling_mode.current_phase` advances `Phase 5 → Phase 6`; `current_pre_release_identifier` advances `0.5.0-phase5 → 0.6.0-phase6`. +8. Standing autopilot grant covers D-day-by-D-day execution; v0.5.0 close PR is maintainer-triggered. +9. Live MacBook E2E verification — dashboard renders enriched panel with real quota data (probe live, not mocked). + +--- + +## Consequences + +**Positive.** + +- OLP finally has the load-bearing observability OCP had — maintainer can see live "39% session / 30% weekly" and decide whether voluntary `claude -p` traffic stays or moves. +- Family members on the LAN see real reset times instead of "—", which makes the "wait 2 hours" guidance concrete vs. abstract. +- The institutional-knowledge audit captured the schema in a memory file pinned with date stamps — future ports (mistral, future provider) re-use the verification protocol without re-deriving. +- v1.x roadmap #8 (Dashboard enrichment per Claude.ai-style usage page) closes inside Phase 5 rather than waiting for a separate phase. +- Compiled-binary-distribution awareness ("no more cli.js to grep") is now codified in OLP governance; the next time Anthropic ships a major CC version, the verification protocol is already written. + +**Negative.** + +- ADR 0002 gains another amendment (Amendment 8). The constitution surface area for `anthropic.mjs` grows. Counter-pressure: the alternative (probe lives in `server.mjs`, like OCP) violates the plugin-architecture principle that per-provider knowledge stays in `lib/providers/`. Amendment 8 is the smaller violation. +- The probe makes one `/v1/messages` call per 5min cache miss. That's ~12 calls/hour worst case across the whole proxy (probe is per-credentials, not per-key). With `max_tokens: 1` the cost is < $0.01/day at family-scale traffic. Negligible but not zero. +- Schema-drift risk over the long horizon. Anthropic could rename or remove headers in a future version. The mitigation protocol (strings + live probe diff) is in place, but it's a manual check — needs to be invoked by the maintainer or scheduled. +- Dashboard refactor introduces a breaking-change risk for the existing dashboard.html consumers (none today, but conceptually). Bumping to v0.5.0 signals this clearly. + +**Neutral.** + +- Phase 5 has more ADR work than Phase 4 (3 governance docs vs. 2). The constitutional layer is deliberately heavier because direct-API access is the single biggest authority decision since the plugin contract itself. + +--- + +## Authority + cross-references + +- **Iron Rule 11 (IDR)** — Phase 5 ships across 6 D-days, each a minimum reviewable unit. The governance trio (this ADR + Amendment 8 + ADR 0013) lands at D79 as a single coupled commit (reviewing them separately cannot verify consumer-producer alignment), per ADR 0002 Amendment 7's precedent. +- **Iron Rule 12 (prior-art search)** — discharged via the audit memory at `~/.cc-rules/memory/learnings/anthropic_plan_usage_probe_schema_2026_05_26.md`. Memory committed prior to D80 implementation. +- **ALIGNMENT.md Rule 1 (citation)** — D80 commit must cite compiled-binary `strings` evidence per audit memory § Path A (Claude Code v2.1.x has no traditional `§ section` structure because it is a Mach-O / ELF compiled binary) plus the audit memory file path. Live-probe transcript MUST be included in the commit body. +- **ALIGNMENT.md Rule 2 (provider-CLI-as-authority)** — direct-API access bypasses the spawn-binary contract. Amendment 8 is the explicit exemption. Without Amendment 8, the D80 commit is unalignable. +- **ALIGNMENT.md Rule 5 (CI alignment.yml)** — must continue to pass. `api.anthropic.com/v1/messages` is NOT on the blacklist (correct — that's the real endpoint). The hallucinated `/api/oauth/usage` IS on the blacklist (transitive from OCP) and must remain. +- **ADR 0002 Amendment 8** — companion ADR. Direct-API access scoping; READ-ONLY constraint; opt-in via config flag (default off). +- **ADR 0013** — companion ADR. OAuth credentials shared between spawn path + probe path; refresh backoff; schema-drift mitigation protocol. +- **CLAUDE.md release_kit** — Phase boundary triggers maintainer-led version bump. D-day commits within Phase 5 stay under "Unreleased". `0.5.0-phase5` is the pre-release identifier during the phase. diff --git a/docs/adr/0013-oauth-read-only-consumption-and-schema-drift.md b/docs/adr/0013-oauth-read-only-consumption-and-schema-drift.md new file mode 100644 index 0000000..7c9ca5d --- /dev/null +++ b/docs/adr/0013-oauth-read-only-consumption-and-schema-drift.md @@ -0,0 +1,186 @@ +# ADR 0013 — OAuth READ-ONLY Consumption Rules + Schema-Drift Mitigation Protocol + +**Status:** Accepted (2026-05-26) +**Date:** 2026-05-26 +**D-day:** D79 (lands alongside ADR 0012 Phase 5 charter + ADR 0002 Amendment 8 as the constitutional trio of Phase 5) + +--- + +## Context + +ADR 0002 Amendment 8 permits `quotaStatus()` to call provider HTTP APIs directly, subject to a READ-ONLY constraint. That Amendment opens the door but does not specify HOW READ-ONLY discipline is preserved across credential lifecycle events (refresh, expiry, revocation), nor how OLP detects when the upstream API schema drifts. ADR 0013 fills both gaps. + +The motivating concern: a provider that ships its CLI as a **compiled native binary** (Anthropic Claude Code v2.1.x is now Mach-O on macOS, ELF on Linux) closes off the previous schema-verification path (grep `cli.js`). If OLP's probe parser silently breaks because a header was renamed, the dashboard shows stale or wrong numbers, and the maintainer's load-bearing throttling decision is based on bad data. This ADR establishes the verification protocol that survives the binary-distribution shift. + +A second motivating concern: the OAuth credentials used by the probe are the SAME credentials the spawn path uses for `claude -p`. Both paths consume them; the probe must not interfere with the spawn path's ability to refresh or invalidate them. Concretely: the probe must not write to the credentials artifact, must not race the spawn path on refresh, and must not amplify a 429 into a refresh storm. + +--- + +## Decision + +### Rule 1 — Credential reuse is mandatory + +The probe MUST consume the same OAuth artifact the spawn path reads via the plugin's `readAuthArtifact()`. No new OAuth grant. No alternate credential store. No environment-variable-only fallback (env var `CLAUDE_CODE_OAUTH_TOKEN` is supported as an override consistent with the spawn path, but is not the probe's primary source). + +Precedence order (mirrors OCP `getOAuthCredentials` 2026-04-stable): + +1. `process.env.CLAUDE_CODE_OAUTH_TOKEN` if non-empty (manual override; common in CI / dev / one-off debugging). +2. `~/.claude/.credentials.json` → `claudeAiOauth.accessToken` (Linux + macOS without keychain access). +3. macOS Keychain: `security find-generic-password -a "${USER}" -s "Claude Code-credentials" -w` (preferred on macOS — current `lib/providers/anthropic.mjs` only covers (1) + (2); D80 adds (3)). + +Rationale: a separate OAuth grant would require the maintainer to repeat `claude setup-token` against an OLP-specific scope, doubling credential exposure and divergence risk. Reusing the spawn path's credentials guarantees the probe never has more permission than the spawn path itself. + +### Rule 2 — READ-ONLY at the wire + +The probe MUST issue exactly one HTTP request per cache miss. Method MAY be POST (Anthropic's ratelimit headers come back on `POST /v1/messages`; this is the only way to read them). Request body MUST minimise side effects: + +- `max_tokens: 1` (cost: ~$0.000001 per probe) +- `messages: [{role: "user", content: "hi"}]` (any minimal valid payload) +- Model: cheapest available in the plan (`claude-haiku-4-5` at v0.5.0) +- Do NOT include `system` prompts, `tools[]`, `tool_choice`, large content arrays, or anything that the upstream might bill differently. + +The probe MUST discard the response body. Only response headers are parsed. + +The probe MUST NOT call any other HTTP path on the provider's API. No `/v1/models` enumeration, no admin endpoints, no `/v1/messages/` retrievals. The only permitted endpoint is `POST /v1/messages`. + +### Rule 3 — Cache TTL and refresh discipline + +- Cache TTL: 5 minutes. Cache miss triggers a real probe. Cache hit returns the cached value. +- The dashboard refreshes every 1 minute; that's served from the cache between probes. A manual refresh button MAY force-clear the cache (per maintainer request 2026-05-26); ADR 0012 D82 documents the button. +- On refresh failure (token expired, 401/403/429, network error), the probe schedules an exponential backoff: minimum 60s, maximum 3600s. The cache entry is NOT invalidated during backoff; `quotaStatus()` returns the stale cache marked `{ stale: true, last_fresh_at: }`. If no stale entry exists, returns `null`. +- Successive successful probes reset the backoff to the minimum. +- Token refresh (`POST https://platform.claude.com/v1/oauth/token`) follows the same backoff discipline. The probe MUST NOT refresh a token more than once per backoff window. The refresh path is shared with the spawn path; both observe the same backoff. + +### Rule 4 — Opt-in via config + +A new config field at `~/.olp/config.json` controls per-provider opt-in: + +```json +{ + "providers": { + "anthropic": { + "enabled": true, + "quota_probe_enabled": false + } + } +} +``` + +Default: `false`. The maintainer must explicitly opt in after credentials are configured. Reasoning: a fresh install on a machine without OAuth credentials should not bombard `api.anthropic.com` with 401-bound probes. + +`olp doctor` adds a per-provider check `.quota_probe_reachable` (only runs if `quota_probe_enabled: true`). Failed check provides a `next_action.ai_executable[]` recipe to either re-authenticate or disable the probe. + +### Rule 5 — Schema-drift mitigation protocol + +The CC binary-distribution shift means OCP's "grep cli.js" verification is no longer applicable. OLP adopts a two-path protocol: + +**Path A — Compiled-binary string extraction.** Run `strings` over the platform-specific binary in the claude-code distribution. Captures all hardcoded header names the binary expects: + +```bash +BIN_DIR=$(npm root -g)/@anthropic-ai/claude-code/node_modules/@anthropic-ai/claude-code-* +strings "$BIN_DIR/claude" | grep -iE "anthropic-ratelimit|/v1/(messages|oauth)|platform\.claude\.com" +``` + +**Path A prerequisites.** GNU or BSD `strings` (part of binutils/coreutils on Linux + macOS — always present on a normal developer machine; Windows requires WSL or `binutils-mingw`). A locally installed Claude Code v2.1.x (npm-global or volta-managed). A reviewer without `claude` installed can still run Path B but Path A is gated on having the binary on disk. A future Claude Code version that ships as a different distribution shape (e.g. Rust binary, statically linked Go) keeps the protocol valid: `strings` works on any ELF/Mach-O regardless of compile source. + +**Path B — Live API probe.** Run the actual probe against `api.anthropic.com` with valid OAuth credentials. Captures what the server returns today: + +```bash +curl -s -i -m 10 -X POST https://api.anthropic.com/v1/messages \ + -H "Authorization: Bearer $TOKEN" \ + -H "anthropic-beta: oauth-2025-04-20" \ + -H "anthropic-version: 2023-06-01" \ + -H "Content-Type: application/json" \ + -d '{"model":"claude-haiku-4-5","max_tokens":1,"messages":[{"role":"user","content":"hi"}]}' \ + | grep -iE "^anthropic-ratelimit" +``` + +Path A tells you what the client expects. Path B tells you what the server actually emits. The diff is the actionable schema delta. + +**Required cadence.** The diff MUST be re-run at every major `claude --version` bump (v2.x → v3.x is the next trigger). The current pinned schema lives at `~/.cc-rules/memory/learnings/anthropic_plan_usage_probe_schema_2026_05_26.md`. After re-verification, that memory file MUST be updated (or a successor file written with a new date stamp; the old one cross-linked). + +**Trigger for re-running the diff.** There is no automated detector for a major `claude --version` bump at v0.5.0. Three explicit hooks share this responsibility: + +1. **Annual Alignment Audit** (`ALIGNMENT.md` § Annual Alignment Audit, every 14 May) — diff is mandatory as part of the audit checklist. +2. **`olp doctor anthropic.quota_probe_reachable` failure** — if the probe returns non-2xx for any reason other than 401/403/429/network (typical schema breaks manifest as 422 or 400), `olp doctor` surfaces a `kind: fix_provider` recipe whose first step is "re-run the Rule 5 dual-path diff". +3. **Manual maintainer attention at a major Claude Code release** — if the maintainer sees a major version bump in `claude --version`, kick off the diff before the next Phase opens. Rolling-mode discipline (CLAUDE.md release_kit) means major-version bumps usually intersect with Phase boundaries. + +If the diff is missed across a major version bump, the failure mode is graceful degradation: the parser silently drops unknown headers; the dashboard shows older values (cached stale) or `null` per Rule 3; `olp doctor` surfaces the staleness. + +**Required action on drift detection.** If a header is renamed or removed: + +1. File a Phase-N issue tagging the maintainer. +2. Update the parser in `lib/providers/anthropic.mjs:quotaStatus()` to handle both names (graceful migration), prefer the new name. +3. Update the audit memory file with a "drift event" section recording: date, old field, new field, evidence URLs. +4. Bump the `models-registry.json` `quota_probe.schema_version` (NEW field added at D80) so downstream consumers can detect. + +If a new header appears in the live response that the parser doesn't read: low-priority enhancement; add to the parser, document in the audit memory, no schema_version bump required. + +### Rule 6 — Failure transparency + +The probe's failure modes are visible to the operator: + +- `/v0/management/dashboard-data` includes per-provider `{ quota_probe: { status: 'ok' | 'stale' | 'failed' | 'disabled', last_fresh_at, last_error?, backoff_until? } }`. +- `olp doctor` surfaces probe failure as `kind: fix_oauth` (if 401/403) or `kind: fix_provider` (if 429 with no stale cache or network error). +- The dashboard row badge shows the status; clicking a failed row shows the last error (truncated to 200 chars, no full credential traces). + +### Rule 7 — Out-of-scope + +This ADR does NOT govern: + +- Spawn-path OAuth refresh (the spawn path's refresh logic predates this ADR and is governed by the underlying CLI). The probe shares the credential artifact but does not own the refresh. +- Anthropic-specific bearer revocation (Anthropic side). Revocation manifests as 401 to the probe, which falls into Rule 6. +- Non-Anthropic provider OAuth flows. Mistral / future providers MAY adopt this protocol via plugin-specific ADRs; ADR 0013 establishes the template. + +--- + +## Consequences + +**Positive.** + +- The probe is bounded — Rule 2 caps the wire traffic, Rule 3 caps the refresh rate, Rule 4 caps activation surface. +- Schema-drift detection is procedural and reproducible — Rule 5 gives the maintainer a runbook that doesn't depend on Anthropic publishing a deprecation notice. +- Failure is visible — Rule 6 means a broken probe shows up in `olp doctor` and the dashboard, not as a silent "—" in the quota row. +- Credential reuse (Rule 1) keeps the security surface area minimal. + +**Negative.** + +- The `quota_probe_enabled` opt-in adds a configuration step. Mitigated by `olp doctor` surfacing the recipe when credentials are present but the probe is off. +- The schema-drift protocol is manual. Anthropic could ship a v3.x binary tomorrow and the verification only happens when the maintainer or a doctor probe failure prompts it. Counter-pressure: drift events at OCP scale (~12 months) suggest manual verification on major version bumps is sufficient. +- Stale-cache-on-failure (Rule 3) means the dashboard could show 30-minute-old data without an obvious "stale" indicator unless the UI explicitly renders the `stale: true` marker. ADR 0012 D82 requires the dashboard to surface staleness; reviewing that during P5-2 implementation. + +**Neutral.** + +- The protocol is portable. Future provider plugins adopting direct-API probes (mistral if its `/v1/usage` exists) can reuse the same six rules with provider-specific endpoint substitution. + +--- + +## Alternatives considered + +### A — Probe lives in `server.mjs` (OCP-style) + +OCP's probe is in `server.mjs:842-1109` because OCP is single-provider and pre-plugin-architecture. Porting that pattern to OLP would violate ADR 0002 (per-provider knowledge stays in `lib/providers/`). Rejected. + +### B — Spawn `claude -p --dry-run` and parse ratelimit headers + +`claude -p` does not expose response headers; the CLI consumes and discards them. Even if it did, parsing CLI stdout is fragile. Rejected. + +### C — Wait for Anthropic to publish a public quota API + +The 2026-06-15 Agent SDK Credit billing-split announcement does not include a public quota API. Anthropic may publish one in the future; this ADR is forward-compatible (Rule 7 explicitly notes "if Anthropic publishes a public ratelimit API, this entire workaround becomes obsolete — re-evaluate"). Rejected for v0.5.0 (no ETA). + +### D — Mandate token-rotation in OLP + +Tempting (auditability), but OCP's experience shows token rotation breaks the spawn path more often than it improves security at family-scale deployment. The credential rotation cadence is Anthropic-side (token TTL); OLP respects whatever Claude Code does. Rejected. + +--- + +## Authority + cross-references + +- **ADR 0002 Amendment 8** — the contract-level permission. ADR 0013 is the implementation discipline for that permission. +- **ADR 0012** — Phase 5 charter that schedules D80 implementation. +- **ADR 0011** — anonymous-key deployment-context (LAN-only). Separate scope; this ADR does not amend it. +- **`~/.cc-rules/memory/learnings/anthropic_plan_usage_probe_schema_2026_05_26.md`** — the live schema pin. Updated on every drift event per Rule 5. +- **`alignment.yml`** — must continue to blacklist `/api/oauth/usage` and related hallucinated tokens. Must NOT add `/v1/messages` to the blacklist (legitimate endpoint). +- **OCP `server.mjs:842-1109`** — the source-of-truth port reference for D80. +- **OCP `ALIGNMENT.md`** — the institutional precedent (2026-04-11 drift → ALIGNMENT introduction) this ADR consolidates for OLP. diff --git a/docs/adr/README.md b/docs/adr/README.md index 13d74e4..14a2e80 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -25,6 +25,8 @@ New ADRs increment from the highest existing number. Filenames are `NNNN-.quota_probe_enabled` (default false); schema-drift mitigation via dual-path verification (compiled-binary `strings` + live API probe diff); failure transparency through `olp doctor` + dashboard staleness markers; out-of-scope clarifications. Bound by `~/.cc-rules/memory/learnings/anthropic_plan_usage_probe_schema_2026_05_26.md` as the live schema pin. | ## When to write a new ADR