use std::collections::HashMap;
use polyc_eventlog_model::Event;
use polyc_proto::kinds;
use crate::{
ApprovalFact, ApprovalSignatureStatus, AttributionScope, GrantReplaySignatureStatus,
PreparedSource, attribution_events_with_positions, committed_turn_ids, fold_approval_event,
fold_grant_replay_event, prepare_conversation_core,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApprovalOutcome {
Approved,
Denied,
Unanswered,
}
impl ApprovalOutcome {
pub const ALL: [Self; 3] = [Self::Approved, Self::Denied, Self::Unanswered];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Approved => "approved",
Self::Denied => "denied",
Self::Unanswered => "unanswered",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApprovalHistoryFact {
pub position: u64,
pub turn_id: String,
pub request_id: String,
pub tool_name: String,
pub args_json: String,
pub outcome: ApprovalOutcome,
pub response_reason: String,
pub signature_status: String,
pub routine_grant: bool,
pub tool_descriptor_hash: String,
pub grant_scope: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApprovalDetailFact {
pub position: u64,
pub request_reason: String,
pub request_sandbox_mode: String,
pub signer_public_key: [u8; 32],
pub modified_args_json: String,
pub approved_for_session: bool,
pub caller: String,
pub approver: String,
pub response_sandbox_mode: String,
pub injected_context: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttributionHistoryFact {
pub position: u64,
pub turn_id: String,
pub persona_id: String,
pub role: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttributionProvenanceFact {
pub position: u64,
pub identity_provider: String,
pub identity_scope: String,
pub identity_external_id: String,
pub identity_display_name: String,
pub asserting_edge_id: String,
pub signer_pk_hex: String,
pub signature_hex: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GrantReplayHistoryFact {
pub position: u64,
pub turn_id: String,
pub tool: String,
pub grant_ref: String,
pub covered_capabilities: String,
pub coverage_hash: String,
pub signature_status: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GrantReplaySignerFact {
pub position: u64,
pub signer_pk_hex: String,
pub signature_hex: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ConversationSecurityFacts {
pub approvals: Vec<ApprovalHistoryFact>,
pub approval_details: Vec<ApprovalDetailFact>,
pub attribution: Vec<AttributionHistoryFact>,
pub attribution_provenance: Vec<AttributionProvenanceFact>,
pub grant_replays: Vec<GrantReplayHistoryFact>,
pub grant_replay_signers: Vec<GrantReplaySignerFact>,
pub routine_grants: Vec<RoutineGrantMutationFact>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoutineGrantMutationFact {
pub position: u64,
pub turn_id: String,
pub tool_name: String,
pub tool_descriptor_hash: String,
pub grant_scope: String,
pub approved: bool,
pub response_reason: String,
pub signature_status: String,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ConversationSecurityError {
#[error("the source prefix repeats position {position}")]
RepeatedPosition {
position: u64,
},
#[error("the {kind} record at position {position} did not decode")]
Malformed {
kind: &'static str,
position: u64,
},
}
fn refuse_repeated_positions(events: &[(u64, Event)]) -> Result<(), ConversationSecurityError> {
let mut seen = std::collections::HashSet::with_capacity(events.len());
for (position, _) in events {
if !seen.insert(*position) {
return Err(ConversationSecurityError::RepeatedPosition {
position: *position,
});
}
}
Ok(())
}
pub fn fold_conversation_security(
events: &[(u64, Event)],
trusted_signers: &[Vec<u8>],
partition: &str,
) -> Result<ConversationSecurityFacts, ConversationSecurityError> {
refuse_repeated_positions(events)?;
let mut facts = ConversationSecurityFacts::default();
fold_approvals(events, trusted_signers, &mut facts)?;
fold_attribution(events, &mut facts);
fold_grant_replays(events, trusted_signers, &mut facts);
fold_routine_grants(events, trusted_signers, partition, &mut facts);
Ok(facts)
}
fn fold_routine_grants(
events: &[(u64, Event)],
trusted_signers: &[Vec<u8>],
partition: &str,
facts: &mut ConversationSecurityFacts,
) {
for (position, event) in events {
let (base, turn) = kinds::parse(&event.kind);
if base != kinds::APPROVAL_RESPONSE {
continue;
}
let Some(turn) = turn else {
continue;
};
let Some(ApprovalFact::Response(response)) = fold_approval_event(event, trusted_signers)
else {
continue;
};
if response.routine_grant != Some(true) {
continue;
}
let Some(conversation_id) = response.conversation_id.as_deref() else {
continue;
};
if format!("conv-{conversation_id}") != partition {
continue;
}
facts.routine_grants.push(RoutineGrantMutationFact {
position: *position,
turn_id: turn.to_string(),
tool_name: response.tool_name.unwrap_or_default(),
tool_descriptor_hash: response.tool_descriptor_hash.unwrap_or_default(),
grant_scope: response.grant_scope.unwrap_or_default(),
approved: response.approved,
response_reason: response.response_reason.unwrap_or_default(),
signature_status: signature_status(response.signature_status),
});
}
}
fn fold_grant_replays(
events: &[(u64, Event)],
trusted_signers: &[Vec<u8>],
facts: &mut ConversationSecurityFacts,
) {
for (position, event) in events {
let (base, turn) = kinds::parse(&event.kind);
if base != kinds::GRANT_REPLAY {
continue;
}
let Some(fact) = fold_grant_replay_event(event, trusted_signers) else {
continue;
};
let signature_status = match fact.signature_status {
GrantReplaySignatureStatus::Verified => "verified",
GrantReplaySignatureStatus::Invalid => "invalid",
}
.to_owned();
facts.grant_replays.push(GrantReplayHistoryFact {
position: *position,
turn_id: turn.map(|turn| turn.to_string()).unwrap_or_default(),
tool: fact.tool,
grant_ref: fact.grant_ref,
covered_capabilities: serde_json::to_string(&fact.covered_capabilities)
.unwrap_or_default(),
coverage_hash: fact.coverage_hash,
signature_status,
});
facts.grant_replay_signers.push(GrantReplaySignerFact {
position: *position,
signer_pk_hex: fact
.signer_public_key
.as_deref()
.map(polyc_crypto::hex::lower)
.unwrap_or_default(),
signature_hex: fact.signature_hex.unwrap_or_default(),
});
}
}
fn fold_approvals(
events: &[(u64, Event)],
trusted_signers: &[Vec<u8>],
facts: &mut ConversationSecurityFacts,
) -> Result<(), ConversationSecurityError> {
let committed = committed_turn_ids(events.iter().map(|(_, event)| event));
let mut by_occurrence: HashMap<(String, String), usize> = HashMap::new();
for (position, event) in events {
let (base, turn) = kinds::parse(&event.kind);
if base != kinds::APPROVAL_REQUEST && base != kinds::APPROVAL_RESPONSE {
continue;
}
let Some(turn) = turn.filter(|turn| committed.contains(turn)) else {
continue;
};
let kind = if base == kinds::APPROVAL_REQUEST {
kinds::APPROVAL_REQUEST
} else {
kinds::APPROVAL_RESPONSE
};
let fact = fold_approval_event(event, trusted_signers).ok_or(
ConversationSecurityError::Malformed {
kind,
position: *position,
},
)?;
let turn_id = turn.to_string();
match fact {
ApprovalFact::Request(request) => {
let occurrence = (turn_id.clone(), request.request_id.clone());
let row = ApprovalHistoryFact {
position: *position,
turn_id,
request_id: request.request_id,
tool_name: request.tool_name,
args_json: request.args_json,
outcome: ApprovalOutcome::Unanswered,
response_reason: String::new(),
signature_status: String::new(),
routine_grant: false,
tool_descriptor_hash: String::new(),
grant_scope: String::new(),
};
let detail = ApprovalDetailFact {
position: *position,
request_reason: request.reason,
request_sandbox_mode: request.sandbox_mode,
signer_public_key: [0u8; 32],
modified_args_json: String::new(),
approved_for_session: false,
caller: String::new(),
approver: String::new(),
response_sandbox_mode: String::new(),
injected_context: String::new(),
};
if let Some(&index) = by_occurrence.get(&occurrence) {
facts.approvals[index] = row;
facts.approval_details[index] = detail;
} else {
by_occurrence.insert(occurrence, facts.approvals.len());
facts.approvals.push(row);
facts.approval_details.push(detail);
}
}
ApprovalFact::Response(response) => {
let occurrence = (turn_id, response.request_id.clone());
let Some(&index) = by_occurrence.get(&occurrence) else {
continue;
};
let row = &mut facts.approvals[index];
row.outcome = if response.approved {
ApprovalOutcome::Approved
} else {
ApprovalOutcome::Denied
};
row.response_reason = response.response_reason.unwrap_or_default();
row.signature_status = signature_status(response.signature_status);
row.routine_grant = response.routine_grant.unwrap_or(false);
row.tool_descriptor_hash = response.tool_descriptor_hash.unwrap_or_default();
row.grant_scope = response.grant_scope.unwrap_or_default();
let detail = &mut facts.approval_details[index];
detail.signer_public_key = response
.signer_public_key
.and_then(|key| <[u8; 32]>::try_from(key.as_slice()).ok())
.unwrap_or([0u8; 32]);
detail.modified_args_json = response.modified_args_json.unwrap_or_default();
detail.approved_for_session = response.approved_for_session.unwrap_or(false);
detail.caller = response.caller.unwrap_or_default();
detail.approver = response.approver.unwrap_or_default();
detail.response_sandbox_mode = response.sandbox_mode.unwrap_or_default();
detail.injected_context = response.injected_context.unwrap_or_default();
}
}
}
Ok(())
}
fn fold_attribution(events: &[(u64, Event)], facts: &mut ConversationSecurityFacts) {
let committed = committed_turn_ids(events.iter().map(|(_, event)| event));
for fact in attribution_events_with_positions(
events.iter().map(|(position, event)| (*position, event)),
AttributionScope::Both,
) {
let Some(turn) = fact.turn_id.filter(|turn| committed.contains(turn)) else {
continue;
};
facts.attribution.push(AttributionHistoryFact {
position: fact.position,
turn_id: turn.to_string(),
persona_id: fact.persona_id,
role: fact.role,
});
let identity = fact.identity.unwrap_or_default();
facts
.attribution_provenance
.push(AttributionProvenanceFact {
position: fact.position,
identity_provider: identity.provider,
identity_scope: identity.scope,
identity_external_id: identity.external_id,
identity_display_name: identity.display_name,
asserting_edge_id: fact.asserting_edge_id,
signer_pk_hex: fact.signer_pk_hex,
signature_hex: fact.signature_hex,
});
}
}
fn signature_status(status: ApprovalSignatureStatus) -> String {
match status {
ApprovalSignatureStatus::Verified => "verified",
ApprovalSignatureStatus::Invalid => "invalid",
ApprovalSignatureStatus::LegacyUnverifiable => "legacy_unverifiable",
}
.to_owned()
}
pub fn prepare_and_fold_conversation_security(
events: &mut [(u64, Event)],
partition: &str,
trusted_signers: &[Vec<u8>],
) -> Result<(PreparedSource, ConversationSecurityFacts), ConversationSecurityError> {
let prepared = prepare_conversation_core(events, partition);
let facts = fold_conversation_security(events, trusted_signers, partition)?;
Ok((prepared, facts))
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs, clippy::unwrap_used)]
use polyc_crypto::approval::{self, ApprovalSigner, response_payload, test_util};
use uuid::Uuid;
use super::*;
fn turn() -> Uuid {
Uuid::from_u128(0x5EC0)
}
fn tagged(base: &str) -> String {
kinds::tagged(base, &turn())
}
fn commit_markers(from: u64) -> Vec<(u64, Event)> {
vec![
(from, Event::new(tagged(kinds::TURN_START), Vec::new())),
(
from + 1,
Event::new(tagged(kinds::TURN_COMPLETE), Vec::new()),
),
]
}
fn request(position: u64, request_id: &str, tool: &str) -> (u64, Event) {
(
position,
Event::new(
tagged(kinds::APPROVAL_REQUEST),
approval::request_payload(
request_id,
tool,
"{}",
"default",
"",
&[],
"",
"",
"",
&[],
false,
),
),
)
}
fn response(
position: u64,
request_id: &str,
tool: &str,
approved: bool,
signer: &ApprovalSigner,
) -> (u64, Event) {
let payload = response_payload(
request_id,
tool,
"{}",
"",
approved,
false,
&[],
"caller-1",
"",
"",
"because",
"",
"conv-1",
"nonce-1",
"",
signer,
)
.0;
(
position,
Event::new(tagged(kinds::APPROVAL_RESPONSE), payload),
)
}
fn fold(events: Vec<(u64, Event)>, signer: &ApprovalSigner) -> ConversationSecurityFacts {
fold_conversation_security(&events, &[signer.public_key_bytes()], "conv-1")
.expect("the fold")
}
#[test]
fn every_outcome_appears_in_all() {
for outcome in ApprovalOutcome::ALL {
match outcome {
ApprovalOutcome::Approved
| ApprovalOutcome::Denied
| ApprovalOutcome::Unanswered => {}
}
}
assert_eq!(ApprovalOutcome::ALL.len(), 3);
}
#[test]
fn a_re_prompted_occurrence_yields_one_answered_row() {
let signer = ApprovalSigner::from_seed(7);
let mut events = commit_markers(1);
events.push(request(3, "call-1", "rm"));
events.push(request(4, "call-1", "rm"));
events.push(response(5, "call-1", "rm", true, &signer));
let facts = fold(events, &signer);
assert_eq!(facts.approvals.len(), 1, "one row per occurrence");
assert_eq!(facts.approvals[0].outcome, ApprovalOutcome::Approved);
assert_eq!(
facts.approvals[0].position, 4,
"the row carries the LATEST request's position"
);
assert_eq!(facts.approval_details.len(), 1, "detail rows stay paired");
}
#[test]
fn a_request_after_a_response_reads_unanswered() {
let signer = ApprovalSigner::from_seed(8);
let mut events = commit_markers(1);
events.push(request(3, "call-1", "rm"));
events.push(response(4, "call-1", "rm", true, &signer));
events.push(request(5, "call-1", "rm"));
let facts = fold(events, &signer);
assert_eq!(facts.approvals.len(), 1);
assert_eq!(
facts.approvals[0].outcome,
ApprovalOutcome::Unanswered,
"the latest request supersedes the answered one"
);
assert_eq!(facts.approvals[0].position, 5);
assert!(
facts.approvals[0].response_reason.is_empty(),
"a superseding request clears the earlier response"
);
}
#[test]
fn a_denial_reads_denied_and_keeps_its_reason() {
let signer = ApprovalSigner::from_seed(9);
let mut events = commit_markers(1);
events.push(request(3, "call-1", "rm"));
events.push(response(4, "call-1", "rm", false, &signer));
let facts = fold(events, &signer);
assert_eq!(facts.approvals[0].outcome, ApprovalOutcome::Denied);
assert_eq!(facts.approvals[0].response_reason, "because");
assert_eq!(facts.approvals[0].signature_status, "verified");
}
#[test]
fn an_uncommitted_turn_contributes_no_approval() {
let signer = ApprovalSigner::from_seed(10);
let events = vec![
(1, Event::new(tagged(kinds::TURN_START), Vec::new())),
request(2, "call-1", "rm"),
response(3, "call-1", "rm", true, &signer),
];
let facts = fold(events, &signer);
assert!(
facts.approvals.is_empty(),
"an uncommitted turn is not history yet"
);
}
#[test]
fn a_response_without_its_request_resolves_nothing() {
let signer = ApprovalSigner::from_seed(11);
let mut events = commit_markers(1);
events.push(response(3, "call-ghost", "rm", true, &signer));
let facts = fold(events, &signer);
assert!(
facts.approvals.is_empty(),
"an approver is never inferred from a neighbouring event"
);
}
fn caller_event(position: u64, kind: String, persona_id: &str) -> (u64, Event) {
use buffa::Message as _;
let event = polyc_proto::proto::polychrome::events::v1::AttributionEvent {
persona_id: persona_id.to_owned(),
identity: buffa::MessageField::none(),
role: "initiator".to_owned(),
asserting_edge_id: "edge-1".to_owned(),
signer_pk_hex: "aa".repeat(32),
signature_hex: "bb".repeat(64),
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
(position, Event::new(kind, event.encode_to_vec()))
}
#[test]
fn only_a_committed_turn_contributes_attribution() {
let signer = ApprovalSigner::from_seed(20);
let mut events = commit_markers(1);
events.push(caller_event(3, tagged(kinds::CALLER), "persona-ada"));
let facts = fold(events, &signer);
assert_eq!(facts.attribution.len(), 1, "the committed record publishes");
assert_eq!(facts.attribution[0].persona_id, "persona-ada");
assert_eq!(facts.attribution[0].turn_id, turn().to_string());
assert_eq!(
facts.attribution_provenance.len(),
1,
"its Fleet-only provenance rides alongside it"
);
}
#[test]
fn an_uncommitted_turn_contributes_no_attribution() {
let signer = ApprovalSigner::from_seed(21);
let events = vec![
(1, Event::new(tagged(kinds::TURN_START), Vec::new())),
caller_event(2, tagged(kinds::CALLER), "persona-ada"),
caller_event(3, tagged(kinds::PARTICIPANT), "persona-ida"),
];
let facts = fold(events, &signer);
assert!(
facts.attribution.is_empty(),
"a participant of an uncommitted turn is not history yet"
);
assert!(
facts.attribution_provenance.is_empty(),
"and neither is their raw external identity"
);
}
#[test]
fn an_untagged_attribution_record_is_not_published() {
let signer = ApprovalSigner::from_seed(22);
let mut events = commit_markers(1);
events.push(caller_event(3, kinds::CALLER.to_owned(), "persona-ada"));
let facts = fold(events, &signer);
assert!(
facts.attribution.is_empty(),
"a record naming no turn was unreachable at every scope before"
);
}
#[test]
fn a_repeated_source_position_is_refused() {
let signer = ApprovalSigner::from_seed(12);
let events = vec![request(2, "call-1", "rm"), request(2, "call-2", "rm")];
assert_eq!(
fold_conversation_security(&events, &[signer.public_key_bytes()], "conv-1"),
Err(ConversationSecurityError::RepeatedPosition { position: 2 })
);
}
#[test]
fn an_unanswered_occurrence_publishes_a_zeroed_key() {
let signer = ApprovalSigner::from_seed(13);
let mut events = commit_markers(1);
events.push(request(3, "call-1", "rm"));
let facts = fold(events, &signer);
assert_eq!(facts.approvals[0].outcome, ApprovalOutcome::Unanswered);
assert_eq!(facts.approval_details[0].signer_public_key, [0u8; 32]);
assert_ne!(
signer.public_key_bytes().as_slice(),
[0u8; 32].as_slice(),
"a real key is never the sentinel"
);
}
fn grant_replay_event(
turn_id: &str,
tool: &str,
grant_ref: &str,
covered_capabilities: &[String],
coverage_hash: &str,
signer: &ApprovalSigner,
) -> Event {
let (payload, _sig, _pk) = test_util::grant_replay_payload(
"conv-1",
turn_id,
tool,
grant_ref,
covered_capabilities,
coverage_hash,
signer,
);
Event::new(
kinds::tagged(kinds::GRANT_REPLAY, &Uuid::parse_str(turn_id).unwrap()),
payload,
)
}
#[test]
fn a_trusted_grant_replay_folds_with_its_signer_evidence() {
let signer = ApprovalSigner::from_seed(30);
let turn = Uuid::from_u128(0x6EA7);
let events = vec![(
1,
grant_replay_event(
&turn.to_string(),
"read_file",
"grant-ref-1",
&["fs.read".to_owned()],
"hash-1",
&signer,
),
)];
let facts = fold_conversation_security(&events, &[signer.public_key_bytes()], "conv-1")
.expect("the fold");
assert_eq!(facts.grant_replays.len(), 1);
let row = &facts.grant_replays[0];
assert_eq!(row.turn_id, turn.to_string());
assert_eq!(row.tool, "read_file");
assert_eq!(row.grant_ref, "grant-ref-1");
assert_eq!(row.covered_capabilities, r#"["fs.read"]"#);
assert_eq!(row.coverage_hash, "hash-1");
assert_eq!(row.signature_status, "verified");
assert_eq!(
facts.grant_replay_signers.len(),
1,
"one signer row per history row"
);
let signer_row = &facts.grant_replay_signers[0];
assert_eq!(signer_row.position, row.position);
assert_eq!(
signer_row.signer_pk_hex,
polyc_crypto::hex::lower(&signer.public_key_bytes())
);
assert!(!signer_row.signature_hex.is_empty());
}
#[test]
fn an_untrusted_grant_replay_is_kept_tagged_invalid() {
let trusted = ApprovalSigner::from_seed(31);
let untrusted = ApprovalSigner::from_seed(32);
let turn = Uuid::from_u128(0x6EA8);
let events = vec![(
1,
grant_replay_event(
&turn.to_string(),
"read_file",
"grant-ref-2",
&["fs.read".to_owned()],
"hash-2",
&untrusted,
),
)];
let facts = fold_conversation_security(&events, &[trusted.public_key_bytes()], "conv-1")
.expect("the fold");
assert_eq!(facts.grant_replays.len(), 1, "the row still surfaces");
assert_eq!(facts.grant_replays[0].signature_status, "invalid");
assert_eq!(
facts.grant_replay_signers[0].signer_pk_hex,
polyc_crypto::hex::lower(&untrusted.public_key_bytes()),
"the untrusted signer's own key is still surfaced, for audit"
);
}
#[test]
fn a_structurally_malformed_grant_replay_drops_the_row() {
let event = Event::new(kinds::GRANT_REPLAY.to_owned(), vec![0xFF, 0xFE]);
let facts = fold_conversation_security(&[(1, event)], &[], "conv-1").expect("the fold");
assert!(facts.grant_replays.is_empty());
assert!(facts.grant_replay_signers.is_empty());
}
fn routine_grant_event(
tool: &str,
approved: bool,
tool_descriptor_hash: &str,
grant_scope: &str,
conversation_id: &str,
signer: &ApprovalSigner,
) -> Event {
let (payload, ..) = polyc_crypto::approval::routine_grant_payload(
"call-routine-grant",
tool,
"{}",
"",
approved,
"owner-1",
"",
"default",
"",
&[],
conversation_id,
"nonce-routine-grant",
"",
tool_descriptor_hash,
grant_scope,
signer,
);
Event::new(tagged(kinds::APPROVAL_RESPONSE), payload)
}
#[test]
fn a_routine_grant_folds_with_no_commit_markers_and_no_preceding_request() {
let signer = ApprovalSigner::from_seed(41);
let events = vec![(
1,
routine_grant_event("fs_write", true, "hash-abc", "tool", "fire-1", &signer),
)];
let facts =
fold_conversation_security(&events, &[signer.public_key_bytes()], "conv-fire-1")
.expect("the fold");
assert!(
facts.approvals.is_empty(),
"a requestless response must not appear in the occurrence-folded table"
);
assert_eq!(facts.routine_grants.len(), 1);
let grant = &facts.routine_grants[0];
assert_eq!(grant.position, 1);
assert_eq!(grant.tool_name, "fs_write");
assert_eq!(grant.tool_descriptor_hash, "hash-abc");
assert_eq!(grant.grant_scope, "tool");
assert!(grant.approved);
assert_eq!(grant.signature_status, "verified");
}
#[test]
fn a_routine_grant_revocation_folds_as_a_disapproving_record() {
let signer = ApprovalSigner::from_seed(42);
let events = vec![(
1,
routine_grant_event("fs_write", false, "hash-abc", "tool", "fire-1", &signer),
)];
let facts =
fold_conversation_security(&events, &[signer.public_key_bytes()], "conv-fire-1")
.expect("the fold");
assert_eq!(facts.routine_grants.len(), 1);
assert!(!facts.routine_grants[0].approved);
}
#[test]
fn an_untrusted_routine_grant_is_kept_tagged_invalid() {
let trusted = ApprovalSigner::from_seed(43);
let untrusted = ApprovalSigner::from_seed(44);
let events = vec![(
1,
routine_grant_event("fs_write", true, "hash-abc", "tool", "fire-1", &untrusted),
)];
let facts =
fold_conversation_security(&events, &[trusted.public_key_bytes()], "conv-fire-1")
.expect("the fold");
assert_eq!(facts.routine_grants.len(), 1, "the row still surfaces");
assert_eq!(facts.routine_grants[0].signature_status, "invalid");
}
#[test]
fn a_non_grant_requestless_response_is_still_dropped() {
let signer = ApprovalSigner::from_seed(45);
let events = vec![response(1, "req-1", "shell_exec", true, &signer)];
let facts = fold_conversation_security(&events, &[signer.public_key_bytes()], "conv-1")
.expect("the fold");
assert!(facts.approvals.is_empty());
assert!(facts.routine_grants.is_empty());
}
}