9 Commits
Author SHA1 Message Date
cc250e71bf fix(olp-connect): line 547 referenced undefined \$remote_host (bash nounset crash) (#61)
bin/olp-connect declares \$host and \$port locally (line 436) but line 547
referenced an undefined \$remote_host. Under set -u (nounset) the script
aborted with "remote_host: unbound variable" on every successful /health
probe path that surfaced an anonymousKey, blocking the zero-config
client setup that D68-D70 + ADR 0011 designed.

Fix: use \${host}:\${port} (the actual local variables) for the error-log
context. The variable was only used as a message-context string passed
into validate_olp_token, so no behavior change beyond the message.

Discovered while bringing up MacBook as anonymous client of PI231 server
(2026-05-27). Reproduced cleanly on the v0.5.1 main branch.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:25:17 +10:00
fa2d1af130 feat+test: F4 CLI/plugin quota_v2 migration + v1.x #7 AUTH_MISSING test pin (#59)
## F4 — bin/olp.mjs + olp-plugin/index.js migrate to quota_v2

Codex post-v0.5.0 review Q4: both `olp usage` CLI and `/olp usage`
plugin handler read legacy `body.quota` shape, which never has
`percent_used` or meaningful `available` data → display always fell
through to "no quota api" even when anthropic live quota was visible
on the dashboard (D81/D82).

Authority: codex review Q4 (PR #58); ADR 0008 Amendment 2 (quota_v2
shape: { provider, status, utilization, reset, representative_claim,
fallback_percentage, overage, failure?, failure_kind?, backoff_until? }).

### bin/olp.mjs cmdUsage

- When `body.quota_v2` present (non-empty array, server v0.5.0+): render
  per-provider rows with status badge (live/stale/unreachable/unavailable),
  5h + 7d utilization % (color-coded: green <50% / yellow 50-80% / red ≥80%),
  reset countdowns, binding claim, ⚠ stale /  unreachable annotations.
- When `body.quota_v2` absent: fall through to existing legacy `body.quota`
  rendering (preserves backwards compat with pre-v0.5.0 servers).
- Added `formatResetCountdown(epochSeconds)` — 5-range formatter (past /
  <1h / <24h / <7d / ≥7d), ported from dashboard.html D82. Exported for
  tests. Kept in bin/olp.mjs (not lib/) per spec guidance.
- Added `formatAgo(diffMs)` — small helper for stale-row age display.

### olp-plugin/index.js fmtUsage()

- Same quota_v2 migration. Plain-text output (no ANSI), one line per
  provider. Legacy body.quota fallback preserved.
- Added `pluginFormatResetCountdown(epochSeconds)` — intentionally
  duplicated from bin/olp.mjs (olp-plugin ships as a separate package
  and must not import from bin/). Exported for tests.
- Also fixed fmtUsage to read `w.request_count` (dashboard-data shape)
  in addition to legacy `w.requests` (OCP-era shape) for completeness.

### Backwards compat

- Pre-v0.5.0 server returns body.quota only → both surfaces use legacy
  display (unchanged behaviour).
- v0.5.0+ server returns both quota and quota_v2 → both surfaces prefer
  quota_v2.
- Legacy code paths kept (5-10 lines each, not deleted).

## v1.x roadmap #7 — AUTH_MISSING tuple path test coverage — CLOSED

The dedicated test was already shipped at D56 (test-features.mjs line
6255: 'engine: AUTH_MISSING terminates chain, fallbackDetail tuple
records trigger_type:"auth_missing" (D56, v1.x roadmap #7)'). This
commit closes the roadmap entry with a date stamp and PR reference.

Authority: docs/v1x-roadmap.md § "#7 — AUTH_MISSING tuple path test
coverage (D40 follow-up)".

## Tests

Suite 40 (9 new tests — 40a through 40i):
  40a — cmdUsage parses quota_v2 live rows (mock server)
  40b — cmdUsage falls back to legacy body.quota when quota_v2 absent
  40c — olp-plugin fmtUsage parses quota_v2 live row
  40d — olp-plugin fmtUsage falls back to legacy body.quota
  40e — formatResetCountdown covers all 5 time ranges
  40f — pluginFormatResetCountdown covers past/<1h/<24h/<7d/≥7d
  40g — cmdUsage renders quota_v2 stale row with ⚠ stale note
  40h — cmdUsage renders quota_v2 unreachable row with  indicator
  40i — cmdUsage renders quota_v2 unavailable rows correctly

759 → 768 tests, 0 fail.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 11:24:46 +10:00
704d4fc8a0 fix+docs+release(v0.4.4): D78 — olp-connect stale strings + CDN-safe README URL + pre-publish audit (#49)
* fix+docs+release(v0.4.4): D78 — olp-connect stale strings + README CDN-safe tag URL + repo public-flip pre-audit

Patch release on top of v0.4.3. Three small issues caught when running
olp-connect for real on MacBook (D77 client-install verification):

## G11: repo visibility flip

Repo dtzp555-max/olp flipped PRIVATE -> PUBLIC during this session.
Closes the original G11 finding (anonymous curl can't fetch raw URL from
private repos). Pre-publish audit (per cc-rules pre-publish-audit.md
checklist):
- Identity scrub: 0 hits — no taodeng, no 老大, no /Users/.../ paths,
  no personal hostnames, no personal emails, no real LAN IPs (only RFC
  documentation placeholders 192.168.1.10 + 10.0.0.5)
- Credential scrub: gitleaks 'no leaks found' — all olp_ matches are
  placeholder (olp_XXXX...) or test fixtures (olp_not-a-real-key-...)
- Git history: maintainer accepted Option A (GitHub-account email already
  verified-public on profile; flip exposes nothing new)

## G11 mitigation: README curl URL CDN-cache-safe

GitHub raw CDN serves a stale 404 for /main/<file> for ~5-15min after a
private->public visibility flip (negative-cache TTL). Tag-pinned URLs
bypass this because the tag ref was never queried while private.

D78 makes README's primary olp-connect curl URL tag-pinned:
  bash <(curl -fsSL .../v0.4.4/bin/olp-connect) <ip>
with /main/ listed as alternative for trusted-head users.

## G12: detect_openclaw claimed plugin not shipped

bin/olp-connect's OpenClaw detection block said "The OpenClaw OLP plugin
(D71-D73) is NOT YET SHIPPED" — but D71-D73 shipped olp-plugin/ at
v0.4.0. Replaces stale text with real install instructions:

  git clone https://github.com/dtzp555-max/olp.git /tmp/olp-repo
  openclaw plugins install /tmp/olp-repo/olp-plugin
  # or symlink: ln -sf .../olp-plugin ~/.openclaw/extensions/olp

Points at docs/integrations/openclaw.md for the full setup with
dedicated bot apiKey + restart-gateway notes.

## G13: olp-connect self-version hardcoded literal

Pre-D78 the script declared OLP_CONNECT_VERSION="0.4.0-phase4" as a
hardcoded literal that nobody updated through v0.4.1 / v0.4.2 / v0.4.3.
D78 derives the version at runtime from sibling package.json via
python3. When invoked from a checked-out repo, version resolves to the
actual value; when curl-piped (no on-disk package.json next to script),
falls back to "unknown".

  bash bin/olp-connect --version  # -> olp-connect 0.4.4 (automatic)

## Test count

717 (v0.4.3) -> 720 (v0.4.4). +3 D78 regression tests in Suite 36:
- 36v: pins absence of NOT YET SHIPPED text + presence of real install path
- 36w: pins runtime version derivation from package.json
- 36x: pins README tag-pinned URL recommendation

## Authority

- D77 MacBook client-install verification session (2026-05-26)
- ~/.cc-rules/docs/guides/pre-publish-audit.md (the checklist that
  preceded the visibility flip)
- Process learning: every README that includes a `curl raw-URL | bash`
  install pattern should pin to a release tag (not /main/) for CDN-
  cache resilience.

## Out of D78 scope (deferred)

- F6 (doctor client-side limitation) — Phase 5 ADR amendment
- D75 reviewer P2-1 (ADR 0004 per-hop schema) + P2-2 (defensive type
  assert) — non-blocking
- scripts/migrate-from-ocp.mjs — Phase 7

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: D78 reviewer P2 fold-in — _resolve_version defensive guards (require /bin suffix + env-var path passthrough + nounset default)

---------

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 14:21:20 +10:00
6edf6e0b94 fix+release(v0.4.2): D75 — codex CLI v0.133.0 schema + per-hop model override (#47)
Patch release fixing 5 bugs caught by real-machine E2E testing on PI231 +
Mac mini (2026-05-26 session). Prior D-day reviewers + the post-v0.4.0
maintainer review all missed these because they reviewed against spec text
and against the local OLP install's cached codex CLI shape, not against a
fresh `npm install -g @openai/codex` on a remote operator host getting
v0.133.0 for the first time.

F1 — codex auth.json schema pin (lib/providers/codex.mjs readAuthArtifact)
  Real codex CLI v0.133.0 nests the access token under `tokens.access_token`,
  not at top-level access_token / token / accessToken. Pre-D75 readAuthArtifact
  returned null → OLP reported "auth artifact missing" even for fully
  logged-in users. Fix: prepend creds?.tokens?.access_token to the precedence
  chain at both override + default branches. Legacy fields preserved as
  fallback. Authority: codex CLI v0.133.0 on-disk auth.json shape verified
  empirically on PI231 2026-05-26 E2E session.

F2 — codex spawn args + --skip-git-repo-check (lib/providers/codex.mjs irToCodex)
  codex CLI v0.133.0 trusted-directory sandbox refuses with "Not inside a
  trusted directory" outside git repos. OLP deploys typically outside a git
  repo. Fix: add '--skip-git-repo-check' to args before '--model'. Authority:
  codex CLI v0.133.0 reference (`codex exec --help` documents the flag).

F3 — codex NDJSON event shape pin (lib/providers/codex.mjs codexChunkToIR)
  Real v0.133.0 stream: thread.started → turn.started → item.completed
  (item.type='agent_message', item.text=<response>) → turn.completed.
  D6 defensive parser only recognised top-level content/delta/text +
  type:'stop'/done:true → every chunk silently dropped → response body had
  content: null. Fix: add three new recognisers (item.completed → delta;
  turn.completed → stop; turn.failed → error) before the legacy fallback
  chain. Legacy recognisers preserved for backward/forward compat.

F4 — `olp status` reads body.stats.cache.size, not body.cache.entries
  (bin/olp.mjs cmdStatus). Server payload nests stats under stats.cache;
  CacheStore.stats() exposes {hits, misses, size, inflightCount} — there is
  no `entries` field. D74 P2-3 fixed cmdUsage + cmdCache for the same bug
  class but missed cmdStatus.

F7 — per-hop chain `model` overrides IR model in provider.spawn()
  (server.mjs executeHopFn + streaming sourceFactory). Pre-D75 executeHopFn
  used hopModel for cache key + audit ctx but passed the original irReq
  (with irReq.model = user's request) to provider.spawn(). Chain config
  [{anthropic, claude-X}, {openai, gpt-5.5}] would always spawn BOTH plugins
  with --model claude-X — openai rejected the unknown model and the chain
  died. This broke the core OLP value prop (cross-provider fallback with
  provider-appropriate model substitution). Fix: build per-hop IR variant
  with { ...irReq, model: hopModel } and pass to spawn. Conditional skips
  clone when hopModel === irReq.model. Applied to BOTH buffered path AND
  streaming path. Authority: ADR 0004 § Chain advancement step 1 (per-hop
  config supplies provider AND model — contract always specified, code
  didn't complete it).

Out of scope (deferred to Phase 5):
- F5 (server bind / OLP_BIND env) — needs anonymous-key trust review
- F6 (doctor client-vs-server-side limit) — needs trigger-taxonomy ADR

Test count: 704 → 714 (+10 Suite 36 D75 regression tests: 36i–36r).
Files touched: lib/providers/codex.mjs, bin/olp.mjs, server.mjs,
test-features.mjs, package.json, CHANGELOG.md.

Phase 5 process learning: every provider plugin D-day must include a
real-CLI E2E on a remote operator host before merging — not on the
maintainer workstation (which may have an older CLI cached from a prior
install). D6/D7 codex E2E was deferred and that deferral compounded across
3 layers. F7 reinforces a separate lesson: when a function signature takes
(provider, model, ir), reviewers must check that `model` is consumed
everywhere downstream — not just at the call site they happened to look at.

Authority: ADR 0002 (provider contract — codex plugin), ADR 0004 (fallback
engine — per-hop model contract), lib/providers/codex.mjs D6 assumption
A2/A3/A4 docstrings (which all said "D7 will pin" and D7 never did); codex
CLI v0.133.0 on-disk schema + `codex exec --help` output verified
empirically on PI231 (2026-05-26 E2E session); Iron Rule 第二律
evidence-over-should-work; CLAUDE.md release_kit.phase_rolling_mode
cross-Phase discipline ("hotfix to a shipped Phase N deliverable → bump
patch, tag, release before next push").

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:50:41 +10:00
f3716a19fd fix+release(v0.4.1): D74 post-Phase-4 hotfix batch — 5 maintainer-review findings (#46)
* fix+release(v0.4.1): D74 post-Phase-4 hotfix batch — maintainer-review findings

Patch release fixing 5 issues caught by maintainer post-v0.4.0 independent
review (main / v0.4.0 / commit ee4d945). All five are real runtime bugs
the per-D-day fresh-context opus reviewers missed because they checked
spec text instead of runtime contracts (default auth.allow_anonymous:
false, real /health payload shape, real /cache/stats payload shape, real
/v0/management/dashboard-data payload shape).

Phase 4 process learning: every implementation D-day MUST include at
least one test that boots the server with default production config and
exercises the new feature end-to-end. D74 Suite 36 pins the wire-
contract shapes so a future D-day refactor can't silently re-break the
CLI / plugin / docs.

## P1-1 — olp doctor false-negative on auth-required /health

lib/doctor.mjs: buildBuiltinChecks() accepts opts.authHeaders and passes
to httpGet for both server.running + server.version probes. The
server.running check now distinguishes 401/403 ("server up, bearer token
missing or invalid — set OLP_API_KEY") from "server unreachable" so the
kind discriminator routes to a clean fix-auth path instead of fix_server
when operator just forgot to export the env var.

bin/olp.mjs cmdDoctor: threads authHeaders() through to runDoctor.

## P1-2 — olp-connect token validation + shell-quoting

bin/olp-connect: validate_olp_token() enforces ^olp_[A-Za-z0-9_-]{43}$
(per ADR 0007 § 3 token format) at THREE input sites: --key arg,
/health.anonymousKey server-advertised consumption, interactive prompt
fallback. shell_quote() POSIX-single-quote-wraps with embedded-quote
escape per:
  foo'bar  →  'foo'\''bar'
Applied to all rc-file writes + dry-run output. systemd
environment.d/olp.conf write additionally rejects embedded newlines.
Hostile or malformed keys can no longer persist as shell startup
injection.

## P2-3 — olp usage + olp cache human formatter wire-contract fix

bin/olp.mjs cmdUsage previously read body.usage_24h.requests /
body.providers / body.top_fallback_chains — none of which exist in the
real server payload (server.mjs:2027 + lib/audit-query.mjs). Users saw
"requests: ?" + missing per-provider quota + missing top-chains. Now
reads body.window_24h.request_count / body.cache_hit_24h.hit_rate /
body.quota / body.top_fallback_chains_24h.

bin/olp.mjs cmdCache previously read body.entries / body.bytes /
body.maxBytes (OCP-era field names). Real CacheStore.stats() returns
{hits, misses, size, inflightCount}. Now reads body.size /
body.inflightCount + computes hit rate from hits/(hits+misses).

## P2-4 — olp-plugin/ fmtHealth iterates providers.status

olp-plugin/index.js: previously walked Object.entries(body.providers)
which surfaced `enabled` / `available` / `status` as pseudo-providers
(chat showed "🟢 status" instead of "🟢 anthropic"). Now extracts the
real provider map from body.providers.status, renders enabled/available
counts in a header line, lists per-provider names with activeSpawns when
present. Falls back to flat body.providers.* for older OCP shape
(backwards compat).

## P3-5 — stale v0.3.0-era doc strings updated

README.md: header status line + Implementation Status § now reflect
v0.4.0 shipped + Phase 5 open. Known-limitations Phase 3 line moved out
of "pending v0.3.0" state; new Phase 4 line added with full deliverable
list.

server.mjs: startup banner no longer hardcodes "Phase 1 in progress"
(now just lists version + provider count). Banner derives state from
VERSION so future Phase boundaries don't need touch-ups here.

## Test count

696 (v0.4.0) → 704 (v0.4.1). +8 D74 regression tests in Suite 36:

- 36a: runDoctor accepts authHeaders + threads to checks
- 36b: server.running distinguishes 401 (auth) from "server down"
- 36c: olp-connect rejects malformed --key (validator fires before rc write)
- 36d: olp-connect accepts properly-formed olp_ token
- 36e: CacheStore.stats() shape pin + cmdCache source pin
- 36f: dashboard-data payload shape pin + cmdUsage source pin
- 36g: olp-plugin fmtHealth iterates providers.status not providers.*
- 36h: server.mjs banner doesn't hardcode stale phase

## Authority

- Maintainer independent review of main / v0.4.0 / commit ee4d945
  (2026-05-26 session — 5 findings P1×2 + P2×2 + P3×1)
- Iron Rule 第二律 (evidence over "should work") — runtime smoke
  against default production config now mandatory per Suite 36 pattern
- CLAUDE.md release_kit.phase_rolling_mode cross-Phase discipline
  ("hotfix to a shipped Phase N deliverable → bump patch, tag, release
  before next push")
- ADR 0007 § 3 (token format ^olp_[A-Za-z0-9_-]{43}$) — D74 P1-2
  validator authority

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: Suite 36 paths use import.meta.dirname for CI portability (was hardcoded /Users/taodeng/olp/)

---------

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 10:43:12 +10:00
0bdecd1235 feat+test+docs: D68-D70 — bin/olp-connect + /health.anonymousKey + ADR 0011 (#43)
* feat+test+docs: D68+D69+D70 — bin/olp-connect + /health.anonymousKey + ADR 0011

Third substantive Phase 4 implementation. 3 D-days bundled per Iron Rule
11 IDR — olp-connect consumes /health.anonymousKey for zero-config
client setup, both governed by ADR 0011's trusted-LAN-only invariant.

## D68 — bin/olp-connect (zero-config client setup)

Ports OCP ocp-connect (721 lines) → OLP olp-connect (564 lines, pure bash).
Bash over Node (per ADR 0010 § Notes) because client machines may lack
recent Node; bash + curl + python3 = max portability.

CLI: `olp-connect <host-ip> [--port PORT] [--key API_KEY] [--no-system-env]
                            [--dry-run] [--help] [--version]`

Workflow:
1. Connectivity probe (curl /health, 5s timeout, distinguishes TCP
   unreachable from auth-required)
2. Auth resolution: --key flag → /health.anonymousKey (D69) → interactive
   prompt fallback
3. Smoke test (GET /v1/models with bearer)
4. IDE detection + per-IDE config:
   - Claude Code: detect + warn (NOT supported as OLP client per ADR 0010)
   - Cline: detect + print manual VSCode-settings snippet
   - Continue.dev: detect (extension OR ~/.continue/config.yaml) + write
     idempotent models: entry
   - Cursor: detect + print snippet + WARNING (per prior-art known
     base-URL fragility)
   - Aider: detect + write OPENAI_API_BASE + OPENAI_API_KEY to rc files
   - OpenClaw: detect + print "install /olp plugin (D71-D73 deliverable)"
5. System-level env: macOS launchctl setenv / Linux ~/.config/environment.d
   (so VSCode/Cursor started via Dock inherit)
6. Summary + test command

Idempotent (bracketed `# OLP LAN (added by olp-connect)` ... `# /OLP LAN`
blocks in rc files). --dry-run exercises every state-change site without
modifying anything. Exit 0/1/2 conventions.

Installed via package.json bin so `npx olp-connect` works.

## D69 — /health.anonymousKey + auth.advertise_anonymous_key

server.mjs handleHealth emits OPTIONAL `anonymousKey: "olp_..."` field
when ALL THREE prerequisites hold:
1. config.json auth.advertise_anonymous_key === true
2. config.json auth.allow_anonymous === true (per ADR 0007 § 7)
3. At least one non-revoked key has plaintext_advertise field set

Default-off: field is ABSENT (not null) — preserves v0.3.x /health shape;
existing tests don't regress.

bin/olp-keys.mjs new flags: `keygen --anonymous --advertise` writes the
plaintext into the manifest's optional `plaintext_advertise` field AND
prints a WARNING about disk-storage + /health exposure + ADR 0011
pointer. Owner-tier --advertise rejected at BOTH CLI + lib layers.

Implementation note: reused existing guest tier (no new owner_tier:
'anonymous'); plaintext_advertise is a forward-compat optional manifest
field per ADR 0007 § 4 unknown-fields-allowed convention. Cleaner than
introducing a new tier.

anonymousKey appears in BOTH trimmed AND full /health payloads — the
trimmed payload's purpose is to be readable by anonymous clients so they
can self-bootstrap. Tested.

Startup warns on prereq failure (anonymous_key_advertised_but_denied /
anonymous_key_advertised_but_no_anonymous_key_exists) so the relaxed-
posture failure mode is observable. Graceful-degrade: server still
boots; handleHealth re-checks at request time and silently omits the
field when any prereq fails (defense-in-depth).

## D70 — ADR 0011 (anonymous-key deployment-context limits)

New ADR codifying the trusted-LAN-only invariant.

Trade-off documented: anonymous key advertised via /health = anyone who
can reach the server can read /health and use the key. Acceptable ONLY
when "anyone who can reach the server" ≈ "trusted family on the LAN".
Public-internet deployment = instant compromise.

Soft enforcement: server logs startup warn if BIND_ADDRESS resolves to
a public IP AND advertise_anonymous_key: true. No hard allowlist (TLS-
fronted private networks indistinguishable from public from server's
perspective).

Re-evaluation trigger: any time OLP gains "expose to public internet"
deployment mode (e.g., Cloudflare Tunnel guidance in README), revisit.

References ADR 0007 § 7 (identity classes), ADR 0010 § Phase 4 charter
D68-D70 line, OCP server.mjs:148/1454/1488/1555 (PROXY_ANONYMOUS_KEY
reference).

## Test count

658 → 672 (+14 D68-D70 tests across Suite 34: 5 keys.mjs unit + 6 /health
HTTP integration + 3 CLI integration).

## Scope discipline

NO /v1/messages entry surface (out of Phase 4 per ADR 0010).
NO olp-plugin/ Telegram plugin (D71-D73).
NO docs/integrations/*.md files (D71-D73).
NO CHANGELOG / package.json version bump (Phase 4 close handles versioning;
only package.json bin entry for olp-connect added).
NO new npm deps.

## Authority

- ADR 0010 § Phase 4 D-day plan D68-D70 line
- ADR 0011 (this commit — new ADR)
- ADR 0007 § 4 (manifest forward-compat unknown fields) + § 7 (identity
  classes) + § 9 (keygen flow) — extended by D69 plaintext_advertise
- OCP ocp-connect /Users/taodeng/ocp/ocp-connect (port reference)
- OCP server.mjs:148, 1454, 1488, 1555 (PROXY_ANONYMOUS_KEY reference)
- 2026-05-26 brainstorm (Top 5 OCP inheritance candidates, item 3:
  /health.anonymousKey + olp-connect zero-config UX)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: D68-D70 reviewer P1 + P2 fold-in — README impact note + listKeys redaction + schema_version note

Reviewer APPROVE WITH MINOR — 0 P0, 1 P1, 2 P2; all three folded in.

P1 — README impact note for new Phase 4 user-visible surfaces.

Per CLAUDE.md release_kit.new_feature_doc_expectations:
- new env / config knob → README § Environment Variables
- new endpoint or response field → README § API Endpoints
- new CLI surface → dedicated §

README now documents:
- /health.anonymousKey optional field (in API Endpoints table) with cross-
  ref to ADR 0011 + the three-prereq gate
- streaming.heartbeat_interval_ms config (D61) + auth.advertise_anonymous_key
  config (D69) under new "config.json keys introduced at Phase 4" subsection
- Operator CLI surfaces summary: olp / olp-connect / olp-keys keygen
  --anonymous --advertise, with cross-refs to ADR 0010 + 0002 Amendment 7

P2-1 — lib/keys.mjs listKeys() now strips plaintext_advertise alongside
token_hash. Callers wanting the advertised plaintext for the /health
publication path MUST go through findAdvertisedKey() — the only sanctioned
read site. Defends against a future caller of listKeys() leaking the
plaintext into logs / HTTP responses / dashboards. Tests still pass
(no in-repo caller of listKeys depends on plaintext_advertise being
present).

P2-2 — ADR 0011 now documents the schema_version-stays-at-1 decision
explicitly. Additive optional fields don't require bump per ADR 0007 § 4,
but a future archaeologist asking "why didn't D69 bump schema_version?"
now has a one-line answer. Same paragraph documents the listKeys()
redaction policy in plain text alongside the manifest-field contract.

672/672 tests still pass.

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>
2026-05-26 09:15:09 +10:00
e69e908dae feat+test+docs: D64-D67 — olp Node CLI + doctor framework + per-provider doctor checks + ADR 0002 Amendment 7 (#42)
* feat+test+docs: D64+D65+D66+D67 — olp Node CLI + olp doctor framework + per-provider doctor checks + ADR 0002 Amendment 7

Second substantive Phase 4 implementation. 4 D-days bundled per Iron Rule
11 IDR — CLI dispatches to doctor; doctor calls into provider plugins via
the new contract method; ADR amendment authorizes the contract change.
Single PR is the minimum reviewable unit for "does plugin amendment +
plugin impl + doctor consumer line up?"

## D64 — bin/olp.mjs Node CLI scaffold

Operator surface for OLP. Node not bash (per ADR 0010 § Notes — bash with
python3 JSON parsing is a known fragile point; OLP standardizes on Node).

Subcommands:
- status / health / usage / models / cache — HTTP calls to existing endpoints
- providers — local: cross-references models-registry.json + config.json
- chain show [<model>] — local: prints routing.chains from ~/.olp/config.json
- logs [N] [--level X] — reads ~/.olp/logs/audit.ndjson via audit-query
- restart — launchctl (macOS) / systemctl --user (Linux), best-effort
- keys ... — delegates to bin/olp-keys.mjs runCli (no logic duplicated)
- doctor [--check <id|category>] [--json] — D65 framework
- help / --help / -h

Token / URL resolution:
- OLP_PROXY_URL env → OLP_PORT env → http://127.0.0.1:4567 (D60 default)
- OLP_API_KEY env → OLP_OWNER_TOKEN env (filesystem manifest tokens are
  one-way SHA-256 per ADR 0007 § 5 — not recoverable; CLI surfaces
  helpful 401 message pointing at olp-keys keygen)

Output:
- Default: human-readable ANSI-colored text (no chalk dep, auto-suppressed
  under --json)
- --json: raw JSON for scripting
- Exit codes: 0=ok / 1=usage / 2=network|HTTP / 3=auth

No npm deps. Built-ins only.

Installed via package.json bin entry so `npx olp <subcommand>` works.

## D65 — lib/doctor.mjs framework

Ports OCP scripts/doctor.mjs (the bedrock of AI-driven self-repair per the
OCP audit's #2 inheritance candidate). Machine-readable next_action so a
Claude Code / Cursor / etc. agent can self-repair OLP.

Check shape:
  { id, category, async run(): { status: 'ok'|'fail'|'warn', message, evidence? } }

Built-in checks: server.running, server.version, config.exists,
config.providers_enabled, config.chains_configured, auth.owner_key_exists,
system.node_version. Per-provider checks collected dynamically via
provider.doctorChecks() per D67.

--json output:
  { checks: [...], kind: noop|update|fix_oauth|fix_config|fresh_install|
    fix_server|fix_provider, next_action: { ai_executable: [],
    human_required: [], verify: 'olp doctor' }, summary }

--check <id-or-category> for tight repair-loop fast paths.

## D66 — Per-provider doctorChecks() implementations

Each shipped plugin contributes its own checks (lives in plugin file so the
provider's maintainer updates it naturally):

- anthropic.mjs: cli_available (claude --version) + oauth_token_present
  (~/.claude/.credentials.json OR ANTHROPIC_OAUTH_TOKEN env)
- codex.mjs: cli_available (codex --version) + auth_present
  (~/.codex/config.json)
- mistral.mjs: cli_available (vibe --version) + api_key_present
  (MISTRAL_API_KEY env OR ~/.vibe/.env)

Each fail returns evidence.fix_commands (for ai_executable[]) or
evidence.human_required (e.g., 'run: claude auth login').

## D67 — ADR 0002 Amendment 7

New amendment adds OPTIONAL provider.doctorChecks(): DoctorCheck[] to the
Provider contract. Backwards compatible — plugins without doctorChecks()
contribute no provider checks (default behavior). Validator extended in
lib/providers/base.mjs validateProvider.

## Test count

636 → 658 (+22 tests across Suites 32, 33).

- Suite 32 — bin/olp.mjs CLI scaffold (10 tests): parseArgv, USAGE,
  unknown-subcommand, providers local + --json, chain show, status via
  ephemeral server with owner token, ECONNREFUSED → exit 2,
  resolveBearerToken precedence
- Suite 33 — lib/doctor.mjs framework (12 tests): all kind branches
  (noop / fresh_install / fix_server / fix_oauth / fix_provider),
  collectProviderChecks reads doctorChecks(), throwing plugin captured,
  --check filter, built-in checks against temp HOME, anthropic plugin
  probe set, resolveProxyUrl precedence, deriveKind/deriveNextAction units

## Scope discipline

server.mjs UNTOUCHED. All HTTP subcommands consume EXISTING endpoints.
No new endpoints. No /health.anonymousKey. No olp-connect. No Telegram
plugin. No IDE docs bundle. No CHANGELOG / package.json version bump
(Phase 4 close handles versioning; only package.json bin entries updated).

## Known limitations (flagged for reviewer)

- olp restart not unit-tested (would require mocking child_process.spawn
  in invasive way; manual smoke-test only at this D-day)
- olp logs --level filtering matches optional level field if present in
  audit-event objects; appendAuditEvent already populates it where
  meaningful — no schema change needed in this bundle
- olp usage panel shape inferred from lib/audit-query.mjs exports; if
  /v0/management/dashboard-data wire shape differs in subtle ways,
  formatter degrades to '?' but --json always works

## Authority

- ADR 0010 § Phase 4 D-day plan D64-D67 line
- ADR 0002 Amendment 7 (this commit — new amendment)
- OCP ocp bash wrapper /Users/taodeng/ocp/ocp (subcommand reference,
  translated to Node)
- OCP scripts/doctor.mjs /Users/taodeng/ocp/scripts/doctor.mjs
  (framework reference)
- 2026-05-26 brainstorm (Top 5 OCP inheritance candidates, item 2:
  olp doctor machine-readable next_action)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: D64-D67 reviewer P2 fold-in — shell-quote ai_executable paths + launchctl kickstart caveat

Reviewer APPROVE — 0 P0/P1, 2 P2 hardening notes folded in.

P2-1 — Shell-quote interpolated paths in fix_commands.

lib/doctor.mjs config.exists fix_commands previously interpolated
${olpHome} / ${configPath} unquoted into the printf template. A
malicious OLP_HOME env value containing shell metacharacters could
inject commands into the suggested-fix string an AI agent (or human)
pastes back into a terminal.

Added _shellQuote(s) helper (POSIX single-quote-wrap with escape for
embedded single quotes per POSIX shell rules). Risk surface is narrow
at family scale (operator local env, single-user proxy), but hardening
cost is one helper.

P2-2 — Document launchctl kickstart -k env-stale pitfall.

cmdRestart header now carries an explicit caveat that `launchctl
kickstart -k` does NOT re-read the plist EnvironmentVariables block —
launchd uses cached env from the most recent bootstrap. This is a
known OCP institutional lesson (PIT INDEX in cc-rules MEMORY.md). The
comment documents the bootout/bootstrap dance for env reloads and
notes that the Phase 4 installer (post-D73) will expose `olp restart
--full` for the safer reload path.

658/658 tests still pass; the _shellQuote change is invisible to
existing tests because the test fixtures use safe paths.

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>
2026-05-26 08:48:20 +10:00
408d5a839a feat+test+docs: D52 — daily audit rotation (lib/audit.mjs + bin/olp-audit-rotate.mjs) (#29)
Fifth Phase 3 D-day. Adds daily UTC-aware rotation to lib/audit.mjs
per ADR 0008 § 5 + ships an external cron tool. Rotation is
SYNCHRONOUS at v0.3.0 — synchronous design eliminates the race that
an async wrapper would create between date-change-detection and the
append.

lib/audit.mjs EXTENSIONS:

  - New _maybeRotateAudit({ olpHome, logEvent }) (sync): probes live
    audit.ndjson; if it holds events from past UTC date, renames it
    to audit-YYYY-MM-DD.ndjson. Idempotent. If target file exists
    (cron beat in-server check), logs warn + skips per § 5.3.

  - appendAuditEvent extended: cheap fast-path date check via module-
    cached _lastSeenUtcDate. On date change, calls _maybeRotateAudit
    synchronously BEFORE appendFileSync — so old-date events land in
    the rotated file and new-date events land in the fresh live file.
    No event straddles the boundary.

  - Why SYNCHRONOUS: an async wrapper would let the sync
    appendFileSync race the not-yet-completed renameSync, landing
    today's event in the about-to-be-renamed file. Sync rotation is
    the only correct ordering at the append-fired-from-many-routes
    scale OLP runs. (Test 26b-1 caught this during local run; the
    initial async-wrapper implementation failed because the live
    file at assertion time didn't exist.)

  - New exports: _maybeRotateAudit (sync), getAuditRotateCount,
    getAuditRotateFailCount, __resetAuditRotateState,
    __setLastSeenUtcDateForTesting.

  - First-event-date discovery: when probing the live file's date,
    reads only the first ndjson line + parses its ts. Falls back to
    file mtime if events absent (corrupt/empty edge).

bin/olp-audit-rotate.mjs (~95 lines): external cron tool per § 5.2.

  Calls _maybeRotateAudit once + reports outcome. Exit codes 0
  (success or no-op), 1 (bad usage), 2 (rotation failed). Installed
  via package.json bin so `npx olp-audit-rotate [--olp-home=<path>]`
  works. Example cron line in file header.

CONCURRENT-SAFETY (§ 5.3):

  In-process sequential appends after the first date-change detection
  short-circuit via the updated _lastSeenUtcDate cache → exactly 1
  rename even under N sequential appends. Cross-process (cron + server)
  coexistence handled by the "target already exists → skip + warn"
  branch.

TESTS — Suite 26, +12 (588 → 600):

  26a-1..5: _maybeRotateAudit (no live file / today already /
    yesterday→rotate / idempotent re-call / cron-race target-exists
    warn)
  26b-1: appendAuditEvent past UTC date change triggers sync rotation
    + append lands in fresh live file
  26c-1: 10 sequential appendAuditEvent across date change → exactly
    1 rotation + all 10 events in new live file
  26d-1..4: bin/olp-audit-rotate.mjs CLI (--help / no-live-file /
    yesterday-file-rotates / unknown-flag exit 1)
  26e-1: rotated files queryable via lib/audit-query.mjs
    discoverAuditFiles + readAuditWindow cross-file read

package.json: bin.olp-audit-rotate + scripts.olp-audit-rotate entries
added.

DOCUMENTATION:

  - AGENTS.md: lib/audit.mjs marker promoted  (D45 append + D52
    rotation both shipped); new bin/olp-audit-rotate.mjs entry.
  - CHANGELOG.md: D52 entry under Unreleased per release_kit overlay.

NOT IN D52:

  - tried_providers schema fix (D53; D45 P2 deferral)
  - E2E + docs polish (D54)
  - Phase 3 close → v0.3.0 (D55; maintainer-triggered)

Test count: 588 → 600 (+12). Verified locally via npm test.

AUTHORITY:

  - ADR 0008 § 5.1 (first-append-after-UTC-midnight trigger).
  - ADR 0008 § 5.2 (external cron alternative).
  - ADR 0008 § 5.3 (concurrent-rotation safety + cron-coexistence).
  - ADR 0008 § 5.4 (renamed-file query path consumed by D49 lib/
    audit-query.mjs).
  - CLAUDE.md release_kit overlay phase_rolling_mode — under
    Unreleased.
  - Standing autopilot grant.

ALIGNMENT.md scope check: extends lib/audit.mjs (a Phase 2 internal
module) + adds new bin/ CLI + small package.json bin/scripts entries.
No provider plugin / entry surface / IR change.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 16:27:26 +10:00
939f3e6bd9 feat+test+docs: D47 — bin/olp-keys.mjs keygen CLI (Phase 2 functional scope closes) (#23)
Fourth Phase 2 implementation D-day. Closes ADR 0007 § 10 acceptance
criterion #9 (bootstrap workflow must be reproducible without manual
file editing) by shipping a minimal keygen CLI per § 9.1. Phase 2
functional scope is complete with this D-day — remaining work is
Phase 2 close → v0.2.0 (maintainer-triggered, explicit per
CLAUDE.md release_kit.phase_close_trigger).

NEW bin/olp-keys.mjs (~250 lines): subcommand CLI

  keygen [--owner|--name=X|--tier=guest|owner|--providers=csv|--force]
    Creates a key + prints plaintext token to stdout ONCE; manifest
    stores only SHA-256 hash. --force revokes existing owner keys
    before creating the new owner (ADR § 9.3 recovery flow).

  list [--owner-only|--include-revoked]
    Lists keys with token_hash redacted.

  revoke --id=<key-id>
    Marks the key's revoked_at; idempotent (already-revoked → no-op
    + status message); missing id → exit 2.

  Common flag: --olp-home=<path> overrides ~/.olp/ (defaults to
  OLP_HOME env then ~/.olp/).

package.json bin field

  "bin": { "olp-keys": "./bin/olp-keys.mjs" } so npx olp-keys ...
  resolves. Also "scripts": { "olp-keys": "node bin/olp-keys.mjs" }
  for npm run.

Module shape (testability)

  Exports runCli(argv, { out, err }) so tests invoke with synthetic
  argv + IO writers (no process spawn). Main guard auto-runs when
  invoked as entrypoint.

Plaintext token discipline (ADR § 5 + § 9.1)

  Plaintext printed exactly once on stdout. Never logged, never
  written to manifest, never written to audit. Operators capture
  immediately; lost → --force revoke + regenerate.

--force async correctness

  cmdKeygen is async and awaits each revokeKey (which is async —
  acquires per-key write lock per § 6.4). Sequence: revoke each
  existing owner manifest atomically → then createKey for new
  owner. Avoids race where create-new runs before revoke-old
  completes.

TESTS — Suite 22, +20 (524 → 544):

  22a-1..5: parseArgv unit (--flag=value, --flag value, boolean,
    mixed positional)
  22b-1..5: keygen (owner default, name+providers, missing-name
    error, invalid-tier error, --force revoke-then-create with
    isolation tmpdir)
  22c-1..3: list (empty, populated with token_hash-redaction check,
    --owner-only filter)
  22d-1..4: revoke (valid id, idempotent re-revoke, missing-id
    error, nonexistent-id error)
  22e-1..3: top-level CLI (--help / no args / unknown subcommand
    exit codes)

DOCUMENTATION:

  - AGENTS.md: lib/keys.mjs marker promoted to ; new bin/olp-keys.mjs
    entry. Implementation-status-note + shipped-set updated.
  - README.md: Implementation Status row added for bin/olp-keys.mjs;
    Known limitations note rewritten to "Phase 2 functional scope
    complete; close pending"; new Bootstrap workflow section with
    copy-pasteable npx commands + recovery flow.
  - CHANGELOG.md: D47 entry under Unreleased per release_kit overlay.

AUTHORITY:

  - ADR 0007 — § 5 token format, § 9.1 minimal keygen command
    surface, § 9.3 recovery, § 10 acceptance criterion #9 covered.
  - CLAUDE.md release_kit overlay phase_rolling_mode — under
    Unreleased.
  - Standing autopilot grant.

Verified: 544/544 pass via npm test (no regression in 524 pre-D47
tests; 20 new Suite 22 tests all green).

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 14:54:05 +10:00