bxcore, brokerd, loopd: an invalid-arguments denial says why
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -12,6 +12,8 @@ use std::fmt;
|
||||
pub const MAX_PATH: usize = 4096;
|
||||
/// Maximum length, in bytes, of a URL.
|
||||
pub const MAX_URL: usize = 2048;
|
||||
/// Maximum length, in characters, of the detail an invalid call is denied with.
|
||||
pub const MAX_DETAIL: usize = 300;
|
||||
|
||||
/// The tools the broker can dispatch to: four that run in a container, and the two memory
|
||||
/// tools that `memoryd` runs (M5a spec, section 8).
|
||||
@@ -190,6 +192,25 @@ impl fmt::Display for ArgsError {
|
||||
}
|
||||
}
|
||||
|
||||
impl ArgsError {
|
||||
/// The error as the model is shown it: the `Display` text on one line, each control character
|
||||
/// and each Unicode line or paragraph separator replaced by a space, cut to `MAX_DETAIL`
|
||||
/// characters. The text holds the model's own arguments, which may be long or hold newlines.
|
||||
pub fn detail(&self) -> String {
|
||||
self.to_string()
|
||||
.chars()
|
||||
.map(|c| {
|
||||
if c.is_control() || c == '\u{2028}' || c == '\u{2029}' {
|
||||
' '
|
||||
} else {
|
||||
c
|
||||
}
|
||||
})
|
||||
.take(MAX_DETAIL)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ArgsError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
None
|
||||
|
||||
@@ -251,7 +251,10 @@ pub fn handle(mut stream: UnixStream, broker: &Broker) {
|
||||
let now = Timestamp::now();
|
||||
let grants = broker.grants();
|
||||
let answer = match broker.ledger().decide(request, &grants, now) {
|
||||
Decided::Denied(reason) => Some(ToolResponse::Denied { reason }), // 3a. denied
|
||||
Decided::Denied(denial) => Some(ToolResponse::Denied {
|
||||
reason: denial.reason,
|
||||
detail: denial.detail,
|
||||
}), // 3a. denied
|
||||
Decided::Allowed { decision, seq } => Some(run(broker, decision, seq)), // 3b. allowed
|
||||
Decided::Ask { ask, seq, state } => {
|
||||
pending(&mut stream, broker, id, ask, seq, state, now) // 3c. ask
|
||||
@@ -334,7 +337,10 @@ fn pending(
|
||||
// 5. Answer from the verdict.
|
||||
match outcome {
|
||||
Ok(verdict) => match verdict {
|
||||
Verdict::Denied(reason) => Some(ToolResponse::Denied { reason }),
|
||||
Verdict::Denied(reason) => Some(ToolResponse::Denied {
|
||||
reason,
|
||||
detail: None,
|
||||
}),
|
||||
Verdict::Run(decision) => {
|
||||
let decision = *decision;
|
||||
if !alive(stream) {
|
||||
@@ -350,6 +356,9 @@ fn pending(
|
||||
Some(run(broker, decision, seq))
|
||||
}
|
||||
},
|
||||
Err(reason) => Some(ToolResponse::Denied { reason }),
|
||||
Err(reason) => Some(ToolResponse::Denied {
|
||||
reason,
|
||||
detail: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,8 @@ impl Call {
|
||||
}
|
||||
}
|
||||
|
||||
/// How a call was decided.
|
||||
/// How a call was decided. A denial carries the policy's `Denial`, so an invalid call keeps its
|
||||
/// detail on the way to the answer.
|
||||
#[derive(Debug)]
|
||||
pub enum Decided {
|
||||
Allowed {
|
||||
@@ -74,7 +75,7 @@ pub enum Decided {
|
||||
seq: u64,
|
||||
state: SessionState,
|
||||
},
|
||||
Denied(DenyReason),
|
||||
Denied(Denial),
|
||||
}
|
||||
|
||||
/// The value a decided call carries past the record, or the denial reason when there is none.
|
||||
@@ -210,7 +211,7 @@ impl Ledger {
|
||||
// 1. Lock fails → denied, nothing written.
|
||||
let mut guard = match self.lock() {
|
||||
Some(guard) => guard,
|
||||
None => return Decided::Denied(DenyReason::AuditUnavailable),
|
||||
None => return Decided::Denied(Denial::new(DenyReason::AuditUnavailable)),
|
||||
};
|
||||
|
||||
// 2. Read the state, keeping the Result. The recorded state is "unknown" when unreadable.
|
||||
@@ -281,16 +282,16 @@ impl Ledger {
|
||||
};
|
||||
match self.append(&mut guard, now, event) {
|
||||
Ok(seq) => match payload {
|
||||
RecordPayload::Simple(reason) => Decided::Denied(reason),
|
||||
RecordPayload::Simple(reason) => Decided::Denied(Denial::new(reason)),
|
||||
RecordPayload::Allowed(decision) => Decided::Allowed { decision, seq },
|
||||
RecordPayload::Ask(ask) => Decided::Ask {
|
||||
ask,
|
||||
seq,
|
||||
state: recorded,
|
||||
},
|
||||
RecordPayload::Denial(denial) => Decided::Denied(denial.reason),
|
||||
RecordPayload::Denial(denial) => Decided::Denied(denial),
|
||||
},
|
||||
Err(()) => Decided::Denied(DenyReason::AuditUnavailable),
|
||||
Err(()) => Decided::Denied(Denial::new(DenyReason::AuditUnavailable)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,9 @@ pub struct Denial {
|
||||
pub grant: Option<String>,
|
||||
/// `Some` only for `DeniedByGrant`: the deny grant's file hash.
|
||||
pub grant_sha256: Option<Hash32>,
|
||||
/// `Some` only for `InvalidArguments`: why, from `ArgsError::detail`. It goes to the model in
|
||||
/// the `ToolResponse`, never into the audit record, which holds the arguments already.
|
||||
pub detail: Option<String>,
|
||||
}
|
||||
|
||||
impl Denial {
|
||||
@@ -128,6 +131,7 @@ impl Denial {
|
||||
reason,
|
||||
grant: None,
|
||||
grant_sha256: None,
|
||||
detail: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,9 +248,16 @@ pub fn decide(
|
||||
let Some(tool) = ToolName::parse(&request.tool) else {
|
||||
return Outcome::Denied(Denial::new(DenyReason::NoGrant));
|
||||
};
|
||||
// 2. Malformed arguments are refused before matching, whatever the grants say.
|
||||
let Ok(args) = args::parse(tool, &request.arguments) else {
|
||||
return Outcome::Denied(Denial::new(DenyReason::InvalidArguments));
|
||||
// 2. Malformed arguments are refused before matching, whatever the grants say, and the
|
||||
// model is told why, so that it can correct the call.
|
||||
let args = match args::parse(tool, &request.arguments) {
|
||||
Ok(args) => args,
|
||||
Err(error) => {
|
||||
return Outcome::Denied(Denial {
|
||||
detail: Some(error.detail()),
|
||||
..Denial::new(DenyReason::InvalidArguments)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let MatchResult {
|
||||
@@ -273,6 +284,7 @@ pub fn decide(
|
||||
reason: DenyReason::DeniedByGrant,
|
||||
grant: Some(winner.id.clone()),
|
||||
grant_sha256: Some(winner.grant_sha256),
|
||||
detail: None,
|
||||
}),
|
||||
Mode::Ask => Outcome::Ask(Ask {
|
||||
request,
|
||||
@@ -316,6 +328,7 @@ pub fn redecide(
|
||||
reason: DenyReason::DeniedByGrant,
|
||||
grant: Some(winner.id.clone()),
|
||||
grant_sha256: Some(winner.grant_sha256),
|
||||
detail: None,
|
||||
}),
|
||||
Mode::Ask | Mode::Auto => Ok(Decision {
|
||||
request: ask.request().clone(),
|
||||
|
||||
Reference in New Issue
Block a user