mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-22 13:35:08 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43cd7712e6 | ||
|
|
ba273aaf06 | ||
|
|
7cff33cc18 | ||
|
|
d4f4aaf33a | ||
|
|
22806bffb5 | ||
|
|
6bfffd2cba | ||
|
|
2853088261 | ||
|
|
fd7973addb |
@@ -0,0 +1,40 @@
|
|||||||
|
# Pull Request
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
<!-- One or two sentences describing the change and why it is in scope for OCP. -->
|
||||||
|
|
||||||
|
## Claude Code Alignment Evidence (REQUIRED)
|
||||||
|
|
||||||
|
Per `ALIGNMENT.md`, every PR that touches `server.mjs` or any network-facing surface must fill out this section. PRs with this section blank or unchecked will receive a `request changes` review and cannot be merged.
|
||||||
|
|
||||||
|
- [ ] **Corresponding `cli.js` reference.** I have identified the `cli.js` function and line range that performs the operation this PR forwards. Citation (format `cli.js:NNNN` or `cli.js vE4 <functionName>`):
|
||||||
|
<!-- e.g. cli.js:18423-18467 (function: sendUserMessage) -->
|
||||||
|
|
||||||
|
- [ ] **If `cli.js` does not perform this operation**, I have stated this explicitly below and justified the scope under `ALIGNMENT.md` Rule 2. (Note: in almost all cases this means the PR should be closed, not merged. Proxy layers do not invent endpoints.)
|
||||||
|
<!-- Justification, if applicable. Empty is fine when cli.js does perform the operation. -->
|
||||||
|
|
||||||
|
- [ ] **Commit message citations.** Every "Claude Code uses X" or "cli.js uses X" assertion in every commit of this PR is immediately followed by a `cli.js:NNNN` or `cli.js vE4 <functionName>` citation. I have verified this by rereading each commit message.
|
||||||
|
|
||||||
|
## Type of change
|
||||||
|
|
||||||
|
- [ ] Bug fix (alignment with existing `cli.js` behavior)
|
||||||
|
- [ ] Feature (new `cli.js` behavior now surfaced through OCP)
|
||||||
|
- [ ] Refactor (no wire-level behavior change)
|
||||||
|
- [ ] Deletion (unalignable feature removal per `ALIGNMENT.md` Unalignable Policy)
|
||||||
|
- [ ] Documentation / governance
|
||||||
|
|
||||||
|
## Reviewer checklist
|
||||||
|
|
||||||
|
Reviewers: this section is for you, not the author. Do not approve until every box is checked.
|
||||||
|
|
||||||
|
- [ ] I opened `cli.js` at the cited line range and confirmed the operation matches.
|
||||||
|
- [ ] I ran (or confirmed CI ran) `.github/workflows/alignment.yml` and it passed.
|
||||||
|
- [ ] I am not the commit author of any commit in this PR (Iron Rule 10).
|
||||||
|
- [ ] If the PR asserts scope without a `cli.js` citation, I confirmed the justification is sound per `ALIGNMENT.md` Rule 2.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- `ALIGNMENT.md` Rule(s) invoked: <!-- e.g. Rule 3 -->
|
||||||
|
- Related issue / prior PR: <!-- #NNN -->
|
||||||
|
- Historical lesson reference (if relevant): <!-- e.g. 2026-04-11 drift, b87992f -->
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
name: Alignment Guardrail
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'server.mjs'
|
||||||
|
- '.github/workflows/alignment.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
blacklist:
|
||||||
|
name: server.mjs blacklist (hard fail)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Scan server.mjs for hallucinated tokens
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ ! -f server.mjs ]; then
|
||||||
|
echo "server.mjs not found; nothing to scan."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Known-hallucinated tokens. Extend only via an ALIGNMENT.md amendment PR.
|
||||||
|
# Each token is matched as a fixed string against server.mjs only.
|
||||||
|
BLACKLIST=(
|
||||||
|
"api.anthropic.com/api/oauth/usage"
|
||||||
|
)
|
||||||
|
|
||||||
|
FAIL=0
|
||||||
|
for token in "${BLACKLIST[@]}"; do
|
||||||
|
if sed "s|//.*\$||" server.mjs | grep -n -F "$token"; then
|
||||||
|
echo "::error file=server.mjs::Blacklisted token '$token' detected in server.mjs."
|
||||||
|
FAIL=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$FAIL" -ne 0 ]; then
|
||||||
|
cat <<'EOF'
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
ALIGNMENT GUARDRAIL FAILURE
|
||||||
|
============================================================
|
||||||
|
server.mjs contains a token on the OCP alignment blacklist.
|
||||||
|
|
||||||
|
These tokens were introduced by LLM hallucinations and do
|
||||||
|
not appear in cli.js at any shipped Claude Code version.
|
||||||
|
See ALIGNMENT.md -> "Historical Lesson: The 2026-04-11 Drift"
|
||||||
|
(commit b87992f) for the full incident record.
|
||||||
|
|
||||||
|
Required action:
|
||||||
|
1. Remove the token from server.mjs.
|
||||||
|
2. grep the reference cli.js for the operation you
|
||||||
|
intended and cite the real line numbers.
|
||||||
|
3. See ALIGNMENT.md Rules 1, 2, and 5.
|
||||||
|
|
||||||
|
Do not add allowlist entries to this workflow without an
|
||||||
|
amendment PR to ALIGNMENT.md (see Amendment Procedure).
|
||||||
|
============================================================
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Blacklist scan clean."
|
||||||
|
|
||||||
|
commit-citation:
|
||||||
|
name: commit message citation (soft check)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Soft check: reports a warning but does not block merge. Reviewers
|
||||||
|
# are expected to enforce per CLAUDE.md. Escalate to hard-fail via
|
||||||
|
# an ALIGNMENT.md amendment if drift recurs.
|
||||||
|
continue-on-error: true
|
||||||
|
steps:
|
||||||
|
- name: Checkout full history
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Scan PR commits for uncited assertions
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ -z "${BASE_SHA:-}" ] || [ -z "${HEAD_SHA:-}" ]; then
|
||||||
|
echo "No PR context; skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect commit messages in range.
|
||||||
|
MSGS="$(git log --format=%B "${BASE_SHA}..${HEAD_SHA}")"
|
||||||
|
|
||||||
|
# Look for assertions of the form "Claude Code uses ..." or
|
||||||
|
# "cli.js uses ..." (case-insensitive). For each hit, require
|
||||||
|
# a citation in the same commit message in one of the forms:
|
||||||
|
# cli.js:NNNN (line-number citation)
|
||||||
|
# cli.js vE4 <name> (version + function-name citation)
|
||||||
|
# Absence of a citation is a soft finding.
|
||||||
|
|
||||||
|
WARN=0
|
||||||
|
# Split log into per-commit blocks for precise matching.
|
||||||
|
git log --format="%H" "${BASE_SHA}..${HEAD_SHA}" | while read -r sha; do
|
||||||
|
BODY="$(git log -1 --format=%B "$sha")"
|
||||||
|
if echo "$BODY" | grep -E -i -q '(claude[[:space:]]+code|cli\.js)[[:space:]]+uses'; then
|
||||||
|
if echo "$BODY" | grep -E -q '(cli\.js:[0-9]+|cli\.js[[:space:]]+v[A-Za-z0-9]+[[:space:]]+[A-Za-z_][A-Za-z0-9_]*)'; then
|
||||||
|
echo "OK $sha: assertion cited."
|
||||||
|
else
|
||||||
|
echo "::warning::Commit $sha asserts 'Claude Code uses ...' or 'cli.js uses ...' but does not cite a cli.js line number or versioned function name. See CLAUDE.md -> Commit message conventions."
|
||||||
|
WARN=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$WARN" -ne 0 ]; then
|
||||||
|
echo "Soft check raised warnings. Reviewer: please enforce per CLAUDE.md."
|
||||||
|
else
|
||||||
|
echo "Commit citation soft check clean."
|
||||||
|
fi
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# OCP Alignment Constitution
|
||||||
|
|
||||||
|
**Status:** Active. This document is the supreme source of truth for OCP scope decisions. Conflicts with other documents (README, issues, prior commit messages) resolve in favor of this file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Principle
|
||||||
|
|
||||||
|
OCP (Open Claude Proxy) is a **proxy layer** for the Claude Code CLI. It forwards, observes, and multiplexes the traffic that `cli.js` already emits. It is **not** an extension layer. If `cli.js` does not perform a given operation, or performs it differently, OCP does not invent one.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **Rule 1 (Grep First).** Before adding, renaming, or changing any endpoint, header, parameter, or response shape, the author must `grep` the reference `cli.js` and record the exact line numbers in the commit message and PR body. An absent grep hit is itself a finding and must be declared.
|
||||||
|
|
||||||
|
2. **Rule 2 (No Invention).** OCP must not introduce endpoints, headers, request fields, or response fields that are not present in `cli.js`. Speculative "Claude Code probably uses X" statements are prohibited. If the behavior is not observable in `cli.js`, the feature is out of scope.
|
||||||
|
|
||||||
|
3. **Rule 3 (Match the Implementation).** When `cli.js` does perform a given operation, OCP must match it byte-for-byte on the wire: same path, same method, same headers (including casing and ordering constraints), same body schema, same auth scheme. Deviations require an explicit, reviewed exception recorded in this file.
|
||||||
|
|
||||||
|
4. **Rule 4 (Unalignable Features Are Deleted).** Any existing OCP feature that cannot be traced to a concrete `cli.js` reference is deleted. There is no "grandfathering" and no "keep it disabled." The policy is removal, not deprecation. See the Unalignable Policy section below.
|
||||||
|
|
||||||
|
5. **Rule 5 (Cite Line Numbers in Commits).** Every commit that touches `server.mjs` must reference `cli.js` by line number or function name in the form `cli.js:NNNN` or `cli.js vE4 <functionName>`. Commits asserting "Claude Code uses X" without such a citation are blocked by CI and must be reverted on detection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Golden Reference: `cli.js`
|
||||||
|
|
||||||
|
`cli.js` is the Claude Code CLI JavaScript bundle shipped inside the `@anthropic-ai/claude-code` npm package. It is the single source of truth for "what Claude Code actually does."
|
||||||
|
|
||||||
|
### Canonical paths per machine
|
||||||
|
|
||||||
|
| Machine / environment | Path |
|
||||||
|
| --- | --- |
|
||||||
|
| macOS (npm global) | `/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js` |
|
||||||
|
| macOS (nvm) | `~/.nvm/versions/node/*/lib/node_modules/@anthropic-ai/claude-code/cli.js` |
|
||||||
|
| Linux (npm global) | `/usr/lib/node_modules/@anthropic-ai/claude-code/cli.js` |
|
||||||
|
| Linux (OCI opc user) | `~/.npm-global/lib/node_modules/@anthropic-ai/claude-code/cli.js` |
|
||||||
|
| Windows (npm global) | `%APPDATA%\npm\node_modules\@anthropic-ai\claude-code\cli.js` |
|
||||||
|
| Raspberry Pi (nvm) | `~/.nvm/versions/node/*/lib/node_modules/@anthropic-ai/claude-code/cli.js` |
|
||||||
|
|
||||||
|
### Current audit pin
|
||||||
|
|
||||||
|
- **Claude Code version under audit:** `2.1.89`
|
||||||
|
- **`cli.js` SHA-256:** `a9950ef6407fdc750bddb673852485500387e524a99d42385cb81e7d17128e01`
|
||||||
|
- **Audit date:** `2026-04-20`
|
||||||
|
- **Auditor:** `Tao Deng`
|
||||||
|
|
||||||
|
The audit pin is updated once per year (see Annual Alignment Audit) and whenever a drift incident forces a re-verification.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Historical Lesson: The 2026-04-11 Drift
|
||||||
|
|
||||||
|
On 2026-04-11, commit `b87992f` ("fix: use dedicated /api/oauth/usage endpoint for reliable plan data") was merged. The commit message asserted that `/api/oauth/usage` was "the dedicated usage endpoint that Claude Code CLI uses."
|
||||||
|
|
||||||
|
**This assertion was false.** The string `/api/oauth/usage` does not appear in `cli.js` at any version shipped up to that date. The endpoint was fabricated by an LLM-assisted authoring pass that generalized from adjacent OAuth paths without verifying against `cli.js`. A follow-up commit `cb6c2a8` ("fallback to stale cache on usage API 429 + extend cache to 15min") compounded the error by caching the fabricated response to hide the 4xx failures.
|
||||||
|
|
||||||
|
**Impact:** The `/usage` progress bar in the dashboard was broken for nine days (2026-04-11 through 2026-04-20) before the drift was isolated.
|
||||||
|
|
||||||
|
**Root cause:** LLM hallucination accepted without `grep cli.js` verification, compounded by the absence of a CI blacklist and the absence of this constitution.
|
||||||
|
|
||||||
|
**Fix commit:** `fd7973a` (PR #21 — restored header-based `/usage`); follow-up `01e260c` (PR #24 — OAuth Bearer header correction)
|
||||||
|
|
||||||
|
**Lesson codified:** Rules 1, 2, and 5 of this document; the CI blacklist in `.github/workflows/alignment.yml`; and the PR template evidence section exist to make the 2026-04-11 drift structurally impossible to repeat.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Unalignable Policy
|
||||||
|
|
||||||
|
A feature is **unalignable** if, after a good-faith search, it cannot be mapped to a specific `cli.js` line range or function.
|
||||||
|
|
||||||
|
- Unalignable features are **deleted**, not disabled, not feature-flagged, not deprecated.
|
||||||
|
- Deletion is the default outcome of an alignment audit finding. The burden of proof is on the feature, not on the auditor.
|
||||||
|
- A deletion PR does not require user-facing deprecation notice, because the feature was never legitimately in scope.
|
||||||
|
- If a user workflow depended on an unalignable feature, the correct remediation is to upstream the behavior into `cli.js` or to move it out of OCP into a separate tool. OCP does not retain it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Annual Alignment Audit
|
||||||
|
|
||||||
|
- **Date:** 11 April each year (the anniversary of the `b87992f` drift).
|
||||||
|
- **Scope:** Diff the current `cli.js` against the pinned SHA-256 in the Golden Reference section. For every network call in `server.mjs`, re-verify that the corresponding `cli.js` reference still exists at the cited line numbers (adjust citations if line numbers shifted across Claude Code versions).
|
||||||
|
- **Output:** A signed audit note committed to `docs/alignment-audits/YYYY-04-11.md`, updating the pin.
|
||||||
|
- **Failure mode:** Any audit finding that cannot be reconciled triggers an immediate deletion PR per the Unalignable Policy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Amendment Procedure
|
||||||
|
|
||||||
|
This constitution is amended only by a PR that (a) cites the evidence motivating the amendment, (b) is reviewed by an independent reviewer per CC Iron Rule 10, and (c) updates the Historical Lesson section if the amendment was driven by an incident. Amendments never retroactively legitimize previously unalignable features.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# OCP Project Session Instructions
|
||||||
|
|
||||||
|
> **WARNING — READ BEFORE WRITING ANY CODE IN THIS REPO**
|
||||||
|
>
|
||||||
|
> Before touching `server.mjs` or any network-facing surface, read [`./ALIGNMENT.md`](./ALIGNMENT.md) in full. The constitution is binding. Non-compliant commits are reverted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Before starting any task
|
||||||
|
|
||||||
|
1. Read `./ALIGNMENT.md`. Internalize the five Rules and the 2026-04-11 drift lesson.
|
||||||
|
2. Run `/dev-start <task description>` to get a pre-flight plan that incorporates the iron rules, `SKILL_ROUTING.md`, this file, and `ALIGNMENT.md`.
|
||||||
|
3. If the task touches `server.mjs`, locate the corresponding `cli.js` reference **before** drafting any code. No code is written ahead of the `grep cli.js` evidence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hard requirements for `server.mjs` changes
|
||||||
|
|
||||||
|
Every PR that modifies `server.mjs` must satisfy all three of the following. A PR missing any one of them is blocked from merge.
|
||||||
|
|
||||||
|
1. **`cli.js` citation.** The commit message and PR body declare the corresponding `cli.js` function name and line number range, using the format `cli.js:NNNN` or `cli.js vE4 <functionName>`. If `cli.js` does not perform the operation, the PR must state this explicitly and justify scope under `ALIGNMENT.md` Rule 2 (in practice, this almost always means the PR should be closed).
|
||||||
|
2. **CI blacklist pass.** The `alignment.yml` workflow must pass. The workflow greps `server.mjs` for known-hallucinated tokens (including `api/oauth/usage` and `api/usage`) and fails the build on any hit. Do not suppress the workflow. Do not add allowlist entries without an amendment PR to `ALIGNMENT.md`.
|
||||||
|
3. **Independent reviewer (Iron Rule 10).** The implementation author may not self-approve. A separate reviewer — human or a subagent spawned with a fresh context — must read the diff, verify the `cli.js` citation by opening `cli.js` at the cited lines, and explicitly approve. A review comment that does not confirm the `cli.js` citation was checked is not a valid approval.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Iron rules in force
|
||||||
|
|
||||||
|
This repo operates under the CC Development Iron Rules (CC 开发铁律) v1.3. Three rules are load-bearing for OCP work:
|
||||||
|
|
||||||
|
- **Iron Rule 10 (Code Review).** Every implementation phase has an independent reviewer. Self-review does not count. See `server.mjs` hard requirement #3 above.
|
||||||
|
- **Iron Rule 11 (Incremental Diff Review).** Non-trivial work is split into the minimum reviewable unit — one PR per layer per severity. `ALIGNMENT.md`, `CLAUDE.md`, the PR template, and the CI workflow are therefore shipped as the same constitutional PR (they are one layer: governance), but any subsequent `server.mjs` remediation lands as its own PR.
|
||||||
|
- **Iron Rule 12 (Pre-Brainstorm Prior-Art Search).** Before proposing any new endpoint or header, search GitHub, Anthropic docs, and the `cli.js` bundle. For OCP specifically, the `cli.js` grep is the decisive search: if it does not hit, Rule 2 of the constitution applies.
|
||||||
|
|
||||||
|
The full iron rules are at `~/.claude/CC_DEV_IRON_RULES.md` (symlinked from the cc-rules repo on Tao's workstations). Load them into session context with `/cc-rules` when needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Skills relevant to this repo
|
||||||
|
|
||||||
|
- `/dev-start` — pre-flight planning, always first.
|
||||||
|
- `/cc-rules` — load the iron rules into context.
|
||||||
|
- `/agent-dispatch` — pick the correct model (opus for design and review, sonnet for straightforward edits, haiku for mechanical chores) before spawning any subagent.
|
||||||
|
- `/cc-mem search <keyword>` — look up cross-machine memory for prior decisions, especially prior drift incidents.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commit message conventions
|
||||||
|
|
||||||
|
- Subject line uses Conventional Commits (`fix:`, `feat:`, `docs:`, `refactor:`, `chore:`).
|
||||||
|
- Any assertion of the form "Claude Code uses X" or "cli.js uses X" in the body must be immediately followed by a citation in the form `cli.js:NNNN` or `cli.js vE4 <functionName>`. CI performs a soft check for this pattern on all commits in the PR.
|
||||||
|
- Co-author trailer is required for LLM-assisted commits (`Co-Authored-By: Claude <model> <noreply@anthropic.com>`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project-level escalation
|
||||||
|
|
||||||
|
If a design decision cannot be resolved by reference to `cli.js` and `ALIGNMENT.md`, escalate to Tao (老大) via `/cc-chat` rather than guessing. Silent guessing is what produced the 2026-04-11 drift.
|
||||||
@@ -139,7 +139,7 @@ OCP Connect v1.3.0
|
|||||||
Checking connectivity...
|
Checking connectivity...
|
||||||
✓ Connected
|
✓ Connected
|
||||||
|
|
||||||
Remote OCP v3.8.0 (auth: multi)
|
Remote OCP v3.10.0 (auth: multi)
|
||||||
|
|
||||||
ⓘ Using server-advertised anonymous key: ocp_publ...n_v1
|
ⓘ Using server-advertised anonymous key: ocp_publ...n_v1
|
||||||
(set by admin via PROXY_ANONYMOUS_KEY; see issue #12 §14 Path A)
|
(set by admin via PROXY_ANONYMOUS_KEY; see issue #12 §14 Path A)
|
||||||
@@ -195,7 +195,7 @@ OCP Connect v1.3.0
|
|||||||
The script automatically:
|
The script automatically:
|
||||||
- Writes env vars to all relevant shell rc files (`.bashrc`, `.zshrc`)
|
- Writes env vars to all relevant shell rc files (`.bashrc`, `.zshrc`)
|
||||||
- Sets system-level env vars (`launchctl setenv` on macOS, `environment.d` on Linux)
|
- Sets system-level env vars (`launchctl setenv` on macOS, `environment.d` on Linux)
|
||||||
- **Auto-discovers anonymous key** from `/health.anonymousKey` when no `--key` given (v1.3.0+, requires server v3.8.0+)
|
- **Auto-discovers anonymous key** from `/health.anonymousKey` when no `--key` given (v1.3.0+, requires server v3.10.0+)
|
||||||
- Configures OpenClaw automatically (including per-agent `auth-profiles.json` for multi-agent setups)
|
- Configures OpenClaw automatically (including per-agent `auth-profiles.json` for multi-agent setups)
|
||||||
- Detects Cline, Continue.dev, Cursor, and opencode, and prints setup hints (manual configuration required for these IDEs)
|
- Detects Cline, Continue.dev, Cursor, and opencode, and prints setup hints (manual configuration required for these IDEs)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "openclaw-claude-proxy",
|
"name": "openclaw-claude-proxy",
|
||||||
"version": "3.8.0",
|
"version": "3.10.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.",
|
"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",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
+139
-85
@@ -146,18 +146,20 @@ const _BREAKER_DISABLED_NOTE = "disabled";
|
|||||||
// ── Model mapping ───────────────────────────────────────────────────────
|
// ── Model mapping ───────────────────────────────────────────────────────
|
||||||
// Maps request model IDs and aliases to canonical claude CLI model IDs.
|
// Maps request model IDs and aliases to canonical claude CLI model IDs.
|
||||||
const MODEL_MAP = {
|
const MODEL_MAP = {
|
||||||
|
"claude-opus-4-7": "claude-opus-4-7",
|
||||||
"claude-opus-4-6": "claude-opus-4-6",
|
"claude-opus-4-6": "claude-opus-4-6",
|
||||||
"claude-sonnet-4-6": "claude-sonnet-4-6",
|
"claude-sonnet-4-6": "claude-sonnet-4-6",
|
||||||
"claude-haiku-4-5-20251001": "claude-haiku-4-5-20251001",
|
"claude-haiku-4-5-20251001": "claude-haiku-4-5-20251001",
|
||||||
"claude-opus-4": "claude-opus-4-6",
|
"claude-opus-4": "claude-opus-4-7",
|
||||||
"claude-haiku-4": "claude-haiku-4-5-20251001",
|
"claude-haiku-4": "claude-haiku-4-5-20251001",
|
||||||
"claude-haiku-4-5": "claude-haiku-4-5-20251001",
|
"claude-haiku-4-5": "claude-haiku-4-5-20251001",
|
||||||
"opus": "claude-opus-4-6",
|
"opus": "claude-opus-4-7",
|
||||||
"sonnet": "claude-sonnet-4-6",
|
"sonnet": "claude-sonnet-4-6",
|
||||||
"haiku": "claude-haiku-4-5-20251001",
|
"haiku": "claude-haiku-4-5-20251001",
|
||||||
};
|
};
|
||||||
|
|
||||||
const MODELS = [
|
const MODELS = [
|
||||||
|
{ id: "claude-opus-4-7", name: "Claude Opus 4.7" },
|
||||||
{ id: "claude-opus-4-6", name: "Claude Opus 4.6" },
|
{ id: "claude-opus-4-6", name: "Claude Opus 4.6" },
|
||||||
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
|
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" },
|
||||||
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5" },
|
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5" },
|
||||||
@@ -680,25 +682,42 @@ function completionResponse(res, id, model, content) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Plan usage probe ────────────────────────────────────────────────────
|
// ── Plan usage probe ────────────────────────────────────────────────────
|
||||||
// Uses the dedicated /api/oauth/usage endpoint (same as Claude Code CLI)
|
// ── Plan usage probe ────────────────────────────────────────────────────
|
||||||
// with Bearer auth + anthropic-beta header. Auto-refreshes expired tokens.
|
// ALIGNMENT: mirrors Claude Code cli.js vE4 rate-limit header extraction.
|
||||||
// Caches the result for 5 minutes to avoid excessive API calls.
|
// DO NOT switch endpoints without grepping "anthropic-ratelimit-unified" in cli.js.
|
||||||
|
// 2026-04-11 b87992f drift lesson: /api/oauth/usage is a hallucinated endpoint.
|
||||||
|
// See ALIGNMENT.md for full history.
|
||||||
|
//
|
||||||
|
// Reads OAuth token (keychain / Linux credentials / CLAUDE_CODE_OAUTH_TOKEN env)
|
||||||
|
// and makes a minimal /v1/messages request to capture anthropic-ratelimit-unified-*
|
||||||
|
// headers. Caches the result for 5 minutes.
|
||||||
|
|
||||||
let usageCache = { data: null, fetchedAt: 0 };
|
let usageCache = { data: null, fetchedAt: 0 };
|
||||||
const USAGE_CACHE_TTL = 900000; // 15 min
|
const USAGE_CACHE_TTL = 5 * 60 * 1000; // 5 min
|
||||||
const OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
const OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
||||||
const OAUTH_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
const OAUTH_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
||||||
const OAUTH_BETA_HEADER = "oauth-2025-04-20";
|
|
||||||
|
// Refresh backoff state — exponential 60s → 3600s.
|
||||||
|
// Prevents tight loops hammering the token endpoint after a failure
|
||||||
|
// (lesson from pre-fix session that burned through rate-limit in seconds).
|
||||||
|
const OAUTH_REFRESH_MIN_BACKOFF = 60 * 1000;
|
||||||
|
const OAUTH_REFRESH_MAX_BACKOFF = 3600 * 1000;
|
||||||
|
let oauthRefreshBackoff = { nextAttemptAt: 0, currentDelay: OAUTH_REFRESH_MIN_BACKOFF };
|
||||||
|
|
||||||
function getOAuthCredentials() {
|
function getOAuthCredentials() {
|
||||||
// Try Linux file-based credentials first
|
// 1. Env var fallback — highest precedence for explicit overrides.
|
||||||
|
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
||||||
|
return { accessToken: process.env.CLAUDE_CODE_OAUTH_TOKEN };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Linux file-based credentials
|
||||||
try {
|
try {
|
||||||
const credPath = join(homedir(), ".claude", ".credentials.json");
|
const credPath = join(homedir(), ".claude", ".credentials.json");
|
||||||
const creds = JSON.parse(readFileSync(credPath, "utf8"));
|
const creds = JSON.parse(readFileSync(credPath, "utf8"));
|
||||||
if (creds?.claudeAiOauth?.accessToken) return creds.claudeAiOauth;
|
if (creds?.claudeAiOauth?.accessToken) return creds.claudeAiOauth;
|
||||||
} catch { /* fall through to macOS keychain */ }
|
} catch { /* fall through to macOS keychain */ }
|
||||||
|
|
||||||
// Try macOS keychain (both label formats)
|
// 3. macOS keychain (both label formats)
|
||||||
for (const label of ["claude-code-credentials", "Claude Code-credentials"]) {
|
for (const label of ["claude-code-credentials", "Claude Code-credentials"]) {
|
||||||
try {
|
try {
|
||||||
const raw = execFileSync("security", [
|
const raw = execFileSync("security", [
|
||||||
@@ -712,6 +731,13 @@ function getOAuthCredentials() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshOAuthToken(refreshToken) {
|
async function refreshOAuthToken(refreshToken) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now < oauthRefreshBackoff.nextAttemptAt) {
|
||||||
|
logEvent("info", "oauth_refresh_backoff_skip", {
|
||||||
|
waitMs: oauthRefreshBackoff.nextAttemptAt - now,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(OAUTH_TOKEN_URL, {
|
const resp = await fetch(OAUTH_TOKEN_URL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -725,13 +751,34 @@ async function refreshOAuthToken(refreshToken) {
|
|||||||
});
|
});
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const body = await resp.text();
|
const body = await resp.text();
|
||||||
logEvent("warn", "oauth_refresh_failed", { status: resp.status, body: body.slice(0, 200) });
|
// Exponential backoff on failure
|
||||||
|
oauthRefreshBackoff.nextAttemptAt = Date.now() + oauthRefreshBackoff.currentDelay;
|
||||||
|
oauthRefreshBackoff.currentDelay = Math.min(
|
||||||
|
oauthRefreshBackoff.currentDelay * 2,
|
||||||
|
OAUTH_REFRESH_MAX_BACKOFF,
|
||||||
|
);
|
||||||
|
logEvent("warn", "oauth_refresh_failed", {
|
||||||
|
status: resp.status,
|
||||||
|
body: body.slice(0, 200),
|
||||||
|
nextBackoffMs: oauthRefreshBackoff.currentDelay,
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
|
// Reset backoff on success
|
||||||
|
oauthRefreshBackoff.currentDelay = OAUTH_REFRESH_MIN_BACKOFF;
|
||||||
|
oauthRefreshBackoff.nextAttemptAt = 0;
|
||||||
return data.access_token || null;
|
return data.access_token || null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logEvent("warn", "oauth_refresh_error", { error: err.message });
|
oauthRefreshBackoff.nextAttemptAt = Date.now() + oauthRefreshBackoff.currentDelay;
|
||||||
|
oauthRefreshBackoff.currentDelay = Math.min(
|
||||||
|
oauthRefreshBackoff.currentDelay * 2,
|
||||||
|
OAUTH_REFRESH_MAX_BACKOFF,
|
||||||
|
);
|
||||||
|
logEvent("warn", "oauth_refresh_error", {
|
||||||
|
error: err.message,
|
||||||
|
nextBackoffMs: oauthRefreshBackoff.currentDelay,
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -739,73 +786,89 @@ async function refreshOAuthToken(refreshToken) {
|
|||||||
async function fetchUsageFromApi() {
|
async function fetchUsageFromApi() {
|
||||||
const creds = getOAuthCredentials();
|
const creds = getOAuthCredentials();
|
||||||
if (!creds?.accessToken) {
|
if (!creds?.accessToken) {
|
||||||
return { error: "No OAuth token found in keychain" };
|
return { error: "No OAuth token found (keychain / ~/.claude/.credentials.json / CLAUDE_CODE_OAUTH_TOKEN)" };
|
||||||
}
|
}
|
||||||
|
|
||||||
let token = creds.accessToken;
|
let token = creds.accessToken;
|
||||||
|
|
||||||
// Check if token looks expired (5 min buffer, same as Claude Code)
|
// Pre-emptive refresh if token looks expired (5 min buffer, same as Claude Code)
|
||||||
if (creds.expiresAt && Date.now() + 300000 >= creds.expiresAt) {
|
if (creds.expiresAt && Date.now() + 300000 >= creds.expiresAt && creds.refreshToken) {
|
||||||
if (creds.refreshToken) {
|
logEvent("info", "oauth_token_expired_refreshing");
|
||||||
logEvent("info", "oauth_token_expired_refreshing");
|
const newToken = await refreshOAuthToken(creds.refreshToken);
|
||||||
const newToken = await refreshOAuthToken(creds.refreshToken);
|
if (newToken) token = newToken;
|
||||||
if (newToken) token = newToken;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Minimal /v1/messages request — we only need the response headers.
|
||||||
|
// Mirrors Claude Code cli.js vE4: headers anthropic-ratelimit-unified-{5h,7d}-{utilization,reset}.
|
||||||
|
const body = JSON.stringify({
|
||||||
|
model: "claude-haiku-4-5-20251001",
|
||||||
|
max_tokens: 1,
|
||||||
|
messages: [{ role: "user", content: "." }],
|
||||||
|
});
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), 10000);
|
const timeout = setTimeout(() => controller.abort(), 15000);
|
||||||
|
|
||||||
|
const doFetch = (bearerToken) => fetch("https://api.anthropic.com/v1/messages", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${bearerToken}`,
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
"anthropic-beta": "oauth-2025-04-20",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
let resp = await doFetch(token);
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Authorization": `Bearer ${token}`,
|
|
||||||
"anthropic-beta": OAUTH_BETA_HEADER,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
signal: controller.signal,
|
|
||||||
});
|
|
||||||
clearTimeout(timeout);
|
|
||||||
|
|
||||||
if (!resp.ok) {
|
// 401 → try a single refresh-and-retry
|
||||||
// If 401, try refreshing token once
|
if (resp.status === 401 && creds.refreshToken) {
|
||||||
if (resp.status === 401 && creds.refreshToken) {
|
logEvent("info", "oauth_usage_401_refreshing");
|
||||||
logEvent("info", "oauth_usage_401_refreshing");
|
const newToken = await refreshOAuthToken(creds.refreshToken);
|
||||||
const newToken = await refreshOAuthToken(creds.refreshToken);
|
if (newToken) {
|
||||||
if (newToken) {
|
token = newToken;
|
||||||
const retryResp = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
resp = await doFetch(token);
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Authorization": `Bearer ${newToken}`,
|
|
||||||
"anthropic-beta": OAUTH_BETA_HEADER,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (retryResp.ok) {
|
|
||||||
const retryData = await retryResp.json();
|
|
||||||
return parseUsageResponse(retryData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { error: `Usage API auth failed after refresh (${resp.status})` };
|
|
||||||
}
|
}
|
||||||
return { error: `Usage API returned ${resp.status}` };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await resp.json();
|
clearTimeout(timeout);
|
||||||
return parseUsageResponse(data);
|
|
||||||
|
// Extract all rate-limit headers (we do not need the response body)
|
||||||
|
const rl = {};
|
||||||
|
for (const [k, v] of resp.headers) {
|
||||||
|
if (k.startsWith("anthropic-ratelimit")) rl[k] = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resp.ok && Object.keys(rl).length === 0) {
|
||||||
|
return { error: `Usage API returned ${resp.status} with no rate-limit headers` };
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseRateLimitHeaders(rl);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
return { error: `Failed to fetch usage: ${err.message}` };
|
return { error: `Failed to fetch usage: ${err.message}` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseUsageResponse(data) {
|
function parseRateLimitHeaders(rl) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
function formatReset(isoStr) {
|
const session5hUtil = parseFloat(rl["anthropic-ratelimit-unified-5h-utilization"] || "0");
|
||||||
if (!isoStr) return "unknown";
|
const session5hReset = parseInt(rl["anthropic-ratelimit-unified-5h-reset"] || "0", 10);
|
||||||
const diff = new Date(isoStr).getTime() - now;
|
const weekly7dUtil = parseFloat(rl["anthropic-ratelimit-unified-7d-utilization"] || "0");
|
||||||
|
const weekly7dReset = parseInt(rl["anthropic-ratelimit-unified-7d-reset"] || "0", 10);
|
||||||
|
const overageStatus = rl["anthropic-ratelimit-unified-overage-status"] || "unknown";
|
||||||
|
const overageDisabledReason = rl["anthropic-ratelimit-unified-overage-disabled-reason"] || "";
|
||||||
|
const status = rl["anthropic-ratelimit-unified-status"] || "unknown";
|
||||||
|
const representativeClaim = rl["anthropic-ratelimit-unified-representative-claim"] || "";
|
||||||
|
const fallbackPct = parseFloat(rl["anthropic-ratelimit-unified-fallback-percentage"] || "0");
|
||||||
|
|
||||||
|
function formatReset(epochSec) {
|
||||||
|
if (!epochSec) return "unknown";
|
||||||
|
const diff = epochSec * 1000 - now;
|
||||||
if (diff <= 0) return "now";
|
if (diff <= 0) return "now";
|
||||||
const h = Math.floor(diff / 3600000);
|
const h = Math.floor(diff / 3600000);
|
||||||
const m = Math.floor((diff % 3600000) / 60000);
|
const m = Math.floor((diff % 3600000) / 60000);
|
||||||
@@ -816,42 +879,38 @@ function parseUsageResponse(data) {
|
|||||||
return h > 0 ? `${h}h ${m}m` : `${m}m`;
|
return h > 0 ? `${h}h ${m}m` : `${m}m`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetDay(isoStr) {
|
function resetDay(epochSec) {
|
||||||
if (!isoStr) return "";
|
if (!epochSec) return "";
|
||||||
const d = new Date(isoStr);
|
const d = new Date(epochSec * 1000);
|
||||||
return d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
|
return d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const fiveHour = data.five_hour || {};
|
|
||||||
const sevenDay = data.seven_day || {};
|
|
||||||
const extraUsage = data.extra_usage || {};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: "active",
|
status,
|
||||||
fetchedAt: new Date(now).toISOString(),
|
fetchedAt: new Date(now).toISOString(),
|
||||||
plan: {
|
plan: {
|
||||||
currentSession: {
|
currentSession: {
|
||||||
utilization: (fiveHour.utilization || 0) / 100,
|
utilization: session5hUtil,
|
||||||
percent: `${Math.round(fiveHour.utilization || 0)}%`,
|
percent: `${Math.round(session5hUtil * 100)}%`,
|
||||||
resetsIn: formatReset(fiveHour.resets_at),
|
resetsIn: formatReset(session5hReset),
|
||||||
resetsAt: fiveHour.resets_at || null,
|
resetsAt: session5hReset ? new Date(session5hReset * 1000).toISOString() : null,
|
||||||
resetsAtHuman: resetDay(fiveHour.resets_at),
|
resetsAtHuman: resetDay(session5hReset),
|
||||||
},
|
},
|
||||||
weeklyLimits: {
|
weeklyLimits: {
|
||||||
allModels: {
|
allModels: {
|
||||||
utilization: (sevenDay.utilization || 0) / 100,
|
utilization: weekly7dUtil,
|
||||||
percent: `${Math.round(sevenDay.utilization || 0)}%`,
|
percent: `${Math.round(weekly7dUtil * 100)}%`,
|
||||||
resetsIn: formatReset(sevenDay.resets_at),
|
resetsIn: formatReset(weekly7dReset),
|
||||||
resetsAt: sevenDay.resets_at || null,
|
resetsAt: weekly7dReset ? new Date(weekly7dReset * 1000).toISOString() : null,
|
||||||
resetsAtHuman: resetDay(sevenDay.resets_at),
|
resetsAtHuman: resetDay(weekly7dReset),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraUsage: {
|
extraUsage: {
|
||||||
status: extraUsage.is_enabled ? "enabled" : "disabled",
|
status: overageStatus,
|
||||||
monthlyLimit: extraUsage.monthly_limit,
|
disabledReason: overageDisabledReason || undefined,
|
||||||
usedCredits: extraUsage.used_credits,
|
|
||||||
utilization: extraUsage.utilization,
|
|
||||||
},
|
},
|
||||||
|
representativeClaim,
|
||||||
|
fallbackPercentage: fallbackPct,
|
||||||
},
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
totalRequests: stats.totalRequests,
|
totalRequests: stats.totalRequests,
|
||||||
@@ -861,7 +920,7 @@ function parseUsageResponse(data) {
|
|||||||
uptime: `${Math.floor((now - START_TIME) / 3600000)}h ${Math.floor(((now - START_TIME) % 3600000) / 60000)}m`,
|
uptime: `${Math.floor((now - START_TIME) / 3600000)}h ${Math.floor(((now - START_TIME) % 3600000) / 60000)}m`,
|
||||||
},
|
},
|
||||||
models: getModelStatsSnapshot(),
|
models: getModelStatsSnapshot(),
|
||||||
_raw: data,
|
_raw: rl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -874,9 +933,6 @@ async function handleUsage(_req, res) {
|
|||||||
data = await fetchUsageFromApi();
|
data = await fetchUsageFromApi();
|
||||||
if (!data.error) {
|
if (!data.error) {
|
||||||
usageCache = { data, fetchedAt: now };
|
usageCache = { data, fetchedAt: now };
|
||||||
} else if (usageCache.data) {
|
|
||||||
// Fallback to stale cache on error (e.g. 429 rate limit)
|
|
||||||
data = { ...usageCache.data, _stale: true, _fetchError: data.error };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Always attach live model stats and proxy stats (not cached)
|
// Always attach live model stats and proxy stats (not cached)
|
||||||
@@ -948,8 +1004,6 @@ async function handleStatus(_req, res) {
|
|||||||
usage = await fetchUsageFromApi();
|
usage = await fetchUsageFromApi();
|
||||||
if (!usage.error) {
|
if (!usage.error) {
|
||||||
usageCache = { data: usage, fetchedAt: now };
|
usageCache = { data: usage, fetchedAt: now };
|
||||||
} else if (usageCache.data) {
|
|
||||||
usage = { ...usageCache.data, _stale: true };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user