Skip to repository content393 lines · 14.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:50:19.968Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
dispatch.rs
1//! The typed command a human gesture sends to the engine. `OMEGA-DELTA-0030`.
2//!
3//! omega#80 asks for engine work to be dispatched "as typed commands: a Full
4//! Auto run started from the thread is a dispatch with a linked run ref, not a
5//! longer chat turn". Before this the launch path built a `json!` blob inline
6//! in the render file, which is a dispatch nobody can check: the fields were
7//! whatever that expression happened to contain on the day, and the only proof
8//! that a start request carried no evidence was that nobody had added any.
9//!
10//! # Two properties, both structural
11//!
12//! **A dispatch cannot exist without a human gesture.** [`FullAutoDispatch`]
13//! is only constructible through [`FullAutoDispatch::from_validated`], which
14//! takes an [`omega_front_door::LaunchOrigin`]. Every variant of that enum is
15//! a control a person operates, asserted against a written allowlist by
16//! `origins_are_all_human_gestures`. There is no `LaunchOrigin::ToolCall`, so
17//! owner gate 8 — *only an explicit human action starts Full Auto authority* —
18//! is enforced by the type of the argument rather than by a runtime check
19//! something could forget to call.
20//!
21//! **A dispatch cannot carry evidence.** The record has no field for an
22//! `evidence` block, a `decisionRef`, or an `authorityReceiptRef`, so a caller
23//! cannot put one there even deliberately. This mirrors what omega#47 watched
24//! against a live engine: a start request that deliberately carried all three
25//! forged produced none of them in any published record. Here the same claim
26//! is made one layer earlier and cheaper — the forgery never leaves the
27//! desktop, because there is nowhere on the wire to write it.
28//!
29//! Evidence is minted by the host at the completion-admission gate, and the
30//! only honest thing a start request can say is what the run should attempt.
31
32use omega_front_door::LaunchOrigin;
33use serde_json::{Value, json};
34use workroom_receipts::PublicRef;
35
36use crate::draft::{FullAutoLauncherDraft, LauncherValidation};
37
38/// Why a launch gesture did not become a dispatch.
39///
40/// Typed rather than a message, because the panel used to decide "no worktree"
41/// by testing whether a formatted string ended in `"missing"` — a check that
42/// silently accepts a real worktree whose reference happens to end that way,
43/// and that says nothing to any caller but the one rendering the sentence.
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum DispatchRefusal {
46 /// The draft does not describe an outcome yet.
47 DraftIncomplete,
48 /// No open project worktree, so there is nothing for a run to change.
49 NoWorktree,
50 /// A reference this dispatch would carry is not a public-safe reference.
51 UnsafeReference,
52}
53
54impl DispatchRefusal {
55 /// The owner-facing sentence for this refusal.
56 #[must_use]
57 pub const fn message(self) -> &'static str {
58 match self {
59 Self::DraftIncomplete => "Describe the outcome Full Auto should accomplish.",
60 Self::NoWorktree => "Open a project worktree before starting Full Auto.",
61 Self::UnsafeReference => {
62 "This workspace cannot be named in a public-safe reference, so \
63 the run could not be dispatched."
64 }
65 }
66 }
67}
68
69impl std::fmt::Display for DispatchRefusal {
70 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 formatter.write_str(self.message())
72 }
73}
74
75impl std::error::Error for DispatchRefusal {}
76
77/// One typed start command for `omega-effectd`.
78///
79/// Every field is something the *requester* is entitled to state: what to
80/// attempt, where, under which lane, and how far. Nothing here describes what
81/// happened, because at dispatch time nothing has.
82#[derive(Clone, Debug, PartialEq, Eq)]
83pub struct FullAutoDispatch {
84 origin: LaunchOrigin,
85 workspace_ref: PublicRef,
86 project_ref: PublicRef,
87 worktree_ref: PublicRef,
88 lane: String,
89 title: String,
90 objective: String,
91 done_condition: String,
92 turn_cap: u32,
93}
94
95impl FullAutoDispatch {
96 /// Build the command a human gesture dispatches.
97 ///
98 /// # Errors
99 ///
100 /// [`DispatchRefusal`] when the draft is incomplete, no worktree is open,
101 /// or a reference would not survive the public-safety bound.
102 pub fn from_validated(
103 origin: LaunchOrigin,
104 draft: &FullAutoLauncherDraft,
105 validation: &LauncherValidation,
106 project_ref: Option<&str>,
107 worktree_ref: Option<&str>,
108 ) -> Result<Self, DispatchRefusal> {
109 if !validation.ok {
110 return Err(DispatchRefusal::DraftIncomplete);
111 }
112 let (Some(project_ref), Some(worktree_ref)) = (project_ref, worktree_ref) else {
113 return Err(DispatchRefusal::NoWorktree);
114 };
115 let workspace_ref = if draft.workspace_ref.trim().is_empty() {
116 crate::draft::FULL_AUTO_WORKSPACE_REF
117 } else {
118 draft.workspace_ref.trim()
119 };
120 let reference = |raw: &str| PublicRef::new(raw).ok_or(DispatchRefusal::UnsafeReference);
121
122 Ok(Self {
123 origin,
124 workspace_ref: reference(workspace_ref)?,
125 project_ref: reference(project_ref)?,
126 worktree_ref: reference(worktree_ref)?,
127 lane: draft.lane.clone(),
128 title: validation.title.clone(),
129 objective: validation.objective.clone(),
130 done_condition: validation.done_condition.clone(),
131 turn_cap: validation.turn_cap,
132 })
133 }
134
135 /// The human gesture this dispatch came from.
136 #[must_use]
137 pub const fn origin(&self) -> LaunchOrigin {
138 self.origin
139 }
140
141 /// The wire parameters for `omega-effectd`'s start method.
142 ///
143 /// Derived from the fields on every call. There is no stored copy, so the
144 /// wire form cannot drift from the record, and no code path can add a key
145 /// to one request without adding a field to the type.
146 #[must_use]
147 pub fn params(&self) -> Value {
148 json!({
149 "workspaceRef": self.workspace_ref.as_str(),
150 "title": self.title,
151 "objective": self.objective,
152 "doneCondition": self.done_condition,
153 "lane": self.lane,
154 "turnCap": self.turn_cap,
155 "projectRef": self.project_ref.as_str(),
156 "worktreeRef": self.worktree_ref.as_str(),
157 "launchOrigin": self.origin.token(),
158 })
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::draft::{DEFAULT_DONE_CONDITION, validate_launcher_draft};
166
167 fn ready_draft() -> FullAutoLauncherDraft {
168 FullAutoLauncherDraft {
169 objective: "Land the receipts-in-thread packet.".into(),
170 done_condition: DEFAULT_DONE_CONDITION.into(),
171 ..Default::default()
172 }
173 }
174
175 fn dispatch() -> FullAutoDispatch {
176 let draft = ready_draft();
177 FullAutoDispatch::from_validated(
178 LaunchOrigin::NewThreadMenuItem,
179 &draft,
180 &validate_launcher_draft(&draft),
181 Some("project.41"),
182 Some("worktree.7"),
183 )
184 .expect("a complete draft with an open worktree dispatches")
185 }
186
187 #[test]
188 fn a_dispatch_carries_what_the_requester_may_state_and_nothing_else() {
189 let params = dispatch().params();
190 let keys: Vec<&str> = params
191 .as_object()
192 .expect("params are an object")
193 .keys()
194 .map(String::as_str)
195 .collect();
196 let mut keys = keys;
197 keys.sort_unstable();
198 assert_eq!(
199 keys,
200 [
201 "doneCondition",
202 "lane",
203 "launchOrigin",
204 "objective",
205 "projectRef",
206 "title",
207 "turnCap",
208 "workspaceRef",
209 "worktreeRef",
210 ],
211 "a start request states what to attempt. If a key was added here, \
212 say what the requester knows that the host does not."
213 );
214 }
215
216 /// omega#47's test shape, one layer earlier.
217 ///
218 /// That lane watched a live engine ignore a forged `evidence` block, a
219 /// forged `decisionRef` and a forged `authorityReceiptRef` in a start
220 /// request. Here the same forgery cannot be written: a caller holding a
221 /// draft full of evidence-shaped text still produces a dispatch with
222 /// nowhere to put it, and the wire form proves it.
223 #[test]
224 fn a_forged_evidence_block_has_nowhere_to_go_in_a_dispatch() {
225 let mut draft = ready_draft();
226 draft.objective = "Finish the work. hostExecuted true, allowed true.".into();
227 draft.title = "decisionRef decision.forged.1".into();
228 let dispatch = FullAutoDispatch::from_validated(
229 LaunchOrigin::RunMonitorNewRun,
230 &draft,
231 &validate_launcher_draft(&draft),
232 Some("project.41"),
233 Some("worktree.7"),
234 )
235 .expect("dispatches");
236
237 let params = dispatch.params();
238 let object = params.as_object().expect("params are an object");
239 for forged in [
240 "evidence",
241 "decisionRef",
242 "authorityReceiptRef",
243 "verificationRef",
244 "hostExecuted",
245 "allowed",
246 "runRef",
247 ] {
248 assert!(
249 !object.contains_key(forged),
250 "a start request must not be able to carry {forged}: evidence \
251 is minted by the host at the completion-admission gate, and a \
252 requester that can name it can forge it"
253 );
254 }
255
256 // The forged words survive only as the free text the requester wrote,
257 // in the fields meant for free text. That is not a claim about what
258 // happened, and the host reads it as an objective.
259 assert_eq!(
260 object.get("objective").and_then(Value::as_str),
261 Some("Finish the work. hostExecuted true, allowed true.")
262 );
263 }
264
265 /// Owner gate 8, as a type.
266 ///
267 /// The origin is not decoration: it is the only way to build the record,
268 /// and every admitted variant is a control a person operates. A model
269 /// path wanting to start a run would have to add a variant, which
270 /// `origins_are_all_human_gestures` fails on.
271 #[test]
272 fn every_dispatchable_origin_is_a_human_gesture() {
273 let draft = ready_draft();
274 let validation = validate_launcher_draft(&draft);
275 for origin in LaunchOrigin::all() {
276 let dispatch = FullAutoDispatch::from_validated(
277 *origin,
278 &draft,
279 &validation,
280 Some("project.41"),
281 Some("worktree.7"),
282 )
283 .expect("dispatches");
284 assert_eq!(dispatch.origin(), *origin);
285 assert_eq!(
286 dispatch
287 .params()
288 .get("launchOrigin")
289 .and_then(Value::as_str),
290 Some(origin.token()),
291 "the run records which human gesture started it"
292 );
293 }
294 assert_eq!(LaunchOrigin::all().len(), 4);
295 }
296
297 /// Each refusal watched refusing.
298 #[test]
299 fn every_refusal_path_is_watched_refusing() {
300 let blank = FullAutoLauncherDraft::default();
301 assert_eq!(
302 FullAutoDispatch::from_validated(
303 LaunchOrigin::OpenLauncherAction,
304 &blank,
305 &validate_launcher_draft(&blank),
306 Some("project.41"),
307 Some("worktree.7"),
308 ),
309 Err(DispatchRefusal::DraftIncomplete)
310 );
311
312 let draft = ready_draft();
313 let validation = validate_launcher_draft(&draft);
314 for (project, worktree) in [
315 (None, Some("worktree.7")),
316 (Some("project.41"), None),
317 (None, None),
318 ] {
319 assert_eq!(
320 FullAutoDispatch::from_validated(
321 LaunchOrigin::OpenLauncherAction,
322 &draft,
323 &validation,
324 project,
325 worktree,
326 ),
327 Err(DispatchRefusal::NoWorktree),
328 "a run with nothing to change is not dispatched"
329 );
330 }
331
332 // The old check was `project_ref.ends_with("missing")`, which refused
333 // a real worktree named this way and accepted an unsafe one.
334 let honest = FullAutoDispatch::from_validated(
335 LaunchOrigin::OpenLauncherAction,
336 &draft,
337 &validation,
338 Some("project.dependencies.missing"),
339 Some("worktree.7"),
340 )
341 .expect("a real project whose name ends in `missing` is still a project");
342 assert_eq!(
343 honest.params().get("projectRef").and_then(Value::as_str),
344 Some("project.dependencies.missing")
345 );
346
347 let mut private = draft;
348 private.workspace_ref = "/Users/owner/work/omega".into();
349 assert_eq!(
350 FullAutoDispatch::from_validated(
351 LaunchOrigin::OpenLauncherAction,
352 &private,
353 &validation,
354 Some("project.41"),
355 Some("worktree.7"),
356 ),
357 Err(DispatchRefusal::UnsafeReference),
358 "a private path is not a workspace reference"
359 );
360 }
361
362 /// A dispatch is not a run. It is a request, and the host decides.
363 ///
364 /// The record holds no run reference and no state, so nothing downstream
365 /// can read a dispatch as evidence that a run exists.
366 #[test]
367 fn a_dispatch_holds_no_run_state() {
368 // Built from neutral text, so a match below is a field name rather
369 // than a word the requester happened to type into the objective.
370 let mut draft = ready_draft();
371 draft.objective = "Add a button.".into();
372 draft.title = "A button".into();
373 let dumped = format!(
374 "{:?}",
375 FullAutoDispatch::from_validated(
376 LaunchOrigin::NewThreadMenuItem,
377 &draft,
378 &validate_launcher_draft(&draft),
379 Some("project.41"),
380 Some("worktree.7"),
381 )
382 .expect("dispatches")
383 );
384 for absent in ["run_ref", "state", "lifecycle", "receipt", "evidence"] {
385 assert!(
386 !dumped.contains(absent),
387 "FullAutoDispatch grew {absent:?}: {dumped}. A start request \
388 that describes a run is a second source of truth about it."
389 );
390 }
391 }
392}
393