remove Role::Manager + ManagerSurface + Flavor::Manager — there is only one role: agent

This commit is contained in:
damocles 2026-06-04 00:31:00 +02:00 committed by mara
commit f56b272a23
8 changed files with 100 additions and 489 deletions

View file

@ -1,10 +1,7 @@
//! Unified hyperhive harness binary. Picks role from `HIVE_ROLE`
//! (`"agent"` | `"manager"`), dispatches one of three subcommands
//! (`serve` / `mcp` / `wake`), and runs the turn loop through a
//! generic `Surface` trait so both wire surfaces stay in lockstep.
//!
//! Architecture (single-binary rationale, Surface-trait + zero-sized
//! type tags, boot wiring, turn-outcome branch) lives in
//! Unified hyperhive harness binary. Dispatches one of three subcommands
//! (`serve` / `mcp` / `wake`). There is one role: agent. The `Surface`
//! trait + `AgentSurface` zero-sized type tag keeps the turn loop
//! generic and testable. Architecture lives in
//! [`docs/turn-loop.md::Harness binary shape`](../../../docs/turn-loop.md).
use std::path::{Path, PathBuf};
@ -13,7 +10,7 @@ use std::time::Duration;
use hive_ag3nt::web_ui::TurnLock;
use anyhow::{Result, bail};
use anyhow::Result;
use clap::{Parser, Subcommand};
use hive_ag3nt::events::{Bus, LiveEvent, TurnState};
use hive_ag3nt::login::{self, LoginState};
@ -21,15 +18,10 @@ use hive_ag3nt::turn_stats::TurnStats;
use hive_ag3nt::{
DEFAULT_SOCKET, DEFAULT_WEB_PORT, client, mcp, plugins, serve_common, turn, web_ui,
};
use hive_sh4re::{
AgentRequest, AgentResponse, HelperEvent, ManagerRequest, ManagerResponse, SYSTEM_SENDER,
};
use hive_sh4re::{AgentRequest, AgentResponse, HelperEvent, SYSTEM_SENDER};
#[derive(Parser)]
#[command(
name = "hive",
about = "hyperhive harness — role from $HIVE_ROLE (agent|manager)"
)]
#[command(name = "hive", about = "hyperhive harness")]
struct Cli {
/// Path to the per-agent MCP socket (bind-mounted from the host).
#[arg(long, global = true, default_value = DEFAULT_SOCKET)]
@ -48,16 +40,14 @@ enum Cmd {
#[arg(long, default_value_t = 1000)]
poll_ms: u64,
},
/// Run this role's MCP server on stdio. Spawned by `claude` via
/// Run the MCP server on stdio. Spawned by `claude` via
/// `--mcp-config`; tools dispatch through `/run/hive/mcp.sock` back
/// into the hyperhive broker.
Mcp,
/// Inject a wake-up event into this harness's inbox so the next
/// turn fires with the given body. Intended for extra MCP servers
/// / helpers (matrix bridge, scraper, webhook listener, etc.) that
/// need to nudge claude on external events. Available on both
/// agent and manager roles; mirrors the `AgentRequest::Wake` /
/// `ManagerRequest::Wake` pair already on the wire.
/// need to nudge claude on external events.
Wake {
#[arg(long)]
from: String,
@ -67,20 +57,6 @@ enum Cmd {
},
}
#[derive(Copy, Clone)]
enum Role {
Agent,
Manager,
}
fn resolve_role() -> Result<Role> {
match std::env::var("HIVE_ROLE").as_deref() {
Ok("agent") | Err(_) => Ok(Role::Agent),
Ok("manager") => Ok(Role::Manager),
Ok(other) => bail!("unknown HIVE_ROLE={other:?}; expected 'agent' or 'manager'"),
}
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
@ -91,26 +67,11 @@ async fn main() -> Result<()> {
.init();
let cli = Cli::parse();
let role = resolve_role()?;
// Generic dispatch: one `serve_main` / `wake` body, two
// monomorphisations driven by the `Surface` type parameter. See
// `docs/turn-loop.md::Surface trait + zero-sized type tags`.
match (role, cli.cmd) {
(Role::Agent, Cmd::Serve { poll_ms }) => {
serve_main::<AgentSurface>(&cli.socket, poll_ms).await
}
(Role::Manager, Cmd::Serve { poll_ms }) => {
serve_main::<ManagerSurface>(&cli.socket, poll_ms).await
}
(Role::Agent, Cmd::Mcp) => mcp::serve_agent_stdio(cli.socket).await,
(Role::Manager, Cmd::Mcp) => mcp::serve_agent_stdio(cli.socket).await,
(Role::Agent, Cmd::Wake { from, body }) => {
wake::<AgentSurface>(&cli.socket, from, body).await
}
(Role::Manager, Cmd::Wake { from, body }) => {
wake::<ManagerSurface>(&cli.socket, from, body).await
}
match cli.cmd {
Cmd::Serve { poll_ms } => serve_main::<AgentSurface>(&cli.socket, poll_ms).await,
Cmd::Mcp => mcp::serve_agent_stdio(cli.socket).await,
Cmd::Wake { from, body } => wake::<AgentSurface>(&cli.socket, from, body).await,
}
}
@ -186,16 +147,11 @@ enum RecvOutcome {
TransportError,
}
/// Per-role wire surface. Two impls — `AgentSurface`, `ManagerSurface`
/// — wrap the disjoint `Request`/`Response` enums plus a handful of
/// boot-time constants that vary by role. Every other function in this
/// binary that talks to the broker goes through this trait so the turn
/// loop itself has zero per-role branches.
/// Wire surface abstraction. `AgentSurface` is the only impl — the trait
/// exists to keep the turn loop generic and testable. Every function that
/// talks to the broker goes through this so there are zero hard-coded
/// `AgentRequest` / `AgentResponse` references in the turn loop itself.
trait Surface {
/// MCP flavor passed to `TurnFiles::prepare`. Picks which static
/// system-prompt block + tool registration goes into the spawned
/// `claude` process.
const FLAVOR: mcp::Flavor;
/// Ack the in-flight turn. Logs warnings on transport/broker
/// errors but never propagates — turn loop continues either way.
fn ack_turn(socket: &Path) -> impl Future<Output = ()>;
@ -237,12 +193,11 @@ trait Surface {
// ---------- AgentSurface ----------
/// Zero-sized type tag for the sub-agent wire surface.
/// Zero-sized type tag for the agent wire surface.
/// Talks `AgentRequest` / `AgentResponse`.
struct AgentSurface;
impl Surface for AgentSurface {
const FLAVOR: mcp::Flavor = mcp::Flavor::Agent;
async fn ack_turn(socket: &Path) {
match client::request::<_, AgentResponse>(socket, &AgentRequest::AckTurn).await {
@ -382,159 +337,11 @@ impl Surface for AgentSurface {
}
}
// ---------- ManagerSurface ----------
/// Zero-sized type tag for the manager wire surface.
/// Talks `ManagerRequest` / `ManagerResponse`.
struct ManagerSurface;
impl Surface for ManagerSurface {
const FLAVOR: mcp::Flavor = mcp::Flavor::Manager;
async fn ack_turn(socket: &Path) {
match client::request::<_, ManagerResponse>(socket, &ManagerRequest::AckTurn).await {
Ok(ManagerResponse::Ok) => {}
Ok(ManagerResponse::Err { message }) => {
tracing::warn!(%message, "ack_turn rejected by broker");
}
Ok(other) => tracing::warn!(?other, "ack_turn unexpected response"),
Err(e) => tracing::warn!(error = ?e, "ack_turn transport error"),
}
}
async fn requeue_inflight(socket: &Path) {
match client::request::<_, ManagerResponse>(socket, &ManagerRequest::RequeueInflight).await
{
Ok(ManagerResponse::Ok) => {}
Ok(ManagerResponse::Err { message }) => {
tracing::warn!(%message, "requeue_inflight rejected by broker");
}
Ok(other) => tracing::warn!(?other, "requeue_inflight unexpected response"),
Err(e) => tracing::warn!(error = ?e, "requeue_inflight transport error"),
}
}
async fn inbox_unread(socket: &Path) -> u64 {
match client::request::<_, ManagerResponse>(socket, &ManagerRequest::Status).await {
Ok(ManagerResponse::Status { unread }) => unread,
_ => 0,
}
}
async fn post_turn_counts(socket: &Path) -> (Option<u64>, Option<u64>) {
let threads = match client::request::<_, ManagerResponse>(
socket,
&ManagerRequest::GetLooseEnds { agent: None },
)
.await
{
Ok(ManagerResponse::LooseEnds { loose_ends }) => u64::try_from(loose_ends.len()).ok(),
_ => None,
};
let reminders = match client::request::<_, ManagerResponse>(
socket,
&ManagerRequest::CountPendingReminders { agent: None },
)
.await
{
Ok(ManagerResponse::PendingRemindersCount { count }) => Some(count),
_ => None,
};
(threads, reminders)
}
async fn send_to_parent(socket: &Path, body: String) {
let res = client::request::<_, ManagerResponse>(
socket,
&ManagerRequest::Send {
to: hive_sh4re::PARENT_RECIPIENT.into(),
body,
in_reply_to: None,
},
)
.await;
if let Err(e) = res {
tracing::warn!(error = ?e, "failed to notify parent of turn failure");
}
}
async fn self_wake(socket: &Path) {
let res = client::request::<_, ManagerResponse>(
socket,
&ManagerRequest::Wake {
from: "self".into(),
body: "continue".into(),
transient: false,
},
)
.await;
match res {
Ok(ManagerResponse::Ok) => {
tracing::info!("request_next_turn: injected self-continue wake");
}
Ok(ManagerResponse::Err { message }) => {
tracing::warn!(%message, "check_and_inject_continue: wake rejected");
}
Err(e) => {
tracing::warn!(error = ?e, "check_and_inject_continue: wake transport error");
}
_ => {}
}
}
async fn recv_next(socket: &Path) -> RecvOutcome {
let recv: Result<ManagerResponse> = client::request(
socket,
&ManagerRequest::Recv {
wait_seconds: Some(180),
max: None,
},
)
.await;
match recv {
Ok(ManagerResponse::Messages { messages }) if !messages.is_empty() => {
let first = messages.into_iter().next().expect("checked non-empty");
RecvOutcome::Message(first)
}
Ok(ManagerResponse::Messages { .. }) => RecvOutcome::Empty,
Ok(ManagerResponse::Err { message }) => {
tracing::warn!(%message, "recv error");
RecvOutcome::TransportError
}
Ok(other) => {
tracing::warn!(?other, "recv produced unexpected response kind");
RecvOutcome::TransportError
}
Err(e) => {
tracing::warn!(error = ?e, "recv failed; retrying");
RecvOutcome::TransportError
}
}
}
async fn wake_external(socket: &Path, from: String, body: String) -> Result<()> {
let resp: ManagerResponse = client::request(
socket,
&ManagerRequest::Wake {
from,
body,
transient: false,
},
)
.await?;
match resp {
ManagerResponse::Ok => Ok(()),
ManagerResponse::Err { message } => anyhow::bail!("wake: {message}"),
other => anyhow::bail!("wake: unexpected response {other:?}"),
}
}
}
// ---------- generic turn loop ----------
/// Per-role boot — wires up the web UI, login state, stats, plugins,
/// forge notifier, and either drops into `serve_loop` directly
/// (`Online`) or parks on the login flow first (`NeedsLogin`). See
/// Boot — wires up the web UI, login state, stats, plugins, forge
/// notifier, and either drops into `serve_loop` directly (`Online`) or
/// parks on the login flow first (`NeedsLogin`). See
/// `docs/turn-loop.md::Boot wiring`.
async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
let port = std::env::var("HIVE_PORT")
@ -559,13 +366,12 @@ async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
bus.seed_usage(ctx, cost);
}
}
let files = turn::TurnFiles::prepare(socket, &label, S::FLAVOR).await?;
let files = turn::TurnFiles::prepare(socket, &label).await?;
let turn_lock: TurnLock = Arc::new(tokio::sync::Mutex::new(()));
// Plugin install runs role-agnostic: failures come back as a
// Vec<String> and we route each through `<parent>` via the same
// `send_to_parent` failure-notify path the turn loop uses. The
// broker resolves `<parent>` per `topology::parent_of`; root
// agents and the manager fall through to operator.
// Plugin install failures come back as a Vec<String> — route each
// through `<parent>` via the `send_to_parent` failure-notify path.
// The broker resolves `<parent>` per `topology::parent_of`;
// root agents fall through to operator.
for failure in plugins::install_configured(socket).await {
S::send_to_parent(socket, failure).await;
}

View file

@ -1744,13 +1744,11 @@ pub const SERVER_NAME: &str = "hyperhive";
/// should plan in /state notes instead.
pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &["Edit", "Glob", "Grep", "Read", "Write"];
/// Which MCP tool surface to advertise via `--allowedTools`. The agent
/// list is the strict subset of the manager list, so we just thread the
/// flavor through.
/// Kept for API stability — was two-variant (Agent/Manager) but the manager
/// role no longer exists. Only `Agent` remains; everything is perms + caps.
#[derive(Debug, Clone, Copy)]
pub enum Flavor {
Agent,
Manager,
}
/// Env var written by the meta renderer with a comma-separated list of
@ -1800,18 +1798,11 @@ fn allowed_capability_tools() -> Vec<String> {
/// token is matched (case-insensitive) against the `ToolGroup` serde names
/// (`messaging`, `meta`, `inbox`, `lifecycle`, `approvals`, `scheduling`,
/// `diagnostics`, `execution`). Unrecognised tokens are logged and skipped.
/// Falls back to the flavor default when the env var is absent or empty.
fn effective_tool_groups(flavor: Flavor) -> Vec<hive_sh4re::ToolGroup> {
/// Falls back to `AGENT_DEFAULT` when the env var is absent or empty.
fn effective_tool_groups() -> Vec<hive_sh4re::ToolGroup> {
let raw = match std::env::var(TOOL_GROUPS_ENV) {
Ok(v) if !v.trim().is_empty() => v,
_ => {
// No env var — use the flavor default unchanged.
let defaults = match flavor {
Flavor::Agent => hive_sh4re::ToolGroup::AGENT_DEFAULT,
Flavor::Manager => hive_sh4re::ToolGroup::MANAGER_DEFAULT,
};
return defaults.to_vec();
}
_ => return hive_sh4re::ToolGroup::AGENT_DEFAULT.to_vec(),
};
let mut groups = Vec::new();
for token in raw.split(',') {
@ -1829,12 +1820,9 @@ fn effective_tool_groups(flavor: Flavor) -> Vec<hive_sh4re::ToolGroup> {
if groups.is_empty() {
tracing::warn!(
"{TOOL_GROUPS_ENV} set but contained no recognised groups; \
falling back to flavor default"
falling back to AGENT_DEFAULT"
);
return match flavor {
Flavor::Agent => hive_sh4re::ToolGroup::AGENT_DEFAULT.to_vec(),
Flavor::Manager => hive_sh4re::ToolGroup::MANAGER_DEFAULT.to_vec(),
};
return hive_sh4re::ToolGroup::AGENT_DEFAULT.to_vec();
}
groups
}
@ -1872,8 +1860,8 @@ pub fn allowed_mcp_tools(groups: &[hive_sh4re::ToolGroup]) -> Vec<String> {
/// Combined allow-list passed to `--allowedTools` (auto-approve) — covers
/// both the built-ins and the MCP surface.
#[must_use]
pub fn allowed_tools_arg(flavor: Flavor) -> String {
let groups = effective_tool_groups(flavor);
pub fn allowed_tools_arg() -> String {
let groups = effective_tool_groups();
// Base built-ins always present.
let mut all: Vec<String> = ALLOWED_BUILTIN_TOOLS
.iter()
@ -1903,15 +1891,7 @@ pub fn allowed_tools_arg(flavor: Flavor) -> String {
/// `WebFetch`/`WebSearch` when the `web_tools` group is active).
#[must_use]
pub fn builtin_tools_arg() -> String {
builtin_tools_arg_for_flavor(Flavor::Agent)
}
/// Flavor-aware variant used by `turn.rs` via `builtin_tools_arg`. Reads
/// the effective tool groups for `flavor` so `--tools` matches what
/// `--allowedTools` includes for the same session.
#[must_use]
pub fn builtin_tools_arg_for_flavor(flavor: Flavor) -> String {
let groups = effective_tool_groups(flavor);
let groups = effective_tool_groups();
let mut tools: Vec<&str> = ALLOWED_BUILTIN_TOOLS.to_vec();
for group in &groups {
for t in group.builtin_tools() {

View file

@ -1,38 +1,31 @@
//! System-prompt renderer. Single `prompts/system.md` with
//! HTML-comment markers gating role-specific blocks; this module
//! assembles the final prompt for a given flavor. Marker grammar +
//! placeholder substitution rules in
//! assembles the final prompt (always "agent" role — there is only one
//! role). Marker grammar + placeholder substitution rules in
//! `docs/turn-loop.md::On-boot files` (`claude-system-prompt.md`).
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use crate::mcp::Flavor;
/// Assemble the system prompt for a given flavor + label + pronouns +
/// optional hive / swarm display names. Pure function — no I/O. Splits
/// out from [`write_system_prompt`] so the marker logic + substitution
/// is unit-testable in isolation. The caller supplies the template body
/// so tests can pass an inline fixture and production reads it once at
/// harness startup via [`hive_sh4re::assets::prompt_template`]
/// Assemble the system prompt for a given label + pronouns + optional hive /
/// swarm display names. Pure function — no I/O. Splits out from
/// [`write_system_prompt`] so the marker logic + substitution is unit-testable
/// in isolation. The caller supplies the template body so tests can pass an
/// inline fixture and production reads it once at harness startup via
/// [`hive_sh4re::assets::prompt_template`]
/// (`$HIVE_ASSETS_DIR/prompts/system.md`). Substitution placeholders +
/// marker grammar documented in
/// `docs/turn-loop.md::On-boot files` (`claude-system-prompt.md`).
#[must_use]
pub fn render(
template: &str,
flavor: Flavor,
label: &str,
operator_pronouns: &str,
hive_name: Option<&str>,
swarm_name: Option<&str>,
) -> String {
let target = match flavor {
Flavor::Agent => "agent",
Flavor::Manager => "manager",
};
let body = filter_role_blocks(template, target);
let body = filter_role_blocks(template, "agent");
let qualified = crate::identity::qualify(label);
let hive_identity = hive_name
.filter(|n| !n.is_empty())
@ -113,7 +106,7 @@ fn parse_close_marker(line: &str) -> Option<&str> {
/// # Errors
///
/// Returns an error if the system prompt file cannot be written.
pub async fn write_system_prompt(_socket: &Path, label: &str, flavor: Flavor) -> Result<PathBuf> {
pub async fn write_system_prompt(_socket: &Path, label: &str) -> Result<PathBuf> {
let parent = crate::paths::config_dir();
tokio::fs::create_dir_all(&parent).await.ok();
let pronouns = std::env::var("HIVE_OPERATOR_PRONOUNS").unwrap_or_else(|_| "she/her".to_owned());
@ -133,7 +126,6 @@ pub async fn write_system_prompt(_socket: &Path, label: &str, flavor: Flavor) ->
let swarm_name = crate::identity::swarm_name();
let body = render(
&template,
flavor,
label,
&pronouns,
hive_name.as_deref(),
@ -279,7 +271,6 @@ shared closer
// harness already relied on.
let rendered = render(
&PRODUCTION_TEMPLATE,
Flavor::Agent,
"alice",
"they/them",
None,
@ -292,66 +283,32 @@ shared closer
}
#[test]
fn render_agent_excludes_manager_only_tools() {
// Spot-check: the manager-only tool block (request_init_config,
// kill, schedule_*) MUST NOT appear in the agent's rendered
// prompt. Drift between flavor and tool surface bites every
// time it happens.
fn render_no_role_markers_in_output() {
// No raw role markers should survive into the rendered prompt.
let rendered = render(
&PRODUCTION_TEMPLATE,
Flavor::Agent,
"alice",
"she/her",
None,
None,
);
assert!(!rendered.contains("request_init_config"));
assert!(!rendered.contains("request_apply_commit"));
assert!(!rendered.contains("get_logs"));
// Sanity: shared tools DO appear.
assert!(!rendered.contains("<!-- role:"));
assert!(!rendered.contains("<!-- /role:"));
// Shared tools appear.
assert!(rendered.contains("mcp__hyperhive__recv"));
assert!(rendered.contains("mcp__hyperhive__ask"));
}
#[test]
fn render_manager_includes_manager_only_tools() {
fn render_uses_agent_opener() {
let rendered = render(
&PRODUCTION_TEMPLATE,
Flavor::Manager,
"ruth",
"she/her",
None,
None,
);
assert!(rendered.contains("request_init_config"));
assert!(rendered.contains("request_apply_commit"));
assert!(rendered.contains("get_logs"));
assert!(rendered.contains("request_schedule_prompt"));
assert!(rendered.contains("cancel_schedule"));
// Sub-agent-only sections must NOT appear in manager prompt.
assert!(!rendered.contains("request_next_turn"));
}
#[test]
fn render_uses_correct_role_opener() {
let agent = render(
&PRODUCTION_TEMPLATE,
Flavor::Agent,
"alice",
"she/her",
None,
None,
);
assert!(agent.starts_with("You are hyperhive agent"));
let manager = render(
&PRODUCTION_TEMPLATE,
Flavor::Manager,
"ruth",
"she/her",
None,
None,
);
assert!(manager.starts_with("You are the hyperhive manager"));
assert!(rendered.starts_with("You are hyperhive agent"));
}
// Inline fixture for the {hive_identity} / {swarm_identity}
@ -371,7 +328,6 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
fn render_substitutes_hive_identity_when_set() {
let rendered = render(
IDENTITY_FIXTURE,
Flavor::Agent,
"alice",
"she/her",
Some("pr1ma"),
@ -389,7 +345,6 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
fn render_substitutes_swarm_identity_when_set() {
let rendered = render(
IDENTITY_FIXTURE,
Flavor::Manager,
"ruth",
"she/her",
None,
@ -403,7 +358,6 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
fn render_substitutes_both_when_both_set() {
let rendered = render(
IDENTITY_FIXTURE,
Flavor::Agent,
"iris",
"she/her",
Some("pr1ma"),
@ -418,14 +372,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
fn render_omits_identity_when_unset() {
// None / None must round-trip the non-identity opener verbatim
// — single-hive deployments see zero diff.
let rendered = render(
IDENTITY_FIXTURE,
Flavor::Agent,
"alice",
"she/her",
None,
None,
);
let rendered = render(IDENTITY_FIXTURE, "alice", "she/her", None, None);
assert!(!rendered.contains("on hive"));
assert!(!rendered.contains("in swarm"));
assert!(!rendered.contains("{hive_identity}"));
@ -438,14 +385,7 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity
// through `identity::hive_name()` as None (the accessor
// filters empty), but `render` should still no-op on a
// direct `Some("")` from a test fixture or a future caller.
let rendered = render(
IDENTITY_FIXTURE,
Flavor::Agent,
"alice",
"she/her",
Some(""),
Some(""),
);
let rendered = render(IDENTITY_FIXTURE, "alice", "she/her", Some(""), Some(""));
assert!(!rendered.contains("on hive"));
assert!(!rendered.contains("in swarm"));
}

View file

@ -1,7 +1,6 @@
//! Per-turn claude invocation shared by `hive-ag3nt` and `hive-m1nd`. The
//! two binaries differ only in their MCP `Flavor` (agent surface vs.
//! manager surface) and their wake-prompt wording; the spawn shape,
//! arg-vector, stdin plumbing, and stream-json pumping are identical.
//! Per-turn claude invocation. The spawn shape, arg-vector, stdin plumbing,
//! and stream-json pumping are shared across all roles (there is only one
//! role: agent).
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
@ -107,7 +106,6 @@ pub struct TurnFiles {
pub mcp_config: PathBuf,
pub settings: PathBuf,
pub system_prompt: PathBuf,
pub flavor: mcp::Flavor,
}
impl TurnFiles {
@ -117,12 +115,11 @@ impl TurnFiles {
/// # Errors
///
/// Returns an error if any of the config files cannot be written to disk.
pub async fn prepare(socket: &Path, label: &str, flavor: mcp::Flavor) -> Result<Self> {
pub async fn prepare(socket: &Path, label: &str) -> Result<Self> {
Ok(Self {
mcp_config: write_mcp_config(socket).await?,
settings: write_settings(socket).await?,
system_prompt: write_system_prompt(socket, label, flavor).await?,
flavor,
system_prompt: write_system_prompt(socket, label).await?,
})
}
}
@ -184,12 +181,8 @@ pub async fn write_settings(_socket: &Path) -> Result<PathBuf> {
/// # Errors
///
/// Returns an error if the system prompt file cannot be written.
pub async fn write_system_prompt(
socket: &Path,
label: &str,
flavor: mcp::Flavor,
) -> Result<PathBuf> {
crate::prompt::write_system_prompt(socket, label, flavor).await
pub async fn write_system_prompt(socket: &Path, label: &str) -> Result<PathBuf> {
crate::prompt::write_system_prompt(socket, label).await
}
/// One claude turn's outcome. The harness uses this to decide whether to
@ -682,9 +675,9 @@ async fn run_claude(prompt: &str, files: &TurnFiles, bus: &Bus) -> Result<(bool,
.arg(&files.mcp_config)
.arg("--strict-mcp-config")
.arg("--tools")
.arg(mcp::builtin_tools_arg_for_flavor(files.flavor))
.arg(mcp::builtin_tools_arg())
.arg("--allowedTools")
.arg(mcp::allowed_tools_arg(files.flavor));
.arg(mcp::allowed_tools_arg());
let mut child = cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())

View file

@ -30,7 +30,6 @@ use crate::client;
use crate::events::Bus;
use crate::login::LoginState;
use crate::login_session::{LoginSession, drop_if_finished};
use crate::mcp;
use crate::turn::TurnFiles;
/// Live login state for the web UI. The harness updates this in place as it
@ -63,8 +62,6 @@ struct AppState {
gui_vnc_port: Option<u16>,
}
/// Re-export so callers in `turn.rs` can name the type via `web_ui::Flavor`.
pub type Flavor = mcp::Flavor;
/// Bind the per-container web listener and serve the SPA.
///