* docs: D43-B — ADR 0007 multi-key auth design draft (design-only, no code change) Phase 2 mainline design ADR. Ratifies the storage / token / manifest / atomic-write / owner-gating / bootstrap / Node-baseline decisions ahead of D44+ implementation D-days. Pure design doc — no .mjs / no tests / 4 files touched. Test count 468 → 468. - docs/adr/0007-multi-key-auth.md (new, ~400 lines): 13 sections covering Context / Decision (Option 2 filesystem manifest + opaque token) / Storage layout / Manifest schema / Token format (olp_+32B base64url, SHA-256 hash) / Atomic write & audit append (manifest lifecycle-only atomic via tmpfile+fsync+rename; audit per-request append with warn+1-retry, no memory buffer at Phase 2) / Owner-vs- guest-vs-anonymous gating (config.json auth.allow_anonymous default false, no env auto-detection) / Audit ndjson schema (no PII) / Bootstrap & recovery (minimal keygen command surface + OLP_OWNER_TOKEN env override with stable __env_owner__ keyId) / Acceptance criteria (11 test surfaces) / Node baseline (Option 1 SQLite port rejection rationale citing engines >=18 + CI 20/24 vs node:sqlite v22.5.0/RC) / Out of scope (Dashboard, quota enforcement, audit query, file locking deferred to Phase 3+) / Future forward (Option 3 hybrid migration trigger + preconditions). - docs/adr/README.md index: added ADR 0007 row with one-paragraph summary covering storage choice + rejection rationale. - docs/v1x-roadmap.md #2: marked PHASE 2 ACTIVE (no longer deferred); "Design ADR (NOT YET RATIFIED)" → "Design ADR (ratified) → ADR 0007"; trigger updated to "already fired 2026-05-25"; code anchors pinned to exact line numbers (cache/store.mjs:77-79/:287, server .mjs:502/:531/:392/:1072/:1101). - CHANGELOG.md Unreleased: D43-B entry per release_kit overlay phase_rolling_mode discipline. Authority: - Phase 2 kickoff handoff (~/.cc-rules/memory/handoffs/2026-05-25-phase-2-kickoff.md, cc-rules d9da966) - OLP v0.1 spec § 4.5 (planning authority for ~/.olp/ layout) - OCP keys.mjs (prior-art for opaque-key + per-key isolation model) - Node node:sqlite docs (https://nodejs.org/api/sqlite.html — Option 1 rejection per ADR 0007 § 11) - CC 开发铁律 v1.6 § 10 — fresh-context opus reviewer required for design ADR per Iron Rule 10 ALIGNMENT.md scope check: this PR introduces a new ADR; per ALIGNMENT.md Rule 1 (Cite First), the ADR itself contains the authority citations its decisions rest on (v0.1 spec § 4.5, OCP keys.mjs, Node docs URL). No provider plugin / entry surface / IR change in this commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: D43-B fold-in — ADR 0007 reviewer findings (2 P2 + 3 P3, all polish) Fresh-context opus reviewer (PR #19) returned APPROVE_WITH_MINOR with 2 P2 load-bearing-but-non-blocking findings + 3 P3 polish findings. All five are accepted as suggested; design contract clarified without semantic change. - § 6.2 step 1 (P2 #1) — pin audit serialization to fire AFTER status_code is determined and latency_ms is measured. Makes acceptance criterion #2 (anonymous-401 audit event records the 401 + latency) testable in the way the criterion was written. - § 6.3.5 (P2 #2, new subsection) — explicit "Token validation MUST hit the manifest on every authenticated request (no in-process validation cache at Phase 2)" rule. The acceptance criterion #6 (post-revoke 401 within the next request) was previously enforced only by the test; the rule now belongs to the design contract. Forward-path note documents when a Phase 3+ amendment may add a cache. - § 6.1 atomic-write step 5 follow-up (P3 #3) — document the deliberate omission of directory fsync after rename. Single-process family-scale deployment accepts the tiny rename-loss window under abrupt host crash; future POSIX-strict deployments know where to add the step. - § 9.4 (P3 #4) — declare token-collision between OLP_OWNER_TOKEN and a filesystem-stored key's plaintext as undefined behaviour. Operators MUST NOT reuse plaintext across both surfaces. Phase MAY add startup collision-detection later. - § 10 criterion #4 (P3 #5) — rephrased to assert against the config- driven owner_only_endpoints predicate rather than a hardcoded trimmed payload shape. The test stays stable if an operator removes /health from owner_only_endpoints. CHANGELOG D43-B entry: fold-in bullet added to summarize the 5 fixes. Test count: 468 → 468 (npm test verified locally after fold-in). Authority: PR #19 fresh-context opus reviewer findings; CLAUDE.md release_kit overlay phase_rolling_mode — under Unreleased. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: D43-B fold-in #2 — maintainer text-review findings (1 P1 + 1 P2 + 1 P3) Maintainer (codex) did final text review of PR #19 against the Phase 2 ADR-ratification checklist. Returned 3 findings; all accepted as suggested. Two are contract-level (P1 safety + P2 factual); one is trivial (P3 line-count drift). No semantic change beyond what the findings called out. P1 — § 6.3 / § 6.4 / § 10 #7 — revoke-dominates-touch safety contract Original § 6.3 said touchLastUsed used the same atomic-write pattern as 6.1; § 6.4 said concurrent CLI revoke + touchLastUsed left "both states valid" with "observability-grade" failure mode. Codex correctly identified the bug: a stale manifest snapshot held by the touch path could overwrite a fresh revoke and silently clear revoked_at back to null, breaking acceptance criterion #6 (post-revoke 401 within next request) under concurrent CLI revoke + in-flight server request. The ADR was promising security-grade behavior on a path that was actually last-write-wins. Fix: - § 6.3 rewritten with explicit read-modify-write discipline: touch MUST re-read latest manifest from disk inside the per-key write-lock, NO-OP if revoked_at is non-null, otherwise merge last_used_at preserving all other fields including revoked_at. - § 6.4 reframed from "both states valid" to "revoke dominates touch" safety frame, citing § 6.3 as the load-bearing discipline. The CLI revoke writer always wins the dimension that matters; touch may lose its last_used_at update if it raced. - § 10 criterion #7 expanded to test all three orderings (revoke -> touch, touch -> revoke, interleaved) with the explicit MUST: revoked_at is non-null and equals the revoke writer's timestamp after any interleaving; FAIL if any path produces revoked_at: null. - Forward-path § 6.4 file-locking note updated to clarify §6.3 already holds the contract single-process; flock adds defense-in- depth for rare multi-writer TOCTOU. P2 — § 11 forward path step (1) — Node baseline version history corrected Original wording "Node v22.5.0+ for unflagged but RC; Node TBD for stable" was wrong. v22.5.0 added with --experimental-sqlite flag; v22.12 still required the flag; the module moved past flag-gating in v22.13.0 (LTS) / v23.4.0 (current); entered Release Candidate at v25.7.0 per current docs. Fix: § 11 forward path step (1) rewritten with accurate versions + two Node release-history URLs cited (https://nodejs.org/download/ release/v22.12.0/docs/api/sqlite.html and https://nodejs.org/api/ sqlite.html). Minimum non-flag-gated baseline is now stated as >=22.13.0 (LTS) / >=23.4.0 (current); stable baseline TBD pending Node v25.x+. The rejection-evidence paragraph earlier in § 11 ("v22.12 still required --experimental-sqlite ... current docs mark RC") was already correct and is untouched. P3 — CHANGELOG D43-B line-count corrected Entry said ADR was "~270 lines"; actual file is 420 lines after both fold-ins. Changed to "~420 lines after fold-ins". Phase 2 fold-in #2 bullet enumerates the 3 fixes in this commit; fold-in #1 bullet retained for the opus reviewer round. Test count: 468 / 468 (npm test verified locally after fold-in; design-only doc changes, no test file touched). Authority: PR #19 maintainer text review findings 2026-05-25; CLAUDE.md release_kit overlay phase_rolling_mode — under Unreleased; Node SQLite docs URLs cited in ADR § 11 forward path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
11 KiB
OLP v1.x Roadmap — Deferred Work Tracker
Purpose. Single landing page for every Phase-1 deferral that an actual v1.x sprint must pick up. Each entry cross-references its ratifying ADR, its GitHub issue (if any), and the load-bearing code anchor so a future maintainer can resume without spelunking the commit history.
Status: Living document. Add new entries at the top. Each item should answer:
- What is deferred?
- Why was it deferred (link the ratifying ADR amendment).
- Where does the work live in the tree today (file + anchor).
- When does it need to land (trigger: load profile, security event, governance amendment).
Reading order for a v1.x sprint kickoff. Items #1–#3 are the most architecturally consequential and should be designed in dependency order: #2 (multi-key auth) blocks header gating in #1 and observability ownership in #4. #1 (streaming SF) blocks #5 (soft trigger reactivation) only if soft triggers are wired on streaming requests.
#1 — Streaming-path singleflight + TOCTOU close
- What.
cacheStore.getOrComputeStreaming(keyId, cacheKey, sourceFactory)API replacing the currentpeek + spawnpattern inserver.mjs. Per-(keyId, cacheKey) inflight Map with tee fan-out, bounded per-client backpressure queues, late-joiner replay buffer, AbortController propagation on all-disconnect. - Why deferred. Personal/family-scale single-tenant load — N concurrent identical streaming requests is an edge case that has not been reported. Each concurrent caller receives the correct response; the waste is N CLI processes instead of one.
- Design ADR (ratified).
docs/adr/0005-cache-cross-provider.mdAmendment 8 — full design including the inflight Map shape, tee policy, late-joiner replay, backpressure cap, D38 semaphore coordination, abort policy, cache TTL race handling, observability event set, and X-OLP-Streaming-Inflight header. Implementation acceptance criteria are in Amendment 8 §13. - Tracking issue. GitHub issue #16 — STAYS OPEN as v1.x tracker. Sibling: the closed-but-not-implemented Amendment 6 deferral (D34 F1).
- Code anchors today.
server.mjslines ~782 (preCheckHit = await cacheStore.peek(...)) and ~811–817 (streaming branch entry) — these are the lines the new API replaces.lib/cache/store.mjsgetOrCompute— sibling API; the new one mirrors its shape on the streaming path.
- Trigger to start. Any of: (a) report of N>1 concurrent identical streaming requests in the wild, (b) v1.x sprint planning kickoff with the maintainer explicitly opening this scope, (c) downstream feature requiring tee-streaming primitive (e.g., browser-side observer attaching to an existing stream).
- Estimated effort. Design ADR ratified (Amendment 8) = 30 min done. Implementation = 200–400 lines + 15-20 tests + fresh-context reviewer pass. ~3-4 hours of subagent runtime with full Iron Rule 10 discipline.
#2 — Multi-key auth (lib/keys.mjs) — PHASE 2 ACTIVE (no longer deferred)
- Status. Phase 2 active as of 2026-05-25. Design ratified at D43-B. This entry stays for cross-reference but is no longer a v1.x deferral; implementation D-days D44+ execute within Phase 2.
- What. Per-API-key identity, namespace scoping for the cache, ownership tier (owner vs guest) for header gating, and audit log of which key issued which request. Detailed scope in ADR 0007.
- Design ADR (ratified).
docs/adr/0007-multi-key-auth.md— Option 2 (filesystem manifest) + opaque token, with explicit forward path to Option 3 hybrid (SQLite-indexed mirror) when Phase 3+ Dashboard / SQL-aggregate quota work justifies. Migratable, manifest-as-SPOT. - Tracking. Not a GitHub issue. Tracked here + via ADR 0007 acceptance criteria (§ 10) which drive the D44+ test surface.
- Resolves.
X-OLP-Fallback-Detailowner-only gating (D40 / ADR 0004 Amendment 5 — currently ungated; Phase 2 re-gates per ADR 0007 § 7)./healthper-key visibility (currently anonymous-only — owner / guest / anonymous tiers per ADR 0007 § 7).
- Code anchors today (unchanged at ADR ratification; replaced by D44+ implementation).
lib/cache/store.mjs:77-79per-keyId namespace Map — wire is in place.lib/cache/store.mjs:287singleflight composition${keyId}:${cacheKey}— wire is in place.server.mjs:502, :531— the twokeyId='__anonymous__'call sites to replace.server.mjs:392—/healthhandler entry (Phase 2 gate).server.mjs:1072, :1101—X-OLP-Fallback-Detailheader-write paths (Phase 2 gate).
- Trigger (already fired). Maintainer opened Phase 2 sprint 2026-05-25.
#3 — Soft trigger reactivation (ADR 0004 Amendment 2)
- What. Per-provider
quotaStatuspolling,softThresholdcomparisons, soft-skip advancement when quota approaches limit. CurrentlyevaluateSoftTriggersalways returnsfalsebecausequotaSnapshotis never populated. - Why deferred. v0.1 hard triggers (SPAWN_FAILED / CLI_NOT_FOUND / SPAWN_TIMEOUT / CONCURRENCY_LIMIT) are sufficient for fallback advancement at personal/family scale. Soft triggers require persistent quota snapshots and a polling mechanism, which adds operational surface (timer drift, snapshot staleness, observability burden).
- Design ADR.
docs/adr/0004-fallback-engine.mdAmendment 2 — explicit v1.x deferral with mitigations (startup warning if user configures soft thresholds without runtime enforcement). - Tracking. Not a GitHub issue. Tracked here + via the startup warning in
server.mjs(the_softTriggersConfiguredwarn emission). - Blocks.
- Issue #8 (
X-OLP-Provider-Usedchain-origin semantics) — Option A (trackfirstAttemptedProvider) becomes preferable once soft triggers can fire. See ADR 0004 Amendment 6 § v1.x re-evaluation. X-OLP-Fallback-Detailtrigger_type: 'soft'path — currently dead code, becomes live with this work.
- Issue #8 (
- Code anchors today.
lib/fallback/engine.mjsevaluateSoftTriggers(returns false unconditionally at v0.1).lib/providers/base.mjsProvider.quotaStatuscontract (declared but unused at v0.1).
- Trigger to start. First quota-rate-limit event in the wild — at which point the operator would want pre-emptive advancement rather than spawn-then-fail.
#4 — /health activeSpawns integration
- What. Surface D38
getActiveSpawnCount(providerName)per-provider on the/healthendpoint at the pathproviders.status.<name>.activeSpawns. - Why deferred. D38 (issue #1) shipped the runtime enforcement and exported
getActiveSpawnCount;/healthintegration was scoped out as forward-looking polish. - Design ADR.
docs/adr/0002-plugin-architecture.mdAmendment 6 — names the target path explicitly: "/healthintegration deferred — when surfaced there will land atproviders.status.<name>.activeSpawns; not wired at D38." - Tracking. Not a GitHub issue. Tracked here.
- Code anchors today.
lib/providers/index.mjsexportsgetActiveSpawnCountalready.server.mjs handleHealth— extension point for the new field.
- Trigger to start. First time the maintainer wants per-provider concurrency visibility for capacity planning.
#5 — Provider-level cacheKeyFields (per-plugin mask)
- What. Per-plugin declaration of which IR fields are actually consumed by the underlying CLI invocation, used by
computeCacheKeyto skip fields that the plugin drops at spawn. Reduces spurious-miss rate from the v0.1 conservative-posture trade-off (Amendment 7). - Why deferred. At personal/family scale the extra spawn cost from spurious misses is negligible. The contract extension adds complexity (per-plugin field set + plumbing through
buildDefaultChain→executeHopFn→computeCacheKey). - Design ADR.
docs/adr/0005-cache-cross-provider.mdAmendment 7 § Forward path. - Tracking. Not a GitHub issue. Tracked here.
- Code anchors today.
- Plugin file headers — each lists its "fields dropped at spawn" table for human reference; the v1.x amendment makes that table machine-readable.
lib/cache/keys.mjs computeCacheKey— would acceptpluginCacheKeyMaskparameter.
- Trigger to start. First time spurious-miss rate becomes a measurable load factor.
#6 — Streaming-path SPAWN_FAILED salvage
- What. Currently the streaming branch does NOT participate in D16 salvage (the salvage-on-SPAWN_FAILED + chunks pattern that the buffered path uses). Streaming SPAWN_FAILED mid-stream → the truncation marker (D35 #10) fires, but no salvage logic captures partial chunks for downstream cache reuse.
- Why deferred. Less impactful than #1 — at most one client benefits per spawn event, and the buffered path already provides salvage for the bulk of requests. Streaming is the minority path.
- Design ADR. Not yet ratified. Coordinated with #1 because the tee architecture changes the salvage semantics (multiple clients may want different finish_reason interpretations on source-mid-stream-failure).
- Tracking. Not a GitHub issue. Tracked here.
- Trigger to start. Bundled with #1 implementation work (the inflight tee architecture changes the salvage semantics, so designing them together is cheaper than serializing).
#7 — AUTH_MISSING tuple path test coverage (D40 follow-up)
- What. Dedicated test in
test-features.mjsSuite D40 that asserts thefallbackDetailtuple records the AUTH_MISSING path withtrigger_type: 'auth_missing'. D40 reviewer flagged this as the last gap in the engine-path matrix; code is structurally correct, just lacks an explicit pin. - Why deferred. Low priority — the AUTH_MISSING early-return branch has the tuple push BEFORE it (verified in D40 reviewer pass), so coverage is implicit via the other engine-path tests. A 3-line dedicated test would make the pin explicit.
- Design. No ADR needed. ~5-line test addition.
- Tracking. Not a GitHub issue. Tracked here.
- Trigger to start. Next routine test-suite hardening pass, OR when AUTH_MISSING handling is changed for any reason.
Adding a new entry
When a future D-day defers work, the deferring commit should:
- Always update this file with a new entry at the top.
- Always name the ratifying ADR amendment (or note "no ADR yet — future work needs one").
- Always name the load-bearing code anchor (
file:lineform preferred over symbolic names — the symbolic name can drift). - Always name a concrete trigger to start the work — vague triggers ("when needed") let entries rot.
- If the deferral has a GitHub issue, keep it OPEN and reference it here. If it does NOT, leave a note explaining why (e.g., "tracked here only — no external governance event filed").
The maintainer's session-startup discipline should grep this file at sprint kickoff. If an entry's "trigger to start" condition is met, it leaves this page and becomes a sprint item.