Files
ocp/AGENTS.md
T
c180987376 docs: guard the asymmetric cache keys (#200); document the ltBoot fault lever (#197) (#207)
* docs: guard the asymmetric cache keys (#200); document the ltBoot fault lever (#197)

Two knowledge-preservation fixes. No behavior change: server.mjs gains comments
only, verified by `git diff --stat` (comment lines) and an unchanged suite.

#200 — structuredHash and dedupKey compute the same value and must NOT be
collapsed. They take identical arguments and read as obvious duplicate work,
which is how someone "optimises" them into a bug. Their GUARDS differ, verified
line by line rather than asserted:

  structuredHash: if (CACHE_TTL > 0 && !conversationId && !hasCacheControl(...))
  dedupKey:       (!conversationId && !hasCacheControl(...))          <- no CACHE_TTL

CLAUDE_CACHE_TTL defaults to 0, so in the DEFAULT configuration structuredHash
is null while dedupKey must still be computed — it drives #153's single-flight
stampede protection, which is deliberately independent of response caching.
`dedupKey = structuredHash` would silently disable stampede protection by
default, in exactly the concurrent-AI-Task case it exists to bound. The comment
records that, and says what a safe deduplication would look like if anyone
still wants one (compute under the weaker guard, derive under the stronger,
add a stampede test first).

This was first raised in the #194 review as a cosmetic nit and then RETRACTED
by that reviewer after reading the guards — the retraction is the valuable
part, so it goes in the code rather than staying in a PR thread.

#197 — new "Testing: reaching faults inside server.mjs" section in AGENTS.md.
The premise the issue was originally filed on was wrong twice over, and both
errors were mine: first that no live-server harness existed (ltBoot has been
there all along, and I had used it myself in #191), then that a synchronous
fault deep inside spawnClaudeProcess needed a production test hook (it needs
--stack-size, which ltBoot already forwards).

The section documents the fixture, the --stack-size fault lever with the
concrete numbers (~124k element spread threshold at the default stack vs ~24k
at --stack-size=200, which is what fits Linux's 131072-byte MAX_ARG_STRLEN so
the test RUNS in CI instead of skipping), and the three rules that made it hold:
discover the threshold in a child under the same flags, assert the fault
actually fired, and wait for the thing you are about to assert rather than a
proxy for it. Each rule cites the issue that taught it (#193/#199/#203).

Written to be read BEFORE someone concludes a bug class is untestable, since
that conclusion has now been reached wrongly twice in this repo.

cli.js citation: NOT APPLICABLE — comments only, no wire behavior, no code
path, no argv change. Authorized by ADR 0006 as a behaviour-preserving edit;
the field set and every request/response contract are byte-identical.

Tests: 457 passed, 0 failed (unchanged, as expected for a comments-only diff).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbqUZ8HfBZpjjbzQ85oH8

* docs: reference only helpers that exist on main (AGENTS.md)

Review caught a real forward-reference: the new AGENTS.md section named
`ltDiag` and told readers to wait on `closed`, and NEITHER exists on main —
both are introduced by the still-unmerged #204. `git grep ltDiag` matched
exactly one line: the documentation itself.

That is worse than a broken link. AGENTS.md is, by its own "Handoff
expectations", the FIRST file a fresh session reads; it would have sent that
session looking for two identifiers it cannot find, in the file whose whole job
is to orient them.

It also meant this PR had an undeclared merge-order dependency on #204, which
I had not stated anywhere.

Fixed so each PR is self-consistent regardless of merge order:
  - names `ltPostStatus` (which does exist) instead of `ltDiag`
  - states the #203 rule as a PRINCIPLE — a terminated child's pipes may still
    hold unread data, so wait for stdio to close rather than for the exit —
    instead of naming the `closed` flag that #204 adds

Verified: every helper the section now names resolves to a `function <name>` in
test-features.mjs on this branch, and neither `ltDiag` nor `closed` is
referenced any more.

Tests: 457 passed, 0 failed (comments/docs only, unchanged).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gbqUZ8HfBZpjjbzQ85oH8

---------

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 12:05:03 +10:00

7.3 KiB

Inherits: @~/.cc-rules/AGENTS.md

OCP — Open Claude Proxy — Agent Guidelines

Scope: the dtzp555-max/ocp repository. Audience: any AI coding agent (Claude Code / Cursor / OpenCode / Copilot / Codex / Gemini) touching OCP source.


What this project is

OCP (Open Claude Proxy) is an open-source HTTP gateway that sits between the Claude Code CLI (cli.js) and Anthropic's public API. It forwards, observes, and multiplexes traffic that cli.js already emits — it is explicitly not an extension layer. A secondary role: registering OCP as a local provider inside OpenClaw (a sibling IDE-agnostic tool), so that users running OpenClaw against OCP see the same model list as native Claude Code.

Runtime: Node.js (ESM, .mjs throughout). No build step. No bundler. server.mjs is the single executable entrypoint; ocp and ocp-connect are CLI wrappers.


Stack

  • Node.js >=18, native ESM modules
  • http/https built-ins for the proxy core (no Express, no Fastify)
  • models.json as the single source of truth for model metadata
  • GitHub Actions for CI (alignment.yml, release.yml)
  • gh CLI assumed for PR creation and release automation
  • No TypeScript. No test framework beyond test-features.mjs (run via npm test; CI workflow .github/workflows/test.yml). Keep dependencies minimal.

Key files to know

  • server.mjs — the proxy itself; every request path lives here. Governed by ALIGNMENT.md.
  • models.json — single source of truth for model IDs, aliases, and context windows. See ADR 0003.
  • models.schema.json — the schema models.json declares in its $schema. CI validates the SPOT against it (test-features.mjs) using the repo's own validateJsonSchema, so a malformed entry fails the build instead of surfacing downstream in OpenClaw.
  • setup.mjs — first-time installer; reads models.json to derive bootstrap config.
  • scripts/sync-openclaw.mjs — idempotent OpenClaw registry sync invoked by ocp update. See ADR 0004.
  • ocp — user-facing CLI (install, update, start, stop, status, logs, etc.).
  • ALIGNMENT.md — the constitution. Binding for any server.mjs change. See ADR 0002.
  • .github/workflows/alignment.yml — CI blacklist grep; fails the build on known-hallucinated tokens.
  • CLAUDE.md — Claude-Code-specific session instructions + release_kit overlay (Iron Rule 5.5).
  • docs/adr/ — Architecture Decision Records. Read these before proposing governance or SPOT changes. See docs/adr/README.md for the index.
  • docs/superpowers/plans/ — active spec-kit plans. docs/superpowers/plans/shipped/ archives plans that have been delivered (don't propose changes against shipped plans — they're history). docs/superpowers/specs/ holds long-lived design documents that other code references (e.g., the SSE heartbeat design referenced from server.mjs).
  • memory/constitution.md — spec-kit's project constitution (its standard memory/ location). Distinct from ~/.cc-rules/memory/ (cross-machine personal memory) and from this repo's ALIGNMENT.md (the OCP code-level constitution).

Project-specific constraints

  • ALIGNMENT.md is binding. Any PR touching server.mjs must cite cli.js:NNNN (or cli.js vE4 <functionName>) in the commit body and PR description. See CLAUDE.md § "Hard requirements for server.mjs changes" and ADR 0002.
  • Alignment CI is not suppressible. The alignment.yml workflow greps server.mjs for known-hallucinated tokens (currently blocking api.anthropic.com/api/oauth/usage). Adding new tokens is done via PR amendment to alignment.yml; removing entries requires an ALIGNMENT.md amendment PR.
  • No self-approval. Implementation author cannot merge their own PR (Iron Rule 10). A fresh-context reviewer must open cli.js at the cited lines and confirm in the review comment.
  • models.json is the only place to add/edit models. Do not touch MODEL_MAP or MODELS arrays directly in server.mjs or setup.mjs. See ADR 0003.
  • OpenClaw boundary. scripts/sync-openclaw.mjs only writes models.providers["claude-local"].models and agents.defaults.models["claude-local/*"] in ~/.openclaw/openclaw.json. Do not expand scope. See ADR 0004.

Testing: reaching faults inside server.mjs

test-features.mjs cannot import server.mjs (it calls server.listen() at top level), and that has twice led to the wrong conclusion that a class of bug is untestable. It isn't. Read this before writing "no regression test is possible here".

There is a real live-server fixture. ltBoot(env, dir, nodeArgs) (around test-features.mjs:990) spawns the actual server.mjs as a child with a fake claude binary, so integration tests cost no quota. ltPost / ltPostStatus / ltWait / ltFreePort round it out. It already covers boot gates, cache-epoch invalidation across two boots sharing one SQLite store, and system-prompt capture.

--stack-size is a fault lever. ltBoot's third argument passes V8 flags to the child, which puts recursion- and argument-count-limited failures in reach at a much smaller input. #193 needed a synchronous throw deep inside spawnClaudeProcess; buildCliArgs does args.push("--allowedTools", ...ALLOWED_TOOLS), and under --stack-size=200 that spread throws at ~24k elements instead of ~124k — which is what brings the trigger under Linux's MAX_ARG_STRLEN (131072 bytes for a single env string) so the test runs in CI rather than skipping. No production fault hook was needed.

Three rules that made it hold up, all learned the hard way:

  • Discover the threshold in a child under the same flags, never in the test process — the parent's stack is not the one that matters.
  • Assert that the fault actually fired, not just that the outcome looks right. #193 asserts HTTP 500 and that the body carries call stack size exceeded; a control mutation (trigger neutered, bug still present) proves the test fails rather than passing vacuously.
  • Wait for the thing you are about to assert, not a proxy for it. Waiting on listening on and then asserting a different line is a race (#199); waiting for the process to exit and then reading its stderr is another, because a terminated child's pipes may still hold unread data (#203 — wait for the stdio to close, not for the exit).

Allocate ports with ltFreePort(). Fixed ports have caused at least one flake here.

Release protocol

OCP follows the machine-readable release_kit: overlay in CLAUDE.md (Iron Rule 5.5). Before any version bump or tag push, re-read that YAML block and walk every item in new_feature_doc_expectations and bootstrap_quirk_policy. Tag push triggers .github/workflows/release.yml, which creates the GitHub Release automatically — do not create the release manually.

Version is sourced from package.json; changelog from CHANGELOG.md; user-facing docs from README.md.


Handoff expectations

A fresh session picking up OCP work should read, in order:

  1. This file (AGENTS.md).
  2. ALIGNMENT.md — constitution; non-optional.
  3. CLAUDE.md — tool-specific instructions and release_kit overlay.
  4. docs/adr/ — most recent ADRs first; they explain why the current structure exists.
  5. Any active plan under docs/superpowers/plans/ (excluding shipped/ which is the archive).
  6. ~/.cc-rules/memory/auto/MEMORY.md — cross-machine memory index.

Only after these should the session touch code.