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
152 lines
5.2 KiB
Rust
152 lines
5.2 KiB
Rust
//! Opening and reading the per-agent permissions KV bucket. The names, the
|
|
//! stored type and the import messages are `swarm_wire::agent_permissions`;
|
|
//! reading needs the `agent-permissions` feature and writing needs `kv`.
|
|
|
|
#[cfg(feature = "agent-permissions")]
|
|
use swarm_wire::agent_permissions::AgentPermissions;
|
|
|
|
/// Open the permissions bucket, creating it if nothing has yet.
|
|
///
|
|
/// **Controller-side only.** `history: 1`: a hive wants the current value.
|
|
#[cfg(feature = "kv")]
|
|
pub async fn open_or_create(
|
|
client: &async_nats::Client,
|
|
) -> Result<async_nats::jetstream::kv::Store, crate::Error> {
|
|
let js = async_nats::jetstream::new(client.clone());
|
|
crate::kv::open_or_create(
|
|
&js,
|
|
swarm_wire::agent_permissions::BUCKET,
|
|
"agent-permissions",
|
|
"Permissions the swarm controller sets for each agent".to_owned(),
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// What reading one agent's key found.
|
|
#[cfg(feature = "agent-permissions")]
|
|
#[derive(Debug)]
|
|
pub enum Read {
|
|
/// The bucket answered; an absent or deleted key has every field unset.
|
|
Current(AgentPermissions),
|
|
/// Nothing answers for the bucket's stream. Not an unset value: it says
|
|
/// nothing about the agent's permissions.
|
|
NoBucket,
|
|
}
|
|
|
|
/// What an agent holds until its first read of its key succeeds: nothing, the
|
|
/// narrowest set. Not the default, which includes `Execution`; a read of an
|
|
/// unset key is what makes an agent hold the default.
|
|
#[cfg(feature = "agent-permissions")]
|
|
pub const BEFORE_FIRST_READ: &[hive_types::Permission] = &[];
|
|
|
|
/// The permissions held under `record`: its list when set (`[]` holds none),
|
|
/// the default when unset.
|
|
#[cfg(feature = "agent-permissions")]
|
|
#[must_use]
|
|
pub fn held(record: &AgentPermissions) -> Vec<hive_types::Permission> {
|
|
record
|
|
.permissions
|
|
.clone()
|
|
.unwrap_or_else(|| hive_types::Permission::DEFAULT.to_vec())
|
|
}
|
|
|
|
/// Read `agent`'s current permissions with one direct get: the one subject
|
|
/// an agent's own grant covers, and one of those a hive's covers.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// A failed request or an undecodable value. A request the queue refused for
|
|
/// permissions reaches the client as a timeout, not as a refusal.
|
|
#[cfg(feature = "agent-permissions")]
|
|
pub async fn read(
|
|
client: &async_nats::Client,
|
|
agent: &str,
|
|
) -> Result<Read, Box<dyn std::error::Error + Send + Sync>> {
|
|
use async_nats::jetstream::stream::DirectGetErrorKind;
|
|
|
|
let js = async_nats::jetstream::new(client.clone());
|
|
let stream = js
|
|
.get_stream_no_info(swarm_wire::agent_permissions::STREAM)
|
|
.await?;
|
|
let message = match stream
|
|
.direct_get_last_for_subject(swarm_wire::agent_permissions::key_subject(agent))
|
|
.await
|
|
{
|
|
Ok(message) => message,
|
|
Err(e) if matches!(e.kind(), DirectGetErrorKind::NotFound) => {
|
|
return Ok(Read::Current(AgentPermissions::default()));
|
|
}
|
|
Err(e)
|
|
if matches!(
|
|
e.kind(),
|
|
DirectGetErrorKind::ErrorResponse(status, _)
|
|
if status == async_nats::StatusCode::NO_RESPONDERS
|
|
) =>
|
|
{
|
|
return Ok(Read::NoBucket);
|
|
}
|
|
Err(e) => return Err(e.into()),
|
|
};
|
|
Ok(Read::Current(decode(
|
|
message
|
|
.headers
|
|
.get("KV-Operation")
|
|
.map(async_nats::HeaderValue::as_str),
|
|
&message.payload,
|
|
)?))
|
|
}
|
|
|
|
/// One stored message as a value: a KV delete or purge is stored as an empty
|
|
/// marker carrying the `KV-Operation` header, and reads as every field unset.
|
|
#[cfg(feature = "agent-permissions")]
|
|
fn decode(operation: Option<&str>, payload: &[u8]) -> serde_json::Result<AgentPermissions> {
|
|
if matches!(operation, Some("DEL" | "PURGE")) {
|
|
return Ok(AgentPermissions::default());
|
|
}
|
|
serde_json::from_slice(payload)
|
|
}
|
|
|
|
#[cfg(all(test, feature = "agent-permissions"))]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// A set key wins, `[]` holds none, and an unset key is the default.
|
|
#[test]
|
|
fn an_unset_key_holds_the_default() {
|
|
use hive_types::Permission;
|
|
let record = |json: &str| -> AgentPermissions { serde_json::from_str(json).unwrap() };
|
|
|
|
assert_eq!(
|
|
held(&record(r#"{"permissions":["web_tools"]}"#)),
|
|
vec![Permission::WebTools]
|
|
);
|
|
assert_eq!(held(&record(r#"{"permissions":[]}"#)), Vec::new());
|
|
assert_eq!(held(&record("{}")), Permission::DEFAULT.to_vec());
|
|
assert_ne!(
|
|
Permission::DEFAULT.to_vec(),
|
|
Vec::new(),
|
|
"control: the default is not the empty list"
|
|
);
|
|
}
|
|
|
|
/// Fields a newer controller adds must not cost an older hive the ones it
|
|
/// knows; a deleted key is unset.
|
|
#[cfg(feature = "agent-permissions")]
|
|
#[test]
|
|
fn decoding_tolerates_unknown_fields_and_reads_a_delete_as_unset() {
|
|
let decoded = decode(
|
|
None,
|
|
br#"{"permissions":["manage_root_agent"],"later_field":["meta"]}"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(decoded.permissions.map(|c| c.len()), Some(1));
|
|
assert_eq!(
|
|
decode(Some("DEL"), b"").unwrap(),
|
|
AgentPermissions::default()
|
|
);
|
|
assert_eq!(
|
|
decode(Some("PURGE"), b"").unwrap(),
|
|
AgentPermissions::default()
|
|
);
|
|
}
|
|
}
|