Skip to main content

macp_modes/mode/
mod.rs

1pub mod decision;
2pub mod handoff;
3pub mod multi_round;
4pub mod passthrough;
5pub mod proposal;
6pub mod quorum;
7pub mod task;
8pub mod util;
9
10use macp_core::error::MacpError;
11use macp_core::session::Session;
12use macp_pb::pb::{Envelope, ModeDescriptor};
13use std::collections::HashMap;
14
15/// The canonical standards-track modes implemented by this runtime.
16pub const STANDARD_MODE_NAMES: &[&str] = &[
17    "macp.mode.decision.v1",
18    "macp.mode.proposal.v1",
19    "macp.mode.task.v1",
20    "macp.mode.handoff.v1",
21    "macp.mode.quorum.v1",
22];
23
24/// Built-in extension modes shipped with this runtime but not yet standards-track.
25pub const EXTENSION_MODE_NAMES: &[&str] = &["ext.multi_round.v1"];
26
27// `ModeResponse` (data) lives in `macp-core` so `Session::apply_mode_response`
28// can consume it without a core->modes cycle. The `Mode` trait (behavior) stays
29// here. Re-exported so `crate::mode::ModeResponse` keeps resolving.
30pub use macp_core::mode::{MessageContext, ModeResponse};
31
32/// Trait that coordination modes implement.
33/// Modes receive immutable session references and return a ModeResponse.
34/// The runtime kernel is responsible for applying the response.
35pub trait Mode: Send + Sync {
36    fn on_session_start(
37        &self,
38        session: &Session,
39        env: &Envelope,
40    ) -> Result<ModeResponse, MacpError>;
41
42    fn on_message(&self, session: &Session, env: &Envelope) -> Result<ModeResponse, MacpError>;
43
44    /// Kernel entry point: `on_message` plus the runtime's
45    /// [`macp_core::mode::MessageContext`] (acceptance clock). Defaulted to plain
46    /// `on_message` so most modes ignore it; modes that need a trustworthy
47    /// time source (Handoff) override this instead of reading the forgeable
48    /// `Envelope.timestamp_unix_ms`. The runtime and replay always call this,
49    /// with the same clock value that the log entry records.
50    fn on_message_at(
51        &self,
52        session: &Session,
53        env: &Envelope,
54        ctx: &macp_core::mode::MessageContext,
55    ) -> Result<ModeResponse, MacpError> {
56        let _ = ctx;
57        self.on_message(session, env)
58    }
59
60    /// The client boundary: validate an envelope that a *client* submitted on
61    /// the live path, before it is dispatched.
62    ///
63    /// Called on live client-submitted envelopes **only** — never on replay,
64    /// and never on a runtime-synthesized envelope. Library kernels MUST call
65    /// it on inbound traffic. Default is `Ok(())`, so a mode with no
66    /// client-boundary rules needs no override.
67    ///
68    /// # Why this is a hook and not a check inside `on_message`
69    ///
70    /// Some envelope shapes are legitimate *as recorded history* but illegal
71    /// *as client submissions*. The motivating case is the handoff mode's
72    /// runtime-synthesized implicit `HandoffAccept` (RFC-MACP-0010 §5.1(3),
73    /// whose prohibition is scoped to submission "via `Send`"): once such an
74    /// entry is in the append-only log, replay must dispatch it through the
75    /// ordinary mode path, so the mode cannot refuse the shape outright. Only
76    /// the live boundary can tell the two apart, because only the live
77    /// boundary knows the envelope came from a client. Keeping the rejection
78    /// here — rather than marking the log entry with a discriminator — is what
79    /// makes the recorded payload itself a trustworthy provenance signal, and
80    /// it keeps a *stale reader* loud: an old binary replaying a newer log
81    /// rejects the entry in its own mode and fails replay visibly, instead of
82    /// silently skipping an entry kind it does not recognise.
83    ///
84    /// # Hazard: this hook is fail-open by construction
85    ///
86    /// Nothing forces a caller to invoke it — a kernel that drives the phases
87    /// by hand and never calls it simply has no client boundary, and still
88    /// compiles. This runtime is the worked example of why that matters:
89    /// `crate::step::validate_message` does call the hook, but the runtime
90    /// does **not** go through `step::validate_message` — `process_message`
91    /// calls [`Mode::authorize_sender`] and [`Mode::on_message_at`] directly so
92    /// it can interpose its durable append between validation and commit. So
93    /// wiring the hook into `step` alone would have left the runtime
94    /// unprotected; the runtime calls it explicitly at its own two live entry
95    /// points (`process_message` and `process_session_start`), which together
96    /// cover every client envelope (`Send` and `StreamSession` both funnel
97    /// into `Runtime::process`), while replay and crash recovery only re-read
98    /// entries that already passed the hook when they were first accepted.
99    /// There is no compile-time forcing function here — only this note.
100    fn validate_client_envelope(&self, session: &Session, env: &Envelope) -> Result<(), MacpError> {
101        let _ = (session, env);
102        Ok(())
103    }
104
105    /// The synthesis seam: given this session and this clock reading, the
106    /// envelope (if any) that MUST enter accepted history *before* the message
107    /// currently being processed.
108    ///
109    /// Default `None` — nothing is ever due, which is the answer for every
110    /// mode but Handoff. The motivating case is RFC-MACP-0010 §5.1(2): once an
111    /// outstanding handoff offer's `implicit_accept_timeout_ms` has elapsed,
112    /// the runtime "MUST append a synthetic `HandoffAccept` envelope to the
113    /// session's accepted history — before evaluating any subsequent message
114    /// against the offer's acceptance state, and in particular before any
115    /// `Commitment` evaluation".
116    ///
117    /// # The kernel contract
118    ///
119    /// A kernel that calls this MUST, holding the session's lock and *before*
120    /// it validates or dispatches the triggering message:
121    ///
122    /// 1. dispatch the returned envelope through [`Mode::on_message_at`] with
123    ///    `accepted_at_ms` equal to the envelope's own `timestamp_unix_ms`,
124    /// 2. append it durably as an ordinary accepted (`Incoming`) history entry,
125    ///    stamping `received_at_ms` with the envelope's own
126    ///    `timestamp_unix_ms` — **not** wall-clock. Replay derives its dispatch
127    ///    clock from `received_at_ms` (`src/replay.rs`), so stamping anything
128    ///    else desynchronizes the live and replayed clocks for this entry and
129    ///    breaks byte-identical rebuild of `mode_state`. Harmless for handoff
130    ///    specifically, whose accept arm is time-blind, but the contract is
131    ///    general and the next mode to use this hook may not be.
132    /// 3. commit the resulting session state and insert the envelope's
133    ///    `message_id` into the dedup set,
134    /// 4. publish it to the session's subscribers,
135    ///
136    /// and only then process the triggering message. Appending without
137    /// dispatching, or dispatching without appending, forks live state from
138    /// what replay will rebuild from the log.
139    ///
140    /// # Never called on replay
141    ///
142    /// The recorded entry *is* the product: replay dispatches it through the
143    /// ordinary message path like any other accepted entry, so the timer stays
144    /// outside the replay boundary while its recorded product is inside — the
145    /// same construction as the runtime-emitted lifecycle envelopes of
146    /// RFC-MACP-0001 §7.5. An implementation must therefore never read a clock
147    /// of its own: every field of the returned envelope, `timestamp_unix_ms`
148    /// included, has to be a pure function of the session state and `now_ms`,
149    /// because it is baked into permanent history and an observation-dependent
150    /// value could never be reproduced.
151    ///
152    /// # Idempotence
153    ///
154    /// Implementations MUST return `None` once the returned envelope has been
155    /// applied to the session: a second emission would append a duplicate
156    /// entry whose deterministic `message_id` already holds a dedup slot.
157    ///
158    /// Like [`Mode::validate_client_envelope`], this hook is fail-open by
159    /// construction — a kernel that never calls it simply never synthesizes,
160    /// and still compiles. There is no compile-time forcing function, only
161    /// this note.
162    fn due_synthetic_envelope(&self, session: &Session, now_ms: i64) -> Option<Envelope> {
163        let _ = (session, now_ms);
164        None
165    }
166
167    /// Authorize the sender for this message. Modes can override to customize
168    /// authorization (e.g., allowing orchestrator bypass for Commitment messages).
169    fn authorize_sender(&self, session: &Session, env: &Envelope) -> Result<(), MacpError> {
170        if !session.participants.is_empty() && !session.participants.contains(&env.sender) {
171            return Err(MacpError::Forbidden);
172        }
173        Ok(())
174    }
175}
176
177pub fn standard_mode_names() -> &'static [&'static str] {
178    STANDARD_MODE_NAMES
179}
180
181pub fn extension_mode_names() -> &'static [&'static str] {
182    EXTENSION_MODE_NAMES
183}
184
185fn schema_map(path: &str) -> HashMap<String, String> {
186    HashMap::from([("protobuf".to_string(), path.to_string())])
187}
188
189pub fn standard_mode_descriptors() -> Vec<ModeDescriptor> {
190    vec![
191        ModeDescriptor {
192            mode: "macp.mode.decision.v1".into(),
193            mode_version: "1.0.0".into(),
194            title: "Decision Mode".into(),
195            description: "Structured decision making with proposals, evaluations, objections, votes, and a terminal Commitment.".into(),
196            determinism_class: "semantic-deterministic".into(),
197            participant_model: "declared".into(),
198            message_types: vec![
199                "SessionStart".into(),
200                "Proposal".into(),
201                "Evaluation".into(),
202                "Objection".into(),
203                "Vote".into(),
204                "Commitment".into(),
205            ],
206            terminal_message_types: vec!["Commitment".into()],
207            schema_uris: schema_map("buf.build/multiagentcoordinationprotocol/macp"),
208        },
209        ModeDescriptor {
210            mode: "macp.mode.proposal.v1".into(),
211            mode_version: "1.0.0".into(),
212            title: "Proposal Mode".into(),
213            description: "Negotiation with proposals, counterproposals, accepts, rejects, withdrawals, and a terminal Commitment.".into(),
214            determinism_class: "semantic-deterministic".into(),
215            participant_model: "peer".into(),
216            message_types: vec![
217                "SessionStart".into(),
218                "Proposal".into(),
219                "CounterProposal".into(),
220                "Accept".into(),
221                "Reject".into(),
222                "Withdraw".into(),
223                "Commitment".into(),
224            ],
225            terminal_message_types: vec!["Commitment".into()],
226            schema_uris: schema_map("buf.build/multiagentcoordinationprotocol/macp"),
227        },
228        ModeDescriptor {
229            mode: "macp.mode.task.v1".into(),
230            mode_version: "1.0.0".into(),
231            title: "Task Mode".into(),
232            description: "One bounded delegated task with assignee responses, progress, completion/failure reports, and a terminal Commitment.".into(),
233            determinism_class: "structural-only".into(),
234            participant_model: "orchestrated".into(),
235            message_types: vec![
236                "SessionStart".into(),
237                "TaskRequest".into(),
238                "TaskAccept".into(),
239                "TaskReject".into(),
240                "TaskUpdate".into(),
241                "TaskComplete".into(),
242                "TaskFail".into(),
243                "Commitment".into(),
244            ],
245            terminal_message_types: vec!["Commitment".into()],
246            schema_uris: schema_map("buf.build/multiagentcoordinationprotocol/macp"),
247        },
248        ModeDescriptor {
249            mode: "macp.mode.handoff.v1".into(),
250            mode_version: "1.0.0".into(),
251            title: "Handoff Mode".into(),
252            description: "Scoped responsibility transfer with handoff offers, context, target responses, and a terminal Commitment.".into(),
253            determinism_class: "context-frozen".into(),
254            participant_model: "delegated".into(),
255            message_types: vec![
256                "SessionStart".into(),
257                "HandoffOffer".into(),
258                "HandoffContext".into(),
259                "HandoffAccept".into(),
260                "HandoffDecline".into(),
261                "Commitment".into(),
262            ],
263            terminal_message_types: vec!["Commitment".into()],
264            schema_uris: schema_map("buf.build/multiagentcoordinationprotocol/macp"),
265        },
266        ModeDescriptor {
267            mode: "macp.mode.quorum.v1".into(),
268            mode_version: "1.0.0".into(),
269            title: "Quorum Mode".into(),
270            description: "Threshold approval with one approval request, participant ballots, and a terminal Commitment.".into(),
271            determinism_class: "semantic-deterministic".into(),
272            participant_model: "quorum".into(),
273            message_types: vec![
274                "SessionStart".into(),
275                "ApprovalRequest".into(),
276                "Approve".into(),
277                "Reject".into(),
278                "Abstain".into(),
279                "Commitment".into(),
280            ],
281            terminal_message_types: vec!["Commitment".into()],
282            schema_uris: schema_map("buf.build/multiagentcoordinationprotocol/macp"),
283        },
284    ]
285}
286
287pub fn extension_mode_descriptors() -> Vec<ModeDescriptor> {
288    vec![ModeDescriptor {
289        mode: "ext.multi_round.v1".into(),
290        mode_version: "1.0.0".into(),
291        title: "Multi-Round Mode".into(),
292        description: "Iterative convergence through multiple contribution rounds until all participants agree, with a terminal Commitment.".into(),
293        determinism_class: "semantic-deterministic".into(),
294        participant_model: "peer".into(),
295        message_types: vec![
296            "SessionStart".into(),
297            "Contribute".into(),
298            "Commitment".into(),
299        ],
300        terminal_message_types: vec!["Commitment".into()],
301        schema_uris: schema_map("buf.build/multiagentcoordinationprotocol/macp"),
302    }]
303}
304
305pub fn all_mode_descriptors() -> Vec<ModeDescriptor> {
306    let mut all = standard_mode_descriptors();
307    all.extend(extension_mode_descriptors());
308    all
309}