From 5cf9432092da40a2653c3d156ca5a4746e853827 Mon Sep 17 00:00:00 2001 From: main Date: Mon, 23 Mar 2026 16:13:37 -0400 Subject: Inject consult prompt prefix --- README.md | 2 ++ assets/codex-skills/phone-opus/SKILL.md | 1 + crates/phone-opus/src/mcp/protocol.rs | 14 ++++++++++++++ crates/phone-opus/src/mcp/service.rs | 24 +++++++++++++++++++----- crates/phone-opus/tests/mcp_hardening.rs | 17 ++++++++++++++++- 5 files changed, 52 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 19b0743..bf81172 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ It exposes one blocking domain tool: - `consult`: run the system `claude` install in print mode, wait for the answer, and return the response plus execution metadata - pass `session_id` from a previous response to resume that Claude Code conversation + - a fixed consult prefix is prepended before the caller-supplied prompt The server keeps the public MCP session in a durable host, isolates the actual Claude invocation in a disposable worker, and ships standard health and @@ -23,6 +24,7 @@ Each `consult` call runs Claude Code with: - the system `claude` binary - `--model claude-opus-4-6` - `--effort max` +- a baked-in consult prefix telling Claude it is acting in read-only advisory mode for another model and should return a prioritized actionable report - no configured MCP servers (`--strict-mcp-config --mcp-config '{"mcpServers":{}}'`) - a read-only built-in toolset: - `Bash,Read,Grep,Glob,LS,WebFetch,WebSearch` diff --git a/assets/codex-skills/phone-opus/SKILL.md b/assets/codex-skills/phone-opus/SKILL.md index 9ea2674..8fb2000 100644 --- a/assets/codex-skills/phone-opus/SKILL.md +++ b/assets/codex-skills/phone-opus/SKILL.md @@ -29,6 +29,7 @@ should be taken as authoritative or final. It is a pure consultant. ## Runtime posture - Pins Claude to Opus 4.6 with max effort. +- Prepends a fixed consult prefix before your prompt so Opus knows it is advising another model in read-only mode and should return a prioritized actionable report. - Uses `--permission-mode dontAsk`, so only globally preapproved read-only Bash commands can execute. - This surface is consultative only. Edit tools are unavailable. - The returned `session_id` is reusable: pass it back into a later `consult` call to continue that Claude conversation. diff --git a/crates/phone-opus/src/mcp/protocol.rs b/crates/phone-opus/src/mcp/protocol.rs index 5cd8313..b1ee587 100644 --- a/crates/phone-opus/src/mcp/protocol.rs +++ b/crates/phone-opus/src/mcp/protocol.rs @@ -12,6 +12,20 @@ pub(crate) const HOST_STATE_ENV: &str = "PHONE_OPUS_MCP_HOST_STATE"; pub(crate) const FORCE_ROLLOUT_ENV: &str = "PHONE_OPUS_MCP_TEST_FORCE_ROLLOUT_KEY"; pub(crate) const WORKER_CRASH_ONCE_ENV: &str = "PHONE_OPUS_MCP_TEST_WORKER_CRASH_ONCE_KEY"; pub(crate) const CLAUDE_BIN_ENV: &str = "PHONE_OPUS_CLAUDE_BIN"; +pub(crate) const CLAUDE_CONSULT_PREFIX: &str = r"You are being invoked in a read-only consultation mode by another model. You cannot edit files or make changes; your role is to inspect, reason, and advise. + +Take your time. Think deeply, check assumptions, and prefer a careful, high-signal analysis over a fast, shallow response. + +Your job is to produce a report for the calling model, not for an end user. Make it actionable. Itemize your findings and prioritize them by importance and urgency. Focus on concrete issues, risks, design flaws, behavioral regressions, missing validations, and better alternatives when relevant. + +When useful, distinguish clearly between: +- confirmed findings +- plausible risks or hypotheses +- open questions that would change the recommendation + +Prefer specific recommendations over vague commentary. If there are no meaningful problems, say so plainly. + +The real prompt follows."; pub(crate) const CLAUDE_EFFORT: &str = "max"; pub(crate) const CLAUDE_MODEL: &str = "claude-opus-4-6"; pub(crate) const CLAUDE_TOOLSET: &str = "Bash,Read,Grep,Glob,LS,WebFetch,WebSearch"; diff --git a/crates/phone-opus/src/mcp/service.rs b/crates/phone-opus/src/mcp/service.rs index 378ce43..d42c0a0 100644 --- a/crates/phone-opus/src/mcp/service.rs +++ b/crates/phone-opus/src/mcp/service.rs @@ -14,7 +14,8 @@ use crate::mcp::output::{ ToolOutput, fallback_detailed_tool_output, split_presentation, tool_success, }; use crate::mcp::protocol::{ - CLAUDE_BIN_ENV, CLAUDE_EFFORT, CLAUDE_MODEL, CLAUDE_TOOLSET, EMPTY_MCP_CONFIG, + CLAUDE_BIN_ENV, CLAUDE_CONSULT_PREFIX, CLAUDE_EFFORT, CLAUDE_MODEL, CLAUDE_TOOLSET, + EMPTY_MCP_CONFIG, }; pub(crate) fn run_worker(generation: u64) -> Result<(), Box> { @@ -138,18 +139,28 @@ impl ConsultRequest { } #[derive(Debug, Clone)] -struct PromptText(String); +struct PromptText { + original: String, + rendered: String, +} impl PromptText { fn parse(raw: String) -> Result { if raw.trim().is_empty() { return Err(ConsultRequestError::EmptyPrompt); } - Ok(Self(raw)) + Ok(Self { + rendered: format!("{CLAUDE_CONSULT_PREFIX}\n\n{raw}"), + original: raw, + }) } fn as_str(&self) -> &str { - self.0.as_str() + self.original.as_str() + } + + fn rendered(&self) -> &str { + self.rendered.as_str() } } @@ -364,7 +375,7 @@ fn invoke_claude(request: &ConsultRequest) -> Result = Result>; fn must( @@ -304,6 +311,10 @@ fn consult_can_resume_a_prior_session_with_read_only_toolset_and_requested_worki tool_content(&consult)["requested_session_id"].as_str(), Some(resumed_session) ); + assert_eq!( + tool_content(&consult)["prompt_prefix_injected"].as_bool(), + Some(true) + ); assert_eq!( tool_content(&consult)["cwd"].as_str(), Some(sandbox.display().to_string().as_str()) @@ -339,7 +350,11 @@ fn consult_can_resume_a_prior_session_with_read_only_toolset_and_requested_worki assert!(lines.contains(&resumed_session)); assert!(lines.contains(&"--max-turns")); assert!(lines.contains(&"7")); - assert_eq!(lines.last().copied(), Some("say oracle")); + assert!(args.contains(PROMPT_PREFIX)); + assert!(args.contains("The real prompt follows.")); + let prefix_index = must_some(args.find(PROMPT_PREFIX), "prefixed consult prompt")?; + let user_prompt_index = must_some(args.find("say oracle"), "user prompt inside args")?; + assert!(prefix_index < user_prompt_index); let telemetry = harness.call_tool(4, "telemetry_snapshot", json!({}))?; assert_tool_ok(&telemetry); -- cgit v1.2.3