Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f83c0aa717 | ||
|
|
dfbcf2b9d1 | ||
|
|
f1fd787f17 | ||
|
|
824914807a | ||
|
|
edc1de3197 |
13 changed files with 128 additions and 17 deletions
|
|
@ -222,10 +222,11 @@ line (broker's `count_pending`). When adding new tools (manager surface,
|
||||||
notes/state, etc.), use `run_tool` and they pick up the envelope for free.
|
notes/state, etc.), use `run_tool` and they pick up the envelope for free.
|
||||||
|
|
||||||
**Tool whitelist** (see `ALLOWED_BUILTIN_TOOLS` in `hive-ag3nt::mcp`):
|
**Tool whitelist** (see `ALLOWED_BUILTIN_TOOLS` in `hive-ag3nt::mcp`):
|
||||||
- Allowed built-ins: `Bash`, `Edit`, `Glob`, `Grep`, `NotebookEdit`, `Read`,
|
- Allowed built-ins: `Bash`, `Edit`, `Glob`, `Grep`, `Read`, `TodoWrite`,
|
||||||
`TodoWrite`, `Write`.
|
`Write`.
|
||||||
- Denied by omission: `WebFetch`, `WebSearch`, `Task` — no external egress
|
- Denied by omission: `WebFetch`, `WebSearch`, `Task`, `NotebookEdit` —
|
||||||
or nested-agent spawning until we have a real policy story.
|
no external egress, nested-agent spawning, or Jupyter handling until we
|
||||||
|
have a real policy story.
|
||||||
- Allowed MCP tools: `mcp__hyperhive__send`, `mcp__hyperhive__recv`.
|
- Allowed MCP tools: `mcp__hyperhive__send`, `mcp__hyperhive__recv`.
|
||||||
|
|
||||||
`Bash` is on the allow-list "for now" — pending a finer-grained allow-list
|
`Bash` is on the allow-list "for now" — pending a finer-grained allow-list
|
||||||
|
|
|
||||||
|
|
@ -217,6 +217,9 @@ fn emit_turn_end(bus: &Bus, outcome: &turn::TurnOutcome) {
|
||||||
/// about it and the MCP tools, and is expected to drive any further
|
/// about it and the MCP tools, and is expected to drive any further
|
||||||
/// recv/send itself.
|
/// recv/send itself.
|
||||||
fn format_wake_prompt(label: &str, from: &str, body: &str) -> String {
|
fn format_wake_prompt(label: &str, from: &str, body: &str) -> String {
|
||||||
|
// Manager broker name. Lifecycle calls it `hm1nd` (container), broker
|
||||||
|
// calls it `manager`. Sub-agents address the manager via `manager`.
|
||||||
|
let manager = hive_sh4re::MANAGER_AGENT;
|
||||||
format!(
|
format!(
|
||||||
"You are hyperhive agent `{label}` in a multi-agent system.\n\
|
"You are hyperhive agent `{label}` in a multi-agent system.\n\
|
||||||
\n\
|
\n\
|
||||||
|
|
@ -231,6 +234,14 @@ fn format_wake_prompt(label: &str, from: &str, body: &str) -> String {
|
||||||
- `mcp__hyperhive__send(to, body)` — message a peer (by their name) \
|
- `mcp__hyperhive__send(to, body)` — message a peer (by their name) \
|
||||||
or the operator (recipient `operator`, surfaces in the dashboard).\n\
|
or the operator (recipient `operator`, surfaces in the dashboard).\n\
|
||||||
\n\
|
\n\
|
||||||
|
Need new packages, env vars, or other NixOS config for yourself? \
|
||||||
|
You can't edit your own config directly — message the manager \
|
||||||
|
(recipient `{manager}`) describing what you need. The manager \
|
||||||
|
edits `/agents/{label}/config/agent.nix` on your behalf, commits, \
|
||||||
|
and submits an approval that the operator can accept on the \
|
||||||
|
dashboard; on approve hive-c0re rebuilds your container with the \
|
||||||
|
new config.\n\
|
||||||
|
\n\
|
||||||
Handle the inbox, then stop. Don't narrate intent — act."
|
Handle the inbox, then stop. Don't narrate intent — act."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -355,7 +355,6 @@ pub const ALLOWED_BUILTIN_TOOLS: &[&str] = &[
|
||||||
"Edit",
|
"Edit",
|
||||||
"Glob",
|
"Glob",
|
||||||
"Grep",
|
"Grep",
|
||||||
"NotebookEdit",
|
|
||||||
"Read",
|
"Read",
|
||||||
"TodoWrite",
|
"TodoWrite",
|
||||||
"Write",
|
"Write",
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,12 @@ async fn index(State(state): State<AppState>) -> Html<String> {
|
||||||
(LoginState::NeedsLogin, None) => render_needs_login_idle(),
|
(LoginState::NeedsLogin, None) => render_needs_login_idle(),
|
||||||
(LoginState::NeedsLogin, Some(session)) => render_login_in_progress(&session),
|
(LoginState::NeedsLogin, Some(session)) => render_login_in_progress(&session),
|
||||||
};
|
};
|
||||||
|
let dashboard_port = std::env::var("HIVE_DASHBOARD_PORT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.parse::<u16>().ok())
|
||||||
|
.unwrap_or(7000);
|
||||||
Html(format!(
|
Html(format!(
|
||||||
"<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>{label} // hyperhive</title>\n{STYLE}\n</head>\n<body>\n<pre class=\"banner\">░▒▓█▓▒░ {label} ░▒▓█▓▒░ hyperhive ag3nt ░▒▓█▓▒░</pre>\n<h2>◆ {label} ◆</h2>\n<div class=\"divider\">══════════════════════════════════════════════════════════════</div>\n{body}\n</body>\n</html>\n",
|
"<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>{label} // hyperhive</title>\n{STYLE}\n</head>\n<body>\n<pre class=\"banner\">░▒▓█▓▒░ {label} ░▒▓█▓▒░ hyperhive ag3nt ░▒▓█▓▒░</pre>\n<h2>◆ {label} ◆ <a href=\"#\" id=\"rebuild-btn\" class=\"btn-rebuild\" data-port=\"{dashboard_port}\" data-label=\"{label}\">↻ R3BU1LD</a></h2>\n<div class=\"divider\">══════════════════════════════════════════════════════════════</div>\n{body}\n<script>\n(function() {{\n const b = document.getElementById('rebuild-btn');\n b.addEventListener('click', function(e) {{\n e.preventDefault();\n if (!confirm('rebuild ' + b.dataset.label + '? container will hot-reload.')) return;\n const url = window.location.protocol + '//' + window.location.hostname + ':' + b.dataset.port + '/rebuild/' + b.dataset.label;\n const form = document.createElement('form');\n form.method = 'POST';\n form.action = url;\n document.body.appendChild(form);\n form.submit();\n }});\n}})();\n</script>\n</body>\n</html>\n",
|
||||||
label = state.label,
|
label = state.label,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
@ -435,6 +439,19 @@ const STYLE: &str = r#"
|
||||||
.btn:hover { background: rgba(204, 102, 255, 0.1); }
|
.btn:hover { background: rgba(204, 102, 255, 0.1); }
|
||||||
.btn-login { color: var(--amber); border-color: var(--amber); }
|
.btn-login { color: var(--amber); border-color: var(--amber); }
|
||||||
.btn-cancel { color: #ff6b6b; border-color: #ff6b6b; font-size: 0.85em; padding: 0.15em 0.6em; }
|
.btn-cancel { color: #ff6b6b; border-color: #ff6b6b; font-size: 0.85em; padding: 0.15em 0.6em; }
|
||||||
|
.btn-rebuild {
|
||||||
|
color: var(--amber);
|
||||||
|
border: 1px solid var(--amber);
|
||||||
|
padding: 0.15em 0.6em;
|
||||||
|
font-size: 0.55em;
|
||||||
|
font-family: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
margin-left: 0.6em;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-rebuild:hover { background: rgba(255, 184, 77, 0.1); }
|
||||||
.btn-send { color: var(--green); border-color: var(--green); }
|
.btn-send { color: var(--green); border-color: var(--green); }
|
||||||
.sendform { display: flex; gap: 0.6em; margin-top: 0.5em; }
|
.sendform { display: flex; gap: 0.6em; margin-top: 0.5em; }
|
||||||
.sendform input {
|
.sendform input {
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
|
||||||
&agent_dir,
|
&agent_dir,
|
||||||
&applied_dir,
|
&applied_dir,
|
||||||
&claude_dir,
|
&claude_dir,
|
||||||
|
coord.dashboard_port,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -69,6 +70,7 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
|
||||||
&proposed_dir,
|
&proposed_dir,
|
||||||
&applied_dir,
|
&applied_dir,
|
||||||
&claude_dir,
|
&claude_dir,
|
||||||
|
coord_bg.dashboard_port,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
coord_bg.clear_transient(&agent_bg);
|
coord_bg.clear_transient(&agent_bg);
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ async fn serve(stream: UnixStream, agent: String, broker: Arc<Broker>) -> Result
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let resp = match serde_json::from_str::<AgentRequest>(line.trim()) {
|
let resp = match serde_json::from_str::<AgentRequest>(line.trim()) {
|
||||||
Ok(req) => dispatch(&req, &agent, &broker),
|
Ok(req) => dispatch(&req, &agent, &broker).await,
|
||||||
Err(e) => AgentResponse::Err {
|
Err(e) => AgentResponse::Err {
|
||||||
message: format!("parse error: {e}"),
|
message: format!("parse error: {e}"),
|
||||||
},
|
},
|
||||||
|
|
@ -77,7 +77,11 @@ async fn serve(stream: UnixStream, agent: String, broker: Arc<Broker>) -> Result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch(req: &AgentRequest, agent: &str, broker: &Broker) -> AgentResponse {
|
/// How long the long-poll `Recv` holds a connection open waiting for new
|
||||||
|
/// mail. Set well below typical TCP/proxy idle limits.
|
||||||
|
const RECV_LONG_POLL: std::time::Duration = std::time::Duration::from_secs(30);
|
||||||
|
|
||||||
|
async fn dispatch(req: &AgentRequest, agent: &str, broker: &Broker) -> AgentResponse {
|
||||||
match req {
|
match req {
|
||||||
AgentRequest::Send { to, body } => {
|
AgentRequest::Send { to, body } => {
|
||||||
match broker.send(&Message {
|
match broker.send(&Message {
|
||||||
|
|
@ -91,7 +95,7 @@ fn dispatch(req: &AgentRequest, agent: &str, broker: &Broker) -> AgentResponse {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AgentRequest::Recv => match broker.recv(agent) {
|
AgentRequest::Recv => match broker.recv_blocking(agent, RECV_LONG_POLL).await {
|
||||||
Ok(Some(msg)) => AgentResponse::Message {
|
Ok(Some(msg)) => AgentResponse::Message {
|
||||||
from: msg.from,
|
from: msg.from,
|
||||||
body: msg.body,
|
body: msg.body,
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ pub async fn rebuild_agent(coord: &Arc<Coordinator>, name: &str, current_rev: &s
|
||||||
&agent_dir,
|
&agent_dir,
|
||||||
&applied_dir,
|
&applied_dir,
|
||||||
&claude_dir,
|
&claude_dir,
|
||||||
|
coord.dashboard_port,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
std::fs::write(rev_marker_path(name), current_rev)
|
std::fs::write(rev_marker_path(name), current_rev)
|
||||||
|
|
@ -78,8 +79,26 @@ pub async fn rebuild_agent(coord: &Arc<Coordinator>, name: &str, current_rev: &s
|
||||||
/// the approval queue — manager is required infrastructure. Idempotent.
|
/// the approval queue — manager is required infrastructure. Idempotent.
|
||||||
pub async fn ensure_manager(coord: &Arc<Coordinator>) -> Result<()> {
|
pub async fn ensure_manager(coord: &Arc<Coordinator>) -> Result<()> {
|
||||||
let existing = lifecycle::list().await.unwrap_or_default();
|
let existing = lifecycle::list().await.unwrap_or_default();
|
||||||
|
let current_rev = current_flake_rev(&coord.hyperhive_flake);
|
||||||
if existing.iter().any(|c| c == MANAGER_NAME) {
|
if existing.iter().any(|c| c == MANAGER_NAME) {
|
||||||
tracing::debug!("manager container already present");
|
// Container exists already. If it predates the unified lifecycle
|
||||||
|
// (no applied flake on disk) we must rebuild — otherwise it's
|
||||||
|
// running whatever the host-declarative config was at create
|
||||||
|
// time, with a wrong systemd unit and port.
|
||||||
|
let applied_flake = Coordinator::agent_applied_dir(MANAGER_NAME).join("flake.nix");
|
||||||
|
if !applied_flake.exists()
|
||||||
|
&& let Some(rev) = current_rev.as_ref()
|
||||||
|
{
|
||||||
|
tracing::warn!(
|
||||||
|
"manager container exists but no applied flake — forcing rebuild to migrate"
|
||||||
|
);
|
||||||
|
let coord_clone = coord.clone();
|
||||||
|
if let Err(e) = rebuild_agent(&coord_clone, MANAGER_NAME, rev).await {
|
||||||
|
tracing::warn!(error = ?e, "manager migration rebuild failed");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::debug!("manager container already present");
|
||||||
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
tracing::info!("manager container missing — spawning");
|
tracing::info!("manager container missing — spawning");
|
||||||
|
|
@ -94,9 +113,10 @@ pub async fn ensure_manager(coord: &Arc<Coordinator>) -> Result<()> {
|
||||||
&proposed,
|
&proposed,
|
||||||
&applied,
|
&applied,
|
||||||
&claude_dir,
|
&claude_dir,
|
||||||
|
coord.dashboard_port,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
if let Some(rev) = current_flake_rev(&coord.hyperhive_flake) {
|
if let Some(rev) = current_rev {
|
||||||
let _ = std::fs::write(rev_marker_path(MANAGER_NAME), rev);
|
let _ = std::fs::write(rev_marker_path(MANAGER_NAME), rev);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,42 @@ impl Broker {
|
||||||
Ok(u64::try_from(n.max(0)).unwrap_or(0))
|
Ok(u64::try_from(n.max(0)).unwrap_or(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Long-poll variant of `recv`: returns immediately if there's a
|
||||||
|
/// pending message; otherwise waits up to `timeout` for the broker to
|
||||||
|
/// emit a `Sent { to: recipient }` event, then retries the pop. Lets
|
||||||
|
/// agents react to new mail without polling their socket on a fixed
|
||||||
|
/// interval.
|
||||||
|
pub async fn recv_blocking(
|
||||||
|
&self,
|
||||||
|
recipient: &str,
|
||||||
|
timeout: std::time::Duration,
|
||||||
|
) -> Result<Option<Message>> {
|
||||||
|
if let Some(m) = self.recv(recipient)? {
|
||||||
|
return Ok(Some(m));
|
||||||
|
}
|
||||||
|
let mut rx = self.subscribe();
|
||||||
|
let deadline = tokio::time::Instant::now() + timeout;
|
||||||
|
loop {
|
||||||
|
let Some(remaining) = deadline.checked_duration_since(tokio::time::Instant::now())
|
||||||
|
else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
match tokio::time::timeout(remaining, rx.recv()).await {
|
||||||
|
Err(_) => return Ok(None),
|
||||||
|
// Channel lagged or closed — fall back to a single direct
|
||||||
|
// pop (in case we missed our notification while behind).
|
||||||
|
Ok(Err(_)) => return self.recv(recipient),
|
||||||
|
Ok(Ok(MessageEvent::Sent { to, .. })) if to == recipient => {
|
||||||
|
if let Some(m) = self.recv(recipient)? {
|
||||||
|
return Ok(Some(m));
|
||||||
|
}
|
||||||
|
// Lost a race (concurrent recv elsewhere). Keep waiting.
|
||||||
|
}
|
||||||
|
Ok(Ok(_)) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn recv(&self, recipient: &str) -> Result<Option<Message>> {
|
pub fn recv(&self, recipient: &str) -> Result<Option<Message>> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
let row: Option<(i64, String, String, String)> = conn
|
let row: Option<(i64, String, String, String)> = conn
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,10 @@ pub struct Coordinator {
|
||||||
/// URL of the hyperhive flake (no fragment). Inlined into per-agent
|
/// URL of the hyperhive flake (no fragment). Inlined into per-agent
|
||||||
/// `flake.nix` files as `inputs.hyperhive.url`.
|
/// `flake.nix` files as `inputs.hyperhive.url`.
|
||||||
pub hyperhive_flake: String,
|
pub hyperhive_flake: String,
|
||||||
|
/// TCP port the host's hive-c0re dashboard listens on. Inlined into
|
||||||
|
/// each per-agent flake so the agent's web UI can build the right
|
||||||
|
/// rebuild-button URL pointing back at the dashboard.
|
||||||
|
pub dashboard_port: u16,
|
||||||
agents: Mutex<HashMap<String, AgentSocket>>,
|
agents: Mutex<HashMap<String, AgentSocket>>,
|
||||||
/// Agents whose lifecycle action (currently just spawn) is in flight.
|
/// Agents whose lifecycle action (currently just spawn) is in flight.
|
||||||
/// Read by the dashboard to render a spinner; cleared when the action
|
/// Read by the dashboard to render a spinner; cleared when the action
|
||||||
|
|
@ -51,13 +55,14 @@ pub enum TransientKind {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Coordinator {
|
impl Coordinator {
|
||||||
pub fn open(db_path: &Path, hyperhive_flake: String) -> Result<Self> {
|
pub fn open(db_path: &Path, hyperhive_flake: String, dashboard_port: u16) -> Result<Self> {
|
||||||
let broker = Broker::open(db_path).context("open broker")?;
|
let broker = Broker::open(db_path).context("open broker")?;
|
||||||
let approvals = Approvals::open(db_path).context("open approvals")?;
|
let approvals = Approvals::open(db_path).context("open approvals")?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
broker: Arc::new(broker),
|
broker: Arc::new(broker),
|
||||||
approvals: Arc::new(approvals),
|
approvals: Arc::new(approvals),
|
||||||
hyperhive_flake,
|
hyperhive_flake,
|
||||||
|
dashboard_port,
|
||||||
agents: Mutex::new(HashMap::new()),
|
agents: Mutex::new(HashMap::new()),
|
||||||
transient: Mutex::new(HashMap::new()),
|
transient: Mutex::new(HashMap::new()),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -101,10 +101,11 @@ pub async fn spawn(
|
||||||
proposed_dir: &Path,
|
proposed_dir: &Path,
|
||||||
applied_dir: &Path,
|
applied_dir: &Path,
|
||||||
claude_dir: &Path,
|
claude_dir: &Path,
|
||||||
|
dashboard_port: u16,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
validate(name)?;
|
validate(name)?;
|
||||||
setup_proposed(proposed_dir, name).await?;
|
setup_proposed(proposed_dir, name).await?;
|
||||||
setup_applied(applied_dir, name, hyperhive_flake).await?;
|
setup_applied(applied_dir, name, hyperhive_flake, dashboard_port).await?;
|
||||||
ensure_claude_dir(claude_dir)?;
|
ensure_claude_dir(claude_dir)?;
|
||||||
let container = container_name(name);
|
let container = container_name(name);
|
||||||
let flake_ref = format!("{}#default", applied_dir.display());
|
let flake_ref = format!("{}#default", applied_dir.display());
|
||||||
|
|
@ -145,9 +146,10 @@ pub async fn rebuild(
|
||||||
agent_dir: &Path,
|
agent_dir: &Path,
|
||||||
applied_dir: &Path,
|
applied_dir: &Path,
|
||||||
claude_dir: &Path,
|
claude_dir: &Path,
|
||||||
|
dashboard_port: u16,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
validate(name)?;
|
validate(name)?;
|
||||||
setup_applied(applied_dir, name, hyperhive_flake).await?;
|
setup_applied(applied_dir, name, hyperhive_flake, dashboard_port).await?;
|
||||||
ensure_claude_dir(claude_dir)?;
|
ensure_claude_dir(claude_dir)?;
|
||||||
let container = container_name(name);
|
let container = container_name(name);
|
||||||
let flake_ref = format!("{}#default", applied_dir.display());
|
let flake_ref = format!("{}#default", applied_dir.display());
|
||||||
|
|
@ -205,7 +207,12 @@ pub async fn setup_proposed(proposed_dir: &Path, name: &str) -> Result<()> {
|
||||||
/// Maintain the authoritative applied repo. Rewrites `flake.nix` every call
|
/// Maintain the authoritative applied repo. Rewrites `flake.nix` every call
|
||||||
/// (so a new hyperhive flake URL propagates on rebuild); seeds `agent.nix`
|
/// (so a new hyperhive flake URL propagates on rebuild); seeds `agent.nix`
|
||||||
/// only on first call. `apply_commit` overwrites `agent.nix` later.
|
/// only on first call. `apply_commit` overwrites `agent.nix` later.
|
||||||
pub async fn setup_applied(applied_dir: &Path, name: &str, hyperhive_flake: &str) -> Result<()> {
|
pub async fn setup_applied(
|
||||||
|
applied_dir: &Path,
|
||||||
|
name: &str,
|
||||||
|
hyperhive_flake: &str,
|
||||||
|
dashboard_port: u16,
|
||||||
|
) -> Result<()> {
|
||||||
std::fs::create_dir_all(applied_dir)
|
std::fs::create_dir_all(applied_dir)
|
||||||
.with_context(|| format!("create {}", applied_dir.display()))?;
|
.with_context(|| format!("create {}", applied_dir.display()))?;
|
||||||
|
|
||||||
|
|
@ -242,6 +249,7 @@ pub async fn setup_applied(applied_dir: &Path, name: &str, hyperhive_flake: &str
|
||||||
systemd.services.{service}.environment = {{
|
systemd.services.{service}.environment = {{
|
||||||
HIVE_PORT = "{port}";
|
HIVE_PORT = "{port}";
|
||||||
HIVE_LABEL = "{name}";
|
HIVE_LABEL = "{name}";
|
||||||
|
HIVE_DASHBOARD_PORT = "{dashboard_port}";
|
||||||
}};
|
}};
|
||||||
}}
|
}}
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ async fn main() -> Result<()> {
|
||||||
db,
|
db,
|
||||||
dashboard_port,
|
dashboard_port,
|
||||||
} => {
|
} => {
|
||||||
let coord = Arc::new(Coordinator::open(&db, hyperhive_flake)?);
|
let coord = Arc::new(Coordinator::open(&db, hyperhive_flake, dashboard_port)?);
|
||||||
manager_server::start(coord.clone())?;
|
manager_server::start(coord.clone())?;
|
||||||
// Auto-create the manager container if it isn't there yet. Block
|
// Auto-create the manager container if it isn't there yet. Block
|
||||||
// on this — without hm1nd the system has no manager harness.
|
// on this — without hm1nd the system has no manager harness.
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,8 @@ async fn serve(stream: UnixStream, coord: Arc<Coordinator>) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MANAGER_RECV_LONG_POLL: std::time::Duration = std::time::Duration::from_secs(30);
|
||||||
|
|
||||||
async fn dispatch(req: &ManagerRequest, coord: &Coordinator) -> ManagerResponse {
|
async fn dispatch(req: &ManagerRequest, coord: &Coordinator) -> ManagerResponse {
|
||||||
match req {
|
match req {
|
||||||
ManagerRequest::Send { to, body } => match coord.broker.send(&Message {
|
ManagerRequest::Send { to, body } => match coord.broker.send(&Message {
|
||||||
|
|
@ -97,7 +99,11 @@ async fn dispatch(req: &ManagerRequest, coord: &Coordinator) -> ManagerResponse
|
||||||
message: format!("{e:#}"),
|
message: format!("{e:#}"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ManagerRequest::Recv => match coord.broker.recv(MANAGER_AGENT) {
|
ManagerRequest::Recv => match coord
|
||||||
|
.broker
|
||||||
|
.recv_blocking(MANAGER_AGENT, MANAGER_RECV_LONG_POLL)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(Some(msg)) => ManagerResponse::Message {
|
Ok(Some(msg)) => ManagerResponse::Message {
|
||||||
from: msg.from,
|
from: msg.from,
|
||||||
body: msg.body,
|
body: msg.body,
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
|
||||||
&proposed_dir,
|
&proposed_dir,
|
||||||
&applied_dir,
|
&applied_dir,
|
||||||
&claude_dir,
|
&claude_dir,
|
||||||
|
coord.dashboard_port,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|
@ -110,6 +111,7 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
|
||||||
&agent_dir,
|
&agent_dir,
|
||||||
&applied_dir,
|
&applied_dir,
|
||||||
&claude_dir,
|
&claude_dir,
|
||||||
|
coord.dashboard_port,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
HostResponse::success()
|
HostResponse::success()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue