# Changelog ## v3.18.0 — 2026-06-01 Hardening release from a multi-agent code audit (1 P0 + 14 P2 + 2 P3 findings, each adversarially verified and independently reviewed) plus three follow-ups (#123–#125). Every change shipped as its own PR with a fresh-context reviewer (Iron Rule 10). The single-user default path (`AUTH_MODE=none`, no TUI) is behavior-identical **except** the `/health` change in #109. ### Security - **#109 (P0)** — `/health` no longer advertises `PROXY_ANONYMOUS_KEY` to remote callers by default. The `anonymousKey` field is gated behind a new `PROXY_ADVERTISE_ANON_KEY=1` opt-in env var; localhost callers are always exempt. Prevents any LAN-reachable device from harvesting a working, quota-spending bearer credential from the unauthenticated `/health` endpoint. **Behavior change:** `ocp-connect` zero-config Path A now requires the server to set `PROXY_ADVERTISE_ANON_KEY=1`; otherwise pass `--key` or use anonymous access. - **#114** — Dashboard escapes all DB-sourced strings (key names, usage rows) before `innerHTML`; the revoke button uses a `data-` attribute + listener instead of an inline `onclick` a quote could break out of; `POST /api/keys` validates key names server-side (`[A-Za-z0-9 ._-]{1,64}`). - **#124** — Dashboard status/plan summary cards escaped too (uniform defense-in-depth over all `innerHTML` sinks). - **#111** — Streaming error paths strip filesystem paths from claude error text / stderr before sending them to clients (`sanitizeError`), matching the non-streaming path. ### Reliability / correctness - **#110** — Non-array `messages` is rejected with a 400 (was silently hanging the connection until socket timeout); OpenAI array `content` is flattened into the prompt instead of dumped as raw JSON; a streamed upstream error now emits an SSE `error` frame instead of a success-looking `finish_reason:"stop"`. - **#111** — `res.on("close")` escalates SIGTERM→SIGKILL on client disconnect (closes a narrow re-occurrence of the #37 concurrency-slot leak on the hottest exit path); `overallTimer` is cleared on semantic completion so a slow-exiting child can't record a spurious post-success timeout; per-key quota is documented as best-effort (bounded overshoot ≤ `MAX_CONCURRENT`, cache hits uncounted). - **#113** — CLI/installer hardening: `ocp-plugin` restart uses the live uid + `dev.ocp.proxy`/`ocp-proxy` labels and drops the unsafe `pkill` fallback; `ocp-connect` quotes + `chmod 600`s the persisted key; `setup.mjs` XML-escapes and newline-validates injected service-unit secrets. ### Alignment / governance - **#112** — OAuth token-refresh host (`platform.claude.com/v1/oauth/token`) re-verified against the compiled cli.js v2.1.154 (`strings`, no live probe) and recorded in `ALIGNMENT.md`; usage-probe and default request model now derive from `models.json` (ADR 0003 SPOT) instead of hardcoded IDs. - **#123** — The legacy `console.anthropic.com/v1/oauth/token` host is pinned in the `alignment.yml` blacklist so a future OAuth-host drift hard-fails CI; the blacklist now documents its dual purpose (known hallucinations + pinned wrong-host variants of a verified Class A endpoint). ### TUI - **#115** — The TUI LAN gate refuses any non-loopback bind (not just literal `0.0.0.0`); the achieved `cc_entrypoint` is asserted each turn and a `tui_entrypoint_mismatch` warning is logged on a silent degrade to the metered sdk-cli pool. ### Refactor - **#125** — `isLoopbackBind` extracted to `lib/net.mjs`, shared by `server.mjs` and the test suite (was duplicated via a copy-paste mirror). ### New environment variables - `PROXY_ADVERTISE_ANON_KEY` — opt-in (default off); advertise `PROXY_ANONYMOUS_KEY` on the public `/health` body for remote zero-config discovery (#109). ## v3.17.1 — 2026-05-31 ### Fix — code-audit P1/P2 hardening Fixes from a multi-agent code audit (3 P1 + 5 P2, adversarially verified). The single-user default path (`AUTH_MODE=none`, no TUI) is behavior-identical. **Availability / correctness (P1):** - Guard `proc.stdin` against EPIPE — a fast-failing spawned `claude` (auth error, bad model, large prompt) no longer crashes the single-process daemon. - Add `unhandledRejection`/`uncaughtException`/`clientError` safety nets + wrap all request-body read loops — a client aborting mid-upload no longer crashes the daemon. - TUI transcript reader: only `turn_duration` is terminal (was also `tool_use`), which silently truncated any TUI turn that used a built-in tool. **Security gates / cache integrity (P2):** - `AUTH_MODE=multi`: the default spawn now passes `--disallowedTools` (Bash/Read/Write/Edit/…) so a guest prompt cannot drive operator-filesystem tools. Single-user path unchanged. - `/sessions` (DELETE), `/settings` (PATCH), `/logs`, `/usage`, `/status` are now admin-gated (were dispatched before the admin check). - Streaming path no longer caches an `is_error` response as success (cache-poisoning fix). - TUI fail-loud guard extended to `none`+`0.0.0.0` (unless `OCP_TUI_ALLOW_LAN=1`) and `+ PROXY_ANONYMOUS_KEY`. - TUI `send-keys` paste uses `-l` (literal) so a prompt equal to a tmux key token (e.g. `C-c`) is typed, not interpreted. --- ## v3.17.0 — 2026-05-31 ### Provider — default claude invocation ported to stream-json + `--system-prompt` (Phase 6c) OCP's default (non-TUI) claude spawn moves from `claude -p --output-format text` to `claude --output-format stream-json --verbose --no-session-persistence --system-prompt ` (no `-p`). The NDJSON event stream is parsed into the assembled response. Benefits: ~64% per-request cost reduction and anti-hallucination via `--system-prompt` tool-use suppression. Clients see no API change — the OpenAI-compatible request/response shapes are identical. Faithful port of OLP's production-verified implementation; covered by 17 new stream-json parser tests. ⚠️ **Billing note:** from 2026-06-15 this default path carries `cc_entrypoint=sdk-cli` and bills against the Agent SDK credit pool. Use the new opt-in `CLAUDE_TUI_MODE` (below) to keep traffic on the Pro/Max subscription pool. --- ### feat(tui): opt-in CLAUDE_TUI_MODE — serve via interactive claude (cc_entrypoint=cli / subscription pool), single-user only; default stream-json path unchanged From 2026-06-15 Anthropic routes `claude -p` / `--output-format` invocations to the Agent SDK credit pool (`cc_entrypoint=sdk-cli`). This feature adds an opt-in bridge: when `CLAUDE_TUI_MODE=true`, OCP serves each request via a real interactive `claude` session (no `-p`, no `--output-format`) so it carries `cc_entrypoint=cli` and bills against the Pro/Max subscription. The complete string response is read from claude's native JSONL session transcript and replayed to callers as a normal OpenAI completion or chunked SSE. Clients see no API change. The default stream-json path is byte-for-byte unchanged when `CLAUDE_TUI_MODE` is unset. **Security:** single-user / single-operator only. Never enable on a multi-user OCP. See ADR 0007 and README § "Subscription-pool (TUI) mode". New env vars: `CLAUDE_TUI_MODE`, `CLAUDE_TUI_WALLCLOCK_MS`, `OCP_TUI_CWD`, `OCP_TUI_HOME`. New ADR: `docs/adr/0007-tui-interactive-mode.md`. New modules: `lib/tui/transcript.mjs`, `lib/tui/session.mjs` (shipped in preceding commits on this branch). --- ### Model — add claude-opus-4-8 Add `claude-opus-4-8` as the newest Opus to `models.json` (index 0, newest first). Repoint `aliases.opus` from `claude-opus-4-7` to `claude-opus-4-8`. `claude-opus-4-7` remains in the list callable by literal id. `legacyAliases.claude-opus-4` left pointing at `claude-opus-4-7` (no change — legacy alias tracks the prior generation). README Available Models table and model-count references updated accordingly. --- ## v3.16.4 — 2026-05-13 ### Refactor — port-literal SPOT + CI guardrail Closes the structural side of the port-drift cascade addressed by v3.16.2 and v3.16.3. Those two releases reverted plist / plugin / scripts back to 3456 line-by-line, but the underlying invitation to drift — a hardcoded port literal scattered across six source files — was still intact. Changes: - **New `lib/constants.mjs`** — single source of truth for shared literals. Exports `DEFAULT_PORT = 3456`, `LOCAL_HOST = "127.0.0.1"`, `OPENAI_API_BASE = "/v1"`, `LOCAL_PROXY_URL`. - **`server.mjs:127`, `setup.mjs:36`, `scripts/upgrade.mjs:137`, `scripts/doctor.mjs:84` + `:205`, `scripts/sync-openclaw.mjs:73`** — all replaced with imports from `lib/constants.mjs`. Behavior is identical; the literal `3456` now exists in exactly one place per language (`lib/constants.mjs` for `.mjs`, `ocp` + `ocp-connect` for bash, `test-features.mjs` for pinned historical-port tests). - **`.github/workflows/alignment.yml`** — extended the path filter to `setup.mjs`, `scripts/**`, `lib/**`, `ocp`, `ocp-connect`. Added a new `port-spot` hard-fail job that greps for any hardcoded `3478` or `3456` literal in `.mjs/.js/.ts/.json` outside the EXEMPT_REGEX (which lists `lib/constants.mjs`, `test-features.mjs`, the bash CLIs, docs, and the workflow itself). Any future PR re-introducing a hardcoded port literal will be blocked at CI before it can cascade. - Doc comments in `server.mjs` env-var summary and `setup.mjs` usage banner reworded so the literal `3456` no longer appears as documentation text (CI grep is intentionally aggressive — it does not parse comments — so doc strings reference `DEFAULT_PORT from lib/constants.mjs` instead). No behavior change for any user. `CLAUDE_PROXY_PORT` env var remains the runtime override; the only difference is the unset-env fallback now flows through one shared constant. ALIGNMENT.md hard-requirements: this PR modifies `server.mjs` (one-line import + one literal swap, mechanical). No cli.js operation changed; the citation requirement does not apply. SPOT principle (Rule 2 spirit) is the entire motivation. ## v3.16.3 — 2026-05-13 ### Fixes — completes v3.16.2 port-drift revert v3.16.2 reverted the plugin / `openclaw.plugin.json` / README / Mac mini plist back to `3456` (the historical source default since `593d0dc`), but missed three places in `scripts/` that still defaulted to `3478`. Those three lines were the residual cascade source: every time `ocp doctor` or `ocp upgrade` ran without `CLAUDE_PROXY_PORT` in the env, they probed `3478`, reported "OCP not responding" against a healthy 3456 instance, and (in the case of OpenClaw sync follow-ups on the maintainer's host) re-introduced 3478 into downstream config. Changes: - `scripts/upgrade.mjs:137` — default port `3478` → `3456`. - `scripts/doctor.mjs:84` — default port `3478` → `3456`. - `scripts/doctor.mjs:205` — default port `3478` → `3456`. No behavior change for users who set `CLAUDE_PROXY_PORT` explicitly; env still takes precedence. The fix only affects the unset-env fallback, which now matches `server.mjs:126` and the rest of the codebase. Test plan: existing `test-features.mjs` cases that pin `CLAUDE_PROXY_PORT=3478` continue to pass — they use the env path, not the default. ## v3.16.2 — 2026-05-12 ### Fixes — corrects v3.16.1 The v3.16.1 fix was directionally correct (plugin now reads env first, falls back to a hardcoded default) but **the narrative and the hardcoded default were both wrong**. What v3.16.1 said: "OCP server moved to 3478 default in v3.14+; plugin lagged at 3456." What is actually true: - **OCP server source default has been `3456` since `593d0dc` (initial release) and has never changed.** Every line in `server.mjs`, `setup.mjs`, and the `ocp` CLI still uses `3456` as the documented and code-level default. - The single OCP installation observed on `3478` is the maintainer's Mac mini, whose plist was rewritten with `--port 3478` during a PR #71 dogfood smoke-test accident on 2026-05-08 (see `~/.cc-rules/memory/learnings/subagent_setup_mjs_prod_host_collision.md`). The plist drift was never reconciled back to source default, and v3.16.1 incorrectly canonised the post-accident value as if it had been a release decision. This release: - Restores the plugin fallback to `http://127.0.0.1:3456` to match server source default. - Updates `openclaw.plugin.json` `configSchema.proxyUrl.default` back to `3456`. - Restores README §"Environment Variables" `CLAUDE_PROXY_PORT` default to `3456`. - Plugin reads `OCP_PROXY_URL` env (full URL) first, then `CLAUDE_PROXY_PORT` env (port only), then falls back to `3456`. Hosts whose OCP plist injects a non-default port must also inject the same `CLAUDE_PROXY_PORT` into the OpenClaw plist for the plugin to follow. - Maintainer's Mac mini plist was reverted from `3478` to `3456` as part of this release deploy (no source change reflects this; it was a one-host correction). ### Governance - No `cli.js` citation needed (no `server.mjs` change). ALIGNMENT.md Rule 2 not engaged. ## v3.16.1 — 2026-05-12 (superseded — narrative incorrect; see v3.16.2 erratum) ### Fixes (as shipped — note erratum above) - **OCP plugin port lag** — `ocp-plugin/index.js` hard-coded `http://127.0.0.1:3456`. ~~While OCP server moved to 3478 in v3.14+,~~ **(corrected v3.16.2: no such move ever happened.)** The Mac mini's plist was on `3478` only as residue from a dogfood accident. Result: `/ocp` slash commands from the home Telegram bot returned "OCP error: fetch failed". v3.16.1 changed the plugin default to `3478` (wrong direction; v3.16.2 reverts to `3456`). ### Governance - No `cli.js` citation needed (no `server.mjs` change). ALIGNMENT.md Rule 2 not engaged. ## v3.16.0 — 2026-05-10 ### Features - **`ocp doctor --check oauth`** (PR #93) — fast path that runs only the OAuth check, skipping version detection / from-version / git operations / models endpoint. ~50ms vs. full doctor's ~200-500ms. Use cases: AI agent repair loops, post-`claude auth login` verify, quick health gates. Help text in `cmd_doctor_help` now reflects working behaviour. - **`ocp update --rollback --gc`** — manually garbage-collect old upgrade snapshots. Retention policy: keep last 5 snapshots OR snapshots newer than 30 days OR the single most recent (always-keep safety net). `--dry-run` previews. Successful `ocp update` runs auto-GC at the end of the full path; light path does not (no snapshot created there). ### Behavior changes - After a successful cross-minor `ocp update`, the auto-GC emits `[gc] removed N old snapshots` to stderr if any were collected. Safe to ignore; manual gc is `ocp update --rollback --gc`. ### Governance - No `cli.js` citation needed (no `server.mjs` change). ALIGNMENT.md Rule 2 not engaged. - PR #93 (--check oauth) merged separately; this release bundles it with the GC feature. ## v3.15.1 — 2026-05-10 ### Fixes - **doctor: dynamic `latest_version` from `origin/main:package.json`** — v3.15.0 doctor used a hard-coded `latest = "v3.14.0"` fallback, which made any v3.15.0+ install report `kind = upgrade` (against a stale value). `ocp update` would then attempt `git checkout v3.14.0` — a downgrade. Doctor now fetches `git -C ~/ocp show origin/main:package.json` to determine the actual latest version; on failure (offline, fresh clone with no remote), falls back to `currentVersion` so `kind = noop` instead of recommending a downgrade. ## v3.15.0 — 2026-05-10 ### Features - **`ocp doctor`** — health & upgrade-readiness check; primary entry for AI-driven debugging. `--json` mode emits a `next_action` with `ai_executable[]` for agents to run verbatim and `human_required[]` for steps requiring the user (typically only OAuth). - **`ocp update` cross-version path** — for cross-minor jumps (e.g. v3.10 → v3.14), `ocp update` now runs doctor → snapshot → `setup.mjs` (with the plist env-merge from PR #90) → service restart → post-flight `/health` + `/v1/models` verification. Same-patch updates retain the existing light path; users see no change for routine patch bumps. - **`ocp update --rollback`** — restore the most recent (or specified) upgrade snapshot. Snapshots are saved to `~/.ocp/upgrade-snapshot-/` and never auto-deleted. - **Fresh-install routing** — `ocp update` on installations < v3.4.0 routes to a fresh-install flow (with `--yes` to skip confirmation; AI agents pass this). OAuth survives via Claude Code's credential store; users do not re-OAuth unless their token was independently broken. - **AI prompt blocks in README** — §Installation, §Upgrading, and §Troubleshooting each start with a copy-paste prompt for Claude Code / Cursor / Copilot, so users can drive install / setup / upgrade through their existing AI assistant. ### Behavior changes - `ocp update` may take 10–30s longer when a cross-minor jump triggers the full path (snapshot + post-flight). Patch bumps are unchanged. - Pre-v3.4.0 installs are routed to fresh-install rather than failing silently or half-migrating. ### Governance - No `cli.js` citation needed (no `server.mjs` change). ALIGNMENT.md Rule 2 not engaged. - Depends on PR #90 (plist env merge bug fix; merged before this release). ## v3.14.0 — 2026-05-10 ### Features (security hardening) - **Per-key session isolation** (PR #86, S1) — the `sessions` Map in `server.mjs` is now keyed by `${keyName}|${conversationId}` instead of bare `conversationId`. Before this fix, two clients using distinct API keys but the same `session_id` value (e.g. both defaulting to `"default"`) would share the same `cli.js` subprocess and conversation history, creating a cross-tenant leak path. Post-fix each (key, session) pair is isolated end-to-end, extending the per-key cache isolation shipped in v3.13.0 D1 to the session layer. - **On-disk credential file modes 0700/0600** (PR #87, S2) — `setup.mjs` now creates `~/.ocp` at mode 0700 and both `admin-key` and `ocp.db` at mode 0600. An idempotent `reconcileFileModes()` call in `server.mjs` startup tightens any existing installation to these modes automatically on every launch, so existing prod boxes fix themselves without manual `chmod`. Before this fix, all three files were created at the process's default umask (typically world-readable 0644 / 0755), leaving plaintext credentials readable by other local users. - **`/api/usage` default scope = self; admin all-keys requires `?all=true`** (PR #88, S3) — the usage endpoint now applies a least-privilege default: anonymous callers receive only their own rows, non-admin authenticated callers receive only their own rows, and admin callers receive only their own rows unless they explicitly pass `?all=true`. When `?all=true` is used, an audit log line is emitted. Before this fix, any admin-token holder could silently enumerate usage data for every key on the server. ### Behavior changes - **Breaking change for admin tooling**: `/api/usage` no longer returns all-keys data by default. Existing cron jobs, dashboards, or scripts that rely on the admin token seeing all-keys output must add `?all=true` to their request URL after upgrading to v3.14.0. - **File mode reconcile at server startup** logs a one-line notice per path when mode is tightened (e.g. `[security] tightened ~/.ocp/ocp.db → 0600`). No action is required from the operator; the reconcile is idempotent and silent when modes are already correct. - **`sessions` Map key is now `${keyName}|${conversationId}` internally.** No client-visible wire change — the `session_id` field in request/response is unchanged. ### Verification - Stress-test pass: 11/11 phases including S1/S2/S3 security regression checks (Phase E, I, J). 35-minute sustained run, 60 calls, 0 errors, 0 timeouts. RSS dropped 51→47 MB across the window. Per-key cache isolation, singleflight, cache_control bypass, quota enforcement, file-mode reconcile, and scope guard against escalation all verified against running code. ### Governance - All three PRs (#86, #87, #88) include the explicit `cli.js`-citation-not-applicable disclaimer (per PR #75 pattern) since they are OCP-internal access-control, session-state, and file-permission changes with no corresponding `cli.js` operation to cite. ### No new env vars / no public API surface change beyond the documented breaking change This release adds no new env vars or endpoints. The only externally visible change is the `/api/usage` scope guard (breaking for admin all-keys consumers; see Behavior changes above). ## v3.13.0 — 2026-05-07 ### Features (cache layer hardening) - **Per-key cache isolation** (D1) — the cache key now includes the API key id, so distinct keys never share cache entries. Anonymous/unauthenticated callers share one `anon` pool. Hash format upgraded to `v2`; legacy v1-format rows orphan and are reaped by the existing TTL cleanup interval (no migration script). - **`cache_control` bypass** (D2) — when a request carries an Anthropic `cache_control` annotation (top-level or nested in a content array), OCP skips its own cache entirely. The caller is using Anthropic-side prompt caching deliberately, and OCP must not interfere. A `cache_skipped{reason: cache_control_present}` log line is emitted on bypass. - **Chunked stream replay** (D3) — when a streaming request hits the cache, the cached content is now emitted as multiple SSE chunks (80 codepoints/chunk, codepoint-safe via `Array.from()`) instead of a single large delta. Multibyte characters (CJK / emoji) stay intact. - **Singleflight stampede protection** (D4) — concurrent identical cache-miss requests now share one upstream `cli.js` spawn instead of spawning N processes. Followers receive byte-identical responses to what the leader returns. All-or-nothing failure semantics: if the leader errors, all followers receive the same error. Streaming-path singleflight is explicitly out of scope (TODO left for follow-up). ### Behavior changes - `/cache/stats` response now includes additive fields `inflight` and `requesters` (current in-flight singleflight entries and total waiting callers). Existing fields `entries`, `totalHits`, `sizeBytes` are preserved unchanged. ### Governance - New ADR [`docs/adr/0005-no-multi-provider.md`](docs/adr/0005-no-multi-provider.md): OCP stays single-provider (Anthropic via `cli.js` spawn). Multi-provider gateway refactor explicitly out of scope; cache improvements are explicitly in scope. - Design spec for this release: [`docs/superpowers/specs/2026-05-07-cache-upgrade-design.md`](docs/superpowers/specs/2026-05-07-cache-upgrade-design.md). ### No new env vars / no public API surface change This release adds no new env vars or endpoints. All four improvements are internal correctness/concurrency upgrades to the existing `CLAUDE_CACHE_TTL`-gated cache layer. No client-observable wire shape change. ## v3.12.0 — 2026-04-25 ### Features - **Streaming heartbeat** — opt-in SSE comment frame (`: keepalive\n\n`) emitted during silent windows on the streaming response. Controlled by `CLAUDE_HEARTBEAT_INTERVAL` env var (ms; `0` = disabled, default). Covers both pre-first-byte and mid-stream tool-use pauses. Addresses #47. See [design doc](docs/superpowers/specs/2026-04-25-47-sse-heartbeat-design.md). - **`X-Accel-Buffering: no`** response header added to SSE responses so heartbeats survive nginx/Cloudflare default buffering. ### Behavior changes - SSE headers are now sent immediately after the claude CLI spawns successfully, not on first stdout byte. The rare "spawn succeeded but subprocess died before any byte" path now closes the SSE stream cleanly rather than returning a JSON error. ### Config additions | Variable | Default | Description | |---|---|---| | `CLAUDE_HEARTBEAT_INTERVAL` | `0` (disabled) | Interval in ms for SSE keepalive comment frames on streaming path. Resets on every real frame. | ## v3.11.1 — 2026-04-21 ### Fixes - Concurrency slot leak on subprocess timeout (#37). The request-timeout handler called `proc.kill("SIGTERM")` without decrementing `stats.activeRequests`. A subprocess stuck in a syscall that ignored SIGTERM would hold its slot until (or beyond) the 5s SIGKILL escalation actually reaped it. Slot release is now wired to `proc.once("exit", cleanup)` so every termination path — normal close, error, SIGTERM, SIGKILL — releases the slot exactly once. ## v3.11.0 — 2026-04-20 ### Features - `ocp update` now automatically syncs OpenClaw's registry with the latest models (scripts/sync-openclaw.mjs) - Server logs warn if OpenClaw registry drifts from models.json ### Refactor - models.json is now the single source of truth for model list - server.mjs and setup.mjs derive MODEL_MAP/MODELS from models.json - Adding a new model is now a one-file edit ### Fixes - OpenClaw's model dropdown now shows all 4 current models (opus-4-7, opus-4-6, sonnet-4-6, haiku-4.5) on existing installs after `ocp update`. Previously setup.mjs only wrote the registry at install time.