mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-26 15:35:07 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2823f34c7d | ||
|
|
ca14e6e834 | ||
|
|
a32bc9af17 | ||
|
|
8aac87aa61 | ||
|
|
c3294b404a |
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Claude Opus 5 (`claude-opus-5`), and the `opus` alias now resolves to it.** New `models.json` entry — `/v1/models` goes 6 → 7 and OpenClaw picks it up on the next `ocp update` (via `scripts/sync-openclaw.mjs`). Verified against the installed CLI rather than assumed: the compiled `claude` 2.1.220 bundle carries `latest_per_family:{fable:"claude-fable-5",opus:"claude-opus-5",sonnet:"claude-sonnet-5",haiku:"claude-haiku-4-5"}`, so repointing `opus` mirrors what the CLI itself defaults to — the same reasoning as #168 (sonnet → sonnet-5). Availability confirmed with a live `claude -p --model claude-opus-5` completion on the subscription pool. Pricing is unchanged from Opus 4.8 ($5/$25 per MTok, CLI registry `pricing:"tier_5_25"`), so the alias repoint carries no cost change. `claude-opus-4-8` is retained for pinning.
|
||||||
|
- `contextWindow` is deliberately **200000**, not Opus 5's native 1M. Two reasons, both verified: (1) `MAX_PROMPT_CHARS` is a **single global** budget — `derivePromptCharBudget` takes `max(contextWindow) × 3` across *all* entries (`lib/prompt.mjs`), so a 1M entry would raise the truncation ceiling to 3,000,000 chars for `claude-haiku-4-5` too, which is genuinely 200k native, converting clean OCP-side truncation into an upstream API rejection; (2) OpenClaw scales its history budget linearly off this value (`contextWindow × maxHistoryShare × SAFETY_MARGIN` = `× 0.6`, plus an oversized-message threshold at `× 0.5`, per `compaction-planning` in OpenClaw 2026.7.1), and its own bundled registry hardcodes 200000 for Claude — the upstream request to raise it to 1M ([openclaw#22979](https://github.com/openclaw/openclaw/issues/22979)) was closed *not planned*. A new regression test pins the invariant so a future 1M entry has to be a deliberate, reviewed change. Raising it for real needs per-model budgets — tracked separately, ADR-level.
|
||||||
|
|
||||||
## v3.24.0 — 2026-07-21
|
## v3.24.0 — 2026-07-21
|
||||||
|
|
||||||
Minor release. Headline: two long-requested **OpenAI-compat features** land — **multimodal vision** (`image_url` parts) and **structured outputs** (`response_format` / JSON schema). Also: the prompt-char budget now derives from the model SPOT instead of a hand-set constant, an agentic-turn bug that dropped the model's final answer is fixed, and `OCP_LOCAL_TOOLS` supports the OpenClaw-backend use case. Four of the six landed from external contributors (@vvlasy-openclaw). Every code PR carried a fresh-context reviewer (Iron Rule 10); no new endpoint, no new `cli.js` wire behavior.
|
Minor release. Headline: two long-requested **OpenAI-compat features** land — **multimodal vision** (`image_url` parts) and **structured outputs** (`response_format` / JSON schema). Also: the prompt-char budget now derives from the model SPOT instead of a hand-set constant, an agentic-turn bug that dropped the model's final answer is fixed, and `OCP_LOCAL_TOOLS` supports the OpenClaw-backend use case. Four of the six landed from external contributors (@vvlasy-openclaw). Every code PR carried a fresh-context reviewer (Iron Rule 10); no new endpoint, no new `cli.js` wire behavior.
|
||||||
|
|||||||
@@ -113,11 +113,11 @@ node setup.mjs
|
|||||||
|
|
||||||
`setup.mjs` verifies the Claude CLI, starts the proxy on port 3456, and installs auto-start (launchd on macOS, systemd on Linux). The `ocp` CLI lands at `~/ocp/ocp` — symlink it onto your PATH (`sudo ln -sf ~/ocp/ocp /usr/local/bin/ocp`, or `ln -sf ~/ocp/ocp ~/.local/bin/ocp`) or alias it (`alias ocp=~/ocp/ocp`); the rest of the docs assume `ocp` is on your PATH.
|
`setup.mjs` verifies the Claude CLI, starts the proxy on port 3456, and installs auto-start (launchd on macOS, systemd on Linux). The `ocp` CLI lands at `~/ocp/ocp` — symlink it onto your PATH (`sudo ln -sf ~/ocp/ocp /usr/local/bin/ocp`, or `ln -sf ~/ocp/ocp ~/.local/bin/ocp`) or alias it (`alias ocp=~/ocp/ocp`); the rest of the docs assume `ocp` is on your PATH.
|
||||||
|
|
||||||
**Verify** — should list 6 models:
|
**Verify** — should list 7 models:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://127.0.0.1:3456/v1/models
|
curl http://127.0.0.1:3456/v1/models
|
||||||
# claude-opus-4-8, claude-opus-4-7, claude-opus-4-6, claude-sonnet-5, claude-sonnet-4-6, claude-haiku-4-5-20251001
|
# claude-opus-5, claude-opus-4-8, claude-opus-4-7, claude-opus-4-6, claude-sonnet-5, claude-sonnet-4-6, claude-haiku-4-5-20251001
|
||||||
```
|
```
|
||||||
|
|
||||||
**Connect one IDE** — point any OpenAI-compatible tool at the proxy, then reload your shell and start a tool (Cline / Continue / Cursor / OpenCode):
|
**Connect one IDE** — point any OpenAI-compatible tool at the proxy, then reload your shell and start a tool (Cline / Continue / Cursor / OpenCode):
|
||||||
@@ -169,8 +169,9 @@ Any tool use happens server-side, under the `--allowedTools` set configured on t
|
|||||||
|
|
||||||
| Model ID | Notes |
|
| Model ID | Notes |
|
||||||
|----------|-------|
|
|----------|-------|
|
||||||
| `claude-opus-4-8` | Most capable (default for `opus` alias) |
|
| `claude-opus-5` | Most capable (default for `opus` alias) |
|
||||||
| `claude-opus-4-7` | Previous Opus, retained for pinning |
|
| `claude-opus-4-8` | Previous Opus, retained for pinning |
|
||||||
|
| `claude-opus-4-7` | Older Opus, retained for pinning |
|
||||||
| `claude-opus-4-6` | Older Opus, retained for pinning |
|
| `claude-opus-4-6` | Older Opus, retained for pinning |
|
||||||
| `claude-sonnet-5` | Latest Sonnet (default for `sonnet` alias) |
|
| `claude-sonnet-5` | Latest Sonnet (default for `sonnet` alias) |
|
||||||
| `claude-sonnet-4-6` | Previous Sonnet, retained for pinning |
|
| `claude-sonnet-4-6` | Previous Sonnet, retained for pinning |
|
||||||
|
|||||||
+6
-5
@@ -78,7 +78,7 @@ Run `ocp lan` to see your IP and ready-to-share instructions.
|
|||||||
**Verify:**
|
**Verify:**
|
||||||
```bash
|
```bash
|
||||||
curl http://127.0.0.1:3456/v1/models
|
curl http://127.0.0.1:3456/v1/models
|
||||||
# Returns: claude-opus-4-8, claude-opus-4-7, claude-opus-4-6, claude-sonnet-5, claude-sonnet-4-6, claude-haiku-4-5-20251001
|
# Returns: claude-opus-5, claude-opus-4-8, claude-opus-4-7, claude-opus-4-6, claude-sonnet-5, claude-sonnet-4-6, claude-haiku-4-5-20251001
|
||||||
```
|
```
|
||||||
|
|
||||||
### Headless install notes
|
### Headless install notes
|
||||||
@@ -114,7 +114,7 @@ Please follow https://github.com/dtzp555-max/ocp/blob/main/README.md
|
|||||||
installed and logged in (`claude auth status`). Install missing pieces
|
installed and logged in (`claude auth status`). Install missing pieces
|
||||||
using my system's package manager.
|
using my system's package manager.
|
||||||
2. git clone the repo, cd in, and run `node setup.mjs`.
|
2. git clone the repo, cd in, and run `node setup.mjs`.
|
||||||
3. Verify with `curl http://127.0.0.1:3456/v1/models` (should list 6 models).
|
3. Verify with `curl http://127.0.0.1:3456/v1/models` (should list 7 models).
|
||||||
4. Add `export OPENAI_BASE_URL=http://127.0.0.1:3456/v1` to my shell rc.
|
4. Add `export OPENAI_BASE_URL=http://127.0.0.1:3456/v1` to my shell rc.
|
||||||
5. Tell me to reload my shell and try a tool like Cline / Continue / Cursor.
|
5. Tell me to reload my shell and try a tool like Cline / Continue / Cursor.
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ Please follow https://github.com/dtzp555-max/ocp/blob/main/docs/lan-mode.md
|
|||||||
5. Add OCP_ADMIN_KEY to my shell rc (~/.zshrc or ~/.bashrc).
|
5. Add OCP_ADMIN_KEY to my shell rc (~/.zshrc or ~/.bashrc).
|
||||||
6. Run `ocp lan` to show me the LAN IP and connect command.
|
6. Run `ocp lan` to show me the LAN IP and connect command.
|
||||||
7. Optionally create example keys: `ocp keys add laptop`, `ocp keys add tablet`.
|
7. Optionally create example keys: `ocp keys add laptop`, `ocp keys add tablet`.
|
||||||
8. Verify: `curl http://127.0.0.1:3456/v1/models` returns 6 models.
|
8. Verify: `curl http://127.0.0.1:3456/v1/models` returns 7 models.
|
||||||
|
|
||||||
Tell me each step before running it. On error, diagnose before retrying.
|
Tell me each step before running it. On error, diagnose before retrying.
|
||||||
```
|
```
|
||||||
@@ -163,7 +163,7 @@ Please follow https://github.com/dtzp555-max/ocp/blob/main/docs/lan-mode.md
|
|||||||
chmod +x ocp-connect
|
chmod +x ocp-connect
|
||||||
2. Run `./ocp-connect <SERVER_IP>` (add `--key <KEY>` if you have one).
|
2. Run `./ocp-connect <SERVER_IP>` (add `--key <KEY>` if you have one).
|
||||||
3. Follow any IDE-specific manual hints it prints.
|
3. Follow any IDE-specific manual hints it prints.
|
||||||
4. Verify: `curl http://<SERVER_IP>:3456/v1/models` returns 6 models.
|
4. Verify: `curl http://<SERVER_IP>:3456/v1/models` returns 7 models.
|
||||||
5. Tell me to reload my shell + restart any IDE that was already running.
|
5. Tell me to reload my shell + restart any IDE that was already running.
|
||||||
|
|
||||||
Don't auto-retry on error. Tell me the failure mode first.
|
Don't auto-retry on error. Tell me the failure mode first.
|
||||||
@@ -216,7 +216,7 @@ OCP Connect v1.3.0
|
|||||||
(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)
|
||||||
|
|
||||||
Testing API access...
|
Testing API access...
|
||||||
✓ API accessible (6 models available)
|
✓ API accessible (7 models available)
|
||||||
|
|
||||||
Shell config:
|
Shell config:
|
||||||
✓ .bashrc
|
✓ .bashrc
|
||||||
@@ -246,6 +246,7 @@ OCP Connect v1.3.0
|
|||||||
✓ OpenClaw configured
|
✓ OpenClaw configured
|
||||||
Provider: ocp
|
Provider: ocp
|
||||||
Models:
|
Models:
|
||||||
|
• ocp/claude-opus-5
|
||||||
• ocp/claude-opus-4-8
|
• ocp/claude-opus-4-8
|
||||||
• ocp/claude-opus-4-7
|
• ocp/claude-opus-4-7
|
||||||
• ocp/claude-opus-4-6
|
• ocp/claude-opus-4-6
|
||||||
|
|||||||
+1
-1
@@ -65,7 +65,7 @@ Then restart OCP. At boot you will see (with the env token set, isolated home au
|
|||||||
- **Callers see no API change.** The response is a normal OpenAI completion object or chunked SSE — identical wire format.
|
- **Callers see no API change.** The response is a normal OpenAI completion object or chunked SSE — identical wire format.
|
||||||
- **Real streaming is opt-in (`OCP_TUI_STREAM=1`), and off by default.** By default TUI-mode buffers the full response and replays it as chunked SSE — you see a delay, then the complete response. Set `OCP_TUI_STREAM=1` and `stream:true` turns emit real SSE `delta.content` chunks as `claude` renders them, sourced from `claude`'s own `MessageDisplay` hook (byte-faithful raw markdown, on the subscription pool, no `-p`). Two honest caveats: granularity is **block-level** — the hook fires once per rendered block, so a handful of chunks per answer, scaling with length, not token-by-token; and it moves the **first** byte, not the last, so a consumer that must parse a complete reply gains nothing. The transcript stays authoritative: every streamed turn is asserted against it at the end, and a turn whose stream disagrees is **failed rather than served** (watch `tui.streamDivergences` on `/health`). Evidence: [`plans/2026-07-13-tui-latency/streaming-spike.md`](plans/2026-07-13-tui-latency/streaming-spike.md).
|
- **Real streaming is opt-in (`OCP_TUI_STREAM=1`), and off by default.** By default TUI-mode buffers the full response and replays it as chunked SSE — you see a delay, then the complete response. Set `OCP_TUI_STREAM=1` and `stream:true` turns emit real SSE `delta.content` chunks as `claude` renders them, sourced from `claude`'s own `MessageDisplay` hook (byte-faithful raw markdown, on the subscription pool, no `-p`). Two honest caveats: granularity is **block-level** — the hook fires once per rendered block, so a handful of chunks per answer, scaling with length, not token-by-token; and it moves the **first** byte, not the last, so a consumer that must parse a complete reply gains nothing. The transcript stays authoritative: every streamed turn is asserted against it at the end, and a turn whose stream disagrees is **failed rather than served** (watch `tui.streamDivergences` on `/health`). Evidence: [`plans/2026-07-13-tui-latency/streaming-spike.md`](plans/2026-07-13-tui-latency/streaming-spike.md).
|
||||||
- **Cache and singleflight work normally.** TUI-mode writes the buffered response to the cache on success; cache-hits skip the interactive turn entirely.
|
- **Cache and singleflight work normally.** TUI-mode writes the buffered response to the cache on success; cache-hits skip the interactive turn entirely.
|
||||||
- **The host's `CLAUDE.md` / auto-memory is never injected.** OCP is a proxy — the proxied client (OpenClaw / your IDE) owns its own context and memory. TUI-mode always runs `claude` with `CLAUDE_CODE_DISABLE_CLAUDE_MDS` + `CLAUDE_CODE_DISABLE_AUTO_MEMORY`, so a `CLAUDE.md` on the OCP host can never leak into proxied turns (verified live; see #4). Built-in tool schemas + the interactive system prompt remain (the inherent ~20–35K context floor of interactive mode); MCP is hard-disabled.
|
- **The host's `CLAUDE.md` / auto-memory is never injected.** OCP is a proxy — the proxied client (OpenClaw / your IDE) owns its own context and memory. TUI-mode runs `claude` with `--safe-mode`, which disables all host customizations (CLAUDE.md, skills, plugins, hooks, MCP servers) while leaving auth, model selection, built-in tools, and permissions untouched — so a `CLAUDE.md` on the OCP host can never leak into a proxied turn and steer the answer, and the session still bills the subscription pool (`cc_entrypoint=cli`, unlike `--bare`). The `CLAUDE_CODE_DISABLE_CLAUDE_MDS` / `CLAUDE_CODE_DISABLE_AUTO_MEMORY` env vars are kept as a fallback (see #4). Built-in tool schemas + the interactive system prompt remain (the inherent ~20–35K context floor of interactive mode); MCP is hard-disabled. **Exception:** the two opt-in modes that deliberately need a customization `--safe-mode` would strip — real streaming (`OCP_TUI_STREAM`, which registers a `MessageDisplay` **hook** via `--settings`) and `OCP_TUI_FULL_TOOLS` (which grants an **MCP** / skills surface) — run without `--safe-mode` and rely on the env-var suppression alone.
|
||||||
- **Authenticate via `CLAUDE_CODE_OAUTH_TOKEN` in a credential-isolated home (recommended).** tmux does not forward the parent process's env to the pane, so OCP sets the token explicitly on the spawned `claude` when `CLAUDE_CODE_OAUTH_TOKEN` is present. With the env token set and `OCP_TUI_HOME` unset, OCP runs claude in a **credential-isolated home** (`$HOME/.ocp-tui/home`) that has **no `credentials.json`** — so the env token is the only credential and is authoritative, and claude never runs the token-refresh path. This both stops a stale `credentials.json` from shadowing the token and ends the refresh-token corruption behind the permanent `Please run /login · API Error: 401` (full two-layer root cause, live proof, and fix in [Troubleshooting § the permanent TUI-mode 401](troubleshooting.md#tui-401)). Transcripts land under the same isolated home, so the answer-reader is unaffected. Without the env token, claude falls back to the real home's `credentials.json` (byte-for-byte the previous behaviour). (The token is visible in `ps` on the pane command — acceptable for the single-user A-path; the multi-user B-path is refused at boot.) See ADR 0007 PR-C / PR-D amendments.
|
- **Authenticate via `CLAUDE_CODE_OAUTH_TOKEN` in a credential-isolated home (recommended).** tmux does not forward the parent process's env to the pane, so OCP sets the token explicitly on the spawned `claude` when `CLAUDE_CODE_OAUTH_TOKEN` is present. With the env token set and `OCP_TUI_HOME` unset, OCP runs claude in a **credential-isolated home** (`$HOME/.ocp-tui/home`) that has **no `credentials.json`** — so the env token is the only credential and is authoritative, and claude never runs the token-refresh path. This both stops a stale `credentials.json` from shadowing the token and ends the refresh-token corruption behind the permanent `Please run /login · API Error: 401` (full two-layer root cause, live proof, and fix in [Troubleshooting § the permanent TUI-mode 401](troubleshooting.md#tui-401)). Transcripts land under the same isolated home, so the answer-reader is unaffected. Without the env token, claude falls back to the real home's `credentials.json` (byte-for-byte the previous behaviour). (The token is visible in `ps` on the pane command — acceptable for the single-user A-path; the multi-user B-path is refused at boot.) See ADR 0007 PR-C / PR-D amendments.
|
||||||
- **Stale tmux sessions are reaped.** The pane's `claude` is a child of the tmux server (not OCP), so OCP cannot reap it directly; `claude` zombies can otherwise accumulate as `<defunct>` over a long-running host. OCP reaps them at boot and on a 15-min idle sweep by issuing `tmux kill-server` — but **only when no foreign tmux session remains** (it never disrupts a co-hosted `olp-tui-*` instance). See ADR 0007 PR-C amendment.
|
- **Stale tmux sessions are reaped.** The pane's `claude` is a child of the tmux server (not OCP), so OCP cannot reap it directly; `claude` zombies can otherwise accumulate as `<defunct>` over a long-running host. OCP reaps them at boot and on a 15-min idle sweep by issuing `tmux kill-server` — but **only when no foreign tmux session remains** (it never disrupts a co-hosted `olp-tui-*` instance). See ADR 0007 PR-C amendment.
|
||||||
- **Default path unchanged.** Unset `CLAUDE_TUI_MODE` and restart → `callClaude` / `callClaudeStreaming` are used again, byte-for-byte identical to today.
|
- **Default path unchanged.** Unset `CLAUDE_TUI_MODE` and restart → `callClaude` / `callClaudeStreaming` are used again, byte-for-byte identical to today.
|
||||||
|
|||||||
+41
-6
@@ -182,8 +182,13 @@ function tuiCapturePane(tmux, tmuxName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// True once claude's input bar is rendered and ready for keystrokes.
|
// True once claude's input bar is rendered and ready for keystrokes.
|
||||||
|
// Two known ready-state footers across claude versions: the classic `? for shortcuts`
|
||||||
|
// hint, and the `shift+tab to cycle` hint (part of "⏵⏵ bypass permissions on (shift+tab
|
||||||
|
// to cycle)") that newer claude 2.1.x renders in its place. Match EITHER — a matcher that
|
||||||
|
// only knew the classic string silently reported "never ready" on those builds, timing out
|
||||||
|
// every boot (tui_pane_not_ready) and, with the warm pool on, failing every pre-boot.
|
||||||
function tuiInputReady(pane) {
|
function tuiInputReady(pane) {
|
||||||
return /\? for shortcuts/.test(pane);
|
return /\? for shortcuts|shift\+tab to cycle/.test(pane);
|
||||||
}
|
}
|
||||||
|
|
||||||
// True once the pasted prompt has POSITIVELY landed in the input box. We only trust
|
// True once the pasted prompt has POSITIVELY landed in the input box. We only trust
|
||||||
@@ -384,9 +389,15 @@ export function buildTuiCmd(claudeBin, model, sessionId, ehome, entrypointMode,
|
|||||||
// CLAUDE.md and auto-memory must NEVER leak into the agent OCP runs on the user's behalf.
|
// CLAUDE.md and auto-memory must NEVER leak into the agent OCP runs on the user's behalf.
|
||||||
// Without these, claude loads the host's project/user CLAUDE.md + memory into every proxied
|
// Without these, claude loads the host's project/user CLAUDE.md + memory into every proxied
|
||||||
// turn — verified live 2026-06-02: a cwd CLAUDE.md ("end every reply with QUACKMARKER_42") was
|
// turn — verified live 2026-06-02: a cwd CLAUDE.md ("end every reply with QUACKMARKER_42") was
|
||||||
// obeyed by the proxied turn until these flags were set, after which it was not. Unconditional
|
// obeyed by the proxied turn until these flags were set, after which it was not. Mirrors the
|
||||||
// by design (not gated): proxy purity is not an opt-in. Harmless on hosts with no CLAUDE.md
|
// -p path's CLAUDE_NO_CONTEXT vars. Harmless on hosts with no CLAUDE.md (they suppress nothing).
|
||||||
// (the common case — they suppress nothing). Mirrors the -p path's CLAUDE_NO_CONTEXT vars.
|
//
|
||||||
|
// These env vars stopped being sufficient on newer claude: they are present in the pane's
|
||||||
|
// environment yet the host CLAUDE.md is still injected into the turn's context, so a proxied
|
||||||
|
// turn again obeys the operator's private CLAUDE.md instead of the caller's prompt — a leak of
|
||||||
|
// the operator's private context into API responses. The robust suppression is the --safe-mode
|
||||||
|
// flag added to the argv below; these env vars are kept as belt-and-braces for the two argv
|
||||||
|
// paths that cannot take --safe-mode (see the safeModeArgs note near the return).
|
||||||
const sets = [
|
const sets = [
|
||||||
`HOME=${shq(ehome)}`,
|
`HOME=${shq(ehome)}`,
|
||||||
"DISABLE_AUTOUPDATER=1",
|
"DISABLE_AUTOUPDATER=1",
|
||||||
@@ -477,9 +488,32 @@ export function buildTuiCmd(claudeBin, model, sessionId, ehome, entrypointMode,
|
|||||||
// so the OFF argv is byte-for-byte the pre-streaming argv.
|
// so the OFF argv is byte-for-byte the pre-streaming argv.
|
||||||
const settingsArgs = stream && stream.settings ? ["--settings", shq(stream.settings)] : [];
|
const settingsArgs = stream && stream.settings ? ["--settings", shq(stream.settings)] : [];
|
||||||
|
|
||||||
|
// --safe-mode: disable ALL customizations (host CLAUDE.md, skills, plugins, hooks, MCP
|
||||||
|
// servers, custom commands/agents, output styles, ...) while leaving auth, model selection,
|
||||||
|
// built-in tools, and permissions untouched (claude 2.1.216 --help). This is the robust
|
||||||
|
// replacement for the CLAUDE_CODE_DISABLE_CLAUDE_MDS / _AUTO_MEMORY env vars above, which no
|
||||||
|
// longer suppress the host CLAUDE.md on newer claude — so the operator's private CLAUDE.md
|
||||||
|
// could otherwise leak into a proxied API response. Unlike --bare, --safe-mode does NOT drop
|
||||||
|
// the interactive session off the subscription pool (it keeps auth + model selection), so the
|
||||||
|
// billing classifier stays cc_entrypoint=cli.
|
||||||
|
//
|
||||||
|
// NOT applied when the pane deliberately relies on a customization --safe-mode would strip,
|
||||||
|
// because those would break SILENTLY:
|
||||||
|
// - streaming (settingsArgs present): the MessageDisplay hook is a HOOK, which --safe-mode
|
||||||
|
// disables — every streamed turn would then fire zero deltas (tui.streamZeroDeltaTurns)
|
||||||
|
// and fall back to buffered, defeating OCP_TUI_STREAM.
|
||||||
|
// - OCP_TUI_FULL_TOOLS=1: grants an MCP / skills / agent surface (--mcp-config, ...) that
|
||||||
|
// --safe-mode disables wholesale.
|
||||||
|
// Those two opt-in paths keep the env-var suppression above as their best-effort fallback.
|
||||||
|
const safeModeArgs =
|
||||||
|
settingsArgs.length === 0 && process.env.OCP_TUI_FULL_TOOLS !== "1"
|
||||||
|
? ["--safe-mode"]
|
||||||
|
: [];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
envPrefix,
|
envPrefix,
|
||||||
shq(claudeBin),
|
shq(claudeBin),
|
||||||
|
...safeModeArgs,
|
||||||
"--model", shq(model),
|
"--model", shq(model),
|
||||||
"--session-id", sessionId,
|
"--session-id", sessionId,
|
||||||
...toolArgs,
|
...toolArgs,
|
||||||
@@ -615,8 +649,9 @@ export async function bootTuiPane({
|
|||||||
// A pooled pane is SINGLE-USE: it already carries its own fresh --session-id, it
|
// A pooled pane is SINGLE-USE: it already carries its own fresh --session-id, it
|
||||||
// serves this one turn, and it is killed in the finally like any other pane.
|
// serves this one turn, and it is killed in the finally like any other pane.
|
||||||
// 2. On a MISS: pre-trust the scratch cwd, boot an interactive `claude` in a fresh tmux
|
// 2. On a MISS: pre-trust the scratch cwd, boot an interactive `claude` in a fresh tmux
|
||||||
// session in the scratch cwd, poll capture-pane until the `? for shortcuts` input bar
|
// session in the scratch cwd, poll capture-pane until the input bar appears — see
|
||||||
// appears (bootTuiPane). BOOT_MS is the max wait, not a fixed delay.
|
// tuiInputReady for the ready-state footers matched (bootTuiPane). BOOT_MS is the max
|
||||||
|
// wait, not a fixed delay.
|
||||||
// 3. Write prompt to a 0600 temp file (no shell injection from prompt content).
|
// 3. Write prompt to a 0600 temp file (no shell injection from prompt content).
|
||||||
// 4. Paste the prompt via tmux load-buffer + paste-buffer -p (bracketed paste) —
|
// 4. Paste the prompt via tmux load-buffer + paste-buffer -p (bracketed paste) —
|
||||||
// reliable for large multi-line prompts where send-keys -l is not (issue #130).
|
// reliable for large multi-line prompts where send-keys -l is not (issue #130).
|
||||||
|
|||||||
+9
-1
@@ -2,6 +2,14 @@
|
|||||||
"$schema": "./models.schema.json",
|
"$schema": "./models.schema.json",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"models": [
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "claude-opus-5",
|
||||||
|
"displayName": "Claude Opus 5",
|
||||||
|
"openclawName": "Claude Opus 5 (via CLI)",
|
||||||
|
"reasoning": true,
|
||||||
|
"contextWindow": 200000,
|
||||||
|
"maxTokens": 16384
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "claude-opus-4-8",
|
"id": "claude-opus-4-8",
|
||||||
"displayName": "Claude Opus 4.8",
|
"displayName": "Claude Opus 4.8",
|
||||||
@@ -52,7 +60,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"opus": "claude-opus-4-8",
|
"opus": "claude-opus-5",
|
||||||
"sonnet": "claude-sonnet-5",
|
"sonnet": "claude-sonnet-5",
|
||||||
"haiku": "claude-haiku-4-5-20251001"
|
"haiku": "claude-haiku-4-5-20251001"
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -204,7 +204,7 @@ const OCP_SYSTEM_PROMPT_WRAPPER = `You are accessed via the OCP HTTP proxy. You
|
|||||||
// default wrapper above is byte-for-byte unchanged. Selecting the positive wrapper does NOT expand
|
// default wrapper above is byte-for-byte unchanged. Selecting the positive wrapper does NOT expand
|
||||||
// the tool surface (governed independently by --allowedTools/--disallowedTools) — it only changes the
|
// the tool surface (governed independently by --allowedTools/--disallowedTools) — it only changes the
|
||||||
// prompt — and is boot-gated below (multi/non-loopback/anon → refuse) mirroring OCP_TUI_FULL_TOOLS.
|
// prompt — and is boot-gated below (multi/non-loopback/anon → refuse) mirroring OCP_TUI_FULL_TOOLS.
|
||||||
const OCP_LOCAL_TOOLS_WRAPPER = `You are accessed via the OCP HTTP proxy running on the operator's own machine. You have full access to the local filesystem, working directory, and shell through your available tools (Bash, Read, Write, Edit, Glob, Grep, etc.). Use them as needed to complete the operator's requests.`;
|
const OCP_LOCAL_TOOLS_WRAPPER = `You are accessed via the OCP HTTP proxy running on the operator's own machine. Unlike the shared-gateway posture, you may use your available local tools to act on the operator's machine as the task requires. Use only the tools you actually have — do not assume filesystem, shell, or other access beyond the tool set provided to you in this session.`;
|
||||||
|
|
||||||
// OCP_LOCAL_TOOLS is inert in TUI mode: the interactive (non-`-p`) path composes its own prompt via
|
// OCP_LOCAL_TOOLS is inert in TUI mode: the interactive (non-`-p`) path composes its own prompt via
|
||||||
// callClaudeTui/messagesToPrompt and never calls extractSystemPrompt, so the wrapper is only ever
|
// callClaudeTui/messagesToPrompt and never calls extractSystemPrompt, so the wrapper is only ever
|
||||||
|
|||||||
+62
-5
@@ -910,7 +910,7 @@ test("appendOperatorPrompt: operator value is trimmed before appending", () => {
|
|||||||
console.log("\nOCP_LOCAL_TOOLS wrapper + safety gate:");
|
console.log("\nOCP_LOCAL_TOOLS wrapper + safety gate:");
|
||||||
|
|
||||||
const NEG = "You do NOT have access to any local filesystem";
|
const NEG = "You do NOT have access to any local filesystem";
|
||||||
const POS = "You have full access to the local filesystem";
|
const POS = "you may use your available local tools";
|
||||||
|
|
||||||
test("selectPromptWrapper: default (disabled) returns the negative wrapper BYTE-IDENTICAL", () => {
|
test("selectPromptWrapper: default (disabled) returns the negative wrapper BYTE-IDENTICAL", () => {
|
||||||
// Mutation-proof: flip the ternary and the default path leaks the positive wrapper.
|
// Mutation-proof: flip the ternary and the default path leaks the positive wrapper.
|
||||||
@@ -967,7 +967,7 @@ import { fileURLToPath as _ltF2P } from "node:url";
|
|||||||
const LT_SERVER = _ltF2P(new URL("./server.mjs", import.meta.url));
|
const LT_SERVER = _ltF2P(new URL("./server.mjs", import.meta.url));
|
||||||
const LT_POSIX = process.platform !== "win32"; // fake is a /bin/sh script; CI is POSIX
|
const LT_POSIX = process.platform !== "win32"; // fake is a /bin/sh script; CI is POSIX
|
||||||
const LT_NEG_MARK = "You do NOT have access to any local filesystem";
|
const LT_NEG_MARK = "You do NOT have access to any local filesystem";
|
||||||
const LT_POS_MARK = "You have full access to the local filesystem";
|
const LT_POS_MARK = "you may use your available local tools";
|
||||||
// Fake claude: record the --system-prompt it was spawned with, bump an optional spawn counter,
|
// Fake claude: record the --system-prompt it was spawned with, bump an optional spawn counter,
|
||||||
// then emit a minimal valid stream-json response so the request completes (and caches).
|
// then emit a minimal valid stream-json response so the request completes (and caches).
|
||||||
const LT_FAKE = `#!/bin/sh
|
const LT_FAKE = `#!/bin/sh
|
||||||
@@ -2209,10 +2209,32 @@ console.log("\nTUI command construction (proxy-purity / #4):");
|
|||||||
test("buildTuiCmd suppresses host CLAUDE.md + auto-memory (proxy purity, #4)", () => {
|
test("buildTuiCmd suppresses host CLAUDE.md + auto-memory (proxy purity, #4)", () => {
|
||||||
const cmd = buildTuiCmd("/usr/bin/claude", "claude-haiku", "sid-1", "/home/u", "cli");
|
const cmd = buildTuiCmd("/usr/bin/claude", "claude-haiku", "sid-1", "/home/u", "cli");
|
||||||
// OCP is a proxy: the host's CLAUDE.md / auto-memory must never leak into the proxied turn.
|
// OCP is a proxy: the host's CLAUDE.md / auto-memory must never leak into the proxied turn.
|
||||||
|
// Primary mechanism is --safe-mode (env vars alone stopped suppressing on newer claude);
|
||||||
|
// the env vars remain as belt-and-braces.
|
||||||
|
assert.ok(/(^| )--safe-mode( |$)/.test(cmd), "default pane must pass --safe-mode (disables host CLAUDE.md/skills/plugins/hooks)");
|
||||||
assert.ok(/(^| )CLAUDE_CODE_DISABLE_CLAUDE_MDS=1( |$)/.test(cmd), "must disable CLAUDE.md injection");
|
assert.ok(/(^| )CLAUDE_CODE_DISABLE_CLAUDE_MDS=1( |$)/.test(cmd), "must disable CLAUDE.md injection");
|
||||||
assert.ok(/(^| )CLAUDE_CODE_DISABLE_AUTO_MEMORY=1( |$)/.test(cmd), "must disable auto-memory injection");
|
assert.ok(/(^| )CLAUDE_CODE_DISABLE_AUTO_MEMORY=1( |$)/.test(cmd), "must disable auto-memory injection");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("buildTuiCmd omits --safe-mode when a customization it would strip is in use", () => {
|
||||||
|
const save = process.env.OCP_TUI_FULL_TOOLS;
|
||||||
|
try {
|
||||||
|
delete process.env.OCP_TUI_FULL_TOOLS;
|
||||||
|
// streaming registers a MessageDisplay HOOK via --settings; --safe-mode would kill the hook
|
||||||
|
// (zero deltas), so it must be omitted on the streaming pane.
|
||||||
|
const streaming = buildTuiCmd("/usr/bin/claude", "m", "sid-s", "/home/u", "cli", { file: "/d/sid-s.jsonl", settings: "/d/s.json" });
|
||||||
|
assert.ok(!/--safe-mode/.test(streaming), "streaming pane must NOT pass --safe-mode (would disable the MessageDisplay hook)");
|
||||||
|
assert.ok(streaming.includes("--settings '/d/s.json'"), "streaming pane keeps its --settings hook");
|
||||||
|
|
||||||
|
// OCP_TUI_FULL_TOOLS grants an MCP/skills surface --safe-mode disables wholesale.
|
||||||
|
process.env.OCP_TUI_FULL_TOOLS = "1";
|
||||||
|
const full = buildTuiCmd("/usr/bin/claude", "m", "sid-f", "/home/u", "cli");
|
||||||
|
assert.ok(!/--safe-mode/.test(full), "full-tools pane must NOT pass --safe-mode (would disable MCP/skills)");
|
||||||
|
} finally {
|
||||||
|
if (save === undefined) delete process.env.OCP_TUI_FULL_TOOLS; else process.env.OCP_TUI_FULL_TOOLS = save;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("buildTuiCmd keeps version pin + entrypoint label + MCP wall", () => {
|
test("buildTuiCmd keeps version pin + entrypoint label + MCP wall", () => {
|
||||||
const cli = buildTuiCmd("/usr/bin/claude", "m", "sid-2", "/home/u", "cli");
|
const cli = buildTuiCmd("/usr/bin/claude", "m", "sid-2", "/home/u", "cli");
|
||||||
assert.ok(cli.includes("DISABLE_AUTOUPDATER=1"), "version pin retained");
|
assert.ok(cli.includes("DISABLE_AUTOUPDATER=1"), "version pin retained");
|
||||||
@@ -3504,7 +3526,7 @@ if (process.env.OCP_TUI_LIVE === "1") {
|
|||||||
// Replicates tuiInputReady, tuiPromptLanded verbatim from lib/tui/session.mjs.
|
// Replicates tuiInputReady, tuiPromptLanded verbatim from lib/tui/session.mjs.
|
||||||
// Keep in sync with the definitions there.
|
// Keep in sync with the definitions there.
|
||||||
function _tuiInputReady(pane) {
|
function _tuiInputReady(pane) {
|
||||||
return /\? for shortcuts/.test(pane);
|
return /\? for shortcuts|shift\+tab to cycle/.test(pane);
|
||||||
}
|
}
|
||||||
function _tuiPromptLanded(pane, prompt) {
|
function _tuiPromptLanded(pane, prompt) {
|
||||||
const flatPane = pane.replace(/\s+/g, " ");
|
const flatPane = pane.replace(/\s+/g, " ");
|
||||||
@@ -3522,7 +3544,12 @@ const TUI_READY_PANE = `❯ Try "how does <filepath> work?"
|
|||||||
const TUI_LANDED_PANE = `❯ Reply with exactly: PONG_TEST
|
const TUI_LANDED_PANE = `❯ Reply with exactly: PONG_TEST
|
||||||
? for shortcuts · ← for agents`;
|
? for shortcuts · ← for agents`;
|
||||||
|
|
||||||
// Welcome splash shown before input bar is rendered — no `? for shortcuts`.
|
// Newer claude 2.1.x renders the input bar with a `shift+tab to cycle` footer instead of
|
||||||
|
// `? for shortcuts` — the matcher must accept it too, or the pane reads as never-ready.
|
||||||
|
const TUI_READY_PANE_SHIFT_TAB = `❯ Try "how does <filepath> work?"
|
||||||
|
⏵⏵ bypass permissions on (shift+tab to cycle)`;
|
||||||
|
|
||||||
|
// Welcome splash shown before input bar is rendered — neither ready-state footer.
|
||||||
const TUI_BOOT_PANE = `╭─ Claude Code v2.1.114 ─ Welcome back Tao! ─╮\n│ Tips for getting started │`;
|
const TUI_BOOT_PANE = `╭─ Claude Code v2.1.114 ─ Welcome back Tao! ─╮\n│ Tips for getting started │`;
|
||||||
|
|
||||||
console.log("\nTUI readiness + paste-verify predicates (issue #130):");
|
console.log("\nTUI readiness + paste-verify predicates (issue #130):");
|
||||||
@@ -3533,6 +3560,9 @@ test("tuiInputReady(READY_PANE) === true (input bar rendered)", () => {
|
|||||||
test("tuiInputReady(LANDED_PANE) === true (input bar still present after paste)", () => {
|
test("tuiInputReady(LANDED_PANE) === true (input bar still present after paste)", () => {
|
||||||
assert.equal(_tuiInputReady(TUI_LANDED_PANE), true);
|
assert.equal(_tuiInputReady(TUI_LANDED_PANE), true);
|
||||||
});
|
});
|
||||||
|
test("tuiInputReady(READY_PANE_SHIFT_TAB) === true (newer claude `shift+tab to cycle` footer)", () => {
|
||||||
|
assert.equal(_tuiInputReady(TUI_READY_PANE_SHIFT_TAB), true);
|
||||||
|
});
|
||||||
test("tuiInputReady(BOOT_PANE) === false (welcome splash, no input bar yet)", () => {
|
test("tuiInputReady(BOOT_PANE) === false (welcome splash, no input bar yet)", () => {
|
||||||
assert.equal(_tuiInputReady(TUI_BOOT_PANE), false);
|
assert.equal(_tuiInputReady(TUI_BOOT_PANE), false);
|
||||||
});
|
});
|
||||||
@@ -4037,6 +4067,10 @@ test("models.json aliases.sonnet === 'claude-sonnet-5' (default-request-model SP
|
|||||||
assert.equal(_spotModels.aliases.sonnet, "claude-sonnet-5");
|
assert.equal(_spotModels.aliases.sonnet, "claude-sonnet-5");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("models.json aliases.opus === 'claude-opus-5' (opus-alias SPOT)", () => {
|
||||||
|
assert.equal(_spotModels.aliases.opus, "claude-opus-5");
|
||||||
|
});
|
||||||
|
|
||||||
// ── Referential integrity (PR #152 review) ──────────────────────────────────
|
// ── Referential integrity (PR #152 review) ──────────────────────────────────
|
||||||
// The value-mirror assertions above only prove the alias equals a string literal —
|
// The value-mirror assertions above only prove the alias equals a string literal —
|
||||||
// they pass even if that literal points at a model that does not exist in
|
// they pass even if that literal points at a model that does not exist in
|
||||||
@@ -4050,6 +4084,25 @@ test("models.json: claude-sonnet-5 is present in models[] (the entry this PR add
|
|||||||
assert.ok(_spotModelIds.has("claude-sonnet-5"), "claude-sonnet-5 must exist as a models[].id");
|
assert.ok(_spotModelIds.has("claude-sonnet-5"), "claude-sonnet-5 must exist as a models[].id");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("models.json: claude-opus-5 is present in models[] (the entry this PR adds)", () => {
|
||||||
|
assert.ok(_spotModelIds.has("claude-opus-5"), "claude-opus-5 must exist as a models[].id");
|
||||||
|
});
|
||||||
|
|
||||||
|
// The prompt-char budget is GLOBAL (max across every entry × 3 chars/token), not
|
||||||
|
// per-model — see lib/prompt.mjs derivePromptCharBudget. An entry declaring a native 1M
|
||||||
|
// window would therefore raise the truncation ceiling for claude-haiku-4-5 too (genuinely
|
||||||
|
// 200k), turning OCP-side truncation into an upstream API rejection.
|
||||||
|
//
|
||||||
|
// Asserts the MAX, deliberately, not every entry: ADR 0009 states the budget "scales
|
||||||
|
// automatically — no code change", so a future entry with a SMALLER window (say a 128k
|
||||||
|
// model) must stay legal and must not fail this suite. Only raising the ceiling is the
|
||||||
|
// hazard, and that is an ADR-level decision requiring per-model budgets first.
|
||||||
|
test("models.json: max contextWindow is 200000 (global prompt-budget ceiling)", () => {
|
||||||
|
const windows = _spotModels.models.map(m => m.contextWindow);
|
||||||
|
assert.equal(Math.max(...windows), 200000,
|
||||||
|
`max contextWindow re-scales MAX_PROMPT_CHARS for ALL models incl. the 200k-native haiku (see lib/prompt.mjs + ADR 0009)`);
|
||||||
|
});
|
||||||
|
|
||||||
test("models.json: every aliases value resolves to a real models[].id (referential integrity)", () => {
|
test("models.json: every aliases value resolves to a real models[].id (referential integrity)", () => {
|
||||||
for (const [name, target] of Object.entries(_spotModels.aliases)) {
|
for (const [name, target] of Object.entries(_spotModels.aliases)) {
|
||||||
assert.ok(_spotModelIds.has(target), `aliases.${name} -> '${target}' is a dangling alias (no matching models[].id)`);
|
assert.ok(_spotModelIds.has(target), `aliases.${name} -> '${target}' is a dangling alias (no matching models[].id)`);
|
||||||
@@ -4523,13 +4576,17 @@ test("stream: sink path is keyed by session_id (concurrent panes cannot interlea
|
|||||||
assert.ok(A.endsWith("/aaaa-1111.jsonl"));
|
assert.ok(A.endsWith("/aaaa-1111.jsonl"));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("stream: buildTuiCmd — OFF is byte-for-byte the pre-streaming argv; ON adds only env + --settings", () => {
|
test("stream: buildTuiCmd — streaming ON adds env + --settings and drops --safe-mode (hook survives)", () => {
|
||||||
const off = buildTuiCmd("/bin/claude", "m", "SID", "/h", "cli");
|
const off = buildTuiCmd("/bin/claude", "m", "SID", "/h", "cli");
|
||||||
assert.ok(!off.includes("--settings"), "no --settings when streaming is off");
|
assert.ok(!off.includes("--settings"), "no --settings when streaming is off");
|
||||||
assert.ok(!off.includes("OCP_TUI_STREAM_FILE"), "no sink env when streaming is off");
|
assert.ok(!off.includes("OCP_TUI_STREAM_FILE"), "no sink env when streaming is off");
|
||||||
|
assert.ok(off.includes("--safe-mode"), "the non-streaming pane carries --safe-mode");
|
||||||
const on = buildTuiCmd("/bin/claude", "m", "SID", "/h", "cli", { file: "/d/SID.jsonl", settings: "/d/s.json" });
|
const on = buildTuiCmd("/bin/claude", "m", "SID", "/h", "cli", { file: "/d/SID.jsonl", settings: "/d/s.json" });
|
||||||
assert.ok(on.includes("OCP_TUI_STREAM_FILE='/d/SID.jsonl'"), "sink delivered via the pane env");
|
assert.ok(on.includes("OCP_TUI_STREAM_FILE='/d/SID.jsonl'"), "sink delivered via the pane env");
|
||||||
assert.ok(on.includes("--settings '/d/s.json'"));
|
assert.ok(on.includes("--settings '/d/s.json'"));
|
||||||
|
// --safe-mode would disable the MessageDisplay hook registered by --settings, so the
|
||||||
|
// streaming pane must NOT carry it (it keeps the env-var suppression instead).
|
||||||
|
assert.ok(!on.includes("--safe-mode"), "streaming pane omits --safe-mode so the hook fires");
|
||||||
// must not regress the MCP wall or the pinned effort (#156)
|
// must not regress the MCP wall or the pinned effort (#156)
|
||||||
assert.ok(on.includes("--strict-mcp-config") && on.includes("--disallowedTools 'mcp__*'"), "MCP wall intact");
|
assert.ok(on.includes("--strict-mcp-config") && on.includes("--disallowedTools 'mcp__*'"), "MCP wall intact");
|
||||||
assert.ok(on.includes("--effort low"), "OCP_TUI_EFFORT default intact");
|
assert.ok(on.includes("--effort low"), "OCP_TUI_EFFORT default intact");
|
||||||
|
|||||||
Reference in New Issue
Block a user