Watch
0
0
Fork
You've already forked hyperhive
0

agent permissions: drop hive-c0re's swarm-agent-permissions.json fallback, fail closed before the first read

Removes the HiveCopy hand-off: hive-c0re no longer writes each agent's
entry from swarm-agent-permissions.json into its runtime dir as
swarm-permissions.json, and the harness and subagent daemon no longer
read it. hive-c0re keeps the file as its own last read of the swarm for
the manage_root_agent mounts. Also removes permissions_import::pending(),
its only caller being the hand-off. No nix provisioned the copy, so no
nix option is removed and no tombstone is needed.

Rule after this change: until an agent's first successful read of its
key it holds no permissions (no execution, so no bash), and a subagent it
starts holds the same. After a read a set key wins, [] holds none and an
unset key is the default. A failed read keeps the last value, so an
unreachable queue at start keeps the agent closed until a read succeeds.
The harness logs once at warn that it starts closed.

Fixes #5315
This commit is contained in:
atlas 2026-10-21 01:56:37 +02:00 • committed by mara
commit 2f00f9041f
11 changed files with 121 additions and 283 deletions

View file

@ -20,7 +20,6 @@
//! [`Read::Failed`]: the caller checks against `HIVE_AVAILABLE_MODELS` and
//! applies no cap.
use std::path::Path;
#[cfg(test)]
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, OnceLock};
@ -28,7 +27,7 @@ use std::time::Duration;
use hive_types::Permission;
use swarm_queue_client::agent_inference::{self, read_own};
use swarm_queue_client::agent_permissions::{self, HIVE_COPY_PATH, HiveCopy};
use swarm_queue_client::agent_permissions::{self, BEFORE_FIRST_READ, held};
use swarm_wire::agent_inference::AgentInference;
use crate::swarm_term::{Target, connect};
@ -38,15 +37,15 @@ use crate::swarm_term::{Target, connect};
static LAST_PERMISSIONS: std::sync::RwLock<Option<Vec<Permission>>> = std::sync::RwLock::new(None);
/// The permissions this agent holds now, for the subagent being spawned: the
/// last set read, or what hive-c0re stored for the agent before any read
/// succeeds ([`HiveCopy::before_read`]).
/// last set read, or nothing before any read succeeds
/// ([`BEFORE_FIRST_READ`]).
#[must_use]
pub fn permissions_now() -> Vec<Permission> {
let last = LAST_PERMISSIONS
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
last.unwrap_or_else(|| HiveCopy::read_at(Path::new(HIVE_COPY_PATH)).before_read())
last.unwrap_or_else(|| BEFORE_FIRST_READ.to_vec())
}
/// How long after a failed connect the task tries again.
@ -150,8 +149,8 @@ impl Reader {
}
/// Read the permissions the swarm grants this agent and keep them for
/// [`permissions_now`], judged by [`HiveCopy::after_read`]. A read that
/// fails keeps the last value.
/// [`permissions_now`], with an unset key the default. A read that fails keeps the
/// last value.
pub async fn refresh_permissions(&self) {
let read = match &self.source {
Source::Queue { agent, client } => read_permissions(agent, client).await,
@ -173,9 +172,7 @@ async fn read_permissions(
let client = client.get()?;
swarm_queue_client::ensure_connected(client).ok()?;
match tokio::time::timeout(READ_TIMEOUT, agent_permissions::read(client, agent)).await {
Ok(Ok(agent_permissions::Read::Current(record))) => {
Some(HiveCopy::read_at(Path::new(HIVE_COPY_PATH)).after_read(&record))
}
Ok(Ok(agent_permissions::Read::Current(record))) => Some(held(&record)),
Ok(Ok(agent_permissions::Read::NoBucket)) => None,
Ok(Err(e)) => {
tracing::warn!(error = %e, "subagent permissions: reading this agent's key failed");
@ -238,4 +235,16 @@ mod tests {
let client = OnceLock::new();
assert!(matches!(read_queue("iris", &client).await, Read::Failed(_)));
}
/// No test sets `LAST_PERMISSIONS` (a fixed reader never reads the
/// permissions), so this is the state before the first read.
#[test]
fn before_the_first_read_a_subagent_gets_no_execution() {
let held = permissions_now();
assert!(held.is_empty(), "held {held:?} before any read");
assert!(
Permission::DEFAULT.contains(&Permission::Execution),
"control: the default does hold it"
);
}
}