Add reusable IFC policy engine

Signed-off-by: Jordan Mecom <jm@squareup.com>
This commit is contained in:
Jordan Mecom
2026-08-28 12:06:36 -07:00
parent ed11c8d8bf
commit 4c2de983fc
9 changed files with 1673 additions and 0 deletions

12
Cargo.lock generated
View File

@@ -1126,6 +1126,18 @@ dependencies = [
"zeroize",
]
[[package]]
name = "buzz-ifc"
version = "0.1.0"
dependencies = [
"hex",
"nostr 0.44.7",
"serde",
"sha2 0.11.0",
"thiserror 2.0.18",
"uuid",
]
[[package]]
name = "buzz-media"
version = "0.1.0"

View File

@@ -10,6 +10,7 @@ members = [
"crates/buzz-search",
"crates/buzz-audit",
"crates/buzz-acp",
"crates/buzz-ifc",
"crates/buzz-agent",
"crates/sprig",
"crates/buzz-test-client",
@@ -137,6 +138,7 @@ schemars = { version = "1", default-features = false }
# Internal crates
buzz-core = { path = "crates/buzz-core" }
buzz-ifc = { path = "crates/buzz-ifc" }
buzz-conformance = { path = "crates/buzz-conformance" }
buzz-db = { path = "crates/buzz-db" }
buzz-deletion = { path = "crates/buzz-deletion" }

View File

@@ -0,0 +1,16 @@
[package]
name = "buzz-ifc"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Audience-scoped information-flow policy for Buzz agents"
[dependencies]
hex = { workspace = true }
nostr = { workspace = true }
serde = { workspace = true }
sha2 = { workspace = true }
thiserror = { workspace = true }
uuid = { workspace = true }

View File

@@ -0,0 +1,136 @@
use std::marker::PhantomData;
use crate::domain::DomainContext;
use crate::label::{ConfidentialityLabel, Principal};
/// Typestate marker for a grant whose owner signature has not been checked.
pub struct PendingGrant;
/// Typestate marker for a grant authenticated by an external verifier.
pub struct VerifiedGrant;
/// Paper: "Declassification." A content- and destination-specific grant.
///
/// A verified value is consumed by its first matching publication decision.
/// The broker remains responsible for durable replay and expiry enforcement
/// before verification.
pub struct DeclassificationGrant<State> {
approver: Principal,
source_domain_id: String,
destination: ConfidentialityLabel,
destination_context: DomainContext,
content_digest: [u8; 32],
consumed: bool,
_state: PhantomData<State>,
}
impl<State> DeclassificationGrant<State> {
/// Return the principal that approved this release.
pub fn approver(&self) -> &Principal {
&self.approver
}
/// Return the exact execution domain from which the content came.
pub fn source_domain_id(&self) -> &str {
&self.source_domain_id
}
/// Return the audience approved to receive the content.
pub fn destination(&self) -> &ConfidentialityLabel {
&self.destination
}
/// Return the exact destination context approved for the release.
pub fn destination_context(&self) -> &DomainContext {
&self.destination_context
}
/// Return the digest of the exact content approved for release.
pub fn content_digest(&self) -> &[u8; 32] {
&self.content_digest
}
}
/// Verifies the owner signature over a pending grant's canonical payload.
pub trait GrantSignatureVerifier {
/// Return true only when the grant bears an authentic owner signature and
/// its externally stored expiry or replay policy still permits use.
fn verifies(&self, grant: &DeclassificationGrant<PendingGrant>) -> bool;
}
impl DeclassificationGrant<PendingGrant> {
/// Construct an unverified grant from signed-event fields.
pub fn pending(
approver: Principal,
source_domain_id: String,
destination: ConfidentialityLabel,
destination_context: DomainContext,
content_digest: [u8; 32],
) -> Self {
Self {
approver,
source_domain_id,
destination,
destination_context,
content_digest,
consumed: false,
_state: PhantomData,
}
}
/// Authenticate the owner and move the grant into the verified typestate.
pub fn verify<V: GrantSignatureVerifier>(
self,
expected_owner: &Principal,
verifier: &V,
) -> Result<DeclassificationGrant<VerifiedGrant>, GrantError> {
if &self.approver != expected_owner {
return Err(GrantError::WrongApprover);
}
if !verifier.verifies(&self) {
return Err(GrantError::InvalidSignature);
}
Ok(DeclassificationGrant {
approver: self.approver,
source_domain_id: self.source_domain_id,
destination: self.destination,
destination_context: self.destination_context,
content_digest: self.content_digest,
consumed: false,
_state: PhantomData,
})
}
}
impl DeclassificationGrant<VerifiedGrant> {
pub(crate) fn matches(
&mut self,
source_domain_id: &str,
destination: &ConfidentialityLabel,
destination_context: &DomainContext,
content_digest: &[u8; 32],
) -> bool {
if self.consumed {
return false;
}
let matches = self.source_domain_id == source_domain_id
&& &self.destination == destination
&& &self.destination_context == destination_context
&& &self.content_digest == content_digest;
if matches {
self.consumed = true;
}
matches
}
}
/// A declassification grant failed owner authentication.
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
pub enum GrantError {
/// The signer is not the expected bot owner.
#[error("declassification grant was signed by the wrong principal")]
WrongApprover,
/// The supplied signature did not authenticate the grant.
#[error("declassification grant signature is invalid")]
InvalidSignature,
}

View File

@@ -0,0 +1,592 @@
use std::collections::BTreeSet;
use serde::Serialize;
use sha2::{Digest, Sha256};
use uuid::Uuid;
use crate::hash::{hash_field, short_fingerprint};
use crate::label::{ConfidentialityLabel, LabelError, Principal, ReaderSet, RealmId};
/// Which retained context a worker belongs to.
///
/// Paper: "Execution domains." Context remains separate from audience: two
/// conversations may have identical participants without implicitly sharing
/// memory.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum DomainContext {
/// Shared state for public conversations in one realm.
RealmPublic(RealmId),
/// State retained for one specific restricted conversation.
Conversation {
/// The Buzz community containing the conversation.
realm: RealmId,
/// The channel, DM, or group-DM identifier.
channel_id: Uuid,
},
/// State visible only to the bot owner.
OwnerPrivate {
/// The Buzz community containing the owner relationship.
realm: RealmId,
/// The bot owner.
owner: Principal,
},
}
/// Runtime placement required by an execution domain.
///
/// The IFC rules do not implement an OS sandbox. They tell the harness whether
/// a worker may use the shared public runtime or must be placed in a compartment
/// dedicated to one complete execution domain.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CompartmentProfile {
/// Realm-public conversations may share a worker, public memory, and public
/// tools. The worker must still be unable to reach broker secrets or private
/// compartments.
SharedPublic,
/// A restricted conversation or owner-private task requires a worker whose
/// writable state and output paths are confined to the exact domain.
DomainConfined,
}
impl CompartmentProfile {
/// Return the stable wire and log representation.
pub fn as_str(self) -> &'static str {
match self {
Self::SharedPublic => "shared_public",
Self::DomainConfined => "domain_confined",
}
}
}
impl DomainContext {
/// Return the realm containing this context.
pub fn realm(&self) -> &RealmId {
match self {
Self::RealmPublic(realm)
| Self::Conversation { realm, .. }
| Self::OwnerPrivate { realm, .. } => realm,
}
}
/// Return a stable context category for logs and protocol responses.
pub fn kind(&self) -> &'static str {
match self {
Self::RealmPublic(_) => "public",
Self::Conversation { .. } => "conversation",
Self::OwnerPrivate { .. } => "owner_private",
}
}
/// Whether this is the private aggregation context for `owner`.
pub fn is_owner_private_for(&self, owner: &Principal) -> bool {
matches!(self, Self::OwnerPrivate { owner: candidate, .. } if candidate == owner)
}
fn resource_context(&self) -> ResourceContext {
match self {
Self::RealmPublic(realm) => ResourceContext::RealmPublic(realm.clone()),
Self::Conversation { realm, channel_id } => ResourceContext::Conversation {
realm: realm.clone(),
channel_id: *channel_id,
},
Self::OwnerPrivate { realm, owner } => ResourceContext::OwnerPrivate {
realm: realm.clone(),
owner: owner.clone(),
},
}
}
pub(crate) fn permits(&self, resource: &ResourceContext) -> bool {
match resource {
ResourceContext::TrustedConfiguration => true,
ResourceContext::RealmPublic(resource_realm) => self.realm() == resource_realm,
ResourceContext::Conversation {
realm: resource_realm,
channel_id: resource_channel,
} => match self {
Self::Conversation { realm, channel_id } => {
realm == resource_realm && channel_id == resource_channel
}
Self::OwnerPrivate { realm, .. } => realm == resource_realm,
Self::RealmPublic(_) => false,
},
ResourceContext::OwnerPrivate {
realm: resource_realm,
owner: resource_owner,
} => matches!(
self,
Self::OwnerPrivate { realm, owner }
if realm == resource_realm && owner == resource_owner
),
}
}
fn stable_hash(&self, hasher: &mut Sha256) {
match self {
Self::RealmPublic(realm) => {
hasher.update(b"realm-public");
realm.stable_hash(hasher);
}
Self::Conversation { realm, channel_id } => {
hasher.update(b"conversation");
realm.stable_hash(hasher);
hasher.update(channel_id.as_bytes());
}
Self::OwnerPrivate { realm, owner } => {
hasher.update(b"owner-private");
realm.stable_hash(hasher);
hash_field(hasher, owner.0.as_bytes());
}
}
}
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) enum ResourceContext {
TrustedConfiguration,
RealmPublic(RealmId),
Conversation { realm: RealmId, channel_id: Uuid },
OwnerPrivate { realm: RealmId, owner: Principal },
}
/// An operation that a worker may request.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
struct Capability(String);
/// The complete set of operations admitted for one execution domain.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CapabilitySet(BTreeSet<Capability>);
impl CapabilitySet {
/// Build a set from stable operation names.
pub fn from_names<I, S>(names: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self(
names
.into_iter()
.map(|name| Capability(name.into()))
.collect(),
)
}
/// Paper: "Broker behavior — Enforcement rules." Compute
/// `C(bot) ∩ C(requester) ∩ C(domain)`.
pub fn effective(bot: &Self, requester: &Self, domain: &Self) -> Self {
let bot_and_requester: BTreeSet<_> = bot.0.intersection(&requester.0).cloned().collect();
Self(bot_and_requester.intersection(&domain.0).cloned().collect())
}
/// Whether this set admits an operation.
pub fn contains(&self, operation: &str) -> bool {
self.0.contains(&Capability(operation.to_string()))
}
fn stable_hash(&self, hasher: &mut Sha256) {
for capability in &self.0 {
hash_field(hasher, capability.0.as_bytes());
}
}
}
/// Capability ceilings used while deriving an invocation's effective set.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CapabilityPolicy {
bot: CapabilitySet,
conversation: CapabilitySet,
}
impl CapabilityPolicy {
/// Construct policy from the bot's full ceiling and the ceiling permitted
/// in shared Buzz conversations.
pub fn new(bot: CapabilitySet, conversation: CapabilitySet) -> Self {
Self { bot, conversation }
}
}
/// The membership or policy version under which state was created.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MembershipEpoch(String);
impl MembershipEpoch {
/// Construct an epoch from a stable, verifier-controlled identifier.
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
/// Return a short identifier suitable for logs.
pub fn fingerprint(&self) -> String {
short_fingerprint(&self.0)
}
}
/// Buzz conversation classification after signed metadata verification.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ConversationKind {
/// Realm-wide public channel. All public channels intentionally share one
/// execution domain.
Public,
/// Invite-only channel or group DM with conversation-specific state.
Restricted,
/// A DM. A two-party owner/bot DM becomes owner-private; group DMs remain
/// conversation-specific.
DirectMessage,
}
/// Verified Buzz facts from which the shared policy derives an execution
/// domain.
///
/// A trusted adapter constructs this only after checking trigger signatures,
/// channel binding, and the relay signature on metadata and membership.
pub struct DomainFacts {
/// Community realm selected by the trusted Buzz connection.
pub realm: RealmId,
/// Channel, DM, or group-DM identifier that triggered the invocation.
pub channel_id: Uuid,
/// Verified conversation classification.
pub kind: ConversationKind,
/// Relay-controlled membership or community policy version.
pub epoch: MembershipEpoch,
/// Complete verified roster. Public derivation does not consume this set.
pub members: BTreeSet<Principal>,
/// Managed Buzz identity whose work the execution domain contains.
pub executing_agent: Principal,
/// Authors whose events are included in this invocation.
pub requesters: BTreeSet<Principal>,
/// Optional relay principal allowed to author trusted workflow events.
pub system_principal: Option<Principal>,
/// Optional human owner of the executing agent.
pub owner: Option<Principal>,
}
/// Domain derivation failed despite the adapter's claim that its facts were
/// already verified.
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
pub enum DerivationError {
/// Every invocation must contain at least one authenticated requester.
#[error("invocation has no authenticated requester")]
EmptyRequesters,
/// Restricted conversations must include the executing agent in their
/// verified roster.
#[error("executing agent is absent from channel membership")]
AgentNotMember,
/// A non-system requester is absent from restricted membership.
#[error("requester is absent from channel membership")]
RequesterNotMember,
/// Removing the executing processor left no authorized recipient.
#[error("restricted conversation has no recipient audience")]
EmptyRestrictedAudience,
/// The derived audience and context violated a domain invariant.
#[error("derived execution domain is inconsistent")]
InvalidDomain,
}
/// Paper: "Execution domains." Derive
/// `D = (Agent, Audience, Context, Epoch, Capabilities)` from verified Buzz
/// facts.
/// This is the mapping shared by local ACP and remote agent harnesses.
pub fn derive_execution_domain(
facts: DomainFacts,
policy: &CapabilityPolicy,
) -> Result<ExecutionDomain, DerivationError> {
if facts.requesters.is_empty() {
return Err(DerivationError::EmptyRequesters);
}
if facts.kind == ConversationKind::Public {
let context = DomainContext::RealmPublic(facts.realm.clone());
let capabilities = effective_capabilities(&context, &facts, policy);
return Ok(ExecutionDomain::public(
facts.executing_agent,
facts.realm,
facts.epoch,
capabilities,
));
}
if !facts.members.contains(&facts.executing_agent) {
return Err(DerivationError::AgentNotMember);
}
if facts.requesters.iter().any(|requester| {
facts.system_principal.as_ref() != Some(requester) && !facts.members.contains(requester)
}) {
return Err(DerivationError::RequesterNotMember);
}
let mut readers = facts.members.clone();
readers.remove(&facts.executing_agent);
if readers.is_empty() {
return Err(DerivationError::EmptyRestrictedAudience);
}
let context = match (&facts.owner, facts.kind) {
(Some(owner), ConversationKind::DirectMessage)
if readers.len() == 1 && readers.contains(owner) =>
{
DomainContext::OwnerPrivate {
realm: facts.realm.clone(),
owner: owner.clone(),
}
}
_ => DomainContext::Conversation {
realm: facts.realm.clone(),
channel_id: facts.channel_id,
},
};
let capabilities = effective_capabilities(&context, &facts, policy);
let audience = ConfidentialityLabel::restricted(facts.realm, readers)
.map_err(|_| DerivationError::EmptyRestrictedAudience)?;
ExecutionDomain::new(
facts.executing_agent,
audience,
context,
facts.epoch,
capabilities,
)
.map_err(|_| DerivationError::InvalidDomain)
}
fn effective_capabilities(
context: &DomainContext,
facts: &DomainFacts,
policy: &CapabilityPolicy,
) -> CapabilitySet {
let requester_is_owner = facts
.owner
.as_ref()
.is_some_and(|owner| facts.requesters.iter().all(|requester| requester == owner));
let requester = if requester_is_owner {
&policy.bot
} else {
&policy.conversation
};
let domain = if matches!(context, DomainContext::OwnerPrivate { .. }) {
&policy.bot
} else {
&policy.conversation
};
CapabilitySet::effective(&policy.bot, requester, domain)
}
/// Opaque routing key for one complete execution domain.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct DomainKey(String);
impl DomainKey {
/// Return a short identifier suitable for logs.
pub fn fingerprint(&self) -> String {
short_fingerprint(&self.0)
}
/// Return the full stable identifier used as a worker-pool routing key.
pub fn as_str(&self) -> &str {
&self.0
}
}
/// `D = (Agent, Audience, Context, Epoch, Capabilities)`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExecutionDomain {
agent: Principal,
pub(crate) audience: ConfidentialityLabel,
pub(crate) context: DomainContext,
epoch: MembershipEpoch,
pub(crate) capabilities: CapabilitySet,
}
impl ExecutionDomain {
/// Construct a domain after the trusted adapter has resolved its inputs.
pub fn new(
agent: Principal,
audience: ConfidentialityLabel,
context: DomainContext,
epoch: MembershipEpoch,
capabilities: CapabilitySet,
) -> Result<Self, DomainError> {
if audience.realm() != context.realm() {
return Err(DomainError::ContextRealmMismatch);
}
match (&audience.readers, &context) {
(ReaderSet::Everyone, DomainContext::RealmPublic(_)) => {}
(ReaderSet::Only(readers), DomainContext::Conversation { .. })
if !readers.is_empty() => {}
(ReaderSet::Only(readers), DomainContext::OwnerPrivate { owner, .. })
if readers.len() == 1 && readers.contains(owner) => {}
_ => return Err(DomainError::AudienceContextMismatch),
}
Ok(Self {
agent,
audience,
context,
epoch,
capabilities,
})
}
/// Construct the realm-wide public domain.
pub fn public(
agent: Principal,
realm: RealmId,
epoch: MembershipEpoch,
capabilities: CapabilitySet,
) -> Self {
Self {
agent,
audience: ConfidentialityLabel::public(realm.clone()),
context: DomainContext::RealmPublic(realm),
epoch,
capabilities,
}
}
/// Construct the exact owner-private domain.
pub fn owner_private(
agent: Principal,
realm: RealmId,
owner: Principal,
epoch: MembershipEpoch,
capabilities: CapabilitySet,
) -> Self {
let readers = BTreeSet::from([owner.clone()]);
Self {
agent,
audience: ConfidentialityLabel {
realm: realm.clone(),
readers: ReaderSet::Only(readers),
},
context: DomainContext::OwnerPrivate { realm, owner },
epoch,
capabilities,
}
}
/// Return the managed Buzz identity whose work this domain contains.
pub fn agent(&self) -> &Principal {
&self.agent
}
/// Return the authorized audience.
pub fn audience(&self) -> &ConfidentialityLabel {
&self.audience
}
/// Return the retained-state context.
pub fn context(&self) -> &DomainContext {
&self.context
}
/// Return the runtime placement required to preserve this domain.
pub fn compartment_profile(&self) -> CompartmentProfile {
if self.audience.is_public() {
CompartmentProfile::SharedPublic
} else {
CompartmentProfile::DomainConfined
}
}
/// Return the effective capability set.
pub fn capabilities(&self) -> &CapabilitySet {
&self.capabilities
}
/// Return a short fingerprint of the membership or policy epoch.
pub fn epoch_fingerprint(&self) -> String {
self.epoch.fingerprint()
}
/// Return the canonical identifier for the complete domain tuple.
pub fn id(&self) -> String {
let mut hasher = Sha256::new();
hasher.update(b"buzz-ifc-domain-v1");
hash_field(&mut hasher, self.agent.0.as_bytes());
self.audience.realm.stable_hash(&mut hasher);
self.audience.readers.stable_hash(&mut hasher);
self.context.stable_hash(&mut hasher);
hash_field(&mut hasher, self.epoch.0.as_bytes());
self.capabilities.stable_hash(&mut hasher);
hex::encode(hasher.finalize())
}
/// Return the opaque worker-pool routing key.
pub fn key(&self) -> DomainKey {
DomainKey(self.id())
}
/// Label information whose provenance is this domain itself.
pub fn resource_label(&self) -> ResourceLabel {
ResourceLabel {
confidentiality: self.audience.clone(),
context: self.context.resource_context(),
}
}
}
/// An execution domain contains inconsistent realms.
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
pub enum DomainError {
/// The audience and retained context belong to different Buzz realms.
#[error("execution-domain audience and context belong to different realms")]
ContextRealmMismatch,
/// Public, conversation, and owner-private contexts require their
/// corresponding audience shape.
#[error("execution-domain audience does not match its context")]
AudienceContextMismatch,
}
/// The confidentiality and context assigned to one input resource.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResourceLabel {
pub(crate) confidentiality: ConfidentialityLabel,
pub(crate) context: ResourceContext,
}
impl ResourceLabel {
/// Label immutable configuration that is public in the supplied realm.
pub fn trusted_configuration(realm: RealmId) -> Self {
Self {
confidentiality: ConfidentialityLabel::public(realm),
context: ResourceContext::TrustedConfiguration,
}
}
/// Label information already scoped to an execution domain.
pub fn domain(domain: &ExecutionDomain) -> Self {
domain.resource_label()
}
/// Label information public to every member of one realm.
pub fn realm_public(realm: RealmId) -> Self {
Self {
confidentiality: ConfidentialityLabel::public(realm.clone()),
context: ResourceContext::RealmPublic(realm),
}
}
/// Label information belonging to one restricted conversation.
pub fn conversation(
realm: RealmId,
channel_id: Uuid,
readers: BTreeSet<Principal>,
) -> Result<Self, LabelError> {
Ok(Self {
confidentiality: ConfidentialityLabel::restricted(realm.clone(), readers)?,
context: ResourceContext::Conversation { realm, channel_id },
})
}
/// Label owner-private information such as personal memory.
pub fn owner_private(realm: RealmId, owner: Principal) -> Self {
let readers = BTreeSet::from([owner.clone()]);
Self {
confidentiality: ConfidentialityLabel {
realm: realm.clone(),
readers: ReaderSet::Only(readers),
},
context: ResourceContext::OwnerPrivate { realm, owner },
}
}
}

