mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-21 21:15:09 +00:00
fix(server): wire CLAUDE_SYSTEM_PROMPT (dead since APPEND_SYSTEM_PROMPT retirement) (#175)
* fix(server): wire CLAUDE_SYSTEM_PROMPT into the composed system prompt (was dead since APPEND_SYSTEM_PROMPT retirement) The var was read (SYSTEM_PROMPT, server.mjs), documented in the file header as "appended to all requests", and echoed on /health.systemPrompt — but nothing on the request path consumed it: extractSystemPrompt() composed only the wrapper + client system messages. The wiring was lost when APPEND_SYSTEM_PROMPT was retired, leaving the header comment and the buildCliArgs comment describing behavior that did not exist (caught by the PR #170 independent reviewer). Wire, not delete, because: - the /health `systemPrompt` field is part of the grandfathered B.2 contract (ADR 0006, frozen at v3.16.4) — removal would need an ADR; wiring keeps the shape and makes the field honest; - fleet check: no deployment sets the var (Mac prod plist, Oracle systemd unit, PI231 /etc/ocp/ocp.env all clean), so wiring changes behavior for NOBODY today; - with the var unset, appendOperatorPrompt returns its input string unchanged — the default path is byte-for-byte identical. Mechanics: new pure lib/prompt.mjs `appendOperatorPrompt(base, operatorAppend)` (trimmed; whitespace-only treated as unset so a stray space in a service unit cannot inject "\n\n " into every request), applied as the LAST segment in both extractSystemPrompt branches — an operator-wide directive reads as the final instruction, after client system messages. TUI-mode is untouched (panes keep the interactive CLI's own system prompt); documented in the README row. ALIGNMENT.md Rule 2: no cli.js citation applies. No endpoint, header, or wire field is added or altered; the change affects only the CONTENT OCP passes to the already-established `--system-prompt` flag (file header § verified v2.1.104), i.e. OCP-owned prompt composition. /health shape unchanged. Tests: +3, doubly mutation-proven (unconditional-base revert → 2 failures; trim removal → 2 failures). Suite 341 passed / 0 failed. Co-Authored-By: Claude <claude-opus-4-8> <noreply@anthropic.com> * docs(readme): cache-staleness caveat on CLAUDE_SYSTEM_PROMPT row (reviewer advisory) --------- Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude <claude-opus-4-8> <noreply@anthropic.com>
This commit is contained in:
@@ -228,6 +228,7 @@ The canonical list lives in [`models.json`](./models.json) — the single source
|
|||||||
| `CLAUDE_ALLOWED_TOOLS` | `Bash,Read,...,Agent` | Comma-separated tools to pre-approve |
|
| `CLAUDE_ALLOWED_TOOLS` | `Bash,Read,...,Agent` | Comma-separated tools to pre-approve |
|
||||||
| `CLAUDE_SKIP_PERMISSIONS` | `false` | Bypass all permission checks |
|
| `CLAUDE_SKIP_PERMISSIONS` | `false` | Bypass all permission checks |
|
||||||
| `CLAUDE_MCP_CONFIG` | *(unset)* | Path to an MCP server config JSON, passed to the spawned `claude` as `--mcp-config` (both the `-p` path and TUI `OCP_TUI_FULL_TOOLS` panes) |
|
| `CLAUDE_MCP_CONFIG` | *(unset)* | Path to an MCP server config JSON, passed to the spawned `claude` as `--mcp-config` (both the `-p` path and TUI `OCP_TUI_FULL_TOOLS` panes) |
|
||||||
|
| `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: the response cache key does not include server config — after changing this value, flush the cache (`ocp clear`) or let TTL expire. |
|
||||||
| `CLAUDE_NO_CONTEXT` | `false` | Suppress CLAUDE.md and auto-memory injection (pure API mode) |
|
| `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_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). |
|
| `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). |
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// lib/prompt.mjs — pure operator-append step for the system prompt.
|
||||||
|
//
|
||||||
|
// Extracted so the rule is unit-testable (the suite never imports server.mjs — it
|
||||||
|
// boots a listener). server.mjs composes wrapper + client system messages exactly as
|
||||||
|
// before, then passes the result through this. With CLAUDE_SYSTEM_PROMPT unset the
|
||||||
|
// return is the INPUT STRING UNCHANGED — the default path stays byte-for-byte
|
||||||
|
// identical, which is the repo's bar for touching a request-shaping function.
|
||||||
|
//
|
||||||
|
// The operator prompt goes LAST deliberately: a server-wide directive ("answer in
|
||||||
|
// Chinese") should read as the final instruction, not something a client system
|
||||||
|
// message overrides by coming later. Whitespace-only values are treated as unset —
|
||||||
|
// a stray space in a service unit's Environment= line must not inject "\n\n " into
|
||||||
|
// every request.
|
||||||
|
export function appendOperatorPrompt(base, operatorAppend) {
|
||||||
|
const op = typeof operatorAppend === "string" ? operatorAppend.trim() : "";
|
||||||
|
return op ? `${base}\n\n${op}` : base;
|
||||||
|
}
|
||||||
+7
-4
@@ -49,6 +49,7 @@ import { TuiSemaphore, SemaphoreAbortError, recordTuiEntrypoint, buildTuiHealthB
|
|||||||
import { TuiPanePool, resolvePoolSize, POOL_MAX_SIZE } from "./lib/tui/pool.mjs";
|
import { TuiPanePool, resolvePoolSize, POOL_MAX_SIZE } from "./lib/tui/pool.mjs";
|
||||||
import { TuiDeltaAssembler, DEFAULT_HOLDBACK_CHARS, resolveStreamHoldback } from "./lib/tui/stream.mjs";
|
import { TuiDeltaAssembler, DEFAULT_HOLDBACK_CHARS, resolveStreamHoldback } from "./lib/tui/stream.mjs";
|
||||||
import { createSerialMutex, createTtlCache, isTokenExpiring, orderLabelsLastGoodFirst } from "./lib/spawn-auth.mjs";
|
import { createSerialMutex, createTtlCache, isTokenExpiring, orderLabelsLastGoodFirst } from "./lib/spawn-auth.mjs";
|
||||||
|
import { appendOperatorPrompt } from "./lib/prompt.mjs";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const _pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8"));
|
const _pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8"));
|
||||||
@@ -195,17 +196,19 @@ function resolveClaude() {
|
|||||||
const OCP_SYSTEM_PROMPT_WRAPPER = `You are accessed via the OCP HTTP proxy. You do NOT have access to any local filesystem, working directory, shell, git status, or machine environment. Do not infer or invent such information from any context you observe. Respond only based on the conversation provided.`;
|
const OCP_SYSTEM_PROMPT_WRAPPER = `You are accessed via the OCP HTTP proxy. You do NOT have access to any local filesystem, working directory, shell, git status, or machine environment. Do not infer or invent such information from any context you observe. Respond only based on the conversation provided.`;
|
||||||
|
|
||||||
// Build the full system-prompt string: OCP_SYSTEM_PROMPT_WRAPPER prepended,
|
// Build the full system-prompt string: OCP_SYSTEM_PROMPT_WRAPPER prepended,
|
||||||
// then any system-role messages from the request appended (separated by blank line).
|
// then any system-role messages from the request appended (separated by blank line),
|
||||||
// ADR 0009 Amendment 1 analogue § "OLP system prompt wrapper".
|
// then the operator-wide CLAUDE_SYSTEM_PROMPT appended LAST (lib/prompt.mjs — a
|
||||||
|
// no-op returning the same string when the var is unset, so the default path is
|
||||||
|
// byte-for-byte unchanged). ADR 0009 Amendment 1 analogue § "OLP system prompt wrapper".
|
||||||
function extractSystemPrompt(messages) {
|
function extractSystemPrompt(messages) {
|
||||||
const systemMessages = (messages ?? []).filter(m => m.role === "system");
|
const systemMessages = (messages ?? []).filter(m => m.role === "system");
|
||||||
if (systemMessages.length === 0) {
|
if (systemMessages.length === 0) {
|
||||||
return OCP_SYSTEM_PROMPT_WRAPPER;
|
return appendOperatorPrompt(OCP_SYSTEM_PROMPT_WRAPPER, SYSTEM_PROMPT);
|
||||||
}
|
}
|
||||||
const clientContent = systemMessages.map(m =>
|
const clientContent = systemMessages.map(m =>
|
||||||
contentToText(m.content)
|
contentToText(m.content)
|
||||||
).join("\n\n");
|
).join("\n\n");
|
||||||
return `${OCP_SYSTEM_PROMPT_WRAPPER}\n\n${clientContent}`;
|
return appendOperatorPrompt(`${OCP_SYSTEM_PROMPT_WRAPPER}\n\n${clientContent}`, SYSTEM_PROMPT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── NDJSON line buffer parser (Phase 6c port) ─────────────────────────────
|
// ── NDJSON line buffer parser (Phase 6c port) ─────────────────────────────
|
||||||
|
|||||||
@@ -821,6 +821,32 @@ test("doctor falls back to currentVersion when origin/main unreachable (no stale
|
|||||||
assert.equal(result.next_action.kind, "noop");
|
assert.equal(result.next_action.kind, "noop");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── System-prompt operator append (CLAUDE_SYSTEM_PROMPT wiring) ─────────────
|
||||||
|
// The var was documented + echoed on /health but never reached a request (dead
|
||||||
|
// since APPEND_SYSTEM_PROMPT was retired — caught in PR #170 review). The wiring
|
||||||
|
// contract lives in lib/prompt.mjs. Mutation-proof: make appendOperatorPrompt
|
||||||
|
// return `base` unconditionally and the first test fails; make it stop trimming
|
||||||
|
// and the whitespace test fails.
|
||||||
|
import { appendOperatorPrompt } from "./lib/prompt.mjs";
|
||||||
|
|
||||||
|
console.log("\nSystem-prompt operator append:");
|
||||||
|
|
||||||
|
test("appendOperatorPrompt: appends the operator prompt LAST, blank-line separated", () => {
|
||||||
|
assert.equal(appendOperatorPrompt("WRAPPER\n\nclient", "Answer in Chinese."), "WRAPPER\n\nclient\n\nAnswer in Chinese.");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("appendOperatorPrompt: unset/empty/whitespace-only → base returned BYTE-IDENTICAL", () => {
|
||||||
|
const base = "WRAPPER\n\nclient sys";
|
||||||
|
assert.equal(appendOperatorPrompt(base, undefined), base);
|
||||||
|
assert.equal(appendOperatorPrompt(base, ""), base);
|
||||||
|
assert.equal(appendOperatorPrompt(base, " \n "), base, "a stray space in a service unit must not inject anything");
|
||||||
|
assert.equal(appendOperatorPrompt(base, null), base);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("appendOperatorPrompt: operator value is trimmed before appending", () => {
|
||||||
|
assert.equal(appendOperatorPrompt("W", " hi "), "W\n\nhi");
|
||||||
|
});
|
||||||
|
|
||||||
// ── Upgrade Tests ──
|
// ── Upgrade Tests ──
|
||||||
import { runUpgrade, postFlightOk } from "./scripts/upgrade.mjs";
|
import { runUpgrade, postFlightOk } from "./scripts/upgrade.mjs";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user