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:
parent
6e7b5f84e9
commit
2f00f9041f
11 changed files with 121 additions and 283 deletions
|
|
@ -51,7 +51,7 @@ operator-profiles = ["async-nats/jetstream"]
|
|||
# `agent_limits::read`, for hive-c0re, which reads the key of each agent it
|
||||
# runs the same way. Explicit for the same reason as `notices`.
|
||||
agent-limits = ["async-nats/jetstream"]
|
||||
# `agent_permissions::read` and `HiveCopy`, for hive-c0re, the agent
|
||||
# `agent_permissions::read` and `held`, for hive-c0re, the agent
|
||||
# harness, the subagent daemon and swarm-controller. Pulls `hive-types` for the
|
||||
# `Permission` names, so the crates that only need the bucket name do not link
|
||||
# it.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
//! Opening and reading the per-agent permissions KV bucket, and the copy
|
||||
//! hive-c0re keeps of a record for the agent's own processes. The names, the
|
||||
//! 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`.
|
||||
|
||||
|
|
@ -34,72 +33,21 @@ pub enum Read {
|
|||
NoBucket,
|
||||
}
|
||||
|
||||
/// The file hive-c0re writes an agent's [`HiveCopy`] to in that agent's
|
||||
/// runtime dir.
|
||||
/// 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 HIVE_COPY_FILE: &str = "swarm-permissions.json";
|
||||
pub const BEFORE_FIRST_READ: &[hive_types::Permission] = &[];
|
||||
|
||||
/// Where the agent's own processes read [`HIVE_COPY_FILE`]: the runtime dir
|
||||
/// is mounted at `/run/hive` in the container.
|
||||
/// The permissions held under `record`: its list when set (`[]` holds none),
|
||||
/// the default when unset.
|
||||
#[cfg(feature = "agent-permissions")]
|
||||
pub const HIVE_COPY_PATH: &str = "/run/hive/swarm-permissions.json";
|
||||
|
||||
/// What hive-c0re holds for one agent: its record as the hive last stored it
|
||||
/// (read from this bucket, or taken from the meta repo before the import),
|
||||
/// and whether the meta repo's import is still pending. The agent's harness
|
||||
/// and subagent daemon hold [`HiveCopy::before_read`] until their own first
|
||||
/// read of the key succeeds, and judge every read with
|
||||
/// [`HiveCopy::after_read`].
|
||||
#[cfg(feature = "agent-permissions")]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct HiveCopy {
|
||||
#[serde(flatten)]
|
||||
pub stored: AgentPermissions,
|
||||
#[serde(default)]
|
||||
pub import_pending: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "agent-permissions")]
|
||||
impl HiveCopy {
|
||||
/// The copy at `path`. A missing or unreadable file is the empty copy,
|
||||
/// which holds the default and has no import pending.
|
||||
#[must_use]
|
||||
pub fn read_at(path: &std::path::Path) -> Self {
|
||||
let bytes = match std::fs::read(path) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Self::default(),
|
||||
Err(e) => {
|
||||
tracing::warn!(path = %path.display(), error = %e, "permissions: the hive's copy is unreadable");
|
||||
return Self::default();
|
||||
}
|
||||
};
|
||||
serde_json::from_slice(&bytes).unwrap_or_else(|e| {
|
||||
tracing::warn!(path = %path.display(), error = %e, "permissions: the hive's copy does not decode");
|
||||
Self::default()
|
||||
})
|
||||
}
|
||||
|
||||
/// What the agent holds before any read of its key has succeeded: the
|
||||
/// stored list, or the default when the hive stored none.
|
||||
#[must_use]
|
||||
pub fn before_read(&self) -> Vec<hive_types::Permission> {
|
||||
self.stored
|
||||
.permissions
|
||||
.clone()
|
||||
.unwrap_or_else(|| hive_types::Permission::DEFAULT.to_vec())
|
||||
}
|
||||
|
||||
/// What the agent holds after reading `read` from its key. An unset key
|
||||
/// while the import is pending may only mean the grant is not imported
|
||||
/// yet, so the stored list stands; otherwise unset is the default.
|
||||
#[must_use]
|
||||
pub fn after_read(&self, read: &AgentPermissions) -> Vec<hive_types::Permission> {
|
||||
match &read.permissions {
|
||||
Some(list) => list.clone(),
|
||||
None if self.import_pending => self.before_read(),
|
||||
None => hive_types::Permission::DEFAULT.to_vec(),
|
||||
}
|
||||
}
|
||||
#[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
|
||||
|
|
@ -162,28 +110,23 @@ fn decode(operation: Option<&str>, payload: &[u8]) -> serde_json::Result<AgentPe
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Before a read the stored list stands; after one, a set key wins and an
|
||||
/// unset key is the default unless the import is pending.
|
||||
#[cfg(feature = "agent-permissions")]
|
||||
/// A set key wins, `[]` holds none, and an unset key is the default.
|
||||
#[test]
|
||||
fn the_hive_copy_stands_until_read_and_while_the_import_is_pending() {
|
||||
fn an_unset_key_holds_the_default() {
|
||||
use hive_types::Permission;
|
||||
let record = |json: &str| -> AgentPermissions { serde_json::from_str(json).unwrap() };
|
||||
let copy = |json: &str| -> HiveCopy { serde_json::from_str(json).unwrap() };
|
||||
let web = vec![Permission::WebTools];
|
||||
let default = Permission::DEFAULT.to_vec();
|
||||
|
||||
assert_eq!(copy(r#"{"permissions":["web_tools"]}"#).before_read(), web);
|
||||
assert_eq!(copy("{}").before_read(), default);
|
||||
|
||||
let pending = copy(r#"{"permissions":["web_tools"],"import_pending":true}"#);
|
||||
assert_eq!(pending.after_read(&record("{}")), web);
|
||||
assert_eq!(
|
||||
pending.after_read(&record(r#"{"permissions":[]}"#)),
|
||||
Vec::new()
|
||||
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"
|
||||
);
|
||||
let settled = copy(r#"{"permissions":["web_tools"]}"#);
|
||||
assert_eq!(settled.after_read(&record("{}")), default);
|
||||
}
|
||||
|
||||
/// Fields a newer controller adds must not cost an older hive the ones it
|
||||
|
|
|
|||
Loading…
Reference in a new issue