View File

@@ -0,0 +1,12 @@
use sha2::{Digest, Sha256};
pub(crate) fn hash_field(hasher: &mut Sha256, value: &[u8]) {
let length = u64::try_from(value.len()).unwrap_or(u64::MAX);
hasher.update(length.to_be_bytes());
hasher.update(value);
}
pub(crate) fn short_fingerprint(value: &str) -> String {
let digest = Sha256::digest(value.as_bytes());
hex::encode(&digest[..6])
}

View File

@@ -0,0 +1,202 @@
use std::collections::BTreeSet;
use nostr::PublicKey;
use serde::Serialize;
use sha2::{Digest, Sha256};
use crate::hash::hash_field;
/// A Buzz principal represented by a validated, normalized Nostr public key.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct Principal(pub(crate) String);
impl Principal {
/// Parse and normalize a hexadecimal Nostr public key.
pub fn from_hex(value: &str) -> Result<Self, PrincipalError> {
let key = PublicKey::from_hex(value).map_err(|_| PrincipalError::InvalidPublicKey)?;
Self::from_public_key(&key)
}
/// Validate and convert a Nostr public key.
///
/// PublicKey can hold any 32-byte value, including values that are not
/// valid x-only secp256k1 points. IFC identities must reject those values
/// before they enter reader sets or domain keys.
pub fn from_public_key(value: &PublicKey) -> Result<Self, PrincipalError> {
value
.xonly()
.map_err(|_| PrincipalError::InvalidPublicKey)?;
Ok(Self(value.to_hex().to_ascii_lowercase()))
}
/// Return the normalized hexadecimal public key.
pub fn as_hex(&self) -> &str {
&self.0
}
}
/// A principal could not be constructed from the supplied key.
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
pub enum PrincipalError {
/// The value is not a valid Nostr public key.
#[error("invalid Nostr public key")]
InvalidPublicKey,
}
/// A confidentiality universe derived from one canonical Buzz relay URL.
///
/// Public data in one community is not public in another, so labels from
/// different realms never flow to one another.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct RealmId(pub(crate) [u8; 32]);
impl RealmId {
/// Derive a realm identifier from the relay URL selected by Buzz.
pub fn from_relay_url(relay_url: &str) -> Self {
Self(Sha256::digest(relay_url.as_bytes()).into())
}
/// Return a short identifier suitable for structured logs.
pub fn fingerprint(&self) -> String {
hex::encode(&self.0[..6])
}
pub(crate) fn stable_hash(&self, hasher: &mut Sha256) {
hasher.update(self.0);
}
}
/// The authorized readers of a value.
///
/// Paper: "Appendix: Security labels as a lattice — Labels and ordering."
/// `Everyone` is the public lattice element. An explicit set becomes more
/// restrictive as principals are removed.
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum ReaderSet {
Everyone,
Only(BTreeSet<Principal>),
}
impl ReaderSet {
/// The IFC ordering is reverse set inclusion: every reader at the
/// destination must already be authorized to read the source.
fn can_flow_to(&self, destination: &Self) -> bool {
match (self, destination) {
(Self::Everyone, _) => true,
(Self::Only(_), Self::Everyone) => false,
(Self::Only(source), Self::Only(destination)) => destination.is_subset(source),
}
}
/// Paper: "Combining information." Inputs are joined by intersecting their
/// authorized reader sets.
pub(crate) fn join(&self, other: &Self) -> Self {
match (self, other) {
(Self::Everyone, value) | (value, Self::Everyone) => value.clone(),
(Self::Only(left), Self::Only(right)) => {
Self::Only(left.intersection(right).cloned().collect())
}
}
}
#[cfg(test)]
pub(crate) fn meet(&self, other: &Self) -> Self {
match (self, other) {
(Self::Everyone, _) | (_, Self::Everyone) => Self::Everyone,
(Self::Only(left), Self::Only(right)) => {
Self::Only(left.union(right).cloned().collect())
}
}
}
fn explicit_count(&self) -> Option<usize> {
match self {
Self::Everyone => None,
Self::Only(readers) => Some(readers.len()),
}
}
pub(crate) fn stable_hash(&self, hasher: &mut Sha256) {
match self {
Self::Everyone => hasher.update(b"everyone"),
Self::Only(readers) => {
hasher.update(b"only");
for reader in readers {
hash_field(hasher, reader.0.as_bytes());
}
}
}
}
}
/// A reader-set label within one Buzz community.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfidentialityLabel {
pub(crate) realm: RealmId,
pub(crate) readers: ReaderSet,
}
impl ConfidentialityLabel {
/// Label data that every member of the realm may read.
pub fn public(realm: RealmId) -> Self {
Self {
realm,
readers: ReaderSet::Everyone,
}
}
/// Label data with an explicit non-empty authorized reader set.
pub fn restricted(realm: RealmId, readers: BTreeSet<Principal>) -> Result<Self, LabelError> {
if readers.is_empty() {
return Err(LabelError::EmptyReaderSet);
}
Ok(Self {
realm,
readers: ReaderSet::Only(readers),
})
}
/// Return the realm in which this label is meaningful.
pub fn realm(&self) -> &RealmId {
&self.realm
}
/// Whether the label permits every member of the realm to read the value.
pub fn is_public(&self) -> bool {
matches!(self.readers, ReaderSet::Everyone)
}
/// Return the explicit number of readers, or `None` for public data.
pub fn reader_count(&self) -> Option<usize> {
self.readers.explicit_count()
}
/// Whether information with this label may flow to `destination`.
pub fn can_flow_to(&self, destination: &Self) -> bool {
self.realm == destination.realm && self.readers.can_flow_to(&destination.readers)
}
/// Combine the influence of two inputs.
pub fn join(&self, other: &Self) -> Result<Self, LabelError> {
if self.realm != other.realm {
return Err(LabelError::CrossRealm);
}
Ok(Self {
realm: self.realm.clone(),
readers: self.readers.join(&other.readers),
})
}
}
/// A confidentiality label violates the reader-set lattice invariants.
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
pub enum LabelError {
/// Restricted information must name at least one authorized reader.
#[error("restricted label has no authorized readers")]
EmptyReaderSet,
/// Labels from different Buzz communities cannot be combined.
#[error("labels belong to different Buzz realms")]
CrossRealm,
}

