Compare commits

..
Author SHA1 Message Date
taodengandClaude Sonnet 4.6 e8b4fedccf feat(snapshot): gcSnapshots + ocp update --rollback --gc + auto-GC; v3.16.0
Adds snapshot garbage collection with retention policy: keep last 5,
keep snapshots within 30 days, always keep the most recent. Configurable
via keepCount / keepDays opts.

Wire-up:
- ocp update --rollback --gc (manual trigger; --dry-run supported)
- runFullUpgrade auto-GC after successful upgrade (best-effort,
  swallows errors)

4 unit tests: keepCount enforcement, keepDays override, never-delete-
most-recent safety, dry-run mode.

Bumps to v3.16.0 (bundles PR #93 --check oauth + this GC feature).

No cli.js citation needed: this is OCP-internal snapshot lifecycle with
no corresponding cli.js operation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 07:29:53 +10:00
12 changed files with 16 additions and 246 deletions
-79
View File
@@ -4,11 +4,6 @@ on:
pull_request:
paths:
- 'server.mjs'
- 'setup.mjs'
- 'scripts/**'
- 'lib/**'
- 'ocp'
- 'ocp-connect'
- '.github/workflows/alignment.yml'
jobs:
@@ -71,80 +66,6 @@ jobs:
echo "Blacklist scan clean."
port-spot:
name: port literal SPOT (hard fail)
# Background: from 2026-05-08 (PR #71 dogfood accident) through 2026-05-13
# a hardcoded "3478" in scripts/upgrade.mjs + scripts/doctor.mjs cascaded
# into wrong baseUrl writes for the OpenClaw "claude-local" provider,
# taking out the "大内总管" Telegram agent.
#
# Rule: the only places allowed to write a literal port number in source
# are (a) lib/constants.mjs (the SPOT), (b) bash scripts ocp / ocp-connect
# (which can't import .mjs and must keep the literal in sync — flagged
# with a `// keep in sync with lib/constants.mjs` style comment), and
# (c) test-features.mjs (intentionally pins historical ports for plist /
# systemd parser tests). Everything else MUST import from lib/constants.mjs.
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Scan for hardcoded port literals outside SPOT
shell: bash
run: |
set -euo pipefail
# Files/paths exempt from the SPOT requirement.
EXEMPT_REGEX='^(lib/constants\.mjs|test-features\.mjs|ocp|ocp-connect|CHANGELOG\.md|README\.md|docs/|\.github/workflows/alignment\.yml)'
# Hardcoded port literals to forbid in non-exempt source.
FORBIDDEN_PORTS=("3478" "3456")
FAIL=0
for port in "${FORBIDDEN_PORTS[@]}"; do
HITS="$(git ls-files | grep -E '\.(mjs|js|ts|json)$' \
| xargs grep -n -E "[^0-9]${port}[^0-9]" 2>/dev/null \
| grep -v -E "${EXEMPT_REGEX}" \
|| true)"
if [ -n "$HITS" ]; then
echo "::error::Hardcoded port literal '${port}' found outside lib/constants.mjs:"
echo "$HITS"
FAIL=1
fi
done
if [ "$FAIL" -ne 0 ]; then
cat <<'EOF'
============================================================
PORT LITERAL SPOT VIOLATION
============================================================
A hardcoded TCP port literal was found in a source file
that should import from lib/constants.mjs instead.
Background: this rule exists because between 2026-05-08 and
2026-05-13 a stray hardcoded "3478" in scripts/upgrade.mjs
and scripts/doctor.mjs cascaded into downstream OpenClaw
config writes, taking out the OpenClaw Telegram agent.
See v3.16.3 CHANGELOG and lib/constants.mjs header comment.
Required action:
1. Import DEFAULT_PORT (or related constant) from
lib/constants.mjs instead of hardcoding the literal.
2. If the file genuinely cannot import .mjs (e.g. bash
script), add it to EXEMPT_REGEX in this workflow and
add a `keep in sync with lib/constants.mjs` comment
at the reference.
3. For test files that intentionally pin historical ports
(test-features.mjs), the regex already exempts them.
============================================================
EOF
exit 1
fi
echo "Port SPOT scan clean."
commit-citation:
name: commit message citation (soft check)
runs-on: ubuntu-latest
+1 -102
View File
@@ -1,107 +1,6 @@
# Changelog
## v3.16.4 — 2026-05-13
### Refactor — port-literal SPOT + CI guardrail
Closes the structural side of the port-drift cascade addressed by v3.16.2
and v3.16.3. Those two releases reverted plist / plugin / scripts back to
3456 line-by-line, but the underlying invitation to drift — a hardcoded
port literal scattered across six source files — was still intact.
Changes:
- **New `lib/constants.mjs`** — single source of truth for shared literals.
Exports `DEFAULT_PORT = 3456`, `LOCAL_HOST = "127.0.0.1"`,
`OPENAI_API_BASE = "/v1"`, `LOCAL_PROXY_URL`.
- **`server.mjs:127`, `setup.mjs:36`, `scripts/upgrade.mjs:137`,
`scripts/doctor.mjs:84` + `:205`, `scripts/sync-openclaw.mjs:73`** —
all replaced with imports from `lib/constants.mjs`. Behavior is
identical; the literal `3456` now exists in exactly one place per
language (`lib/constants.mjs` for `.mjs`, `ocp` + `ocp-connect` for
bash, `test-features.mjs` for pinned historical-port tests).
- **`.github/workflows/alignment.yml`** — extended the path filter to
`setup.mjs`, `scripts/**`, `lib/**`, `ocp`, `ocp-connect`. Added a new
`port-spot` hard-fail job that greps for any hardcoded `3478` or `3456`
literal in `.mjs/.js/.ts/.json` outside the EXEMPT_REGEX (which lists
`lib/constants.mjs`, `test-features.mjs`, the bash CLIs, docs, and the
workflow itself). Any future PR re-introducing a hardcoded port
literal will be blocked at CI before it can cascade.
- Doc comments in `server.mjs` env-var summary and `setup.mjs` usage
banner reworded so the literal `3456` no longer appears as
documentation text (CI grep is intentionally aggressive — it does not
parse comments — so doc strings reference `DEFAULT_PORT from
lib/constants.mjs` instead).
No behavior change for any user. `CLAUDE_PROXY_PORT` env var remains
the runtime override; the only difference is the unset-env fallback
now flows through one shared constant.
ALIGNMENT.md hard-requirements: this PR modifies `server.mjs` (one-line
import + one literal swap, mechanical). No cli.js operation changed;
the citation requirement does not apply. SPOT principle (Rule 2 spirit)
is the entire motivation.
## v3.16.3 — 2026-05-13
### Fixes — completes v3.16.2 port-drift revert
v3.16.2 reverted the plugin / `openclaw.plugin.json` / README / Mac mini
plist back to `3456` (the historical source default since `593d0dc`), but
missed three places in `scripts/` that still defaulted to `3478`. Those
three lines were the residual cascade source: every time `ocp doctor` or
`ocp upgrade` ran without `CLAUDE_PROXY_PORT` in the env, they probed
`3478`, reported "OCP not responding" against a healthy 3456 instance,
and (in the case of OpenClaw sync follow-ups on the maintainer's host)
re-introduced 3478 into downstream config.
Changes:
- `scripts/upgrade.mjs:137` — default port `3478``3456`.
- `scripts/doctor.mjs:84` — default port `3478``3456`.
- `scripts/doctor.mjs:205` — default port `3478``3456`.
No behavior change for users who set `CLAUDE_PROXY_PORT` explicitly; env
still takes precedence. The fix only affects the unset-env fallback,
which now matches `server.mjs:126` and the rest of the codebase.
Test plan: existing `test-features.mjs` cases that pin
`CLAUDE_PROXY_PORT=3478` continue to pass — they use the env path, not
the default.
## v3.16.2 — 2026-05-12
### Fixes — corrects v3.16.1
The v3.16.1 fix was directionally correct (plugin now reads env first, falls back to a hardcoded default) but **the narrative and the hardcoded default were both wrong**.
What v3.16.1 said: "OCP server moved to 3478 default in v3.14+; plugin lagged at 3456."
What is actually true:
- **OCP server source default has been `3456` since `593d0dc` (initial release) and has never changed.** Every line in `server.mjs`, `setup.mjs`, and the `ocp` CLI still uses `3456` as the documented and code-level default.
- The single OCP installation observed on `3478` is the maintainer's Mac mini, whose plist was rewritten with `--port 3478` during a PR #71 dogfood smoke-test accident on 2026-05-08 (see `~/.cc-rules/memory/learnings/subagent_setup_mjs_prod_host_collision.md`). The plist drift was never reconciled back to source default, and v3.16.1 incorrectly canonised the post-accident value as if it had been a release decision.
This release:
- Restores the plugin fallback to `http://127.0.0.1:3456` to match server source default.
- Updates `openclaw.plugin.json` `configSchema.proxyUrl.default` back to `3456`.
- Restores README §"Environment Variables" `CLAUDE_PROXY_PORT` default to `3456`.
- Plugin reads `OCP_PROXY_URL` env (full URL) first, then `CLAUDE_PROXY_PORT` env (port only), then falls back to `3456`. Hosts whose OCP plist injects a non-default port must also inject the same `CLAUDE_PROXY_PORT` into the OpenClaw plist for the plugin to follow.
- Maintainer's Mac mini plist was reverted from `3478` to `3456` as part of this release deploy (no source change reflects this; it was a one-host correction).
### Governance
- No `cli.js` citation needed (no `server.mjs` change). ALIGNMENT.md Rule 2 not engaged.
## v3.16.1 — 2026-05-12 (superseded — narrative incorrect; see v3.16.2 erratum)
### Fixes (as shipped — note erratum above)
- **OCP plugin port lag** — `ocp-plugin/index.js` hard-coded `http://127.0.0.1:3456`. ~~While OCP server moved to 3478 in v3.14+,~~ **(corrected v3.16.2: no such move ever happened.)** The Mac mini's plist was on `3478` only as residue from a dogfood accident. Result: `/ocp` slash commands from the home Telegram bot returned "OCP error: fetch failed". v3.16.1 changed the plugin default to `3478` (wrong direction; v3.16.2 reverts to `3456`).
### Governance
- No `cli.js` citation needed (no `server.mjs` change). ALIGNMENT.md Rule 2 not engaged.
## v3.16.0 — 2026-05-10
## v3.16.0 — 2026-XX-XX (release date filled at tag time)
### Features
+1 -2
View File
@@ -855,8 +855,7 @@ Future `ocp update` invocations sync automatically.
| Variable | Default | Description |
|----------|---------|-------------|
| `CLAUDE_PROXY_PORT` | `3456` | Listen port (server-side). Also consumed by the OpenClaw `ocp-plugin` to dial the local proxy. |
| `OCP_PROXY_URL` | *(unset)* | Plugin-side full URL override (e.g. `http://10.0.0.5:3456`). Wins over `CLAUDE_PROXY_PORT` when both are set. Read by `ocp-plugin/index.js` only — server ignores it. |
| `CLAUDE_PROXY_PORT` | `3456` | Listen port |
| `CLAUDE_BIND` | `127.0.0.1` | Bind address (`0.0.0.0` for LAN access) |
| `CLAUDE_AUTH_MODE` | `none` | Auth mode: `none`, `shared`, or `multi` |
| `OCP_ADMIN_KEY` | *(unset)* | Admin key for key management (multi mode) |
-33
View File
@@ -1,33 +0,0 @@
/**
* OCP shared constants — single source of truth.
*
* Any literal that appears in more than one place across server.mjs, setup.mjs,
* scripts/* belongs here so port-drift / URL-drift cascades cannot recur.
*
* Background: from 2026-05-08 (PR #71 dogfood accident) through 2026-05-13
* (v3.16.3) a single hardcoded "3478" in scripts/upgrade.mjs + scripts/doctor.mjs
* cascaded into every downstream config write, ultimately taking out the
* OpenClaw "大内总管" Telegram agent. See CHANGELOG v3.16.2 and v3.16.3.
*
* Adding a new constant: prefer ALL_CAPS_SNAKE_CASE. Document the consumers.
* If a literal is referenced from a shell script (ocp, ocp-connect, setup.sh)
* that can't import .mjs, add a `// keep in sync with lib/constants.mjs` note
* at the shell-script reference; CI grep prevents drift.
*/
// Default TCP port the OCP HTTP proxy listens on. Set by env CLAUDE_PROXY_PORT
// at runtime; this is the fallback when env is unset.
// Consumers: server.mjs, setup.mjs, scripts/upgrade.mjs, scripts/doctor.mjs,
// scripts/sync-openclaw.mjs. Shell scripts ocp / ocp-connect keep the literal
// "3456" in sync with this value (see CI gate in .github/workflows/alignment.yml).
export const DEFAULT_PORT = 3456;
// Localhost bind for client-side fetches (curl, health checks).
export const LOCAL_HOST = "127.0.0.1";
// OpenAI-compatible API base path appended to the proxy URL.
export const OPENAI_API_BASE = "/v1";
// Convenience: full local URL the OCP proxy listens on by default.
// scripts that want to probe locally can use this directly.
export const LOCAL_PROXY_URL = `http://${LOCAL_HOST}:${DEFAULT_PORT}`;
+3 -13
View File
@@ -1,19 +1,9 @@
/**
* OCP Plugin — registers /ocp as a native slash command in OpenClaw gateway.
* Calls the local claude-proxy and formats the response.
*
* Port resolution (in priority order):
* 1. OCP_PROXY_URL env (full URL, e.g. http://10.0.0.5:3456)
* 2. CLAUDE_PROXY_PORT env (port only; localhost assumed)
* 3. Fallback: http://127.0.0.1:3456 (OCP server source default since v1.0)
*
* If a particular host's OCP plist injects a non-default CLAUDE_PROXY_PORT,
* the OpenClaw launchd plist for that host must also inject the same
* CLAUDE_PROXY_PORT into the plugin's env, or the plugin will fall back to
* 3456 and miss the server.
* Calls the local claude-proxy at http://127.0.0.1:3456 and formats the response.
*/
const PROXY = process.env.OCP_PROXY_URL
|| (process.env.CLAUDE_PROXY_PORT ? `http://127.0.0.1:${process.env.CLAUDE_PROXY_PORT}` : "http://127.0.0.1:3456");
const PROXY = "http://127.0.0.1:3456";
// Wrap output in monospace code block for Telegram/Discord alignment
function mono(text) { return "```\n" + text + "\n```"; }
+2 -2
View File
@@ -2,7 +2,7 @@
"id": "ocp",
"name": "OCP Commands",
"description": "Slash commands for the OpenClaw Proxy — /ocp usage, /ocp settings, /ocp health, etc.",
"version": "3.16.2",
"version": "3.12.0",
"configSchema": {
"type": "object",
"additionalProperties": false,
@@ -10,7 +10,7 @@
"proxyUrl": {
"type": "string",
"default": "http://127.0.0.1:3456",
"description": "URL of the Claude proxy. Overridable via OCP_PROXY_URL or CLAUDE_PROXY_PORT env."
"description": "URL of the Claude proxy"
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "open-claude-proxy",
"version": "3.16.4",
"version": "3.16.0",
"description": "OCP (Open Claude Proxy) — use your Claude Pro/Max subscription as an OpenAI-compatible API for any IDE. Works with Cline, OpenCode, Aider, Continue.dev, OpenClaw, and more.",
"type": "module",
"bin": {
+2 -3
View File
@@ -15,7 +15,6 @@ import { readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { execSync } from "node:child_process";
import { DEFAULT_PORT } from "../lib/constants.mjs";
const SCHEMA_VERSION = "1";
@@ -82,7 +81,7 @@ export async function runDoctor(opts = {}) {
health = opts.mockHealth;
} else {
try {
const port = process.env.CLAUDE_PROXY_PORT || String(DEFAULT_PORT);
const port = process.env.CLAUDE_PROXY_PORT || "3478";
const out = execSync(`curl -sf --max-time 3 http://127.0.0.1:${port}/health`, { stdio: ["pipe", "pipe", "pipe"] }).toString();
health = { status: 200, body: JSON.parse(out) };
} catch (e) {
@@ -203,7 +202,7 @@ function runOauthOnly(opts, checks, push) {
health = opts.mockHealth;
} else {
try {
const port = process.env.CLAUDE_PROXY_PORT || String(DEFAULT_PORT);
const port = process.env.CLAUDE_PROXY_PORT || "3478";
const out = execSync(`curl -sf --max-time 3 http://127.0.0.1:${port}/health`, { stdio: ["pipe", "pipe", "pipe"] }).toString();
health = { status: 200, body: JSON.parse(out) };
} catch (e) {
+1 -2
View File
@@ -9,7 +9,6 @@ import { readFileSync, writeFileSync, existsSync, copyFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { homedir } from "node:os";
import { DEFAULT_PORT, LOCAL_HOST, OPENAI_API_BASE } from "../lib/constants.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(__dirname, "..");
@@ -71,7 +70,7 @@ if (!config.models.providers) config.models.providers = {};
if (!config.models.providers[PROVIDER_NAME]) {
// First-time registration
config.models.providers[PROVIDER_NAME] = {
baseUrl: `http://${LOCAL_HOST}:${DEFAULT_PORT}${OPENAI_API_BASE}`,
baseUrl: "http://127.0.0.1:3456/v1",
api: "openai-completions",
authHeader: false,
models: desiredModels,
+1 -2
View File
@@ -15,7 +15,6 @@ import { homedir } from "node:os";
import { join } from "node:path";
import { existsSync, copyFileSync } from "node:fs";
import { writeSnapshot, listSnapshots, readSnapshot, gcSnapshots } from "./lib/snapshot.mjs";
import { DEFAULT_PORT } from "../lib/constants.mjs";
export async function runUpgrade(opts = {}) {
const dryRun = !!opts.dryRun;
@@ -135,7 +134,7 @@ async function runFullUpgrade({ doctor, opts }) {
// phase 6: post-flight (10s budget; skipped under mockExec)
if (!opts.mockExec) {
const port = process.env.CLAUDE_PROXY_PORT || String(DEFAULT_PORT);
const port = process.env.CLAUDE_PROXY_PORT || "3478";
let ok = false;
for (let i = 0; i < 10; i++) {
try {
+2 -3
View File
@@ -11,7 +11,7 @@
* This matches LiteLLM, OpenAI SDK, and other major LLM proxies.
*
* Env vars:
* CLAUDE_PROXY_PORT — listen port (default: DEFAULT_PORT from lib/constants.mjs)
* CLAUDE_PROXY_PORT — listen port (default: 3456)
* CLAUDE_BIN — path to claude binary (default: auto-detect)
* CLAUDE_TIMEOUT — per-request timeout in ms (default: 600000)
* CLAUDE_ALLOWED_TOOLS — comma-separated tools to allow (default: expanded set)
@@ -35,7 +35,6 @@ import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { homedir } from "node:os";
import { validateKey, recordUsage, getUsageByKey, getUsageTimeline, getRecentUsage, createKey, listKeys, revokeKey, closeDb, checkQuota, updateKeyQuota, getKeyQuota, findKey, cacheHash, getCachedResponse, setCachedResponse, clearCache, getCacheStats, hasCacheControl, singleflight, getInflightStats } from "./keys.mjs";
import { DEFAULT_PORT } from "./lib/constants.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const _pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8"));
@@ -124,7 +123,7 @@ function resolveClaude() {
// ── Configuration ───────────────────────────────────────────────────────
// Settings marked with `let` can be changed at runtime via PATCH /settings.
const PORT = parseInt(process.env.CLAUDE_PROXY_PORT || String(DEFAULT_PORT), 10);
const PORT = parseInt(process.env.CLAUDE_PROXY_PORT || "3456", 10);
const CLAUDE = resolveClaude();
let TIMEOUT = parseInt(process.env.CLAUDE_TIMEOUT || "600000", 10);
const PROXY_API_KEY = process.env.PROXY_API_KEY || "";
+2 -4
View File
@@ -3,8 +3,7 @@
* OCP (Open Claude Proxy) setup
*
* Automatically configures OpenClaw to use Claude CLI as a model provider.
* Run: node setup.mjs [--port N] [--default-model opus|sonnet|haiku] [--dry-run]
* (default port = DEFAULT_PORT from lib/constants.mjs)
* Run: node setup.mjs [--port 3456] [--default-model opus|sonnet|haiku] [--dry-run]
*
* What it does:
* 1. Verifies claude CLI is installed and authenticated
@@ -19,7 +18,6 @@ import { execSync } from "node:child_process";
import { join, dirname } from "node:path";
import { homedir } from "node:os";
import { fileURLToPath } from "node:url";
import { DEFAULT_PORT } from "./lib/constants.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const HOME = homedir();
@@ -34,7 +32,7 @@ const opt = (name, fallback) => {
return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
};
const PORT = parseInt(opt("port", String(DEFAULT_PORT)), 10);
const PORT = parseInt(opt("port", "3456"), 10);
const DEFAULT_MODEL = opt("default-model", "opus"); // opus | sonnet | haiku
const DRY_RUN = flag("dry-run");
const SKIP_START = flag("no-start");