brokerd: answers carry the Mattermost post; Table::contains

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
2026-09-24 04:15:11 -07:00
parent a5c37d8756
commit a00711e379
8 changed files with 172 additions and 27 deletions
+2
View File
@@ -80,6 +80,7 @@ pub fn handle(mut stream: UnixStream, broker: &Broker) {
entry,
Answer::Approved {
by: Some(BY.to_string()),
post: None,
},
now,
);
@@ -96,6 +97,7 @@ pub fn handle(mut stream: UnixStream, broker: &Broker) {
entry,
Answer::Refused {
by: Some(BY.to_string()),
post: None,
reason,
},
now,
+29 -1
View File
@@ -2,7 +2,7 @@
//! gone. In memory only.
use crate::policy::{Ask, Decision};
use proto::{DenyReason, PendingApproval, Timestamp};
use proto::{DecisionRecord, DenyReason, PendingApproval, Timestamp};
use std::collections::BTreeMap;
use std::sync::mpsc::{Receiver, Sender, channel};
use std::sync::{Mutex, MutexGuard};
@@ -14,6 +14,29 @@ pub enum Verdict {
Denied(DenyReason),
}
/// An owner's answer to a pending call.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Answer {
/// `by` is "bxctl" or a Mattermost user id; `post` the Mattermost post that answered.
Approved {
by: Option<String>,
post: Option<String>,
},
Refused {
by: Option<String>,
post: Option<String>,
reason: Option<String>,
},
Expired,
}
/// What an approval leaves: the verdict (run or deny) and the record written.
#[derive(Debug)]
pub struct Answered {
pub verdict: Verdict,
pub outcome: DecisionRecord,
}
#[derive(Debug)]
pub struct Entry {
pub info: PendingApproval,
@@ -52,6 +75,11 @@ impl Table {
receiver
}
/// Is approval `id` still pending? Only a hint: it may be taken the moment after.
pub fn contains(&self, id: u64) -> bool {
self.lock().contains_key(&id)
}
pub fn take(&self, id: u64) -> Option<Entry> {
self.lock().remove(&id)
}
+13 -25
View File
@@ -10,6 +10,7 @@ use proto::{
};
use crate::approvals::Verdict;
pub use crate::approvals::{Answer, Answered};
use crate::audit::{AuditError, Writer};
use crate::grants::GrantSet;
use crate::policy::{Ask, Denial, Label, SessionState};
@@ -82,26 +83,6 @@ enum RecordPayload {
Simple(DenyReason),
}
/// An owner's answer to a pending call.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Answer {
Approved {
by: Option<String>,
},
Refused {
by: Option<String>,
reason: Option<String>,
},
Expired,
}
/// What an approval leaves: the verdict (run or deny) and the record written.
#[derive(Debug)]
pub struct Answered {
pub verdict: Verdict,
pub outcome: DecisionRecord,
}
struct Inner {
audit: Box<dyn AuditSink>,
state: StateStore,
@@ -339,10 +320,17 @@ impl Ledger {
};
// 3. The answer and its fields.
let (answer, by, reason) = match &answer {
Answer::Approved { by } => (ApprovalAnswer::Approved, by.clone(), None),
Answer::Refused { by, reason } => (ApprovalAnswer::Refused, by.clone(), reason.clone()),
Answer::Expired => (ApprovalAnswer::Expired, None, None),
let (answer, by, post, reason) = match &answer {
Answer::Approved { by, post } => {
(ApprovalAnswer::Approved, by.clone(), post.clone(), None)
}
Answer::Refused { by, post, reason } => (
ApprovalAnswer::Refused,
by.clone(),
post.clone(),
reason.clone(),
),
Answer::Expired => (ApprovalAnswer::Expired, None, None, None),
};
// 4. The re-decision (approved only); every other answer is a denial.
@@ -382,7 +370,7 @@ impl Ledger {
decision,
answer,
by,
post: None,
post,
reason,
outcome: outcome.clone(),
grant,