491
crates/buzz-ifc/src/lib.rs Normal file
View File

@@ -0,0 +1,491 @@
//! Deterministic information-flow policy for Buzz agent execution.
//!
//! This crate contains no relay, ACP, process, or storage code. A trusted Buzz
//! adapter verifies events and membership, supplies [`DomainFacts`], and uses
//! these rules to derive an [`ExecutionDomain`] and decide whether a worker may
//! be reused, read a resource, call an operation, or publish a result. Keeping
//! the policy pure lets local ACP and remote harnesses apply the same rules
//! without sharing an agent implementation.
//!
//! Comments prefixed with `Paper:` identify the matching section of
//! "Practical information-flow for Buzz agents."
mod declassification;
mod domain;
mod hash;
mod label;
mod policy;
pub use declassification::{
DeclassificationGrant, GrantError, GrantSignatureVerifier, PendingGrant, VerifiedGrant,
};
pub use domain::{
derive_execution_domain, CapabilityPolicy, CapabilitySet, CompartmentProfile, ConversationKind,
DerivationError, DomainContext, DomainError, DomainFacts, DomainKey, ExecutionDomain,
MembershipEpoch, ResourceLabel,
};
pub use label::{ConfidentialityLabel, LabelError, Principal, PrincipalError, RealmId};
pub use policy::{ProcessState, RuleDecision, RuleEvaluator};
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use nostr::Keys;
use uuid::Uuid;
use super::*;
use crate::label::ReaderSet;
fn principal(value: u8) -> Principal {
let secret_key = format!("{value:064x}");
let keys = Keys::parse(&secret_key).expect("test secret key");
Principal::from_public_key(&keys.public_key()).expect("test principal")
}
fn readers(values: &[u8]) -> BTreeSet<Principal> {
values.iter().copied().map(principal).collect()
}
fn realm() -> RealmId {
RealmId::from_relay_url("wss://buzz.example")
}
fn label(values: &[u8]) -> ConfidentialityLabel {
ConfidentialityLabel::restricted(realm(), readers(values)).expect("non-empty readers")
}
#[test]
fn principals_must_be_valid_x_only_secp256k1_points() {
for invalid in ["00".repeat(32), "ff".repeat(32), format!("{:064x}", 5)] {
assert_eq!(
Principal::from_hex(&invalid),
Err(PrincipalError::InvalidPublicKey)
);
}
}
fn conversation(values: &[u8], channel_id: Uuid, epoch: &str) -> ExecutionDomain {
ExecutionDomain::new(
principal(9),
label(values),
DomainContext::Conversation {
realm: realm(),
channel_id,
},
MembershipEpoch::new(epoch),
CapabilitySet::from_names(["buzz.read.current"]),
)
.expect("same realm")
}
#[test]
fn reader_sets_obey_lattice_laws() {
let values = [
ReaderSet::Everyone,
ReaderSet::Only(readers(&[1])),
ReaderSet::Only(readers(&[2])),
ReaderSet::Only(readers(&[1, 2])),
];
for a in &values {
assert_eq!(a.join(a), *a);
assert_eq!(a.meet(a), *a);
for b in &values {
assert_eq!(a.join(b), b.join(a));
assert_eq!(a.meet(b), b.meet(a));
assert_eq!(a.join(&a.meet(b)), *a);
assert_eq!(a.meet(&a.join(b)), *a);
for c in &values {
assert_eq!(a.join(&b.join(c)), a.join(b).join(c));
assert_eq!(a.meet(&b.meet(c)), a.meet(b).meet(c));
}
}
}
}
#[test]
fn combining_inputs_intersects_authorized_readers() {
let left = label(&[1, 2]);
let right = label(&[1, 3]);
let combined = left.join(&right).expect("same realm");
assert!(combined.can_flow_to(&label(&[1])));
assert!(!combined.can_flow_to(&label(&[1, 2])));
}
#[test]
fn labels_never_flow_across_realms() {
let first = ConfidentialityLabel::public(RealmId::from_relay_url("wss://one"));
let second = ConfidentialityLabel::public(RealmId::from_relay_url("wss://two"));
assert!(!first.can_flow_to(&second));
assert_eq!(first.join(&second), Err(LabelError::CrossRealm));
}
#[test]
fn read_requires_both_audience_and_context() {
let channel = Uuid::from_u128(1);
let domain = conversation(&[1, 2], channel, "v1");
let wrong_audience = ResourceLabel::conversation(realm(), channel, readers(&[1]))
.expect("non-empty readers");
let wrong_context =
ResourceLabel::conversation(realm(), Uuid::from_u128(2), readers(&[1, 2]))
.expect("non-empty readers");
assert!(!RuleEvaluator::read(&domain, &wrong_audience).allowed());
assert!(!RuleEvaluator::read(&domain, &wrong_context).allowed());
assert!(RuleEvaluator::read(&domain, &domain.resource_label()).allowed());
}
#[test]
fn owner_private_context_aggregates_only_owner_readable_conversations() {
let owner = principal(1);
let domain = ExecutionDomain::owner_private(
principal(9),
realm(),
owner,
MembershipEpoch::new("owner-v1"),
CapabilitySet::default(),
);
let readable = ResourceLabel::conversation(realm(), Uuid::from_u128(1), readers(&[1, 2]))
.expect("non-empty readers");
let unreadable = ResourceLabel::conversation(realm(), Uuid::from_u128(2), readers(&[2, 3]))
.expect("non-empty readers");
assert!(RuleEvaluator::read(&domain, &readable).allowed());
assert!(!RuleEvaluator::read(&domain, &unreadable).allowed());
}
#[test]
fn effective_capabilities_are_the_three_way_intersection() {
let bot = CapabilitySet::from_names(["buzz.read.current", "email.read", "drive.read"]);
let requester = CapabilitySet::from_names(["buzz.read.current", "email.read"]);
let domain = CapabilitySet::from_names(["buzz.read.current", "drive.read"]);
assert_eq!(
CapabilitySet::effective(&bot, &requester, &domain),
CapabilitySet::from_names(["buzz.read.current"])
);
}
#[test]
fn domain_derivation_grants_personal_tools_only_in_owner_private_work() {
let owner = principal(1);
let agent = principal(9);
let policy = CapabilityPolicy::new(
CapabilitySet::from_names(["buzz.read.current", "email.read"]),
CapabilitySet::from_names(["buzz.read.current"]),
);
let owner_dm = derive_execution_domain(
DomainFacts {
realm: realm(),
channel_id: Uuid::from_u128(1),
kind: ConversationKind::DirectMessage,
epoch: MembershipEpoch::new("membership:v1"),
members: BTreeSet::from([agent.clone(), owner.clone()]),
executing_agent: agent.clone(),
requesters: BTreeSet::from([owner.clone()]),
system_principal: None,
owner: Some(owner.clone()),
},
&policy,
)
.expect("owner DM domain");
assert!(owner_dm.context().is_owner_private_for(&owner));
assert!(owner_dm.capabilities().contains("email.read"));
let public = derive_execution_domain(
DomainFacts {
realm: realm(),
channel_id: Uuid::from_u128(2),
kind: ConversationKind::Public,
epoch: MembershipEpoch::new("community:v1"),
members: BTreeSet::new(),
executing_agent: agent,
requesters: BTreeSet::from([owner.clone()]),
system_principal: None,
owner: Some(owner),
},
&policy,
)
.expect("public domain");
assert!(!public.capabilities().contains("email.read"));
}
#[test]
fn restricted_domain_derivation_checks_agent_and_requester_membership() {
let owner = principal(1);
let agent = principal(9);
let outsider = principal(8);
let policy = CapabilityPolicy::new(
CapabilitySet::from_names(["buzz.read.current"]),
CapabilitySet::from_names(["buzz.read.current"]),
);
let facts = |members, requesters| DomainFacts {
realm: realm(),
channel_id: Uuid::from_u128(1),
kind: ConversationKind::Restricted,
epoch: MembershipEpoch::new("membership:v1"),
members,
executing_agent: agent.clone(),
requesters,
system_principal: None,
owner: Some(owner.clone()),
};
assert_eq!(
derive_execution_domain(
facts(
BTreeSet::from([owner.clone()]),
BTreeSet::from([owner.clone()]),
),
&policy,
),
Err(DerivationError::AgentNotMember)
);
assert_eq!(
derive_execution_domain(
facts(
BTreeSet::from([owner.clone(), agent.clone()]),
BTreeSet::from([outsider]),
),
&policy,
),
Err(DerivationError::RequesterNotMember)
);
}
#[test]
fn process_reuse_requires_the_complete_domain() {
let first = conversation(&[1, 2], Uuid::from_u128(1), "v1");
let same = first.clone();
let changed_epoch = conversation(&[1, 2], Uuid::from_u128(1), "v2");
let mut state = ProcessState::default();
assert!(state.enter(&first).allowed());
assert!(state.enter(&same).allowed());
assert!(!state.enter(&changed_epoch).allowed());
assert!(!state.enter(&first).allowed());
state.observe(&first.resource_label());
assert!(!state
.publish(&first, first.audience(), first.context(), &[1; 32], None)
.allowed());
}
#[test]
fn domain_id_has_a_canonical_golden_value() {
let domain = conversation(&[1, 2], Uuid::from_u128(1), "membership:event-1");
assert_eq!(
domain.id(),
"2ada28a5b33888f827a5845fd6e84b37e954a31dd4d0652d2468015d68e9080a"
);
}
#[test]
fn domain_ids_bind_the_managed_agent_identity() {
let first = conversation(&[1, 2], Uuid::from_u128(1), "membership:event-1");
let second = ExecutionDomain::new(
principal(8),
first.audience().clone(),
first.context().clone(),
MembershipEpoch::new("membership:event-1"),
first.capabilities().clone(),
)
.expect("same realm");
assert_ne!(first.id(), second.id());
}
#[test]
fn domain_shape_rejects_a_public_audience_for_a_private_context() {
let result = ExecutionDomain::new(
principal(9),
ConfidentialityLabel::public(realm()),
DomainContext::Conversation {
realm: realm(),
channel_id: Uuid::from_u128(1),
},
MembershipEpoch::new("v1"),
CapabilitySet::default(),
);
assert_eq!(result, Err(DomainError::AudienceContextMismatch));
}
#[test]
fn public_domain_ids_do_not_depend_on_the_triggering_channel() {
let first = ExecutionDomain::public(
principal(9),
realm(),
MembershipEpoch::new("community"),
CapabilitySet::from_names(["buzz.read.current"]),
);
let second = ExecutionDomain::public(
principal(9),
realm(),
MembershipEpoch::new("community"),
CapabilitySet::from_names(["buzz.read.current"]),
);
assert_eq!(first.id(), second.id());
}
#[test]
fn compartment_profile_is_asymmetric() {
let public = ExecutionDomain::public(
principal(9),
realm(),
MembershipEpoch::new("community"),
CapabilitySet::default(),
);
let restricted = conversation(&[1, 2], Uuid::from_u128(1), "membership:v1");
let owner_private = ExecutionDomain::owner_private(
principal(9),
realm(),
principal(1),
MembershipEpoch::new("owner:v1"),
CapabilitySet::default(),
);
assert_eq!(
public.compartment_profile(),
CompartmentProfile::SharedPublic
);
assert_eq!(
restricted.compartment_profile(),
CompartmentProfile::DomainConfined
);
assert_eq!(
owner_private.compartment_profile(),
CompartmentProfile::DomainConfined
);
}
#[test]
fn denied_input_still_taints_an_audit_only_process() {
let domain = conversation(&[1, 2], Uuid::from_u128(1), "v1");
let private = ResourceLabel::owner_private(realm(), principal(1));
assert!(!RuleEvaluator::read(&domain, &private).allowed());
let mut state = ProcessState::default();
state.enter(&domain);
state.observe(&domain.resource_label());
state.observe(&private);
assert!(!state
.publish(&domain, domain.audience(), domain.context(), &[7; 32], None,)
.allowed());
}
#[test]
fn ordinary_publication_stays_in_the_source_context() {
let domain = conversation(&[1, 2], Uuid::from_u128(1), "v1");
let other_context = DomainContext::Conversation {
realm: realm(),
channel_id: Uuid::from_u128(2),
};
let mut state = ProcessState::default();
state.enter(&domain);
state.observe(&domain.resource_label());
assert!(!state
.publish(&domain, domain.audience(), &other_context, &[7; 32], None)
.allowed());
}
#[test]
fn equal_reader_sets_do_not_hide_cross_context_input() {
let domain = conversation(&[1, 2], Uuid::from_u128(1), "v1");
let other = ResourceLabel::conversation(realm(), Uuid::from_u128(2), readers(&[1, 2]))
.expect("non-empty readers");
let mut state = ProcessState::default();
state.enter(&domain);
state.observe(&domain.resource_label());
state.observe(&other);
assert!(!state
.publish(&domain, domain.audience(), domain.context(), &[7; 32], None)
.allowed());
}
struct AlwaysValid;
impl GrantSignatureVerifier for AlwaysValid {
fn verifies(&self, _grant: &DeclassificationGrant<PendingGrant>) -> bool {
true
}
}
#[test]
fn declassification_is_owner_verified_and_exact() {
let owner = principal(1);
let domain = conversation(&[1], Uuid::from_u128(1), "v1");
let destination = ConfidentialityLabel::public(realm());
let destination_context = DomainContext::RealmPublic(realm());
let content = [9; 32];
let mut grant = DeclassificationGrant::pending(
owner.clone(),
domain.id(),
destination.clone(),
destination_context.clone(),
content,
)
.verify(&owner, &AlwaysValid)
.expect("owner-authenticated grant");
let mut state = ProcessState::default();
state.enter(&domain);
state.observe(&domain.resource_label());
assert!(!state
.publish(
&domain,
&destination,
&destination_context,
&[8; 32],
Some(&mut grant),
)
.allowed());
assert!(state
.publish(
&domain,
&destination,
&destination_context,
&content,
Some(&mut grant),
)
.allowed());
assert!(!state
.publish(
&domain,
&destination,
&destination_context,
&content,
Some(&mut grant),
)
.allowed());
state.mark_unknown();
assert!(!state
.publish(
&domain,
&destination,
&destination_context,
&content,
Some(&mut grant),
)
.allowed());
}
#[test]
fn unknown_input_prevents_publication() {
let domain = conversation(&[1, 2], Uuid::from_u128(1), "v1");
let mut state = ProcessState::default();
state.enter(&domain);
state.observe(&domain.resource_label());
state.mark_unknown();
assert!(!state
.publish(&domain, domain.audience(), domain.context(), &[7; 32], None,)
.allowed());
}
}

