Compare commits
11 changed files with 128 additions and 155 deletions
|
|
@ -80,13 +80,7 @@ async fn main() -> Result<()> {
|
||||||
});
|
});
|
||||||
match initial {
|
match initial {
|
||||||
LoginState::Online => {
|
LoginState::Online => {
|
||||||
serve(
|
serve(&cli.socket, Duration::from_millis(poll_ms), login_state, bus).await
|
||||||
&cli.socket,
|
|
||||||
Duration::from_millis(poll_ms),
|
|
||||||
login_state,
|
|
||||||
bus,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
LoginState::NeedsLogin => {
|
LoginState::NeedsLogin => {
|
||||||
// Partial-run mode: keep the harness alive (so the web UI
|
// Partial-run mode: keep the harness alive (so the web UI
|
||||||
|
|
@ -158,7 +152,8 @@ async fn serve(
|
||||||
body: body.clone(),
|
body: body.clone(),
|
||||||
});
|
});
|
||||||
let prompt = format_wake_prompt(&label, &from, &body);
|
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);
|
emit_turn_end(&bus, &outcome);
|
||||||
}
|
}
|
||||||
Ok(AgentResponse::Empty) => {}
|
Ok(AgentResponse::Empty) => {}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,9 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
match initial {
|
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 => {
|
LoginState::NeedsLogin => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
claude_dir = %claude_dir.display(),
|
claude_dir = %claude_dir.display(),
|
||||||
|
|
@ -172,7 +174,8 @@ async fn serve(socket: &Path, interval: Duration, bus: Bus) -> Result<()> {
|
||||||
body: body.clone(),
|
body: body.clone(),
|
||||||
});
|
});
|
||||||
let prompt = format_wake_prompt(&label, &from, &body);
|
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);
|
emit_turn_end(&bus, &outcome);
|
||||||
}
|
}
|
||||||
Ok(ManagerResponse::Empty) => {}
|
Ok(ManagerResponse::Empty) => {}
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,10 @@ use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use rmcp::{
|
use rmcp::{
|
||||||
ServerHandler, ServiceExt, handler::server::wrapper::Parameters, schemars, tool, tool_handler,
|
ServerHandler, ServiceExt,
|
||||||
tool_router, transport::stdio,
|
handler::server::wrapper::Parameters,
|
||||||
|
schemars, tool, tool_handler, tool_router,
|
||||||
|
transport::stdio,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::client;
|
use crate::client;
|
||||||
|
|
@ -31,7 +33,12 @@ use crate::client;
|
||||||
/// a status line → post-log. Free function so both `AgentServer` and
|
/// a status line → post-log. Free function so both `AgentServer` and
|
||||||
/// `ManagerServer` use the same shape; the per-server `status_line`
|
/// `ManagerServer` use the same shape; the per-server `status_line`
|
||||||
/// closure is what differs (different `Status` wire types).
|
/// 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
|
where
|
||||||
F: Future<Output = String>,
|
F: Future<Output = String>,
|
||||||
S: Future<Output = String>,
|
S: Future<Output = String>,
|
||||||
|
|
@ -216,10 +223,8 @@ impl ManagerServer {
|
||||||
|
|
||||||
#[tool_router]
|
#[tool_router]
|
||||||
impl ManagerServer {
|
impl ManagerServer {
|
||||||
#[tool(
|
#[tool(description = "Send a message to a sub-agent (by logical name), to another agent, \
|
||||||
description = "Send a message to a sub-agent (by logical name), to another agent, \
|
or to the operator (recipient `operator`, surfaces in the dashboard).")]
|
||||||
or to the operator (recipient `operator`, surfaces in the dashboard)."
|
|
||||||
)]
|
|
||||||
async fn send(&self, Parameters(args): Parameters<SendArgs>) -> String {
|
async fn send(&self, Parameters(args): Parameters<SendArgs>) -> String {
|
||||||
let log = format!("{args:?}");
|
let log = format!("{args:?}");
|
||||||
let to = args.to.clone();
|
let to = args.to.clone();
|
||||||
|
|
@ -240,10 +245,8 @@ impl ManagerServer {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tool(
|
#[tool(description = "Pop one message from the manager inbox. Returns sender + body, or \
|
||||||
description = "Pop one message from the manager inbox. Returns sender + body, or \
|
empty.")]
|
||||||
empty."
|
|
||||||
)]
|
|
||||||
async fn recv(&self, Parameters(args): Parameters<RecvArgs>) -> String {
|
async fn recv(&self, Parameters(args): Parameters<RecvArgs>) -> String {
|
||||||
let log = format!("{args:?}");
|
let log = format!("{args:?}");
|
||||||
run_tool_envelope("recv", log, self.status_line(), async move {
|
run_tool_envelope("recv", log, self.status_line(), async move {
|
||||||
|
|
@ -253,9 +256,7 @@ impl ManagerServer {
|
||||||
format!("from: {from}\n\n{body}")
|
format!("from: {from}\n\n{body}")
|
||||||
}
|
}
|
||||||
Ok(hive_sh4re::ManagerResponse::Empty) => "(empty)".into(),
|
Ok(hive_sh4re::ManagerResponse::Empty) => "(empty)".into(),
|
||||||
Ok(hive_sh4re::ManagerResponse::Err { message }) => {
|
Ok(hive_sh4re::ManagerResponse::Err { message }) => format!("recv failed: {message}"),
|
||||||
format!("recv failed: {message}")
|
|
||||||
}
|
|
||||||
Ok(other) => format!("recv unexpected response: {other:?}"),
|
Ok(other) => format!("recv unexpected response: {other:?}"),
|
||||||
Err(e) => format!("recv transport error: {e:#}"),
|
Err(e) => format!("recv transport error: {e:#}"),
|
||||||
}
|
}
|
||||||
|
|
@ -263,10 +264,8 @@ impl ManagerServer {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tool(
|
#[tool(description = "Queue a Spawn approval for a brand-new sub-agent. The operator \
|
||||||
description = "Queue a Spawn approval for a brand-new sub-agent. The operator \
|
approves on the dashboard before the container is actually created.")]
|
||||||
approves on the dashboard before the container is actually created."
|
|
||||||
)]
|
|
||||||
async fn request_spawn(&self, Parameters(args): Parameters<RequestSpawnArgs>) -> String {
|
async fn request_spawn(&self, Parameters(args): Parameters<RequestSpawnArgs>) -> String {
|
||||||
let log = format!("{args:?}");
|
let log = format!("{args:?}");
|
||||||
let name = args.name.clone();
|
let name = args.name.clone();
|
||||||
|
|
@ -284,10 +283,8 @@ impl ManagerServer {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tool(
|
#[tool(description = "Stop a sub-agent container (graceful). The state dir is kept; \
|
||||||
description = "Stop a sub-agent container (graceful). The state dir is kept; \
|
recreating reuses prior config + Claude credentials.")]
|
||||||
recreating reuses prior config + Claude credentials."
|
|
||||||
)]
|
|
||||||
async fn kill(&self, Parameters(args): Parameters<KillArgs>) -> String {
|
async fn kill(&self, Parameters(args): Parameters<KillArgs>) -> String {
|
||||||
let log = format!("{args:?}");
|
let log = format!("{args:?}");
|
||||||
let name = args.name.clone();
|
let name = args.name.clone();
|
||||||
|
|
@ -295,9 +292,7 @@ impl ManagerServer {
|
||||||
let req = hive_sh4re::ManagerRequest::Kill { name: args.name };
|
let req = hive_sh4re::ManagerRequest::Kill { name: args.name };
|
||||||
match client::request::<_, hive_sh4re::ManagerResponse>(&self.socket, &req).await {
|
match client::request::<_, hive_sh4re::ManagerResponse>(&self.socket, &req).await {
|
||||||
Ok(hive_sh4re::ManagerResponse::Ok) => format!("killed {name}"),
|
Ok(hive_sh4re::ManagerResponse::Ok) => format!("killed {name}"),
|
||||||
Ok(hive_sh4re::ManagerResponse::Err { message }) => {
|
Ok(hive_sh4re::ManagerResponse::Err { message }) => format!("kill failed: {message}"),
|
||||||
format!("kill failed: {message}")
|
|
||||||
}
|
|
||||||
Ok(other) => format!("kill unexpected response: {other:?}"),
|
Ok(other) => format!("kill unexpected response: {other:?}"),
|
||||||
Err(e) => format!("kill transport error: {e:#}"),
|
Err(e) => format!("kill transport error: {e:#}"),
|
||||||
}
|
}
|
||||||
|
|
@ -305,11 +300,9 @@ impl ManagerServer {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tool(
|
#[tool(description = "Submit a config change for operator approval. Pass the agent name \
|
||||||
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 \
|
(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(
|
async fn request_apply_commit(
|
||||||
&self,
|
&self,
|
||||||
Parameters(args): Parameters<RequestApplyCommitArgs>,
|
Parameters(args): Parameters<RequestApplyCommitArgs>,
|
||||||
|
|
@ -317,27 +310,22 @@ impl ManagerServer {
|
||||||
let log = format!("{args:?}");
|
let log = format!("{args:?}");
|
||||||
let agent = args.agent.clone();
|
let agent = args.agent.clone();
|
||||||
let commit_ref = args.commit_ref.clone();
|
let commit_ref = args.commit_ref.clone();
|
||||||
run_tool_envelope(
|
run_tool_envelope("request_apply_commit", log, self.status_line(), async move {
|
||||||
"request_apply_commit",
|
let req = hive_sh4re::ManagerRequest::RequestApplyCommit {
|
||||||
log,
|
agent: args.agent,
|
||||||
self.status_line(),
|
commit_ref: args.commit_ref,
|
||||||
async move {
|
};
|
||||||
let req = hive_sh4re::ManagerRequest::RequestApplyCommit {
|
match client::request::<_, hive_sh4re::ManagerResponse>(&self.socket, &req).await {
|
||||||
agent: args.agent,
|
Ok(hive_sh4re::ManagerResponse::Ok) => {
|
||||||
commit_ref: args.commit_ref,
|
format!("apply approval queued for {agent} @ {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
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -362,8 +350,15 @@ pub const SERVER_NAME: &str = "hyperhive";
|
||||||
/// (`Task`) are intentionally omitted for now; `Bash` is allowed pending a
|
/// (`Task`) are intentionally omitted for now; `Bash` is allowed pending a
|
||||||
/// finer-grained allow-list system for shell command patterns. Edit later
|
/// finer-grained allow-list system for shell command patterns. Edit later
|
||||||
/// as our trust model evolves.
|
/// as our trust model evolves.
|
||||||
pub const ALLOWED_BUILTIN_TOOLS: &[&str] =
|
pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &[
|
||||||
&["Bash", "Edit", "Glob", "Grep", "Read", "TodoWrite", "Write"];
|
"Bash",
|
||||||
|
"Edit",
|
||||||
|
"Glob",
|
||||||
|
"Grep",
|
||||||
|
"Read",
|
||||||
|
"TodoWrite",
|
||||||
|
"Write",
|
||||||
|
];
|
||||||
|
|
||||||
/// Which MCP tool surface to advertise via `--allowedTools`. The agent
|
/// Which MCP tool surface to advertise via `--allowedTools`. The agent
|
||||||
/// list is the strict subset of the manager list, so we just thread the
|
/// list is the strict subset of the manager list, so we just thread the
|
||||||
|
|
@ -381,13 +376,7 @@ pub enum Flavor {
|
||||||
pub fn allowed_mcp_tools(flavor: Flavor) -> Vec<String> {
|
pub fn allowed_mcp_tools(flavor: Flavor) -> Vec<String> {
|
||||||
let names: &[&str] = match flavor {
|
let names: &[&str] = match flavor {
|
||||||
Flavor::Agent => &["send", "recv"],
|
Flavor::Agent => &["send", "recv"],
|
||||||
Flavor::Manager => &[
|
Flavor::Manager => &["send", "recv", "request_spawn", "kill", "request_apply_commit"],
|
||||||
"send",
|
|
||||||
"recv",
|
|
||||||
"request_spawn",
|
|
||||||
"kill",
|
|
||||||
"request_apply_commit",
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
names
|
names
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -399,10 +388,7 @@ pub fn allowed_mcp_tools(flavor: Flavor) -> Vec<String> {
|
||||||
/// both the built-ins and the MCP surface.
|
/// both the built-ins and the MCP surface.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn allowed_tools_arg(flavor: Flavor) -> String {
|
pub fn allowed_tools_arg(flavor: Flavor) -> String {
|
||||||
let mut all: Vec<String> = ALLOWED_BUILTIN_TOOLS
|
let mut all: Vec<String> = ALLOWED_BUILTIN_TOOLS.iter().map(|s| (*s).to_owned()).collect();
|
||||||
.iter()
|
|
||||||
.map(|s| (*s).to_owned())
|
|
||||||
.collect();
|
|
||||||
all.extend(allowed_mcp_tools(flavor));
|
all.extend(allowed_mcp_tools(flavor));
|
||||||
all.join(",")
|
all.join(",")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -323,9 +323,9 @@ async fn events_stream(
|
||||||
let rx = state.bus.subscribe();
|
let rx = state.bus.subscribe();
|
||||||
// Drop a "hello" note into the bus so every new subscriber sees at
|
// Drop a "hello" note into the bus so every new subscriber sees at
|
||||||
// least one event immediately and can clear the connecting placeholder.
|
// least one event immediately and can clear the connecting placeholder.
|
||||||
state.bus.emit(crate::events::LiveEvent::Note(
|
state
|
||||||
"live stream attached".into(),
|
.bus
|
||||||
));
|
.emit(crate::events::LiveEvent::Note("live stream attached".into()));
|
||||||
let stream = BroadcastStream::new(rx).filter_map(|res| {
|
let stream = BroadcastStream::new(rx).filter_map(|res| {
|
||||||
let ev = res.ok()?;
|
let ev = res.ok()?;
|
||||||
let json = serde_json::to_string(&ev).ok()?;
|
let json = serde_json::to_string(&ev).ok()?;
|
||||||
|
|
@ -356,7 +356,10 @@ struct CodeForm {
|
||||||
code: String,
|
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 session = state.session.lock().unwrap().clone();
|
||||||
let Some(session) = session else {
|
let Some(session) = session else {
|
||||||
return error_response("no login session running");
|
return error_response("no login session running");
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,7 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use hive_sh4re::{
|
use hive_sh4re::{ApprovalKind, ApprovalStatus, HelperEvent, MANAGER_AGENT, Message, SYSTEM_SENDER};
|
||||||
ApprovalKind, ApprovalStatus, HelperEvent, MANAGER_AGENT, Message, SYSTEM_SENDER,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::coordinator::{Coordinator, TransientKind};
|
use crate::coordinator::{Coordinator, TransientKind};
|
||||||
use crate::lifecycle::{self, MANAGER_NAME};
|
use crate::lifecycle::{self, MANAGER_NAME};
|
||||||
|
|
@ -85,11 +83,7 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish_approval(
|
fn finish_approval(coord: &Coordinator, approval: &hive_sh4re::Approval, result: Result<()>) -> Result<()> {
|
||||||
coord: &Coordinator,
|
|
||||||
approval: &hive_sh4re::Approval,
|
|
||||||
result: Result<()>,
|
|
||||||
) -> Result<()> {
|
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
notify_manager(
|
notify_manager(
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ pub async fn rebuild_agent(coord: &Arc<Coordinator>, name: &str, current_rev: &s
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Auto-create the manager container on startup if it isn't already there.
|
/// Auto-create the manager container on startup if it isn't already there.
|
||||||
/// hive-c0re manages hm1nd end-to-end (Phase 8 follow-up): operators no
|
/// hive-c0re manages hm1nd end-to-end (Phase 8 follow-up): operators no
|
||||||
/// longer declare `containers.hm1nd` in their host NixOS config. Bypasses
|
/// longer declare `containers.hm1nd` in their host NixOS config. Bypasses
|
||||||
|
|
|
||||||
|
|
@ -75,11 +75,7 @@ pub fn is_manager(name: &str) -> bool {
|
||||||
/// extends. Manager → `manager`; everyone else → `agent-base`.
|
/// extends. Manager → `manager`; everyone else → `agent-base`.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn flake_base(name: &str) -> &'static str {
|
pub fn flake_base(name: &str) -> &'static str {
|
||||||
if is_manager(name) {
|
if is_manager(name) { "manager" } else { "agent-base" }
|
||||||
"manager"
|
|
||||||
} else {
|
|
||||||
"agent-base"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(name: &str) -> Result<()> {
|
fn validate(name: &str) -> Result<()> {
|
||||||
|
|
@ -243,10 +239,13 @@ pub async fn setup_applied(
|
||||||
modules = [
|
modules = [
|
||||||
./agent.nix
|
./agent.nix
|
||||||
{{
|
{{
|
||||||
programs.git.config.user = {{
|
environment.etc."gitconfig".text = ''
|
||||||
name = "{name}";
|
[user]
|
||||||
email = "{name}@hyperhive";
|
name = {name}
|
||||||
}};
|
email = {name}@hyperhive
|
||||||
|
[init]
|
||||||
|
defaultBranch = main
|
||||||
|
'';
|
||||||
systemd.services.{service}.environment = {{
|
systemd.services.{service}.environment = {{
|
||||||
HIVE_PORT = "{port}";
|
HIVE_PORT = "{port}";
|
||||||
HIVE_LABEL = "{name}";
|
HIVE_LABEL = "{name}";
|
||||||
|
|
|
||||||
|
|
@ -218,9 +218,7 @@ pub enum ManagerRequest {
|
||||||
Status,
|
Status,
|
||||||
/// Operator-injected message TO the manager (from the manager's own web
|
/// Operator-injected message TO the manager (from the manager's own web
|
||||||
/// UI). Same shape as `AgentRequest::OperatorMsg`.
|
/// UI). Same shape as `AgentRequest::OperatorMsg`.
|
||||||
OperatorMsg {
|
OperatorMsg { body: String },
|
||||||
body: String,
|
|
||||||
},
|
|
||||||
/// Submit a spawn request for the user to approve. On approval the host
|
/// Submit a spawn request for the user to approve. On approval the host
|
||||||
/// creates and starts the container. Brand-new agent names only — if an
|
/// creates and starts the container. Brand-new agent names only — if an
|
||||||
/// agent of the same name already exists, the approval will fail.
|
/// agent of the same name already exists, the approval will fail.
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,31 @@
|
||||||
{ pkgs, ... }:
|
{ pkgs, ... }:
|
||||||
{
|
{
|
||||||
imports = [ ./harness-base.nix ];
|
boot.isNspawnContainer = true;
|
||||||
|
|
||||||
|
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg) [ "claude-code" ];
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
hyperhive
|
||||||
|
claude-code
|
||||||
|
bashInteractive
|
||||||
|
git
|
||||||
|
coreutils-full
|
||||||
|
];
|
||||||
|
# claude's Bash tool refuses to run without a POSIX shell + $SHELL set.
|
||||||
|
environment.variables.SHELL = "${pkgs.bashInteractive}/bin/bash";
|
||||||
|
|
||||||
systemd.services.hive-ag3nt = {
|
systemd.services.hive-ag3nt = {
|
||||||
description = "hive-ag3nt harness";
|
description = "hive-ag3nt harness";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
after = [ "network.target" ];
|
after = [ "network.target" ];
|
||||||
# systemd units get a minimal PATH by default and don't inherit
|
# The harness shells out to `claude` (turn loop + login flow). systemd
|
||||||
# `environment.systemPackages`. Pointing at `/run/current-system/sw`
|
# units get a minimal PATH by default, so we have to put claude-code on
|
||||||
# gives the harness (and any tools claude shells out to via Bash)
|
# it explicitly even though it's in environment.systemPackages above.
|
||||||
# access to everything declared in `systemPackages` — including
|
# bash is on PATH so claude's Bash tool can spawn `$SHELL`.
|
||||||
# anything an agent adds to its own `agent.nix` — without having to
|
path = [
|
||||||
# touch the service definition.
|
pkgs.claude-code
|
||||||
path = [ "/run/current-system/sw" ];
|
pkgs.bashInteractive
|
||||||
|
];
|
||||||
environment.SHELL = "${pkgs.bashInteractive}/bin/bash";
|
environment.SHELL = "${pkgs.bashInteractive}/bin/bash";
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = "${pkgs.hyperhive}/bin/hive-ag3nt serve";
|
ExecStart = "${pkgs.hyperhive}/bin/hive-ag3nt serve";
|
||||||
|
|
@ -20,4 +33,6 @@
|
||||||
RestartSec = 2;
|
RestartSec = 2;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
system.stateVersion = "25.11";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
{ pkgs, ... }:
|
|
||||||
{
|
|
||||||
# Shared scaffolding for any hyperhive harness container — both
|
|
||||||
# sub-agents (`agent-base.nix`) and the manager (`manager.nix`) extend
|
|
||||||
# this. The systemd service that actually runs the harness binary
|
|
||||||
# differs per role and lives in the child module.
|
|
||||||
|
|
||||||
boot.isNspawnContainer = true;
|
|
||||||
|
|
||||||
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg) [ "claude-code" ];
|
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [
|
|
||||||
hyperhive
|
|
||||||
claude-code
|
|
||||||
bashInteractive
|
|
||||||
coreutils-full
|
|
||||||
];
|
|
||||||
|
|
||||||
# Git is needed by claude's Bash tool (for the agent <-> manager config
|
|
||||||
# request flow) and by hive-c0re's own setup_applied / setup_proposed.
|
|
||||||
# `programs.git.enable` installs the binary + manages `/etc/gitconfig`
|
|
||||||
# declaratively so the inline module in `applied/<name>/flake.nix` can
|
|
||||||
# override `user.name` / `user.email` per agent without fighting a raw
|
|
||||||
# `environment.etc."gitconfig"` block.
|
|
||||||
programs.git = {
|
|
||||||
enable = true;
|
|
||||||
config = {
|
|
||||||
user = {
|
|
||||||
name = "hyperhive";
|
|
||||||
email = "hyperhive@local";
|
|
||||||
};
|
|
||||||
init.defaultBranch = "main";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# claude's Bash tool refuses to run without a POSIX shell + $SHELL set.
|
|
||||||
environment.variables.SHELL = "${pkgs.bashInteractive}/bin/bash";
|
|
||||||
|
|
||||||
system.stateVersion = "25.11";
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,27 @@
|
||||||
{ pkgs, ... }:
|
{ pkgs, ... }:
|
||||||
{
|
{
|
||||||
imports = [ ./harness-base.nix ];
|
boot.isNspawnContainer = true;
|
||||||
|
|
||||||
|
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg) [ "claude-code" ];
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
hyperhive
|
||||||
|
claude-code
|
||||||
|
bashInteractive
|
||||||
|
git
|
||||||
|
coreutils-full
|
||||||
|
];
|
||||||
|
# claude's Bash tool refuses to run without a POSIX shell + $SHELL set.
|
||||||
|
environment.variables.SHELL = "${pkgs.bashInteractive}/bin/bash";
|
||||||
|
|
||||||
|
environment.etc."gitconfig".text = ''
|
||||||
|
[user]
|
||||||
|
name = hm1nd
|
||||||
|
email = hm1nd@hyperhive
|
||||||
|
[init]
|
||||||
|
defaultBranch = main
|
||||||
|
'';
|
||||||
|
|
||||||
# HIVE_PORT/HIVE_LABEL/gitconfig are also injected by the generated
|
|
||||||
# `applied/hm1nd/flake.nix` (see `lifecycle::setup_applied`); the values
|
|
||||||
# here are the base config so the container stays sensible if anyone
|
|
||||||
# ever evaluates `nixosConfigurations.manager` standalone.
|
|
||||||
systemd.services.hive-m1nd = {
|
systemd.services.hive-m1nd = {
|
||||||
description = "hive-m1nd manager harness";
|
description = "hive-m1nd manager harness";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
@ -13,17 +29,20 @@
|
||||||
environment = {
|
environment = {
|
||||||
HIVE_PORT = "8000";
|
HIVE_PORT = "8000";
|
||||||
HIVE_LABEL = "hm1nd";
|
HIVE_LABEL = "hm1nd";
|
||||||
SHELL = "${pkgs.bashInteractive}/bin/bash";
|
|
||||||
};
|
};
|
||||||
# See note in agent-base.nix — `/run/current-system/sw` makes the
|
# See note in agent-base.nix — `claude` and a POSIX shell have to be on
|
||||||
# harness service PATH track `environment.systemPackages` so anything
|
# the service PATH explicitly for the harness + claude's Bash tool.
|
||||||
# an agent adds to its own `agent.nix` is visible without editing the
|
path = [
|
||||||
# service definition.
|
pkgs.claude-code
|
||||||
path = [ "/run/current-system/sw" ];
|
pkgs.bashInteractive
|
||||||
|
];
|
||||||
|
environment.SHELL = "${pkgs.bashInteractive}/bin/bash";
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = "${pkgs.hyperhive}/bin/hive-m1nd serve";
|
ExecStart = "${pkgs.hyperhive}/bin/hive-m1nd serve";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
RestartSec = 2;
|
RestartSec = 2;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
system.stateVersion = "25.11";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue