mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-19 09:44:07 +00:00
a8601a6d30f1499e23ab5b5857c4c9ff17052b2c
71
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a8601a6d30 |
feat(doctor): --check oauth fast path (#93)
* feat(doctor): --check oauth fast path Implements the --check oauth fast path documented in cmd_doctor_help but previously unimplemented. Skips version detection, from-version check, git operations, and models endpoint — runs only the curl against /health + auth.ok extraction. Use cases: - After `claude auth login`, fast verify OCP can spawn cli.js - After a known service blip, quick health gate before larger ops - AI agent's setup-repair loop: ./ocp doctor --check oauth in a retry-after-fix step 3 unit tests cover: PASS path, OAuth FAIL → fix_oauth, service down → fix_service. No cli.js citation needed: this is OCP-internal doctor logic with no corresponding cli.js operation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(doctor): nit fixes for --check oauth (N3 body=null test + N4 skipped sentinel comment) --------- Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
cd6ec2a212 |
fix(doctor): dynamic latest_version from origin/main; release v3.15.1 (#92)
v3.15.0 doctor used a hard-coded `latest = "v3.14.0"` fallback, causing any v3.15.0+ install to 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. On failure (offline, fresh clone, no remote), falls back to currentVersion so kind=noop instead of recommending a downgrade. Regression test added: doctor with unreachable ocpDir falls back to currentVersion as latest (not the old hardcoded v3.14.0). Caught during v3.15.0 post-deploy verification on home-mac: ./ocp doctor reported `kind=upgrade` immediately after v3.15.0 install, which would have been a critical user-facing bug. No cli.js citation needed: this is OCP-internal doctor logic with no corresponding cli.js operation. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ab03c13332 |
feat(upgrade): ocp doctor + cross-version ocp update + rollback + AI prompts (#91)
* feat(doctor): add ocp doctor with --json + next_action contract Implements scripts/doctor.mjs with semver-aware path selection (noop/update/upgrade/fresh_install/fix_oauth/fix_service) and the JSON contract documented in the design spec. Service health + OAuth checks integrated; mockable via opts.mockHealth for unit tests. 8 unit tests cover the kind dispatch tree and the next_action shape for each kind. No cli.js citation needed: this is OCP-internal tooling with no corresponding cli.js operation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ocp): wire cmd_doctor into bash CLI; dispatch to scripts/doctor.mjs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(doctor): handle unparseable version + empty health body Three issues raised by code-quality reviewer on |
||
|
|
55c576bbb1 |
fix(setup): preserve user-customised plist/systemd env vars on re-setup (#90)
* fix(setup): merge plist/systemd env vars instead of overwriting
setup.mjs previously wrote the launchd plist and the Linux systemd unit
with writeFileSync(path, NEW_TEMPLATE_STRING), which silently dropped any
user-customised env vars (CLAUDE_HEARTBEAT_INTERVAL, CLAUDE_CACHE_TTL,
etc.) on every re-setup or upgrade. This change introduces
scripts/lib/plist-merge.mjs which preserves keys present only in the
existing file and lets the template's known keys win. Linux variant uses
the same logic against Environment=KEY=VALUE lines.
Tests added in test-features.mjs cover preserve / override / first-install
for both formats.
No cli.js citation needed: this is OCP-internal installer behaviour with
no corresponding cli.js operation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(setup): plist-merge code-quality follow-up
Three issues raised by the code-quality reviewer on
|
||
|
|
750b25ba77 |
chore(release): v3.14.0 — security hardening (sessions namespacing + file modes + /api/usage scope) (#89)
Bump version 3.13.0 → 3.14.0. No functional code change in this PR; all three security fixes are already merged in main via PRs #86, #87, #88. This PR covers only metadata + docs: - package.json: version bump to 3.14.0 - CHANGELOG.md: v3.14.0 entry with Features / Behavior changes / Verification / Governance sections - README.md: /api/usage self-scope note in Auth Modes §, API Endpoints table row update, file-mode bullet in Important Notes § cli.js citation N/A: this release PR modifies no server.mjs / setup.mjs / keys.mjs. The three underlying security PRs (#86, #87, #88) each carry their own cli.js-citation-not-applicable disclaimer per PR #75 pattern, as they are OCP-internal access-control, session-state, and file-permission changes with no corresponding cli.js operation. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
fd6e875bd7 |
fix(security): /api/usage default scope = self; admin all-keys requires ?all=true (#88)
Identified during privacy/security audit (.claude/research/ocp-security-audit.md
§3 "/api/usage 全量泄露所有 key 时序"). Previously, any caller authenticated as
admin received the full per-key usage byKey + recent + timeline block, which
means a brief admin-token compromise exposes every household member's request
volume, timing, and which-model — metadata, but sensitive metadata.
This is a deliberate breaking change for least-privilege.
Behavior matrix (post-change):
Caller | Default scope | ?all=true
----------------------------------------|-------------------|---------------------
anonymous (PROXY_ANONYMOUS_KEY) | own ("anonymous") | ignored (still own)
authenticated non-admin key | own (key.name) | ignored (still own)
admin (no flag) | own ("admin") | n/a
admin with ?all=true | n/a | full byKey/recent
localhost-no-token / "local" | own ("local") | full (isAdmin=true)
Response shape is unchanged except for an additional advisory `scope` object
(`{self, all}`); existing fields keep their structure so dashboards and scripts
continue to parse cleanly — they just see less data unless they opt in.
Audit-friendly addition: when admin opts into `?all=true`, the server now
emits `logEvent("info", "admin_usage_full_scope", { caller, ip })` so every
privilege-escalation moment is recorded.
Backward-compat warning for admin scripts:
existing cron jobs / scripts that call `/api/usage` to inventory all keys must
add `?all=true` after this change. Default-self is the new safe behavior.
Dashboard (dashboard.html): adds a "Show all keys" checkbox in the Usage by
Key section. Hidden by default; revealed only when refreshKeys() succeeds
(same admin gate as the keys-management section). Toggle state persists in
localStorage as `ocp_usage_show_all`.
cli.js citation
---------------
This is OCP-internal admin API behavior — there is no `cli.js` operation to
cite. ALIGNMENT.md Rule 2 (the rule that limits OCP to operations that exist
in `cli.js`) does not constrain access-control rules for the OCP server's
own admin endpoints.
Smoke test (isolated test server on :3489, prod :3478 untouched):
- node --check server.mjs SYNTAX_OK
- npm test 43/43 passed
- alignment.yml blacklist grep BLACKLIST_CLEAR
- LAN scope matrix alice/bob own only; alice ?all=true denied;
anon ?all=true denied; admin all=true full +
audit log emitted
Iron Rule 10
------------
Author cannot self-approve. A separate fresh-context reviewer must read the
diff and confirm the cli.js-not-applicable statement is correct before merge.
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
8c0b97f3ae |
fix(security): tighten on-disk credential file modes (700/600) (#87)
Credential-bearing OCP artifacts were created at default umask (0644/0755),
making them world-readable on multi-user hosts. This commit hardens them at
writer time and adds an idempotent startup reconciliation so existing prod
installs are fixed automatically on next service restart.
Changes:
- keys.mjs: mkdirSync(OCP_DIR, { mode: 0o700 }) + chmodSync after to handle
pre-existing dirs; chmodSync(DB_PATH, 0o600) after first getDb() open.
- setup.mjs: chmodSync(plistPath, 0o600) after writeFileSync on macOS;
chmodSync(unitPath, 0o600) after writeFileSync on Linux.
- server.mjs: _tightenFileModesIfPossible() reconciliation block — idempotently
chmods ~/.ocp (700), ~/.ocp/admin-key (600), ~/.ocp/ocp.db (600) on startup;
emits a single info-level log line when any file is tightened; ignores ENOENT
and wraps EPERM in a warn log so startup is never crashed by chmod failure.
Backward compat: all files remain accessible to the same-user owner; 0o600
is still fully readable/writable by the process. Existing prod boxes with
old 0644 ~/.ocp directories get fixed-up on next launchd/systemd restart
without any manual intervention.
cli.js citation: this change is OCP-internal file-permission hardening only.
No cli.js function corresponds to chmod or credential-file management. This
is an OCP-local security improvement that is out of scope for the cli.js
citation requirement per ALIGNMENT.md Rule 2; the cli.js boundary applies
to proxy protocol and API surface changes, not host-filesystem hardening.
Identified during privacy/security audit — .claude/research/ocp-security-audit.md.
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
68acf15373 |
fix(security): namespace sessions Map by keyId — close cross-key conversation collision (#86)
**Bug**: the sessions Map used the raw client-supplied conversationId string as
its key. Two callers with different API keys (or one anonymous + one
authenticated) using the same session_id="default" collided in the Map,
sharing a cli.js subprocess and conversation history — a cross-tenant context leak.
**Fix**: introduce _sessionKey(conversationId, keyName) → "${keyName}|${conversationId}".
Replace every sessions Map site (has / set / get / delete) with the namespaced key.
keyName comes from req._authKeyName (set by auth middleware). Anonymous callers
produce "anon|<id>"; admin produces "admin|<id>"; per-key callers produce
"<keyName>|<id>" — matching the convention used by cacheHash() for per-key cache
isolation (D1, v3.13.0).
**cli.js citation — not applicable**: This PR changes OCP-internal session lifecycle
bookkeeping (the sessions Map that OCP maintains to thread --resume flags across
requests). There is no corresponding cli.js operation to cite. ALIGNMENT.md Rule 2
(limiting OCP to operations cli.js performs) does not constrain in-process state
representation. Same pattern as #75.
**Scope of changes (server.mjs only)**:
- New helper: _sessionKey() — 3 lines after sessions Map declaration
- spawnClaudeProcess(): accepts keyName param; all 4 sessions Map calls → _sessionKey
- handleSessionFailure(): uses sessionKey (not raw conversationId) for sessions.delete
- callClaude(): accepts and forwards keyName to spawnClaudeProcess
- callClaudeStreaming(): forwards authInfo.keyName to spawnClaudeProcess
- Request handler: both callClaude() call sites pass req._authKeyName
- Cleanup interval + GET /health + GET /sessions: strip "keyName|" prefix before log/display
**Smoke test**:
- node --check server.mjs → SYNTAX OK
- npm test → 43/43 passed, 0 failed
- Hand-traced has/set/get/delete paths: cross-key collision absent ✓
Identified during privacy/security positioning brainstorm — `.claude/research/ocp-security-audit.md`
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
a71c939bf8 |
docs(readme): remove zhihu blog backlink + badge (article was removed) (#85)
The Chinese blog post linked from these two places was taken down by the 知乎 platform shortly after publication. Both pointers now resolve to a "content removed" warning page, which is a worse signal to visitors than no link at all. Removing both before they reach more README readers. Reverts the additions from #81 specifically: - Top badges row: drop the "blog · engineering story" shield - §Why OCP? closing blockquote: drop the line pointing at the article The 知乎 article URL is no longer reachable, so retaining the references would route incoming traffic to a dead page that suggests the project is itself problematic. Better to leave that section silent until a working canonical write-up exists somewhere. Doc-only change. server.mjs not touched. ALIGNMENT.md Rule 5 (cli.js citation) does not apply. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
d245c62df7 |
docs(images): replace dashboard.png with multi-client + multi-session traffic (#84)
Continues #82 / #83 — those captures had stats counters at 0 / 3 respectively, with 0 active sessions. The Sessions card therefore visually undermined the rest of the dashboard. Re-captured after deliberately seeding LAN-side traffic from both Pi231 and MacBook clients (each holding a per-key API token issued on the Mac mini server), mixing single-turn and multi-turn requests with distinct session_id values to populate the in-memory sessions Map. New screenshot data points: - Uptime: 21h 56m - Requests: 24 / 0 active (up from 3 / 0) - Errors: 0 / 0 timeouts - Sessions: 8 active (was 0) - Plan Usage: 5h 20%, weekly 28% - byKey table: 11 keys with rich history, now including pi231-test + macbook-test rows from this round - Recent Requests: visible row count grown Multi-turn correctness was incidentally verified during seeding — a Pi231 request with session_id=pi-multi-01 turn 2 correctly recalled the number passed in turn 1 ("the number 7"), confirming session state persists across cli.js subprocess turns as designed. Capture method unchanged (Chrome headless, 1400x2400, --virtual-time-budget=14000). Doc-only change. server.mjs not touched. ALIGNMENT.md Rule 5 (cli.js citation) does not apply. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
047750e642 |
docs(images): replace dashboard.png with stats-populated version (#83)
#82 captured the dashboard mid-idle — server had been up 21h with zero traffic since restart, so the in-memory stats counters all showed 0 (Requests 0 / Errors 0 / Sessions 0). That made the top strip of the screenshot look like a dead service even though the historical byKey + Plan Usage data below were healthy. Re-captured after firing 3 small haiku requests through localhost to populate stats.totalRequests. The new screenshot now shows: - Status: ok / v3.13.0 - Uptime: 21h 39m (up from 21h 28m) - Requests: 3 / 0 active (was 0 / 0) - Plan Usage: 5h 17%, weekly 28% (was 13% / 27%) - Recent Requests: 2 visible rows showing model + latency + status (was empty) Same capture method as #82: Chrome headless --window-size=1400,2400 --virtual-time-budget=12000. The 3 priming requests were short haiku prompts ("reply with the single word OKn"), max_tokens=12 each. Plan-usage delta < 1%, byKey table already had 11 active keys with rich history so the priming did not distort the long-tail data. This addresses feedback that the previous screenshot's empty stats strip undermined the rest of the dashboard's data richness for first- time README readers. Doc-only change. server.mjs not touched. ALIGNMENT.md Rule 5 (cli.js citation) does not apply. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
3bdeb50ed5 |
docs(images): refresh dashboard.png with current prod data (#82)
The previous dashboard.png was captured on day-1, before any real traffic — the screenshot was effectively empty (no Plan Usage bars, single API key, no Recent Requests). It made the Web Dashboard look like a placeholder rather than a working observability surface. Replaced with a fresh capture of the maintainer's Mac mini production OCP (running v3.13.0, multi auth) showing: - Status / uptime / active+queued counters - Plan Usage bars (5h: 13%, weekly: 27%) — real subscription draw - Usage by Key table — 11 active keys with request counts, success/error rates, average latency, last-seen timestamps - API Keys section — all 32 registered keys with truncated key prefixes + creation date + active/revoked status (truncation matches existing dashboard rendering, no full keys are exposed) - Recent Requests log — request stream with model, prompt size, latency Capture method: Chrome headless (no Playwright extension required) at 1400x2400, --virtual-time-budget=12000 to let dashboard JS finish fetching + rendering before the screenshot frame. PNG dimensions: 1400 x 2400 (was 1400 x 1739). The added height comes from the now-populated Usage by Key + API Keys + Recent Requests sections — not from layout changes. Doc-only change. server.mjs not touched. ALIGNMENT.md Rule 5 (cli.js citation) does not apply. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
fbbf3b6c7c |
docs(readme): add engineering-story backlink to 知乎 article + badge (#81)
Adds two pointers from README to a Chinese-language engineering blog post (https://zhuanlan.zhihu.com/p/2036388634207770402) that walks through OCP's cli.js-alignment philosophy, the 2026-04-11 drift incident (the 9-day hallucinated /api/oauth/usage endpoint), the three-tier guardrail design, and the recent fresh-state E2E pass that produced PRs #74-#78. Two minimal touches to README only: 1. Badges row: a "blog · engineering story" shield linking to the article. Slot fits between the existing Release badge and the Buy Me a Coffee badge so the row's visual rhythm is preserved. 2. New blockquote line at the end of §Why OCP? (between the single-maintainer / pre-1.0 disclosure and §Supported Tools). Brief, factual, no marketing voice. Why this PR The 知乎 piece is a self-contained engineering narrative about maintaining a single-maintainer LLM-assisted proxy without endpoint drift. README is the project's primary surface; pointing at the narrative lets readers who land on the repo decide if the project's discipline matters to them before they invest in install. Reverse direction: GitHub readers who follow the link bring some traffic back to the article, validating that engineering content has a home. Doc-only change. server.mjs untouched. ALIGNMENT.md Rule 5 (cli.js citation) does not apply. Same pattern as #68 / #69 / #71 / #78 / #79. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
d760d7fcce |
docs(readme): add restrained star + issue CTA below tagline (#79)
* docs(install): remove false symlink claim, fix Anonymous mode startup command Three doc inaccuracies surfaced during fresh-state install testing on Pi231 + MacBook (Round 1+2): 1. README §Server Setup claimed setup.mjs "Symlink `ocp` to /usr/local/bin for CLI access" — false. setup.mjs writes start.sh + plist/systemd unit but creates no PATH symlink. Removed the line; added a short PATH tip showing the user's options (manual symlink to ~/.local/bin or /usr/local/bin, or shell alias) right after the install summary. 2. README §Anonymous Access told users to run `ocp start` to enable the feature — there is no `ocp start` subcommand. Available commands are restart / stop / status / logs / keys / usage / update / lan / health / clear / settings (verified via `~/ocp/ocp` enumeration). Replaced with the correct flow: export PROXY_ANONYMOUS_KEY, then `node setup.mjs --bind 0.0.0.0 --auth-mode multi`. 3. The same paragraph implied that exporting PROXY_ANONYMOUS_KEY in an interactive shell is enough to enable anonymous access — but the running proxy is auto-started by launchd/systemd from the service unit's own env, not from the user's shell. Spelled this out and noted that if OCP was installed before exporting the env var, the user must re-run setup.mjs (idempotent) so the service unit env is refreshed, then `ocp restart`. The PROXY_ANONYMOUS_KEY mechanism described becomes 100% accurate when PR B (`fix/setup-inject-service-env`, sibling PR) lands; current setup.mjs on main does not yet inject this env into the service unit. Doc-only — no `server.mjs` change, no version bump, no `cli.js` citation required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(readme): add restrained star + issue CTA below tagline A single italic line under the existing personal-note italic, asking readers to ⭐ the repo if they get value, and to file issues — framed explicitly as "issues are even more useful than stars" so the CTA reads as feedback-seeking rather than vanity-metric chasing. Tone matches the rest of README: low-key, single-maintainer self-deprecating, no marketing voice. No "save money / free / $0" verbiage. Coexists with the existing buy-me-a-coffee personal-note line above it (different ask: funding vs. social-proof). This is doc-only. ALIGNMENT.md Rule 5 (cli.js citation) does not apply. Same pattern as #68 / #69 / #71 / #78. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
5e2effd05b |
fix(ocp-connect): write ~/.zshrc on macOS (default shell since Catalina) (#77)
macOS default shell has been zsh since Catalina (2019). The previous
rc-file selection logic treated macOS the same as Linux, so on a fresh
Mac where ~/.zshrc did not already exist AND the script was invoked via
`bash -s --` (e.g. `curl | bash`), the $SHELL guard was `/bin/bash`
and neither condition for zshrc was true — resulting in only ~/.bashrc
being written. Since ~/.bashrc is not sourced by interactive zsh
sessions, the OPENAI_BASE_URL / OPENAI_API_KEY exports were invisible
to interactive shells.
Fix:
- Add an explicit `elif $is_mac` branch that unconditionally includes
~/.zshrc: create the file (empty) if it does not yet exist, since
zsh tolerates an empty ~/.zshrc.
- On macOS, ~/.bashrc is only written if it already exists — consistent
with the task spec ("don't create ~/.bashrc if it didn't exist").
- Linux path is preserved unchanged.
- Fix the "Reload your shell" hint at script end: previously it printed
only the last loop variable `$rc_file` (stale reference outside the
loop). Now it iterates `${rc_files[@]}` so both files are shown on
macOS (reproducing the Round A bug: hint said only `source ~/.bashrc`
even when zshrc should also be reloaded).
Smoke-tested with HOME=/tmp/fakehome redirect for three scenarios:
1. Fresh MacBook (no .bashrc, no .zshrc) → only .zshrc created+written
2. macOS with existing .bashrc → both .bashrc and .zshrc written
3. Linux with bash, no .bashrc → .bashrc written (unchanged)
Identified during Round A testing on MacBook 2026-05-08.
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
fb2d1d3feb |
fix(ocp-cli): replace eval-curl with bash array to preserve JSON body quoting (#74)
`eval curl "$_AUTH_HEADER" "$@"` re-tokenizes its argument list according
to bash word-splitting rules. When OCP_ADMIN_KEY is set, the JSON body
`'{"name": "laptop"}'` (which contains a space) gets split into
`'{name:'` and `'laptop}'` — two separate args — so curl receives a
malformed body and the server rejects the request.
Fix: replace the `_AUTH_HEADER` string + `eval` pattern with a bash array
`_AUTH_ARGS`. Array expansion via `"${_AUTH_ARGS[@]}"` preserves word
boundaries across substitution with no eval required. Both code paths
(OCP_ADMIN_KEY env var and ~/.ocp/admin-key file fallback) and the empty
case (no admin key) are preserved unchanged.
Verified via `bash -x` trace:
Before: `curl … -d '{name:' 'laptop}'` (body split, malformed)
After: `curl … -d '{"name": "testkey-pr-c"}'` (body intact, single arg)
Identified during fresh-state Round 2 testing on MacBook.
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
12b09c236e |
fix(server): resolve claude binary from nvm/fnm/asdf and PATH fallback (#75)
Real-world macOS dev machines using nvm-managed Node hit a startup FATAL because the hardcoded candidate list in resolveClaude() only covered homebrew, /usr/local, /usr/bin, and ~/.local/bin. With Claude CLI installed at $HOME/.nvm/versions/node/<v>/bin/claude, the launchd job failed without manual CLAUDE_BIN injection. Fix: extend the candidate list with user-local Node version manager paths — nvm (with default-alias), fnm, asdf, and npm-prefix-relocated $HOME/.npm-global/bin. The existing CLAUDE_BIN env override and `which` fallback are preserved; resolution order is now explicit CLAUDE_BIN > hardcoded list > nvm/fnm/asdf > which > FATAL (with the message upgraded to mention CLAUDE_BIN as a hint). This is OCP-internal binary discovery — there is no `cli.js` operation to cite. ALIGNMENT.md Rule 2 (the rule that limits OCP to operations that exist in `cli.js`) does not constrain runtime path discovery for the OCP server itself. Smoke tests: - default (no CLAUDE_BIN): picks /opt/homebrew/bin/claude (unchanged) - CLAUDE_BIN=/nonexistent/claude: fail-fast preserved - HOME=/tmp/fakenvm with synthetic .nvm tree: candidate list contains the fake nvm path; alias-default unshift logic verified - npm test: 43/43 unit tests pass - node --check server.mjs: OK - alignment.yml blacklist grep: no hits Identified during fresh-state Round 2 testing on MacBook. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
c0f2d3ab20 |
docs(install): remove false symlink claim, fix Anonymous mode startup command (#78)
Three doc inaccuracies surfaced during fresh-state install testing on Pi231 + MacBook (Round 1+2): 1. README §Server Setup claimed setup.mjs "Symlink `ocp` to /usr/local/bin for CLI access" — false. setup.mjs writes start.sh + plist/systemd unit but creates no PATH symlink. Removed the line; added a short PATH tip showing the user's options (manual symlink to ~/.local/bin or /usr/local/bin, or shell alias) right after the install summary. 2. README §Anonymous Access told users to run `ocp start` to enable the feature — there is no `ocp start` subcommand. Available commands are restart / stop / status / logs / keys / usage / update / lan / health / clear / settings (verified via `~/ocp/ocp` enumeration). Replaced with the correct flow: export PROXY_ANONYMOUS_KEY, then `node setup.mjs --bind 0.0.0.0 --auth-mode multi`. 3. The same paragraph implied that exporting PROXY_ANONYMOUS_KEY in an interactive shell is enough to enable anonymous access — but the running proxy is auto-started by launchd/systemd from the service unit's own env, not from the user's shell. Spelled this out and noted that if OCP was installed before exporting the env var, the user must re-run setup.mjs (idempotent) so the service unit env is refreshed, then `ocp restart`. The PROXY_ANONYMOUS_KEY mechanism described becomes 100% accurate when PR B (`fix/setup-inject-service-env`, sibling PR) lands; current setup.mjs on main does not yet inject this env into the service unit. Doc-only — no `server.mjs` change, no version bump, no `cli.js` citation required. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
0d61da5153 |
fix(setup): inject CLAUDE_BIN, OCP_ADMIN_KEY, PROXY_ANONYMOUS_KEY into service env (#76)
Gap #1 partial (CLAUDE_BIN): setup.mjs now detects `which claude` at install time (or reads $CLAUDE_BIN) and writes CLAUDE_BIN into the service unit EnvironmentVariables dict. Works for nvm/homebrew paths not in server.mjs's hardcoded list, and for older server.mjs deployments. Gap #2 (OCP_ADMIN_KEY): reads $OCP_ADMIN_KEY from the user's shell env and conditionally injects it into the plist/systemd unit. Empty/unset → key is omitted entirely (server.mjs treats empty string as "no admin"). Key value is never logged; only its length is reported. Gap #6 partial (PROXY_ANONYMOUS_KEY): reads $PROXY_ANONYMOUS_KEY and conditionally injects it. Unset → key is omitted (anonymous access disabled). All three keys are read via process.env at install time; no new CLI flags. Injection status is logged before the !DRY_RUN guard so dry-run shows what would be written. Identified during fresh-state Round 2 testing. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
49baffe2da |
fix(setup): decouple OpenClaw config patch from being mandatory (#73)
OCP markets itself as a standalone OpenAI-compatible proxy with six
supported IDE clients (Cline / Cursor / Continue.dev / OpenCode / Aider /
OpenClaw). The README §Server Setup says "node setup.mjs" is the
installer entrypoint. But on a truly fresh box without OpenClaw
installed, setup.mjs hard-fails at line 111:
if (!existsSync(CONFIG_PATH)) fail(`OpenClaw config not found...`);
This contradicts the standalone-proxy stance and was caught during
PR #71 dogfood testing on Pi231.
Root cause: the OpenClaw config patch (lines 110-148) was baked in as
required because OCP started life as an OpenClaw-internal helper. The
project has since rebranded to standalone OCP without decoupling the
installer.
Fix: gate the OpenClaw config patch (Step 2) and auth-profiles patch
(Step 3) on existsSync(CONFIG_PATH). When OpenClaw is present, behavior
is byte-for-byte identical to current main (verified via dry-run hash
diff against ~/.openclaw/openclaw.json — UNCHANGED). When OpenClaw is
absent, the installer logs a graceful warning, skips both patch
sections, and continues to start.sh / launchd-plist / systemd-unit
creation as before. The summary banner is also conditional: when
OpenClaw is absent, it points the user at README § "Client Setup"
instead of giving openclaw.json edit instructions.
Also moves `import { readdirSync }` from mid-file (line 178, post-use)
to the top-level imports block; this was a latent ESM-hoisting quirk
that worked but is now syntactically required at the top because the
readdirSync call moved inside an `if` block.
Out of scope (Iron Rule 11, single-layer PR): server.mjs, models.json,
scripts/sync-openclaw.mjs, README.md, ALIGNMENT.md, package.json
version, the duplicate-spawn / health-verify logic at line 268+ (that's
a separate fix/setup-spawn-conflict PR).
Mac mini production unaffected: ~/.openclaw/openclaw.json already
exists there, so the OpenClaw-present path is preserved. `ocp update`
doesn't invoke setup.mjs, so running services are not touched.
Smoke-tested locally:
- Path 1 (OpenClaw present): dry-run output functionally identical to
current main; config sha256 unchanged after run.
- Path 2 (OpenClaw absent, OPENCLAW_STATE_DIR=/tmp/no-such-dir-*):
graceful warn, both patch sections skipped, banner shows
standalone-mode message, dry-run completes successfully.
- npm test: 43/43 pass.
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
4b01d4e768 |
fix(setup): remove duplicate server spawn; verify health post-install (#72)
## Dogfood evidence (Pi231, 2026-05-08)
A user ran `node setup.mjs --bind 0.0.0.0 --auth-mode multi` and got:
- setup.mjs exit 0
- /health responded with authMode:"none" and server bound to 127.0.0.1 only
- ps showed two server.mjs processes: one orphan from setup (wrong config),
one systemd child restart-looping on EADDRINUSE
## Root cause (two-step conflict)
Step 6 (the deleted block) called `execSync('bash "${startPath}"')` which ran
start.sh's `nohup node server.mjs &` — spawning the server WITHOUT exporting
CLAUDE_BIND or CLAUDE_AUTH_MODE. That server ran with default bind=127.0.0.1
and authMode=none, ignoring the user's CLI flags.
Step 7 then wrote the systemd unit/launchd plist WITH the correct env vars and
bootstrapped the service — but port 3456 was already taken by Step 6's spawn,
causing EADDRINUSE and a silent restart loop. setup.mjs exited 0 because Step 6
had "succeeded" (a server was running, just the wrong one).
## What changed
1. Deleted Step 6 entirely (the `execSync('bash "${startPath}"')` block).
The systemd/launchd service installed in Step 7 is now the sole authoritative
start path. start.sh is unchanged and still available for manual non-systemd use.
2. Added Step 8 inside the `if (!DRY_RUN)` block: after Step 7 bootstrap,
waits 3 s, then GETs http://127.0.0.1:${PORT}/health with a 5 s timeout.
- On 200 OK: logs version, authMode, and bind socket (best-effort).
- On failure: prints clear error pointing to service logs, exits 1.
- Skipped when --no-start is set (existing flag).
Identified during PR #71 dogfood testing on Pi231 (RPi4 / Debian Bookworm).
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
36fa81d1e6 |
docs(install): add §Quick install with AI assistance + plug five new-user pitfalls (#71)
* docs(install): add §Quick install with AI assistance + plug five new-user pitfalls
Context: a returning user observed that letting an AI assistant follow the
README to install OCP would mostly work but stumble on a handful of small
gaps — missing OS qualifier, no admin-key generation hint, server IP
discovery buried, and a few common setup errors not in Troubleshooting.
This patch closes those gaps and adds a copy-paste prompt section for
new users who'd rather have an AI walk them through the install.
Five additive changes (README only, server.mjs untouched):
1. **§Server Setup prerequisites** — add the macOS/Linux qualifier
("Windows is not supported — setup.mjs installs launchd / systemd")
and `git`, both of which were implicit before.
2. **OCP_ADMIN_KEY generation hint** — replace the placeholder
`your-secret-admin-key` with a one-line `openssl rand -base64 32`
example, plus a reminder to add the export to ~/.zshrc / ~/.bashrc
so it survives shells.
3. **§Client Setup** — add an inline note pointing readers to run
`ocp lan` on the server to discover the server's LAN IP. Previously
`ocp lan` was mentioned only in §Server Setup, leaving client-side
readers to guess.
4. **§Troubleshooting** — three new entries for setup-time errors
(claude not found / EADDRINUSE 3456 / node version), with the
specific recovery commands. Existing entries left unchanged.
5. **§Installation → new ###Quick install with AI assistance subsection**
— three copy-paste prompts (single-machine, LAN server, client) that
pin the AI to the right README path, name the verification step, and
forbid silent retries. Includes a pointer to the manual handbook
sections for readers who prefer that path.
Doc-only change. server.mjs not modified. ALIGNMENT.md Rule 5 (cli.js
citation requirement) does not apply. Same pattern as #68, #69, #70.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(install): add Claude CLI install command + ####Headless install notes
Live Pi231 (RPi4 / Debian Bookworm) test of PR #71's "Quick install with AI
assistance → LAN server" prompt surfaced two more new-user pitfalls in the
README's Prerequisites + Server Setup flow:
1. **Claude CLI install command was missing.** Previous text said "Claude CLI
installed and authenticated" with a docs link — but the actual install
command (`npm install -g @anthropic-ai/claude-code`) appeared nowhere.
An AI assistant following the prompt has to fetch external docs to
guess at the install path. Now inlined.
2. **Headless servers (Pi / NAS / VPS) had no auth guidance.** OCP's main
deployment targets are always-on headless devices. `claude auth login`
actually works headless (prints URL + code, OAuth completes on any
browser-capable device), and `claude setup-token` provides a long-lived
token — but neither was documented. New ####Headless install notes
subsection explains both paths.
Test trail (Pi231):
- ✅ Linux (aarch64 Debian Bookworm), Node v22.22.2, git 2.39.5 — prereqs
satisfied except Claude CLI.
- ❌ `claude` not on PATH (expected — fresh Pi). README didn't tell the
user how to install it. → fix #1 above.
- ⚠️ Even after AI fetches the install command, headless OAuth was a
documentation gap. → fix #2 above.
Doc-only change. server.mjs not modified. ALIGNMENT.md Rule 5 does not
apply. Extends PR #71 (same install-UX layer per Iron Rule 11 IDR).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
cce0110253 |
docs(why-ocp): reorder bullets + soften alignment framing + visceral hooks (#70)
The "Why OCP?" section previously led with technical/governance bullets
(SSE heartbeat, alignment, models.json) and buried the most relatable
selling points (LAN multi-user keys, ocp-connect IDE auto-config, cache).
First-time readers stopped reading before reaching the bullets that would
have sold them on the project.
Changes (README.md only, line 19-26):
1. Reorder: human-relatable benefits first, governance discipline last.
New order: LAN multi-user → ocp-connect → cache → quota → SSE heartbeat
→ alignment → models.json. Quota is split out from the old combined
"quota + cache" bullet so each gets its own one-liner.
2. Soften the alignment bullet's framing. The previous prose flagged
"Other Claude proxies have shipped exactly that" and was deleted —
no need to call out competitors. Replaced with a measured "LLM-assisted
code drifts easily — it's tempting to invent plausible-looking endpoints
that cli.js doesn't actually use" plus a deliberately understated
payoff: "your setup keeps working when cli.js ships its next minor."
3. Add visceral hooks where bullets benefit from them:
- SSE heartbeat: "If you've ever watched your IDE die at the 60s idle
mark during a long Claude tool-use pause — that's nginx/Cloudflare
default behavior" (frames the problem in user-felt terms before
describing the fix).
- Per-key quota: concrete example "set a kid's iPad to 20/day, a
partner's laptop to 100/week" (replaces abstract "limits per key").
4. Cache bullet now explicitly states the per-key isolation guarantee:
"cross-user pollution is impossible by hash construction, not by
application logic" — this addresses the most common pre-adoption
concern (and reflects the v3.13.0 D1 design).
5. Total length compressed ~20% despite added context — old bullets had
redundant "what" descriptions; new bullets lead with the "what for".
Doc-only change. server.mjs not touched. ALIGNMENT.md Rule 5 (cli.js
citation requirement) does not apply. Same pattern as #68, #69.
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
40391791a1 |
docs(funding): raise sponsorship visibility — top badges + intro CTA + expanded support section (#69)
Context: Buy Me a Coffee + Stripe onboarding now fully live (verified buymeacoffee.com/dtzp555 returns og:type=profile with Support CTA, default $3 price tier, membership tier active). Previous §Support OCP at line 709 was effectively buried — last section before License, missed by most readers. Changes (README.md only, server.mjs untouched): 1. Top of file — three shields.io badges (License MIT, latest release, Buy Me a Coffee) under H1, before tagline. Standard OSS pattern (Vue / Vite / Tailwind), high visibility, doesn't disrupt prose. 2. Just under tagline — one-line italic personal note pointing to §Support OCP, with inline ☕ link as fallback for users who don't scroll. Quotes the spirit of the longer section without duplicating it. 3. §Support OCP expanded — adds the "open source from day one" framing (not freemium, not commercial-turned-open), the "my family uses it daily" angle, and an explicit feedback / issues invitation. The debugging-history paragraph is preserved verbatim. Doc-only change. ALIGNMENT.md Rule 5 (cli.js citation) does not apply — server.mjs is not modified. Same pattern as #68. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
342a0a44f5 |
docs(funding): add Buy Me a Coffee link + GitHub FUNDING.yml (#68)
- .github/FUNDING.yml — enables GitHub's native "Sponsor" button on the repo page, pointing to buymeacoffee.com/dtzp555. Other platforms (GitHub Sponsors, Ko-fi) are commented out and can be enabled later by uncommenting + filling in handles. - README.md § Support OCP — new section just before License. States the free-and-open-source commitment, lists the kinds of work that don't show up in commits (multi-machine debugging, IDE validation, drift incidents, concurrency leaks), and offers a single ☕ link for users who want to support continued maintenance. Explicitly disclaims paid tiers / premium features so the open-source posture stays unambiguous. 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: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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
|
||
|
|
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> |
||
|
|
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
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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 (
|
||
|
|
d4f4aaf33a |
docs(alignment): backfill fix commit SHAs for 2026-04-11 drift (#25)
PR #21 ( |
||
|
|
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
|
||
|
|
6bfffd2cba |
chore(server): revert stale-cache compensation for hallucinated endpoint (#23)
Removes the stale-cache fallback branches in handleUsage() and handleStatus() originally introduced by |
||
|
|
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 |
||
|
|
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:
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
47434a8ba7 |
fix: security hardening + real streaming (#4)
* Add graceful shutdown handling for SIGTERM and SIGINT On receiving SIGTERM or SIGINT, the server now: 1. Stops accepting new connections (server.close) 2. Clears session cleanup and auth check intervals 3. Sends SIGTERM to all active child processes 4. Waits up to 5s for processes to exit, then SIGKILL + exit(1) 5. Exits cleanly with exit(0) once all processes are gone Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: security hardening — bind localhost, validate models, limit body size - Bind to 127.0.0.1 instead of 0.0.0.0 to prevent network exposure - Restrict CORS Access-Control-Allow-Origin to http://127.0.0.1 - Add 10MB request body size limit (413 on exceed) - Validate model parameter against known MODEL_MAP keys (400 on unknown) - Remove catch-all POST route; only /v1/chat/completions accepted - Use crypto.timingSafeEqual for API key comparison - Sanitize error messages to strip internal file paths Bumps version to 2.4.1. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: replace fake streaming with real stdout-piped SSE streaming Previously, streaming requests waited for the entire claude CLI process to complete, then split the output into artificial 500-char chunks sent as SSE events. This defeated the purpose of streaming and added latency. Now, stdout from the claude child process is piped directly to SSE chunks as data arrives. Each `data` event from the process stdout becomes an immediate SSE delta event, giving clients real incremental output. The shared process setup logic is extracted into spawnClaudeProcess() to avoid duplication between streaming and non-streaming paths. Client disconnect detection kills the child process to free resources. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: tighten CORS to dynamic localhost-only and reduce body limit to 5MB - CORS now dynamically matches localhost/127.0.0.1 origins instead of static http://127.0.0.1, preventing cross-origin abuse while supporting any localhost port - Body size limit reduced from 10MB to 5MB to limit OOM risk 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> |