brokerd: the ledger raises a session's state by the stricter label
M5a task 21. The work is Ornith's, from the stopped run; the design model checked it against the task file and committed it after finding that the gate's failure came from its own reference worktree's stale test binaries in the shared target/ (fixed with cargo clean; 944 tests). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ use proto::{ApprovalAnswer, AuditEvent, DataClass, DenyReason, Timestamp, ToolRe
|
||||
|
||||
use super::{Answer, Answered, Call, Grants, Inner, Ledger};
|
||||
use crate::approvals::Verdict;
|
||||
use crate::policy::{Ask, Denial, SessionState};
|
||||
use crate::policy::{Ask, Denial, Label, SessionState};
|
||||
|
||||
impl Ledger {
|
||||
/// Re-decide an approval and record it.
|
||||
@@ -159,10 +159,17 @@ impl Ledger {
|
||||
};
|
||||
|
||||
match &response {
|
||||
// 3. A result raises the state, then is recorded.
|
||||
ToolResponse::Result { .. } => {
|
||||
// 3. A result raises the state, then is recorded. The state follows the stricter of
|
||||
// the grant's label and the result's own: `memoryd` can make a result stricter.
|
||||
ToolResponse::Result {
|
||||
class, untrusted, ..
|
||||
} => {
|
||||
let label = Label {
|
||||
class: call.label.class.max(*class),
|
||||
untrusted: call.label.untrusted || *untrusted,
|
||||
};
|
||||
let raised = match self.state_read(&guard, &call.session) {
|
||||
Ok(state) => match guard.state.raise(&call.session, state, call.label) {
|
||||
Ok(state) => match guard.state.raise(&call.session, state, label) {
|
||||
Ok(raised) => raised,
|
||||
Err(e) => {
|
||||
(self.log)(&format!("brokerd: {e}"));
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
//! The ledger raises a session's state by the stricter of the grant's label and the result's own
|
||||
//! (M5a spec, section 8). `memoryd` can make a `recall` result untrusted; if the session did not
|
||||
//! follow, a clean session could recall a tainted note and then write it into `memory.md`. Do not
|
||||
//! edit.
|
||||
|
||||
#[path = "support/build.rs"]
|
||||
mod build;
|
||||
#[path = "support/rig.rs"]
|
||||
mod rig;
|
||||
#[path = "support/sink.rs"]
|
||||
mod sink;
|
||||
#[path = "support/tmp.rs"]
|
||||
mod tmp;
|
||||
|
||||
use brokerd::ledger::Call;
|
||||
use brokerd::policy::{Label, SessionState};
|
||||
use build::now;
|
||||
use proto::{AuditEvent, CallId, DataClass, SessionId, ToolResponse};
|
||||
use rig::Rig;
|
||||
|
||||
fn label(class: DataClass, untrusted: bool) -> Label {
|
||||
Label { class, untrusted }
|
||||
}
|
||||
|
||||
fn call(label: Label) -> Call {
|
||||
Call {
|
||||
session: SessionId::new("s1").unwrap(),
|
||||
call: CallId(1),
|
||||
decision: 0,
|
||||
label,
|
||||
}
|
||||
}
|
||||
|
||||
fn result(label: Label) -> ToolResponse {
|
||||
ToolResponse::Result {
|
||||
content: "[1] memory-tainted · note".to_string(),
|
||||
class: label.class,
|
||||
untrusted: label.untrusted,
|
||||
truncated: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn state(rig: &Rig) -> SessionState {
|
||||
rig.state().read(&SessionId::new("s1").unwrap()).unwrap()
|
||||
}
|
||||
|
||||
/// The one result record's (class, untrusted, taint_after).
|
||||
fn recorded(rig: &Rig) -> (DataClass, bool, DataClass) {
|
||||
match rig.events().as_slice() {
|
||||
[
|
||||
AuditEvent::Result {
|
||||
class,
|
||||
untrusted,
|
||||
taint_after,
|
||||
..
|
||||
},
|
||||
] => (*class, *untrusted, *taint_after),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_recall_of_a_tainted_note_makes_a_clean_session_untrusted() {
|
||||
let rig = Rig::new("label-recall");
|
||||
let ledger = rig.ledger();
|
||||
// `recall`'s built-in grant labels its results private and trusted; `memoryd` found a
|
||||
// `memory-tainted` hit and said untrusted.
|
||||
let found = label(DataClass::Private, true);
|
||||
let answer = ledger.finish(
|
||||
&call(label(DataClass::Private, false)),
|
||||
result(found),
|
||||
now(),
|
||||
);
|
||||
assert_eq!(answer, result(found), "passed on unchanged");
|
||||
assert_eq!(
|
||||
state(&rig),
|
||||
SessionState {
|
||||
taint: DataClass::Private,
|
||||
untrusted: true
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
recorded(&rig),
|
||||
(DataClass::Private, true, DataClass::Private)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_result_of_a_higher_class_than_its_grant_raises_the_taint() {
|
||||
let rig = Rig::new("label-class");
|
||||
let ledger = rig.ledger();
|
||||
let found = label(DataClass::Secret, false);
|
||||
ledger.finish(&call(label(DataClass::Public, false)), result(found), now());
|
||||
assert_eq!(
|
||||
state(&rig),
|
||||
SessionState {
|
||||
taint: DataClass::Secret,
|
||||
untrusted: false
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
recorded(&rig),
|
||||
(DataClass::Secret, false, DataClass::Secret)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_result_laxer_than_its_grant_still_raises_the_state_by_the_grant() {
|
||||
let rig = Rig::new("label-laxer");
|
||||
let ledger = rig.ledger();
|
||||
ledger.finish(
|
||||
&call(label(DataClass::Secret, true)),
|
||||
result(label(DataClass::Public, false)),
|
||||
now(),
|
||||
);
|
||||
assert_eq!(
|
||||
state(&rig),
|
||||
SessionState {
|
||||
taint: DataClass::Secret,
|
||||
untrusted: true
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user