move Permission into hive-types, drop swarm-controller's hive-sh4re dep
This commit is contained in:
parent
04786da58f
commit
e4a9005468
27 changed files with 221 additions and 175 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
|
@ -1675,6 +1675,7 @@ dependencies = [
|
||||||
"hive-runtime",
|
"hive-runtime",
|
||||||
"hive-sh4re",
|
"hive-sh4re",
|
||||||
"hive-sock-client",
|
"hive-sock-client",
|
||||||
|
"hive-types",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
|
|
@ -2024,7 +2025,6 @@ dependencies = [
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"strum",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4876,7 +4876,6 @@ dependencies = [
|
||||||
"hive-jobq-metrics",
|
"hive-jobq-metrics",
|
||||||
"hive-jobq-wire",
|
"hive-jobq-wire",
|
||||||
"hive-log",
|
"hive-log",
|
||||||
"hive-sh4re",
|
|
||||||
"hive-types",
|
"hive-types",
|
||||||
"hmac 0.13.0",
|
"hmac 0.13.0",
|
||||||
"http",
|
"http",
|
||||||
|
|
@ -5031,7 +5030,7 @@ dependencies = [
|
||||||
"async-nats",
|
"async-nats",
|
||||||
"axum",
|
"axum",
|
||||||
"data-encoding",
|
"data-encoding",
|
||||||
"hive-sh4re",
|
"hive-types",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
||||||
|
|
@ -272,7 +272,7 @@ JSON-line protocol with their own result types; RFC 9457 is an HTTP format.
|
||||||
|
|
||||||
## Permissions
|
## Permissions
|
||||||
|
|
||||||
One set of named `Permission` values (`hive_sh4re::permissions::Permission`)
|
One set of named `Permission` values (`hive_types::Permission`)
|
||||||
decides what an agent may do: which MCP and built-in tools it gets, and which
|
decides what an agent may do: which MCP and built-in tools it gets, and which
|
||||||
host access it has.
|
host access it has.
|
||||||
|
|
||||||
|
|
@ -342,7 +342,7 @@ agent grant itself any tool by submitting a config commit, bypassing the
|
||||||
operator gate.
|
operator gate.
|
||||||
|
|
||||||
**Adding to the set** — add a variant to `Permission` in
|
**Adding to the set** — add a variant to `Permission` in
|
||||||
`hive-sh4re/src/permissions.rs` and to `Permission::ALL`, and enforce it where
|
`hive-types/src/permission.rs` and to `Permission::ALL`, and enforce it where
|
||||||
it applies. A new `#[tool]` fn on `AgentServer` (`hive-agent-mcp/src/mcp/mod.rs`)
|
it applies. A new `#[tool]` fn on `AgentServer` (`hive-agent-mcp/src/mcp/mod.rs`)
|
||||||
goes into the matching `Permission::tools()` slice: that's the single source
|
goes into the matching `Permission::tools()` slice: that's the single source
|
||||||
of truth `mcp_config::allowed_mcp_tools` (`hive-agent/src/mcp_config.rs`)
|
of truth `mcp_config::allowed_mcp_tools` (`hive-agent/src/mcp_config.rs`)
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,8 @@ status hint lives in the wake prompt + UI header, not here.
|
||||||
## Tool allowlist (`hive_sh4re::permissions::ALLOWED_BUILTIN_TOOLS`)
|
## Tool allowlist (`hive_sh4re::permissions::ALLOWED_BUILTIN_TOOLS`)
|
||||||
|
|
||||||
The built-in list and the `--tools` value it resolves to live in
|
The built-in list and the `--tools` value it resolves to live in
|
||||||
`hive-sh4re` alongside `Permission`, not in the harness, because the
|
`hive-sh4re` (the `Permission` enum itself is in `hive-types`), not in the
|
||||||
|
harness, because the
|
||||||
subagent daemon spawns its own `claude` and must resolve the same set —
|
subagent daemon spawns its own `claude` and must resolve the same set —
|
||||||
see [`docs/tools/subagent.md`](../tools/subagent.md).
|
see [`docs/tools/subagent.md`](../tools/subagent.md).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ impl AgentServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
// IMPORTANT: when adding a new `#[tool]` fn to this impl, also add
|
// IMPORTANT: when adding a new `#[tool]` fn to this impl, also add
|
||||||
// its name to the matching `Permission::tools()` slice in hive-sh4re.
|
// its name to the matching `Permission::tools()` slice in hive-types.
|
||||||
// Claude Code's permission gate refuses uninlisted MCP tools in
|
// Claude Code's permission gate refuses uninlisted MCP tools in
|
||||||
// non-interactive `--print` mode with "permissions not granted yet".
|
// non-interactive `--print` mode with "permissions not granted yet".
|
||||||
#[tool_router]
|
#[tool_router]
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ hive-core-agent-sock.workspace = true
|
||||||
hive-log.workspace = true
|
hive-log.workspace = true
|
||||||
hive-sh4re.workspace = true
|
hive-sh4re.workspace = true
|
||||||
hive-sock-client.workspace = true
|
hive-sock-client.workspace = true
|
||||||
|
hive-types.workspace = true
|
||||||
libc.workspace = true
|
libc.workspace = true
|
||||||
opentelemetry.workspace = true
|
opentelemetry.workspace = true
|
||||||
opentelemetry_sdk.workspace = true
|
opentelemetry_sdk.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
//! running MCP server (a separate binary) — the `send` allow-list check that
|
//! running MCP server (a separate binary) — the `send` allow-list check that
|
||||||
//! server enforces lives alongside it in the `hive-agent-mcp` crate.
|
//! server enforces lives alongside it in the `hive-agent-mcp` crate.
|
||||||
|
|
||||||
use hive_sh4re::permissions::Permission;
|
use hive_types::Permission;
|
||||||
|
|
||||||
/// Name of the hyperhive MCP server inside claude's view. Claude prefixes
|
/// Name of the hyperhive MCP server inside claude's view. Claude prefixes
|
||||||
/// tools as `mcp__<this>__<tool>` (e.g. `mcp__hyperhive__send`).
|
/// tools as `mcp__<this>__<tool>` (e.g. `mcp__hyperhive__send`).
|
||||||
|
|
@ -55,7 +55,7 @@ fn extra_server_enabled(server: &str, perms: &[Permission]) -> bool {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod extra_server_gate_tests {
|
mod extra_server_gate_tests {
|
||||||
use super::{extra_server_enabled, extra_server_required_permission};
|
use super::{extra_server_enabled, extra_server_required_permission};
|
||||||
use hive_sh4re::permissions::Permission;
|
use hive_types::Permission;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bash_is_gated_behind_execution() {
|
fn bash_is_gated_behind_execution() {
|
||||||
|
|
@ -84,7 +84,7 @@ mod extra_server_gate_tests {
|
||||||
|
|
||||||
/// MCP tools claude is allowed to call without prompting, derived from
|
/// MCP tools claude is allowed to call without prompting, derived from
|
||||||
/// the supplied permissions. Adding a new `#[tool]` fn to a server impl
|
/// the supplied permissions. Adding a new `#[tool]` fn to a server impl
|
||||||
/// requires updating the matching `Permission::tools()` slice in hive-sh4re
|
/// requires updating the matching `Permission::tools()` slice in hive-types
|
||||||
/// (single source of truth).
|
/// (single source of truth).
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn allowed_mcp_tools(perms: &[Permission]) -> Vec<String> {
|
pub fn allowed_mcp_tools(perms: &[Permission]) -> Vec<String> {
|
||||||
|
|
@ -212,7 +212,7 @@ fn build_mcp_servers() -> serde_json::Map<String, serde_json::Value> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{SERVER_NAME, allowed_mcp_tools};
|
use super::{SERVER_NAME, allowed_mcp_tools};
|
||||||
use hive_sh4re::permissions::Permission;
|
use hive_types::Permission;
|
||||||
|
|
||||||
fn qualified(tool: &str) -> String {
|
fn qualified(tool: &str) -> String {
|
||||||
format!("mcp__{SERVER_NAME}__{tool}")
|
format!("mcp__{SERVER_NAME}__{tool}")
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ use std::path::Path;
|
||||||
use std::sync::{PoisonError, RwLock};
|
use std::sync::{PoisonError, RwLock};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use hive_sh4re::permissions::Permission;
|
use hive_types::Permission;
|
||||||
use swarm_queue_client::agent_permissions::{HIVE_COPY_PATH, HiveCopy, Read, read};
|
use swarm_queue_client::agent_permissions::{HIVE_COPY_PATH, HiveCopy, Read, read};
|
||||||
|
|
||||||
/// How often the key is read. Also the longest a change can take to reach
|
/// How often the key is read. Also the longest a change can take to reach
|
||||||
|
|
|
||||||
|
|
@ -351,8 +351,7 @@ pub fn make_session(bus: &Bus) -> Result<AgentSession> {
|
||||||
/// against the agent's permissions at the moment it asks.
|
/// against the agent's permissions at the moment it asks.
|
||||||
fn acp_permission_policy() -> PermissionPolicy {
|
fn acp_permission_policy() -> PermissionPolicy {
|
||||||
std::sync::Arc::new(|ask: &PermissionAsk<'_>| {
|
std::sync::Arc::new(|ask: &PermissionAsk<'_>| {
|
||||||
let web = crate::swarm_permissions::current()
|
let web = crate::swarm_permissions::current().contains(&hive_types::Permission::WebTools);
|
||||||
.contains(&hive_sh4re::permissions::Permission::WebTools);
|
|
||||||
acp_permits(ask, web)
|
acp_permits(ask, web)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use hive_sh4re::permissions::Permission;
|
use hive_types::Permission;
|
||||||
use swarm_queue_client::agent_permissions::{AgentPermissions, HIVE_COPY_FILE, HiveCopy};
|
use swarm_queue_client::agent_permissions::{AgentPermissions, HIVE_COPY_FILE, HiveCopy};
|
||||||
|
|
||||||
const FILE: &str = "swarm-agent-permissions.json";
|
const FILE: &str = "swarm-agent-permissions.json";
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
//! dashboard's tree view. All of that is gone; what the file is
|
//! dashboard's tree view. All of that is gone; what the file is
|
||||||
//! *for* now is the one thing that survived the removal — naming every
|
//! *for* now is the one thing that survived the removal — naming every
|
||||||
//! agent, which is the set a
|
//! agent, which is the set a
|
||||||
//! [`hive_sh4re::permissions::Permission::ManageRootAgent`] holder gets
|
//! [`hive_types::Permission::ManageRootAgent`] holder gets
|
||||||
//! bind-mounted ([`all_agents`]).
|
//! bind-mounted ([`all_agents`]).
|
||||||
//!
|
//!
|
||||||
//! Broader-than-your-own bind-mount grants are **not** stored here: they
|
//! Broader-than-your-own bind-mount grants are **not** stored here: they
|
||||||
|
|
@ -65,7 +65,7 @@ fn read_from(path: &Path) -> std::io::Result<BTreeSet<String>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every agent the roster knows about, in name order. This is the set
|
/// Every agent the roster knows about, in name order. This is the set
|
||||||
/// a [`hive_sh4re::permissions::Permission::ManageRootAgent`] holder gets
|
/// a [`hive_types::Permission::ManageRootAgent`] holder gets
|
||||||
/// bind-mounted, and it is deliberately unfiltered: that permission means
|
/// bind-mounted, and it is deliberately unfiltered: that permission means
|
||||||
/// "may manage any agent", so the set is all of them.
|
/// "may manage any agent", so the set is all of them.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use hive_priv_sock::{BindMount, CredentialMount};
|
use hive_priv_sock::{BindMount, CredentialMount};
|
||||||
use hive_sh4re::permissions::Permission;
|
use hive_types::Permission;
|
||||||
|
|
||||||
use crate::coordinator::{AgentPaths, HiveEnv};
|
use crate::coordinator::{AgentPaths, HiveEnv};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use hive_sh4re::permissions::Permission;
|
use hive_types::Permission;
|
||||||
use swarm_queue_client::agent_permissions::{
|
use swarm_queue_client::agent_permissions::{
|
||||||
self, AgentPermissions, IMPORT_REPLY_DEADLINE, Import, ImportReply,
|
self, AgentPermissions, IMPORT_REPLY_DEADLINE, Import, ImportReply,
|
||||||
};
|
};
|
||||||
|
|
@ -240,7 +240,7 @@ async fn send_at(client: &async_nats::Client, hive: &str, meta: &Path) -> Result
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{FileMap, grants, seed_local_at};
|
use super::{FileMap, grants, seed_local_at};
|
||||||
use hive_sh4re::permissions::Permission::{self, *};
|
use hive_types::Permission::{self, *};
|
||||||
|
|
||||||
fn file(json: &str) -> FileMap {
|
fn file(json: &str) -> FileMap {
|
||||||
serde_json::from_str(json).expect("decodes")
|
serde_json::from_str(json).expect("decodes")
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use hive_sh4re::permissions::Permission;
|
use hive_types::Permission;
|
||||||
use swarm_queue_client::agent_permissions::{self, AgentPermissions, Read};
|
use swarm_queue_client::agent_permissions::{self, AgentPermissions, Read};
|
||||||
|
|
||||||
use crate::coordinator::Coordinator;
|
use crate::coordinator::Coordinator;
|
||||||
|
|
@ -216,11 +216,7 @@ mod tests {
|
||||||
assert!(changed.is_empty());
|
assert!(changed.is_empty());
|
||||||
assert!(crate::permissions::read_at(&path).expect("read").is_empty());
|
assert!(crate::permissions::read_at(&path).expect("read").is_empty());
|
||||||
assert!(
|
assert!(
|
||||||
!crate::permissions::holds(
|
!crate::permissions::holds(&path, "ruth", hive_types::Permission::ManageRootAgent)
|
||||||
&path,
|
|
||||||
"ruth",
|
|
||||||
hive_sh4re::permissions::Permission::ManageRootAgent
|
|
||||||
)
|
|
||||||
.expect("read")
|
.expect("read")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,3 @@ schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
# `stream_enrich` walks raw claude `stream-json` values into `term_msg` rows.
|
# `stream_enrich` walks raw claude `stream-json` values into `term_msg` rows.
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
strum.workspace = true
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
//! server instead); the permission transfers to a subagent, not the
|
//! server instead); the permission transfers to a subagent, not the
|
||||||
//! mechanism.
|
//! mechanism.
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use hive_types::Permission;
|
||||||
|
|
||||||
/// Built-in claude tools present in every session regardless of permissions.
|
/// Built-in claude tools present in every session regardless of permissions.
|
||||||
/// Anything not in this list (and not added by a permission's
|
/// Anything not in this list (and not added by a permission's
|
||||||
|
|
@ -90,131 +90,10 @@ pub fn subagent_builtin_tools_for(perms: &[Permission]) -> Vec<&'static str> {
|
||||||
tools
|
tools
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One thing an agent may be permitted. Stored by its `snake_case` name.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, strum::IntoStaticStr)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
#[strum(serialize_all = "snake_case")]
|
|
||||||
pub enum Permission {
|
|
||||||
/// `send`, `recv`, `ack_until`
|
|
||||||
Messaging,
|
|
||||||
/// `get_agent_meta` (`set_status` is always-on — see `ALWAYS_ON_TOOLS`)
|
|
||||||
Meta,
|
|
||||||
/// `get_loose_ends`, `cancel_loose_end`, `remind`
|
|
||||||
Inbox,
|
|
||||||
/// Gates whether the `bash` MCP server (`mcp__bash__run`/`status`/
|
|
||||||
/// `kill`) is rendered into the agent's config at all — see
|
|
||||||
/// `extra_server_required_permission` in `hive-agent/src/mcp_config.rs`.
|
|
||||||
/// `tools()` returns `&[]`: this isn't a `mcp__hyperhive__*`
|
|
||||||
/// allowlist entry, the gate lives at config-render time instead.
|
|
||||||
Execution,
|
|
||||||
/// Claude built-in web egress tools: `WebFetch` (retrieve a URL) and
|
|
||||||
/// `WebSearch` (search the web). Both are omitted from `--tools`
|
|
||||||
/// without it; holding it enables them in the session and in
|
|
||||||
/// `--allowedTools` so they run without a confirmation prompt.
|
|
||||||
WebTools,
|
|
||||||
/// Agent may manage *any* agent in the hive. Concretely, hive-c0re
|
|
||||||
/// bind-mounts every agent's state dir (RW) and config dir (RO) into the
|
|
||||||
/// holder's container, plus `/applied` and `/meta` read-only, so the
|
|
||||||
/// holder can recover an agent that is down. See
|
|
||||||
/// `lifecycle::host_config::set_nspawn_flags`.
|
|
||||||
///
|
|
||||||
/// The name is historical, from when "the root agent" was the widest
|
|
||||||
/// thing to manage. Kept under that spelling because every stored grant
|
|
||||||
/// carries it.
|
|
||||||
ManageRootAgent,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Permission {
|
|
||||||
/// Every permission in a stable order.
|
|
||||||
pub const ALL: &'static [Self] = &[
|
|
||||||
Self::Messaging,
|
|
||||||
Self::Meta,
|
|
||||||
Self::Inbox,
|
|
||||||
Self::Execution,
|
|
||||||
Self::WebTools,
|
|
||||||
Self::ManageRootAgent,
|
|
||||||
];
|
|
||||||
|
|
||||||
/// What an agent the swarm sets nothing for holds, whatever its name.
|
|
||||||
pub const DEFAULT: &'static [Self] =
|
|
||||||
&[Self::Messaging, Self::Meta, Self::Inbox, Self::Execution];
|
|
||||||
|
|
||||||
/// The MCP tool names (without the `mcp__hyperhive__` prefix) this
|
|
||||||
/// permission grants. Empty for the permissions that grant something
|
|
||||||
/// else; see each variant's doc comment.
|
|
||||||
#[must_use]
|
|
||||||
pub fn tools(self) -> &'static [&'static str] {
|
|
||||||
match self {
|
|
||||||
Self::Messaging => &["send", "recv", "ack_until"],
|
|
||||||
Self::Meta => &["get_agent_meta"],
|
|
||||||
Self::Inbox => &["get_loose_ends", "cancel_loose_end", "remind"],
|
|
||||||
// `Execution` grants the out-of-process `bash` MCP server,
|
|
||||||
// gated at config-render time in `hive-agent/src/mcp_config.rs`
|
|
||||||
// — 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 => &[],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// MCP tools that are always exposed regardless of which permissions an
|
|
||||||
/// agent holds. `set_status` lives here because the operator
|
|
||||||
/// dashboard depends on every agent being able to report its status
|
|
||||||
/// chip — gating it would let a misconfigured agent go dark on the
|
|
||||||
/// dashboard. The server-side `SetStatus` handler has no permission
|
|
||||||
/// check either (only length validation), so listing it here keeps the
|
|
||||||
/// `--allowedTools` list honest with that reality.
|
|
||||||
///
|
|
||||||
/// `compact` lives here too: it's pure self-management (no cross-agent
|
|
||||||
/// effect, no privilege), gated server-side on context usage, and every
|
|
||||||
/// agent should be able to reach for it — same reasoning as `set_status`.
|
|
||||||
///
|
|
||||||
/// `mark_todos_done` too: todos are pushed to an agent whether or not it
|
|
||||||
/// holds `Inbox` (which only gates `get_loose_ends`/`cancel_loose_end`/
|
|
||||||
/// `remind`), so an agent without `Inbox` must still be able to clear
|
|
||||||
/// them.
|
|
||||||
pub const ALWAYS_ON_TOOLS: &'static [&'static str] =
|
|
||||||
&["set_status", "compact", "mark_todos_done"];
|
|
||||||
|
|
||||||
/// The Claude built-in tool names this permission enables. Only
|
|
||||||
/// `WebTools` returns a non-empty slice.
|
|
||||||
#[must_use]
|
|
||||||
pub fn builtin_tools(self) -> &'static [&'static str] {
|
|
||||||
match self {
|
|
||||||
Self::WebTools => &["WebFetch", "WebSearch"],
|
|
||||||
_ => &[],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// The stored names are the `snake_case` spellings the old tool-group
|
|
||||||
/// and capability files used, so an imported grant decodes.
|
|
||||||
#[test]
|
|
||||||
fn stored_names_are_snake_case() {
|
|
||||||
let names: Vec<&str> = Permission::ALL.iter().map(|p| <&str>::from(*p)).collect();
|
|
||||||
assert_eq!(
|
|
||||||
names,
|
|
||||||
[
|
|
||||||
"messaging",
|
|
||||||
"meta",
|
|
||||||
"inbox",
|
|
||||||
"execution",
|
|
||||||
"web_tools",
|
|
||||||
"manage_root_agent"
|
|
||||||
]
|
|
||||||
);
|
|
||||||
for perm in Permission::ALL {
|
|
||||||
let json = serde_json::to_string(perm).unwrap();
|
|
||||||
assert_eq!(json, format!("\"{}\"", <&str>::from(*perm)));
|
|
||||||
}
|
|
||||||
assert!(serde_json::from_str::<Permission>("\"lifecycle\"").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adding permissions only ever adds built-ins — the base set is in every
|
/// Adding permissions only ever adds built-ins — the base set is in every
|
||||||
/// resolution, and no permission can remove one. Both `--tools` callers (the
|
/// resolution, and no permission can remove one. Both `--tools` callers (the
|
||||||
/// harness and the subagent daemon) rely on this to reason about "the
|
/// harness and the subagent daemon) rely on this to reason about "the
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ use hive_claude::{Attach, Claude, Config, SessionStore};
|
||||||
use hive_runtime::{
|
use hive_runtime::{
|
||||||
AcpCommand, AcpRuntime, PermissionAsk, PermissionPolicy, Runtime as _, RuntimeSpec, TurnStop,
|
AcpCommand, AcpRuntime, PermissionAsk, PermissionPolicy, Runtime as _, RuntimeSpec, TurnStop,
|
||||||
};
|
};
|
||||||
use hive_sh4re::permissions::Permission;
|
use hive_types::Permission;
|
||||||
use swarm_wire::agent_inference::AgentInference;
|
use swarm_wire::agent_inference::AgentInference;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::{Arc, OnceLock};
|
use std::sync::{Arc, OnceLock};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use hive_sh4re::permissions::Permission;
|
use hive_types::Permission;
|
||||||
use swarm_queue_client::agent_inference::{self, read_own};
|
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, HIVE_COPY_PATH, HiveCopy};
|
||||||
use swarm_wire::agent_inference::AgentInference;
|
use swarm_wire::agent_inference::AgentInference;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ Foundational shared **newtypes** for the hyperhive workspace. A zero-dependency
|
||||||
|
|
||||||
## What's here
|
## What's here
|
||||||
|
|
||||||
|
- **`Permission`** — the set of named permissions an agent may hold, with its
|
||||||
|
stored `snake_case` names and the MCP and built-in tools each grants.
|
||||||
- **`Ident`** — a validated agent-name newtype. Typing agent-name fields as
|
- **`Ident`** — a validated agent-name newtype. Typing agent-name fields as
|
||||||
`Ident` (rather than bare `String`) gets serde-validated parsing **at the
|
`Ident` (rather than bare `String`) gets serde-validated parsing **at the
|
||||||
socket boundary for free**: a malformed name is rejected at deserialize time
|
socket boundary for free**: a malformed name is rejected at deserialize time
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! Foundational shared newtypes for the hyperhive workspace.
|
//! Foundational shared types for the hyperhive workspace: [`Ident`] and [`Permission`].
|
||||||
//!
|
//!
|
||||||
//! A zero-dependency (bar `serde`) leaf crate so every wire-type crate
|
//! A zero-dependency (bar `serde`) leaf crate so every wire-type crate
|
||||||
//! (`hive-sh4re`, `hive-host-sock`, `hive-core-agent-sock`) and both binaries
|
//! (`hive-sh4re`, `hive-host-sock`, `hive-core-agent-sock`) and both binaries
|
||||||
|
|
@ -6,6 +6,10 @@
|
||||||
//! and get serde-validated parsing at the socket boundary for free — with
|
//! and get serde-validated parsing at the socket boundary for free — with
|
||||||
//! no cross-crate coupling and without growing `hive-sh4re`.
|
//! no cross-crate coupling and without growing `hive-sh4re`.
|
||||||
|
|
||||||
|
mod permission;
|
||||||
|
|
||||||
|
pub use permission::Permission;
|
||||||
|
|
||||||
/// The environment variable nix uses to hand this process the blacklist of
|
/// The environment variable nix uses to hand this process the blacklist of
|
||||||
/// names that already mean something to the message layer.
|
/// names that already mean something to the message layer.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
169
hive-types/src/permission.rs
Normal file
169
hive-types/src/permission.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
//! The [`Permission`] set an agent may hold.
|
||||||
|
//!
|
||||||
|
//! Lives in this leaf crate so every crate that names a permission, hive and
|
||||||
|
//! swarm side alike, can do so without depending on `hive-sh4re` (and through
|
||||||
|
//! it `hive-priv-sock`). The built-in tool resolution that consumes it is
|
||||||
|
//! `hive_sh4re::permissions`.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// One thing an agent may be permitted. Stored by its `snake_case` name.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Permission {
|
||||||
|
/// `send`, `recv`, `ack_until`
|
||||||
|
Messaging,
|
||||||
|
/// `get_agent_meta` (`set_status` is always-on — see `ALWAYS_ON_TOOLS`)
|
||||||
|
Meta,
|
||||||
|
/// `get_loose_ends`, `cancel_loose_end`, `remind`
|
||||||
|
Inbox,
|
||||||
|
/// Gates whether the `bash` MCP server (`mcp__bash__run`/`status`/
|
||||||
|
/// `kill`) is rendered into the agent's config at all — see
|
||||||
|
/// `extra_server_required_permission` in `hive-agent/src/mcp_config.rs`.
|
||||||
|
/// `tools()` returns `&[]`: this isn't a `mcp__hyperhive__*`
|
||||||
|
/// allowlist entry, the gate lives at config-render time instead.
|
||||||
|
Execution,
|
||||||
|
/// Claude built-in web egress tools: `WebFetch` (retrieve a URL) and
|
||||||
|
/// `WebSearch` (search the web). Both are omitted from `--tools`
|
||||||
|
/// without it; holding it enables them in the session and in
|
||||||
|
/// `--allowedTools` so they run without a confirmation prompt.
|
||||||
|
WebTools,
|
||||||
|
/// Agent may manage *any* agent in the hive. Concretely, hive-c0re
|
||||||
|
/// bind-mounts every agent's state dir (RW) and config dir (RO) into the
|
||||||
|
/// holder's container, plus `/applied` and `/meta` read-only, so the
|
||||||
|
/// holder can recover an agent that is down. See
|
||||||
|
/// `lifecycle::host_config::set_nspawn_flags`.
|
||||||
|
///
|
||||||
|
/// The name is historical, from when "the root agent" was the widest
|
||||||
|
/// thing to manage. Kept under that spelling because every stored grant
|
||||||
|
/// carries it.
|
||||||
|
ManageRootAgent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Permission {
|
||||||
|
/// Every permission in a stable order.
|
||||||
|
pub const ALL: &'static [Self] = &[
|
||||||
|
Self::Messaging,
|
||||||
|
Self::Meta,
|
||||||
|
Self::Inbox,
|
||||||
|
Self::Execution,
|
||||||
|
Self::WebTools,
|
||||||
|
Self::ManageRootAgent,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// What an agent the swarm sets nothing for holds, whatever its name.
|
||||||
|
pub const DEFAULT: &'static [Self] =
|
||||||
|
&[Self::Messaging, Self::Meta, Self::Inbox, Self::Execution];
|
||||||
|
|
||||||
|
/// The MCP tool names (without the `mcp__hyperhive__` prefix) this
|
||||||
|
/// permission grants. Empty for the permissions that grant something
|
||||||
|
/// else; see each variant's doc comment.
|
||||||
|
#[must_use]
|
||||||
|
pub fn tools(self) -> &'static [&'static str] {
|
||||||
|
match self {
|
||||||
|
Self::Messaging => &["send", "recv", "ack_until"],
|
||||||
|
Self::Meta => &["get_agent_meta"],
|
||||||
|
Self::Inbox => &["get_loose_ends", "cancel_loose_end", "remind"],
|
||||||
|
// `Execution` grants the out-of-process `bash` MCP server,
|
||||||
|
// gated at config-render time in `hive-agent/src/mcp_config.rs`
|
||||||
|
// — 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 => &[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MCP tools that are always exposed regardless of which permissions an
|
||||||
|
/// agent holds. `set_status` lives here because the operator
|
||||||
|
/// dashboard depends on every agent being able to report its status
|
||||||
|
/// chip — gating it would let a misconfigured agent go dark on the
|
||||||
|
/// dashboard. The server-side `SetStatus` handler has no permission
|
||||||
|
/// check either (only length validation), so listing it here keeps the
|
||||||
|
/// `--allowedTools` list honest with that reality.
|
||||||
|
///
|
||||||
|
/// `compact` lives here too: it's pure self-management (no cross-agent
|
||||||
|
/// effect, no privilege), gated server-side on context usage, and every
|
||||||
|
/// agent should be able to reach for it — same reasoning as `set_status`.
|
||||||
|
///
|
||||||
|
/// `mark_todos_done` too: todos are pushed to an agent whether or not it
|
||||||
|
/// holds `Inbox` (which only gates `get_loose_ends`/`cancel_loose_end`/
|
||||||
|
/// `remind`), so an agent without `Inbox` must still be able to clear
|
||||||
|
/// them.
|
||||||
|
pub const ALWAYS_ON_TOOLS: &'static [&'static str] =
|
||||||
|
&["set_status", "compact", "mark_todos_done"];
|
||||||
|
|
||||||
|
/// The Claude built-in tool names this permission enables. Only
|
||||||
|
/// `WebTools` returns a non-empty slice.
|
||||||
|
#[must_use]
|
||||||
|
pub fn builtin_tools(self) -> &'static [&'static str] {
|
||||||
|
match self {
|
||||||
|
Self::WebTools => &["WebFetch", "WebSearch"],
|
||||||
|
_ => &[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Permission {
|
||||||
|
/// The stored `snake_case` name; identical to the serde form.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Messaging => "messaging",
|
||||||
|
Self::Meta => "meta",
|
||||||
|
Self::Inbox => "inbox",
|
||||||
|
Self::Execution => "execution",
|
||||||
|
Self::WebTools => "web_tools",
|
||||||
|
Self::ManageRootAgent => "manage_root_agent",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Permission> for &'static str {
|
||||||
|
fn from(perm: Permission) -> Self {
|
||||||
|
perm.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// The `as_str` spelling of every variant. `ALL` is stable-ordered, so
|
||||||
|
/// a variant added to the enum but not to `ALL` or `as_str` shows here.
|
||||||
|
#[test]
|
||||||
|
fn as_str_pins_every_variants_name() {
|
||||||
|
let names: Vec<&str> = Permission::ALL.iter().map(|p| <&str>::from(*p)).collect();
|
||||||
|
assert_eq!(
|
||||||
|
names,
|
||||||
|
[
|
||||||
|
"messaging",
|
||||||
|
"meta",
|
||||||
|
"inbox",
|
||||||
|
"execution",
|
||||||
|
"web_tools",
|
||||||
|
"manage_root_agent"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The serde form of every variant, pinned independently of `as_str`:
|
||||||
|
/// stored grants decode by it, so it must stay byte-identical.
|
||||||
|
#[test]
|
||||||
|
fn serde_form_pins_every_variants_name() {
|
||||||
|
let want = [
|
||||||
|
(Permission::Messaging, "\"messaging\""),
|
||||||
|
(Permission::Meta, "\"meta\""),
|
||||||
|
(Permission::Inbox, "\"inbox\""),
|
||||||
|
(Permission::Execution, "\"execution\""),
|
||||||
|
(Permission::WebTools, "\"web_tools\""),
|
||||||
|
(Permission::ManageRootAgent, "\"manage_root_agent\""),
|
||||||
|
];
|
||||||
|
assert_eq!(want.len(), Permission::ALL.len());
|
||||||
|
for (perm, json) in want {
|
||||||
|
assert_eq!(serde_json::to_string(&perm).unwrap(), json);
|
||||||
|
assert_eq!(serde_json::from_str::<Permission>(json).unwrap(), perm);
|
||||||
|
}
|
||||||
|
assert!(serde_json::from_str::<Permission>("\"lifecycle\"").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -69,7 +69,6 @@ sha2.workspace = true
|
||||||
similar.workspace = true
|
similar.workspace = true
|
||||||
# The `Permission` names `/api/agents/{name}/permissions` accepts, the same
|
# The `Permission` names `/api/agents/{name}/permissions` accepts, the same
|
||||||
# enum the hive enforces.
|
# enum the hive enforces.
|
||||||
hive-sh4re.workspace = true
|
|
||||||
# Validates `POST /api/agents`' `name` before it becomes `agent`/`repo`
|
# Validates `POST /api/agents`' `name` before it becomes `agent`/`repo`
|
||||||
# everywhere downstream — see `create_agent`'s doc comment for why this is
|
# everywhere downstream — see `create_agent`'s doc comment for why this is
|
||||||
# defense-in-depth, not the only gate (per an argus review finding).
|
# defense-in-depth, not the only gate (per an argus review finding).
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ use axum::extract::{Path, State};
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use backon::{ConstantBuilder, Retryable as _};
|
use backon::{ConstantBuilder, Retryable as _};
|
||||||
use futures_util::StreamExt as _;
|
use futures_util::StreamExt as _;
|
||||||
use hive_sh4re::permissions::Permission;
|
use hive_types::Permission;
|
||||||
use problem_details::ProblemDetails;
|
use problem_details::ProblemDetails;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use swarm_queue_client::agent_permissions::{self, AgentPermissions, Import, ImportReply};
|
use swarm_queue_client::agent_permissions::{self, AgentPermissions, Import, ImportReply};
|
||||||
|
|
@ -528,7 +528,7 @@ mod tests {
|
||||||
use crate::test_nats::TestNats;
|
use crate::test_nats::TestNats;
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use hive_sh4re::permissions::Permission;
|
use hive_types::Permission;
|
||||||
use swarm_queue_client::agent_permissions::{Import, ImportReply};
|
use swarm_queue_client::agent_permissions::{Import, ImportReply};
|
||||||
|
|
||||||
async fn published(client: &async_nats::Client, agent: &str) -> Option<String> {
|
async fn published(client: &async_nats::Client, agent: &str) -> Option<String> {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use std::time::Duration;
|
||||||
|
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use hive_sh4re::permissions::Permission;
|
use hive_types::Permission;
|
||||||
use problem_details::ProblemDetails;
|
use problem_details::ProblemDetails;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use swarm_queue_client::archetypes::{ArchetypeMeta, BUILTIN_SOURCE, SourceRecord};
|
use swarm_queue_client::archetypes::{ArchetypeMeta, BUILTIN_SOURCE, SourceRecord};
|
||||||
|
|
|
||||||
|
|
@ -510,7 +510,7 @@ pub(crate) struct CreateAgentRequest {
|
||||||
/// agent holds the default set.
|
/// agent holds the default set.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[schema(value_type = Option<Vec<String>>)]
|
#[schema(value_type = Option<Vec<String>>)]
|
||||||
pub(crate) permissions: Option<Vec<hive_sh4re::permissions::Permission>>,
|
pub(crate) permissions: Option<Vec<hive_types::Permission>>,
|
||||||
/// The archetype to build the agent from, one `GET /api/archetypes`
|
/// The archetype to build the agent from, one `GET /api/archetypes`
|
||||||
/// lists. The agent then gets no `agent-configs/<name>` repo: its hive
|
/// lists. The agent then gets no `agent-configs/<name>` repo: its hive
|
||||||
/// builds it from the archetype's module. Absent is a custom agent.
|
/// builds it from the archetype's module. Absent is a custom agent.
|
||||||
|
|
@ -1513,9 +1513,7 @@ mod tests {
|
||||||
.await
|
.await
|
||||||
.expect("a bucket that refuses any value");
|
.expect("a bucket that refuses any value");
|
||||||
let state = crate::agent_limits::tests::state(&client);
|
let state = crate::agent_limits::tests::state(&client);
|
||||||
let earlier = crate::agent_permissions::at_create(vec![
|
let earlier = crate::agent_permissions::at_create(vec![hive_types::Permission::Messaging])
|
||||||
hive_sh4re::permissions::Permission::Messaging,
|
|
||||||
])
|
|
||||||
.expect("valid");
|
.expect("valid");
|
||||||
crate::agent_permissions::store(&client, "atlas", &earlier)
|
crate::agent_permissions::store(&client, "atlas", &earlier)
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,10 @@ operator-profiles = ["async-nats/jetstream"]
|
||||||
# runs the same way. Explicit for the same reason as `notices`.
|
# runs the same way. Explicit for the same reason as `notices`.
|
||||||
agent-limits = ["async-nats/jetstream"]
|
agent-limits = ["async-nats/jetstream"]
|
||||||
# `agent_permissions::AgentPermissions` and `read`, for hive-c0re, the agent
|
# `agent_permissions::AgentPermissions` and `read`, for hive-c0re, the agent
|
||||||
# harness, the subagent daemon and swarm-controller. Pulls `hive-sh4re` for the
|
# 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
|
# `Permission` names, so the crates that only need the bucket name do not link
|
||||||
# it.
|
# it.
|
||||||
agent-permissions = ["dep:hive-sh4re", "async-nats/jetstream"]
|
agent-permissions = ["dep:hive-types", "async-nats/jetstream"]
|
||||||
# `agent_archetype::read`, for hive-c0re, which reads the archetype of each
|
# `agent_archetype::read`, for hive-c0re, which reads the archetype of each
|
||||||
# agent it provisions or rebuilds. Explicit for the same reason as `notices`.
|
# agent it provisions or rebuilds. Explicit for the same reason as `notices`.
|
||||||
agent-archetype = ["async-nats/jetstream"]
|
agent-archetype = ["async-nats/jetstream"]
|
||||||
|
|
@ -68,7 +68,7 @@ agents-md-initial = ["async-nats/jetstream"]
|
||||||
# Bare (no `kv`/`jetstream`) unless a consumer opts into the `kv` feature
|
# Bare (no `kv`/`jetstream`) unless a consumer opts into the `kv` feature
|
||||||
# above - the connect itself needs none of them.
|
# above - the connect itself needs none of them.
|
||||||
async-nats.workspace = true
|
async-nats.workspace = true
|
||||||
hive-sh4re = { workspace = true, optional = true }
|
hive-types = { workspace = true, optional = true }
|
||||||
# `blocking` on top of the workspace default (`form`/`json`/`rustls`) —
|
# `blocking` on top of the workspace default (`form`/`json`/`rustls`) —
|
||||||
# `mint_token_for_blocking` needs `reqwest::blocking::Client` for a caller
|
# `mint_token_for_blocking` needs `reqwest::blocking::Client` for a caller
|
||||||
# with no tokio reactor to `.await` an async request on (an OTLP exporter's
|
# with no tokio reactor to `.await` an async request on (an OTLP exporter's
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//! The per-agent permissions KV bucket: one key per agent, holding the
|
//! The per-agent permissions KV bucket: one key per agent, holding the
|
||||||
//! [`hive_sh4re::permissions::Permission`] set the swarm grants it.
|
//! [`hive_types::Permission`] set the swarm grants it.
|
||||||
//!
|
//!
|
||||||
//! **swarm-controller is the only writer**, and creates the bucket when it
|
//! **swarm-controller is the only writer**, and creates the bucket when it
|
||||||
//! starts. A missing key or an unset `permissions` field leaves the agent on
|
//! starts. A missing key or an unset `permissions` field leaves the agent on
|
||||||
|
|
@ -71,14 +71,14 @@ pub const IMPORT_REPLY_DEADLINE: std::time::Duration = std::time::Duration::from
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct AgentPermissions {
|
pub struct AgentPermissions {
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub permissions: Option<Vec<hive_sh4re::permissions::Permission>>,
|
pub permissions: Option<Vec<hive_types::Permission>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a hive sends once: the permissions its meta repo held, by agent.
|
/// What a hive sends once: the permissions its meta repo held, by agent.
|
||||||
#[cfg(feature = "agent-permissions")]
|
#[cfg(feature = "agent-permissions")]
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct Import {
|
pub struct Import {
|
||||||
pub permissions: std::collections::BTreeMap<String, Vec<hive_sh4re::permissions::Permission>>,
|
pub permissions: std::collections::BTreeMap<String, Vec<hive_types::Permission>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The controller's answer to an [`Import`].
|
/// The controller's answer to an [`Import`].
|
||||||
|
|
@ -174,22 +174,22 @@ impl HiveCopy {
|
||||||
/// What the agent holds before any read of its key has succeeded: the
|
/// What the agent holds before any read of its key has succeeded: the
|
||||||
/// stored list, or the default when the hive stored none.
|
/// stored list, or the default when the hive stored none.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn before_read(&self) -> Vec<hive_sh4re::permissions::Permission> {
|
pub fn before_read(&self) -> Vec<hive_types::Permission> {
|
||||||
self.stored
|
self.stored
|
||||||
.permissions
|
.permissions
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| hive_sh4re::permissions::Permission::DEFAULT.to_vec())
|
.unwrap_or_else(|| hive_types::Permission::DEFAULT.to_vec())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What the agent holds after reading `read` from its key. An unset key
|
/// 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
|
/// while the import is pending may only mean the grant is not imported
|
||||||
/// yet, so the stored list stands; otherwise unset is the default.
|
/// yet, so the stored list stands; otherwise unset is the default.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn after_read(&self, read: &AgentPermissions) -> Vec<hive_sh4re::permissions::Permission> {
|
pub fn after_read(&self, read: &AgentPermissions) -> Vec<hive_types::Permission> {
|
||||||
match &read.permissions {
|
match &read.permissions {
|
||||||
Some(list) => list.clone(),
|
Some(list) => list.clone(),
|
||||||
None if self.import_pending => self.before_read(),
|
None if self.import_pending => self.before_read(),
|
||||||
None => hive_sh4re::permissions::Permission::DEFAULT.to_vec(),
|
None => hive_types::Permission::DEFAULT.to_vec(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -280,7 +280,7 @@ mod tests {
|
||||||
#[cfg(feature = "agent-permissions")]
|
#[cfg(feature = "agent-permissions")]
|
||||||
#[test]
|
#[test]
|
||||||
fn unset_and_empty_are_different_values() {
|
fn unset_and_empty_are_different_values() {
|
||||||
use hive_sh4re::permissions::Permission;
|
use hive_types::Permission;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
serde_json::to_string(&AgentPermissions::default()).unwrap(),
|
serde_json::to_string(&AgentPermissions::default()).unwrap(),
|
||||||
"{}"
|
"{}"
|
||||||
|
|
@ -297,7 +297,7 @@ mod tests {
|
||||||
#[cfg(feature = "agent-permissions")]
|
#[cfg(feature = "agent-permissions")]
|
||||||
#[test]
|
#[test]
|
||||||
fn the_hive_copy_stands_until_read_and_while_the_import_is_pending() {
|
fn the_hive_copy_stands_until_read_and_while_the_import_is_pending() {
|
||||||
use hive_sh4re::permissions::Permission;
|
use hive_types::Permission;
|
||||||
let record = |json: &str| -> AgentPermissions { serde_json::from_str(json).unwrap() };
|
let record = |json: &str| -> AgentPermissions { serde_json::from_str(json).unwrap() };
|
||||||
let copy = |json: &str| -> HiveCopy { serde_json::from_str(json).unwrap() };
|
let copy = |json: &str| -> HiveCopy { serde_json::from_str(json).unwrap() };
|
||||||
let web = vec![Permission::WebTools];
|
let web = vec![Permission::WebTools];
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
//! Unset leaves the agent on the default set; an empty list grants nothing.
|
//! Unset leaves the agent on the default set; an empty list grants nothing.
|
||||||
//!
|
//!
|
||||||
//! The shape below mirrors the controller's `Permissions`, whose names are
|
//! The shape below mirrors the controller's `Permissions`, whose names are
|
||||||
//! `hive_sh4re::permissions::Permission`, and `swarm-controller-api` does not
|
//! `hive_types::Permission`, and `swarm-controller-api` does not
|
||||||
//! depend on `hive-sh4re`. Permission names are checked by the controller,
|
//! depend on `hive-sh4re`. Permission names are checked by the controller,
|
||||||
//! which answers 422 for one it does not know.
|
//! which answers 422 for one it does not know.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue