proto: gateway, the four messages between brokerd and gatewayd

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
2026-09-24 04:07:46 -07:00
parent db3f9ed01b
commit ebb78e816d
14 changed files with 301 additions and 1 deletions
+90
View File
@@ -0,0 +1,90 @@
//! The messages on `gateway.sock`, between `brokerd` and `gatewayd`: one connection carries one
//! pending approval to Mattermost and its answer back (M4b spec, section 4).
use serde::{Deserialize, Serialize};
use crate::wire::DenyReason;
use crate::wire::PendingApproval;
/// The most characters a refusal's reason may have.
pub const MAX_REASON: usize = 500;
/// `brokerd` → `gatewayd`, first: the approval to post.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ApprovalRequest {
pub approval: PendingApproval,
}
/// `gatewayd` → `brokerd`: it is posted, in these posts, first part first.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ApprovalPosted {
pub posts: Vec<String>,
}
/// `gatewayd` → `brokerd`: an approver answered, with this post.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ApprovalReply {
pub approval: u64,
pub approved: bool,
pub by: String,
pub post: String,
pub reason: Option<String>,
}
/// `brokerd` → `gatewayd`, last: what came of the answer.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ApprovalOutcome {
pub outcome: GatewayOutcome,
}
// JSON: {"outcome":"denied","reason":"no_grant"} ; the tag sits beside the fields
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "outcome", rename_all = "snake_case", deny_unknown_fields)]
pub enum GatewayOutcome {
/// Approved, and the grants as they are now still allow it.
Runs {},
/// Approved, but denied on the re-check, or the answer could not be recorded.
Denied { reason: DenyReason },
/// The refusal is recorded.
Refused {},
/// It was no longer pending when the answer came.
Gone {},
}
/// A Mattermost id: 26 characters of `a-z0-9`.
pub fn is_mattermost_id(id: &str) -> bool {
// `id.len() == 26` and every byte `b.is_ascii_lowercase() || b.is_ascii_digit()`.
id.len() == 26
&& id
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
}
impl ApprovalReply {
/// `by` and `post` are Mattermost ids and the reason is at most `MAX_REASON` characters; the
/// first problem otherwise.
pub fn check(&self) -> Result<(), String> {
// In this order, each an Err with exactly this text:
// 1. `!is_mattermost_id(&self.by)`: "the answer's `by` is not a Mattermost id".
// 2. `!is_mattermost_id(&self.post)`: "the answer's `post` is not a Mattermost id".
// 3. a reason with more than `MAX_REASON` characters (`r.chars().count()`, not bytes):
// `format!("the reason is over {MAX_REASON} characters")`.
// 4. Ok(()).
if !is_mattermost_id(&self.by) {
return Err("the answer's `by` is not a Mattermost id".to_string());
}
if !is_mattermost_id(&self.post) {
return Err("the answer's `post` is not a Mattermost id".to_string());
}
if let Some(reason) = &self.reason
&& reason.chars().count() > MAX_REASON
{
return Err(format!("the reason is over {MAX_REASON} characters"));
}
Ok(())
}
}
+5
View File
@@ -5,6 +5,7 @@ pub mod chain;
pub mod class;
pub mod escape;
pub mod frame;
pub mod gateway;
pub mod grant;
pub mod hash;
pub mod hosts;
@@ -20,6 +21,10 @@ pub use audit::{
pub use chain::{ChainFailure, ChainReport, ChainVerifier, Location, TornTail};
pub use class::DataClass;
pub use frame::{FrameError, MAX_FRAME, read_frame, write_frame};
pub use gateway::{
ApprovalOutcome, ApprovalPosted, ApprovalReply, ApprovalRequest, GatewayOutcome, MAX_REASON,
is_mattermost_id,
};
pub use grant::{Constraints, Grant, Mode};
pub use hash::{HashError, Sha256, sha256};
pub use ids::{CallId, Epoch, Hash32, SessionId, Timestamp, ValueError};
+5
View File
@@ -1,5 +1,6 @@
use serde::{Deserialize, Serialize};
use crate::gateway::{ApprovalOutcome, ApprovalPosted, ApprovalReply, ApprovalRequest};
use crate::{CallId, DataClass, DecisionRecord, SessionId, Timestamp, Usage};
pub const PROTOCOL_VERSION: u32 = 1;
@@ -35,6 +36,10 @@ pub enum Message {
Ok(Empty),
CheckGrants(Empty),
GrantsReport(GrantsReport),
ApprovalRequest(ApprovalRequest),
ApprovalPosted(ApprovalPosted),
ApprovalReply(ApprovalReply),
ApprovalOutcome(ApprovalOutcome),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]