swarm repositories / source
aboutsummaryrefslogtreecommitdiff
path: root/crates/phone-opus
diff options
context:
space:
mode:
authormain <main@swarm.moe>2026-03-23 16:13:37 -0400
committermain <main@swarm.moe>2026-03-23 16:13:37 -0400
commit5cf9432092da40a2653c3d156ca5a4746e853827 (patch)
tree6ffcbf5cd5cfec01d89cbd68f349c764c206af9b /crates/phone-opus
parent1422dfed798ff1356a63449a803a8bbdfab79ec8 (diff)
downloadphone_opus-5cf9432092da40a2653c3d156ca5a4746e853827.zip
Inject consult prompt prefix
Diffstat (limited to 'crates/phone-opus')
-rw-r--r--crates/phone-opus/src/mcp/protocol.rs14
-rw-r--r--crates/phone-opus/src/mcp/service.rs24
-rw-r--r--crates/phone-opus/tests/mcp_hardening.rs17
3 files changed, 49 insertions, 6 deletions
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<dyn std::error::Error>> {
@@ -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<Self, ConsultRequestError> {
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<ConsultResponse, ConsultInv
}
let output = command
.current_dir(request.cwd.as_path())
- .arg(request.prompt.as_str())
+ .arg(request.prompt.rendered())
.output()
.map_err(ConsultInvocationError::Spawn)?;
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_owned();
@@ -445,6 +456,7 @@ fn consult_output(
"cwd": response.cwd.display(),
"session_mode": request.session_mode(),
"requested_session_id": request.requested_session_id(),
+ "prompt_prefix_injected": true,
"model": response.model_name(),
"duration_ms": response.duration_ms,
"num_turns": response.num_turns,
@@ -457,6 +469,8 @@ fn consult_output(
"response": response.result,
"cwd": response.cwd.display(),
"prompt": request.prompt.as_str(),
+ "prompt_prefix": CLAUDE_CONSULT_PREFIX,
+ "effective_prompt": request.prompt.rendered(),
"max_turns": request.max_turns.map(TurnLimit::get),
"session_mode": request.session_mode(),
"requested_session_id": request.requested_session_id(),
diff --git a/crates/phone-opus/tests/mcp_hardening.rs b/crates/phone-opus/tests/mcp_hardening.rs
index 918c3c4..e9ee06b 100644
--- a/crates/phone-opus/tests/mcp_hardening.rs
+++ b/crates/phone-opus/tests/mcp_hardening.rs
@@ -13,6 +13,13 @@ use serde_json::{Value, json};
use thiserror as _;
use uuid as _;
+use phone_opus_test_support::PROMPT_PREFIX;
+
+mod phone_opus_test_support {
+ pub(super) const PROMPT_PREFIX: &str =
+ "You are being invoked in a read-only consultation mode by another model.";
+}
+
type TestResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
fn must<T, E: std::fmt::Display, C: std::fmt::Display>(
@@ -305,6 +312,10 @@ fn consult_can_resume_a_prior_session_with_read_only_toolset_and_requested_worki
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);