Watch
0
0
Fork
You've already forked hyperhive
0

logs and metrics permissions, tolerant record decode, hive capabilities

This commit is contained in:
atlas 2026-11-01 17:40:09 +01:00
commit 04598542b6
2 changed files with 226 additions and 8 deletions

View file

@ -9,7 +9,7 @@
mod permission;
pub use permission::Permission;
pub use permission::{Permission, deserialize_known};
pub mod review;
pub mod term_msg;

View file

@ -38,6 +38,15 @@ pub enum Permission {
/// thing to manage. Kept under that spelling because every stored grant
/// carries it.
ManageRootAgent,
/// Read access to the log store: what `swarm-logs` queries. Checked by
/// the log gateway's `swarm-logs-auth` against the grant swarm-controller
/// mirrors into the secret store, so `tools()` is empty. Not in
/// [`Permission::DEFAULT`].
Logs,
/// Read access to the metrics store: what `swarm-metrics` queries.
/// Checked like [`Permission::Logs`], for the metrics gateway. Not in
/// [`Permission::DEFAULT`].
Metrics,
}
impl Permission {
@ -49,6 +58,8 @@ impl Permission {
Self::Execution,
Self::WebTools,
Self::ManageRootAgent,
Self::Logs,
Self::Metrics,
];
/// What an agent the swarm sets nothing for holds, whatever its name.
@ -69,8 +80,13 @@ impl Permission {
// — an out-of-process server has no later enforcement point, so
// that gate is the actual security boundary. `WebTools` grants
// Claude built-ins (`builtin_tools()`); `ManageRootAgent` grants
// bind mounts.
Self::Execution | Self::WebTools | Self::ManageRootAgent => &[],
// bind mounts; `Logs` and `Metrics` are checked by the store
// gateways, not by a tool allowlist.
Self::Execution
| Self::WebTools
| Self::ManageRootAgent
| Self::Logs
| Self::Metrics => &[],
}
}
@ -115,8 +131,43 @@ impl Permission {
Self::Execution => "execution",
Self::WebTools => "web_tools",
Self::ManageRootAgent => "manage_root_agent",
Self::Logs => "logs",
Self::Metrics => "metrics",
}
}
/// The permission stored under `name`; `None` for a name this build does
/// not know.
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
Self::ALL.iter().copied().find(|p| p.as_str() == name)
}
}
/// Decodes a stored permission list, skipping names this build does not know.
/// For `#[serde(deserialize_with)]` on a stored record: a permission a newer
/// swarm adds must not cost an older reader the rest of the record.
///
/// # Errors
///
/// When the value is not a list of strings.
pub fn deserialize_known<'de, D>(d: D) -> Result<Option<Vec<Permission>>, D::Error>
where
D: serde::Deserializer<'de>,
{
let names: Option<Vec<String>> = Option::deserialize(d)?;
Ok(names.map(|names| {
names
.iter()
.filter_map(|name| {
let known = Permission::from_name(name);
if known.is_none() {
tracing::debug!(name, "skipping a permission this build does not know");
}
known
})
.collect()
}))
}
impl From<Permission> for &'static str {
@ -142,11 +193,46 @@ mod tests {
"inbox",
"execution",
"web_tools",
"manage_root_agent"
"manage_root_agent",
"logs",
"metrics"
]
);
}
/// Reading stores a log or metric grant is never a default: an agent the
/// swarm sets nothing for does not hold them.
#[test]
fn logs_and_metrics_are_off_by_default() {
assert!(!Permission::DEFAULT.contains(&Permission::Logs));
assert!(!Permission::DEFAULT.contains(&Permission::Metrics));
assert!(Permission::ALL.contains(&Permission::Logs));
assert!(Permission::ALL.contains(&Permission::Metrics));
}
#[derive(serde::Deserialize)]
struct Record {
#[serde(default, deserialize_with = "deserialize_known")]
permissions: Option<Vec<Permission>>,
}
#[test]
fn an_unknown_name_is_skipped_not_a_decode_failure() {
let r: Record =
serde_json::from_str(r#"{"permissions":["messaging","telepathy","logs"]}"#).unwrap();
assert_eq!(
r.permissions,
Some(vec![Permission::Messaging, Permission::Logs])
);
// Presence control: an unset field stays unset, `[]` stays empty.
let r: Record = serde_json::from_str("{}").unwrap();
assert_eq!(r.permissions, None);
let r: Record = serde_json::from_str(r#"{"permissions":[]}"#).unwrap();
assert_eq!(r.permissions, Some(vec![]));
// A list of non-strings is still a decode failure.
assert!(serde_json::from_str::<Record>(r#"{"permissions":[1]}"#).is_err());
}
/// The serde form of every variant, pinned independently of `as_str`:
/// stored grants decode by it, so it must stay byte-identical.
#[test]
@ -158,6 +244,8 @@ mod tests {
(Permission::Execution, "\"execution\""),
(Permission::WebTools, "\"web_tools\""),
(Permission::ManageRootAgent, "\"manage_root_agent\""),
(Permission::Logs, "\"logs\""),
(Permission::Metrics, "\"metrics\""),
];
assert_eq!(want.len(), Permission::ALL.len());
for (perm, json) in want {