View File

@@ -0,0 +1,210 @@
use std::collections::BTreeSet;
use serde::Serialize;
use crate::declassification::{DeclassificationGrant, VerifiedGrant};
use crate::domain::{DomainContext, ExecutionDomain, ResourceContext, ResourceLabel};
use crate::label::{ConfidentialityLabel, LabelError};
/// The result of evaluating one IFC rule.
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct RuleDecision {
allowed: bool,
reason: &'static str,
}
impl RuleDecision {
fn allow(reason: &'static str) -> Self {
Self {
allowed: true,
reason,
}
}
fn deny(reason: &'static str) -> Self {
Self {
allowed: false,
reason,
}
}
/// Whether the operation is admitted by policy.
pub fn allowed(&self) -> bool {
self.allowed
}
/// Stable explanation intended for logs and operator diagnostics.
pub fn reason(&self) -> &'static str {
self.reason
}
/// Return `allow` or `deny` for structured logs.
pub fn result(&self) -> &'static str {
if self.allowed {
"allow"
} else {
"deny"
}
}
}
/// Pure IFC rule evaluation.
pub struct RuleEvaluator;
impl RuleEvaluator {
/// Evaluate `read(D, x) ⇔ A(D) ⊆ R(x) ∧ ContextPolicy(D, x)`.
pub fn read(domain: &ExecutionDomain, resource: &ResourceLabel) -> RuleDecision {
if !resource.confidentiality.can_flow_to(&domain.audience) {
return RuleDecision::deny("destination audience includes an unauthorized reader");
}
if !domain.context.permits(&resource.context) {
return RuleDecision::deny("resource belongs to a different context");
}
RuleDecision::allow("audience and context both permit the read")
}
/// Evaluate `call(D, op) ⇔ op ∈ C(D)`.
pub fn call(domain: &ExecutionDomain, operation: &str) -> RuleDecision {
if domain.capabilities.contains(operation) {
RuleDecision::allow("operation is in the effective capability set")
} else {
RuleDecision::deny("operation is absent from the effective capability set")
}
}
/// Require every component of the execution domain to match for reuse.
pub fn reuse(existing: &ExecutionDomain, requested: &ExecutionDomain) -> RuleDecision {
if existing == requested {
RuleDecision::allow("complete execution domain matches")
} else {
RuleDecision::deny("agent process has already entered a different domain")
}
}
}
#[derive(Clone, Debug, Default)]
struct ConfinementState {
label: Option<ConfidentialityLabel>,
contexts: BTreeSet<ResourceContext>,
unknown_input: bool,
cross_realm: bool,
}
impl ConfinementState {
fn observe(&mut self, resource: &ResourceLabel) {
self.contexts.insert(resource.context.clone());
self.label = match self.label.take() {
None => Some(resource.confidentiality.clone()),
Some(existing) => match existing.join(&resource.confidentiality) {
Ok(combined) => Some(combined),
Err(LabelError::CrossRealm) => {
self.cross_realm = true;
Some(existing)
}
Err(LabelError::EmptyReaderSet) => Some(existing),
},
};
}
}
/// Conservative policy state for one actual agent process.
///
/// Paper: "Confinement invariant." The accumulated label and observed contexts
/// cover every input that actually entered the process.
///
/// This state intentionally survives model-session invalidation. A new session
/// does not make the surrounding process forget information it has observed.
#[derive(Clone, Debug, Default)]
pub struct ProcessState {
entered_domains: Vec<ExecutionDomain>,
confinement: ConfinementState,
}
impl ProcessState {
/// Record entry into a domain and decide whether this process is reusable.
pub fn enter(&mut self, requested: &ExecutionDomain) -> RuleDecision {
let decision = match self.entered_domains.as_slice() {
[] => RuleDecision::allow("fresh process has no prior execution domain"),
[existing] => RuleEvaluator::reuse(existing, requested),
_ => RuleDecision::deny("agent process has entered multiple execution domains"),
};
if !self
.entered_domains
.iter()
.any(|domain| domain == requested)
{
self.entered_domains.push(requested.clone());
}
decision
}
/// Record an input that actually entered the process.
pub fn observe(&mut self, resource: &ResourceLabel) {
self.confinement.observe(resource);
}
/// Record input whose provenance could not be established.
pub fn mark_unknown(&mut self) {
self.confinement.unknown_input = true;
}
/// Return how many distinct domains have entered this process.
pub fn entered_domain_count(&self) -> usize {
self.entered_domains.len()
}
/// Paper: "Confinement invariant" and "Declassification." Evaluate
/// publication under the process's accumulated label.
pub fn publish(
&self,
source_domain: &ExecutionDomain,
destination: &ConfidentialityLabel,
destination_context: &DomainContext,
content_digest: &[u8; 32],
grant: Option<&mut DeclassificationGrant<VerifiedGrant>>,
) -> RuleDecision {
if self.entered_domains.len() != 1 || self.entered_domains.first() != Some(source_domain) {
return RuleDecision::deny(
"process state is not confined to the claimed source domain",
);
}
if self.confinement.unknown_input || self.confinement.cross_realm {
return RuleDecision::deny("process state contains unresolved input provenance");
}
if self
.confinement
.contexts
.iter()
.any(|context| !source_domain.context.permits(context))
{
return RuleDecision::deny("process state contains input from another context");
}
if grant.is_some_and(|grant| {
grant.matches(
&source_domain.id(),
destination,
destination_context,
content_digest,
)
}) {
return RuleDecision::allow("exact owner-authorized declassification grant matches");
}
if &source_domain.context != destination_context {
return RuleDecision::deny(
"publishing to a different context requires declassification",
);
}
if !source_domain.audience.can_flow_to(destination) {
return RuleDecision::deny("destination is broader than the execution domain");
}
if self
.confinement
.label
.as_ref()
.is_some_and(|label| label.can_flow_to(destination))
{
return RuleDecision::allow("destination is no broader than accumulated state");
}
RuleDecision::deny("output would widen the accumulated reader set")
}
}