nix templates: factor harness-base.nix (shared scaffolding incl. gitconfig)
This commit is contained in:
parent
cb62e15d4f
commit
e1289a3e4c
11 changed files with 137 additions and 113 deletions
|
|
@ -80,7 +80,13 @@ async fn main() -> Result<()> {
|
|||
});
|
||||
match initial {
|
||||
LoginState::Online => {
|
||||
serve(&cli.socket, Duration::from_millis(poll_ms), login_state, bus).await
|
||||
serve(
|
||||
&cli.socket,
|
||||
Duration::from_millis(poll_ms),
|
||||
login_state,
|
||||
bus,
|
||||
)
|
||||
.await
|
||||
}
|
||||
LoginState::NeedsLogin => {
|
||||
// Partial-run mode: keep the harness alive (so the web UI
|
||||
|
|
@ -152,8 +158,7 @@ async fn serve(
|
|||
body: body.clone(),
|
||||
});
|
||||
let prompt = format_wake_prompt(&label, &from, &body);
|
||||
let outcome =
|
||||
drive_turn(&prompt, &mcp_config, &bus, mcp::Flavor::Agent).await;
|
||||
let outcome = drive_turn(&prompt, &mcp_config, &bus, mcp::Flavor::Agent).await;
|
||||
emit_turn_end(&bus, &outcome);
|
||||
}
|
||||
Ok(AgentResponse::Empty) => {}
|
||||
|
|
|
|||
|
|
@ -90,9 +90,7 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
});
|
||||
match initial {
|
||||
LoginState::Online => {
|
||||
serve(&cli.socket, Duration::from_millis(poll_ms), bus).await
|
||||
}
|
||||
LoginState::Online => serve(&cli.socket, Duration::from_millis(poll_ms), bus).await,
|
||||
LoginState::NeedsLogin => {
|
||||
tracing::warn!(
|
||||
claude_dir = %claude_dir.display(),
|
||||
|
|
@ -174,8 +172,7 @@ async fn serve(socket: &Path, interval: Duration, bus: Bus) -> Result<()> {
|
|||
body: body.clone(),
|
||||
});
|
||||
let prompt = format_wake_prompt(&label, &from, &body);
|
||||
let outcome =
|
||||
drive_turn(&prompt, &mcp_config, &bus, mcp::Flavor::Manager).await;
|
||||
let outcome = drive_turn(&prompt, &mcp_config, &bus, mcp::Flavor::Manager).await;
|
||||
emit_turn_end(&bus, &outcome);
|
||||
}
|
||||
Ok(ManagerResponse::Empty) => {}
|
||||
|
|
|
|||
|
|
@ -21,10 +21,8 @@ use std::path::PathBuf;
|
|||
|
||||
use anyhow::Result;
|
||||
use rmcp::{
|
||||
ServerHandler, ServiceExt,
|
||||
handler::server::wrapper::Parameters,
|
||||
schemars, tool, tool_handler, tool_router,
|
||||
transport::stdio,
|
||||
ServerHandler, ServiceExt, handler::server::wrapper::Parameters, schemars, tool, tool_handler,
|
||||
tool_router, transport::stdio,
|
||||
};
|
||||
|
||||
use crate::client;
|
||||
|
|
@ -33,12 +31,7 @@ use crate::client;
|
|||
/// a status line → post-log. Free function so both `AgentServer` and
|
||||
/// `ManagerServer` use the same shape; the per-server `status_line`
|
||||
/// closure is what differs (different `Status` wire types).
|
||||
pub async fn run_tool_envelope<F, S>(
|
||||
tool: &'static str,
|
||||
args: String,
|
||||
status: S,
|
||||
body: F,
|
||||
) -> String
|
||||
pub async fn run_tool_envelope<F, S>(tool: &'static str, args: String, status: S, body: F) -> String
|
||||
where
|
||||
F: Future<Output = String>,
|
||||
S: Future<Output = String>,
|
||||
|
|
@ -223,8 +216,10 @@ impl ManagerServer {
|
|||
|
||||
#[tool_router]
|
||||
impl ManagerServer {
|
||||
#[tool(description = "Send a message to a sub-agent (by logical name), to another agent, \
|
||||
or to the operator (recipient `operator`, surfaces in the dashboard).")]
|
||||
#[tool(
|
||||
description = "Send a message to a sub-agent (by logical name), to another agent, \
|
||||
or to the operator (recipient `operator`, surfaces in the dashboard)."
|
||||
)]
|
||||
async fn send(&self, Parameters(args): Parameters<SendArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
let to = args.to.clone();
|
||||
|
|
@ -245,8 +240,10 @@ impl ManagerServer {
|
|||
.await
|
||||
}
|
||||
|
||||
#[tool(description = "Pop one message from the manager inbox. Returns sender + body, or \
|
||||
empty.")]
|
||||
#[tool(
|
||||
description = "Pop one message from the manager inbox. Returns sender + body, or \
|
||||
empty."
|
||||
)]
|
||||
async fn recv(&self, Parameters(args): Parameters<RecvArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
run_tool_envelope("recv", log, self.status_line(), async move {
|
||||
|
|
@ -256,7 +253,9 @@ impl ManagerServer {
|
|||
format!("from: {from}\n\n{body}")
|
||||
}
|
||||
Ok(hive_sh4re::ManagerResponse::Empty) => "(empty)".into(),
|
||||
Ok(hive_sh4re::ManagerResponse::Err { message }) => format!("recv failed: {message}"),
|
||||
Ok(hive_sh4re::ManagerResponse::Err { message }) => {
|
||||
format!("recv failed: {message}")
|
||||
}
|
||||
Ok(other) => format!("recv unexpected response: {other:?}"),
|
||||
Err(e) => format!("recv transport error: {e:#}"),
|
||||
}
|
||||
|
|
@ -264,8 +263,10 @@ impl ManagerServer {
|
|||
.await
|
||||
}
|
||||
|
||||
#[tool(description = "Queue a Spawn approval for a brand-new sub-agent. The operator \
|
||||
approves on the dashboard before the container is actually created.")]
|
||||
#[tool(
|
||||
description = "Queue a Spawn approval for a brand-new sub-agent. The operator \
|
||||
approves on the dashboard before the container is actually created."
|
||||
)]
|
||||
async fn request_spawn(&self, Parameters(args): Parameters<RequestSpawnArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
let name = args.name.clone();
|
||||
|
|
@ -283,8 +284,10 @@ impl ManagerServer {
|
|||
.await
|
||||
}
|
||||
|
||||
#[tool(description = "Stop a sub-agent container (graceful). The state dir is kept; \
|
||||
recreating reuses prior config + Claude credentials.")]
|
||||
#[tool(
|
||||
description = "Stop a sub-agent container (graceful). The state dir is kept; \
|
||||
recreating reuses prior config + Claude credentials."
|
||||
)]
|
||||
async fn kill(&self, Parameters(args): Parameters<KillArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
let name = args.name.clone();
|
||||
|
|
@ -292,7 +295,9 @@ impl ManagerServer {
|
|||
let req = hive_sh4re::ManagerRequest::Kill { name: args.name };
|
||||
match client::request::<_, hive_sh4re::ManagerResponse>(&self.socket, &req).await {
|
||||
Ok(hive_sh4re::ManagerResponse::Ok) => format!("killed {name}"),
|
||||
Ok(hive_sh4re::ManagerResponse::Err { message }) => format!("kill failed: {message}"),
|
||||
Ok(hive_sh4re::ManagerResponse::Err { message }) => {
|
||||
format!("kill failed: {message}")
|
||||
}
|
||||
Ok(other) => format!("kill unexpected response: {other:?}"),
|
||||
Err(e) => format!("kill transport error: {e:#}"),
|
||||
}
|
||||
|
|
@ -300,9 +305,11 @@ impl ManagerServer {
|
|||
.await
|
||||
}
|
||||
|
||||
#[tool(description = "Submit a config change for operator approval. Pass the agent name \
|
||||
#[tool(
|
||||
description = "Submit a config change for operator approval. Pass the agent name \
|
||||
(e.g. `alice` or `hm1nd` for the manager's own config) and a commit sha in that \
|
||||
agent's proposed config repo. On approval hive-c0re rebuilds the container.")]
|
||||
agent's proposed config repo. On approval hive-c0re rebuilds the container."
|
||||
)]
|
||||
async fn request_apply_commit(
|
||||
&self,
|
||||
Parameters(args): Parameters<RequestApplyCommitArgs>,
|
||||
|
|
@ -310,22 +317,27 @@ impl ManagerServer {
|
|||
let log = format!("{args:?}");
|
||||
let agent = args.agent.clone();
|
||||
let commit_ref = args.commit_ref.clone();
|
||||
run_tool_envelope("request_apply_commit", log, self.status_line(), async move {
|
||||
let req = hive_sh4re::ManagerRequest::RequestApplyCommit {
|
||||
agent: args.agent,
|
||||
commit_ref: args.commit_ref,
|
||||
};
|
||||
match client::request::<_, hive_sh4re::ManagerResponse>(&self.socket, &req).await {
|
||||
Ok(hive_sh4re::ManagerResponse::Ok) => {
|
||||
format!("apply approval queued for {agent} @ {commit_ref}")
|
||||
run_tool_envelope(
|
||||
"request_apply_commit",
|
||||
log,
|
||||
self.status_line(),
|
||||
async move {
|
||||
let req = hive_sh4re::ManagerRequest::RequestApplyCommit {
|
||||
agent: args.agent,
|
||||
commit_ref: args.commit_ref,
|
||||
};
|
||||
match client::request::<_, hive_sh4re::ManagerResponse>(&self.socket, &req).await {
|
||||
Ok(hive_sh4re::ManagerResponse::Ok) => {
|
||||
format!("apply approval queued for {agent} @ {commit_ref}")
|
||||
}
|
||||
Ok(hive_sh4re::ManagerResponse::Err { message }) => {
|
||||
format!("request_apply_commit failed: {message}")
|
||||
}
|
||||
Ok(other) => format!("request_apply_commit unexpected response: {other:?}"),
|
||||
Err(e) => format!("request_apply_commit transport error: {e:#}"),
|
||||
}
|
||||
Ok(hive_sh4re::ManagerResponse::Err { message }) => {
|
||||
format!("request_apply_commit failed: {message}")
|
||||
}
|
||||
Ok(other) => format!("request_apply_commit unexpected response: {other:?}"),
|
||||
Err(e) => format!("request_apply_commit transport error: {e:#}"),
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
|
@ -350,15 +362,8 @@ pub const SERVER_NAME: &str = "hyperhive";
|
|||
/// (`Task`) are intentionally omitted for now; `Bash` is allowed pending a
|
||||
/// finer-grained allow-list system for shell command patterns. Edit later
|
||||
/// as our trust model evolves.
|
||||
pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &[
|
||||
"Bash",
|
||||
"Edit",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"Read",
|
||||
"TodoWrite",
|
||||
"Write",
|
||||
];
|
||||
pub const ALLOWED_BUILTIN_TOOLS: &[&str] =
|
||||
&["Bash", "Edit", "Glob", "Grep", "Read", "TodoWrite", "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
|
||||
|
|
@ -376,7 +381,13 @@ pub enum Flavor {
|
|||
pub fn allowed_mcp_tools(flavor: Flavor) -> Vec<String> {
|
||||
let names: &[&str] = match flavor {
|
||||
Flavor::Agent => &["send", "recv"],
|
||||
Flavor::Manager => &["send", "recv", "request_spawn", "kill", "request_apply_commit"],
|
||||
Flavor::Manager => &[
|
||||
"send",
|
||||
"recv",
|
||||
"request_spawn",
|
||||
"kill",
|
||||
"request_apply_commit",
|
||||
],
|
||||
};
|
||||
names
|
||||
.iter()
|
||||
|
|
@ -388,7 +399,10 @@ pub fn allowed_mcp_tools(flavor: Flavor) -> Vec<String> {
|
|||
/// both the built-ins and the MCP surface.
|
||||
#[must_use]
|
||||
pub fn allowed_tools_arg(flavor: Flavor) -> String {
|
||||
let mut all: Vec<String> = ALLOWED_BUILTIN_TOOLS.iter().map(|s| (*s).to_owned()).collect();
|
||||
let mut all: Vec<String> = ALLOWED_BUILTIN_TOOLS
|
||||
.iter()
|
||||
.map(|s| (*s).to_owned())
|
||||
.collect();
|
||||
all.extend(allowed_mcp_tools(flavor));
|
||||
all.join(",")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -323,9 +323,9 @@ async fn events_stream(
|
|||
let rx = state.bus.subscribe();
|
||||
// Drop a "hello" note into the bus so every new subscriber sees at
|
||||
// least one event immediately and can clear the connecting placeholder.
|
||||
state
|
||||
.bus
|
||||
.emit(crate::events::LiveEvent::Note("live stream attached".into()));
|
||||
state.bus.emit(crate::events::LiveEvent::Note(
|
||||
"live stream attached".into(),
|
||||
));
|
||||
let stream = BroadcastStream::new(rx).filter_map(|res| {
|
||||
let ev = res.ok()?;
|
||||
let json = serde_json::to_string(&ev).ok()?;
|
||||
|
|
@ -356,10 +356,7 @@ struct CodeForm {
|
|||
code: String,
|
||||
}
|
||||
|
||||
async fn post_login_code(
|
||||
State(state): State<AppState>,
|
||||
Form(form): Form<CodeForm>,
|
||||
) -> Response {
|
||||
async fn post_login_code(State(state): State<AppState>, Form(form): Form<CodeForm>) -> Response {
|
||||
let session = state.session.lock().unwrap().clone();
|
||||
let Some(session) = session else {
|
||||
return error_response("no login session running");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue