Skip to main content

meerkat_contracts/wire/
mob.rs

1//! Mob RPC wire contracts.
2
3use super::connection::WireAuthBindingRef;
4use super::session::WireContentInput;
5use super::supervisor_bridge::BridgeBootstrapToken;
6use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
7use meerkat_core::OutputSchema;
8use meerkat_core::{
9    HandlingMode,
10    types::{RenderClass, RenderMetadata, RenderSalience},
11};
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use std::collections::BTreeMap;
15
16use meerkat_core::{SurfaceMetadata, SurfaceMetadataError};
17
18#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
19#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
20#[serde(rename_all = "snake_case")]
21pub enum WireMobBackendKind {
22    #[default]
23    Session,
24    External,
25}
26
27/// Runtime binding for spawn requests.
28///
29/// First step toward identity-first mobs. Carries backend-specific binding
30/// details at spawn time. `External` requires typed process identity; callers
31/// do not supply raw comms peer IDs.
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
34#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
35pub enum WireRuntimeBinding {
36    Session,
37    External {
38        address: String,
39        #[serde(default, skip_serializing_if = "Option::is_none")]
40        bootstrap_token: Option<BridgeBootstrapToken>,
41        /// Typed Ed25519 signing identity for the external process. The
42        /// canonical comms `PeerId` is derived from this key after the wire
43        /// boundary, so callers cannot spoof an unrelated raw peer id.
44        identity: WireTrustedPeerIdentity,
45    },
46}
47
48#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
49#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
50#[serde(rename_all = "snake_case")]
51pub enum WireMobRuntimeMode {
52    #[default]
53    AutonomousHost,
54    TurnDriven,
55}
56
57/// How a mob member should be launched by `mob/spawn`.
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
59#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
60#[serde(tag = "mode", rename_all = "snake_case")]
61pub enum WireMemberLaunchMode {
62    Fresh,
63    Resume {
64        #[serde(alias = "session_id")]
65        bridge_session_id: String,
66    },
67    Fork {
68        source_member_id: String,
69        #[serde(default)]
70        fork_context: WireForkContext,
71    },
72}
73
74/// Conversation history scope used when forking a mob member.
75#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
76#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
77#[serde(tag = "type", rename_all = "snake_case")]
78pub enum WireForkContext {
79    #[default]
80    FullHistory,
81    LastMessages {
82        count: u32,
83    },
84}
85
86/// Budget split policy for a spawned mob member.
87#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
88#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
89#[serde(tag = "type", content = "value", rename_all = "snake_case")]
90pub enum WireBudgetSplitPolicy {
91    #[default]
92    Equal,
93    Proportional,
94    Remaining,
95    Fixed(u64),
96}
97
98/// Tool access policy for a spawned mob member.
99#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
100#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
101#[serde(tag = "type", content = "value", rename_all = "snake_case")]
102pub enum WireToolAccessPolicy {
103    #[default]
104    Inherit,
105    AllowList(Vec<String>),
106    DenyList(Vec<String>),
107}
108
109/// Pre-resolved tool filter inherited by a spawned mob member.
110#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
111#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
112pub enum WireToolFilter {
113    #[default]
114    All,
115    Allow(Vec<String>),
116    Deny(Vec<String>),
117}
118
119/// Tool configuration embedded in a wire mob profile override.
120#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
121#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
122#[serde(deny_unknown_fields)]
123pub struct WireMobToolConfig {
124    #[serde(default)]
125    pub builtins: bool,
126    #[serde(default)]
127    pub shell: bool,
128    #[serde(default)]
129    pub comms: bool,
130    #[serde(default)]
131    pub memory: bool,
132    #[serde(default)]
133    pub mob: bool,
134    #[serde(default)]
135    pub schedule: bool,
136    #[serde(default)]
137    pub image_generation: bool,
138    #[serde(default)]
139    pub mcp: Vec<String>,
140}
141
142/// Profile override for `mob/spawn`.
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
144#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
145#[serde(deny_unknown_fields)]
146pub struct WireMobProfile {
147    pub model: String,
148    #[serde(default)]
149    pub skills: Vec<String>,
150    #[serde(default)]
151    pub tools: WireMobToolConfig,
152    #[serde(default)]
153    pub peer_description: String,
154    #[serde(default)]
155    pub external_addressable: bool,
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub backend: Option<WireMobBackendKind>,
158    #[serde(default)]
159    pub runtime_mode: WireMobRuntimeMode,
160    #[serde(default, skip_serializing_if = "Option::is_none")]
161    pub max_inline_peer_notifications: Option<i32>,
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub output_schema: Option<Value>,
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    pub provider_params: Option<Value>,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
169#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
170#[serde(deny_unknown_fields)]
171pub struct MobOrchestratorInput {
172    pub profile: String,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
176#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
177#[serde(tag = "source", rename_all = "snake_case")]
178pub enum MobSkillSourceInput {
179    Inline { content: String },
180    Path { path: String },
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
184#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
185#[serde(deny_unknown_fields)]
186pub struct MobRoleWiringRuleInput {
187    pub a: String,
188    pub b: String,
189}
190
191#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
192#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
193#[serde(deny_unknown_fields)]
194pub struct MobWiringRulesInput {
195    #[serde(default)]
196    pub auto_wire_orchestrator: bool,
197    #[serde(default, skip_serializing_if = "Vec::is_empty")]
198    pub role_wiring: Vec<MobRoleWiringRuleInput>,
199}
200
201#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
202#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
203#[serde(deny_unknown_fields)]
204pub struct MobToolConfigInput {
205    #[serde(default)]
206    pub builtins: bool,
207    #[serde(default)]
208    pub shell: bool,
209    #[serde(default)]
210    pub comms: bool,
211    #[serde(default)]
212    pub memory: bool,
213    #[serde(default)]
214    pub mob: bool,
215    #[serde(default)]
216    pub schedule: bool,
217    #[serde(default)]
218    pub image_generation: bool,
219    #[serde(default, skip_serializing_if = "Vec::is_empty")]
220    pub mcp: Vec<String>,
221}
222
223/// Profile binding input: either an inline profile or a realm profile reference.
224///
225/// Not `Eq`: `Inline(MobProfileInput)` transitively carries float provider
226/// params (`temperature`, `top_p`) so `Eq` cannot be derived without
227/// losing fidelity.
228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
229#[allow(clippy::large_enum_variant)]
230#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
231#[serde(untagged)]
232pub enum MobProfileBindingInput {
233    /// Reference to a realm-scoped profile.
234    RealmRef {
235        /// Name of the realm profile.
236        realm_profile: String,
237    },
238    /// Inline profile definition.
239    Inline(MobProfileInput),
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
243#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
244#[serde(deny_unknown_fields)]
245pub struct MobProfileInput {
246    pub model: String,
247    #[serde(default, skip_serializing_if = "Vec::is_empty")]
248    pub skills: Vec<String>,
249    #[serde(default)]
250    pub tools: MobToolConfigInput,
251    #[serde(default, skip_serializing_if = "String::is_empty")]
252    pub peer_description: String,
253    #[serde(default)]
254    pub external_addressable: bool,
255    #[serde(default, skip_serializing_if = "Option::is_none")]
256    pub backend: Option<WireMobBackendKind>,
257    #[serde(default)]
258    pub runtime_mode: WireMobRuntimeMode,
259    #[serde(default, skip_serializing_if = "Option::is_none")]
260    pub max_inline_peer_notifications: Option<i32>,
261    #[serde(default, skip_serializing_if = "Option::is_none")]
262    pub output_schema: Option<OutputSchema>,
263    /// Non-`Eq` field: `WireProviderParamsOverride` contains float scalars
264    /// (`temperature`, `top_p`) so the struct can't derive `Eq` without
265    /// losing fidelity.
266    #[serde(default, skip_serializing_if = "Option::is_none")]
267    pub provider_params: Option<crate::wire::runtime::WireProviderParamsOverride>,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
271#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
272#[serde(deny_unknown_fields)]
273pub struct MobExternalBackendConfigInput {
274    pub address_base: String,
275}
276
277#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
278#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
279#[serde(deny_unknown_fields)]
280pub struct MobBackendConfigInput {
281    #[serde(default)]
282    pub default: WireMobBackendKind,
283    #[serde(default, skip_serializing_if = "Option::is_none")]
284    pub external: Option<MobExternalBackendConfigInput>,
285}
286
287#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
288#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
289#[serde(rename_all = "snake_case")]
290pub enum MobDispatchModeInput {
291    #[default]
292    FanOut,
293    OneToOne,
294    FanIn,
295}
296
297#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
298#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
299#[serde(tag = "type", rename_all = "snake_case")]
300pub enum MobCollectionPolicyInput {
301    #[default]
302    All,
303    Any,
304    Quorum {
305        n: u8,
306    },
307}
308
309#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
310#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
311#[serde(rename_all = "snake_case")]
312pub enum MobDependencyModeInput {
313    #[default]
314    All,
315    Any,
316}
317
318#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
319#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
320#[serde(rename_all = "snake_case")]
321pub enum MobStepOutputFormatInput {
322    #[default]
323    Json,
324    Text,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
328#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
329#[serde(tag = "op", rename_all = "snake_case")]
330pub enum MobConditionExprInput {
331    Eq { path: String, value: Value },
332    In { path: String, values: Vec<Value> },
333    Gt { path: String, value: Value },
334    Lt { path: String, value: Value },
335    And { exprs: Vec<MobConditionExprInput> },
336    Or { exprs: Vec<MobConditionExprInput> },
337    Not { expr: Box<MobConditionExprInput> },
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
341#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
342#[serde(deny_unknown_fields)]
343pub struct MobFrameSpecInput {
344    pub nodes: BTreeMap<String, MobFlowNodeInput>,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
348#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
349#[serde(tag = "kind", rename_all = "snake_case")]
350pub enum MobFlowNodeInput {
351    Step(MobFrameStepInput),
352    RepeatUntil(MobRepeatUntilInput),
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
356#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
357#[serde(deny_unknown_fields)]
358pub struct MobFrameStepInput {
359    pub step_id: String,
360    #[serde(default, skip_serializing_if = "Vec::is_empty")]
361    pub depends_on: Vec<String>,
362    #[serde(default)]
363    pub depends_on_mode: MobDependencyModeInput,
364    #[serde(default, skip_serializing_if = "Option::is_none")]
365    pub branch: Option<String>,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
369#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
370#[serde(deny_unknown_fields)]
371pub struct MobRepeatUntilInput {
372    pub loop_id: String,
373    #[serde(default, skip_serializing_if = "Vec::is_empty")]
374    pub depends_on: Vec<String>,
375    #[serde(default)]
376    pub depends_on_mode: MobDependencyModeInput,
377    pub body: MobFrameSpecInput,
378    pub until: MobConditionExprInput,
379    pub max_iterations: u32,
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
383#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
384#[serde(deny_unknown_fields)]
385pub struct MobFlowStepInput {
386    pub role: String,
387    pub message: WireContentInput,
388    #[serde(default, skip_serializing_if = "Vec::is_empty")]
389    pub depends_on: Vec<String>,
390    #[serde(default)]
391    pub dispatch_mode: MobDispatchModeInput,
392    #[serde(default)]
393    pub collection_policy: MobCollectionPolicyInput,
394    #[serde(default, skip_serializing_if = "Option::is_none")]
395    pub condition: Option<MobConditionExprInput>,
396    #[serde(default, skip_serializing_if = "Option::is_none")]
397    pub timeout_ms: Option<u64>,
398    #[serde(default, skip_serializing_if = "Option::is_none")]
399    pub expected_schema_ref: Option<String>,
400    #[serde(default, skip_serializing_if = "Option::is_none")]
401    pub branch: Option<String>,
402    #[serde(default)]
403    pub depends_on_mode: MobDependencyModeInput,
404    #[serde(default, skip_serializing_if = "Option::is_none")]
405    pub allowed_tools: Option<Vec<String>>,
406    #[serde(default, skip_serializing_if = "Option::is_none")]
407    pub blocked_tools: Option<Vec<String>>,
408    #[serde(default)]
409    pub output_format: MobStepOutputFormatInput,
410}
411
412#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
413#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
414#[serde(deny_unknown_fields)]
415pub struct MobFlowSpecInput {
416    #[serde(default, skip_serializing_if = "Option::is_none")]
417    pub description: Option<String>,
418    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
419    pub steps: BTreeMap<String, MobFlowStepInput>,
420    #[serde(default, skip_serializing_if = "Option::is_none")]
421    pub root: Option<MobFrameSpecInput>,
422}
423
424#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
425#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
426#[serde(rename_all = "snake_case")]
427pub enum MobPolicyModeInput {
428    #[default]
429    Advisory,
430    Strict,
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
434#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
435#[serde(deny_unknown_fields)]
436pub struct MobTopologyRuleInput {
437    pub from_role: String,
438    pub to_role: String,
439    pub allowed: bool,
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
443#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
444#[serde(deny_unknown_fields)]
445pub struct MobTopologySpecInput {
446    pub mode: MobPolicyModeInput,
447    pub rules: Vec<MobTopologyRuleInput>,
448}
449
450#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
451#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
452#[serde(deny_unknown_fields)]
453pub struct MobSupervisorSpecInput {
454    pub role: String,
455    pub escalation_threshold: u32,
456}
457
458#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
459#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
460#[serde(deny_unknown_fields)]
461pub struct MobLimitsSpecInput {
462    #[serde(default, skip_serializing_if = "Option::is_none")]
463    pub max_flow_duration_ms: Option<u64>,
464    #[serde(default, skip_serializing_if = "Option::is_none")]
465    pub max_step_retries: Option<u32>,
466    #[serde(default, skip_serializing_if = "Option::is_none")]
467    pub max_orphaned_turns: Option<u32>,
468    #[serde(default, skip_serializing_if = "Option::is_none")]
469    pub cancel_grace_timeout_ms: Option<u64>,
470    #[serde(default, skip_serializing_if = "Option::is_none")]
471    pub max_active_nodes: Option<u64>,
472    #[serde(default, skip_serializing_if = "Option::is_none")]
473    pub max_active_frames: Option<u64>,
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub max_frame_depth: Option<u64>,
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
479#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
480#[serde(tag = "mode", rename_all = "snake_case")]
481pub enum MobSpawnPolicyInput {
482    None,
483    Auto {
484        profile_map: BTreeMap<String, String>,
485    },
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
489#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
490#[serde(deny_unknown_fields)]
491pub struct MobEventRouterConfigInput {
492    #[serde(default = "default_event_router_buffer_size")]
493    pub buffer_size: usize,
494    #[serde(default, skip_serializing_if = "Option::is_none")]
495    pub include_patterns: Option<Vec<String>>,
496    #[serde(default, skip_serializing_if = "Option::is_none")]
497    pub exclude_patterns: Option<Vec<String>>,
498}
499
500const fn default_event_router_buffer_size() -> usize {
501    256
502}
503
504/// Public mob definition input for `mob/create`.
505///
506/// This mirrors the public creation contract shape. Runtime-owned lifecycle and
507/// bookkeeping fields such as internal owner/runtime bindings,
508/// `session_cleanup_policy`, `is_implicit`, and internal-only profile tool
509/// bundles are intentionally not part of this schema.
510///
511/// Not `Eq`: `profiles` transitively carries float provider params.
512#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
513#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
514#[serde(deny_unknown_fields)]
515pub struct MobDefinitionInput {
516    pub id: String,
517    #[serde(default, skip_serializing_if = "Option::is_none")]
518    pub orchestrator: Option<MobOrchestratorInput>,
519    pub profiles: BTreeMap<String, MobProfileBindingInput>,
520    #[serde(default)]
521    pub wiring: MobWiringRulesInput,
522    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
523    pub skills: BTreeMap<String, MobSkillSourceInput>,
524    #[serde(default)]
525    pub backend: MobBackendConfigInput,
526    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
527    pub flows: BTreeMap<String, MobFlowSpecInput>,
528    #[serde(default, skip_serializing_if = "Option::is_none")]
529    pub topology: Option<MobTopologySpecInput>,
530    #[serde(default, skip_serializing_if = "Option::is_none")]
531    pub supervisor: Option<MobSupervisorSpecInput>,
532    #[serde(default, skip_serializing_if = "Option::is_none")]
533    pub limits: Option<MobLimitsSpecInput>,
534    #[serde(default, skip_serializing_if = "Option::is_none")]
535    pub spawn_policy: Option<MobSpawnPolicyInput>,
536    #[serde(default, skip_serializing_if = "Option::is_none")]
537    pub event_router: Option<MobEventRouterConfigInput>,
538}
539
540/// Request payload for `mob/create`.
541#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
542#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
543#[serde(deny_unknown_fields)]
544pub struct MobCreateParams {
545    pub definition: MobDefinitionInput,
546}
547
548/// Response payload for `mob/create`.
549#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
550#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
551pub struct MobCreateResult {
552    pub mob_id: String,
553}
554
555/// Shared request payload for mob methods that address a mob by id.
556#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
557#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
558#[serde(deny_unknown_fields)]
559pub struct MobIdParams {
560    pub mob_id: String,
561}
562
563/// Shared request payload for mob methods that address one member by identity.
564#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
565#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
566#[serde(deny_unknown_fields)]
567pub struct MobMemberParams {
568    pub mob_id: String,
569    pub agent_identity: String,
570}
571
572/// One active mob row returned by `mob/list`.
573#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
574#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
575pub struct MobStatusResult {
576    pub mob_id: String,
577    pub status: String,
578}
579
580/// Response payload for `mob/list`.
581#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
582#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
583pub struct MobListResult {
584    pub mobs: Vec<MobStatusResult>,
585}
586
587/// Request payload for `mob/spawn`.
588#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
589#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
590#[serde(deny_unknown_fields)]
591pub struct MobSpawnParams {
592    pub mob_id: String,
593    pub profile: String,
594    pub agent_identity: String,
595    #[serde(default, skip_serializing_if = "Option::is_none")]
596    pub initial_message: Option<WireContentInput>,
597    #[serde(default, skip_serializing_if = "Option::is_none")]
598    pub runtime_mode: Option<WireMobRuntimeMode>,
599    #[serde(default, skip_serializing_if = "Option::is_none")]
600    pub backend: Option<WireMobBackendKind>,
601    #[serde(default, skip_serializing_if = "Option::is_none")]
602    pub labels: Option<BTreeMap<String, String>>,
603    #[serde(default, skip_serializing_if = "Option::is_none")]
604    pub context: Option<Value>,
605    #[serde(default, skip_serializing_if = "Option::is_none")]
606    pub additional_instructions: Option<Vec<String>>,
607    #[serde(default, skip_serializing_if = "Option::is_none")]
608    pub binding: Option<WireRuntimeBinding>,
609    #[serde(default, skip_serializing_if = "Option::is_none")]
610    pub shell_env: Option<BTreeMap<String, String>>,
611    #[serde(default, skip_serializing_if = "Option::is_none")]
612    pub auto_wire_parent: Option<bool>,
613    #[serde(default, skip_serializing_if = "Option::is_none")]
614    pub launch_mode: Option<WireMemberLaunchMode>,
615    #[serde(default, skip_serializing_if = "Option::is_none")]
616    pub tool_access_policy: Option<WireToolAccessPolicy>,
617    #[serde(default, skip_serializing_if = "Option::is_none")]
618    pub budget_split_policy: Option<WireBudgetSplitPolicy>,
619    #[serde(default, skip_serializing_if = "Option::is_none")]
620    pub inherited_tool_filter: Option<WireToolFilter>,
621    #[serde(default, skip_serializing_if = "Option::is_none")]
622    pub override_profile: Option<WireMobProfile>,
623    #[serde(default, skip_serializing_if = "Option::is_none")]
624    pub auth_binding: Option<WireAuthBindingRef>,
625}
626
627/// Response payload for `mob/spawn`.
628#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
629#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
630pub struct MobSpawnResult {
631    pub mob_id: String,
632    pub agent_identity: String,
633    pub member_ref: WireMemberRef,
634}
635
636/// Per-member request payload inside `mob/spawn_many`.
637#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
638#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
639#[serde(deny_unknown_fields)]
640pub struct MobSpawnSpecParams {
641    pub profile: String,
642    pub agent_identity: String,
643    #[serde(default, skip_serializing_if = "Option::is_none")]
644    pub initial_message: Option<WireContentInput>,
645    #[serde(default, skip_serializing_if = "Option::is_none")]
646    pub runtime_mode: Option<WireMobRuntimeMode>,
647    #[serde(default, skip_serializing_if = "Option::is_none")]
648    pub backend: Option<WireMobBackendKind>,
649    #[serde(default, skip_serializing_if = "Option::is_none")]
650    pub labels: Option<BTreeMap<String, String>>,
651    #[serde(default, skip_serializing_if = "Option::is_none")]
652    pub context: Option<Value>,
653    #[serde(default, skip_serializing_if = "Option::is_none")]
654    pub additional_instructions: Option<Vec<String>>,
655    #[serde(default, skip_serializing_if = "Option::is_none")]
656    pub auth_binding: Option<WireAuthBindingRef>,
657}
658
659/// Request payload for `mob/spawn_many`.
660#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
661#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
662#[serde(deny_unknown_fields)]
663pub struct MobSpawnManyParams {
664    pub mob_id: String,
665    pub specs: Vec<MobSpawnSpecParams>,
666}
667
668/// Typed status for one `mob/spawn_many` row.
669#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
670#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
671#[serde(rename_all = "snake_case")]
672pub enum MobSpawnManyResultStatus {
673    Spawned,
674    Failed,
675}
676
677/// Successful per-member `mob/spawn_many` result payload.
678#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
679#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
680#[serde(deny_unknown_fields)]
681pub struct MobSpawnManySpawnedResult {
682    pub agent_identity: String,
683    pub member_ref: WireMemberRef,
684}
685
686/// Typed failure cause for one failed `mob/spawn_many` member row.
687#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
688#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
689#[serde(rename_all = "snake_case")]
690pub enum MobSpawnManyFailureCause {
691    ProfileNotFound,
692    MemberNotFound,
693    MemberAlreadyExists,
694    NotExternallyAddressable,
695    InvalidTransition,
696    WiringError,
697    BridgeCommandRejected,
698    MemberRestoreFailed,
699    KickoffWaitTimedOut,
700    ReadyWaitTimedOut,
701    DefinitionError,
702    FlowNotFound,
703    FlowFailed,
704    RunNotFound,
705    RunCanceled,
706    FlowTurnTimedOut,
707    FrameDepthLimitExceeded,
708    FrameAtomicPersistenceUnavailable,
709    SpecRevisionConflict,
710    SchemaValidation,
711    InsufficientTargets,
712    TopologyViolation,
713    BridgeDeliveryRejected,
714    SupervisorEscalation,
715    UnsupportedForMode,
716    MissingMemberCapability,
717    ResetBarrier,
718    StorageError,
719    SessionError,
720    CommsError,
721    CallbackPending,
722    StaleFenceToken,
723    StaleEventCursor,
724    WorkNotFound,
725    Internal,
726}
727
728/// Failed per-member `mob/spawn_many` result payload.
729#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
730#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
731#[serde(deny_unknown_fields)]
732pub struct MobSpawnManyFailedResult {
733    pub cause: MobSpawnManyFailureCause,
734    pub message: String,
735}
736
737/// Typed payload for one `mob/spawn_many` row.
738#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
739#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
740#[serde(untagged)]
741pub enum MobSpawnManyResultPayload {
742    Spawned(MobSpawnManySpawnedResult),
743    Failed(MobSpawnManyFailedResult),
744}
745
746/// One typed result entry in a `mob/spawn_many` response.
747#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
748#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
749#[serde(try_from = "MobSpawnManyResultEntryRaw")]
750pub struct MobSpawnManyResultEntry {
751    pub status: MobSpawnManyResultStatus,
752    pub result: MobSpawnManyResultPayload,
753}
754
755#[derive(Debug, Deserialize)]
756#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
757#[serde(deny_unknown_fields)]
758struct MobSpawnManyResultEntryRaw {
759    status: MobSpawnManyResultStatus,
760    result: MobSpawnManyResultPayload,
761}
762
763impl TryFrom<MobSpawnManyResultEntryRaw> for MobSpawnManyResultEntry {
764    type Error = String;
765
766    fn try_from(raw: MobSpawnManyResultEntryRaw) -> Result<Self, Self::Error> {
767        let entry = Self {
768            status: raw.status,
769            result: raw.result,
770        };
771        entry.validate().map_err(str::to_owned)?;
772        Ok(entry)
773    }
774}
775
776impl MobSpawnManyResultEntry {
777    pub fn spawned(agent_identity: impl Into<String>, member_ref: WireMemberRef) -> Self {
778        Self {
779            status: MobSpawnManyResultStatus::Spawned,
780            result: MobSpawnManyResultPayload::Spawned(MobSpawnManySpawnedResult {
781                agent_identity: agent_identity.into(),
782                member_ref,
783            }),
784        }
785    }
786
787    pub fn failed(cause: MobSpawnManyFailureCause, message: impl Into<String>) -> Self {
788        Self {
789            status: MobSpawnManyResultStatus::Failed,
790            result: MobSpawnManyResultPayload::Failed(MobSpawnManyFailedResult {
791                cause,
792                message: message.into(),
793            }),
794        }
795    }
796
797    pub fn validate(&self) -> Result<(), &'static str> {
798        match (&self.status, &self.result) {
799            (MobSpawnManyResultStatus::Spawned, MobSpawnManyResultPayload::Spawned(_))
800            | (MobSpawnManyResultStatus::Failed, MobSpawnManyResultPayload::Failed(_)) => Ok(()),
801            (MobSpawnManyResultStatus::Spawned, MobSpawnManyResultPayload::Failed(_)) => {
802                Err("mob spawn_many result status spawned requires spawned result")
803            }
804            (MobSpawnManyResultStatus::Failed, MobSpawnManyResultPayload::Spawned(_)) => {
805                Err("mob spawn_many result status failed requires failed result")
806            }
807        }
808    }
809}
810
811/// Response payload for `mob/spawn_many`.
812#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
813#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
814pub struct MobSpawnManyResult {
815    pub results: Vec<MobSpawnManyResultEntry>,
816}
817
818/// Response payload for `mob/retire`.
819#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
820#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
821pub struct MobRetireResult {
822    pub retired: bool,
823}
824
825/// Request payload for `mob/respawn`.
826#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
827#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
828#[serde(deny_unknown_fields)]
829pub struct MobRespawnParams {
830    pub mob_id: String,
831    pub agent_identity: String,
832    #[serde(default, skip_serializing_if = "Option::is_none")]
833    pub initial_message: Option<WireContentInput>,
834}
835
836/// Identity-native respawn receipt returned inside `MobRespawnResult`.
837#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
838#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
839pub struct MobRespawnReceipt {
840    pub identity: String,
841    pub member_ref: WireMemberRef,
842}
843
844/// Response payload for `mob/respawn`.
845#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
846#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
847pub struct MobRespawnResult {
848    pub status: String,
849    pub receipt: MobRespawnReceipt,
850    #[serde(default, skip_serializing_if = "Vec::is_empty")]
851    pub failed_peer_ids: Vec<String>,
852}
853
854/// Response payload for `mob/members`.
855#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
856#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
857pub struct MobMembersResult {
858    pub mob_id: String,
859    pub members: Vec<MobMemberListEntryWire>,
860}
861
862/// Request payload for `mob/events`.
863#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
864#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
865#[serde(deny_unknown_fields)]
866pub struct MobEventsParams {
867    pub mob_id: String,
868    #[serde(default)]
869    pub after_cursor: u64,
870    #[serde(default = "default_mob_events_limit")]
871    pub limit: usize,
872    #[serde(default)]
873    pub strict: bool,
874}
875
876const fn default_mob_events_limit() -> usize {
877    100
878}
879
880/// Response payload for `mob/events`.
881#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
882#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
883pub struct MobEventsResult {
884    pub events: Vec<Value>,
885}
886
887/// Typed external peer identity for public mob wiring surfaces.
888#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
889#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
890#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
891pub enum WireTrustedPeerIdentity {
892    /// Recoverable Ed25519 public key string in `ed25519:<base64>` form.
893    Ed25519PublicKey { public_key: String },
894}
895
896/// Resolved external peer identity atoms used after the wire boundary.
897#[derive(Debug, Clone, Copy, PartialEq, Eq)]
898pub struct ResolvedWireTrustedPeerIdentity {
899    pub peer_id: meerkat_core::comms::PeerId,
900    pub pubkey: [u8; 32],
901}
902
903/// Failure modes for resolving a typed external peer identity.
904#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
905pub enum WireTrustedPeerIdentityError {
906    #[error("external peer identity public_key must start with 'ed25519:'")]
907    MissingEd25519Prefix,
908    #[error("external peer identity public_key is not valid base64: {0}")]
909    InvalidBase64(String),
910    #[error("external peer identity public_key must decode to 32 bytes, got {actual}")]
911    InvalidLength { actual: usize },
912    #[error("external peer identity public_key must be non-zero")]
913    ZeroPublicKey,
914}
915
916impl WireTrustedPeerIdentity {
917    pub fn resolve(&self) -> Result<ResolvedWireTrustedPeerIdentity, WireTrustedPeerIdentityError> {
918        match self {
919            Self::Ed25519PublicKey { public_key } => {
920                let pubkey = parse_ed25519_public_key(public_key)?;
921                if pubkey == [0u8; 32] {
922                    return Err(WireTrustedPeerIdentityError::ZeroPublicKey);
923                }
924                Ok(ResolvedWireTrustedPeerIdentity {
925                    peer_id: meerkat_core::comms::PeerId::from_ed25519_pubkey(&pubkey),
926                    pubkey,
927                })
928            }
929        }
930    }
931}
932
933fn parse_ed25519_public_key(raw: &str) -> Result<[u8; 32], WireTrustedPeerIdentityError> {
934    const PREFIX: &str = "ed25519:";
935    let encoded = raw
936        .strip_prefix(PREFIX)
937        .ok_or(WireTrustedPeerIdentityError::MissingEd25519Prefix)?;
938    let bytes = BASE64
939        .decode(encoded)
940        .map_err(|err| WireTrustedPeerIdentityError::InvalidBase64(err.to_string()))?;
941    let actual = bytes.len();
942    let pubkey: [u8; 32] = bytes
943        .try_into()
944        .map_err(|_| WireTrustedPeerIdentityError::InvalidLength { actual })?;
945    Ok(pubkey)
946}
947
948/// Minimal trusted peer spec for public mob wiring surfaces.
949///
950/// `identity` is required and resolves to the Ed25519 signing public key
951/// plus the canonical comms `PeerId` derived from that key. MCP callers do
952/// not provide raw peer IDs, and missing key material fails at the boundary.
953#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
954#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
955#[serde(deny_unknown_fields)]
956pub struct WireTrustedPeerSpec {
957    pub name: String,
958    pub address: String,
959    pub identity: WireTrustedPeerIdentity,
960}
961
962/// Target for a mob wire/unwire call.
963#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
964#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
965#[serde(rename_all = "snake_case")]
966pub enum MobPeerTarget {
967    Local(String),
968    External(WireTrustedPeerSpec),
969}
970
971/// Request payload for `mob/wire`.
972#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
973#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
974#[serde(deny_unknown_fields)]
975pub struct MobWireParams {
976    pub mob_id: String,
977    pub member: String,
978    pub peer: MobPeerTarget,
979}
980
981/// Response payload for `mob/wire`.
982#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
983#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
984pub struct MobWireResult {
985    pub wired: bool,
986}
987
988/// Request payload for `mob/unwire`.
989#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
990#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
991#[serde(deny_unknown_fields)]
992pub struct MobUnwireParams {
993    pub mob_id: String,
994    pub member: String,
995    pub peer: MobPeerTarget,
996}
997
998/// Response payload for `mob/unwire`.
999#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1000#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1001pub struct MobUnwireResult {
1002    pub unwired: bool,
1003}
1004
1005/// Request payload for host-side mob member delivery.
1006#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1007#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1008#[serde(deny_unknown_fields)]
1009pub struct MobMemberSendParams {
1010    pub mob_id: String,
1011    pub agent_identity: String,
1012    pub content: WireContentInput,
1013    #[serde(default)]
1014    pub handling_mode: WireHandlingMode,
1015    #[serde(default, skip_serializing_if = "Option::is_none")]
1016    pub render_metadata: Option<WireRenderMetadata>,
1017}
1018
1019/// Response payload for host-side mob member delivery.
1020#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1021#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1022pub struct WireAgentRuntimeId {
1023    pub identity: String,
1024    pub generation: u64,
1025}
1026
1027/// Response payload for host-side mob member delivery.
1028#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1029#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1030pub struct MobMemberSendResult {
1031    pub mob_id: String,
1032    /// Identity-native member identity (0.6).
1033    pub agent_identity: String,
1034    /// Server-resolved opaque handle for subsequent member-targeted calls.
1035    /// App code routes through `member_ref`; the binding-era
1036    /// `{identity, generation}` pair carried by `WireAgentRuntimeId` is
1037    /// retired from app-facing responses per dogma #10.
1038    pub member_ref: WireMemberRef,
1039    pub handling_mode: WireHandlingMode,
1040}
1041
1042/// Request payload for `mob/ingress_interaction`.
1043///
1044/// This is the ergonomic "ensure an ingress member, then deliver user input"
1045/// path. It composes the existing declarative roster and member-send
1046/// semantics without introducing a separate thread/project runtime.
1047#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1048#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1049#[serde(deny_unknown_fields)]
1050pub struct MobIngressInteractionParams {
1051    pub mob_id: String,
1052    pub spec: MobMemberSpecWire,
1053    pub content: WireContentInput,
1054    #[serde(default)]
1055    pub handling_mode: WireHandlingMode,
1056    #[serde(default, skip_serializing_if = "Option::is_none")]
1057    pub render_metadata: Option<WireRenderMetadata>,
1058}
1059
1060/// Response payload for `mob/ingress_interaction`.
1061#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1062#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1063pub struct MobIngressInteractionResult {
1064    pub mob_id: String,
1065    pub agent_identity: String,
1066    pub member_ref: WireMemberRef,
1067    pub ensure_outcome: MobEnsureMemberOutcomeWire,
1068    pub delivery: MobMemberSendResult,
1069    /// Cursor observed immediately before the ensure/send composition.
1070    pub events_after_cursor: u64,
1071    /// Cursor observed after delivery was accepted.
1072    pub latest_event_cursor: u64,
1073}
1074
1075/// Public handling mode for mob member delivery.
1076#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
1077#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1078#[serde(rename_all = "snake_case")]
1079pub enum WireHandlingMode {
1080    #[default]
1081    Queue,
1082    Steer,
1083}
1084
1085impl From<WireHandlingMode> for HandlingMode {
1086    fn from(mode: WireHandlingMode) -> Self {
1087        match mode {
1088            WireHandlingMode::Queue => HandlingMode::Queue,
1089            WireHandlingMode::Steer => HandlingMode::Steer,
1090        }
1091    }
1092}
1093
1094impl From<HandlingMode> for WireHandlingMode {
1095    fn from(mode: HandlingMode) -> Self {
1096        match mode {
1097            HandlingMode::Queue => WireHandlingMode::Queue,
1098            HandlingMode::Steer => WireHandlingMode::Steer,
1099        }
1100    }
1101}
1102
1103/// Public render class contract for mob member delivery.
1104#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1105#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1106#[serde(rename_all = "snake_case")]
1107pub enum WireRenderClass {
1108    UserPrompt,
1109    PeerMessage,
1110    PeerRequest,
1111    PeerResponse,
1112    ExternalEvent,
1113    FlowStep,
1114    Continuation,
1115    SystemNotice,
1116    ToolScopeNotice,
1117    OpsProgress,
1118}
1119
1120impl From<WireRenderClass> for RenderClass {
1121    fn from(class: WireRenderClass) -> Self {
1122        match class {
1123            WireRenderClass::UserPrompt => RenderClass::UserPrompt,
1124            WireRenderClass::PeerMessage => RenderClass::PeerMessage,
1125            WireRenderClass::PeerRequest => RenderClass::PeerRequest,
1126            WireRenderClass::PeerResponse => RenderClass::PeerResponse,
1127            WireRenderClass::ExternalEvent => RenderClass::ExternalEvent,
1128            WireRenderClass::FlowStep => RenderClass::FlowStep,
1129            WireRenderClass::Continuation => RenderClass::Continuation,
1130            WireRenderClass::SystemNotice => RenderClass::SystemNotice,
1131            WireRenderClass::ToolScopeNotice => RenderClass::ToolScopeNotice,
1132            WireRenderClass::OpsProgress => RenderClass::OpsProgress,
1133        }
1134    }
1135}
1136
1137impl From<RenderClass> for WireRenderClass {
1138    fn from(class: RenderClass) -> Self {
1139        match class {
1140            RenderClass::UserPrompt => WireRenderClass::UserPrompt,
1141            RenderClass::PeerMessage => WireRenderClass::PeerMessage,
1142            RenderClass::PeerRequest => WireRenderClass::PeerRequest,
1143            RenderClass::PeerResponse => WireRenderClass::PeerResponse,
1144            RenderClass::ExternalEvent => WireRenderClass::ExternalEvent,
1145            RenderClass::FlowStep => WireRenderClass::FlowStep,
1146            RenderClass::Continuation => WireRenderClass::Continuation,
1147            RenderClass::SystemNotice => WireRenderClass::SystemNotice,
1148            RenderClass::ToolScopeNotice => WireRenderClass::ToolScopeNotice,
1149            RenderClass::OpsProgress => WireRenderClass::OpsProgress,
1150        }
1151    }
1152}
1153
1154/// Public render salience contract for mob member delivery.
1155#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1156#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1157#[serde(rename_all = "snake_case")]
1158pub enum WireRenderSalience {
1159    Background,
1160    Normal,
1161    Important,
1162    Urgent,
1163}
1164
1165impl From<WireRenderSalience> for RenderSalience {
1166    fn from(salience: WireRenderSalience) -> Self {
1167        match salience {
1168            WireRenderSalience::Background => RenderSalience::Background,
1169            WireRenderSalience::Normal => RenderSalience::Normal,
1170            WireRenderSalience::Important => RenderSalience::Important,
1171            WireRenderSalience::Urgent => RenderSalience::Urgent,
1172        }
1173    }
1174}
1175
1176impl From<RenderSalience> for WireRenderSalience {
1177    fn from(salience: RenderSalience) -> Self {
1178        match salience {
1179            RenderSalience::Background => WireRenderSalience::Background,
1180            RenderSalience::Normal => WireRenderSalience::Normal,
1181            RenderSalience::Important => WireRenderSalience::Important,
1182            RenderSalience::Urgent => WireRenderSalience::Urgent,
1183        }
1184    }
1185}
1186
1187/// Public render metadata contract for mob member delivery.
1188#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1189#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1190pub struct WireRenderMetadata {
1191    pub class: WireRenderClass,
1192    #[serde(default, skip_serializing_if = "Option::is_none")]
1193    pub salience: Option<WireRenderSalience>,
1194}
1195
1196impl From<WireRenderMetadata> for RenderMetadata {
1197    fn from(metadata: WireRenderMetadata) -> Self {
1198        Self {
1199            class: metadata.class.into(),
1200            salience: metadata
1201                .salience
1202                .unwrap_or(WireRenderSalience::Normal)
1203                .into(),
1204        }
1205    }
1206}
1207
1208impl From<RenderMetadata> for WireRenderMetadata {
1209    fn from(metadata: RenderMetadata) -> Self {
1210        Self {
1211            class: metadata.class.into(),
1212            salience: Some(metadata.salience.into()),
1213        }
1214    }
1215}
1216
1217// ---------------------------------------------------------------------------
1218// Declarative roster API (`mob/ensure_member`, `mob/reconcile`,
1219// `mob/list_members_matching`). These methods compose over spawn / retire /
1220// list_members; they introduce no new lifecycle.
1221// ---------------------------------------------------------------------------
1222
1223/// Per-member spec for `mob/ensure_member` and the `desired` entries of
1224/// `mob/reconcile`.
1225///
1226/// Mirrors the essential, codegen-friendly fields of
1227/// [`meerkat_mob::SpawnMemberSpec`]. Complex sub-types (tool access policy,
1228/// budget split, inherited tool filter, override profile) are not on this
1229/// wire surface — callers that need that parity should use the non-declarative
1230/// `mob/spawn` method.
1231#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1232#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1233pub struct MobMemberSpecWire {
1234    /// Profile name (role) in the mob definition.
1235    pub profile: String,
1236    /// Stable member identity within the mob.
1237    pub agent_identity: String,
1238    #[serde(default, skip_serializing_if = "Option::is_none")]
1239    pub initial_message: Option<WireContentInput>,
1240    #[serde(default, skip_serializing_if = "Option::is_none")]
1241    pub runtime_mode: Option<WireMobRuntimeMode>,
1242    #[serde(default, skip_serializing_if = "Option::is_none")]
1243    pub backend: Option<WireMobBackendKind>,
1244    #[serde(default, skip_serializing_if = "Option::is_none")]
1245    pub binding: Option<WireRuntimeBinding>,
1246    #[serde(default, skip_serializing_if = "Option::is_none")]
1247    pub context: Option<Value>,
1248    #[serde(default, skip_serializing_if = "Option::is_none")]
1249    pub labels: Option<BTreeMap<String, String>>,
1250    #[serde(default, skip_serializing_if = "Option::is_none")]
1251    pub additional_instructions: Option<Vec<String>>,
1252    #[serde(default, skip_serializing_if = "Option::is_none")]
1253    pub auto_wire_parent: Option<bool>,
1254}
1255
1256impl MobMemberSpecWire {
1257    /// Compose the existing member `labels` and opaque `context` fields into
1258    /// the shared surface metadata contract without changing the JSON shape.
1259    #[must_use]
1260    pub fn surface_metadata(&self) -> SurfaceMetadata {
1261        SurfaceMetadata::from_optional_parts(self.labels.clone(), self.context.clone())
1262    }
1263
1264    /// Validate caller-supplied metadata for public member create surfaces.
1265    pub fn validate_public_surface_metadata(&self) -> Result<(), SurfaceMetadataError> {
1266        self.surface_metadata().validate_public()
1267    }
1268}
1269
1270/// Request payload for `mob/ensure_member`.
1271#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1272#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1273#[serde(deny_unknown_fields)]
1274pub struct MobEnsureMemberParams {
1275    pub mob_id: String,
1276    pub spec: MobMemberSpecWire,
1277}
1278
1279/// Server-resolved opaque handle for a mob member.
1280///
1281/// Encodes `{mob_id, agent_identity}` as a single base64url-encoded token
1282/// that callers treat as opaque. The server resolves the current
1283/// `AgentRuntimeId` and fence token against the live mob roster on every
1284/// dispatch — clients never reason about `generation` or `fence_token`
1285/// directly.
1286///
1287/// Use [`WireMemberRef::encode`] to produce a token and
1288/// [`WireMemberRef::decode`] inside an RPC handler to recover the
1289/// `(mob_id, agent_identity)` pair before resolving against the runtime.
1290#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
1291#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1292#[serde(transparent)]
1293pub struct WireMemberRef(String);
1294
1295impl WireMemberRef {
1296    /// Construct a handle from its components. The `mob_id` and
1297    /// `agent_identity` together form the resolution key the server uses to
1298    /// look up the member's current incarnation.
1299    #[must_use]
1300    pub fn encode(mob_id: &str, agent_identity: &str) -> Self {
1301        // Single-letter keys keep the encoded payload short so the token
1302        // remains compact in URLs and JSON payloads.
1303        // `Value::to_string` on a two-field object is infallible.
1304        let payload = serde_json::json!({ "m": mob_id, "a": agent_identity });
1305        Self(base64_url_encode(payload.to_string().as_bytes()))
1306    }
1307
1308    /// Borrow the raw token string for transport.
1309    #[must_use]
1310    pub fn as_str(&self) -> &str {
1311        &self.0
1312    }
1313
1314    /// Construct a handle from a raw token string without validation. Used
1315    /// when forwarding an opaque token received from the wire.
1316    #[must_use]
1317    pub fn from_token(token: impl Into<String>) -> Self {
1318        Self(token.into())
1319    }
1320
1321    /// Decode the handle into `(mob_id, agent_identity)`. Returns `Err` when
1322    /// the token is malformed.
1323    pub fn decode(&self) -> Result<(String, String), WireMemberRefError> {
1324        let bytes = base64_url_decode(&self.0).map_err(|_| WireMemberRefError::Malformed)?;
1325        let value: Value =
1326            serde_json::from_slice(&bytes).map_err(|_| WireMemberRefError::Malformed)?;
1327        let mob_id = value
1328            .get("m")
1329            .and_then(Value::as_str)
1330            .ok_or(WireMemberRefError::Malformed)?;
1331        let agent_identity = value
1332            .get("a")
1333            .and_then(Value::as_str)
1334            .ok_or(WireMemberRefError::Malformed)?;
1335        Ok((mob_id.to_string(), agent_identity.to_string()))
1336    }
1337}
1338
1339/// Failure modes for [`WireMemberRef::decode`].
1340#[derive(Debug, thiserror::Error)]
1341pub enum WireMemberRefError {
1342    /// Token is not valid base64url or its decoded payload is not the
1343    /// expected `{m, a}` shape.
1344    #[error("malformed member ref token")]
1345    Malformed,
1346}
1347
1348fn base64_url_encode(bytes: &[u8]) -> String {
1349    use base64::Engine as _;
1350    base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
1351}
1352
1353fn base64_url_decode(input: &str) -> Result<Vec<u8>, base64::DecodeError> {
1354    use base64::Engine as _;
1355    base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(input)
1356}
1357
1358/// Identity-native payload for `EnsureMemberOutcome::Spawned`.
1359#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1360#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1361pub struct MobSpawnReceiptWire {
1362    pub agent_identity: String,
1363    /// Server-resolved opaque handle for subsequent member-targeted calls
1364    /// (work submission, cancellation, lifecycle). Replaces the binding-era
1365    /// `generation` / `fence_token` pair on app-facing surfaces.
1366    pub member_ref: WireMemberRef,
1367}
1368
1369/// Execution status mirroring `meerkat_mob::runtime::MobMemberStatus`.
1370#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1371#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1372#[serde(rename_all = "snake_case")]
1373pub enum WireMobMemberStatus {
1374    Active,
1375    Retiring,
1376    Broken,
1377    Completed,
1378    Unknown,
1379}
1380
1381/// Public roster entry returned by `mob/ensure_member`'s `Existed` outcome
1382/// (and other surfaces that want a typed snapshot of a single member). Mirrors
1383/// the public-facing fields of `meerkat_mob::runtime::MobMemberListEntry`
1384/// without leaking bridge-internal fields.
1385#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1386#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1387pub struct MobMemberListEntryWire {
1388    pub agent_identity: String,
1389    pub member_ref: WireMemberRef,
1390    pub role: String,
1391    pub runtime_mode: WireMobRuntimeMode,
1392    pub state: WireMemberState,
1393    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1394    pub wired_to: Vec<String>,
1395    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1396    pub labels: BTreeMap<String, String>,
1397    pub status: WireMobMemberStatus,
1398    #[serde(default, skip_serializing_if = "Option::is_none")]
1399    pub error: Option<String>,
1400    pub is_final: bool,
1401}
1402
1403/// Outcome of a `mob/ensure_member` call.
1404///
1405/// `Existed` returns the typed [`MobMemberListEntryWire`] roster snapshot so
1406/// public consumers do not need out-of-band knowledge of the Rust domain
1407/// `MobMemberListEntry` shape.
1408#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1409#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1410pub enum MobEnsureMemberOutcomeWire {
1411    #[serde(rename = "spawned")]
1412    Spawned(MobSpawnReceiptWire),
1413    #[serde(rename = "existed")]
1414    Existed(MobMemberListEntryWire),
1415}
1416
1417/// Response payload for `mob/ensure_member`.
1418#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1419#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1420pub struct MobEnsureMemberResult {
1421    pub outcome: MobEnsureMemberOutcomeWire,
1422}
1423
1424/// Options controlling a `mob/reconcile` pass.
1425#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1426#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1427#[serde(deny_unknown_fields)]
1428pub struct MobReconcileOptionsWire {
1429    /// When `true`, members on the roster whose identity is not in the
1430    /// `desired` set are retired.
1431    #[serde(default)]
1432    pub retire_stale: bool,
1433}
1434
1435/// Closed wire stage for a per-identity `mob/reconcile` failure.
1436#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1437#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1438#[serde(rename_all = "snake_case")]
1439pub enum WireMobReconcileStage {
1440    Spawn,
1441    Retire,
1442}
1443
1444/// Request payload for `mob/reconcile`.
1445#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1446#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1447#[serde(deny_unknown_fields)]
1448pub struct MobReconcileParams {
1449    pub mob_id: String,
1450    #[serde(default)]
1451    pub desired: Vec<MobMemberSpecWire>,
1452    #[serde(default)]
1453    pub options: MobReconcileOptionsWire,
1454}
1455
1456/// Per-identity failure in a `mob/reconcile` pass.
1457#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1458#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1459pub struct MobReconcileFailureWire {
1460    pub agent_identity: String,
1461    pub stage: WireMobReconcileStage,
1462    /// Stringified mob error.
1463    pub error: String,
1464}
1465
1466/// Summary produced by a `mob/reconcile` pass.
1467#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1468#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1469pub struct MobReconcileReportWire {
1470    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1471    pub desired: Vec<String>,
1472    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1473    pub retained: Vec<String>,
1474    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1475    pub spawned: Vec<MobSpawnReceiptWire>,
1476    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1477    pub retired: Vec<String>,
1478    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1479    pub failures: Vec<MobReconcileFailureWire>,
1480}
1481
1482/// Response payload for `mob/reconcile`.
1483#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1484#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1485pub struct MobReconcileResult {
1486    pub report: MobReconcileReportWire,
1487}
1488
1489/// Typed lifecycle action for `mob/lifecycle`. Replaces the prior
1490/// `action: String` discriminator with an exhaustive enum so callers and
1491/// handlers reason about lifecycle transitions through the type system
1492/// rather than string folklore.
1493#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1494#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1495#[serde(rename_all = "snake_case")]
1496pub enum WireMobLifecycleAction {
1497    Stop,
1498    Resume,
1499    Complete,
1500    Reset,
1501    Destroy,
1502}
1503
1504/// Request payload for `mob/lifecycle`.
1505#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1506#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1507#[serde(deny_unknown_fields)]
1508pub struct MobLifecycleParams {
1509    pub mob_id: String,
1510    pub action: WireMobLifecycleAction,
1511}
1512
1513/// Response payload for `mob/lifecycle`.
1514#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1515#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1516pub struct MobLifecycleResult {
1517    pub mob_id: String,
1518    pub action: WireMobLifecycleAction,
1519    pub ok: bool,
1520    #[serde(default, skip_serializing_if = "Option::is_none")]
1521    pub destroy_report: Option<Value>,
1522}
1523
1524/// Request payload for `mob/append_system_context`.
1525#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1526#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1527#[serde(deny_unknown_fields)]
1528pub struct MobAppendSystemContextParams {
1529    pub mob_id: String,
1530    pub agent_identity: String,
1531    pub text: String,
1532    #[serde(default, skip_serializing_if = "Option::is_none")]
1533    pub source: Option<String>,
1534    #[serde(default, skip_serializing_if = "Option::is_none")]
1535    pub idempotency_key: Option<String>,
1536}
1537
1538/// Response payload for `mob/append_system_context`.
1539#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1540#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1541pub struct MobAppendSystemContextResult {
1542    pub mob_id: String,
1543    pub agent_identity: String,
1544    pub status: String,
1545}
1546
1547/// Response payload for `mob/flows`.
1548#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1549#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1550pub struct MobFlowsResult {
1551    pub mob_id: String,
1552    pub flows: Vec<String>,
1553}
1554
1555/// Request payload for `mob/flow_run`.
1556#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1557#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1558#[serde(deny_unknown_fields)]
1559pub struct MobFlowRunParams {
1560    pub mob_id: String,
1561    pub flow_id: String,
1562    #[serde(default)]
1563    pub params: Value,
1564}
1565
1566/// Response payload for `mob/flow_run`.
1567#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1568#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1569pub struct MobFlowRunResult {
1570    pub run_id: String,
1571}
1572
1573/// Request payload for `mob/flow_status`.
1574#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1575#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1576#[serde(deny_unknown_fields)]
1577pub struct MobFlowStatusParams {
1578    pub mob_id: String,
1579    pub run_id: String,
1580}
1581
1582/// Response payload for `mob/flow_status`.
1583#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1584#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1585pub struct MobFlowStatusResult {
1586    pub run: Value,
1587}
1588
1589/// Request payload for `mob/flow_cancel`.
1590#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1591#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1592#[serde(deny_unknown_fields)]
1593pub struct MobFlowCancelParams {
1594    pub mob_id: String,
1595    pub run_id: String,
1596}
1597
1598/// Response payload for `mob/flow_cancel`.
1599#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1600#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1601pub struct MobFlowCancelResult {
1602    pub canceled: bool,
1603}
1604
1605/// Request payload for `mob/spawn_helper`.
1606#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1607#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1608#[serde(deny_unknown_fields)]
1609pub struct MobSpawnHelperParams {
1610    pub mob_id: String,
1611    pub prompt: String,
1612    #[serde(default, skip_serializing_if = "Option::is_none")]
1613    pub agent_identity: Option<String>,
1614    #[serde(default, skip_serializing_if = "Option::is_none")]
1615    pub role_name: Option<String>,
1616    #[serde(default, skip_serializing_if = "Option::is_none")]
1617    pub runtime_mode: Option<WireMobRuntimeMode>,
1618    #[serde(default, skip_serializing_if = "Option::is_none")]
1619    pub backend: Option<WireMobBackendKind>,
1620}
1621
1622/// Request payload for `mob/fork_helper`.
1623#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1624#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1625#[serde(deny_unknown_fields)]
1626pub struct MobForkHelperParams {
1627    pub mob_id: String,
1628    pub source_member_id: String,
1629    pub prompt: String,
1630    #[serde(default, skip_serializing_if = "Option::is_none")]
1631    pub agent_identity: Option<String>,
1632    #[serde(default, skip_serializing_if = "Option::is_none")]
1633    pub role_name: Option<String>,
1634    #[serde(default, skip_serializing_if = "Option::is_none")]
1635    pub fork_context: Option<Value>,
1636    #[serde(default, skip_serializing_if = "Option::is_none")]
1637    pub runtime_mode: Option<WireMobRuntimeMode>,
1638    #[serde(default, skip_serializing_if = "Option::is_none")]
1639    pub backend: Option<WireMobBackendKind>,
1640}
1641
1642/// Response payload for `mob/spawn_helper` and `mob/fork_helper`.
1643#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1644#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1645pub struct MobHelperResult {
1646    #[serde(default, skip_serializing_if = "Option::is_none")]
1647    pub output: Option<String>,
1648    pub tokens_used: u64,
1649    pub agent_identity: String,
1650    pub member_ref: WireMemberRef,
1651}
1652
1653/// Response payload for `mob/force_cancel`.
1654#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1655#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1656pub struct MobForceCancelResult {
1657    pub cancelled: bool,
1658}
1659
1660/// Request payload for `mob/turn_start`.
1661#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1662#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1663#[serde(deny_unknown_fields)]
1664pub struct MobTurnStartParams {
1665    pub mob_id: String,
1666    pub agent_identity: String,
1667    pub prompt: WireContentInput,
1668    #[serde(default, skip_serializing_if = "Option::is_none")]
1669    pub skill_refs: Option<Vec<meerkat_core::skills::SkillRef>>,
1670    #[serde(default, skip_serializing_if = "Option::is_none")]
1671    pub flow_tool_overlay: Option<meerkat_core::service::TurnToolOverlay>,
1672    #[serde(default, skip_serializing_if = "Option::is_none")]
1673    pub additional_instructions: Option<Vec<String>>,
1674    #[serde(default, skip_serializing_if = "Option::is_none")]
1675    pub keep_alive: Option<bool>,
1676    #[serde(default, skip_serializing_if = "Option::is_none")]
1677    pub model: Option<String>,
1678    #[serde(default, skip_serializing_if = "Option::is_none")]
1679    pub provider: Option<String>,
1680    #[serde(default, skip_serializing_if = "Option::is_none")]
1681    pub max_tokens: Option<u32>,
1682    #[serde(default, skip_serializing_if = "Option::is_none")]
1683    pub system_prompt: Option<String>,
1684    #[serde(default, skip_serializing_if = "Option::is_none")]
1685    pub output_schema: Option<Value>,
1686    #[serde(default, skip_serializing_if = "Option::is_none")]
1687    pub structured_output_retries: Option<u32>,
1688    #[serde(default, skip_serializing_if = "Option::is_none")]
1689    pub provider_params: Option<Value>,
1690    #[serde(default)]
1691    pub clear_provider_params: bool,
1692    #[serde(default, skip_serializing_if = "Option::is_none")]
1693    pub auth_binding: Option<WireAuthBindingRef>,
1694    #[serde(default)]
1695    pub clear_auth_binding: bool,
1696}
1697
1698/// Response payload for `mob/member_status`.
1699#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1700#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1701pub struct MobMemberStatusResult {
1702    pub status: WireMobMemberStatus,
1703    #[serde(default, skip_serializing_if = "Option::is_none")]
1704    pub output_preview: Option<String>,
1705    #[serde(default, skip_serializing_if = "Option::is_none")]
1706    pub error: Option<String>,
1707    pub tokens_used: u64,
1708    pub is_final: bool,
1709    #[serde(default, skip_serializing_if = "Option::is_none")]
1710    pub current_session_id: Option<String>,
1711    #[serde(default, skip_serializing_if = "Option::is_none")]
1712    pub peer_connectivity: Option<Value>,
1713    #[serde(default, skip_serializing_if = "Option::is_none")]
1714    pub kickoff: Option<Value>,
1715    #[serde(default, skip_serializing_if = "Option::is_none")]
1716    pub external_member: Option<Value>,
1717    #[serde(default, skip_serializing_if = "Option::is_none")]
1718    pub resolved_capabilities: Option<crate::wire::WireResolvedModelCapabilities>,
1719}
1720
1721/// Response payload for `mob/snapshot`.
1722#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1723#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1724pub struct MobSnapshotResult {
1725    pub mob_id: String,
1726    pub status: String,
1727    pub members: Vec<Value>,
1728}
1729
1730#[cfg(test)]
1731mod member_status_capability_tests {
1732    use super::*;
1733
1734    #[test]
1735    fn member_status_result_round_trips_resolved_capabilities() -> Result<(), serde_json::Error> {
1736        let capabilities = crate::wire::WireResolvedModelCapabilities {
1737            vision: true,
1738            image_input: true,
1739            image_tool_results: false,
1740            inline_video: false,
1741            realtime: true,
1742            web_search: true,
1743            image_generation: true,
1744        };
1745        let result = MobMemberStatusResult {
1746            status: WireMobMemberStatus::Active,
1747            output_preview: None,
1748            error: None,
1749            tokens_used: 0,
1750            is_final: false,
1751            current_session_id: Some("session-1".to_string()),
1752            peer_connectivity: None,
1753            kickoff: None,
1754            external_member: None,
1755            resolved_capabilities: Some(capabilities.clone()),
1756        };
1757
1758        let json = serde_json::to_string(&result)?;
1759        assert!(json.contains("\"resolved_capabilities\""));
1760        let parsed: MobMemberStatusResult = serde_json::from_str(&json)?;
1761        assert_eq!(parsed.resolved_capabilities, Some(capabilities));
1762        Ok(())
1763    }
1764}
1765
1766/// Response payload for `mob/destroy`.
1767#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1768#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1769pub struct MobDestroyResult {
1770    pub mob_id: String,
1771    pub ok: bool,
1772    pub destroy_report: Value,
1773}
1774
1775/// Response payload for `mob/rotate_supervisor`.
1776#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1777#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1778pub struct MobRotateSupervisorResult {
1779    pub mob_id: String,
1780    pub ok: bool,
1781    pub report: SupervisorRotationReportWire,
1782}
1783
1784/// Confirmed supervisor rotation report returned by `mob/rotate_supervisor`.
1785#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1786#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1787pub struct SupervisorRotationReportWire {
1788    pub previous_epoch: u64,
1789    pub current_epoch: u64,
1790    pub public_peer_id: String,
1791}
1792
1793/// Shared request payload for mob readiness waits.
1794#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1795#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1796#[serde(deny_unknown_fields)]
1797pub struct MobWaitParams {
1798    pub mob_id: String,
1799    #[serde(default, skip_serializing_if = "Option::is_none")]
1800    pub member_ids: Option<Vec<String>>,
1801    #[serde(default, skip_serializing_if = "Option::is_none")]
1802    pub timeout_ms: Option<u64>,
1803}
1804
1805/// Response payload for `mob/wait_kickoff` and `mob/wait_ready`.
1806#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1807#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1808pub struct MobWaitMembersResult {
1809    pub members: Vec<Value>,
1810}
1811
1812/// Response payload for `mob/cancel_work`.
1813#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1814#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1815pub struct MobCancelWorkResult {
1816    pub mob_id: String,
1817    pub ok: bool,
1818}
1819
1820/// Response payload for `mob/cancel_all_work`.
1821#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1822#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1823pub struct MobCancelAllWorkResult {
1824    pub mob_id: String,
1825    pub ok: bool,
1826}
1827
1828/// Request payload for `mob/profile/create`.
1829#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1830#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1831#[serde(deny_unknown_fields)]
1832pub struct MobProfileCreateParams {
1833    pub name: String,
1834    pub profile: MobProfileInput,
1835}
1836
1837/// Request payload for `mob/profile/get`.
1838#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1839#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1840#[serde(deny_unknown_fields)]
1841pub struct MobProfileNameParams {
1842    pub name: String,
1843}
1844
1845/// Request payload for `mob/profile/update`.
1846#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1847#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1848#[serde(deny_unknown_fields)]
1849pub struct MobProfileUpdateParams {
1850    pub name: String,
1851    pub profile: MobProfileInput,
1852    pub expected_revision: u64,
1853}
1854
1855/// Request payload for `mob/profile/delete`.
1856#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1857#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1858#[serde(deny_unknown_fields)]
1859pub struct MobProfileDeleteParams {
1860    pub name: String,
1861    pub expected_revision: u64,
1862}
1863
1864/// Stored realm profile projection returned by `mob/profile/*`.
1865#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1866#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1867pub struct MobProfileLookupResult {
1868    #[serde(default)]
1869    pub not_found: bool,
1870    pub name: String,
1871    #[serde(default, skip_serializing_if = "Option::is_none")]
1872    pub profile: Option<Value>,
1873    #[serde(default, skip_serializing_if = "Option::is_none")]
1874    pub revision: Option<u64>,
1875    #[serde(default, skip_serializing_if = "Option::is_none")]
1876    pub created_at: Option<String>,
1877    #[serde(default, skip_serializing_if = "Option::is_none")]
1878    pub updated_at: Option<String>,
1879}
1880
1881/// Response payload for `mob/profile/list`.
1882#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1883#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1884pub struct MobProfileListResult {
1885    pub profiles: Vec<MobProfileLookupResult>,
1886}
1887
1888/// Response payload for `mob/profile/delete`.
1889#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1890#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1891pub struct MobProfileDeleteResult {
1892    pub name: String,
1893    pub deleted_revision: u64,
1894}
1895
1896/// Request payload for `mob/stream_open`.
1897#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1898#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1899#[serde(deny_unknown_fields)]
1900pub struct MobStreamOpenParams {
1901    pub mob_id: String,
1902    #[serde(default, skip_serializing_if = "Option::is_none")]
1903    pub agent_identity: Option<String>,
1904}
1905
1906/// Response payload for `mob/stream_open`.
1907#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1908#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1909pub struct MobStreamOpenResult {
1910    pub stream_id: String,
1911    pub opened: bool,
1912}
1913
1914/// Request payload for `mob/stream_close`.
1915#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1916#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1917#[serde(deny_unknown_fields)]
1918pub struct MobStreamCloseParams {
1919    pub stream_id: String,
1920}
1921
1922/// Response payload for `mob/stream_close`.
1923#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1924#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1925pub struct MobStreamCloseResult {
1926    pub stream_id: String,
1927    pub closed: bool,
1928    pub already_closed: bool,
1929}
1930
1931/// Origin for `MobSubmitWorkParams`. Replaces the prior free-form
1932/// `origin: Option<String>` shape.
1933#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
1934#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1935#[serde(rename_all = "snake_case")]
1936pub enum WireWorkOrigin {
1937    #[default]
1938    External,
1939    Internal,
1940}
1941
1942/// Request payload for `mob/submit_work`.
1943///
1944/// Identifies the member through the opaque [`WireMemberRef`] handle the
1945/// server resolves against the live roster — callers do not pass
1946/// `generation` or `fence_token`.
1947#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1948#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1949#[serde(deny_unknown_fields)]
1950pub struct MobSubmitWorkParams {
1951    pub member_ref: WireMemberRef,
1952    /// Optional caller-supplied work reference. When absent the server
1953    /// generates a fresh UUID.
1954    #[serde(default, skip_serializing_if = "Option::is_none")]
1955    pub work_ref: Option<String>,
1956    pub content: WireContentInput,
1957    #[serde(default)]
1958    pub origin: WireWorkOrigin,
1959}
1960
1961/// Response payload for `mob/submit_work`.
1962#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1963#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1964pub struct MobSubmitWorkResult {
1965    pub mob_id: String,
1966    pub work_ref: String,
1967    pub member_ref: WireMemberRef,
1968}
1969
1970/// Request payload for `mob/cancel_work`.
1971#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1972#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1973#[serde(deny_unknown_fields)]
1974pub struct MobCancelWorkParams {
1975    pub mob_id: String,
1976    pub work_ref: String,
1977}
1978
1979/// Request payload for `mob/cancel_all_work`.
1980#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1981#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1982#[serde(deny_unknown_fields)]
1983pub struct MobCancelAllWorkParams {
1984    pub member_ref: WireMemberRef,
1985}
1986
1987/// Roster member lifecycle state for `MobMemberFilterWire`.
1988#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1989#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1990#[serde(rename_all = "snake_case")]
1991pub enum WireMemberState {
1992    Active,
1993    Retiring,
1994}
1995
1996/// Filter for `mob/list_members_matching`. Non-empty / `Some` fields are
1997/// combined conjunctively; an empty filter matches every member.
1998#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1999#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2000#[serde(deny_unknown_fields)]
2001pub struct MobMemberFilterWire {
2002    /// Required exact matches on member labels.
2003    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
2004    pub labels: BTreeMap<String, String>,
2005    /// Required profile name (role).
2006    #[serde(default, skip_serializing_if = "Option::is_none")]
2007    pub role: Option<String>,
2008    /// Required roster state.
2009    #[serde(default, skip_serializing_if = "Option::is_none")]
2010    pub state: Option<WireMemberState>,
2011}
2012
2013/// Request payload for `mob/list_members_matching`.
2014#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2015#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2016#[serde(deny_unknown_fields)]
2017pub struct MobListMembersMatchingParams {
2018    pub mob_id: String,
2019    #[serde(default)]
2020    pub filter: MobMemberFilterWire,
2021}
2022
2023/// Response payload for `mob/list_members_matching`. Each member is the raw
2024/// roster entry JSON.
2025#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
2026#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2027pub struct MobListMembersMatchingResult {
2028    #[serde(default)]
2029    pub members: Vec<Value>,
2030}
2031
2032#[cfg(test)]
2033#[allow(clippy::expect_used, clippy::panic)]
2034mod tests {
2035    use super::*;
2036
2037    #[test]
2038    fn wire_member_ref_round_trips_through_encode_decode() {
2039        let token = WireMemberRef::encode("mob-42", "worker-1");
2040        let (mob_id, agent_identity) = token.decode().expect("decode round-trips");
2041        assert_eq!(mob_id, "mob-42");
2042        assert_eq!(agent_identity, "worker-1");
2043    }
2044
2045    #[test]
2046    fn wire_member_ref_rejects_malformed_token() {
2047        let err = WireMemberRef::from_token("not-a-token-payload")
2048            .decode()
2049            .expect_err("malformed tokens must fail to decode");
2050        assert!(matches!(err, WireMemberRefError::Malformed));
2051    }
2052
2053    #[test]
2054    fn mob_member_spec_exposes_shared_surface_metadata() {
2055        let spec = MobMemberSpecWire {
2056            profile: "worker".into(),
2057            agent_identity: "w1".into(),
2058            initial_message: None,
2059            runtime_mode: None,
2060            backend: None,
2061            binding: None,
2062            context: Some(serde_json::json!({"client_ref": "member-card"})),
2063            labels: Some(BTreeMap::from([("client.member_id".into(), "w1".into())])),
2064            additional_instructions: None,
2065            auto_wire_parent: None,
2066        };
2067
2068        let metadata = spec.surface_metadata();
2069        assert_eq!(
2070            metadata.labels.get("client.member_id").map(String::as_str),
2071            Some("w1")
2072        );
2073        assert_eq!(
2074            metadata.app_context,
2075            Some(serde_json::json!({"client_ref": "member-card"}))
2076        );
2077    }
2078
2079    #[test]
2080    fn mob_member_spec_surface_metadata_rejects_reserved_keys() {
2081        let spec = MobMemberSpecWire {
2082            profile: "worker".into(),
2083            agent_identity: "w1".into(),
2084            initial_message: None,
2085            runtime_mode: None,
2086            backend: None,
2087            binding: None,
2088            context: None,
2089            labels: Some(BTreeMap::from([("mob_id".into(), "spoof".into())])),
2090            additional_instructions: None,
2091            auto_wire_parent: None,
2092        };
2093
2094        assert!(spec.validate_public_surface_metadata().is_err());
2095    }
2096
2097    #[test]
2098    fn mob_reconcile_failure_stage_is_typed_wire_enum() {
2099        let failure = MobReconcileFailureWire {
2100            agent_identity: "worker-1".into(),
2101            stage: WireMobReconcileStage::Spawn,
2102            error: "spawn failed".into(),
2103        };
2104
2105        let json = serde_json::to_value(&failure).expect("serialize failure");
2106        assert_eq!(json["stage"], "spawn");
2107
2108        let round_trip: MobReconcileFailureWire =
2109            serde_json::from_value(json).expect("deserialize failure");
2110        assert_eq!(round_trip.stage, WireMobReconcileStage::Spawn);
2111
2112        let err = serde_json::from_value::<MobReconcileFailureWire>(serde_json::json!({
2113            "agent_identity": "worker-1",
2114            "stage": "restart",
2115            "error": "bad stage"
2116        }))
2117        .expect_err("unknown reconcile stage must be rejected");
2118        assert!(err.to_string().contains("unknown variant"));
2119    }
2120
2121    #[test]
2122    fn mob_lifecycle_params_reject_unknown_action_string() {
2123        let err = serde_json::from_value::<MobLifecycleParams>(serde_json::json!({
2124            "mob_id": "mob-1",
2125            "action": "explode"
2126        }))
2127        .expect_err("unknown lifecycle actions must fail at the typed wire boundary");
2128
2129        assert!(
2130            err.to_string().contains("unknown variant"),
2131            "unexpected error: {err}"
2132        );
2133    }
2134
2135    #[test]
2136    fn mob_lifecycle_result_round_trips_typed_action() {
2137        let result = MobLifecycleResult {
2138            mob_id: "mob-1".into(),
2139            action: WireMobLifecycleAction::Complete,
2140            ok: true,
2141            destroy_report: None,
2142        };
2143
2144        let json = serde_json::to_value(&result).expect("serialize lifecycle result");
2145        assert_eq!(json["action"], "complete");
2146
2147        let round_trip: MobLifecycleResult =
2148            serde_json::from_value(json).expect("deserialize lifecycle result");
2149        assert_eq!(round_trip.action, WireMobLifecycleAction::Complete);
2150    }
2151
2152    #[test]
2153    fn mob_spawn_many_result_entry_uses_typed_status_result_envelope() {
2154        let member_ref = WireMemberRef::encode("mob-1", "worker-1");
2155        let entry = MobSpawnManyResultEntry::spawned("worker-1", member_ref.clone());
2156
2157        let json = serde_json::to_value(&entry).expect("serialize typed spawn_many row");
2158        assert_eq!(json["status"], "spawned");
2159        assert_eq!(json["result"]["agent_identity"], "worker-1");
2160        assert_eq!(json["result"]["member_ref"], member_ref.as_str());
2161        assert!(json.get("ok").is_none());
2162        assert!(json.get("error").is_none());
2163
2164        let round_trip: MobSpawnManyResultEntry =
2165            serde_json::from_value(json).expect("deserialize typed spawn_many row");
2166        assert_eq!(round_trip, entry);
2167
2168        let failed = MobSpawnManyResultEntry::failed(
2169            MobSpawnManyFailureCause::ProfileNotFound,
2170            "profile missing",
2171        );
2172        let json = serde_json::to_value(&failed).expect("serialize typed failed spawn_many row");
2173        assert_eq!(json["status"], "failed");
2174        assert_eq!(json["result"]["cause"], "profile_not_found");
2175        assert_eq!(json["result"]["message"], "profile missing");
2176        assert!(json.get("ok").is_none());
2177        assert!(json.get("error").is_none());
2178
2179        let round_trip: MobSpawnManyResultEntry =
2180            serde_json::from_value(json).expect("deserialize typed failed spawn_many row");
2181        assert_eq!(round_trip, failed);
2182    }
2183
2184    #[test]
2185    fn mob_spawn_many_result_entry_rejects_legacy_or_malformed_envelopes() {
2186        let legacy = serde_json::json!({
2187            "ok": true,
2188            "agent_identity": "worker-1",
2189            "member_ref": WireMemberRef::encode("mob-1", "worker-1"),
2190        });
2191        let err = serde_json::from_value::<MobSpawnManyResultEntry>(legacy)
2192            .expect_err("legacy ok carrier must not deserialize");
2193        assert!(
2194            err.to_string().contains("missing field `status`")
2195                || err.to_string().contains("unknown field"),
2196            "unexpected error: {err}"
2197        );
2198
2199        let missing_result = serde_json::json!({
2200            "status": "spawned"
2201        });
2202        let err = serde_json::from_value::<MobSpawnManyResultEntry>(missing_result)
2203            .expect_err("missing typed result must fail closed");
2204        assert!(
2205            err.to_string().contains("missing field `result`"),
2206            "unexpected error: {err}"
2207        );
2208
2209        let unknown_status = serde_json::json!({
2210            "status": "ok",
2211            "result": {
2212                "agent_identity": "worker-1",
2213                "member_ref": WireMemberRef::encode("mob-1", "worker-1"),
2214            }
2215        });
2216        let err = serde_json::from_value::<MobSpawnManyResultEntry>(unknown_status)
2217            .expect_err("unknown typed status must fail closed");
2218        assert!(
2219            err.to_string().contains("unknown variant"),
2220            "unexpected error: {err}"
2221        );
2222
2223        let mismatched = serde_json::json!({
2224            "status": "spawned",
2225            "result": {
2226                "cause": "profile_not_found",
2227                "message": "profile missing"
2228            }
2229        });
2230        let err = serde_json::from_value::<MobSpawnManyResultEntry>(mismatched)
2231            .expect_err("status/result mismatch must fail closed");
2232        assert!(
2233            err.to_string()
2234                .contains("status spawned requires spawned result"),
2235            "unexpected error: {err}"
2236        );
2237
2238        let message_only_failure = serde_json::json!({
2239            "status": "failed",
2240            "result": {
2241                "message": "profile missing"
2242            }
2243        });
2244        let err = serde_json::from_value::<MobSpawnManyResultEntry>(message_only_failure)
2245            .expect_err("string-only failure result must fail closed");
2246        assert!(
2247            err.to_string().contains("data did not match any variant")
2248                || err.to_string().contains("missing field `cause`"),
2249            "unexpected error: {err}"
2250        );
2251
2252        let unknown_failure_cause = serde_json::json!({
2253            "status": "failed",
2254            "result": {
2255                "cause": "future_failure",
2256                "message": "future failure"
2257            }
2258        });
2259        let err = serde_json::from_value::<MobSpawnManyResultEntry>(unknown_failure_cause)
2260            .expect_err("unknown failure cause must fail closed");
2261        assert!(
2262            err.to_string().contains("data did not match any variant")
2263                || err.to_string().contains("unknown variant"),
2264            "unexpected error: {err}"
2265        );
2266    }
2267
2268    #[test]
2269    fn mob_wire_params_reject_legacy_local_target_shape() {
2270        let err = serde_json::from_value::<MobWireParams>(serde_json::json!({
2271            "mob_id": "mob-1",
2272            "local": "member-a",
2273            "target": { "local": "member-b" }
2274        }))
2275        .expect_err("legacy local/target shape must be rejected");
2276
2277        let msg = err.to_string();
2278        assert!(
2279            msg.contains("unknown field `local`") || msg.contains("missing field `member`"),
2280            "unexpected error: {msg}"
2281        );
2282    }
2283
2284    #[test]
2285    fn mob_wire_params_accept_canonical_external_peer_identity() {
2286        let params = serde_json::from_value::<MobWireParams>(serde_json::json!({
2287            "mob_id": "mob-1",
2288            "member": "member-a",
2289            "peer": {
2290                "external": {
2291                    "name": "external-worker",
2292                    "address": "inproc://external-worker",
2293                    "identity": {
2294                        "kind": "ed25519_public_key",
2295                        "public_key": "ed25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc="
2296                    }
2297                }
2298            }
2299        }))
2300        .expect("canonical external peer identity should deserialize");
2301
2302        let MobPeerTarget::External(spec) = params.peer else {
2303            panic!("expected external peer target");
2304        };
2305        assert_eq!(spec.name, "external-worker");
2306    }
2307
2308    #[test]
2309    fn mob_wire_params_reject_raw_external_peer_id_shape() {
2310        let err = serde_json::from_value::<MobWireParams>(serde_json::json!({
2311            "mob_id": "mob-1",
2312            "member": "member-a",
2313            "peer": {
2314                "external": {
2315                    "name": "external-worker",
2316                    "peer_id": meerkat_core::comms::PeerId::from_ed25519_pubkey(&[7u8; 32]).to_string(),
2317                    "address": "inproc://external-worker",
2318                    "pubkey": vec![7u8; 32]
2319                }
2320            }
2321        }))
2322        .expect_err("raw peer_id/pubkey external peer shape must be rejected");
2323
2324        let msg = err.to_string();
2325        assert!(
2326            msg.contains("peer_id") || msg.contains("identity"),
2327            "unexpected error: {msg}"
2328        );
2329    }
2330
2331    #[test]
2332    fn mob_wire_params_reject_missing_external_peer_pubkey_material() {
2333        let err = serde_json::from_value::<MobWireParams>(serde_json::json!({
2334            "mob_id": "mob-1",
2335            "member": "member-a",
2336            "peer": {
2337                "external": {
2338                    "name": "external-worker",
2339                    "address": "inproc://external-worker",
2340                    "identity": {
2341                        "kind": "ed25519_public_key"
2342                    }
2343                }
2344            }
2345        }))
2346        .expect_err("missing external peer pubkey material must fail closed");
2347
2348        let msg = err.to_string();
2349        assert!(
2350            msg.contains("public_key") || msg.contains("identity"),
2351            "unexpected error: {msg}"
2352        );
2353    }
2354
2355    #[test]
2356    fn runtime_binding_accepts_canonical_external_peer_identity() {
2357        let binding = serde_json::from_value::<WireRuntimeBinding>(serde_json::json!({
2358            "kind": "external",
2359            "address": "inproc://external-worker",
2360            "identity": {
2361                "kind": "ed25519_public_key",
2362                "public_key": "ed25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc="
2363            }
2364        }))
2365        .expect("canonical external runtime binding identity should deserialize");
2366
2367        let WireRuntimeBinding::External {
2368            identity, address, ..
2369        } = binding
2370        else {
2371            panic!("expected external runtime binding");
2372        };
2373        assert_eq!(address, "inproc://external-worker");
2374        assert_eq!(
2375            identity.resolve().expect("identity resolves").pubkey,
2376            [7u8; 32]
2377        );
2378    }
2379
2380    #[test]
2381    fn runtime_binding_rejects_raw_external_peer_id_shape() {
2382        let err = serde_json::from_value::<WireRuntimeBinding>(serde_json::json!({
2383            "kind": "external",
2384            "peer_id": meerkat_core::comms::PeerId::from_ed25519_pubkey(&[7u8; 32]).to_string(),
2385            "address": "inproc://external-worker",
2386            "pubkey": vec![7u8; 32]
2387        }))
2388        .expect_err("raw peer_id/pubkey external runtime binding shape must be rejected");
2389
2390        let msg = err.to_string();
2391        assert!(
2392            msg.contains("peer_id") || msg.contains("identity"),
2393            "unexpected error: {msg}"
2394        );
2395    }
2396
2397    #[test]
2398    fn runtime_binding_rejects_missing_external_peer_pubkey_material() {
2399        let err = serde_json::from_value::<WireRuntimeBinding>(serde_json::json!({
2400            "kind": "external",
2401            "address": "inproc://external-worker",
2402            "identity": {
2403                "kind": "ed25519_public_key"
2404            }
2405        }))
2406        .expect_err("missing external runtime binding pubkey material must fail closed");
2407
2408        let msg = err.to_string();
2409        assert!(
2410            msg.contains("public_key") || msg.contains("identity"),
2411            "unexpected error: {msg}"
2412        );
2413    }
2414
2415    #[test]
2416    fn mob_turn_start_params_capture_turn_override_fields() {
2417        let params = serde_json::from_value::<MobTurnStartParams>(serde_json::json!({
2418            "mob_id": "mob-1",
2419            "agent_identity": "worker",
2420            "prompt": "continue",
2421            "output_schema": { "type": "object" },
2422            "structured_output_retries": 2
2423        }))
2424        .expect("turn_start should accept explicit turn override fields");
2425
2426        assert_eq!(params.mob_id, "mob-1");
2427        assert_eq!(params.agent_identity, "worker");
2428        assert_eq!(params.prompt, WireContentInput::Text("continue".into()));
2429        assert_eq!(
2430            params.output_schema,
2431            Some(serde_json::json!({ "type": "object" }))
2432        );
2433        assert_eq!(params.structured_output_retries, Some(2));
2434
2435        let err = serde_json::from_value::<MobTurnStartParams>(serde_json::json!({
2436            "mob_id": "mob-1",
2437            "agent_identity": "worker",
2438            "prompt": "continue",
2439            "unknown_override": true
2440        }))
2441        .expect_err("turn_start must reject unknown override fields");
2442        assert!(
2443            err.to_string().contains("unknown field"),
2444            "unexpected error: {err}"
2445        );
2446    }
2447
2448    #[test]
2449    fn mob_create_params_reject_reserved_runtime_lifecycle_fields() {
2450        let err = serde_json::from_value::<MobCreateParams>(serde_json::json!({
2451            "definition": {
2452                "id": "mob-1",
2453                "owner_runtime_binding": "runtime:worker:0",
2454                "profiles": {
2455                    "worker": { "model": "claude-sonnet-4-6" }
2456                }
2457            }
2458        }))
2459        .expect_err("reserved runtime lifecycle fields must be rejected");
2460
2461        assert!(
2462            err.to_string()
2463                .contains("unknown field `owner_runtime_binding`"),
2464            "unexpected error: {err}"
2465        );
2466    }
2467
2468    #[test]
2469    fn mob_create_params_reject_reserved_runtime_bridge_owner_field() {
2470        let err = serde_json::from_value::<MobCreateParams>(serde_json::json!({
2471            "definition": {
2472                "id": "mob-1",
2473                "owner_transport_binding": "transport:worker:0",
2474                "profiles": {
2475                    "worker": { "model": "claude-sonnet-4-6" }
2476                }
2477            }
2478        }))
2479        .expect_err("reserved runtime bridge owner field must be rejected");
2480
2481        assert!(
2482            err.to_string()
2483                .contains("unknown field `owner_transport_binding`"),
2484            "unexpected error: {err}"
2485        );
2486    }
2487
2488    #[test]
2489    fn mob_create_params_reject_internal_profile_tool_bundles() {
2490        let err = serde_json::from_value::<MobCreateParams>(serde_json::json!({
2491            "definition": {
2492                "id": "mob-1",
2493                "profiles": {
2494                    "worker": {
2495                        "model": "claude-sonnet-4-6",
2496                        "tools": {
2497                            "rust_bundles": ["internal-only"]
2498                        }
2499                    }
2500                }
2501            }
2502        }))
2503        .expect_err("internal rust tool bundles must be rejected");
2504
2505        // With untagged MobProfileBindingInput, the error message is about
2506        // no variant matching rather than the specific unknown field.
2507        assert!(
2508            err.to_string().contains("did not match any variant")
2509                || err.to_string().contains("unknown field `rust_bundles`"),
2510            "unexpected error: {err}"
2511        );
2512    }
2513
2514    #[test]
2515    fn mob_create_params_accept_typed_nested_flow_definition() {
2516        let params = serde_json::from_value::<MobCreateParams>(serde_json::json!({
2517            "definition": {
2518                "id": "mob-1",
2519                "profiles": {
2520                    "worker": { "model": "claude-sonnet-4-6" }
2521                },
2522                "flows": {
2523                    "review": {
2524                        "description": "review flow",
2525                        "steps": {
2526                            "draft": {
2527                                "role": "worker",
2528                                "message": "draft it"
2529                            }
2530                        }
2531                    }
2532                }
2533            }
2534        }))
2535        .expect("typed nested flow definition should parse");
2536
2537        assert_eq!(
2538            params.definition.flows["review"].steps["draft"].role,
2539            "worker"
2540        );
2541    }
2542}