From 96ca3338e7b61bc27f266a0e399eca79af0141ae Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Tue, 14 Jul 2026 20:02:17 +0400 Subject: [PATCH 01/15] feat(core): populate passive capability baseline --- CHANGELOG.md | 6 + crates/astrid-core/src/capability_registry.rs | 167 ++++++++- .../src/capability_registry/tests.rs | 324 ++++++++++++++++++ .../src/kernel_router/admin/tests.rs | 34 +- .../kernel_router/capability_catalog_tests.rs | 139 ++++++-- 5 files changed, 648 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eae7b5b3..56a60444a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,12 @@ Changelog tracking starts with 0.2.0. Prior versions were not tracked. Signed `.shuttle` grant composition remains deferred and fails explicitly. Closes #1195. +- **The passive authority migration baseline now has fixed semantics and digest + vectors.** All 51 kernel entries bind scope, target kinds, delegability, + privileged status and provenance; request mappings and current enforcement + roles fail tests if they drift outside the registry. Authorization and + persisted state remain unchanged. Closes #1235. Refs #1228 and #1233. + ### Changed - **Device key IDs now use BLAKE3.** The short per-device handle is derived from diff --git a/crates/astrid-core/src/capability_registry.rs b/crates/astrid-core/src/capability_registry.rs index ed26e74ed..1a505e337 100644 --- a/crates/astrid-core/src/capability_registry.rs +++ b/crates/astrid-core/src/capability_registry.rs @@ -9,7 +9,8 @@ use std::num::NonZeroU32; use thiserror::Error; use crate::capability_grammar::{ - CapabilityDanger, CapabilityGrammarError, CapabilityScope, validate_capability, + CAPABILITY_CATALOG, CapabilityDanger, CapabilityGrammarError, CapabilityScope, + validate_capability, }; use util::{ domain_hash, encode_array_len, encode_bool, encode_bytes, encode_text, encode_unsigned, @@ -115,6 +116,158 @@ const CAPABILITY_REGISTRY_REVISION_1_IDS: [&str; 51] = [ "authority:repair", ]; +/// Schema revision for the 51-ID authority registry. +pub const MIGRATION_BASELINE_SCHEMA_REVISION: NonZeroU32 = NonZeroU32::MIN; + +#[derive(Clone, Copy)] +struct BaselineSemantics { + scope: CapabilityScope, + target_kinds: &'static [AuthorityTargetKind], + delegable: bool, + privileged: bool, +} + +/// Build the content-addressed registry for the 51 fixed capability IDs. +/// +/// # Errors +/// +/// Returns an error if an ID lacks fixed semantics or display metadata, or if +/// any definition fails registry validation. +pub fn migration_baseline_registry() -> Result { + let entries = MIGRATION_BASELINE_CAPABILITY_IDS + .into_iter() + .map(|id| { + let semantics = baseline_semantics(id).ok_or_else(|| { + AuthorityRegistryError::MissingBaselineDefinition { id: id.to_string() } + })?; + let danger = baseline_danger(id).ok_or_else(|| { + AuthorityRegistryError::MissingBaselineDisplayMetadata { id: id.to_string() } + })?; + RegisteredCapability::new( + ExactCapabilityId::new(id.to_string())?, + semantics.scope, + semantics.target_kinds.iter().copied(), + danger, + semantics.delegable, + semantics.privileged, + CapabilitySource::Kernel, + ) + }) + .collect::, AuthorityRegistryError>>()?; + + CapabilityRegistryManifest::new(MIGRATION_BASELINE_SCHEMA_REVISION, entries) +} + +fn baseline_danger(id: &str) -> Option { + CAPABILITY_CATALOG + .iter() + .find(|entry| entry.id == id) + .map(|entry| entry.danger) + .or_else(|| { + matches!( + id, + "system:resources:unbounded" + | "net_bind" + | "uplink" + | "capsule:access:any" + | "authority:profile:manage" + | "authority:repair" + ) + .then_some(CapabilityDanger::Extreme) + }) +} + +fn baseline_semantics(id: &str) -> Option { + use AuthorityTargetKind::{ + AuditScope, CapsuleInstance, CapsulePackage, Credential, Group, Principal, System, + }; + use CapabilityScope::{Global, Self_}; + + let semantics = match id { + "system:shutdown" => BaselineSemantics::new(Global, &[System], false, true), + "system:status" => BaselineSemantics::new(Global, &[System], false, false), + "capsule:install" => BaselineSemantics::new(Global, &[System, CapsulePackage], true, true), + "self:capsule:install" => { + BaselineSemantics::new(Self_, &[Principal, CapsulePackage], true, false) + }, + "capsule:reload" | "capsule:remove" => { + BaselineSemantics::new(Global, &[System, CapsuleInstance], true, true) + }, + "self:capsule:reload" + | "self:capsule:remove" + | "self:workspace:promote" + | "self:workspace:rollback" => { + BaselineSemantics::new(Self_, &[Principal, CapsuleInstance], true, false) + }, + "capsule:list" | "agent:list" | "group:list" | "invite:list" => { + BaselineSemantics::new(Global, &[System], true, true) + }, + "self:capsule:list" + | "self:agent:list" + | "self:group:list" + | "self:quota:get" + | "self:approval:respond" => BaselineSemantics::new(Self_, &[Principal], true, false), + "agent:create" | "agent:create:clone" | "agent:modify" => { + BaselineSemantics::new(Global, &[Principal, Group, CapsulePackage], true, true) + }, + "agent:create:inherit" + | "agent:delete" + | "agent:enable" + | "agent:disable" + | "quota:set" + | "quota:get" + | "caps:grant" + | "caps:revoke" + | "caps:token:list" => BaselineSemantics::new(Global, &[Principal], true, true), + "self:quota:set" => BaselineSemantics::new(Self_, &[Principal], true, true), + "group:create" | "group:delete" | "group:modify" => { + BaselineSemantics::new(Global, &[Group], true, true) + }, + "caps:token:mint" | "caps:token:revoke" => { + BaselineSemantics::new(Global, &[Principal, Credential], true, true) + }, + "invite:issue" => BaselineSemantics::new(Global, &[Group, Credential], true, true), + "invite:redeem" => { + BaselineSemantics::new(Global, &[Principal, Group, Credential], false, true) + }, + "invite:revoke" => BaselineSemantics::new(Global, &[Credential], true, true), + "audit:read_all" => BaselineSemantics::new(Global, &[AuditScope], true, true), + "self:auth:pair" => BaselineSemantics::new(Self_, &[Principal, Credential], true, true), + "self:auth:pair:admin" => { + BaselineSemantics::new(Self_, &[Principal, Credential], false, true) + }, + "auth:pair:redeem" => BaselineSemantics::new(Global, &[Principal, Credential], false, true), + "auth:pair" => BaselineSemantics::new(Global, &[Principal, Credential], true, true), + "system:resources:unbounded" | "net_bind" | "uplink" => { + BaselineSemantics::new(Self_, &[Principal, CapsuleInstance], false, true) + }, + "capsule:access:any" => { + BaselineSemantics::new(Self_, &[CapsulePackage, CapsuleInstance], false, true) + }, + "authority:profile:manage" | "authority:repair" => { + BaselineSemantics::new(Global, &[System, Principal, Group, Credential], false, true) + }, + _ => return None, + }; + Some(semantics) +} + +impl BaselineSemantics { + const fn new( + scope: CapabilityScope, + target_kinds: &'static [AuthorityTargetKind], + delegable: bool, + privileged: bool, + ) -> Self { + Self { + scope, + target_kinds, + delegable, + privileged, + } + } +} + /// A validated capability identifier containing no wildcard segment. /// /// The generic storage parameter permits borrowed views at validation and @@ -653,6 +806,18 @@ impl CapabilityRegistryManifest { #[derive(Debug, Error)] #[non_exhaustive] pub enum AuthorityRegistryError { + /// A fixed capability ID has no authorization definition. + #[error("migration-baseline capability {id:?} has no authorization definition")] + MissingBaselineDefinition { + /// Capability identifier. + id: String, + }, + /// A fixed capability ID has no danger classification. + #[error("migration-baseline capability {id:?} has no display metadata")] + MissingBaselineDisplayMetadata { + /// Capability identifier. + id: String, + }, /// A capability ID failed the existing static capability grammar. #[error("invalid capability id {id:?}: {source}")] InvalidCapabilityId { diff --git a/crates/astrid-core/src/capability_registry/tests.rs b/crates/astrid-core/src/capability_registry/tests.rs index e437514d4..12f8abeb3 100644 --- a/crates/astrid-core/src/capability_registry/tests.rs +++ b/crates/astrid-core/src/capability_registry/tests.rs @@ -71,6 +71,330 @@ fn capability_registry_revision_1_freezes_current_and_dormant_exact_ids() { ); } +#[test] +fn migration_baseline_registry_contains_every_fixed_definition() { + let manifest = migration_baseline_registry().unwrap(); + let ids = manifest + .entries() + .iter() + .map(|entry| entry.id().as_str()) + .collect::>(); + let expected = MIGRATION_BASELINE_CAPABILITY_IDS + .iter() + .copied() + .collect::>(); + + assert_eq!( + manifest.schema_revision(), + MIGRATION_BASELINE_SCHEMA_REVISION + ); + assert_eq!(manifest.entries().len(), 51); + assert_eq!(ids, expected); + assert!( + manifest + .entries() + .iter() + .all(|entry| entry.source() == CapabilitySource::Kernel) + ); + manifest.verify().unwrap(); +} + +#[test] +fn migration_baseline_preserves_catalog_scope_and_danger() { + let manifest = migration_baseline_registry().unwrap(); + for catalog_entry in CAPABILITY_CATALOG { + let registered = manifest + .entries() + .iter() + .find(|entry| entry.id().as_str() == catalog_entry.id) + .unwrap_or_else(|| panic!("missing catalog capability {}", catalog_entry.id)); + assert_eq!( + registered.scope(), + catalog_entry.scope, + "{}", + catalog_entry.id + ); + assert_eq!( + registered.danger(), + catalog_entry.danger, + "{}", + catalog_entry.id + ); + } + + for id in [ + CAP_RESOURCES_UNBOUNDED, + CAP_NET_BIND, + CAP_UPLINK, + "capsule:access:any", + "authority:profile:manage", + "authority:repair", + ] { + let registered = manifest + .entries() + .iter() + .find(|entry| entry.id().as_str() == id) + .unwrap_or_else(|| panic!("missing baseline addition {id}")); + assert_eq!(registered.danger(), CapabilityDanger::Extreme, "{id}"); + } +} + +#[test] +fn migration_baseline_semantics_cover_sensitive_edges() { + let manifest = migration_baseline_registry().unwrap(); + let entry = |id: &str| { + manifest + .entries() + .iter() + .find(|entry| entry.id().as_str() == id) + .unwrap_or_else(|| panic!("missing baseline capability {id}")) + }; + + assert!(!entry("system:status").delegable()); + assert!(!entry("system:status").privileged()); + assert!(entry("self:auth:pair").delegable()); + assert!(entry("self:auth:pair").privileged()); + assert!(!entry("self:auth:pair:admin").delegable()); + assert!(!entry("capsule:access:any").delegable()); + assert!(entry("capsule:access:any").privileged()); + assert_eq!( + entry("authority:repair").target_kinds(), + &BTreeSet::from([ + AuthorityTargetKind::System, + AuthorityTargetKind::Principal, + AuthorityTargetKind::Group, + AuthorityTargetKind::Credential, + ]) + ); +} + +#[test] +fn migration_baseline_digest_vectors_are_stable() { + let expected = [ + ( + "agent:create", + "c9d1587351a1456f6e50f66d377560916a7f6f92d9d131d7024ad294628529cc", + ), + ( + "agent:create:clone", + "eb43c11bb883dedcb5a3221a7d555c20df1ba05b7d8e5bae82a52943059d48f5", + ), + ( + "agent:create:inherit", + "3591843cd8e4056cc0c18b29e69c1af40c9aa7deaf1c5732bbd4d5acbacc0d9a", + ), + ( + "agent:delete", + "b50d4003730c3a4cd97ba0ad60c9e172d5d999d47c45e75a4671e0bb19e92af7", + ), + ( + "agent:disable", + "76218ce076f9af7f55ca306140e59ddcf3addff4937898096d06b9fb01ddc169", + ), + ( + "agent:enable", + "26d69fe48db627f94442f56a1cd1d648e12be670cdc50342fad7051adf82fe0e", + ), + ( + "agent:list", + "b929c8c5e9867f4c8d3b9998ef03f38ef4247f2000f23ae5501871e3de0674db", + ), + ( + "agent:modify", + "688a7b33ada935bc7d12e0bf7a4c062f6040cb6066c2bac3b16bf537129b31d8", + ), + ( + "audit:read_all", + "0b8d39b03f848957d73ad0db00977e73e09da57f69a2680f30b26668a5885c1e", + ), + ( + "auth:pair", + "17dd59e8d673908b9e65e9e1987ef6a956cb54ea52dd91391d39db6fb5c7a8c2", + ), + ( + "auth:pair:redeem", + "755164d6481d3474ce244048c4c3b8d4f7e53e03476150334f61b62f39bd7963", + ), + ( + "authority:profile:manage", + "ffe78ba8b48a775dbb344743463b60a5943c69ca764bb373e7111eb6c173f9a0", + ), + ( + "authority:repair", + "1e1367b51221b5e2667c50aead762b871a1262decf3f90e8f449591912b0d848", + ), + ( + "caps:grant", + "01655efea292a62b3656babf8d888abb1bf7752f40185a661a172633e2325823", + ), + ( + "caps:revoke", + "76ceda7e4be1cc3bc52335d30851eb478e50adb766c1817075bc5f587c4da55a", + ), + ( + "caps:token:list", + "176df3fd4672f24cbae46fbbca7999d9b0374019f93fc36136eecc6cb1a0991f", + ), + ( + "caps:token:mint", + "ec9841e62d5833a1b163350e7b375f25d52bfe27623fc66aecc11c9d0a0f5f25", + ), + ( + "caps:token:revoke", + "6b935504883624fa06e0a0ee97aa21c889424543a08cfc30230c5c71113f1cc2", + ), + ( + "capsule:access:any", + "f9a5c447a08aa674999b32131b9ac560812f2dcfd479a426e84922ff2d52c710", + ), + ( + "capsule:install", + "79eb65b1216c6edc719e107b91f5b33be0bb4865eb57f83fdb6df54cd936c6e5", + ), + ( + "capsule:list", + "8eeb6a4ce2191d78c82dfd9598d44250af37cfe73ce80ce4c41f278688246409", + ), + ( + "capsule:reload", + "d2b4ad2566218eb87d45cd9107d0ae45d282ffc97ec33ebc9a6b6ff03e93740f", + ), + ( + "capsule:remove", + "70a45a0bbce7ea9da81e04360aba4e0f6c67ddc39ae42efe7484e2b19fa45d31", + ), + ( + "group:create", + "d79da52e4033bf4e5febfcee1af9751c65a014fed1472f7b23e12420de5025ad", + ), + ( + "group:delete", + "8a00eb4cd858515050cbcf58174c8294e769fb1d407c22ad6f941c949726a89f", + ), + ( + "group:list", + "e93d376265c24fdbc39321ef86af19c7c719687d7ea12d568bf2684c13605fc4", + ), + ( + "group:modify", + "a3957746717fce4f1922ae255a45ae16966fb54e8f9602b0106c6cbc41fa3e67", + ), + ( + "invite:issue", + "dd6b1efec81f3183f82ae92dae8b3090bc22e5eb6eef6a071453c299a150574c", + ), + ( + "invite:list", + "5d51573ee575411b1569cb738c1b26bac068ee78331947820de912f02d2d0ecc", + ), + ( + "invite:redeem", + "bcf035e877441c90fee85e9fb67e17ca148f4f31d05542c13b580f4452ee4764", + ), + ( + "invite:revoke", + "84f8a09710474578ca166aff32347aefb7e82cdfaafe495324cb7e3e48399faf", + ), + ( + "net_bind", + "573c7e818405313d3d9adb625e8c0f31ab515be1895d229166e8cddca9060f23", + ), + ( + "quota:get", + "b5af127b2ee4038daa9ee3069c3f6b808f6e9afd970ab0398b5e0b46e0d5e74e", + ), + ( + "quota:set", + "4064cc54281dacf202f13bcf3313eb00eb441b556312fd42d9ec23b907bc40c3", + ), + ( + "self:agent:list", + "70ada28d4c69802d4601d3031b294fa4cd4d462f715a3a64c14d40afa0a20c32", + ), + ( + "self:approval:respond", + "fdac7a876ba95c7ba7c42cf1f4f5e3034dbcb3f07c865aa96b6894dd30ee7e9c", + ), + ( + "self:auth:pair", + "965a89a82060415f1440a38947760f017660dfa7cba64c0dd5f80202ed740bb5", + ), + ( + "self:auth:pair:admin", + "17171d102c5907748f961ffef6363267c79142ba277719f2eada06e8140b51fa", + ), + ( + "self:capsule:install", + "04cede94f155ec402dde49baeb2a94b57c089768eb61ef691553863265770081", + ), + ( + "self:capsule:list", + "a5c24d855d68c7b336f74176360bb66dac44affd352d97d54397c149ba8c247a", + ), + ( + "self:capsule:reload", + "b6d260d5a5aea185ee8f04d751ad7e7392b753f24d42988698af0deb0b66e11a", + ), + ( + "self:capsule:remove", + "2f50b2efdd603d2fad8aefc7a0cd785d67d29a07981afeec4c157101b58f9699", + ), + ( + "self:group:list", + "d3a41e67d25c6a2b3c67f728ff03381bfef14432f3044807a1c9fc1185d5173b", + ), + ( + "self:quota:get", + "909eb0ec2fdb481a756e5d367d665219ed742c64008a6b98ec7d1caeda3cf82f", + ), + ( + "self:quota:set", + "0a49cae4d934a7e7cf797530e2c7c9a2dc6df7b6c1349484e497933466cccf9b", + ), + ( + "self:workspace:promote", + "4ed985ebd2dd162f235f6667d7a847016b45ac9ea697a9f6c2c8762d3bf9b08e", + ), + ( + "self:workspace:rollback", + "a826b1764a9e6069a55d9307e96a2cedc2a4fa9df5a8b349e914be29b996a896", + ), + ( + "system:resources:unbounded", + "8c1ff06ff52166e4c21d44483bb6ba58d9d4b3c6356d411b88fa30b210c8a70f", + ), + ( + "system:shutdown", + "88bb6671343b4cfafbe8cb8a0543fea47a9d45b214f06bde24a3a7d75a2e5cef", + ), + ( + "system:status", + "fead56af90fd2681c504d8a87bfc630b7f2085552b81bff03d0bcdd22e70d9b3", + ), + ( + "uplink", + "eb57220a6af0d3eaad2cebd5a6a242a44ddda25ff31bb5968c3861659493c66c", + ), + ]; + let manifest = migration_baseline_registry().unwrap(); + let actual = manifest + .entries() + .iter() + .map(|entry| (entry.id().as_str(), entry.entry_digest().to_hex())) + .collect::>(); + + assert_eq!(actual.len(), expected.len()); + for ((actual_id, actual_digest), (expected_id, expected_digest)) in actual.iter().zip(expected) + { + assert_eq!(*actual_id, expected_id); + assert_eq!(actual_digest.as_str(), expected_digest, "{expected_id}"); + } + assert_eq!( + manifest.digest().to_hex(), + "8acc9a0070c3787531b8000ab14d055003583e2c1d6256ab47186635a58a2b37" + ); +} + #[test] fn target_order_does_not_change_entry_digest() { let left = registered( diff --git a/crates/astrid-kernel/src/kernel_router/admin/tests.rs b/crates/astrid-kernel/src/kernel_router/admin/tests.rs index 528d962d3..0a4db8beb 100644 --- a/crates/astrid-kernel/src/kernel_router/admin/tests.rs +++ b/crates/astrid-kernel/src/kernel_router/admin/tests.rs @@ -49,7 +49,7 @@ fn all_admin_variants() -> Vec { }, AdminRequestKind::AgentModify { principal: pid("a"), - add_groups: vec!["agent".into()], + add_groups: Vec::new(), remove_groups: Vec::new(), add_capsules: Vec::new(), remove_capsules: Vec::new(), @@ -62,6 +62,9 @@ fn all_admin_variants() -> Vec { AdminRequestKind::QuotaGet { principal: pid("a"), }, + AdminRequestKind::UsageGet { + principal: pid("a"), + }, AdminRequestKind::GroupCreate { name: "ops".into(), capabilities: vec!["capsule:install".into()], @@ -97,6 +100,35 @@ fn all_admin_variants() -> Vec { AdminRequestKind::CapsTokenList { principal: pid("a"), }, + AdminRequestKind::InviteIssue { + group: "agent".into(), + expires_secs: None, + max_uses: 1, + metadata: None, + }, + AdminRequestKind::InviteRedeem { + token: "x".into(), + public_key: String::new(), + display_name: None, + }, + AdminRequestKind::InviteList, + AdminRequestKind::InviteRevoke { token: "x".into() }, + AdminRequestKind::PairDeviceIssue { + expires_secs: None, + label: None, + scope: astrid_core::kernel_api::PairScopeArg::Full, + }, + AdminRequestKind::PairDeviceRedeem { + token: "x".into(), + public_key: String::new(), + }, + AdminRequestKind::PairDeviceList { + principal: pid("a"), + }, + AdminRequestKind::PairDeviceRevoke { + principal: pid("a"), + key_id: "k".into(), + }, ] } diff --git a/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs b/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs index 7ec8e3a85..f00d6c0f9 100644 --- a/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs +++ b/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs @@ -1,16 +1,10 @@ -//! Drift checks for `astrid_core::capability_grammar::CAPABILITY_CATALOG`. -//! -//! The structured catalog is what the HTTP gateway returns from -//! `/api/sys/capabilities`. If a new capability lands in -//! `required_capability` or `required_capability_for_admin_request` -//! without being added to the catalog, the gateway silently omits -//! it from discovery — breaking dashboards that build cap-grant UI -//! from the list. These tests enumerate every string returned by -//! both match tables (across both [`AuthorityScope`] variants) and -//! assert each appears in the catalog. +//! Drift checks for enforcement capability mappings and registry definitions. use astrid_core::PrincipalId; use astrid_core::capability_grammar::known_capabilities; +use astrid_core::capability_registry::{ + MIGRATION_BASELINE_CAPABILITY_IDS, migration_baseline_registry, +}; use astrid_core::kernel_api::{AdminRequestKind, KernelRequest}; use astrid_core::profile::Quotas; use std::collections::BTreeSet; @@ -35,6 +29,12 @@ fn all_kernel_request_variants() -> Vec { KernelRequest::UnloadCapsule { id: "x".to_string(), }, + KernelRequest::PromoteWorkspace { + id: "x".to_string(), + }, + KernelRequest::RollbackWorkspace { + id: "x".to_string(), + }, KernelRequest::InstallCapsule { source: "x".to_string(), workspace: false, @@ -42,6 +42,7 @@ fn all_kernel_request_variants() -> Vec { KernelRequest::ListCapsules, KernelRequest::GetCommands, KernelRequest::GetCapsuleMetadata, + KernelRequest::GetAgentReadiness, KernelRequest::ApproveCapability { request_id: "r".to_string(), signature: "s".to_string(), @@ -50,16 +51,20 @@ fn all_kernel_request_variants() -> Vec { } #[test] -fn known_capabilities_covers_every_kernel_request_cap() { +fn migration_registry_covers_every_kernel_request_cap() { + let registry = migration_baseline_registry().unwrap(); + let registered = registry + .entries() + .iter() + .map(|entry| entry.id().as_str()) + .collect::>(); let scopes = [AuthorityScope::Self_, AuthorityScope::Global]; for req in all_kernel_request_variants() { for scope in scopes { let cap = required_capability(&req, scope); assert!( - known_capabilities().any(|c| c == cap), - "kernel returns capability {cap:?} not in \ - astrid_core::capability_grammar::CAPABILITY_CATALOG — \ - update the catalog when adding a capability" + registered.contains(cap), + "kernel returns capability {cap:?} without a migration-baseline registry entry" ); } } @@ -100,6 +105,9 @@ fn all_admin_request_variants() -> Vec { AdminRequestKind::QuotaGet { principal: p.clone(), }, + AdminRequestKind::UsageGet { + principal: p.clone(), + }, AdminRequestKind::GroupCreate { name: "g".into(), capabilities: vec![], @@ -168,21 +176,112 @@ fn all_admin_request_variants() -> Vec { } #[test] -fn known_capabilities_covers_every_admin_request_cap() { +fn migration_registry_covers_every_admin_request_cap() { + let registry = migration_baseline_registry().unwrap(); + let registered = registry + .entries() + .iter() + .map(|entry| entry.id().as_str()) + .collect::>(); let scopes = [AuthorityScope::Self_, AuthorityScope::Global]; for req in &all_admin_request_variants() { for scope in scopes { let cap = required_capability_for_admin_request(req, scope); assert!( - known_capabilities().any(|c| c == cap), - "admin op returns capability {cap:?} not in \ - astrid_core::capability_grammar::CAPABILITY_CATALOG — \ - update the catalog when adding a capability" + registered.contains(cap), + "admin op returns capability {cap:?} without a migration-baseline registry entry" ); } } } +#[test] +fn migration_baseline_classifies_every_current_enforcement_role() { + let primary = BTreeSet::from([ + "system:shutdown", + "system:status", + "capsule:install", + "self:capsule:install", + "capsule:reload", + "self:capsule:reload", + "self:capsule:remove", + "self:workspace:promote", + "self:workspace:rollback", + "self:capsule:list", + "agent:create", + "agent:create:inherit", + "agent:create:clone", + "agent:delete", + "agent:enable", + "agent:disable", + "agent:modify", + "self:agent:list", + "quota:set", + "self:quota:set", + "quota:get", + "self:quota:get", + "group:create", + "group:delete", + "group:modify", + "self:group:list", + "caps:grant", + "caps:revoke", + "caps:token:mint", + "caps:token:revoke", + "caps:token:list", + "invite:issue", + "invite:list", + "invite:revoke", + "self:approval:respond", + "self:auth:pair", + "auth:pair", + ]); + let secondary = BTreeSet::from([ + "capsule:list", + "agent:list", + "group:list", + "audit:read_all", + "self:auth:pair:admin", + "system:resources:unbounded", + "net_bind", + "uplink", + ]); + let token_authenticated = BTreeSet::from(["invite:redeem", "auth:pair:redeem"]); + let dormant = BTreeSet::from([ + "capsule:access:any", + "authority:profile:manage", + "authority:repair", + ]); + let mapping_only = BTreeSet::from(["capsule:remove"]); + + let classes = [ + &primary, + &secondary, + &token_authenticated, + &dormant, + &mapping_only, + ]; + for (index, class) in classes.iter().enumerate() { + for other in classes.iter().skip(index + 1) { + assert!( + class.is_disjoint(other), + "baseline enforcement classes overlap: {:?}", + class.intersection(other).collect::>() + ); + } + } + + let classified = classes + .into_iter() + .flat_map(|class| class.iter().copied()) + .collect::>(); + let baseline = MIGRATION_BASELINE_CAPABILITY_IDS + .iter() + .copied() + .collect::>(); + assert_eq!(classified, baseline); +} + #[test] fn e2e_capability_manifest_covers_catalog() { let manifest: toml::Value = From 6a48efb708eb1bcb32118651381f92725a149a8f Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Wed, 15 Jul 2026 02:39:37 +0400 Subject: [PATCH 02/15] refactor(core): name the immutable registry revision --- CHANGELOG.md | 2 +- crates/astrid-core/src/capability_registry.rs | 81 +++++------ .../src/capability_registry/tests.rs | 131 +++++++++--------- 3 files changed, 106 insertions(+), 108 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56a60444a..8a9982c43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ Changelog tracking starts with 0.2.0. Prior versions were not tracked. Signed `.shuttle` grant composition remains deferred and fails explicitly. Closes #1195. -- **The passive authority migration baseline now has fixed semantics and digest +- **Capability-registry revision 1 now has fixed semantics and BLAKE3 digest vectors.** All 51 kernel entries bind scope, target kinds, delegability, privileged status and provenance; request mappings and current enforcement roles fail tests if they drift outside the registry. Authorization and diff --git a/crates/astrid-core/src/capability_registry.rs b/crates/astrid-core/src/capability_registry.rs index 1a505e337..676e9fc0a 100644 --- a/crates/astrid-core/src/capability_registry.rs +++ b/crates/astrid-core/src/capability_registry.rs @@ -117,10 +117,10 @@ const CAPABILITY_REGISTRY_REVISION_1_IDS: [&str; 51] = [ ]; /// Schema revision for the 51-ID authority registry. -pub const MIGRATION_BASELINE_SCHEMA_REVISION: NonZeroU32 = NonZeroU32::MIN; +pub const CAPABILITY_REGISTRY_REVISION_1: NonZeroU32 = NonZeroU32::MIN; #[derive(Clone, Copy)] -struct BaselineSemantics { +struct RevisionSemantics { scope: CapabilityScope, target_kinds: &'static [AuthorityTargetKind], delegable: bool, @@ -133,15 +133,16 @@ struct BaselineSemantics { /// /// Returns an error if an ID lacks fixed semantics or display metadata, or if /// any definition fails registry validation. -pub fn migration_baseline_registry() -> Result { - let entries = MIGRATION_BASELINE_CAPABILITY_IDS +pub fn capability_registry_revision_1() -> Result +{ + let entries = CAPABILITY_REGISTRY_REVISION_1_IDS .into_iter() .map(|id| { - let semantics = baseline_semantics(id).ok_or_else(|| { - AuthorityRegistryError::MissingBaselineDefinition { id: id.to_string() } + let semantics = revision_1_semantics(id).ok_or_else(|| { + AuthorityRegistryError::MissingRevisionDefinition { id: id.to_string() } })?; - let danger = baseline_danger(id).ok_or_else(|| { - AuthorityRegistryError::MissingBaselineDisplayMetadata { id: id.to_string() } + let danger = revision_1_danger(id).ok_or_else(|| { + AuthorityRegistryError::MissingRevisionDisplayMetadata { id: id.to_string() } })?; RegisteredCapability::new( ExactCapabilityId::new(id.to_string())?, @@ -155,10 +156,10 @@ pub fn migration_baseline_registry() -> Result, AuthorityRegistryError>>()?; - CapabilityRegistryManifest::new(MIGRATION_BASELINE_SCHEMA_REVISION, entries) + CapabilityRegistryManifest::new(CAPABILITY_REGISTRY_REVISION_1, entries) } -fn baseline_danger(id: &str) -> Option { +fn revision_1_danger(id: &str) -> Option { CAPABILITY_CATALOG .iter() .find(|entry| entry.id == id) @@ -177,38 +178,38 @@ fn baseline_danger(id: &str) -> Option { }) } -fn baseline_semantics(id: &str) -> Option { +fn revision_1_semantics(id: &str) -> Option { use AuthorityTargetKind::{ AuditScope, CapsuleInstance, CapsulePackage, Credential, Group, Principal, System, }; use CapabilityScope::{Global, Self_}; let semantics = match id { - "system:shutdown" => BaselineSemantics::new(Global, &[System], false, true), - "system:status" => BaselineSemantics::new(Global, &[System], false, false), - "capsule:install" => BaselineSemantics::new(Global, &[System, CapsulePackage], true, true), + "system:shutdown" => RevisionSemantics::new(Global, &[System], false, true), + "system:status" => RevisionSemantics::new(Global, &[System], false, false), + "capsule:install" => RevisionSemantics::new(Global, &[System, CapsulePackage], true, true), "self:capsule:install" => { - BaselineSemantics::new(Self_, &[Principal, CapsulePackage], true, false) + RevisionSemantics::new(Self_, &[Principal, CapsulePackage], true, false) }, "capsule:reload" | "capsule:remove" => { - BaselineSemantics::new(Global, &[System, CapsuleInstance], true, true) + RevisionSemantics::new(Global, &[System, CapsuleInstance], true, true) }, "self:capsule:reload" | "self:capsule:remove" | "self:workspace:promote" | "self:workspace:rollback" => { - BaselineSemantics::new(Self_, &[Principal, CapsuleInstance], true, false) + RevisionSemantics::new(Self_, &[Principal, CapsuleInstance], true, false) }, "capsule:list" | "agent:list" | "group:list" | "invite:list" => { - BaselineSemantics::new(Global, &[System], true, true) + RevisionSemantics::new(Global, &[System], true, true) }, "self:capsule:list" | "self:agent:list" | "self:group:list" | "self:quota:get" - | "self:approval:respond" => BaselineSemantics::new(Self_, &[Principal], true, false), + | "self:approval:respond" => RevisionSemantics::new(Self_, &[Principal], true, false), "agent:create" | "agent:create:clone" | "agent:modify" => { - BaselineSemantics::new(Global, &[Principal, Group, CapsulePackage], true, true) + RevisionSemantics::new(Global, &[Principal, Group, CapsulePackage], true, true) }, "agent:create:inherit" | "agent:delete" @@ -218,41 +219,41 @@ fn baseline_semantics(id: &str) -> Option { | "quota:get" | "caps:grant" | "caps:revoke" - | "caps:token:list" => BaselineSemantics::new(Global, &[Principal], true, true), - "self:quota:set" => BaselineSemantics::new(Self_, &[Principal], true, true), + | "caps:token:list" => RevisionSemantics::new(Global, &[Principal], true, true), + "self:quota:set" => RevisionSemantics::new(Self_, &[Principal], true, true), "group:create" | "group:delete" | "group:modify" => { - BaselineSemantics::new(Global, &[Group], true, true) + RevisionSemantics::new(Global, &[Group], true, true) }, "caps:token:mint" | "caps:token:revoke" => { - BaselineSemantics::new(Global, &[Principal, Credential], true, true) + RevisionSemantics::new(Global, &[Principal, Credential], true, true) }, - "invite:issue" => BaselineSemantics::new(Global, &[Group, Credential], true, true), + "invite:issue" => RevisionSemantics::new(Global, &[Group, Credential], true, true), "invite:redeem" => { - BaselineSemantics::new(Global, &[Principal, Group, Credential], false, true) + RevisionSemantics::new(Global, &[Principal, Group, Credential], false, true) }, - "invite:revoke" => BaselineSemantics::new(Global, &[Credential], true, true), - "audit:read_all" => BaselineSemantics::new(Global, &[AuditScope], true, true), - "self:auth:pair" => BaselineSemantics::new(Self_, &[Principal, Credential], true, true), + "invite:revoke" => RevisionSemantics::new(Global, &[Credential], true, true), + "audit:read_all" => RevisionSemantics::new(Global, &[AuditScope], true, true), + "self:auth:pair" => RevisionSemantics::new(Self_, &[Principal, Credential], true, true), "self:auth:pair:admin" => { - BaselineSemantics::new(Self_, &[Principal, Credential], false, true) + RevisionSemantics::new(Self_, &[Principal, Credential], false, true) }, - "auth:pair:redeem" => BaselineSemantics::new(Global, &[Principal, Credential], false, true), - "auth:pair" => BaselineSemantics::new(Global, &[Principal, Credential], true, true), + "auth:pair:redeem" => RevisionSemantics::new(Global, &[Principal, Credential], false, true), + "auth:pair" => RevisionSemantics::new(Global, &[Principal, Credential], true, true), "system:resources:unbounded" | "net_bind" | "uplink" => { - BaselineSemantics::new(Self_, &[Principal, CapsuleInstance], false, true) + RevisionSemantics::new(Self_, &[Principal, CapsuleInstance], false, true) }, "capsule:access:any" => { - BaselineSemantics::new(Self_, &[CapsulePackage, CapsuleInstance], false, true) + RevisionSemantics::new(Self_, &[CapsulePackage, CapsuleInstance], false, true) }, "authority:profile:manage" | "authority:repair" => { - BaselineSemantics::new(Global, &[System, Principal, Group, Credential], false, true) + RevisionSemantics::new(Global, &[System, Principal, Group, Credential], false, true) }, _ => return None, }; Some(semantics) } -impl BaselineSemantics { +impl RevisionSemantics { const fn new( scope: CapabilityScope, target_kinds: &'static [AuthorityTargetKind], @@ -807,14 +808,14 @@ impl CapabilityRegistryManifest { #[non_exhaustive] pub enum AuthorityRegistryError { /// A fixed capability ID has no authorization definition. - #[error("migration-baseline capability {id:?} has no authorization definition")] - MissingBaselineDefinition { + #[error("capability-registry revision 1 entry {id:?} has no authorization definition")] + MissingRevisionDefinition { /// Capability identifier. id: String, }, /// A fixed capability ID has no danger classification. - #[error("migration-baseline capability {id:?} has no display metadata")] - MissingBaselineDisplayMetadata { + #[error("capability-registry revision 1 entry {id:?} has no display metadata")] + MissingRevisionDisplayMetadata { /// Capability identifier. id: String, }, diff --git a/crates/astrid-core/src/capability_registry/tests.rs b/crates/astrid-core/src/capability_registry/tests.rs index 12f8abeb3..3381bc5a3 100644 --- a/crates/astrid-core/src/capability_registry/tests.rs +++ b/crates/astrid-core/src/capability_registry/tests.rs @@ -72,22 +72,19 @@ fn capability_registry_revision_1_freezes_current_and_dormant_exact_ids() { } #[test] -fn migration_baseline_registry_contains_every_fixed_definition() { - let manifest = migration_baseline_registry().unwrap(); +fn capability_registry_revision_1_contains_every_fixed_definition() { + let manifest = capability_registry_revision_1().unwrap(); let ids = manifest .entries() .iter() .map(|entry| entry.id().as_str()) .collect::>(); - let expected = MIGRATION_BASELINE_CAPABILITY_IDS + let expected = CAPABILITY_REGISTRY_REVISION_1_IDS .iter() .copied() .collect::>(); - assert_eq!( - manifest.schema_revision(), - MIGRATION_BASELINE_SCHEMA_REVISION - ); + assert_eq!(manifest.schema_revision(), CAPABILITY_REGISTRY_REVISION_1); assert_eq!(manifest.entries().len(), 51); assert_eq!(ids, expected); assert!( @@ -100,8 +97,8 @@ fn migration_baseline_registry_contains_every_fixed_definition() { } #[test] -fn migration_baseline_preserves_catalog_scope_and_danger() { - let manifest = migration_baseline_registry().unwrap(); +fn capability_registry_revision_1_preserves_catalog_scope_and_danger() { + let manifest = capability_registry_revision_1().unwrap(); for catalog_entry in CAPABILITY_CATALOG { let registered = manifest .entries() @@ -134,20 +131,20 @@ fn migration_baseline_preserves_catalog_scope_and_danger() { .entries() .iter() .find(|entry| entry.id().as_str() == id) - .unwrap_or_else(|| panic!("missing baseline addition {id}")); + .unwrap_or_else(|| panic!("missing revision 1 addition {id}")); assert_eq!(registered.danger(), CapabilityDanger::Extreme, "{id}"); } } #[test] -fn migration_baseline_semantics_cover_sensitive_edges() { - let manifest = migration_baseline_registry().unwrap(); +fn capability_registry_revision_1_semantics_cover_sensitive_edges() { + let manifest = capability_registry_revision_1().unwrap(); let entry = |id: &str| { manifest .entries() .iter() .find(|entry| entry.id().as_str() == id) - .unwrap_or_else(|| panic!("missing baseline capability {id}")) + .unwrap_or_else(|| panic!("missing revision 1 capability {id}")) }; assert!(!entry("system:status").delegable()); @@ -169,214 +166,214 @@ fn migration_baseline_semantics_cover_sensitive_edges() { } #[test] -fn migration_baseline_digest_vectors_are_stable() { +fn capability_registry_revision_1_digest_vectors_are_stable() { let expected = [ ( "agent:create", - "c9d1587351a1456f6e50f66d377560916a7f6f92d9d131d7024ad294628529cc", + "4e8a0890fe5a2cedd3dfdaa20329a54a28714541677290b0104ad649094ee58e", ), ( "agent:create:clone", - "eb43c11bb883dedcb5a3221a7d555c20df1ba05b7d8e5bae82a52943059d48f5", + "0e68aaaa67c638ea41abd52a6c38e55dfffaaa98bd7a64c89a0c0f7d54d25b7e", ), ( "agent:create:inherit", - "3591843cd8e4056cc0c18b29e69c1af40c9aa7deaf1c5732bbd4d5acbacc0d9a", + "7c6dc890286f51fc02f6294431007a7353a4454901f9bd05a638b0bb91f532a1", ), ( "agent:delete", - "b50d4003730c3a4cd97ba0ad60c9e172d5d999d47c45e75a4671e0bb19e92af7", + "50b16eb3103205b8a43b23c9570482ed0fb2ef6de0d7d4d4290aeb52b42fd0c9", ), ( "agent:disable", - "76218ce076f9af7f55ca306140e59ddcf3addff4937898096d06b9fb01ddc169", + "70f598abbde78c9fa59d7283204d3112ca90d575a8a1bb8e1e159c91b5b1ecf0", ), ( "agent:enable", - "26d69fe48db627f94442f56a1cd1d648e12be670cdc50342fad7051adf82fe0e", + "f369036099ab2ff2f4970d95a6ab3652921c96f7f335e6817350d96b2950436d", ), ( "agent:list", - "b929c8c5e9867f4c8d3b9998ef03f38ef4247f2000f23ae5501871e3de0674db", + "e3288b4ff5930c8bb0dd5fb132424289d11245029521720933ee06b9d99a58be", ), ( "agent:modify", - "688a7b33ada935bc7d12e0bf7a4c062f6040cb6066c2bac3b16bf537129b31d8", + "2f0b08cbbbb0608e13c41cec8e60d7569b44ab8ce67507261947e2a37d300ff7", ), ( "audit:read_all", - "0b8d39b03f848957d73ad0db00977e73e09da57f69a2680f30b26668a5885c1e", + "d37700942d51a65ae02aef16edf6ab306d1814fe0845b52d2f07c76b19acd3de", ), ( "auth:pair", - "17dd59e8d673908b9e65e9e1987ef6a956cb54ea52dd91391d39db6fb5c7a8c2", + "02d0db72af63a1bda4688296daa5979ca89148a3208d23641a0eafeb21c9691d", ), ( "auth:pair:redeem", - "755164d6481d3474ce244048c4c3b8d4f7e53e03476150334f61b62f39bd7963", + "ecf9c2e6c34b9f6d26bba43040febe57c955e53290ec17516a0ca97c22932dc4", ), ( "authority:profile:manage", - "ffe78ba8b48a775dbb344743463b60a5943c69ca764bb373e7111eb6c173f9a0", + "f5647f9424ca66e8ad86b3bbc6cd69abe769bcfaeef0afdcf1dc32ec04e66d48", ), ( "authority:repair", - "1e1367b51221b5e2667c50aead762b871a1262decf3f90e8f449591912b0d848", + "fa83a7e2fe53adccdfb8f22ec76b16442ad348a821f91f200897385b873cc770", ), ( "caps:grant", - "01655efea292a62b3656babf8d888abb1bf7752f40185a661a172633e2325823", + "ab418eaca028c77daee3cf24e603e2981249ba480605f9dad666abfa91d1d01f", ), ( "caps:revoke", - "76ceda7e4be1cc3bc52335d30851eb478e50adb766c1817075bc5f587c4da55a", + "12e19d80b91efad946ed33a3a75588e4f63b157a3f025a38450c3ec3e79a4970", ), ( "caps:token:list", - "176df3fd4672f24cbae46fbbca7999d9b0374019f93fc36136eecc6cb1a0991f", + "bc57c6774958f6df5216d77153983462c59c89a7f21db693546bbf1ed2f7de20", ), ( "caps:token:mint", - "ec9841e62d5833a1b163350e7b375f25d52bfe27623fc66aecc11c9d0a0f5f25", + "78e21ccbbe92d40efd26d8a4ae2dc9bdfb8ec3b9182a6182ebccd1477f6d11e5", ), ( "caps:token:revoke", - "6b935504883624fa06e0a0ee97aa21c889424543a08cfc30230c5c71113f1cc2", + "2ed0188f7ab75bb99147848c25f8847021dd59ce40bf952c36490b50f136e9af", ), ( "capsule:access:any", - "f9a5c447a08aa674999b32131b9ac560812f2dcfd479a426e84922ff2d52c710", + "c2e5c9eaac898896dc7c0a2bd39ad84ec7cfcc5555ddc50c0b5d8cdf981a1e18", ), ( "capsule:install", - "79eb65b1216c6edc719e107b91f5b33be0bb4865eb57f83fdb6df54cd936c6e5", + "8c6934700efda9c3077c8f2b3cb5dbc563dad62421d0a4725a982c5e5686d878", ), ( "capsule:list", - "8eeb6a4ce2191d78c82dfd9598d44250af37cfe73ce80ce4c41f278688246409", + "4a57fb6dba5592c6d0c7f046b8373fe99de315e82f783fdae86e6b99ebed1e09", ), ( "capsule:reload", - "d2b4ad2566218eb87d45cd9107d0ae45d282ffc97ec33ebc9a6b6ff03e93740f", + "2a8d975f2c82f0c510baf700352f38ad61aaa3244ad4341845a06184d38a6ae5", ), ( "capsule:remove", - "70a45a0bbce7ea9da81e04360aba4e0f6c67ddc39ae42efe7484e2b19fa45d31", + "4758193d4d2aa4ec284792ee4de016aadd51ff28ff802e7bcbf684f68243edc2", ), ( "group:create", - "d79da52e4033bf4e5febfcee1af9751c65a014fed1472f7b23e12420de5025ad", + "d8261e0b62963c7a75e47c73c7a8dfd71c898d55af212b7bbb7b1eedbfaccfbe", ), ( "group:delete", - "8a00eb4cd858515050cbcf58174c8294e769fb1d407c22ad6f941c949726a89f", + "6c1d7bb11fcb15fcbdb5cc00892dc66b24b0721c83e2afb230fd6bd14632c9a8", ), ( "group:list", - "e93d376265c24fdbc39321ef86af19c7c719687d7ea12d568bf2684c13605fc4", + "9a2e4502bc8b79b62eb7d0515486582ffb55d2e33ddd547706aebed0a209f3b9", ), ( "group:modify", - "a3957746717fce4f1922ae255a45ae16966fb54e8f9602b0106c6cbc41fa3e67", + "906832c4748cdef984c2c20a066bb11614438c1197406c984fa3e18ba169f79c", ), ( "invite:issue", - "dd6b1efec81f3183f82ae92dae8b3090bc22e5eb6eef6a071453c299a150574c", + "6304a695cf77a53846a66993940fd7fd5505aa71c65b92cb3fc003814e3eeac9", ), ( "invite:list", - "5d51573ee575411b1569cb738c1b26bac068ee78331947820de912f02d2d0ecc", + "7f62b4b325d5a03b70b21f29ce5d3489d64e8d45572288e5b9c4ca2084b42f80", ), ( "invite:redeem", - "bcf035e877441c90fee85e9fb67e17ca148f4f31d05542c13b580f4452ee4764", + "a6f9b37568ef8d649cbb1652408a640eac085d574313877a288aed1abe158d36", ), ( "invite:revoke", - "84f8a09710474578ca166aff32347aefb7e82cdfaafe495324cb7e3e48399faf", + "71bfda3067d8bdb9d70eef16381bbff69866329f3cf1fae7942cd82fa246e44e", ), ( "net_bind", - "573c7e818405313d3d9adb625e8c0f31ab515be1895d229166e8cddca9060f23", + "de912396886486562595f29f9cdb9e4e86d7cade76aa50c2b99e997cd90852bf", ), ( "quota:get", - "b5af127b2ee4038daa9ee3069c3f6b808f6e9afd970ab0398b5e0b46e0d5e74e", + "9eafffe692d740029403f1cc4f1b997f00a30aacaa4248febde92794e899c575", ), ( "quota:set", - "4064cc54281dacf202f13bcf3313eb00eb441b556312fd42d9ec23b907bc40c3", + "83a27029dde977fc3075653552e5a53fd3e04566297ce6c7ac3ff12fce4c7fab", ), ( "self:agent:list", - "70ada28d4c69802d4601d3031b294fa4cd4d462f715a3a64c14d40afa0a20c32", + "4ae3e25b7eb21956fceb73eee59d9304e9a0830397e55633216e08eebc5cfdfe", ), ( "self:approval:respond", - "fdac7a876ba95c7ba7c42cf1f4f5e3034dbcb3f07c865aa96b6894dd30ee7e9c", + "2e225891a674aee4205da30f5bbc72454df1b5b24f978acec688ddde7950cfd8", ), ( "self:auth:pair", - "965a89a82060415f1440a38947760f017660dfa7cba64c0dd5f80202ed740bb5", + "9ab37c94ad77fc70b34ad1283b4e254573fb73d068c064c365ca3828ae679cb0", ), ( "self:auth:pair:admin", - "17171d102c5907748f961ffef6363267c79142ba277719f2eada06e8140b51fa", + "5a4867fa2fc29d4e6607b149e92e2fcb83023f31962b4817f2b3b98ffc974469", ), ( "self:capsule:install", - "04cede94f155ec402dde49baeb2a94b57c089768eb61ef691553863265770081", + "5383c1090fa3e204fc20f3c04d5b3acc3325566d9d49c91756af05353759c709", ), ( "self:capsule:list", - "a5c24d855d68c7b336f74176360bb66dac44affd352d97d54397c149ba8c247a", + "c0880b629be8f453b9087a1d18f5820750e371e320f09fb7ece038117e3bd59c", ), ( "self:capsule:reload", - "b6d260d5a5aea185ee8f04d751ad7e7392b753f24d42988698af0deb0b66e11a", + "3a0882f7d4969fd06ff42ef722062ddfee384a73e0cce2328045d5dc18384089", ), ( "self:capsule:remove", - "2f50b2efdd603d2fad8aefc7a0cd785d67d29a07981afeec4c157101b58f9699", + "40da32931da6f54e2045c97c936c0a7cbb6e3c04a509b92e869b9adf6462dbeb", ), ( "self:group:list", - "d3a41e67d25c6a2b3c67f728ff03381bfef14432f3044807a1c9fc1185d5173b", + "e774470a69d80d46dfee25a4029a12f52bb4792e8802bc1309c589e69a484536", ), ( "self:quota:get", - "909eb0ec2fdb481a756e5d367d665219ed742c64008a6b98ec7d1caeda3cf82f", + "ca7311bb4622f366a71d12b33f1b3633a58bcd5af9c167a25cc2ba314d4dcbda", ), ( "self:quota:set", - "0a49cae4d934a7e7cf797530e2c7c9a2dc6df7b6c1349484e497933466cccf9b", + "5cc56b5ba3c8232f2022b7730bb0a36ffd944dc0a7eb1eafe800c44ab7060944", ), ( "self:workspace:promote", - "4ed985ebd2dd162f235f6667d7a847016b45ac9ea697a9f6c2c8762d3bf9b08e", + "c2e85a4b1738922a63d7be3f708982474dea52105e69ea4604d26cd0dee49cfc", ), ( "self:workspace:rollback", - "a826b1764a9e6069a55d9307e96a2cedc2a4fa9df5a8b349e914be29b996a896", + "50817838952523b74ec779178cd6ac1ecaa9b649137e4fd09f1767f72f4520f0", ), ( "system:resources:unbounded", - "8c1ff06ff52166e4c21d44483bb6ba58d9d4b3c6356d411b88fa30b210c8a70f", + "541ff0a36f55450dda506f8d07127e9ec585fd77a78d9895d1fe0d8befca7f91", ), ( "system:shutdown", - "88bb6671343b4cfafbe8cb8a0543fea47a9d45b214f06bde24a3a7d75a2e5cef", + "b357447f17a3e8f5821542e96ccef03bb9ebe0be4cd55b57657e4eb8e0fefdc8", ), ( "system:status", - "fead56af90fd2681c504d8a87bfc630b7f2085552b81bff03d0bcdd22e70d9b3", + "86b76eb96e06c806d3599620f8af6e539407f7b667641fe40e52a35bfb752c6d", ), ( "uplink", - "eb57220a6af0d3eaad2cebd5a6a242a44ddda25ff31bb5968c3861659493c66c", + "89561b0ac228a3c5ef22b08059b4b832bd988b038e37cf548aa5d4c8fddceaf5", ), ]; - let manifest = migration_baseline_registry().unwrap(); + let manifest = capability_registry_revision_1().unwrap(); let actual = manifest .entries() .iter() @@ -391,7 +388,7 @@ fn migration_baseline_digest_vectors_are_stable() { } assert_eq!( manifest.digest().to_hex(), - "8acc9a0070c3787531b8000ab14d055003583e2c1d6256ab47186635a58a2b37" + "111cf3fe35104ccd25767d3f0b85778c0bb2561d10016f56ac868de4607940e6" ); } From d28450c0a2f075773b72afecd306a44cab9e3869 Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Wed, 15 Jul 2026 02:46:13 +0400 Subject: [PATCH 03/15] refactor(core): type registry revision constant --- crates/astrid-core/src/capability_registry.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/astrid-core/src/capability_registry.rs b/crates/astrid-core/src/capability_registry.rs index 676e9fc0a..0a0135689 100644 --- a/crates/astrid-core/src/capability_registry.rs +++ b/crates/astrid-core/src/capability_registry.rs @@ -117,7 +117,8 @@ const CAPABILITY_REGISTRY_REVISION_1_IDS: [&str; 51] = [ ]; /// Schema revision for the 51-ID authority registry. -pub const CAPABILITY_REGISTRY_REVISION_1: NonZeroU32 = NonZeroU32::MIN; +pub const CAPABILITY_REGISTRY_REVISION_1: CapabilityRegistryRevision = + CapabilityRegistryRevision::new(NonZeroU32::MIN); #[derive(Clone, Copy)] struct RevisionSemantics { From d7f188e484e4a7c7a133a0922afa725543e8bf36 Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Wed, 15 Jul 2026 03:03:27 +0400 Subject: [PATCH 04/15] refactor(core): activate revision fixture with manifest --- crates/astrid-core/src/capability_registry.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/astrid-core/src/capability_registry.rs b/crates/astrid-core/src/capability_registry.rs index 0a0135689..266dccbe3 100644 --- a/crates/astrid-core/src/capability_registry.rs +++ b/crates/astrid-core/src/capability_registry.rs @@ -61,7 +61,6 @@ impl CapabilityRegistryRevision { /// /// This set is authority-bearing and frozen for its registry schema revision. /// Expanding it requires an intentional schema revision and reviewed digest vectors. -#[cfg(test)] const CAPABILITY_REGISTRY_REVISION_1_IDS: [&str; 51] = [ "system:shutdown", "system:status", From 4cd1b93861d71502922d74f24161290b5354ef99 Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Wed, 15 Jul 2026 03:16:22 +0400 Subject: [PATCH 05/15] fix(kernel): consume typed registry revision --- .../kernel_router/capability_catalog_tests.rs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs b/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs index f00d6c0f9..0c51f3a8d 100644 --- a/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs +++ b/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs @@ -2,9 +2,7 @@ use astrid_core::PrincipalId; use astrid_core::capability_grammar::known_capabilities; -use astrid_core::capability_registry::{ - MIGRATION_BASELINE_CAPABILITY_IDS, migration_baseline_registry, -}; +use astrid_core::capability_registry::capability_registry_revision_1; use astrid_core::kernel_api::{AdminRequestKind, KernelRequest}; use astrid_core::profile::Quotas; use std::collections::BTreeSet; @@ -51,8 +49,8 @@ fn all_kernel_request_variants() -> Vec { } #[test] -fn migration_registry_covers_every_kernel_request_cap() { - let registry = migration_baseline_registry().unwrap(); +fn registry_revision_1_covers_every_kernel_request_cap() { + let registry = capability_registry_revision_1().unwrap(); let registered = registry .entries() .iter() @@ -64,7 +62,7 @@ fn migration_registry_covers_every_kernel_request_cap() { let cap = required_capability(&req, scope); assert!( registered.contains(cap), - "kernel returns capability {cap:?} without a migration-baseline registry entry" + "kernel returns capability {cap:?} without a registry revision 1 entry" ); } } @@ -176,8 +174,8 @@ fn all_admin_request_variants() -> Vec { } #[test] -fn migration_registry_covers_every_admin_request_cap() { - let registry = migration_baseline_registry().unwrap(); +fn registry_revision_1_covers_every_admin_request_cap() { + let registry = capability_registry_revision_1().unwrap(); let registered = registry .entries() .iter() @@ -189,14 +187,14 @@ fn migration_registry_covers_every_admin_request_cap() { let cap = required_capability_for_admin_request(req, scope); assert!( registered.contains(cap), - "admin op returns capability {cap:?} without a migration-baseline registry entry" + "admin op returns capability {cap:?} without a registry revision 1 entry" ); } } } #[test] -fn migration_baseline_classifies_every_current_enforcement_role() { +fn registry_revision_1_classifies_every_current_enforcement_role() { let primary = BTreeSet::from([ "system:shutdown", "system:status", @@ -275,11 +273,13 @@ fn migration_baseline_classifies_every_current_enforcement_role() { .into_iter() .flat_map(|class| class.iter().copied()) .collect::>(); - let baseline = MIGRATION_BASELINE_CAPABILITY_IDS + let revision = capability_registry_revision_1().unwrap(); + let revision_ids = revision + .entries() .iter() - .copied() + .map(|entry| entry.id().as_str()) .collect::>(); - assert_eq!(classified, baseline); + assert_eq!(classified, revision_ids); } #[test] From b9d31b149a4b0c262a0500fafe8fdb6a5e2d3195 Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Wed, 15 Jul 2026 05:29:13 +0400 Subject: [PATCH 06/15] test(core): isolate capability digest vectors --- .../src/capability_registry/tests.rs | 418 +++++++++--------- 1 file changed, 210 insertions(+), 208 deletions(-) diff --git a/crates/astrid-core/src/capability_registry/tests.rs b/crates/astrid-core/src/capability_registry/tests.rs index 3381bc5a3..26874a84f 100644 --- a/crates/astrid-core/src/capability_registry/tests.rs +++ b/crates/astrid-core/src/capability_registry/tests.rs @@ -165,214 +165,215 @@ fn capability_registry_revision_1_semantics_cover_sensitive_edges() { ); } +const CAPABILITY_REGISTRY_REVISION_1_DIGESTS: &[(&str, &str)] = &[ + ( + "agent:create", + "4e8a0890fe5a2cedd3dfdaa20329a54a28714541677290b0104ad649094ee58e", + ), + ( + "agent:create:clone", + "0e68aaaa67c638ea41abd52a6c38e55dfffaaa98bd7a64c89a0c0f7d54d25b7e", + ), + ( + "agent:create:inherit", + "7c6dc890286f51fc02f6294431007a7353a4454901f9bd05a638b0bb91f532a1", + ), + ( + "agent:delete", + "50b16eb3103205b8a43b23c9570482ed0fb2ef6de0d7d4d4290aeb52b42fd0c9", + ), + ( + "agent:disable", + "70f598abbde78c9fa59d7283204d3112ca90d575a8a1bb8e1e159c91b5b1ecf0", + ), + ( + "agent:enable", + "f369036099ab2ff2f4970d95a6ab3652921c96f7f335e6817350d96b2950436d", + ), + ( + "agent:list", + "e3288b4ff5930c8bb0dd5fb132424289d11245029521720933ee06b9d99a58be", + ), + ( + "agent:modify", + "2f0b08cbbbb0608e13c41cec8e60d7569b44ab8ce67507261947e2a37d300ff7", + ), + ( + "audit:read_all", + "d37700942d51a65ae02aef16edf6ab306d1814fe0845b52d2f07c76b19acd3de", + ), + ( + "auth:pair", + "02d0db72af63a1bda4688296daa5979ca89148a3208d23641a0eafeb21c9691d", + ), + ( + "auth:pair:redeem", + "ecf9c2e6c34b9f6d26bba43040febe57c955e53290ec17516a0ca97c22932dc4", + ), + ( + "authority:profile:manage", + "f5647f9424ca66e8ad86b3bbc6cd69abe769bcfaeef0afdcf1dc32ec04e66d48", + ), + ( + "authority:repair", + "fa83a7e2fe53adccdfb8f22ec76b16442ad348a821f91f200897385b873cc770", + ), + ( + "caps:grant", + "ab418eaca028c77daee3cf24e603e2981249ba480605f9dad666abfa91d1d01f", + ), + ( + "caps:revoke", + "12e19d80b91efad946ed33a3a75588e4f63b157a3f025a38450c3ec3e79a4970", + ), + ( + "caps:token:list", + "bc57c6774958f6df5216d77153983462c59c89a7f21db693546bbf1ed2f7de20", + ), + ( + "caps:token:mint", + "78e21ccbbe92d40efd26d8a4ae2dc9bdfb8ec3b9182a6182ebccd1477f6d11e5", + ), + ( + "caps:token:revoke", + "2ed0188f7ab75bb99147848c25f8847021dd59ce40bf952c36490b50f136e9af", + ), + ( + "capsule:access:any", + "c2e5c9eaac898896dc7c0a2bd39ad84ec7cfcc5555ddc50c0b5d8cdf981a1e18", + ), + ( + "capsule:install", + "8c6934700efda9c3077c8f2b3cb5dbc563dad62421d0a4725a982c5e5686d878", + ), + ( + "capsule:list", + "4a57fb6dba5592c6d0c7f046b8373fe99de315e82f783fdae86e6b99ebed1e09", + ), + ( + "capsule:reload", + "2a8d975f2c82f0c510baf700352f38ad61aaa3244ad4341845a06184d38a6ae5", + ), + ( + "capsule:remove", + "4758193d4d2aa4ec284792ee4de016aadd51ff28ff802e7bcbf684f68243edc2", + ), + ( + "group:create", + "d8261e0b62963c7a75e47c73c7a8dfd71c898d55af212b7bbb7b1eedbfaccfbe", + ), + ( + "group:delete", + "6c1d7bb11fcb15fcbdb5cc00892dc66b24b0721c83e2afb230fd6bd14632c9a8", + ), + ( + "group:list", + "9a2e4502bc8b79b62eb7d0515486582ffb55d2e33ddd547706aebed0a209f3b9", + ), + ( + "group:modify", + "906832c4748cdef984c2c20a066bb11614438c1197406c984fa3e18ba169f79c", + ), + ( + "invite:issue", + "6304a695cf77a53846a66993940fd7fd5505aa71c65b92cb3fc003814e3eeac9", + ), + ( + "invite:list", + "7f62b4b325d5a03b70b21f29ce5d3489d64e8d45572288e5b9c4ca2084b42f80", + ), + ( + "invite:redeem", + "a6f9b37568ef8d649cbb1652408a640eac085d574313877a288aed1abe158d36", + ), + ( + "invite:revoke", + "71bfda3067d8bdb9d70eef16381bbff69866329f3cf1fae7942cd82fa246e44e", + ), + ( + "net_bind", + "de912396886486562595f29f9cdb9e4e86d7cade76aa50c2b99e997cd90852bf", + ), + ( + "quota:get", + "9eafffe692d740029403f1cc4f1b997f00a30aacaa4248febde92794e899c575", + ), + ( + "quota:set", + "83a27029dde977fc3075653552e5a53fd3e04566297ce6c7ac3ff12fce4c7fab", + ), + ( + "self:agent:list", + "4ae3e25b7eb21956fceb73eee59d9304e9a0830397e55633216e08eebc5cfdfe", + ), + ( + "self:approval:respond", + "2e225891a674aee4205da30f5bbc72454df1b5b24f978acec688ddde7950cfd8", + ), + ( + "self:auth:pair", + "9ab37c94ad77fc70b34ad1283b4e254573fb73d068c064c365ca3828ae679cb0", + ), + ( + "self:auth:pair:admin", + "5a4867fa2fc29d4e6607b149e92e2fcb83023f31962b4817f2b3b98ffc974469", + ), + ( + "self:capsule:install", + "5383c1090fa3e204fc20f3c04d5b3acc3325566d9d49c91756af05353759c709", + ), + ( + "self:capsule:list", + "c0880b629be8f453b9087a1d18f5820750e371e320f09fb7ece038117e3bd59c", + ), + ( + "self:capsule:reload", + "3a0882f7d4969fd06ff42ef722062ddfee384a73e0cce2328045d5dc18384089", + ), + ( + "self:capsule:remove", + "40da32931da6f54e2045c97c936c0a7cbb6e3c04a509b92e869b9adf6462dbeb", + ), + ( + "self:group:list", + "e774470a69d80d46dfee25a4029a12f52bb4792e8802bc1309c589e69a484536", + ), + ( + "self:quota:get", + "ca7311bb4622f366a71d12b33f1b3633a58bcd5af9c167a25cc2ba314d4dcbda", + ), + ( + "self:quota:set", + "5cc56b5ba3c8232f2022b7730bb0a36ffd944dc0a7eb1eafe800c44ab7060944", + ), + ( + "self:workspace:promote", + "c2e85a4b1738922a63d7be3f708982474dea52105e69ea4604d26cd0dee49cfc", + ), + ( + "self:workspace:rollback", + "50817838952523b74ec779178cd6ac1ecaa9b649137e4fd09f1767f72f4520f0", + ), + ( + "system:resources:unbounded", + "541ff0a36f55450dda506f8d07127e9ec585fd77a78d9895d1fe0d8befca7f91", + ), + ( + "system:shutdown", + "b357447f17a3e8f5821542e96ccef03bb9ebe0be4cd55b57657e4eb8e0fefdc8", + ), + ( + "system:status", + "86b76eb96e06c806d3599620f8af6e539407f7b667641fe40e52a35bfb752c6d", + ), + ( + "uplink", + "89561b0ac228a3c5ef22b08059b4b832bd988b038e37cf548aa5d4c8fddceaf5", + ), +]; + #[test] fn capability_registry_revision_1_digest_vectors_are_stable() { - let expected = [ - ( - "agent:create", - "4e8a0890fe5a2cedd3dfdaa20329a54a28714541677290b0104ad649094ee58e", - ), - ( - "agent:create:clone", - "0e68aaaa67c638ea41abd52a6c38e55dfffaaa98bd7a64c89a0c0f7d54d25b7e", - ), - ( - "agent:create:inherit", - "7c6dc890286f51fc02f6294431007a7353a4454901f9bd05a638b0bb91f532a1", - ), - ( - "agent:delete", - "50b16eb3103205b8a43b23c9570482ed0fb2ef6de0d7d4d4290aeb52b42fd0c9", - ), - ( - "agent:disable", - "70f598abbde78c9fa59d7283204d3112ca90d575a8a1bb8e1e159c91b5b1ecf0", - ), - ( - "agent:enable", - "f369036099ab2ff2f4970d95a6ab3652921c96f7f335e6817350d96b2950436d", - ), - ( - "agent:list", - "e3288b4ff5930c8bb0dd5fb132424289d11245029521720933ee06b9d99a58be", - ), - ( - "agent:modify", - "2f0b08cbbbb0608e13c41cec8e60d7569b44ab8ce67507261947e2a37d300ff7", - ), - ( - "audit:read_all", - "d37700942d51a65ae02aef16edf6ab306d1814fe0845b52d2f07c76b19acd3de", - ), - ( - "auth:pair", - "02d0db72af63a1bda4688296daa5979ca89148a3208d23641a0eafeb21c9691d", - ), - ( - "auth:pair:redeem", - "ecf9c2e6c34b9f6d26bba43040febe57c955e53290ec17516a0ca97c22932dc4", - ), - ( - "authority:profile:manage", - "f5647f9424ca66e8ad86b3bbc6cd69abe769bcfaeef0afdcf1dc32ec04e66d48", - ), - ( - "authority:repair", - "fa83a7e2fe53adccdfb8f22ec76b16442ad348a821f91f200897385b873cc770", - ), - ( - "caps:grant", - "ab418eaca028c77daee3cf24e603e2981249ba480605f9dad666abfa91d1d01f", - ), - ( - "caps:revoke", - "12e19d80b91efad946ed33a3a75588e4f63b157a3f025a38450c3ec3e79a4970", - ), - ( - "caps:token:list", - "bc57c6774958f6df5216d77153983462c59c89a7f21db693546bbf1ed2f7de20", - ), - ( - "caps:token:mint", - "78e21ccbbe92d40efd26d8a4ae2dc9bdfb8ec3b9182a6182ebccd1477f6d11e5", - ), - ( - "caps:token:revoke", - "2ed0188f7ab75bb99147848c25f8847021dd59ce40bf952c36490b50f136e9af", - ), - ( - "capsule:access:any", - "c2e5c9eaac898896dc7c0a2bd39ad84ec7cfcc5555ddc50c0b5d8cdf981a1e18", - ), - ( - "capsule:install", - "8c6934700efda9c3077c8f2b3cb5dbc563dad62421d0a4725a982c5e5686d878", - ), - ( - "capsule:list", - "4a57fb6dba5592c6d0c7f046b8373fe99de315e82f783fdae86e6b99ebed1e09", - ), - ( - "capsule:reload", - "2a8d975f2c82f0c510baf700352f38ad61aaa3244ad4341845a06184d38a6ae5", - ), - ( - "capsule:remove", - "4758193d4d2aa4ec284792ee4de016aadd51ff28ff802e7bcbf684f68243edc2", - ), - ( - "group:create", - "d8261e0b62963c7a75e47c73c7a8dfd71c898d55af212b7bbb7b1eedbfaccfbe", - ), - ( - "group:delete", - "6c1d7bb11fcb15fcbdb5cc00892dc66b24b0721c83e2afb230fd6bd14632c9a8", - ), - ( - "group:list", - "9a2e4502bc8b79b62eb7d0515486582ffb55d2e33ddd547706aebed0a209f3b9", - ), - ( - "group:modify", - "906832c4748cdef984c2c20a066bb11614438c1197406c984fa3e18ba169f79c", - ), - ( - "invite:issue", - "6304a695cf77a53846a66993940fd7fd5505aa71c65b92cb3fc003814e3eeac9", - ), - ( - "invite:list", - "7f62b4b325d5a03b70b21f29ce5d3489d64e8d45572288e5b9c4ca2084b42f80", - ), - ( - "invite:redeem", - "a6f9b37568ef8d649cbb1652408a640eac085d574313877a288aed1abe158d36", - ), - ( - "invite:revoke", - "71bfda3067d8bdb9d70eef16381bbff69866329f3cf1fae7942cd82fa246e44e", - ), - ( - "net_bind", - "de912396886486562595f29f9cdb9e4e86d7cade76aa50c2b99e997cd90852bf", - ), - ( - "quota:get", - "9eafffe692d740029403f1cc4f1b997f00a30aacaa4248febde92794e899c575", - ), - ( - "quota:set", - "83a27029dde977fc3075653552e5a53fd3e04566297ce6c7ac3ff12fce4c7fab", - ), - ( - "self:agent:list", - "4ae3e25b7eb21956fceb73eee59d9304e9a0830397e55633216e08eebc5cfdfe", - ), - ( - "self:approval:respond", - "2e225891a674aee4205da30f5bbc72454df1b5b24f978acec688ddde7950cfd8", - ), - ( - "self:auth:pair", - "9ab37c94ad77fc70b34ad1283b4e254573fb73d068c064c365ca3828ae679cb0", - ), - ( - "self:auth:pair:admin", - "5a4867fa2fc29d4e6607b149e92e2fcb83023f31962b4817f2b3b98ffc974469", - ), - ( - "self:capsule:install", - "5383c1090fa3e204fc20f3c04d5b3acc3325566d9d49c91756af05353759c709", - ), - ( - "self:capsule:list", - "c0880b629be8f453b9087a1d18f5820750e371e320f09fb7ece038117e3bd59c", - ), - ( - "self:capsule:reload", - "3a0882f7d4969fd06ff42ef722062ddfee384a73e0cce2328045d5dc18384089", - ), - ( - "self:capsule:remove", - "40da32931da6f54e2045c97c936c0a7cbb6e3c04a509b92e869b9adf6462dbeb", - ), - ( - "self:group:list", - "e774470a69d80d46dfee25a4029a12f52bb4792e8802bc1309c589e69a484536", - ), - ( - "self:quota:get", - "ca7311bb4622f366a71d12b33f1b3633a58bcd5af9c167a25cc2ba314d4dcbda", - ), - ( - "self:quota:set", - "5cc56b5ba3c8232f2022b7730bb0a36ffd944dc0a7eb1eafe800c44ab7060944", - ), - ( - "self:workspace:promote", - "c2e85a4b1738922a63d7be3f708982474dea52105e69ea4604d26cd0dee49cfc", - ), - ( - "self:workspace:rollback", - "50817838952523b74ec779178cd6ac1ecaa9b649137e4fd09f1767f72f4520f0", - ), - ( - "system:resources:unbounded", - "541ff0a36f55450dda506f8d07127e9ec585fd77a78d9895d1fe0d8befca7f91", - ), - ( - "system:shutdown", - "b357447f17a3e8f5821542e96ccef03bb9ebe0be4cd55b57657e4eb8e0fefdc8", - ), - ( - "system:status", - "86b76eb96e06c806d3599620f8af6e539407f7b667641fe40e52a35bfb752c6d", - ), - ( - "uplink", - "89561b0ac228a3c5ef22b08059b4b832bd988b038e37cf548aa5d4c8fddceaf5", - ), - ]; let manifest = capability_registry_revision_1().unwrap(); let actual = manifest .entries() @@ -380,8 +381,9 @@ fn capability_registry_revision_1_digest_vectors_are_stable() { .map(|entry| (entry.id().as_str(), entry.entry_digest().to_hex())) .collect::>(); - assert_eq!(actual.len(), expected.len()); - for ((actual_id, actual_digest), (expected_id, expected_digest)) in actual.iter().zip(expected) + assert_eq!(actual.len(), CAPABILITY_REGISTRY_REVISION_1_DIGESTS.len()); + for ((actual_id, actual_digest), (expected_id, expected_digest)) in + actual.iter().zip(CAPABILITY_REGISTRY_REVISION_1_DIGESTS) { assert_eq!(*actual_id, expected_id); assert_eq!(actual_digest.as_str(), expected_digest, "{expected_id}"); From 21a69912255c3c2317f5633d325efe9dc4e96e6c Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Wed, 15 Jul 2026 05:44:23 +0400 Subject: [PATCH 07/15] test(core): iterate frozen digest vectors by value --- crates/astrid-core/src/capability_registry/tests.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/astrid-core/src/capability_registry/tests.rs b/crates/astrid-core/src/capability_registry/tests.rs index 26874a84f..61efbd926 100644 --- a/crates/astrid-core/src/capability_registry/tests.rs +++ b/crates/astrid-core/src/capability_registry/tests.rs @@ -382,8 +382,9 @@ fn capability_registry_revision_1_digest_vectors_are_stable() { .collect::>(); assert_eq!(actual.len(), CAPABILITY_REGISTRY_REVISION_1_DIGESTS.len()); - for ((actual_id, actual_digest), (expected_id, expected_digest)) in - actual.iter().zip(CAPABILITY_REGISTRY_REVISION_1_DIGESTS) + for ((actual_id, actual_digest), (expected_id, expected_digest)) in actual + .iter() + .zip(CAPABILITY_REGISTRY_REVISION_1_DIGESTS.iter().copied()) { assert_eq!(*actual_id, expected_id); assert_eq!(actual_digest.as_str(), expected_digest, "{expected_id}"); From 2dafd16b08c28a92f27df430d722fae9b8c9c787 Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Wed, 15 Jul 2026 05:53:22 +0400 Subject: [PATCH 08/15] test(kernel): share admin request inventory --- .../src/kernel_router/admin/tests.rs | 113 +--------------- .../kernel_router/capability_catalog_tests.rs | 110 +--------------- crates/astrid-kernel/src/kernel_router/mod.rs | 2 + .../src/kernel_router/test_util.rs | 124 ++++++++++++++++++ 4 files changed, 133 insertions(+), 216 deletions(-) create mode 100644 crates/astrid-kernel/src/kernel_router/test_util.rs diff --git a/crates/astrid-kernel/src/kernel_router/admin/tests.rs b/crates/astrid-kernel/src/kernel_router/admin/tests.rs index 0a4db8beb..96e8cd33a 100644 --- a/crates/astrid-kernel/src/kernel_router/admin/tests.rs +++ b/crates/astrid-kernel/src/kernel_router/admin/tests.rs @@ -23,115 +23,12 @@ use super::{ AuthorityScope, admin_request_method, admin_response_topic, admin_target_principal, required_capability_for_admin_request, resolve_admin_scope, }; +use crate::kernel_router::test_util::all_admin_request_variants; fn pid(name: &str) -> PrincipalId { PrincipalId::new(name).unwrap() } -fn all_admin_variants() -> Vec { - vec![ - AdminRequestKind::AgentCreate { - name: "n".into(), - groups: Vec::new(), - grants: Vec::new(), - inherit_from: None, - clone_from: None, - allow_admin_clone: false, - }, - AdminRequestKind::AgentDelete { - principal: pid("a"), - }, - AdminRequestKind::AgentEnable { - principal: pid("a"), - }, - AdminRequestKind::AgentDisable { - principal: pid("a"), - }, - AdminRequestKind::AgentModify { - principal: pid("a"), - add_groups: Vec::new(), - remove_groups: Vec::new(), - add_capsules: Vec::new(), - remove_capsules: Vec::new(), - }, - AdminRequestKind::AgentList, - AdminRequestKind::QuotaSet { - principal: pid("a"), - quotas: astrid_core::profile::Quotas::default(), - }, - AdminRequestKind::QuotaGet { - principal: pid("a"), - }, - AdminRequestKind::UsageGet { - principal: pid("a"), - }, - AdminRequestKind::GroupCreate { - name: "ops".into(), - capabilities: vec!["capsule:install".into()], - description: None, - unsafe_admin: false, - }, - AdminRequestKind::GroupDelete { name: "ops".into() }, - AdminRequestKind::GroupModify { - name: "ops".into(), - capabilities: None, - description: None, - unsafe_admin: None, - }, - AdminRequestKind::GroupList, - AdminRequestKind::CapsGrant { - principal: pid("a"), - capabilities: vec!["self:capsule:install".into()], - unsafe_admin: false, - }, - AdminRequestKind::CapsRevoke { - principal: pid("a"), - capabilities: vec!["self:*".into()], - }, - AdminRequestKind::CapsTokenMint { - principal: pid("a"), - resource: "mcp://server:tool".into(), - permission: None, - ttl_secs: None, - }, - AdminRequestKind::CapsTokenRevoke { - token_id: "00000000-0000-0000-0000-000000000000".into(), - }, - AdminRequestKind::CapsTokenList { - principal: pid("a"), - }, - AdminRequestKind::InviteIssue { - group: "agent".into(), - expires_secs: None, - max_uses: 1, - metadata: None, - }, - AdminRequestKind::InviteRedeem { - token: "x".into(), - public_key: String::new(), - display_name: None, - }, - AdminRequestKind::InviteList, - AdminRequestKind::InviteRevoke { token: "x".into() }, - AdminRequestKind::PairDeviceIssue { - expires_secs: None, - label: None, - scope: astrid_core::kernel_api::PairScopeArg::Full, - }, - AdminRequestKind::PairDeviceRedeem { - token: "x".into(), - public_key: String::new(), - }, - AdminRequestKind::PairDeviceList { - principal: pid("a"), - }, - AdminRequestKind::PairDeviceRevoke { - principal: pid("a"), - key_id: "k".into(), - }, - ] -} - fn agent_profile() -> PrincipalProfile { PrincipalProfile { groups: vec!["agent".to_string()], @@ -161,7 +58,7 @@ fn authorize_with( #[test] fn every_variant_has_non_empty_mapping_in_both_scopes() { - for req in all_admin_variants() { + for req in all_admin_request_variants() { for scope in [AuthorityScope::Self_, AuthorityScope::Global] { let cap = required_capability_for_admin_request(&req, scope); assert!( @@ -285,7 +182,7 @@ fn agent_create_mapping_uses_granular_clone_and_inherit_caps() { #[test] fn every_variant_has_a_method_label() { - for req in all_admin_variants() { + for req in all_admin_request_variants() { let m = admin_request_method(&req); assert!( m.starts_with("admin."), @@ -395,7 +292,7 @@ fn agent_denied_cross_tenant_every_admin_topic() { let profile = agent_profile(); let caller = pid("agent_user"); - for req in all_admin_variants() { + for req in all_admin_request_variants() { let method = admin_request_method(&req); let scope = resolve_admin_scope(&req, &caller); let cap = required_capability_for_admin_request(&req, scope); @@ -422,7 +319,7 @@ fn admin_allowed_every_admin_topic() { let profile = admin_profile(); let caller = pid("admin_user"); - for req in all_admin_variants() { + for req in all_admin_request_variants() { let method = admin_request_method(&req); let scope = resolve_admin_scope(&req, &caller); let cap = required_capability_for_admin_request(&req, scope); diff --git a/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs b/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs index 0c51f3a8d..f71fe5da6 100644 --- a/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs +++ b/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs @@ -1,13 +1,12 @@ //! Drift checks for enforcement capability mappings and registry definitions. -use astrid_core::PrincipalId; use astrid_core::capability_grammar::known_capabilities; use astrid_core::capability_registry::capability_registry_revision_1; -use astrid_core::kernel_api::{AdminRequestKind, KernelRequest}; -use astrid_core::profile::Quotas; +use astrid_core::kernel_api::KernelRequest; use std::collections::BTreeSet; use crate::kernel_router::admin::required_capability_for_admin_request; +use crate::kernel_router::test_util::all_admin_request_variants; use crate::kernel_router::{AuthorityScope, required_capability}; /// Mirror of `tests::all_request_variants` from the parent module — @@ -68,111 +67,6 @@ fn registry_revision_1_covers_every_kernel_request_cap() { } } -fn all_admin_request_variants() -> Vec { - let p = PrincipalId::default(); - vec![ - AdminRequestKind::AgentCreate { - name: "alice".into(), - groups: vec![], - grants: vec![], - inherit_from: None, - clone_from: None, - allow_admin_clone: false, - }, - AdminRequestKind::AgentDelete { - principal: p.clone(), - }, - AdminRequestKind::AgentEnable { - principal: p.clone(), - }, - AdminRequestKind::AgentDisable { - principal: p.clone(), - }, - AdminRequestKind::AgentModify { - principal: p.clone(), - add_groups: vec![], - remove_groups: vec![], - add_capsules: vec![], - remove_capsules: vec![], - }, - AdminRequestKind::AgentList, - AdminRequestKind::QuotaSet { - principal: p.clone(), - quotas: Quotas::default(), - }, - AdminRequestKind::QuotaGet { - principal: p.clone(), - }, - AdminRequestKind::UsageGet { - principal: p.clone(), - }, - AdminRequestKind::GroupCreate { - name: "g".into(), - capabilities: vec![], - description: None, - unsafe_admin: false, - }, - AdminRequestKind::GroupDelete { name: "g".into() }, - AdminRequestKind::GroupModify { - name: "g".into(), - capabilities: None, - description: None, - unsafe_admin: None, - }, - AdminRequestKind::GroupList, - AdminRequestKind::CapsGrant { - principal: p.clone(), - capabilities: vec![], - unsafe_admin: false, - }, - AdminRequestKind::CapsRevoke { - principal: p.clone(), - capabilities: vec![], - }, - AdminRequestKind::CapsTokenMint { - principal: p.clone(), - resource: "mcp://server:tool".into(), - permission: None, - ttl_secs: None, - }, - AdminRequestKind::CapsTokenRevoke { - token_id: "00000000-0000-0000-0000-000000000000".into(), - }, - AdminRequestKind::CapsTokenList { - principal: p.clone(), - }, - AdminRequestKind::InviteIssue { - group: "agent".into(), - expires_secs: None, - max_uses: 1, - metadata: None, - }, - AdminRequestKind::InviteRedeem { - token: "x".into(), - public_key: String::new(), - display_name: None, - }, - AdminRequestKind::InviteList, - AdminRequestKind::InviteRevoke { token: "x".into() }, - AdminRequestKind::PairDeviceIssue { - expires_secs: None, - label: None, - scope: astrid_core::kernel_api::PairScopeArg::Full, - }, - AdminRequestKind::PairDeviceRedeem { - token: "x".into(), - public_key: String::new(), - }, - AdminRequestKind::PairDeviceList { - principal: p.clone(), - }, - AdminRequestKind::PairDeviceRevoke { - principal: p, - key_id: "k".into(), - }, - ] -} - #[test] fn registry_revision_1_covers_every_admin_request_cap() { let registry = capability_registry_revision_1().unwrap(); diff --git a/crates/astrid-kernel/src/kernel_router/mod.rs b/crates/astrid-kernel/src/kernel_router/mod.rs index 2b6346698..e52219faa 100644 --- a/crates/astrid-kernel/src/kernel_router/mod.rs +++ b/crates/astrid-kernel/src/kernel_router/mod.rs @@ -36,6 +36,8 @@ use device_scope::resolve_device_scope; mod capability_catalog_tests; #[cfg(test)] mod connection_tracker_tests; +#[cfg(test)] +mod test_util; /// Spawns background tasks for the kernel management API and connection tracking. /// diff --git a/crates/astrid-kernel/src/kernel_router/test_util.rs b/crates/astrid-kernel/src/kernel_router/test_util.rs new file mode 100644 index 000000000..cfd27f7a4 --- /dev/null +++ b/crates/astrid-kernel/src/kernel_router/test_util.rs @@ -0,0 +1,124 @@ +use astrid_core::PrincipalId; +use astrid_core::kernel_api::{AdminRequestKind, PairScopeArg}; +use astrid_core::profile::Quotas; + +pub(crate) fn all_admin_request_variants() -> Vec { + let principal = PrincipalId::default(); + identity_and_policy_variants(&principal) + .into_iter() + .chain(credential_variants(&principal)) + .collect() +} + +fn identity_and_policy_variants(principal: &PrincipalId) -> Vec { + vec![ + AdminRequestKind::AgentCreate { + name: "alice".into(), + groups: vec![], + grants: vec![], + inherit_from: None, + clone_from: None, + allow_admin_clone: false, + }, + AdminRequestKind::AgentDelete { + principal: principal.clone(), + }, + AdminRequestKind::AgentEnable { + principal: principal.clone(), + }, + AdminRequestKind::AgentDisable { + principal: principal.clone(), + }, + AdminRequestKind::AgentModify { + principal: principal.clone(), + add_groups: vec![], + remove_groups: vec![], + add_capsules: vec![], + remove_capsules: vec![], + }, + AdminRequestKind::AgentList, + AdminRequestKind::QuotaSet { + principal: principal.clone(), + quotas: Quotas::default(), + }, + AdminRequestKind::QuotaGet { + principal: principal.clone(), + }, + AdminRequestKind::UsageGet { + principal: principal.clone(), + }, + AdminRequestKind::GroupCreate { + name: "group".into(), + capabilities: vec![], + description: None, + unsafe_admin: false, + }, + AdminRequestKind::GroupDelete { + name: "group".into(), + }, + AdminRequestKind::GroupModify { + name: "group".into(), + capabilities: None, + description: None, + unsafe_admin: None, + }, + AdminRequestKind::GroupList, + AdminRequestKind::CapsGrant { + principal: principal.clone(), + capabilities: vec![], + unsafe_admin: false, + }, + AdminRequestKind::CapsRevoke { + principal: principal.clone(), + capabilities: vec![], + }, + ] +} + +fn credential_variants(principal: &PrincipalId) -> Vec { + vec![ + AdminRequestKind::CapsTokenMint { + principal: principal.clone(), + resource: "mcp://server:tool".into(), + permission: None, + ttl_secs: None, + }, + AdminRequestKind::CapsTokenRevoke { + token_id: "00000000-0000-0000-0000-000000000000".into(), + }, + AdminRequestKind::CapsTokenList { + principal: principal.clone(), + }, + AdminRequestKind::InviteIssue { + group: "agent".into(), + expires_secs: None, + max_uses: 1, + metadata: None, + }, + AdminRequestKind::InviteRedeem { + token: "token".into(), + public_key: String::new(), + display_name: None, + }, + AdminRequestKind::InviteList, + AdminRequestKind::InviteRevoke { + token: "token".into(), + }, + AdminRequestKind::PairDeviceIssue { + expires_secs: None, + label: None, + scope: PairScopeArg::Full, + }, + AdminRequestKind::PairDeviceRedeem { + token: "token".into(), + public_key: String::new(), + }, + AdminRequestKind::PairDeviceList { + principal: principal.clone(), + }, + AdminRequestKind::PairDeviceRevoke { + principal: principal.clone(), + key_id: "key".into(), + }, + ] +} From 742d0343980712b8e8d3ec4c503f9897bdb48ac2 Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Wed, 15 Jul 2026 06:06:58 +0400 Subject: [PATCH 09/15] fix(core): preserve registry error discriminants --- crates/astrid-core/src/capability_registry.rs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/astrid-core/src/capability_registry.rs b/crates/astrid-core/src/capability_registry.rs index 266dccbe3..57a1f03e1 100644 --- a/crates/astrid-core/src/capability_registry.rs +++ b/crates/astrid-core/src/capability_registry.rs @@ -807,18 +807,6 @@ impl CapabilityRegistryManifest { #[derive(Debug, Error)] #[non_exhaustive] pub enum AuthorityRegistryError { - /// A fixed capability ID has no authorization definition. - #[error("capability-registry revision 1 entry {id:?} has no authorization definition")] - MissingRevisionDefinition { - /// Capability identifier. - id: String, - }, - /// A fixed capability ID has no danger classification. - #[error("capability-registry revision 1 entry {id:?} has no display metadata")] - MissingRevisionDisplayMetadata { - /// Capability identifier. - id: String, - }, /// A capability ID failed the existing static capability grammar. #[error("invalid capability id {id:?}: {source}")] InvalidCapabilityId { @@ -875,6 +863,18 @@ pub enum AuthorityRegistryError { /// Recomputed digest. actual: String, }, + /// A fixed capability ID has no authorization definition. + #[error("capability-registry revision 1 entry {id:?} has no authorization definition")] + MissingRevisionDefinition { + /// Capability identifier. + id: String, + }, + /// A fixed capability ID has no danger classification. + #[error("capability-registry revision 1 entry {id:?} has no display metadata")] + MissingRevisionDisplayMetadata { + /// Capability identifier. + id: String, + }, } fn digest_entry( From 192e443a5bdc9e8baefcb288cdd7f57d9da5cf98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 07:38:20 +0000 Subject: [PATCH 10/15] fix(core): freeze revision 1 danger metadata --- crates/astrid-core/src/capability_registry.rs | 57 +++++++++++++------ 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/crates/astrid-core/src/capability_registry.rs b/crates/astrid-core/src/capability_registry.rs index 57a1f03e1..80452e695 100644 --- a/crates/astrid-core/src/capability_registry.rs +++ b/crates/astrid-core/src/capability_registry.rs @@ -9,8 +9,7 @@ use std::num::NonZeroU32; use thiserror::Error; use crate::capability_grammar::{ - CAPABILITY_CATALOG, CapabilityDanger, CapabilityGrammarError, CapabilityScope, - validate_capability, + CapabilityDanger, CapabilityGrammarError, CapabilityScope, validate_capability, }; use util::{ domain_hash, encode_array_len, encode_bool, encode_bytes, encode_text, encode_unsigned, @@ -160,22 +159,44 @@ pub fn capability_registry_revision_1() -> Result Option { - CAPABILITY_CATALOG - .iter() - .find(|entry| entry.id == id) - .map(|entry| entry.danger) - .or_else(|| { - matches!( - id, - "system:resources:unbounded" - | "net_bind" - | "uplink" - | "capsule:access:any" - | "authority:profile:manage" - | "authority:repair" - ) - .then_some(CapabilityDanger::Extreme) - }) + match id { + "system:shutdown" => Some(CapabilityDanger::Extreme), + "system:status" => Some(CapabilityDanger::Safe), + "capsule:install" => Some(CapabilityDanger::Extreme), + "self:capsule:install" => Some(CapabilityDanger::Elevated), + "capsule:reload" | "self:capsule:reload" => Some(CapabilityDanger::Normal), + "capsule:remove" => Some(CapabilityDanger::Elevated), + "self:capsule:remove" => Some(CapabilityDanger::Normal), + "self:workspace:promote" => Some(CapabilityDanger::Elevated), + "self:workspace:rollback" => Some(CapabilityDanger::Normal), + "capsule:list" | "self:capsule:list" => Some(CapabilityDanger::Safe), + "agent:create" => Some(CapabilityDanger::Normal), + "agent:create:inherit" | "agent:create:clone" => Some(CapabilityDanger::Extreme), + "agent:delete" | "agent:disable" | "agent:modify" => Some(CapabilityDanger::Elevated), + "agent:enable" => Some(CapabilityDanger::Normal), + "agent:list" | "self:agent:list" => Some(CapabilityDanger::Safe), + "quota:set" | "self:quota:set" => Some(CapabilityDanger::Normal), + "quota:get" | "self:quota:get" => Some(CapabilityDanger::Safe), + "group:create" | "group:delete" | "group:modify" => Some(CapabilityDanger::Elevated), + "group:list" | "self:group:list" => Some(CapabilityDanger::Safe), + "caps:grant" | "caps:token:mint" => Some(CapabilityDanger::Extreme), + "caps:revoke" | "caps:token:revoke" => Some(CapabilityDanger::Elevated), + "caps:token:list" => Some(CapabilityDanger::Safe), + "invite:issue" | "audit:read_all" | "self:auth:pair:admin" | "auth:pair" => { + Some(CapabilityDanger::Elevated) + }, + "invite:redeem" | "invite:revoke" | "self:auth:pair" | "auth:pair:redeem" => { + Some(CapabilityDanger::Normal) + }, + "invite:list" | "self:approval:respond" => Some(CapabilityDanger::Safe), + "system:resources:unbounded" + | "net_bind" + | "uplink" + | "capsule:access:any" + | "authority:profile:manage" + | "authority:repair" => Some(CapabilityDanger::Extreme), + _ => None, + } } fn revision_1_semantics(id: &str) -> Option { From cf46ae326bf20dc60840a1e13545c848145b5b85 Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Wed, 15 Jul 2026 12:42:44 +0400 Subject: [PATCH 11/15] fix(core): group frozen danger mappings --- crates/astrid-core/src/capability_registry.rs | 86 +++++++++++-------- 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/crates/astrid-core/src/capability_registry.rs b/crates/astrid-core/src/capability_registry.rs index 80452e695..84b024662 100644 --- a/crates/astrid-core/src/capability_registry.rs +++ b/crates/astrid-core/src/capability_registry.rs @@ -159,44 +159,62 @@ pub fn capability_registry_revision_1() -> Result Option { - match id { - "system:shutdown" => Some(CapabilityDanger::Extreme), - "system:status" => Some(CapabilityDanger::Safe), - "capsule:install" => Some(CapabilityDanger::Extreme), - "self:capsule:install" => Some(CapabilityDanger::Elevated), - "capsule:reload" | "self:capsule:reload" => Some(CapabilityDanger::Normal), - "capsule:remove" => Some(CapabilityDanger::Elevated), - "self:capsule:remove" => Some(CapabilityDanger::Normal), - "self:workspace:promote" => Some(CapabilityDanger::Elevated), - "self:workspace:rollback" => Some(CapabilityDanger::Normal), - "capsule:list" | "self:capsule:list" => Some(CapabilityDanger::Safe), - "agent:create" => Some(CapabilityDanger::Normal), - "agent:create:inherit" | "agent:create:clone" => Some(CapabilityDanger::Extreme), - "agent:delete" | "agent:disable" | "agent:modify" => Some(CapabilityDanger::Elevated), - "agent:enable" => Some(CapabilityDanger::Normal), - "agent:list" | "self:agent:list" => Some(CapabilityDanger::Safe), - "quota:set" | "self:quota:set" => Some(CapabilityDanger::Normal), - "quota:get" | "self:quota:get" => Some(CapabilityDanger::Safe), - "group:create" | "group:delete" | "group:modify" => Some(CapabilityDanger::Elevated), - "group:list" | "self:group:list" => Some(CapabilityDanger::Safe), - "caps:grant" | "caps:token:mint" => Some(CapabilityDanger::Extreme), - "caps:revoke" | "caps:token:revoke" => Some(CapabilityDanger::Elevated), - "caps:token:list" => Some(CapabilityDanger::Safe), - "invite:issue" | "audit:read_all" | "self:auth:pair:admin" | "auth:pair" => { - Some(CapabilityDanger::Elevated) - }, - "invite:redeem" | "invite:revoke" | "self:auth:pair" | "auth:pair:redeem" => { - Some(CapabilityDanger::Normal) - }, - "invite:list" | "self:approval:respond" => Some(CapabilityDanger::Safe), - "system:resources:unbounded" + use CapabilityDanger::{Elevated, Extreme, Normal, Safe}; + + Some(match id { + "system:status" + | "capsule:list" + | "self:capsule:list" + | "agent:list" + | "self:agent:list" + | "quota:get" + | "self:quota:get" + | "group:list" + | "self:group:list" + | "caps:token:list" + | "invite:list" + | "self:approval:respond" => Safe, + "capsule:reload" + | "self:capsule:reload" + | "self:capsule:remove" + | "self:workspace:rollback" + | "agent:create" + | "agent:enable" + | "quota:set" + | "self:quota:set" + | "invite:redeem" + | "invite:revoke" + | "self:auth:pair" + | "auth:pair:redeem" => Normal, + "self:capsule:install" + | "capsule:remove" + | "self:workspace:promote" + | "agent:delete" + | "agent:disable" + | "agent:modify" + | "group:create" + | "group:delete" + | "group:modify" + | "caps:revoke" + | "caps:token:revoke" + | "invite:issue" + | "audit:read_all" + | "self:auth:pair:admin" + | "auth:pair" => Elevated, + "system:shutdown" + | "capsule:install" + | "agent:create:inherit" + | "agent:create:clone" + | "caps:grant" + | "caps:token:mint" + | "system:resources:unbounded" | "net_bind" | "uplink" | "capsule:access:any" | "authority:profile:manage" - | "authority:repair" => Some(CapabilityDanger::Extreme), - _ => None, - } + | "authority:repair" => Extreme, + _ => return None, + }) } fn revision_1_semantics(id: &str) -> Option { From 24d77c61362a018601cd7f876597d3f0243b0ea3 Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Wed, 15 Jul 2026 13:04:41 +0400 Subject: [PATCH 12/15] refactor(core): split revision metadata --- crates/astrid-core/src/capability_registry.rs | 62 +------------------ .../src/capability_registry/revision_1.rs | 60 ++++++++++++++++++ 2 files changed, 62 insertions(+), 60 deletions(-) create mode 100644 crates/astrid-core/src/capability_registry/revision_1.rs diff --git a/crates/astrid-core/src/capability_registry.rs b/crates/astrid-core/src/capability_registry.rs index 84b024662..ce69c593d 100644 --- a/crates/astrid-core/src/capability_registry.rs +++ b/crates/astrid-core/src/capability_registry.rs @@ -16,6 +16,7 @@ use util::{ validate_digest_length, }; +mod revision_1; mod util; const ENTRY_DIGEST_DOMAIN: &[u8] = b"astrid-capability-entry\0"; @@ -140,7 +141,7 @@ pub fn capability_registry_revision_1() -> Result Result Option { - use CapabilityDanger::{Elevated, Extreme, Normal, Safe}; - - Some(match id { - "system:status" - | "capsule:list" - | "self:capsule:list" - | "agent:list" - | "self:agent:list" - | "quota:get" - | "self:quota:get" - | "group:list" - | "self:group:list" - | "caps:token:list" - | "invite:list" - | "self:approval:respond" => Safe, - "capsule:reload" - | "self:capsule:reload" - | "self:capsule:remove" - | "self:workspace:rollback" - | "agent:create" - | "agent:enable" - | "quota:set" - | "self:quota:set" - | "invite:redeem" - | "invite:revoke" - | "self:auth:pair" - | "auth:pair:redeem" => Normal, - "self:capsule:install" - | "capsule:remove" - | "self:workspace:promote" - | "agent:delete" - | "agent:disable" - | "agent:modify" - | "group:create" - | "group:delete" - | "group:modify" - | "caps:revoke" - | "caps:token:revoke" - | "invite:issue" - | "audit:read_all" - | "self:auth:pair:admin" - | "auth:pair" => Elevated, - "system:shutdown" - | "capsule:install" - | "agent:create:inherit" - | "agent:create:clone" - | "caps:grant" - | "caps:token:mint" - | "system:resources:unbounded" - | "net_bind" - | "uplink" - | "capsule:access:any" - | "authority:profile:manage" - | "authority:repair" => Extreme, - _ => return None, - }) -} - fn revision_1_semantics(id: &str) -> Option { use AuthorityTargetKind::{ AuditScope, CapsuleInstance, CapsulePackage, Credential, Group, Principal, System, diff --git a/crates/astrid-core/src/capability_registry/revision_1.rs b/crates/astrid-core/src/capability_registry/revision_1.rs new file mode 100644 index 000000000..20d05bca1 --- /dev/null +++ b/crates/astrid-core/src/capability_registry/revision_1.rs @@ -0,0 +1,60 @@ +use super::CapabilityDanger; + +pub(super) fn danger(id: &str) -> Option { + use CapabilityDanger::{Elevated, Extreme, Normal, Safe}; + + Some(match id { + "system:status" + | "capsule:list" + | "self:capsule:list" + | "agent:list" + | "self:agent:list" + | "quota:get" + | "self:quota:get" + | "group:list" + | "self:group:list" + | "caps:token:list" + | "invite:list" + | "self:approval:respond" => Safe, + "capsule:reload" + | "self:capsule:reload" + | "self:capsule:remove" + | "self:workspace:rollback" + | "agent:create" + | "agent:enable" + | "quota:set" + | "self:quota:set" + | "invite:redeem" + | "invite:revoke" + | "self:auth:pair" + | "auth:pair:redeem" => Normal, + "self:capsule:install" + | "capsule:remove" + | "self:workspace:promote" + | "agent:delete" + | "agent:disable" + | "agent:modify" + | "group:create" + | "group:delete" + | "group:modify" + | "caps:revoke" + | "caps:token:revoke" + | "invite:issue" + | "audit:read_all" + | "self:auth:pair:admin" + | "auth:pair" => Elevated, + "system:shutdown" + | "capsule:install" + | "agent:create:inherit" + | "agent:create:clone" + | "caps:grant" + | "caps:token:mint" + | "system:resources:unbounded" + | "net_bind" + | "uplink" + | "capsule:access:any" + | "authority:profile:manage" + | "authority:repair" => Extreme, + _ => return None, + }) +} From 8d53151ec52760d27a66eeaf1bfb6e92b8eed386 Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Wed, 15 Jul 2026 18:01:46 +0400 Subject: [PATCH 13/15] fix(kernel): classify live capsule access authority --- .../src/kernel_router/capability_catalog_tests.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs b/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs index f71fe5da6..6d56ead9a 100644 --- a/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs +++ b/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs @@ -137,13 +137,10 @@ fn registry_revision_1_classifies_every_current_enforcement_role() { "system:resources:unbounded", "net_bind", "uplink", - ]); - let token_authenticated = BTreeSet::from(["invite:redeem", "auth:pair:redeem"]); - let dormant = BTreeSet::from([ "capsule:access:any", - "authority:profile:manage", - "authority:repair", ]); + let token_authenticated = BTreeSet::from(["invite:redeem", "auth:pair:redeem"]); + let dormant = BTreeSet::from(["authority:profile:manage", "authority:repair"]); let mapping_only = BTreeSet::from(["capsule:remove"]); let classes = [ From 93c7f07a8c50d5f63e9f369a1fffb9d27d9a7aea Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Wed, 15 Jul 2026 18:18:29 +0400 Subject: [PATCH 14/15] test(core): bind registry checks to enforcement sites --- CHANGELOG.md | 8 ++-- crates/astrid-capsule/src/access.rs | 12 ++++++ .../src/engine/wasm/host/ipc_tests.rs | 3 +- crates/astrid-capsule/src/engine/wasm/mod.rs | 18 +++++--- .../kernel_router/capability_catalog_tests.rs | 43 +------------------ .../src/kernel_router/test_util.rs | 26 ++++++++++- .../astrid-kernel/src/kernel_router/tests.rs | 38 ++-------------- 7 files changed, 62 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9982c43..e569ad36c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,9 +33,11 @@ Changelog tracking starts with 0.2.0. Prior versions were not tracked. - **Capability-registry revision 1 now has fixed semantics and BLAKE3 digest vectors.** All 51 kernel entries bind scope, target kinds, delegability, - privileged status and provenance; request mappings and current enforcement - roles fail tests if they drift outside the registry. Authorization and - persisted state remain unchanged. Closes #1235. Refs #1228 and #1233. + privileged status and provenance. Kernel/admin request mappings and the + current capsule-side secondary enforcement constants resolve through the + registry in tests, while the complete role partition is frozen independently. + Authorization and persisted state remain unchanged. Closes #1235. Refs #1228 + and #1233. ### Changed diff --git a/crates/astrid-capsule/src/access.rs b/crates/astrid-capsule/src/access.rs index eeee3be9e..86af3efbc 100644 --- a/crates/astrid-capsule/src/access.rs +++ b/crates/astrid-capsule/src/access.rs @@ -361,6 +361,18 @@ mod tests { DeviceKey::new(seed.to_string().repeat(64), scope, None, 0) } + #[test] + fn unrestricted_capsule_access_enforcement_id_is_registered() { + let registry = astrid_core::capability_registry::capability_registry_revision_1().unwrap(); + assert!( + registry + .entries() + .iter() + .any(|entry| entry.id().as_str() == CAPSULE_ACCESS_ANY), + "capsule access enforcement uses {CAPSULE_ACCESS_ANY:?} without a registry revision 1 entry" + ); + } + #[test] fn none_principal_denied() { let (_d, _h, r) = fixture(); diff --git a/crates/astrid-capsule/src/engine/wasm/host/ipc_tests.rs b/crates/astrid-capsule/src/engine/wasm/host/ipc_tests.rs index 6257650a7..5edb3d149 100644 --- a/crates/astrid-capsule/src/engine/wasm/host/ipc_tests.rs +++ b/crates/astrid-capsule/src/engine/wasm/host/ipc_tests.rs @@ -21,7 +21,8 @@ fn audit_topic_literal_pinned() { // `pattern_covers_audit` would otherwise silently stop recognising // audit subscriptions and leave them on the unscoped firehose default // for the renamed topic — exactly the drift the doc comment promises - // is guarded. Mirrors `tests::audit_firehose_cap_literal_pinned`. + // is guarded. The separate firehose capability identifier is checked + // against registry revision 1 by `tests::secondary_enforcement_ids_are_registered`. assert_eq!(AUDIT_TOPIC, "astrid.v1.audit.entry"); } diff --git a/crates/astrid-capsule/src/engine/wasm/mod.rs b/crates/astrid-capsule/src/engine/wasm/mod.rs index ddab17ea2..eb6d34b07 100644 --- a/crates/astrid-capsule/src/engine/wasm/mod.rs +++ b/crates/astrid-capsule/src/engine/wasm/mod.rs @@ -670,7 +670,7 @@ pub(crate) fn resolve_exemption( /// Elevated). A capsule-local literal keeps the kernel/capsule dependency /// boundary clean (the capsule must not reach into the gateway or grow the /// core grammar surface for one internal reference); the value is pinned by -/// [`tests::audit_firehose_cap_literal_pinned`]. +/// [`tests::secondary_enforcement_ids_are_registered`]. const AUDIT_FIREHOSE_CAP: &str = "audit:read_all"; /// Pure decision: does this load principal's profile hold the audit @@ -3562,11 +3562,17 @@ mod tests { // pinned positively/negatively below. #[test] - fn audit_firehose_cap_literal_pinned() { - // The capsule-local literal must stay byte-equal to the gateway's - // `events::AUDIT_FIREHOSE_CAP` and the core grammar's `audit:read_all` - // so the three references can never drift. - assert_eq!(AUDIT_FIREHOSE_CAP, "audit:read_all"); + fn secondary_enforcement_ids_are_registered() { + let registry = astrid_core::capability_registry::capability_registry_revision_1().unwrap(); + for id in std::iter::once(AUDIT_FIREHOSE_CAP).chain(astrid_core::EXEMPT_CAPABILITIES) { + assert!( + registry + .entries() + .iter() + .any(|entry| entry.id().as_str() == id), + "capsule enforcement uses {id:?} without a registry revision 1 entry" + ); + } } #[test] diff --git a/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs b/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs index 6d56ead9a..cc50a7119 100644 --- a/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs +++ b/crates/astrid-kernel/src/kernel_router/capability_catalog_tests.rs @@ -2,51 +2,12 @@ use astrid_core::capability_grammar::known_capabilities; use astrid_core::capability_registry::capability_registry_revision_1; -use astrid_core::kernel_api::KernelRequest; use std::collections::BTreeSet; use crate::kernel_router::admin::required_capability_for_admin_request; -use crate::kernel_router::test_util::all_admin_request_variants; +use crate::kernel_router::test_util::{all_admin_request_variants, all_kernel_request_variants}; use crate::kernel_router::{AuthorityScope, required_capability}; -/// Mirror of `tests::all_request_variants` from the parent module — -/// duplicated here because the parent's helper lives inside a -/// `#[cfg(test)] mod tests` block and isn't visible to sibling test -/// modules. Adding a `KernelRequest` variant requires updating both -/// lists; the size is small enough that the duplication beats -/// plumbing visibility through. -fn all_kernel_request_variants() -> Vec { - vec![ - KernelRequest::Shutdown { reason: None }, - KernelRequest::GetStatus, - KernelRequest::ReloadCapsules, - KernelRequest::ReloadCapsule { - id: "x".to_string(), - }, - KernelRequest::UnloadCapsule { - id: "x".to_string(), - }, - KernelRequest::PromoteWorkspace { - id: "x".to_string(), - }, - KernelRequest::RollbackWorkspace { - id: "x".to_string(), - }, - KernelRequest::InstallCapsule { - source: "x".to_string(), - workspace: false, - }, - KernelRequest::ListCapsules, - KernelRequest::GetCommands, - KernelRequest::GetCapsuleMetadata, - KernelRequest::GetAgentReadiness, - KernelRequest::ApproveCapability { - request_id: "r".to_string(), - signature: "s".to_string(), - }, - ] -} - #[test] fn registry_revision_1_covers_every_kernel_request_cap() { let registry = capability_registry_revision_1().unwrap(); @@ -88,7 +49,7 @@ fn registry_revision_1_covers_every_admin_request_cap() { } #[test] -fn registry_revision_1_classifies_every_current_enforcement_role() { +fn registry_revision_1_freezes_the_complete_role_partition() { let primary = BTreeSet::from([ "system:shutdown", "system:status", diff --git a/crates/astrid-kernel/src/kernel_router/test_util.rs b/crates/astrid-kernel/src/kernel_router/test_util.rs index cfd27f7a4..7c4b17812 100644 --- a/crates/astrid-kernel/src/kernel_router/test_util.rs +++ b/crates/astrid-kernel/src/kernel_router/test_util.rs @@ -1,7 +1,31 @@ use astrid_core::PrincipalId; -use astrid_core::kernel_api::{AdminRequestKind, PairScopeArg}; +use astrid_core::kernel_api::{AdminRequestKind, KernelRequest, PairScopeArg}; use astrid_core::profile::Quotas; +pub(crate) fn all_kernel_request_variants() -> Vec { + vec![ + KernelRequest::Shutdown { reason: None }, + KernelRequest::GetStatus, + KernelRequest::ReloadCapsules, + KernelRequest::ReloadCapsule { id: "x".into() }, + KernelRequest::UnloadCapsule { id: "x".into() }, + KernelRequest::PromoteWorkspace { id: "x".into() }, + KernelRequest::RollbackWorkspace { id: "x".into() }, + KernelRequest::InstallCapsule { + source: "x".into(), + workspace: false, + }, + KernelRequest::ListCapsules, + KernelRequest::GetCommands, + KernelRequest::GetCapsuleMetadata, + KernelRequest::GetAgentReadiness, + KernelRequest::ApproveCapability { + request_id: "r".into(), + signature: "s".into(), + }, + ] +} + pub(crate) fn all_admin_request_variants() -> Vec { let principal = PrincipalId::default(); identity_and_policy_variants(&principal) diff --git a/crates/astrid-kernel/src/kernel_router/tests.rs b/crates/astrid-kernel/src/kernel_router/tests.rs index 0fefa90a1..50b72a83c 100644 --- a/crates/astrid-kernel/src/kernel_router/tests.rs +++ b/crates/astrid-kernel/src/kernel_router/tests.rs @@ -12,6 +12,8 @@ use astrid_core::kernel_api::CommandKind; use astrid_core::profile::{AuthMethod, DeviceKey, DeviceScope, PrincipalProfile}; use std::sync::atomic::AtomicBool; +use super::test_util::all_kernel_request_variants; + struct InventoryCapsule { id: CapsuleId, manifest: CapsuleManifest, @@ -225,41 +227,9 @@ fn rate_limit_for_request_returns_correct_limits() { // ── Capability mapping (issue #670) ────────────────────────────── -fn all_request_variants() -> Vec { - vec![ - KernelRequest::Shutdown { reason: None }, - KernelRequest::GetStatus, - KernelRequest::ReloadCapsules, - KernelRequest::ReloadCapsule { - id: "x".to_string(), - }, - KernelRequest::UnloadCapsule { - id: "x".to_string(), - }, - KernelRequest::PromoteWorkspace { - id: "x".to_string(), - }, - KernelRequest::RollbackWorkspace { - id: "x".to_string(), - }, - KernelRequest::InstallCapsule { - source: "x".to_string(), - workspace: false, - }, - KernelRequest::ListCapsules, - KernelRequest::GetCommands, - KernelRequest::GetCapsuleMetadata, - KernelRequest::GetAgentReadiness, - KernelRequest::ApproveCapability { - request_id: "r".to_string(), - signature: "s".to_string(), - }, - ] -} - #[test] fn required_capability_every_variant_has_non_empty_mapping() { - for req in all_request_variants() { + for req in all_kernel_request_variants() { let cap = required_capability(&req, AuthorityScope::Self_); assert!( !cap.is_empty(), @@ -376,7 +346,7 @@ fn required_capability_mapping_global_scope() { #[test] fn resolve_scope_defaults_to_self_except_daemon_capsule_lifecycle() { let caller = PrincipalId::new("alice").unwrap(); - for req in all_request_variants() { + for req in all_kernel_request_variants() { if matches!( req, KernelRequest::ReloadCapsules From aaf8952f08c2441b217fec60d90de4a40c922754 Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Wed, 15 Jul 2026 18:29:02 +0400 Subject: [PATCH 15/15] test(capsule): pin registered enforcement ids --- crates/astrid-capsule/src/access.rs | 1 + crates/astrid-capsule/src/engine/wasm/mod.rs | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/crates/astrid-capsule/src/access.rs b/crates/astrid-capsule/src/access.rs index 86af3efbc..2283efe53 100644 --- a/crates/astrid-capsule/src/access.rs +++ b/crates/astrid-capsule/src/access.rs @@ -363,6 +363,7 @@ mod tests { #[test] fn unrestricted_capsule_access_enforcement_id_is_registered() { + assert_eq!(CAPSULE_ACCESS_ANY, "capsule:access:any"); let registry = astrid_core::capability_registry::capability_registry_revision_1().unwrap(); assert!( registry diff --git a/crates/astrid-capsule/src/engine/wasm/mod.rs b/crates/astrid-capsule/src/engine/wasm/mod.rs index eb6d34b07..0b98b77cc 100644 --- a/crates/astrid-capsule/src/engine/wasm/mod.rs +++ b/crates/astrid-capsule/src/engine/wasm/mod.rs @@ -3563,6 +3563,11 @@ mod tests { #[test] fn secondary_enforcement_ids_are_registered() { + assert_eq!(AUDIT_FIREHOSE_CAP, "audit:read_all"); + assert_eq!( + astrid_core::EXEMPT_CAPABILITIES, + ["system:resources:unbounded", "net_bind", "uplink"] + ); let registry = astrid_core::capability_registry::capability_registry_revision_1().unwrap(); for id in std::iter::once(AUDIT_FIREHOSE_CAP).chain(astrid_core::EXEMPT_CAPABILITIES) { assert!(