feat(server): OCP_LOCAL_TOOLS — positive local-tools system-prompt wrapper (single-user, default off) (#182)

Motivation (the OpenClaw case). OCP's `-p` path prepends OCP_SYSTEM_PROMPT_WRAPPER,
which tells the model it has NO local filesystem/shell/env access. Correct for a
shared/multi-tenant gateway. But an OpenClaw agent pointed at its own local OCP runs
the model SERVER-SIDE via `claude -p`, which already passes --allowedTools and has the
CLI's built-in tools — and on a loopback instance the OCP host IS the operator's
machine, so those are local tools. The wrapper gags them: the agent replies "I don't
have filesystem access" for tools it actually holds. OCP_LOCAL_TOOLS=1 swaps in a
positive wrapper for that case. (It does NOT enable client-side tool_calls for
OpenClaw/Cline — that remains unsupported by design; OCP is a text-prompt bridge.)

Safety: changes ONLY the system-prompt text, never the tool surface. Tools are governed
solely by --allowedTools/--disallowedTools; AUTH_MODE=multi still --disallowedTools the
whole FS/web/agent surface regardless of the wrapper. Fail-closed boot gate mirroring
OCP_TUI_FULL_TOOLS (ADR 0007): refuse to start when =1 is combined with
CLAUDE_AUTH_MODE=multi, a non-loopback bind, or PROXY_ANONYMOUS_KEY.

Scope/alignment: no new endpoint/header/field/wire operation. The wrapper text is
OCP-owned prompt composition (same class as OCP_SYSTEM_PROMPT_WRAPPER and
CLAUDE_SYSTEM_PROMPT), passed via the already-cited `claude --system-prompt` flag
(unchanged). ALIGNMENT.md Rule 2: nothing invented on the wire.

- lib/prompt.mjs: pure selectPromptWrapper() + localToolsSafetyError() (unit-tested).
- server.mjs: single hoisted flag LOCAL_TOOLS_ACTIVE = OCP_LOCAL_TOOLS && !TUI_MODE
  (the wrapper is only applied on the -p path; TUI composes its own prompt, so the flag
  is inert under TUI — announced with a warning rather than a misleading "ON"). Wrapper
  selection, boot gate, and the CONFIG_EPOCH fold all key off it, so toggling the flag +
  restarting invalidates the standard response cache (#177). Default path byte-for-byte
  unchanged.
- README env-var row + tool-model note; CHANGELOG Unreleased entry.

Tests (+14): unit-test both ternary branches and every gate condition; plus an
INTEGRATION harness that boots real server.mjs with a fake `claude` capturing the
--system-prompt — asserting the POSITIVE wrapper reaches a request under =1 and the
EXACT negative wrapper when unset, the boot gate refuses all three unsafe configs, the
safe config boots, TUI announces inert, and toggling the flag re-spawns (cache
invalidated). Mutation-verified: reverting the wiring / neutering the gate / reverting
the epoch fold each turns a test RED. 443 passed, 0 failed.

Noticed but scoped out (Iron Rule 11): the structured-output cache path
(handleChatCompletions, the response_format branch) does not fold CONFIG_EPOCH at all —
a pre-existing gap from #177/#153, independent of this flag. Happy to fix in a follow-up.

Co-authored-by: vvlasy-openclaw <vvlasy@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
openclaw.vvlasy.cz
2026-07-21 21:05:38 +10:00
committed by GitHub
co-authored by vvlasy-openclaw Claude Opus 4.8
parent 45c5717aea
commit 4f9e2ff281
5 changed files with 284 additions and 7 deletions
+2 -1
View File
@@ -159,7 +159,7 @@ OCP translates OpenAI-compatible `/v1/chat/completions` requests into `claude --
OCP is a **text-prompt bridge** to the official `claude` CLI. It does **not** pass through OpenAI `tools`/`functions` payloads or Anthropic `tool_use` blocks to the client. Clients (Cline, Cursor, OpenClaw, etc.) pointed at OCP receive **assistant TEXT only** — they never get `tool_calls` to execute locally.
Any tool use happens server-side, under the `--allowedTools` set configured on the OCP host. In default mode (no `CLAUDE_NO_CONTEXT`), the `claude` CLI's own built-in tools are available to the model; in TUI mode, the operator controls the tool surface via `OCP_TUI_FULL_TOOLS`. Either way, the tools run under the operator's credentials on the server, and the client sees only the final text output.
Any tool use happens server-side, under the `--allowedTools` set configured on the OCP host. In default mode (no `CLAUDE_NO_CONTEXT`), the `claude` CLI's own built-in tools are available to the model; in TUI mode, the operator controls the tool surface via `OCP_TUI_FULL_TOOLS`. Either way, the tools run under the operator's credentials on the server, and the client sees only the final text output. Note that on the `-p` path OCP prepends a system-prompt wrapper telling the model it has **no** local access (right for a shared gateway) — a single-user loopback instance whose model *should* use its tools can flip this with `OCP_LOCAL_TOOLS=1` (see Environment Variables).
**Client-local tool execution is not supported by design.** Supporting it would require bypassing the `claude` CLI to call the raw Anthropic API directly — that is a different product, and is out of scope per `ALIGNMENT.md` (every OCP endpoint must correspond to something `cli.js` actually does).
@@ -235,6 +235,7 @@ The canonical list lives in [`models.json`](./models.json) — the single source
| `CLAUDE_MAX_IMAGES` | `20` | Max image parts per request. Over-cap gets `HTTP 413`. |
| `CLAUDE_MAX_IMAGE_TOTAL_BYTES` | `20971520` | Aggregate decoded-byte cap across all images in a request (default 20 MB). Over-cap gets `HTTP 413`. |
| `CLAUDE_SYSTEM_PROMPT` | *(unset)* | Operator-wide system-prompt text appended (last) to every request's composed system prompt on the default `-p` path. TUI-mode panes are unaffected (they keep the interactive CLI's own system prompt). Echoed truncated on `/health.systemPrompt`. Note: changing this value and restarting auto-invalidates the response cache (the key carries a boot-config epoch, #177). |
| `OCP_LOCAL_TOOLS` | *(unset)* | **Single-user, loopback only.** `=1` swaps the default *"you have no local filesystem/shell access"* system-prompt wrapper for a positive one telling the model it **may** use its tools. These are the **server-side `claude` tools** OCP spawns via `-p` (`--allowedTools`) — which, on a loopback instance, run on the operator's own machine, i.e. *local* tools. For a personal instance (e.g. an **OpenClaw** agent on its own local OCP) the default wrapper otherwise makes the model refuse to use tools it legitimately has. Changes **only the prompt**, never the tool surface (governed by `--allowedTools`/`--disallowedTools`; multi-tenant still `--disallowedTools` the whole FS surface). **Does not** enable client-side `tool_calls` for OpenClaw/Cline/etc. — that remains unsupported by design (see § How tools work). Fail-closed: OCP **refuses to boot** if `=1` is combined with `CLAUDE_AUTH_MODE=multi`, a non-loopback bind, or `PROXY_ANONYMOUS_KEY` (mirrors `OCP_TUI_FULL_TOOLS`, ADR 0007). **Inert in TUI mode** (the `-p` wrapper is unused there; the TUI tool surface is `OCP_TUI_FULL_TOOLS`) — a warning is logged. Off by default → the default path is byte-for-byte unchanged. Toggling it auto-invalidates the standard response cache (boot-config epoch, #177). |
| `CLAUDE_NO_CONTEXT` | `false` | Suppress CLAUDE.md and auto-memory injection (pure API mode) |
| `PROXY_API_KEY` | *(unset)* | Bearer token for shared-mode authentication |
| `PROXY_ANONYMOUS_KEY` | *(unset)* | Well-known anonymous key (multi mode) — this exact string bypasses `validateKey()` and grants public access. Exposed via `/health.anonymousKey` only to localhost, or to all callers when `PROXY_ADVERTISE_ANON_KEY=1`. Full setup + security notes: [docs/lan-mode.md § Anonymous Access](docs/lan-mode.md#anonymous-access-optional). |