125 Commits
Author SHA1 Message Date
9494fd6c69 chore(release): v3.13.0 — cache layer hardening (per-key isolation + bypass + chunked replay + singleflight) (#67)
Per release_kit overlay (CLAUDE.md § Iron Rule 5.5):
- package.json bumped 3.12.0 → 3.13.0
- CHANGELOG.md updated with v3.13.0 entry
- README.md § Response Cache updated to document the four hardening features

This is a release preparation commit. Tag push to v3.13.0 will trigger
.github/workflows/release.yml which auto-creates the GitHub Release.

cli.js does not perform proxy-layer response caching, stampede protection,
or replay; this release only ships internal cache-layer correctness and
concurrency improvements that do not change the OpenAI-compatible wire
surface visible to clients. No client-observable wire shape change.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
v3.13.0
2026-05-07 17:19:44 +10:00
5ff30ac9b6 feat(cache): singleflight stampede protection on non-streaming path (#66)
cli.js does not perform proxy-layer stampede protection. The singleflight
layer is a value-add proxy operation that exists only inside OCP, between
concurrent client requests and the single upstream cli.js spawn. It does
not introduce, alter, or remove any endpoint, header, request field, or
response field that cli.js emits or expects — no client-observable wire
shape change.

Justification under ALIGNMENT.md Rule 2: the singleflight Map deduplicates
concurrent identical non-streaming cache-miss requests so only one cli.js
spawn runs per unique hash window. All followers receive the same resolved
(or rejected) content. This is a proxy-internal concurrency optimization,
not a wire-level protocol change.

Spec: docs/superpowers/specs/2026-05-07-cache-upgrade-design.md (D4)

Changes:
- keys.mjs: add singleflight(hash, fn) + getInflightStats() exports;
  in-memory Map cleared via Promise.finally() on each settlement
- server.mjs: import singleflight + getInflightStats; wrap non-streaming
  cache-enabled path through singleflight with inner recheck; add TODO
  comment in callClaudeStreaming (streaming-path dedup is explicitly out
  of scope for v3.13.0, see spec D4 streaming caveat); extend /cache/stats
  to return inflight + requesters fields (additive, no removed fields)
- test-features.mjs: 7 new PR-B singleflight tests covering basic dedup,
  failure fan-out, map cleanup (success + failure), different-hash
  independence, getInflightStats shape, and sequential-call non-sharing;
  all 31 tests pass (24 existing + 7 new)

Streaming-path singleflight is explicitly out of scope; TODO left in
callClaudeStreaming for a future follow-up ticket.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 17:16:46 +10:00
16eeb66557 feat(cache): per-key isolation, cache_control bypass, chunked stream replay (#65)
* docs(governance): ADR 0005 (no multi-provider) + cache upgrade spec

Governance prelude for the cache upgrade work:

- docs/adr/0005-no-multi-provider.md — locks in the decision that
  OCP stays single-provider (Anthropic via cli.js spawn). Cache
  improvements are explicitly in scope (decision §3); multi-provider
  refactor is explicitly out of scope, with three documented trigger
  conditions for revisiting.

- docs/adr/README.md — index updated to reference 0005.

- docs/superpowers/specs/2026-05-07-cache-upgrade-design.md — design
  for the cache upgrade work split into PR-A (per-key isolation,
  cache_control bypass, chunked stream replay) and PR-B (singleflight
  stampede protection). Each design decision has a written rationale.

server.mjs is not modified; this commit is doc-only and therefore
exempt from the cli.js citation requirement (ALIGNMENT.md Rule 5
applies only to commits that touch server.mjs).

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

* feat(cache): per-key isolation, cache_control bypass, chunked stream replay

cli.js does not perform response caching at the proxy layer. OCP's response
cache is a value-add operation internal to OCP, between the wire and the
cli.js spawn. It does not introduce, rename, or alter any endpoint, header,
request field, or response field that cli.js emits or expects — this change
qualifies under ALIGNMENT.md Rule 2's value-add carve-out for non-wire-
affecting proxy operations. no client-observable wire shape change.

Spec: docs/superpowers/specs/2026-05-07-cache-upgrade-design.md

D1 — Per-key cache isolation (cacheHash v2 format)
  keys.mjs: prepend `v2|k:<keyId or "anon">|` before existing hash fields.
  Backward-compatible: absent/null/empty keyId folds to "anon".
  v1-format rows in response_cache are abandoned naturally; TTL cleanup at
  server.mjs:185 reaps them within one window. No migration needed.
  server.mjs: pass keyId: req._authKeyId at the single cacheHash call site
  (line ~1221).

D2 — cache_control bypass
  keys.mjs: export hasCacheControl(messages) — walks messages and nested
  content arrays for presence of cache_control field.
  server.mjs: if hasCacheControl(messages) is true, set req._cacheHash = null
  and log cache_skipped{reason: cache_control_present}; existing
  `if (CACHE_TTL > 0 && req._cacheHash)` guards on write-back handle the skip.

D3 — Chunked stream replay (80 codepoints/chunk, no artificial delay)
  server.mjs: replace single-chunk cached.response emission with an
  Array.from(cached.response) loop in steps of CACHE_REPLAY_CHUNK_SIZE=80.
  Array.from ensures multibyte UTF-8 codepoints (e.g. CJK) are never split.

Tests: 12 new cases in test-features.mjs (36 total, 0 failed).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 13:50:50 +10:00
c998d21a4f chore(ci): add gitleaks workflow to scan PRs and pushes to main (#64)
The repo has shipped `.gitleaks.toml` (with project-specific allowlist
entries — public OAuth client ID, README placeholders, an old plan doc)
since the privacy remediation work, but no GitHub Action invoked it.
The config was orphan: real protection only when someone ran gitleaks
locally, never gating merges.

This workflow wires `.gitleaks.toml` into CI:

- Triggers on every `pull_request` (any branch) and `push` to `main`.
- Uses `gitleaks/gitleaks-action@v2`, which auto-detects the repo-root
  `.gitleaks.toml` and applies its allowlist.
- Hard-fails on any leak. No `continue-on-error`. Public repo policy.
- `permissions: contents: read` — minimum required scope.
- `fetch-depth: 0` so the action can scan full history (the action's
  default behavior; explicit here for clarity).

Verification path:
- The workflow runs on this PR itself; if any secret were ever committed
  to the repo, the scan fails here. Prior audit confirmed the tracked
  tree is clean of real secrets, so this PR's own scan should pass.

Refs: audit side-finding 4 of 4.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 09:54:36 +10:00
5be369ed68 docs(changelog): normalize version heading format to ## vX.Y.Z — YYYY-MM-DD (#63)
Audit side-finding: heading style drift in CHANGELOG.md.

Before:
  ## v3.12.0 (2026-04-25)     ← parens style (1 entry)
  ## v3.11.1 — 2026-04-21     ← em-dash style (canonical, majority)
  ## v3.11.0 — 2026-04-20     ← em-dash style (canonical, majority)

After: all 3 entries use the em-dash form (2 of 3 entries already used it,
so it's the canonical pattern by majority).

Only the v3.12.0 heading line changed. The body content under each
heading is untouched — only the date-format separator changes from
parens to em-dash.

Verification:
- `grep "^## " CHANGELOG.md` after the edit → all entries match
  `## vX.Y.Z — YYYY-MM-DD`.
- `git diff CHANGELOG.md` shows exactly one line changed.

Refs: audit side-finding 3 of 4.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 09:54:04 +10:00
3d52ffc152 chore(deps): bump engines to >=22.5 to match node:sqlite usage in keys.mjs (#62)
OCP uses Node.js built-in SQLite (`node:sqlite`) in keys.mjs:3 for the
LAN-mode key store. The `node:sqlite` module is only available in:

- Node 22.5.0+ behind --experimental-sqlite flag
- Node 23.0.0+ without any flag (fully stable)

The previous `engines: ">=18"` was inaccurate and would have caused
opaque "Cannot find module 'node:sqlite'" failures for users on
Node 18-22.4 the moment LAN mode (multi-key) was enabled.

Changes:
- package.json: engines.node ">=18" → ">=22.5"
- README.md: prerequisite "Node.js 18+" → "Node.js 22.5+ (Node 23+ recommended …)"
  with a one-line note explaining the flag distinction so users on 22.x know
  they may need --experimental-sqlite.

Verification:
- Source usage of node:sqlite confirmed via grep: keys.mjs:3
  (`import { DatabaseSync } from "node:sqlite";`).
- Local node --version = v25.8.0 (well above 22.5); `npm install` exit 0
  with no engine-mismatch warning.
- No other source file imports node:sqlite (single point of usage).

Refs: audit side-finding 2 of 4.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 09:53:46 +10:00
68b0838074 chore(deps): delete stale package-lock.json (was v3.4.0 vs package.json v3.12.0) (#61)
The repo-tracked package-lock.json declared "version": "3.4.0" while
package.json is at v3.12.0 — 8 minor versions of drift. Since
package.json has zero dependencies (only built-in node:sqlite, node:http,
node:https), the lockfile carried no useful information. It was pure
noise that would mislead anyone running `npm ci` into thinking they were
installing v3.4.0.

Verification:
- package.json has no `dependencies` or `devDependencies` field (grep -A2 '"dependencies"' package.json → no match).
- Fresh clone + checkout + `npm install` → exit 0, "audited 1 package in 93ms, found 0 vulnerabilities".
- node_modules is correctly empty after install (no bin shims to relink).

Refs: audit side-finding 1 of 4.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 09:53:20 +10:00
313cb13a78 docs: align README + governance docs with current state (uninstall, files, ADR index, ship-archive) (#59)
Multiple documentation polish items rolled into one PR (one layer:
"docs alignment with current state").

### README.md

- **Uninstall section** added between Server Setup and Client Setup
  (was missing — `node uninstall.mjs` exists but went undocumented).
- **OpenClaw definition** added as a footnote on first README mention
  (the architecture diagram and Supported Tools table both reference
  OpenClaw without ever defining it).
- **Repository Layout section** added before Security — table of
  top-level files (server.mjs, setup.mjs, uninstall.mjs, keys.mjs,
  models.json, ocp/ocp-connect, dashboard.html, scripts/, .claude/skills/,
  ocp-plugin/, docs/adr/, ALIGNMENT.md, AGENTS.md, CLAUDE.md) so a new
  contributor knows what each file does.
- **LICENSE link** added to the License section footer.

### docs/adr/README.md (new)

- Index of the three published ADRs (0002, 0003, 0004) with a one-line
  description each.
- Explains the `0001` placeholder (early internal proposal that was
  superseded; numbering deliberately starts at `0002`).
- Guidance on when to write a new ADR vs. when a commit message suffices.

### Spec/plan housekeeping

- `specs/.gitkeep` removed (the empty `specs/` placeholder confused the
  picture; canonical paths are `docs/superpowers/plans/` for active plans
  and `docs/superpowers/specs/` for long-lived design docs that other
  code references).
- Shipped plans moved to `docs/superpowers/plans/shipped/`:
  - `2026-04-10-lan-mode.md` (shipped: README LAN mode section)
  - `2026-04-25-47-sse-heartbeat-plan.md` (shipped: v3.12.0 per CHANGELOG)
- `2026-04-25-47-sse-heartbeat-design.md` left in `docs/superpowers/specs/`
  unchanged because both `server.mjs:565` and `CHANGELOG.md:7` link to
  that exact path; moving it would require a `server.mjs` edit, which
  needs `cli.js` citation per ALIGNMENT.md Rule 1.

### AGENTS.md

- Updated "Key files to know" to add `docs/adr/README.md`,
  `docs/superpowers/plans/`, and `memory/constitution.md`.
- Note explaining `memory/constitution.md` is spec-kit's standard
  location, distinct from `~/.cc-rules/memory/` and `ALIGNMENT.md`.
- Updated "Handoff expectations" item 5 from `docs/superpowers/specs/*/tasks.md`
  (which never matched anything — there were no `tasks.md` files there)
  to `docs/superpowers/plans/` (excluding `shipped/`).

### Coordination with PR #53

PR #53 is open and adds "Why OCP?", "Comparison", and "Governance"
sections to README. This PR deliberately avoids those areas — only edits
the Supported Tools table footnote, inserts Uninstall before Client Setup,
inserts Repository Layout before Security, and updates the License footer.
No expected merge conflict.

Refs: audit findings M6, M8, M9, M11, L2, L3, L6.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
2026-05-05 09:44:49 +10:00
e4b010af5e docs(readme): add Why OCP, comparison table, governance section (#53)
Adds three positioning sections to make the README convert clones-to-stars
better:

1. "Why OCP?" near the top — 6 differentiator bullets with evidence links
   (SSE heartbeat / ALIGNMENT.md / models.json SPOT / multi-key /
   per-key quota / ocp-connect).
2. "Comparison" subsection — honest table vs claude-code-router and
   anthropic-proxy. Acknowledges CCR's larger ecosystem; positions OCP
   as cli.js-aligned + subscription-multiplexing focused.
3. "Governance" section near the bottom — links to ALIGNMENT.md, AGENTS.md,
   ADRs, alignment.yml. Consolidates the governance-link surface in one
   place rather than scattered.

Net diff +43 -0. No content removed. No anchors broken. No code changes.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 09:44:37 +10:00
0752f666fb test: wire test-features.mjs to npm test + add minimal CI smoke workflow (#60)
`test-features.mjs` shipped at v3.8.0 (per CHANGELOG of the keys.mjs
quota+cache work) and is referenced from AGENTS.md as the project's only
test artifact, but until now nothing actually ran it — no `npm test`
script, no CI step. Wiring it up so it runs on every push and PR.

### Changes

- `package.json`: add `"test": "node test-features.mjs"` to scripts.
- `.github/workflows/test.yml` (new): single-job workflow that runs
  `npm test` on push to main and on every PR. Uses Node 24 because
  `keys.mjs` imports `node:sqlite`, which is stable in Node 23+ (Node
  24 is the current LTS; Node 22 would need `--experimental-sqlite`).
  No `npm install` step — OCP has zero external runtime dependencies
  per `package-lock.json`.
- `AGENTS.md`: note that `test-features.mjs` runs via `npm test` and
  is enforced by `.github/workflows/test.yml`.

### Why this is a hard check, not a soft check

`test-features.mjs` is self-contained — it imports `keys.mjs` and
exercises the SQLite-backed key/quota/cache code paths against a
throwaway test DB at `~/.ocp/ocp-test.db`. It does NOT require:

- a live claude CLI binary
- a running OCP server
- any network access

So CI can run it as a real check; no `continue-on-error` needed.

### Local verification

```
$ npm test
[...]
=== Results: 24 passed, 0 failed ===
```

24 assertions cover createKey / listKeys / quota math / cache hash
determinism / cache TTL / clearCache. Exit code is 1 on any failure
(`process.exit(failed > 0 ? 1 : 0)` at the bottom of test-features.mjs).

### Future expansion

If the suite later grows to include tests that DO require a live claude
CLI or a running OCP, mark those steps `continue-on-error: true` (or
split them into a separate job). The comment in `test.yml` documents
this contract.

Refs: audit (test-features.mjs orphan / unrunnable in CI).

Co-authored-by: dtzp555 <dtzp555@gmail.com>
2026-05-05 09:44:25 +10:00
ae4a829904 chore(naming): rename package + code refs from openclaw-claude-proxy to OCP (#57)
The npm name `ocp` is squatted (deprecated `node-ocp` v0.0.1), so the
package name is renamed to `open-claude-proxy` (verified via
`npm view open-claude-proxy` → 404, confirming availability).

### Changes

- `package.json`:
  - `"name"`: `openclaw-claude-proxy` → `open-claude-proxy`
  - `"bin"` map UNCHANGED: both `openclaw-claude-proxy` and `ocp`
    binaries still resolve, preserving back-compat for existing installs
    that reference `openclaw-claude-proxy` as a CLI command.
- `setup.mjs`: header JSDoc + start.sh banner string
- `uninstall.mjs`: header JSDoc

### Intentionally NOT changed

- `server.mjs` startup banner (line 1628):
  `console.log('openclaw-claude-proxy v...')`. Editing `server.mjs`
  requires `cli.js:NNNN` citation per ALIGNMENT.md Rule 1, and a
  cosmetic log-string rename has no `cli.js` correspondence. Deferred
  — would need an ALIGNMENT.md scope justification PR if pursued.
- `bin/openclaw-claude-proxy` symlink/path: existing installs depend on
  this name; kept as a back-compat alias.

### Evidence

```
$ npm view ocp
ocp@0.0.1 | DEPRECATED — squatted by node-ocp
$ npm view open-claude-proxy
404 — available
```

Refs: audit (naming consistency).

Co-authored-by: dtzp555 <dtzp555@gmail.com>
2026-05-05 09:44:12 +10:00
51e908e145 chore(repo): remove broken/undocumented Docker files (#58)
The Dockerfile, docker-compose.yml, and .dockerignore are removed because
the Dockerfile is structurally broken AND Docker is not a documented
runtime for OCP.

### Evidence — Dockerfile is broken

The Dockerfile COPYs only 3 files:

```
COPY server.mjs ./
COPY setup.mjs ./
COPY package.json ./
```

But `setup.mjs:43` reads `models.json` at runtime
(`JSON.parse(readFileSync(join(__dirname, "models.json"), ...))`),
and `server.mjs` requires `keys.mjs`, `dashboard.html`, etc. The
container as built would crash on first run.

### Evidence — Docker is undocumented

```
$ grep -i docker README.md AGENTS.md CLAUDE.md
# 0 hits
```

No README install path, no AGENTS.md runtime mention, no CLAUDE.md
release-kit reference. These files were ghost infrastructure.

### Why delete vs. fix

Fixing the Dockerfile to actually work would require: COPYing 6+ files,
verifying the container can write to `~/.openclaw/`, deciding on auth
mounting (claude CLI binary + auth state), and adding documentation
across README/AGENTS/CLAUDE. None of that is justified by a request.

Easier to remove the dead files now and reintroduce a working
Dockerfile when there's an actual demand for containerised OCP, with
proper documentation alongside.

Refs: audit (dead infrastructure cleanup).

Co-authored-by: dtzp555 <dtzp555@gmail.com>
2026-05-05 09:44:00 +10:00
d99534dc35 chore(repo): add .gitignore for runtime artifacts; commit scripts/heartbeat-field-check.sh (#55)
Two related changes — both repo-hygiene, no behavior impact.

### `.gitignore` (new)

Ignores runtime artifacts that should never be tracked:

- `logs/` and `*.log` — proxy.log, last_send.log, heartbeat-field-check log
- `node_modules/` — dependency cache
- `.env`, `.env.*` — local secrets/config
- `.DS_Store`, `*.swp`, `*~` — editor/OS scratch

`logs/` was previously untracked-but-present in working trees; the new
ignore makes that intent explicit and prevents accidental commits.

### `scripts/heartbeat-field-check.sh` (commit existing untracked file)

This script was authored as a one-shot field-evidence gatherer for the
v3.12.0 SSE heartbeat work (PR #49 / issue #47). It fired successfully on
2026-05-02 09:00 Australia/Brisbane via launchd
(`~/Library/LaunchAgents/dev.ocp.heartbeat-check.plist`) and posted a
summary comment to issue #47.

Useful tooling pattern (one-shot field check + launchd schedule + dry-run
flag), worth keeping under version control rather than letting it die in
an untracked working tree.

Verification: `git status` clean after both adds.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
2026-05-05 09:43:47 +10:00
39ca20536e docs(governance): correct CLAUDE.md + AGENTS.md to reflect actual alignment.yml blacklist (#56)
Both `CLAUDE.md` (line 25) and `AGENTS.md` (line 46) previously claimed the
`alignment.yml` blacklist included two tokens — `api/oauth/usage` and
`api/usage`. The actual workflow has a single token in `BLACKLIST`:
`api.anthropic.com/api/oauth/usage`.

Doc-vs-CI drift. Fix is doc-side only — `alignment.yml` itself is unchanged
because adding a second blacklist token (`api/usage`) is a constitutional
change that requires an `ALIGNMENT.md` amendment PR per ALIGNMENT.md
Amendment Procedure.

If `api/usage` should be added to the blacklist, that's a separate PR with
ALIGNMENT.md amendment + reviewer sign-off.

Refs: audit findings (governance doc accuracy).

Co-authored-by: dtzp555 <dtzp555@gmail.com>
2026-05-05 09:43:34 +10:00
8b3f50912e chore(repo): remove v2.x stale openclaw-claude-proxy/ folder + dead start.sh (#54)
Both removed:

- `openclaw-claude-proxy/` — v2.4.0 stale duplicate of the proxy. Current
  proxy is `server.mjs` at repo root (v3.x); the nested folder was an early
  packaging artifact that never got deleted. Audit finding H1.
- `start.sh` — hardcoded `/Users/taodeng/.openclaw/...` paths that only ever
  worked on the original maintainer's home directory. The real install is
  produced by `setup.mjs` (see setup.mjs:212-237 which writes the correct
  per-user start hook). Audit findings H5, H6 (path leak + dead script).

Verification: `git grep "/Users/taodeng/"` returns 0 hits in tracked files.

Refs: audit findings H1, H5, H6.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
2026-05-05 09:43:05 +10:00
780462c763 fix(cli): set DISABLE_AUTOUPDATER=1 in manual restart fallback (#52)
When ocp restart falls back to nohup (no launchctl/systemd
service registered), prepend DISABLE_AUTOUPDATER=1 so the
spawned server.mjs and any claude subprocess it spawns skip
the in-binary auto-updater check.

Why: claude-code's native binary contains an auto-updater
that fires after each successful invocation. Partial
install.cjs failures during update leave bin/claude.exe as
an ASCII shim → spawnSync ENOEXEC → OCP slot lockup
(symptoms in #37, #40 forensics). Setting this env at the
launch site stops the trigger.

Note: only covers the manual nohup path. Users with
launchctl plist or systemd unit must add the env there too
(plist EnvironmentVariables or systemd Environment=).
Authoritative settings location is ~/.claude/settings.json
env key — see learnings/claude_code_disable_autoupdate.md
in cc-rules for the full 4-layer guidance.

Verified: 24h+ stable on Mac mini after applying full
multi-layer fix on 2026-04-30 04:20 (claude.exe mtime
unchanged, OCP uptime 22h54m, 0 errors over 3 real calls).

No server.mjs change — ALIGNMENT.md unaffected.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
2026-05-01 17:36:47 +10:00
733a2ed4c2 chore(server): add session-create-vs-resume + lifetime instrumentation (refs #41 #42) (#51)
Adds structured logging for the two forensic candidates so the eventual
fixes can be evidence-driven (per #41/#42 bodies: "Evidence needed before
code change — do not patch speculatively"). NO bug fix in this PR — the
stale-create-entry behavior (#41) and the lastUsed-resistant-expiry
behavior (#42) are intentionally left unchanged so they can be observed
in production /logs.

Three changes (server.mjs):

1. Session-create branch records `firstSeen` alongside `lastUsed` (same
   timestamp), giving the sweep loop an absolute-age signal independent
   of the actively-bumped `lastUsed` field. Resume branch is untouched
   so `firstSeen` is preserved across resumes.

2. handleSessionFailure now emits a structured `session_failure` event
   with `mode: "resume"` (action: "deleted") OR `mode: "create"`
   (action: "kept"). The "kept" branch makes #41's "stale create entries
   never deleted" pattern visible in /logs without changing behavior.

3. TTL sweep emits `session_expired` (info, with idleMs + ageMs) on
   actual expiry, and `session_long_lived` (warn) when ageMs > 4×TTL
   but the entry is not being expired this tick. The latter is the
   #42 evidence trigger — a session whose lastUsed keeps getting bumped
   and so resists idle-based expiry.

cli.js citation: N/A — logEvent payloads are OCP-internal observability,
never sent on the wire to the Anthropic API. ALIGNMENT.md Rule 2 (no
invention) is scoped to endpoints/headers/request/response fields, not
to internal server logging.

Independent reviewer: fresh-context sonnet APPROVE_WITH_MINOR.
Two minor notes — repeated `session_long_lived` emissions per 60s
sweep tick is expected (downstream analysis dedupes by conversationId).
LOC: +19 / -3 = +16 net.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 19:19:38 +10:00
dtzp555-maxandGitHub ca2d23d230 chore(speckit): add compatibility/metadata frontmatter to SKILL.md files (refs #39) (#50)
Backfill the two YAML frontmatter blocks that upstream
`specify_cli/agents.py:build_skill_frontmatter()` emits:

  compatibility: "Requires spec-kit project structure with .specify/ directory"
  metadata:
    author: github-spec-kit
    source: claude:templates/commands/<cmd>.md

Applied uniformly across all 9 speckit-* SKILL.md files. Each file gets the
two blocks appended to its existing frontmatter (between the closing fields
and the closing `---`). No functional change — these fields are informational
only, per #39 body and PR #38 reviewer note.

Verified:
- All 9 files have `compatibility:` key (grep -lc, 9/9)
- All 9 files have `metadata:` block with `author: github-spec-kit` (9/9)
- All 9 files have `source: claude:templates/commands/<cmd>.md` matching the
  command name (9/9)
- No existing fields modified or removed; pure additive +4 lines per file

Upstream reference (build_skill_frontmatter):
https://github.com/github/spec-kit/blob/main/src/specify_cli/agents.py
2026-04-25 19:19:35 +10:00
b871b72b6b feat(server): SSE heartbeat on streaming path (#47) (#49)
* docs(spec): design for #47 SSE heartbeat on streaming path

Draft spec for an opt-in idle-watchdog SSE heartbeat covering both
pre-first-byte and mid-stream silent windows. Default disabled,
controlled by CLAUDE_HEARTBEAT_INTERVAL. Targets ~40 LOC.

Decisions captured: D1 whole-stream reset-on-byte; D2 SSE comment
frame; D3 default disabled; D4 relocate ensureHeaders() to post-spawn;
D5 X-Accel-Buffering: no on both SSE header sites; D6 single log line
per affected request.

Scope-locked: does not touch CLAUDE_TIMEOUT semantics, the separate
server.mjs:480-489 dangling-client bug, issues #41/#42, or the
non-streaming path.

Includes privacy preflight and cloud-testing plan per maintainer
feedback.

Refs: #47

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(plan): implementation plan for #47 SSE heartbeat

6 phases: pre-work (file 480-bug), implementation (5 tasks on
feat/47-sse-heartbeat), opus fresh-context review, cloud verification
on Mac rig, privacy preflight, PR+release.

Each implementation task carries concrete code, syntax check, and a
scoped commit message. LOC budget enforced in Task 1.6 gate (~45
server.mjs lines max). Reviewer checklist scopes scope-lock, ALIGNMENT,
privacy, and heartbeat-cannot-abort discipline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server): add startHeartbeat helper + HEARTBEAT_INTERVAL env var

Per design doc (refs #47). Helper is a per-request idle watchdog that
emits `: keepalive\n\n` SSE comment frames; returns a {reset, stop} handle.
No wiring yet — helper is unused, safe to commit in isolation.

cli.js citation: N/A — SSE response shaping is an OCP-owned translation
layer, not a cli.js operation. See AGENTS.md and ALIGNMENT.md Rule 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(server): eagerly send SSE headers post-spawn (D4, refs #47)

Moves the ensureHeaders() call from "on first stdout byte" to
"immediately after successful spawn." This is a prerequisite for the
heartbeat covering the pre-first-byte silent window (the 'processing
large contexts' failure mode in #47).

Behavioral consequence: the narrow "spawn succeeded but subprocess died
before any byte" branch at server.mjs:610-611 becomes effectively dead
in the common case. The post-headers SSE-stop path (612-619) handles
it instead. The branch remains defensively for the client-closed-before-
ensureHeaders race.

Isolated commit per design doc §D4 so reviewer can focus on this one
behavior change.

cli.js citation: N/A — SSE header emission is OCP response-shaping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server): wire heartbeat into streaming path (refs #47)

- sendSSE() accepts optional hb handle and calls hb.reset() before write
- callClaudeStreaming starts heartbeat after ensureHeaders() and passes
  hb to the three streaming sendSSE call sites
- All three exit paths (proc close, proc error, res close) call
  hb.stop() to guarantee timer cleanup; no-op handle when disabled
  means zero runtime cost when CLAUDE_HEARTBEAT_INTERVAL=0

Heartbeat never aborts — only writes comment frames and re-arms. Aligns
with v3.3 timeout discipline (single CLAUDE_TIMEOUT, no secondary
client-killing timers).

cli.js citation: N/A — SSE response shaping is OCP translation layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server): add X-Accel-Buffering: no to SSE response headers (refs #47)

nginx (and many LBs / Cloudflare) default to proxy_buffering=on, which
would buffer heartbeat comment frames indefinitely and defeat the
feature silently. This header hints no-buffering; other stacks ignore
it. Applied at both SSE header sites (real streaming + cache-hit).

cli.js citation: N/A — response header shaping is OCP translation layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(release): v3.12.0 — streaming heartbeat (refs #47)

Bundles the release-kit companion files per Iron Rule 5.2 / 11 example:
version bump across package.json + ocp-plugin + openclaw.plugin.json,
CHANGELOG v3.12.0 section, README env var row + "Streaming heartbeat"
explainer.

Tag push to v3.12.0 triggers .github/workflows/release.yml to create
the GitHub Release automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(server): ensureHeaders returns true when already sent (refs #47)

Phase 3 smoke test revealed every content chunk was being dropped
after the D4 eager ensureHeaders() call: the stdout.on('data') guard
`if (!ensureHeaders()) return;` early-returned on every chunk because
ensureHeaders returned false for the already-sent case (conflated with
the dead-connection case).

Split the two conditions explicitly: return false only when res is
ended/destroyed; return true when headers are (already or now) sent.
This also fixes a latent multi-chunk bug on main that was masked
because claude CLI typically outputs in one stdout chunk.

Verified: node -c server.mjs; subsequent re-run of Phase 3 smoke test
now sees streaming content chunks + heartbeat frames.

cli.js citation: N/A — SSE response shaping is OCP translation layer.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v3.12.0
2026-04-25 09:09:47 +10:00
cff06439fa chore(privacy): remediation follow-up from ocp#44 (#45)
Three-part follow-up from the 2026-04-22 privacy postmortem:

1. OAUTH_CLIENT_ID verified as public Claude Code constant (not a secret).
   Added gitleaks allowlist entry. The value 9d1c250a-e61b-44d9-88ed-5944d1962f5e
   is the public PKCE client ID used by the Claude Code CLI — public clients in
   PKCE flows have no client secret, so the ID itself carries no secret value.
   Introduced in commit b87992f (the 2026-04-11 drift incident); the constant
   mirrors what cli.js embeds for its OAuth token-refresh flow.

2. README.md API key example made clearly fake (ocp_example12345abcde...) to
   avoid gitleaks false-positives and clarify to readers it is not real.
   The ocp_ prefix IS the real prefix (keys.mjs line 80: randomBytes(24).base64url),
   so the prior example could be mistaken for a real truncated key.

3. PR template: added Privacy self-check section for PUBLIC repos below the
   existing 5.3 user-visible change self-check section.

4. .gitleaks.toml: new file with allowlist for the three confirmed non-issues
   (OAUTH_CLIENT_ID constant, README placeholder regex, old plan doc path).

No code or behavior change beyond the docs, README, and new config file.

Closes part of ocp#44.

Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 11:40:50 +10:00
1c29f4867f chore(privacy): scrub personal identifiers from public repo (#43)
Removes all occurrences of maintainer's real name and handle from
tracked files in this PUBLIC repository. Replaces with generic terms
like "project maintainer" / "the maintainer".

Files scrubbed:
- CLAUDE.md (2 instances)
- ALIGNMENT.md (Auditor field)
- docs/adr/0002, 0003, 0004 (Authors/Deciders fields + body text)
- memory/constitution.md (1 instance)
- docs/superpowers/plans/2026-04-10-lan-mode.md (Users/taodeng paths → $HOME)

Also:
- Replaces specific machine hostnames (Taos-Mac-mini, Taos-MacBook-Pro)
  with role-based names (home-mac, travel-macbook) in ADR 0001 (if
  committed; currently untracked).
- Replaces /Users/taodeng/ paths with $HOME/ in old plan doc.

NOTE: git history still contains the personal info. See postmortem
(issue TBD) for whether to rewrite history via git-filter-repo.

Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 11:01:44 +10:00
cdd6b41261 fix(concurrency): release slot on subprocess SIGTERM failure (#40)
Per forensic analysis in #37, the timeout handler at server.mjs:471
called `proc.kill('SIGTERM')` without decrementing the concurrency
counter (`stats.activeRequests`). If the subprocess was stuck in a
syscall (e.g. MCP I/O) and ignored SIGTERM, its slot was not freed
until — and only if — the `proc.on('close')` handler eventually fired
after the 5s SIGKILL escalation successfully reaped the child.
Real-world observation (#37) shows 3 stuck claude subprocesses running
20 minutes to 2h 45m, exhausting the 8-slot pool and returning
`500 concurrency limit reached (8/8)` on every subsequent request.

Fix: attach `cleanup` to `proc.once('exit', ...)`. The 'exit' event
fires before 'close' and runs on every termination path — normal
completion, error, SIGTERM, SIGKILL, crash — even if stdio pipes
stay open. `cleanup()` is already idempotent (guarded by `cleaned`
flag), so this listener is safe alongside the existing 'close' and
'error' paths that also call it.

ALIGNMENT: cli.js has no equivalent subprocess-pool / concurrency-
limit logic — cli.js is a single-user CLI, the process being pooled,
not the pool itself. Per ALIGNMENT.md Rule 2, this fix is proxy-
internal (subprocess lifecycle management) with no cli.js equivalent
to cite; documented here instead. No wire-protocol, HTTP surface, or
response shape change (Rule 3 preserved).

Release kit: bumps version to 3.11.1 and adds CHANGELOG entry so the
auto-release workflow tags v3.11.1 on merge. README version refs are
feature-gate markers for v3.11.0 (models.json SPOT / auto-sync) and
remain historically accurate — no README change needed.

Fixes #37.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
v3.11.1
2026-04-21 20:21:36 +10:00
9facd8307a feat(speckit): integrate github/spec-kit for spec-driven development (#38)
Installs spec-kit slash commands (/speckit-specify, /speckit-plan,
/speckit-tasks, /speckit-implement, /speckit-analyze, /speckit-clarify,
/speckit-checklist, /speckit-constitution, /speckit-taskstoissues) into
Claude Code via .claude/skills/speckit-*/.

Note: specify-cli v1.0.0 init fails on Windows because spec-kit v0.5+
releases no longer attach binary zip assets to GitHub releases (confirmed
by inspecting all 30 releases via API; only v0.4.4 and earlier have
assets). Templates were sourced directly from the github/spec-kit@v0.7.3
repo tree and SKILL.md files were built per the claude integration spec
in src/specify_cli/integrations/claude/__init__.py (user-invocable:true,
disable-model-invocation:false, argument-hint injection).

Enables the "specs in git" workflow for cross-device work state:
- New feature → /speckit-specify → specs/NNN/spec.md
- Design → /speckit-plan → specs/NNN/plan.md
- Work → /speckit-tasks → specs/NNN/tasks.md (handoff artifact)
- Implement → /speckit-implement → code

Preserves existing CLAUDE.md (@AGENTS.md bridges intact) and existing
memory policies in AGENTS.md. memory/constitution.md is annotated at
the top to reference AGENTS.md for project-specific constraints.

No change to server.mjs, models.json, package.json, or any other code.

Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 20:10:52 +10:00
2ecc4945ad docs(governance): add AGENTS.md bridge + 3 historical ADRs (#36)
Integrates OCP with Tao's cross-device development system (Phase 1 L1).

- AGENTS.md: project-level tool-agnostic instructions (read by Cursor,
  OpenCode, Copilot, etc. in addition to Claude Code). Inherits from
  ~/.cc-rules/AGENTS.md.
- CLAUDE.md: prepends @AGENTS.md + @~/.cc-rules/AGENTS.md imports.
- docs/adr/0002-alignment-constitution.md: captures why ALIGNMENT.md
  exists (2026-04-11 drift response).
- docs/adr/0003-models-json-spot.md: captures v3.11.0 SPOT refactor.
- docs/adr/0004-openclaw-auto-sync.md: captures v3.11.0 auto-sync design.

ADRs numbered 0002-0004 because an untracked 0001-cross-device-system.md
already exists locally and was out of scope for this PR.

Governance/docs only. No code change. No version bump (not a release).

Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 19:20:26 +10:00
497ff1dcd2 chore(governance): add release-kit overlay + PR self-check + auto-release workflow (#35)
Implements cc-rules v1.4's 第五律 5.5 project overlay requirement.

- CLAUDE.md: declare release_kit: YAML block (version_source,
  changelog, release_channel, docs_source, resource_lists,
  new_feature_doc_expectations, bootstrap_quirk_policy)
- .github/PULL_REQUEST_TEMPLATE.md: add 5.3 user-visible-change
  self-check section with reviewer gate instruction
- .github/workflows/release.yml (NEW): auto-create GitHub Release
  from CHANGELOG.md section on v* tag push. Idempotent (checks if
  release already exists). Closes the gap that caused v3.9.0 /
  v3.10.0 / v3.11.0 to each miss their GH Release.

Governance-only change. No code, no user-visible behavior change
(the workflow only fires on future tags). No README update needed.

Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 05:16:38 +10:00
2e634f708b docs(readme): document v3.11.0 features (auto-sync, models.json SPOT, troubleshooting) (#34)
Backfills user-visible feature documentation for v3.11.0 that was missing
from PR #33 (which only fixed stale references). Per Tao's review: shipping
a feature without README documentation is a worse failure than stale
references, since users don't know the capability exists.

Added:
- "Self-Update" section: explain the full ocp update pipeline order
- New "OpenClaw Auto-Sync (v3.11.0+)" section: when it triggers, what
  gets synced (with strict scope boundaries), safety guarantees, manual
  invocation, opt-out, bootstrap caveat, behavior for non-OpenClaw IDEs
- "Available Models" expanded: explain models.json as SPOT, contributor
  workflow for adding a new model
- "Troubleshooting" two new entries:
    - "Startup log warns OpenClaw registry out of sync"
    - "OpenClaw shows old models after ocp update (v3.10→v3.11 only)"

No code changes. README only.

Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 20:27:51 +10:00
820abc4b89 docs(readme): refresh model list and version references for v3.11.0 (#33)
- Verify-curl example: 3 stale ids → 4 current ids
- Connect-script output sample: v3.10.0 → v3.11.0, "3 models" → "4 models"
- OpenClaw setup output: add ocp/claude-opus-4-7 to the example
- Available Models table: add opus-4-7, retain opus-4-6 for pinning, mark default aliases, link to models.json as canonical source
- OpenClaw Integration section: add bullet for ocp update auto-sync (v3.11.0+)

No code changes. README only.

Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 20:19:51 +10:00
ab9e0c656b chore(release): v3.11.0 — models.json SPOT + OpenClaw auto-sync (#32)
- Bumps version 3.10.0 → 3.11.0.
- Adds CHANGELOG.md with v3.11.0 entry.

Rolls up PR #30 (refactor: models.json SPOT) and PR #31 (feat:
idempotent OpenClaw registry sync on `ocp update` + passive drift
self-check). No server.mjs changes in this PR.

Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
v3.11.0
2026-04-20 17:37:29 +10:00
5ef163aa95 feat(sync): idempotent OpenClaw registry sync in ocp update (#31)
Adds scripts/sync-openclaw.mjs which reconciles
config.models.providers["claude-local"].models and
config.agents.defaults.models["claude-local/*"] with models.json.

Hooked into `ocp update` between git pull and proxy restart, non-fatal
(sync failure does not abort the update; the gateway still restarts and
/v1/models still works).

Scope boundaries (honoring the no-OpenClaw-source-edit constraint):
  - Only touches claude-local provider block and claude-local/*
    alias keys. baseUrl / api / authHeader preserved on existing
    installs (user may have customized port). All other providers
    and top-level config keys untouched.
  - Creates a timestamped backup (openclaw.json.bak.<ms>) before every
    write. No-op path (already in sync) skips backup.
  - Exits 0 with a skip message when ~/.openclaw/openclaw.json does not
    exist (non-OpenClaw users).

server.mjs change: a 15-line passive self-check added inside the
existing server.listen() callback. It only reads the OpenClaw config
and emits console.warn on drift. No network/endpoint surface added, no
new headers, no new routes, no new Claude-CLI-call path. Per
ALIGNMENT.md Rule 2 this is not an operation cli.js performs, so no
cli.js:NNNN citation is required. The added `existsSync` import from
node:fs is already part of the node:fs module surface.

Independent reviewer: Tao pre-approved the 3-PR plan; Iron Rule 10
waived for this task-scoped execution.

Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 17:35:41 +10:00
c6f7850e89 refactor(models): extract models.json as single source of truth (#30)
Pure refactor. No API surface change. MODEL_MAP and MODELS in server.mjs
are now derived from models.json; /v1/models response is byte-equivalent
to the previous hardcoded table (verified via equality check on the
resulting Object.entries sorted).

setup.mjs MODEL_ID_MAP / MODELS / MODEL_ALIASES also derived from
models.json, fixing a latent drift where setup.mjs still listed
claude-opus-4-6 / claude-haiku-4 (v3.0-era) while server.mjs had
already moved to opus-4-7 + haiku-4-5-20251001 in v3.10.0.

server.mjs change scope: no network/endpoint surface modified. No new
env vars, headers, or routes. This is a pure data-source refactor of
two existing top-level constants (MODEL_MAP, MODELS). No cli.js
operation is being added or changed, so per ALIGNMENT.md Rule 2 this
requires no cli.js:NNNN citation — the change does not touch the
Claude-CLI-call boundary.

Independent reviewer: Tao pre-approved the 3-PR plan that contains this
refactor; review is waived for this PR under that pre-approval
(CLAUDE.md Iron Rule 10 exception, task-scoped).

Unblocks PR B (scripts/sync-openclaw.mjs), which needs a single source
of truth to reconcile with on `ocp update`.

Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 17:32:46 +10:00
43cd7712e6 chore(release): v3.10.0 — Claude Opus 4.7 support (#28)
Minor bump. User-visible new feature: Claude Opus 4.7 model is now
selectable via OCP.

- New model id 'claude-opus-4-7' in MODEL_MAP and /v1/models
- Short aliases 'opus' and 'claude-opus-4' now route to 4.7
- 'claude-opus-4-6' remains explicit for pinned usage

Evidence chain (see PR #27):
  - Anthropic /v1/models: claude-opus-4-7 (display 'Claude Opus 4.7')
  - claude.exe v2.1.114 strings: claude-opus-4-7 present
  - Live probe verified 2026-04-20

No wire-format breaking changes.

Co-authored-by: Oracle Public Cloud User <opc@instance-20230820-1333.subnet07301351.vcn07301351.oraclevcn.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
v3.10.0
2026-04-20 15:52:30 +10:00
ba273aaf06 feat(models): add Claude Opus 4.7 to model selection (#27)
Adds claude-opus-4-7 as an available model and promotes the short
aliases 'opus' and 'claude-opus-4' to point at 4.7 (following the
same 'latest under the short alias' pattern as sonnet/haiku). Explicit
claude-opus-4-6 remains available for pinned usage.

Claude Code alignment evidence (binary-era; see note):
  - Anthropic /v1/models (2026-04-20): returns id='claude-opus-4-7',
    display_name='Claude Opus 4.7', 1M input / 128K output,
    created_at=2026-04-14.
  - Claude Code v2.1.114 /home/opc/.npm-global/lib/node_modules/
    @anthropic-ai/claude-code/bin/claude.exe strings table contains
    'claude-opus-4-7'.
  - Live probe: 'claude -p --model claude-opus-4-7' returns a valid
    response (verified 2026-04-20).

Note on ALIGNMENT.md grep rule: Claude Code 2.1.90+ ships a native
binary (claude.exe) instead of cli.js JavaScript. The grep-based
verification in ALIGNMENT.md Rule 1 is substituted here with
(a) binary 'strings' extraction and (b) Anthropic /v1/models as
an independent authoritative source. A follow-up constitution
amendment will codify the binary-era verification procedure.

Co-authored-by: Oracle Public Cloud User <opc@instance-20230820-1333.subnet07301351.vcn07301351.oraclevcn.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 15:40:57 +10:00
7cff33cc18 chore(release): v3.9.0 — alignment constitution + usage endpoint restoration (#26)
Minor version bump covering substantive functional + governance changes
since v3.8.0:

Functional
  - Restore header-based /usage probe (reads anthropic-ratelimit-unified-*
    response headers; mirrors Claude Code cli.js vE4). Progress bars
    work again after 9-day drift. PR #21 (fd7973a) + #24 (01e260c).
  - Remove stale-cache compensation for the hallucinated endpoint. PR #23.
  - CLAUDE_CODE_OAUTH_TOKEN environment variable fallback for credentials.
  - Exponential backoff on OAuth refresh (60s -> 3600s) to prevent tight
    retry loops that previously burned rate-limit in seconds.

Governance
  - ALIGNMENT.md project constitution (five binding rules, annual audit
    pinned to 11 April, Historical Lesson recording b87992f drift).
  - CLAUDE.md session instructions requiring cli.js citations for every
    server.mjs change.
  - .github/PULL_REQUEST_TEMPLATE.md with mandatory alignment evidence.
  - .github/workflows/alignment.yml CI guardrail (hard-fail blacklist +
    soft-check commit citation).

No API / wire-format breaking changes.

Co-authored-by: Oracle Public Cloud User <opc@instance-20230820-1333.subnet07301351.vcn07301351.oraclevcn.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
v3.9.0
2026-04-20 15:24:36 +10:00
d4f4aaf33a docs(alignment): backfill fix commit SHAs for 2026-04-11 drift (#25)
PR #21 (fd7973a) removed the hallucinated /api/oauth/usage endpoint.
PR #24 (01e260c) then fixed the OAuth Bearer header for the restored
header-based probe.

Co-authored-by: Oracle Public Cloud User <opc@instance-20230820-1333.subnet07301351.vcn07301351.oraclevcn.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 13:22:59 +10:00
22806bffb5 fix(usage): send OAuth Bearer + anthropic-beta header for /v1/messages probe (#24)
Follow-up to #21. The restored header-based fetchUsageFromApi() copied
the golden 47e39d7 implementation verbatim, which used x-api-key auth
because v3.0.0 targeted API-key users (sk-ant-api03-*). Current OCP
uses OAuth tokens (sk-ant-oat01-*) from Claude Pro/Max subscriptions,
so x-api-key with an OAuth token returns 401, breaking /usage.

Claude Code cli.js alignment evidence:
  - For OAuth calls, headers are:
      Authorization: Bearer <accessToken>
      anthropic-beta: oauth-2025-04-20
  - Constant qJ="oauth-2025-04-20" in cli.js
  - Multiple call sites confirmed (e.g. /v1/files upload, /v1/sessions)

Fix: replace x-api-key with Authorization: Bearer + add anthropic-beta
header. Matches the exact headers cli.js sends for every OAuth request.

ALIGNMENT.md compliance: change aligns OCP with cli.js; CI blacklist
unaffected.

Co-authored-by: Oracle Public Cloud User <opc@instance-20230820-1333.subnet07301351.vcn07301351.oraclevcn.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 13:21:22 +10:00
6bfffd2cba chore(server): revert stale-cache compensation for hallucinated endpoint (#23)
Removes the stale-cache fallback branches in handleUsage() and
handleStatus() originally introduced by cb6c2a8 (2026-04-12).

Background: cb6c2a8 was a compensation for b87992f's hallucinated
/api/oauth/usage endpoint starting to 429 within 24h of deployment.
Since PR B restores the correct header-based endpoint from /v1/messages,
this compensation is now dead code masking a problem that no longer exists.

Changes:
  - handleUsage: remove `else if (usageCache.data)` stale fallback
  - handleStatus: remove `else if (usageCache.data)` stale fallback
  - USAGE_CACHE_TTL: already reset to 5min in PR B's rewritten block
    (cb6c2a8 had bumped it to 15min as further compensation)

Depends on #21 (PR B: restore header-based /usage). Merge PR B first.

Co-authored-by: Oracle Public Cloud User <opc@instance-20230820-1333.subnet07301351.vcn07301351.oraclevcn.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 13:17:11 +10:00
2853088261 [Constitution] Alignment principle + CI guardrails (addresses b87992f drift) (#20)
* docs(constitution): establish OCP alignment constitution + CI guardrails (PR A)

Introduces the OCP project constitution to structurally prevent the kind
of scope drift that produced commit b87992f on 2026-04-11 (the fabricated
"/api/oauth/usage" endpoint, which does not appear in cli.js and broke
the dashboard usage bar for nine days).

This PR is governance-only. It does not modify server.mjs, package.json,
or any runtime code. It is intentionally shipped as one reviewable unit
per Iron Rule 11 (governance is one layer).

Files added:
  - ALIGNMENT.md
      Supreme scope document. Core principle: OCP is a proxy layer for
      Claude Code, not an extension layer. Five binding Rules: grep
      cli.js first; no invention; match the implementation; unalignable
      features are deleted; commits cite cli.js line numbers. Includes
      the 2026-04-11 drift postmortem, the Unalignable Policy, and an
      Annual Alignment Audit fixed to 11 April each year.

  - CLAUDE.md
      Project session instructions. Flags ALIGNMENT.md as required
      reading before any code. Codifies three hard requirements for
      server.mjs changes: cli.js citation, CI blacklist pass, and an
      independent reviewer per Iron Rule 10. References CC 开发铁律
      Rules 10, 11, and 12.

  - .github/PULL_REQUEST_TEMPLATE.md
      Mandatory "Claude Code Alignment Evidence" section. Three author
      checkboxes (cli.js citation, scope justification if cli.js does
      not perform the op, commit-message citations). Reviewer checklist
      requires opening cli.js at the cited lines before approval. A PR
      with this section blank receives request-changes.

  - .github/workflows/alignment.yml
      Hard-fail blacklist on server.mjs for tokens "api/oauth/usage"
      and "api/usage" (scan restricted to server.mjs; ALIGNMENT.md and
      CLAUDE.md may quote them as historical references). Soft check
      over all PR commit messages for "Claude Code uses X" / "cli.js
      uses X" assertions lacking a cli.js:NNNN or cli.js vE4 <fn>
      citation.

Historical reference: b87992f ("fix: use dedicated /api/oauth/usage
endpoint for reliable plan data") asserted the endpoint was used by
Claude Code CLI. The string does not occur in cli.js. Root cause was
LLM hallucination accepted without grep verification. See ALIGNMENT.md
-> Historical Lesson for the full record.

Merge precondition: this PR must be approved by an independent reviewer
(Iron Rule 10). The drafter of this commit may not self-approve.

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

* docs(alignment): pin first audit to 2026-04-20 (cli.js 2.1.89, SHA-256 a9950ef6)

First annual alignment audit pin. Records the cli.js version and content
hash that the current ALIGNMENT.md codified implementations mirror.

- Claude Code version: 2.1.89
- cli.js SHA-256: a9950ef6407fdc750bddb673852485500387e524a99d42385cb81e7d17128e01
- Audit date: 2026-04-20
- Auditor: Tao Deng

Next audit: 2027-04-11 (drift anniversary).

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

* ci(alignment): narrow blacklist to full host+path + strip comments before grep

Two false positives discovered during PR #20 bootstrap CI:
1. /api/usage is a legitimate OCP dashboard route (per-key quota, added
   in v3.8, server.mjs:1472). The bare token "api/usage" was too broad.
2. The ANCHOR warning comment in server.mjs (added by PR #21) references
   /api/oauth/usage as a DO-NOT-USE example, triggering the scanner.

Fix: require full host "api.anthropic.com/api/oauth/usage" to ensure
only real outbound fetch calls trip the guard, and strip line comments
with sed before grep so historical ANCHOR warnings pass.

Amendment procedure (ALIGNMENT.md) still governs future blacklist
changes.

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

---------

Co-authored-by: Oracle Public Cloud User <opc@instance-20230820-1333.subnet07301351.vcn07301351.oraclevcn.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 13:15:34 +10:00
fd7973addb fix(server): restore header-based /usage (revert b87992f drift) (#21)
Replaces fetchUsageFromApi() hallucinated /api/oauth/usage endpoint
with the original header-based approach: POST /v1/messages with
max_tokens=1 and extract anthropic-ratelimit-unified-{5h,7d}-{utilization,reset}
from response headers.

Drift: b87992f (2026-04-11) introduced /api/oauth/usage — an endpoint
that does not exist in Claude Code cli.js. Hallucinated. Within 24h
it started returning 429, which cb6c2a8 attempted to mask with stale
cache + extended TTL (cleaned up in follow-up PR C).

Golden reference: 47e39d7 v3.0.0 (2026-03-24) — correct implementation
using anthropic-ratelimit-unified-* headers.

Claude Code cli.js alignment evidence:
  - function vE4 iterates [["five_hour","5h"],["seven_day","7d"]]
  - reads headers anthropic-ratelimit-unified-${key}-utilization
    and anthropic-ratelimit-unified-${key}-reset
  - cli.js path: /home/opc/.npm-global/lib/node_modules/@anthropic-ai/claude-code/cli.js

Preserved modernisations from post-47e39d7 work:
  - getOAuthCredentials(): keychain + Linux ~/.claude/.credentials.json
  - NEW: CLAUDE_CODE_OAUTH_TOKEN env var fallback (highest precedence)
  - OAuth refresh on 401 / pre-emptive on expiry
  - NEW: exponential refresh backoff 60s -> 3600s to prevent tight
    retry loops (previously burned rate-limit in seconds after 401)

Added ALIGNMENT anchor comment at top of block referencing cli.js vE4
and ALIGNMENT.md, so future refactors can grep before re-drifting.

Co-authored-by: Oracle Public Cloud User <opc@instance-20230820-1333.subnet07301351.vcn07301351.oraclevcn.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 13:12:29 +10:00
b908fec7b4 docs(readme): sync to v3.8.0 (#19)
- Remove "Status: Stable — Feature-complete, bug fixes only" tagline
- Add Per-Key Quota section with curl examples and 429 response format
- Add Response Cache section with enable/management instructions
- Update API Endpoints table with new quota + cache endpoints
- Add CLAUDE_CACHE_TTL to Environment Variables table
- Update version references from v3.7.0 to v3.8.0

Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.8.0
2026-04-17 07:04:08 +10:00
97ca91341c feat(server): per-key quota + response cache (v3.8.0) (#18)
Add two new features for LAN sharing governance:

Quota (budget control):
- Per-key daily/weekly/monthly request limits (NULL = unlimited)
- Idempotent schema migration for quota columns
- Single-query check (SUM/CASE) for all 3 periods — no N+1
- PATCH /api/keys/:id/quota (partial update, input validation)
- GET /api/keys/:id/quota (current limits + usage)
- 429 with structured error when exceeded
- Only applies to identified per-key users, not admin/anonymous

Response cache:
- SHA-256 hash of model + messages + temperature/max_tokens/top_p
- Opt-in via CLAUDE_CACHE_TTL env var (0 = disabled, default)
- Cache hit serves both streaming (simulated SSE) and non-streaming
- Streaming responses accumulated and cached on success
- Skips multi-turn sessions (conversationId present)
- GET /cache/stats, DELETE /cache admin endpoints
- Runtime-tunable cacheTTL via PATCH /settings
- 10-minute periodic cleanup of expired entries

Bug fix discovered during testing:
- SQLite datetime('now') stores 'YYYY-MM-DD HH:MM:SS' but JS
  .toISOString() produces 'YYYY-MM-DDTHH:MM:SS.sssZ'. String
  comparison breaks for same-day ranges. Added sqliteDatetime()
  helper for correct format matching.

Code review fixes:
- DELETE /api/keys/:id no longer shadows /quota sub-routes
- updateKeyQuota uses partial UPDATE (only SET provided fields)
- cacheHash includes temperature/max_tokens/top_p in hash
- Replaced raw getDb() in server.mjs with findKey() encapsulation
- Unified UTC midnight calculation across checkQuota/getKeyQuota

Includes 24-test integration suite (test-features.mjs).

Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:45:44 +10:00
f3745fa8fe docs(readme): sync to v3.7.0 — anon key, auto-discovery, current model IDs (#17)
Brings README up to date with the four PRs merged on 2026-04-12:

* #13 ocp-connect v1.1.0 — per-agent auth-profiles write
* #14 ocp-connect v1.2.0 — IDE detection rewrite + hint density
* #15 OCP server v3.7.0 — PROXY_ANONYMOUS_KEY allowlist
* #16 ocp-connect v1.3.0 — auto-discover anonymous key from /health

## Changes

1. Banner updated `v3.6.0` to `v3.7.0`.

2. New "Zero-config" paragraph in Client Setup showing
   `./ocp-connect <server-ip>` works without `--key` when the
   server admin has set PROXY_ANONYMOUS_KEY.

3. Client Setup example output replaced to reflect v1.3.0 actual
   behavior: `OCP Connect v1.3.0` banner, `Remote OCP v3.7.0`,
   new `ⓘ Using server-advertised anonymous key` block, new
   `Per-agent auth profile seeded (2)` line with both main and
   macbook_bot agentDir paths, new three-line smoke test caveat,
   and correct `claude-haiku-4-5-20251001` model ID.

4. "The script automatically" list expanded from 3 bullets to 5:
   added auto-discovery note (v1.3.0+/v3.7.0+), added OpenClaw
   per-agent profile note, corrected IDE hint bullet to list
   Cline/Continue.dev/Cursor/opencode explicitly as "manual
   configuration required" rather than the earlier inaccurate
   "configures" wording.

5. Available Models table updated `claude-haiku-4` to
   `claude-haiku-4-5-20251001` to match the actual `/v1/models`
   response.

6. Environment Variables table: new `PROXY_ANONYMOUS_KEY` row
   added after `PROXY_API_KEY`, with a link to the existing
   "Anonymous Access (optional)" section.

## Not changed

* `package.json` already at 3.7.0 from #15.
* ocp-connect script unchanged (v1.3.0 from #16).
* server.mjs unchanged (v3.7.0 from #15).
* No behavioral changes. README text only.

## Test

`git diff --stat main -- README.md` = +24 -7 on one file. Markdown
spot-checked for broken table alignment and link targets. The
anonymous key section anchor `#anonymous-access-optional` resolves
to the existing heading at line 234.

Co-authored-by: taodeng <taodeng@Taos-MBP>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3.7.0
2026-04-12 21:29:06 +10:00
d71e0455e3 feat(ocp-connect): auto-discover anonymous key from /health (v1.3.0) (#16)
Closes the zero-config loop opened by #15 (server anonymous allowlist).

## Why

After #15 (v3.7.0) the OCP server exposes the admin-chosen anonymous
key via `GET /health.anonymousKey` whenever `PROXY_ANONYMOUS_KEY` is
set. Before this PR, users still had to copy that key manually and
pass it as `--key` every time they ran `ocp-connect`. This commit
closes the last mile: `ocp-connect <host>` now reads the key from
the server and uses it automatically when no `--key` was provided.

Result: `ocp-connect 172.16.2.30` (zero args) on a fresh machine
configures OpenClaw multi-agent correctly against a v3.7.0 OCP
instance that has `PROXY_ANONYMOUS_KEY` set. No admin coordination,
no per-user keys, no manual flags.

## What changes

`OCP_CONNECT_VERSION` 1.2.0 to 1.3.0.

New Step 2.5 between the existing Step 2 (Show remote info) and
Step 3 (Determine if key is needed). The block:

1. Short-circuits if `--key` was passed explicitly, so user intent
   always wins over auto-discovery.
2. Parses `health_json.anonymousKey` via python3 with try/except,
   defensively treating any parse error as "no anon key" so the
   script falls through to the existing Step 3 path.
3. If the server advertised a non-empty anon key, assigns it to the
   internal `key` variable and prints an info banner with the
   truncated key value and a reference to issue #12 section 14
   Path A so users can understand what happened.

All downstream code (key verification via /v1/models, rc file
exports, launchctl setenv, OpenClaw agentDir profile seeding, smoke
test, IDE hints) works unchanged because it reads the same `key`
variable.

Also: defensive check on `--key` to reject explicit empty values
(`--key ""`) with a clear error message. Existing bash `${2:?msg}`
expansion already rejects empty strings, but the new check is an
extra safety net and gives a friendlier error (`omit --key entirely
for anonymous mode`).

## Backward compatibility

Old OCP servers (less than 3.7.0) that don't have the
`anonymousKey` field in `/health`: `python3 d.get("anonymousKey")`
returns None, new block outputs empty string, `[[ -n "" ]]` is
false, block falls through to the existing Step 3 path. Script
behavior is 100% identical to v1.2.0 for these users.

New OCP servers that haven't set `PROXY_ANONYMOUS_KEY`: field is
null, same fall-through path as above.

No user-visible regression in either case. No new CLI flag, no new
prompt, no new error path for existing users.

## Test evidence

Live offline test on macOS 13 against real 172.16.2.30 (OCP v3.7.0
with `PROXY_ANONYMOUS_KEY=ocp_public_anon_v1`):

Step 1: cold install state
  - stopped OpenClaw gateway
  - rm ~/.openclaw/agents/{main,macbook_bot}/agent/auth-profiles.json
  - stripped models.* and agents.defaults.{model,models} from openclaw.json
  - cleaned OCP LAN block from .bashrc
  - unset OPENAI_BASE_URL and OPENAI_API_KEY from launchctl env

Step 2: ran ~/projects/ocp/ocp-connect 172.16.2.30 (no args)

Output:
  OCP Connect v1.3.0
  Remote OCP v3.7.0 (auth: multi)
  Using server-advertised anonymous key: ocp_publ...n_v1
    (set by admin via PROXY_ANONYMOUS_KEY; see issue #12 section 14 Path A)
  API accessible (3 models available)
  Per-agent auth profile seeded (2):
    - ~/.openclaw/agents/main/agent/auth-profiles.json
    - ~/.openclaw/agents/macbook_bot/agent/auth-profiles.json
  Smoke test passed: OK
  EXIT 0

Step 3: verified profiles contain the anon key
  macbook_bot: key = ocp_public_anon_v1
  main: key = ocp_public_anon_v1

Additional regression tests:
  - ocp-connect 172.16.2.30 --key ocp_real_admin_key -> still uses
    the explicit key, auto-discovery banner NOT shown
  - ocp-connect --version -> "ocp-connect 1.3.0"
  - ocp-connect 172.16.2.30 --key "" -> rejected with clear error

## Code review

One independent opus reviewer. Verdict PASS WITH CONCERNS, 0
blockers. Reviewer's Concern B (--key "" edge case) turned out to
be a false alarm on my side verification: bash `${2:?}` expansion
already rejects empty strings. I added an extra defensive check
anyway for clearer error message and safety if the arg parser is
refactored in the future. Other concerns (nice-to-have
simplifications like `print(k or '')`) are cosmetic and deferred.

Co-authored-by: taodeng <taodeng@Taos-MBP>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:12:44 +10:00
38070fbabb feat(server): anonymous key allowlist for multi-mode (v3.7.0) (#15)
Implements issue #12 section 14 Path A. Lets OCP admin designate a
single well-known "anonymous" key that bypasses validateKey() while
keeping AUTH_MODE=multi. Gives OpenClaw multi-agent clients (which
MUST send a non-empty Authorization header per their per-agent
auth-profiles schema) a way to connect without every user needing
a personal key.

## Background

After PR-1 (issue #12), we know OpenClaw multi-agent setups
require a per-agent auth-profiles.json with a non-empty `key`
field. OCP multi-mode rejects any non-empty Bearer token that
isn't in the keys database (server.mjs line 1152), which creates
a deadlock: the only way for OpenClaw to use OCP is with a real
admin-issued key.

Path A resolves this by letting the admin opt in to a public
anonymous key that clients can auto-discover via /health. OpenClaw
writes this key into its agent profiles just like a real key; OCP
server short-circuits validateKey when the key matches the
allowlist. No per-user coordination needed, admin still controls
the policy (can rotate, can unset, can rate-limit).

## Changes

### server.mjs

* Line 100-103: new `PROXY_ANONYMOUS_KEY` env var + warning if set
  while AUTH_MODE is not multi.
* Line 1127-1138 (localhost branch): anonymous allowlist short-circuit
  before validateKey so localhost clients using the anon key are
  labeled "anonymous" instead of "local" in stats.
* Line 1153-1163 (multi branch): the same allowlist check between
  the ADMIN_KEY check and validateKey. Uses timingSafeEqual with
  explicit length check (consistent with the admin and shared key
  patterns above).
* Line 1221 (/health response): new `anonymousKey` field, null when
  not set, the actual value when set. Admins opted into public
  access when they set the env var, so exposing the key here is
  intentional and lets ocp-connect auto-discover it without
  out-of-band coordination.

### package.json

Version 3.6.0 to 3.7.0 (per dev iron rule 5, version bump before push).

### README.md

New "Anonymous Access (optional)" subsection under Auth Modes:
* Enable example (export PROXY_ANONYMOUS_KEY=...)
* Client-side /health discovery explanation
* Security note: opt-in to public access, rate-limit warning
* "Not a secret" note: /health is unauthenticated, the key is
  publicly readable by design; treat it as a convenience handle,
  not as an access credential.

## Test evidence

Offline tests on macOS 13, local server on 0.0.0.0:8889 with
PROXY_ANONYMOUS_KEY=ocp_public_anon_TEST, CLAUDE_AUTH_MODE=multi,
CLAUDE=/bin/echo (mock):

* GET /health ->
    authMode: multi
    anonymousKey: ocp_public_anon_TEST
  (pass, field exposed correctly)

* POST /v1/chat/completions with Bearer ocp_WRONG_KEY from
  172.16.2.29 (non-localhost) -> HTTP 401 in 15ms
  body: Unauthorized: invalid or revoked API key
  (pass, validateKey still rejects unknown keys)

* POST /v1/chat/completions with Bearer ocp_public_anon_TEST from
  172.16.2.29 -> curl hangs at 3s after auth middleware
  (pass, auth passed, request reached Claude handler which hangs
   because CLAUDE=/bin/echo cant serve a real chat; the point is
   the auth middleware accepted the key, confirmed by no 401 return)

* POST /v1/chat/completions with no Authorization from 172.16.2.29
  -> curl hangs at 3s after auth middleware
  (pass, pre-existing empty-token anonymous path not regressed)

node --check server.mjs: syntax OK.

## Code review

One independent opus reviewer. Verdict: PASS WITH CONCERNS, zero
blockers. Two strong-recommend items addressed in this commit:

1. Localhost branch was not covered in the first draft. Reviewer
   pointed out that a localhost client using the anon key would be
   labeled "local" instead of "anonymous" in stats, making operations
   visibility worse. Fixed by applying the same anonymous allowlist
   check in the localhost branch at line 1127-1138.

2. README security note was missing the explicit "not a secret"
   framing. Added a paragraph clarifying that because /health is
   unauthenticated, the anonymous key is publicly readable by
   anyone who can reach the server, which is intentional.

Reviewer nits (tokenBuf3 naming, stats subcategory for header-less
vs anon-key anonymous) are deferred as they do not affect
correctness.

## Upstream dependencies on this commit

After this PR is merged and the OCP instance at 172.16.2.30 is
restarted with `PROXY_ANONYMOUS_KEY` set in the environment, a
follow-up PR can add anonymous key auto-discovery to ocp-connect:

* On startup, ocp-connect calls GET /health
* If anonymousKey field is non-null and --key was not provided,
  ocp-connect automatically uses that key to seed per-agent
  auth-profiles for OpenClaw
* User gets zero-config OCP connectivity for OpenClaw multi-agent
  setups, no admin coordination, no --key flag

That follow-up is NOT in this PR. This PR is server-only.

Co-authored-by: taodeng <taodeng@Taos-MBP>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:36:26 +10:00
2adea6368d fix(ocp-connect): rewrite IDE detection + improve hint density (v1.2.0) (#14)
Follow-up to #12 / #13. Fixes the IDE detection failures documented in
issue #12 section 9.3 (IDE detection coverage matrix).

## What was broken

PR-1 fixed the OpenClaw multi-agent auth bug but left section 9 of the
report (IDE detection coverage) unaddressed. Empirical testing showed
the script's "Other IDEs detected" section had a 25% effective hit rate:

- Cline -> grep pattern was `grep -qi cline` but the actual VSCode
  extension ID is `saoudrizwan.claude-dev`. Hit rate 0%.
- Continue.dev -> checked `~/.continue/config.json` but the directory
  is not created until the user opens the Continue panel for the
  first time, AND v1.x uses `config.yaml` not `config.json`. Hit
  rate 0% on a fresh install.
- Cursor -> only checked `command -v cursor` and `~/.cursor`. Both
  fail when the user installs Cursor.app from the official dmg
  without enabling the optional shell command and without launching
  the app. brew cask installs work by side effect (it links the
  binary). Hit rate 100% via brew, 0% via dmg.
- opencode -> not detected at all. opencode (https://opencode.ai) is
  one of the most popular AI CLI tools, missing entirely from the
  hint list.

The hints themselves were also too sparse. Each one was a single
line like `Cursor: Settings -> OpenAI Base URL = ...`, with no API
key, no model IDs, no path to the actual settings page. Users had
to guess.

## What this commit does

### Detection rewrite (Cline / Continue.dev / Cursor / opencode)

Cline: prefer `code --list-extensions`, fall back to ls, match
pattern `cline|saoudrizwan\.claude-dev`. Real-world Cline detection
goes from 0% to 100%.

Continue.dev: three-condition OR -- VSCode extension ID
`continue.continue` OR `~/.continue/config.yaml` OR
`~/.continue/config.json`. Catches all three real-world install
states (extension installed but never opened / config.yaml / legacy
config.json).

Cursor: add `[[ -d "/Applications/Cursor.app" ]]` as a third
fallback alongside `command -v cursor` and `~/.cursor`. Now detects
Cursor regardless of how it was installed.

opencode: NEW detection branch. Checks `command -v opencode`,
`~/.opencode/bin/opencode`, and `~/.local/share/opencode`. Catches
the official curl install path and any future package manager
installs.

### Hint density improvements

Each hint now includes:
- The exact base URL to paste
- The API key (truncated as `${key:0:8}...${key: -4}` if longer
  than 16 chars to avoid screenshot leakage; or "(none -- anonymous
  mode)" message when --key was not provided)
- The list of available model IDs, extracted from the actual
  /v1/models response so the user does not have to guess
- The exact menu path or config file location

Continue.dev hint specifically shows the YAML snippet under the
`models:` top-level key (per reviewer feedback) so the user can
paste it directly without schema confusion.

opencode hint explicitly says "not yet auto-configured by
ocp-connect (PR follow-up)" so users know to use
`opencode providers login openai` until a future PR adds direct
auth.json writing. opencode does NOT read OPENAI_API_KEY env var
(empirically verified) so the existing rc-file export strategy
does not help it.

### NOT in scope (deferred)

- opencode auto-configure (writing auth.json + opencode.json) --
  schema needs more investigation, opencode model whitelist gets
  in the way of custom claude model IDs
- Continue.dev auto-configure -- v1.x config.yaml schema is well
  enough known but parsing/merging YAML in bash without extra
  dependencies is awkward
- Aider, Cody, Zed, etc. detection -- not in this PR scope

These are future work tracked in #12 section 9.6.

### Version bump (per dev iron rule #5)

OCP_CONNECT_VERSION 1.1.0 -> 1.2.0.

## Test evidence

Offline matrix run on macOS 13 with VSCode 1.115.0, opencode 1.4.3,
Cline 3.78.0, Continue.dev 1.2.22, Cursor 3.0.16:

- All 4 IDEs installed -> all 4 hints fire with full detail
- anonymous mode -> hints fire, key display reads
  "(none -- anonymous mode...)" instead of blank
- HOME=tmpdir mock + only Cline mock dir -> Cline + Cursor (system
  app) hints fire, others suppressed
- HOME=tmpdir mock + nothing -> only Cursor (system app) fires
- --version -> "ocp-connect 1.2.0"
- --help -> still shows the PR-1 wording for item 5

Hit rate matrix on the test machine (5/5 IDEs installed):

| IDE          | before PR-2a | after PR-2a |
|--------------|--------------|-------------|
| OpenClaw     | YES (PR-1)   | YES         |
| Cline        | NO           | YES         |
| Continue.dev | NO           | YES         |
| Cursor       | YES (brew)   | YES         |
| opencode     | NO           | YES         |

Effective detection rate: 25% -> ~100%.

## Code review

One independent opus reviewer (spec compliance + bash 3.2 compat +
UX). Verdict: PASS WITH CONCERNS, 0 blockers. Two non-blocking
suggestions both addressed in this commit:

1. Continue.dev YAML hint -- added `models:` top-level key so
   pasted snippet validates against Continue v1.x schema
2. anonymous mode -- key display now shows
   "(none -- anonymous mode; most external IDEs require a non-empty
    API Key)" instead of a blank field

B1 (OpenClaw per-agent auth) section is intentionally untouched.
Verified by diff line count: all changes fall in lines 347-440
(configure_ides function) + lines 9-13 (version constant). No
edits inside the Python heredoc block.

Co-authored-by: taodeng <taodeng@Taos-MBP>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:59:59 +10:00
7860f71943 fix(ocp-connect): seed per-agent auth-profiles for OpenClaw multi-agent + UX hotfixes (v1.1.0) (#13)
* fix(ocp-connect): seed per-agent auth-profiles for OpenClaw multi-agent + UX hotfixes

Fixes #12. ocp-connect bumped to v1.1.0.

## What was broken

OpenClaw's per-agent auth loader reads <agentDir>/auth-profiles.json
(NOT the root openclaw.json's auth.profiles section, NOT env vars).
ocp-connect only wrote the root config, so OpenClaw multi-agent setups
- any agent with an explicit or implicit agentDir, including the
default `main` agent - silently failed with "No API key found for
provider X" on every Telegram/IDE message. The smoke test passed but
the user's actual workflow was broken.

3 unrelated UX papercuts compounded the bad experience:

* Smoke test only verified OCP direct connectivity, never the
  IDE/agent -> OCP path, so the script printed "Smoke test passed"
  while the bot was silently broken.
* `read ... </dev/tty 2>/dev/null || fallback` leaked
  `Device not configured` errors from the parent shell in CI/SSH/no-tty
  environments. The fallback path worked correctly, but the stderr
  noise looked like a bug.
* Help text claimed `Detects and configures IDEs (OpenClaw, Cline,
  Continue.dev, Cursor)` while only OpenClaw is actually configured.

Full investigation, evidence, and reviewer findings in #12.

## What this commit does

### B1: write per-agent auth-profiles.json (CRITICAL)

After writing root openclaw.json, iterate `agents.list[]`. For each
agent: derive agentDir from the explicit `agentDir` field, OR fall
back to `<openclaw_root>/agents/<id>/agent/` for agents that omit it.
This catches the default `main` agent, discovered via end-to-end
Telegram testing. Merge into existing auth-profiles.json (preserving
other providers' keys), upsert `<provider>:default` with the real
key, chmod 0600.

Anonymous mode (no --key) explicitly skips agentDir writes (empty
keys are dropped by OpenClaw's pi-auth-credentials anyway) and
prints a clear warning that OpenClaw multi-agent requires --key.
This is the "Path C" decision from #12 section 14.

Error handling is split for safety:
* `JSONDecodeError` on existing file -> back up to `.bak`, rebuild
* `OSError` on read -> skip this agent. Do NOT clobber the user's
  existing profile, which may hold other providers' real keys.
* `OSError` on mkdir/write -> record in `_failed` list, surface in
  output. Not silently swallowed.

Three independent report sections (`_seeded` / `_failed` /
`_skipped_anonymous`) so mixed scenarios don't hide warnings behind
each other.

### B2: smoke test caveat

After "Smoke test passed", append three lines explaining that the
test only verifies OCP direct connectivity, and instructing the user
to restart OpenClaw and send a real bot message to verify
end-to-end.

### B3: suppress /dev/tty stderr

Wrap all four `read ... </dev/tty` calls as
`{ read ... </dev/tty; } 2>/dev/null || fallback` so the brace group
captures the shell's own `Device not configured` error before it
reaches the user's terminal. The fallback semantics are unchanged.

### B6: help text wording

Change item 5 from old wording mentioning IDE auto-configuration to
"Configures OpenClaw automatically; prints setup hints for other
IDEs - manual configuration required". Detection logic itself is NOT
changed in this commit. That is deferred to a future PR. See #12
section 9 for details.

### Version bump

Per dev iron rule #5 (version bump before push):
* `OCP_CONNECT_VERSION="1.1.0"` constant
* `--version` CLI flag
* Banner shows version

## Test evidence

End-to-end verified on macOS 13 with the actual OpenClaw 2026.4.11
+ Telegram bot setup that exposed #12:

1. cold-install state (clean ~/.openclaw, no agent profiles)
2. run `ocp-connect 172.16.2.30 --key ocp_xxx` -> exit 0
3. verify both `~/.openclaw/agents/main/agent/auth-profiles.json`
   and `~/.openclaw/agents/macbook_bot/agent/auth-profiles.json`
   are written with mode 0600 and the real key
4. restart gateway -> `agent model: ocp/claude-sonnet-4-6` loads
5. send a real Telegram message -> bot responds via
   `ocp/claude-sonnet-4-6`, `[telegram] sendMessage ok`,
   `auth-state.json` updates `lastUsed` and shows
   `errorCount: 0, lastGood: ocp:default`
6. zero `No API key found` / 401 / lane-error events anywhere

Offline test matrix (also all green):
* anonymous mode -> warning prints, no agentDir write
* corrupted existing auth-profiles.json -> backed up to .bak, rebuilt
* no-tty execution -> 0 `Device not configured` leakage
* `--help` -> new B6 wording
* `--version` -> `ocp-connect 1.1.0`

## Code review

Two independent reviewer subagents (opus, spec compliance + code
quality) plus one blind implementer subagent (sonnet, control
group). The blind implementer reproduced exactly the two MUST-FIX
issues the code-quality reviewer flagged in the first draft (silent
data loss on `except Exception`, misleading success on per-agent
write failure), validating that the review process caught real
non-obvious problems. Both must-fixes are addressed in this commit;
re-tested green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ocp-connect): drop "with agentDir" from anonymous warning

The message previously said "agents with agentDir" but B1.5 now also
seeds agents that omit the field (their dir is derived). Reviewer
feedback on PR #13.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: taodeng <taodeng@Taos-MBP>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 17:36:02 +10:00
taodengandClaude Opus 4.6 e326cee9dd docs: remove outdated troubleshooting workarounds
Removed v3.0.x migration guide, old idleTimeoutSeconds workaround
(auto-handled by setup.mjs since v3.2.1), and manual session cleanup
instructions. Replaced with concise troubleshooting for common issues.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 13:29:05 +10:00
taodengandClaude Opus 4.6 f8ca9b85b0 bump version to 3.6.0 for IDE auto-configuration feature
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 13:27:47 +10:00
taodengandClaude Opus 4.6 c22b0dd074 feat: interactive IDE configuration in ocp-connect
ocp-connect now detects installed IDEs and offers to configure them
automatically. OpenClaw gets full interactive setup (provider name,
primary/backup priority, model aliases). Other IDEs (Cline, Continue.dev,
Cursor) get manual config instructions printed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 13:27:07 +10:00
taodengandClaude Opus 4.6 7f46405152 fix: ocp-connect writes to all shell rc files + system-level env vars
- Writes to both .bashrc and .zshrc on macOS (covers both shells)
- macOS: launchctl setenv for GUI apps and daemons
- Linux: ~/.config/environment.d/ocp.conf for systemd user services
- Ensures IDEs and daemons can discover OCP models

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 12:58:27 +10:00
taodengandClaude Opus 4.6 cb6c2a8b5f fix: fallback to stale cache on usage API 429 + extend cache to 15min
When the /api/oauth/usage endpoint returns 429 (rate limit), fall back
to the most recent cached data instead of returning an error. Also
extended cache TTL from 5min to 15min to reduce API calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 09:44:18 +10:00