Compare commits

...
Author SHA1 Message Date
taodengandClaude Sonnet 4.6 eb6cd09bed fix(setup): remove duplicate server spawn; verify health post-install
## 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: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 14:52:27 +10:00
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>
2026-05-08 13:57:06 +10:00
2 changed files with 56 additions and 14 deletions
+8 -7
View File
@@ -20,14 +20,15 @@ One proxy. Multiple IDEs. All models. **$0 API cost.**
## Why OCP? ## Why OCP?
There are several Claude proxy projects. OCP picks a specific lane: **align tightly with what `cli.js` actually does, observe + multiplex what's already there, don't extend the protocol.** Concretely: There are several Claude proxy projects. OCP picks a specific lane: **align tightly with what `cli.js` actually does, observe + multiplex what's already there, don't extend the protocol.** What you get:
- **SSE heartbeat on streaming** ([v3.12.0](https://github.com/dtzp555-max/ocp/releases/tag/v3.12.0), opt-in via `CLAUDE_HEARTBEAT_INTERVAL`). Long Claude reasoning or tool-use pauses can sit silent for minutes; downstream load balancers and reverse proxies often kill the connection at 60s idle. OCP emits an SSE comment frame during silent windows so the connection stays alive without polluting the response. ([PR #49](https://github.com/dtzp555-max/ocp/pull/49)) - **LAN multi-user keys** (v3.7.0) — share one Claude Pro/Max subscription with family, friends, or your own devices. Each user gets a per-key API token (no OAuth session leak), with independent usage tracking and one-line revocation.
- **Alignment constitution + CI guardrail.** [`ALIGNMENT.md`](./ALIGNMENT.md) is the binding spec: every endpoint OCP exposes must correspond to something `cli.js` actually does, with a line-number citation. The [`alignment.yml`](./.github/workflows/alignment.yml) workflow auto-blocks PRs that introduce known-hallucinated tokens (`api/oauth/usage`, `api/usage`, etc). Hard to drift, even with LLM-assisted contributions. - **`ocp-connect` one-shot IDE setup** — one command on the client machine detects and configures Claude Code, Cursor, Cline, Continue.dev, OpenCode, and OpenClaw. No pasting `OPENAI_BASE_URL` six times.
- **`models.json` single source of truth** (v3.11.0). Adding a model is one file edit; both `/v1/models` and the OpenClaw bootstrap derive from it. No more drift between server and installer. ([PR #30](https://github.com/dtzp555-max/ocp/pull/30)) - **Response cache with per-key isolation + singleflight** (v3.13.0). Optional SHA-256 prompt cache, isolated per API key (cross-user pollution is impossible by hash construction, not by application logic), with stampede protection on concurrent identical prompts. Off by default. ([PR #65](https://github.com/dtzp555-max/ocp/pull/65), [PR #66](https://github.com/dtzp555-max/ocp/pull/66))
- **Multi anonymous-key distribution** (v3.7.0). Share one OCP instance with friends/family/devices without exposing your OAuth session — each gets a per-user key, with usage tracking and revocation. - **Per-key request quotas** (v3.8.0). Daily / weekly / monthly limits per key — set a kid's iPad to 20/day, a partner's laptop to 100/week. ([PR #18](https://github.com/dtzp555-max/ocp/pull/18))
- **Per-key quota + response cache** (v3.8.0). Daily/weekly/monthly request limits per key. Optional SHA-256 prompt cache for development loops. ([PR #18](https://github.com/dtzp555-max/ocp/pull/18)) - **SSE heartbeat for long reasoning** ([v3.12.0](https://github.com/dtzp555-max/ocp/releases/tag/v3.12.0), opt-in). 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. OCP emits an SSE comment frame to keep the connection alive without polluting the response. ([PR #49](https://github.com/dtzp555-max/ocp/pull/49))
- **`ocp-connect` IDE auto-config.** Detects Claude Code, Cursor, Cline, Continue.dev, opencode, and OpenClaw on the client machine and configures them in one command. - **`cli.js` alignment + CI guardrail.** LLM-assisted code drifts easily — it's tempting to invent plausible-looking endpoints that `cli.js` doesn't actually use. [`ALIGNMENT.md`](./ALIGNMENT.md) is binding: every endpoint OCP exposes must cite a `cli.js` line. The [`alignment.yml`](./.github/workflows/alignment.yml) CI workflow blocks PRs that introduce known-hallucinated tokens. The payoff is boring: your setup keeps working when `cli.js` ships its next minor.
- **`models.json` single source of truth** (v3.11.0). Adding a model is one file edit; both `/v1/models` and the OpenClaw bootstrap derive from it. ([PR #30](https://github.com/dtzp555-max/ocp/pull/30))
### Comparison ### Comparison
+48 -7
View File
@@ -264,13 +264,6 @@ console.log(`
╚══════════════════════════════════════════════════════════════╝ ╚══════════════════════════════════════════════════════════════╝
`); `);
// ── Step 6: Optionally start ────────────────────────────────────────────
if (!SKIP_START && !DRY_RUN) {
try {
execSync(`bash "${startPath}"`, { stdio: "inherit" });
} catch { /* ignore */ }
}
// ── Step 7: Install auto-start on boot ────────────────────────────────── // ── Step 7: Install auto-start on boot ──────────────────────────────────
if (!DRY_RUN) { if (!DRY_RUN) {
console.log("\n🔄 Installing auto-start on login...\n"); console.log("\n🔄 Installing auto-start on login...\n");
@@ -405,4 +398,52 @@ WantedBy=default.target
} }
console.log("\n✅ Auto-start installed — proxy will start automatically on login\n"); console.log("\n✅ Auto-start installed — proxy will start automatically on login\n");
// ── Step 8: Post-install health verification ───────────────────────────
if (!SKIP_START) {
console.log("⏳ Waiting for server to bind...\n");
await new Promise(r => setTimeout(r, 3000));
const healthUrl = `http://127.0.0.1:${PORT}/health`;
let verified = false;
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
const res = await fetch(healthUrl, { signal: controller.signal });
clearTimeout(timer);
if (res.ok) {
const body = await res.json().catch(() => ({}));
console.log(` ✓ Health check passed (${healthUrl})`);
console.log(` version: ${body.version ?? "unknown"}`);
console.log(` authMode: ${body.authMode ?? "unknown"}`);
// Verify bind socket
try {
const bindCheck = process.platform === "linux"
? execSync(`ss -tlnp 2>/dev/null | grep ':${PORT}'`, { encoding: "utf-8" }).trim()
: execSync(`lsof -nP -iTCP:${PORT} -sTCP:LISTEN 2>/dev/null`, { encoding: "utf-8" }).trim();
if (bindCheck) {
console.log(` bind: ${bindCheck.split("\n")[0]}`);
}
} catch { /* bind check is best-effort */ }
verified = true;
} else {
warn(`Health check returned HTTP ${res.status} — service may not have started cleanly`);
}
} catch (e) {
const isTimeout = e.name === "AbortError" || (e.cause && e.cause.code === "UND_ERR_CONNECT_TIMEOUT");
warn(`Health check failed: ${isTimeout ? "timeout (5s)" : e.message}`);
}
if (!verified) {
const logHint = process.platform === "linux"
? "journalctl --user -u ocp-proxy -n 50"
: `tail -n 100 ~/.ocp/logs/proxy.log`;
console.error(`\n ✗ Server did not respond on port ${PORT} within 5 seconds.`);
console.error(` Check service logs:\n ${logHint}\n`);
process.exit(1);
}
}
} }