//! Access tiers for the anonymous processing nodes spawned by the //! hive-mind orchestrator (`app::workflow::hive_mind`). //! //! Nodes have no persistent identity of their own — the Core Intelligence //! addresses each one only by directive and access tier. Since node //! designations are system-assigned coordinates rather than named roles, //! tool access can't be a lookup table keyed by role name. Instead the //! Core Intelligence picks one of these three tiers per node, matched to //! what that node's specific directive needs — this keeps the Harness //! gate meaningful while the node roster itself stays fully dynamic. //! //! Tiers (least → most privileged): `read` < `write` < `full`. /// The three tool-access tiers a hive-mind node can be granted. pub mod tool_scope { use tracing; /// Read-only investigation: no file mutation, no shell, no VCS. pub const READ: &str = "read"; /// Read-tier plus file mutation and non-destructive shell (tests/builds). pub const WRITE: &str = "write"; /// Write-tier plus delete, git, and the remaining LSP actions. pub const FULL: &str = "full"; /// The read-only tool set — reused by `context::dedup` as the /// authoritative "safe to deduplicate" classification, so there's a /// single list of read-only tool names in the codebase instead of two. pub const READ_TOOLS: &[&str] = &[ "read", "grep", "glob", "search", "seqthink", "recall", "lsp_connect", "lsp_diagnostics", "lsp_hover", "lsp_definition", "lsp_references", "read_findings", ]; const WRITE_TOOLS: &[&str] = &[ "read", "grep", "glob", "search", "seqthink", "recall", "lsp_connect", "lsp_diagnostics", "lsp_hover", "lsp_definition", "lsp_references", "read_findings", "write", "edit", "bash", "todowrite", "todofinish", "remember", ]; const FULL_TOOLS: &[&str] = &[ "read", "grep", "glob", "search", "seqthink", "recall", "lsp_connect", "lsp_diagnostics", "lsp_hover", "lsp_definition", "lsp_references", "read_findings", "write", "edit", "bash", "todowrite", "todofinish", "remember", "delete", "git_operator", "lsp_completion", "lsp_disconnect", ]; /// Resolve a tier name to its concrete tool allowlist. /// /// Unrecognized scope strings fall back to `READ` — the least-privileged /// tier — rather than silently granting broader access. /// /// Flow: match `scope` against the three known constants → return the /// corresponding static slice → collect into owned `Vec`. /// /// Return: an owned `Vec` suitable for `AgentDefinition::with_allowed_tools`. pub fn tools_for(scope: &str) -> Vec { // Select the tool list matching the requested access tier. // Unknown scope names are treated as "read" (least privilege). let tools: &[&str] = match scope { FULL => FULL_TOOLS, WRITE => WRITE_TOOLS, _ => { tracing::debug!( "[division] unknown scope '{scope}' — falling back to READ", ); READ_TOOLS } }; tools.iter().map(|s| (*s).to_string()).collect() } } #[cfg(test)] mod tests { use super::tool_scope::{tools_for, FULL, READ, WRITE}; /// Verify the READ tier does not contain write or bash tools. #[test] fn read_tier_excludes_write_tools() { let tools = tools_for(READ); assert!(!tools.contains(&"write".to_string())); assert!(!tools.contains(&"bash".to_string())); } /// Verify the WRITE tier includes bash and write but not delete or git. #[test] fn write_tier_includes_bash_but_not_delete_or_git() { let tools = tools_for(WRITE); assert!(tools.contains(&"bash".to_string())); assert!(tools.contains(&"write".to_string())); assert!(!tools.contains(&"delete".to_string())); assert!(!tools.contains(&"git_operator".to_string())); } /// Verify the FULL tier includes delete and git tools. #[test] fn full_tier_includes_delete_and_git() { let tools = tools_for(FULL); assert!(tools.contains(&"delete".to_string())); assert!(tools.contains(&"git_operator".to_string())); } /// Verify that an unrecognized scope name falls back to the READ tier. #[test] fn unknown_scope_falls_back_to_read() { let tools = tools_for("bogus"); assert!(!tools.contains(&"write".to_string())); assert!(!tools.contains(&"delete".to_string())); } /// Verify the tier hierarchy: READ ⊂ WRITE ⊂ FULL (each is a strict superset). #[test] fn read_tier_is_subset_of_write_tier_and_write_is_subset_of_full() { use std::collections::HashSet; let read: HashSet<_> = tools_for(READ).into_iter().collect(); let write: HashSet<_> = tools_for(WRITE).into_iter().collect(); let full: HashSet<_> = tools_for(FULL).into_iter().collect(); assert!( read.is_subset(&write), "read tier must be a subset of write tier" ); assert!( write.is_subset(&full), "write tier must be a subset of full tier" ); } }