Skip to main content

hatchery_harness_protocol/
lib.rs

1//! Bounded, privacy-minimized domain contract for the adjacent harness kernel.
2//!
3//! Harness workflow state is explicit operator/agent intent. Monitoring facts
4//! are deliberately absent from this crate and cannot mutate task state.
5
6use std::{cmp::Ordering, fmt};
7
8use serde::{Deserialize, Deserializer, Serialize};
9use thiserror::Error;
10
11pub const HARNESS_TITLE_MAX_BYTES: usize = 256;
12pub const HARNESS_BODY_MAX_BYTES: usize = 8 * 1024;
13pub const HARNESS_SELECTOR_MAX_BYTES: usize = 128;
14pub const HARNESS_LINKS_MAX: usize = 128;
15pub const HARNESS_DEPENDENCIES_MAX: usize = 64;
16pub const HARNESS_RESULTS_MAX: usize = 64;
17pub const HARNESS_ARTIFACTS_MAX: usize = 128;
18pub const HARNESS_RUN_GIT_STATUS_ENTRIES_MAX: usize = 128;
19pub const HARNESS_RUN_GIT_RECENT_COMMITS_MAX: usize = 12;
20pub const HARNESS_RUN_GIT_BRANCH_MAX_BYTES: usize = 1_024;
21pub const HARNESS_RUN_GIT_PATH_MAX_BYTES: usize = 1_024;
22pub const HARNESS_RUN_GIT_SUMMARY_MAX_BYTES: usize = 1_024;
23pub const HARNESS_ALLOWED_TARGETS_MAX: usize = 128;
24pub const HARNESS_CHILD_COUNT_MAX: u16 = 256;
25pub const HARNESS_CHILD_DEPTH_MAX: u16 = 32;
26pub const HARNESS_TIMEOUT_MIN_MS: u64 = 100;
27pub const HARNESS_TIMEOUT_MAX_MS: u64 = 24 * 60 * 60 * 1_000;
28pub const HARNESS_DELIVERY_RESOURCE_MAX_BYTES: usize = 128;
29pub const HARNESS_DELIVERIES_MAX: usize = 4_096;
30pub const HARNESS_CONTINUATIONS_MAX: usize = 4_096;
31pub const HARNESS_CONTEXT_PACK_MAX_BYTES: u32 = 256 * 1024;
32pub const HARNESS_CONTEXT_PACK_RETAINED_MESSAGES_MAX: u64 = 256;
33pub const HARNESS_SCHEDULER_SCAN_MAX: usize = 8_192;
34pub const HARNESS_DELIVERY_COMPONENT_KINDS_MAX: usize = 9;
35pub const HARNESS_DELIVERY_COMPONENTS_MAX: u32 = 4_096;
36/// Bound on `HarnessTaskCreateResultV1`/`HarnessTaskMoveResultV1`'s own
37/// `why` fields (D5, Slice D). The value they carry is always a short,
38/// fixed-format `HarnessValidationError` `Display` rendering, never
39/// arbitrary caller-supplied prose, but this side still bounds it
40/// defensively -- the same 256-byte ceiling `HARNESS_TITLE_MAX_BYTES`
41/// already holds a task title to.
42pub const HARNESS_TASK_RESULT_WHY_MAX_BYTES: usize = 256;
43
44macro_rules! opaque_id {
45    ($name:ident, $prefix:literal, $label:literal) => {
46        #[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
47        #[serde(transparent)]
48        pub struct $name(String);
49
50        impl $name {
51            pub const PREFIX: &'static str = $prefix;
52
53            pub fn new(value: impl Into<String>) -> Result<Self, HarnessValidationError> {
54                let value = value.into();
55                validate_prefixed_hex_id($label, &value, Self::PREFIX, 24)?;
56                Ok(Self(value))
57            }
58
59            pub fn validate(&self) -> Result<(), HarnessValidationError> {
60                validate_prefixed_hex_id($label, &self.0, Self::PREFIX, 24)
61            }
62
63            pub fn as_str(&self) -> &str {
64                &self.0
65            }
66        }
67
68        impl fmt::Debug for $name {
69            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70                formatter.debug_tuple(stringify!($name)).field(&self.0).finish()
71            }
72        }
73
74        impl fmt::Display for $name {
75            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
76                formatter.write_str(&self.0)
77            }
78        }
79
80        impl<'de> Deserialize<'de> for $name {
81            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
82            where
83                D: Deserializer<'de>,
84            {
85                let value = String::deserialize(deserializer)?;
86                Self::new(value).map_err(serde::de::Error::custom)
87            }
88        }
89    };
90}
91
92opaque_id!(HarnessTaskId, "htask_", "task id");
93opaque_id!(HarnessRunId, "hrun_", "run id");
94opaque_id!(SessionGrantId, "hgrant_", "grant id");
95opaque_id!(HarnessOperationId, "hop_", "operation id");
96opaque_id!(HarnessResultRef, "hresult_", "result reference");
97opaque_id!(HarnessArtifactRef, "hartifact_", "artifact reference");
98opaque_id!(HarnessReceiptRef, "hreceipt_", "receipt reference");
99opaque_id!(HarnessInlineRef, "hinline_", "inline reference");
100opaque_id!(HarnessIdempotencyRef, "hidem_", "idempotency reference");
101opaque_id!(HarnessDeliveryRef, "hdelivery_", "delivery reference");
102opaque_id!(HarnessContinuationRef, "hcontinuation_", "continuation reference");
103opaque_id!(HarnessExecutionSpecId, "hespec_", "execution specification id");
104opaque_id!(HarnessTaskLaunchIssuanceId, "hissue_", "task launch issuance id");
105
106impl HarnessResultRef {
107    /// Derives the result reference for `run_id` by reprefixing its hex
108    /// body. `HarnessRunId` and `HarnessResultRef` share the identical
109    /// 24-hex-body scheme (`opaque_id!`), so this is a mechanical,
110    /// reversible relabeling, never a lookup — see `run_id()` for the
111    /// inverse. The `expect` below is sound by construction: every
112    /// `HarnessRunId` in existence was produced by `HarnessRunId::new`
113    /// (directly or via `Deserialize`), which validates the `hrun_` prefix
114    /// before the value can exist at all.
115    pub fn for_run(run_id: &HarnessRunId) -> Self {
116        let hex = run_id
117            .as_str()
118            .strip_prefix(HarnessRunId::PREFIX)
119            .expect("HarnessRunId always carries its own prefix");
120        Self(format!("{}{hex}", Self::PREFIX))
121    }
122
123    /// Inverts `for_run`: recovers the `HarnessRunId` a result reference
124    /// was derived from. Fails only if `self` was not produced by
125    /// `for_run` from a valid run id (wrong prefix or malformed hex body).
126    pub fn run_id(&self) -> Result<HarnessRunId, HarnessValidationError> {
127        let hex = self
128            .0
129            .strip_prefix(Self::PREFIX)
130            .ok_or(HarnessValidationError::InvalidOpaqueId { field: "result reference" })?;
131        HarnessRunId::new(format!("{}{hex}", HarnessRunId::PREFIX))
132    }
133}
134
135#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
136#[serde(transparent)]
137pub struct HarnessRevision(u64);
138
139impl HarnessRevision {
140    pub fn new(value: u64) -> Result<Self, HarnessValidationError> {
141        if value == 0 {
142            return Err(HarnessValidationError::ZeroRevision);
143        }
144        Ok(Self(value))
145    }
146
147    pub fn get(self) -> u64 {
148        self.0
149    }
150
151    pub fn validate(self) -> Result<(), HarnessValidationError> {
152        Self::new(self.0).map(|_| ())
153    }
154}
155
156impl<'de> Deserialize<'de> for HarnessRevision {
157    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
158    where
159        D: Deserializer<'de>,
160    {
161        Self::new(u64::deserialize(deserializer)?).map_err(serde::de::Error::custom)
162    }
163}
164
165#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
166#[serde(transparent)]
167pub struct HarnessSelectorV1(String);
168
169impl HarnessSelectorV1 {
170    pub fn new(value: impl Into<String>) -> Result<Self, HarnessValidationError> {
171        let value = value.into();
172        validate_selector(&value)?;
173        Ok(Self(value))
174    }
175
176    pub fn as_str(&self) -> &str {
177        &self.0
178    }
179
180    pub fn validate(&self) -> Result<(), HarnessValidationError> {
181        validate_selector(&self.0)
182    }
183}
184
185impl fmt::Debug for HarnessSelectorV1 {
186    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
187        formatter.debug_tuple("HarnessSelectorV1").field(&self.0).finish()
188    }
189}
190
191impl<'de> Deserialize<'de> for HarnessSelectorV1 {
192    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
193    where
194        D: Deserializer<'de>,
195    {
196        Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
197    }
198}
199
200#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
201#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
202pub enum HarnessActorV1 {
203    User { actor_id: HarnessSelectorV1 },
204    ParentRun { run_id: HarnessRunId },
205}
206
207impl HarnessActorV1 {
208    pub fn validate(&self) -> Result<(), HarnessValidationError> {
209        match self {
210            Self::User { actor_id } => actor_id.validate(),
211            Self::ParentRun { run_id } => run_id.validate(),
212        }
213    }
214}
215
216#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
217#[serde(rename_all = "kebab-case")]
218pub enum HarnessTaskStateV1 {
219    Backlog,
220    Ready,
221    Running,
222    Waiting,
223    Review,
224    Done,
225    Failed,
226    Cancelled,
227}
228
229#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
230#[serde(deny_unknown_fields)]
231pub struct HarnessTaskV1 {
232    pub task_id: HarnessTaskId,
233    pub revision: HarnessRevision,
234    pub title: String,
235    pub body: String,
236    pub creator: HarnessActorV1,
237    pub parent_task_id: Option<HarnessTaskId>,
238    pub dependencies: Vec<HarnessTaskId>,
239    pub state: HarnessTaskStateV1,
240    pub run_ids: Vec<HarnessRunId>,
241    pub result_refs: Vec<HarnessResultRef>,
242    pub artifact_refs: Vec<HarnessArtifactRef>,
243    pub created_at_unix_ms: u64,
244    pub updated_at_unix_ms: u64,
245}
246
247impl HarnessTaskV1 {
248    pub fn validate(&self) -> Result<(), HarnessValidationError> {
249        self.task_id.validate()?;
250        self.revision.validate()?;
251        validate_title(&self.title)?;
252        validate_body(&self.body)?;
253        self.creator.validate()?;
254        validate_timestamps(self.created_at_unix_ms, self.updated_at_unix_ms)?;
255        if self.parent_task_id.as_ref() == Some(&self.task_id) {
256            return Err(HarnessValidationError::SelfLink { field: "parent_task_id" });
257        }
258        if self.dependencies.iter().any(|task_id| task_id == &self.task_id) {
259            return Err(HarnessValidationError::SelfLink { field: "dependencies" });
260        }
261        validate_sorted_ids("dependencies", &self.dependencies, HARNESS_DEPENDENCIES_MAX)?;
262        validate_sorted_ids("run_ids", &self.run_ids, HARNESS_LINKS_MAX)?;
263        validate_sorted_ids("result_refs", &self.result_refs, HARNESS_RESULTS_MAX)?;
264        validate_sorted_ids("artifact_refs", &self.artifact_refs, HARNESS_ARTIFACTS_MAX)?;
265        Ok(())
266    }
267}
268
269#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
270#[serde(deny_unknown_fields)]
271pub struct HarnessOperatorAuthorityV1 {
272    pub operation_id: HarnessOperationId,
273    pub idempotency_ref: HarnessIdempotencyRef,
274    pub actor_id: HarnessSelectorV1,
275    pub now_unix_ms: u64,
276}
277
278impl HarnessOperatorAuthorityV1 {
279    pub fn validate(&self) -> Result<(), HarnessValidationError> {
280        self.operation_id.validate()?;
281        self.idempotency_ref.validate()?;
282        self.actor_id.validate()?;
283        if self.now_unix_ms == 0 {
284            return Err(HarnessValidationError::InvalidTimestamps);
285        }
286        Ok(())
287    }
288}
289
290#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
291#[serde(deny_unknown_fields)]
292pub struct HarnessCreateTaskRequestV1 {
293    pub authority: HarnessOperatorAuthorityV1,
294    pub task_id: HarnessTaskId,
295    pub title: String,
296    pub body: String,
297    pub parent_task_id: Option<HarnessTaskId>,
298    pub dependencies: Vec<HarnessTaskId>,
299    pub initial_state: HarnessTaskStateV1,
300}
301
302impl HarnessCreateTaskRequestV1 {
303    pub fn validate(&self) -> Result<(), HarnessValidationError> {
304        self.authority.validate()?;
305        self.task_id.validate()?;
306        validate_title(&self.title)?;
307        validate_body(&self.body)?;
308        if self.parent_task_id.as_ref() == Some(&self.task_id) {
309            return Err(HarnessValidationError::SelfLink { field: "parent_task_id" });
310        }
311        if self.dependencies.iter().any(|dependency| dependency == &self.task_id) {
312            return Err(HarnessValidationError::SelfLink { field: "dependencies" });
313        }
314        validate_sorted_ids("dependencies", &self.dependencies, HARNESS_DEPENDENCIES_MAX)
315    }
316}
317
318#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
319#[serde(deny_unknown_fields)]
320pub struct HarnessReplaceTaskRequestV1 {
321    pub authority: HarnessOperatorAuthorityV1,
322    pub task_id: HarnessTaskId,
323    pub expected_revision: HarnessRevision,
324    pub title: String,
325    pub body: String,
326    pub parent_task_id: Option<HarnessTaskId>,
327    pub dependencies: Vec<HarnessTaskId>,
328}
329
330impl HarnessReplaceTaskRequestV1 {
331    pub fn validate(&self) -> Result<(), HarnessValidationError> {
332        self.authority.validate()?;
333        self.task_id.validate()?;
334        self.expected_revision.validate()?;
335        validate_title(&self.title)?;
336        validate_body(&self.body)?;
337        if self.parent_task_id.as_ref() == Some(&self.task_id) {
338            return Err(HarnessValidationError::SelfLink { field: "parent_task_id" });
339        }
340        if self.dependencies.iter().any(|dependency| dependency == &self.task_id) {
341            return Err(HarnessValidationError::SelfLink { field: "dependencies" });
342        }
343        validate_sorted_ids("dependencies", &self.dependencies, HARNESS_DEPENDENCIES_MAX)
344    }
345}
346
347#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
348#[serde(deny_unknown_fields)]
349pub struct HarnessMoveTaskRequestV1 {
350    pub authority: HarnessOperatorAuthorityV1,
351    pub task_id: HarnessTaskId,
352    pub expected_revision: HarnessRevision,
353    pub state: HarnessTaskStateV1,
354}
355
356impl HarnessMoveTaskRequestV1 {
357    pub fn validate(&self) -> Result<(), HarnessValidationError> {
358        self.authority.validate()?;
359        self.task_id.validate()?;
360        self.expected_revision.validate()
361    }
362}
363
364#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
365#[serde(deny_unknown_fields)]
366pub struct HarnessCancelTaskRequestV1 {
367    pub authority: HarnessOperatorAuthorityV1,
368    pub task_id: HarnessTaskId,
369    pub expected_revision: HarnessRevision,
370}
371
372impl HarnessCancelTaskRequestV1 {
373    pub fn validate(&self) -> Result<(), HarnessValidationError> {
374        self.authority.validate()?;
375        self.task_id.validate()?;
376        self.expected_revision.validate()
377    }
378}
379
380#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
381#[serde(deny_unknown_fields)]
382pub struct HarnessRetryTaskRequestV1 {
383    pub authority: HarnessOperatorAuthorityV1,
384    pub task_id: HarnessTaskId,
385    pub expected_revision: HarnessRevision,
386}
387
388impl HarnessRetryTaskRequestV1 {
389    pub fn validate(&self) -> Result<(), HarnessValidationError> {
390        self.authority.validate()?;
391        self.task_id.validate()?;
392        self.expected_revision.validate()
393    }
394}
395
396/// The outcome of one `g4a_task_create` call (D5, Slice D): an agent creates
397/// a task only under a parent already inside its own subtree (the caller's
398/// own task by default -- the tool's contract), never system-wide the way
399/// the operator's own `HarnessCreateTaskRequestV1` can. `Created` names the
400/// minted id and its first revision; every other variant is a named
401/// refusal, never prose: each refusal is its own variant with its own typed
402/// fields rather than one `Refused { reason }` catch-all. `TitleInvalid` /
403/// `BodyInvalid` / `DependenciesInvalid` carry `why` -- the underlying
404/// `HarnessValidationError`'s own `Display` text for `InvalidTitle` /
405/// `InvalidBody` / (`SelfLink`, `CollectionTooLarge`, or
406/// `CollectionNotCanonical` on `dependencies`) respectively, verbatim.
407#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
408#[serde(tag = "outcome", rename_all = "kebab-case", deny_unknown_fields)]
409pub enum HarnessTaskCreateResultV1 {
410    Created { task_id: HarnessTaskId, revision: HarnessRevision },
411    ParentOutsideOwnSubtree { parent_task_id: HarnessTaskId, own_task_id: HarnessTaskId },
412    ParentTerminal { parent_task_id: HarnessTaskId, state: HarnessTaskStateV1 },
413    TitleInvalid { why: String },
414    BodyInvalid { why: String },
415    DependenciesInvalid { why: String },
416}
417
418impl HarnessTaskCreateResultV1 {
419    pub fn validate(&self) -> Result<(), HarnessValidationError> {
420        match self {
421            Self::Created { task_id, revision } => {
422                task_id.validate()?;
423                revision.validate()
424            }
425            Self::ParentOutsideOwnSubtree { parent_task_id, own_task_id } => {
426                parent_task_id.validate()?;
427                own_task_id.validate()
428            }
429            Self::ParentTerminal { parent_task_id, .. } => parent_task_id.validate(),
430            Self::TitleInvalid { why } | Self::BodyInvalid { why } | Self::DependenciesInvalid { why } => {
431                validate_task_result_why(why)
432            }
433        }
434    }
435}
436
437/// The outcome of one `g4a_task_move` call (D5, Slice D): restricted to a
438/// task inside the caller's own subtree, and never the task that governs
439/// the caller's own run -- a session never moves the task its own run is
440/// attributed to (scope discipline: an agent narrates its own subtree, it
441/// does not narrate itself). `Moved` names the resulting revision and both
442/// endpoints of the transition; every other variant is a named refusal in
443/// the same style `HarnessTaskCreateResultV1` establishes. `IllegalTransition`
444/// answers to the same shape of rule the operator's own
445/// `validate_operator_move` (`hatchery-harness-service`) enforces --
446/// `from != to`, no move out of a terminal state, `Done` only from
447/// `Review` -- without duplicating that function here: the service applies
448/// its own agent-scoped rule and reports the two states it rejected.
449///
450/// `TaskOwnedByLiveRun` names the rule commit 7799812's incident exposed:
451/// while a run is non-terminal, its lifecycle projection owns its task's
452/// state, and a caller-initiated move that would change it races a
453/// projection it cannot win cleanly rather than being told plainly it was
454/// never going to be allowed -- so it is refused by name, carrying the id
455/// of the run that holds the task, instead of racing `RevisionConflict` on
456/// a revision the run's own churn keeps moving out from under the caller.
457#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
458#[serde(tag = "outcome", rename_all = "kebab-case", deny_unknown_fields)]
459pub enum HarnessTaskMoveResultV1 {
460    Moved {
461        task_id: HarnessTaskId,
462        revision: HarnessRevision,
463        from: HarnessTaskStateV1,
464        to: HarnessTaskStateV1,
465    },
466    TaskIsOwn { task_id: HarnessTaskId },
467    TaskOutsideOwnSubtree { task_id: HarnessTaskId, own_task_id: HarnessTaskId },
468    IllegalTransition { task_id: HarnessTaskId, from: HarnessTaskStateV1, to: HarnessTaskStateV1 },
469    TaskOwnedByLiveRun { task_id: HarnessTaskId, run_id: HarnessRunId },
470    RevisionConflict { task_id: HarnessTaskId, expected: HarnessRevision, current: HarnessRevision },
471}
472
473impl HarnessTaskMoveResultV1 {
474    pub fn validate(&self) -> Result<(), HarnessValidationError> {
475        match self {
476            Self::Moved { task_id, revision, .. } => {
477                task_id.validate()?;
478                revision.validate()
479            }
480            Self::TaskIsOwn { task_id } => task_id.validate(),
481            Self::TaskOutsideOwnSubtree { task_id, own_task_id } => {
482                task_id.validate()?;
483                own_task_id.validate()
484            }
485            Self::IllegalTransition { task_id, .. } => task_id.validate(),
486            Self::TaskOwnedByLiveRun { task_id, run_id } => {
487                task_id.validate()?;
488                run_id.validate()
489            }
490            Self::RevisionConflict { task_id, expected, current } => {
491                task_id.validate()?;
492                expected.validate()?;
493                current.validate()
494            }
495        }
496    }
497}
498
499#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
500#[serde(rename_all = "kebab-case")]
501pub enum HarnessExecutionModeV1 {
502    Pty,
503    Inline,
504    Acp,
505}
506
507/// Wire mirror of `gate4agent_types::ApprovalLevel` -- this crate has no
508/// dependency on `gate4agent-types` (see the module doc: a bounded,
509/// privacy-minimized domain contract, not a re-export surface), so the four
510/// levels are named again here rather than imported. Keep the variant set
511/// and the `FullAuto` default in exact lockstep with `ApprovalLevel`'s own;
512/// the two are converted between at the one boundary that links both
513/// crates (`hatchery-harness-service`), never compared or serialized
514/// against each other directly.
515#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
516#[serde(rename_all = "kebab-case")]
517pub enum HarnessApprovalLevelV1 {
518    #[default]
519    FullAuto,
520    Moderate,
521    ReadOnly,
522    Unmanaged,
523}
524
525#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
526#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
527pub enum HarnessWorktreeIntentV1 {
528    Existing,
529    Managed { worktree_ref: HarnessSelectorV1 },
530    ManagedProfile {
531        profile_id: HarnessSelectorV1,
532        expected_profile_revision: HarnessSelectorV1,
533    },
534}
535
536impl HarnessWorktreeIntentV1 {
537    pub fn validate(&self) -> Result<(), HarnessValidationError> {
538        match self {
539            Self::Existing => Ok(()),
540            Self::Managed { worktree_ref } => worktree_ref.validate(),
541            Self::ManagedProfile {
542                profile_id,
543                expected_profile_revision,
544            } => {
545                profile_id.validate()?;
546                expected_profile_revision.validate()
547            }
548        }
549    }
550}
551
552#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
553#[serde(deny_unknown_fields)]
554pub struct HarnessRunIntentV1 {
555    pub node_id: HarnessSelectorV1,
556    pub workspace_id: HarnessSelectorV1,
557    pub worktree: HarnessWorktreeIntentV1,
558    pub provider_profile: HarnessSelectorV1,
559    pub mode: HarnessExecutionModeV1,
560    pub delivery_bundle: Option<HarnessSelectorV1>,
561    pub continuation: Option<HarnessSelectorV1>,
562}
563
564impl HarnessRunIntentV1 {
565    pub fn validate(&self) -> Result<(), HarnessValidationError> {
566        self.node_id.validate()?;
567        self.workspace_id.validate()?;
568        self.worktree.validate()?;
569        self.provider_profile.validate()?;
570        if let Some(delivery_bundle) = &self.delivery_bundle {
571            delivery_bundle.validate()?;
572        }
573        if let Some(continuation) = &self.continuation {
574            continuation.validate()?;
575            HarnessRunId::new(continuation.as_str())
576                .map_err(|_| HarnessValidationError::InvalidContinuationSelector)?;
577        }
578        Ok(())
579    }
580}
581
582impl HarnessRunIntentV1 {
583    /// Resolves continuation only as an exact source HarnessRunId. It is never
584    /// interpreted as a raw Node SpawnContextId.
585    pub fn continuation_source_run_id(
586        &self,
587    ) -> Result<Option<HarnessRunId>, HarnessValidationError> {
588        self.continuation.as_ref().map(|selector| {
589            HarnessRunId::new(selector.as_str())
590                .map_err(|_| HarnessValidationError::InvalidContinuationSelector)
591        }).transpose()
592    }
593}
594
595#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
596#[serde(deny_unknown_fields)]
597pub struct HarnessScheduleRequestV1 {
598    pub operation_id: HarnessOperationId,
599    pub idempotency_ref: HarnessIdempotencyRef,
600    pub actor: HarnessActorV1,
601    pub run_id: HarnessRunId,
602    pub parent_run_id: Option<HarnessRunId>,
603    pub intent: HarnessRunIntentV1,
604    pub now_unix_ms: u64,
605}
606
607#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
608#[serde(deny_unknown_fields)]
609pub struct HarnessScheduleNextRequestV1 {
610    pub authority: HarnessOperatorAuthorityV1,
611    pub plan_id: Option<HarnessSelectorV1>,
612}
613
614impl HarnessScheduleNextRequestV1 {
615    pub fn validate(&self) -> Result<(), HarnessValidationError> {
616        self.authority.validate()?;
617        if let Some(plan_id) = &self.plan_id { plan_id.validate()?; }
618        Ok(())
619    }
620}
621
622#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
623#[serde(deny_unknown_fields)]
624pub struct HarnessLaunchPlanRefV1 {
625    pub plan_id: HarnessSelectorV1,
626    pub revision: HarnessRevision,
627    pub digest: HarnessRequestDigest,
628}
629
630impl HarnessLaunchPlanRefV1 {
631    pub fn validate(&self) -> Result<(), HarnessValidationError> {
632        self.plan_id.validate()?;
633        self.revision.validate()?;
634        self.digest.validate()
635    }
636}
637
638#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
639#[serde(rename_all = "kebab-case")]
640pub enum HarnessLaunchAuthorityRefV1 {
641    OrdinaryOperator,
642    OperatorIssuance {
643        issuance_id: HarnessTaskLaunchIssuanceId,
644        revision: HarnessRevision,
645        digest: HarnessRequestDigest,
646    },
647}
648
649impl HarnessLaunchAuthorityRefV1 {
650    pub fn validate(&self) -> Result<(), HarnessValidationError> {
651        match self {
652            Self::OrdinaryOperator => Ok(()),
653            Self::OperatorIssuance {
654                issuance_id,
655                revision,
656                digest,
657            } => {
658                issuance_id.validate()?;
659                revision.validate()?;
660                digest.validate()
661            }
662        }
663    }
664}
665
666#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
667#[serde(deny_unknown_fields)]
668pub struct HarnessScheduledLaunchRefV2 {
669    pub plan: HarnessLaunchPlanRefV1,
670    pub authority: HarnessLaunchAuthorityRefV1,
671}
672
673impl HarnessScheduledLaunchRefV2 {
674    pub fn validate(&self) -> Result<(), HarnessValidationError> {
675        self.plan.validate()?;
676        self.authority.validate()
677    }
678}
679
680#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
681#[serde(deny_unknown_fields)]
682pub struct HarnessTaskLaunchIssuanceRefV1 {
683    pub issuance_id: HarnessTaskLaunchIssuanceId,
684    pub revision: HarnessRevision,
685    /// Supplied by the service's canonical issuance encoder. This protocol
686    /// validates the digest shape; the engine enforces exact reference equality.
687    pub digest: HarnessRequestDigest,
688}
689
690impl HarnessTaskLaunchIssuanceRefV1 {
691    pub fn validate(&self) -> Result<(), HarnessValidationError> {
692        self.issuance_id.validate()?;
693        self.revision.validate()?;
694        self.digest.validate()
695    }
696}
697
698#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
699#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
700pub enum HarnessTransferAuthorityRefV1 {
701    ParentGrant {
702        grant_id: SessionGrantId,
703        revision: HarnessRevision,
704    },
705    OperatorIssuance {
706        issuance: HarnessTaskLaunchIssuanceRefV1,
707    },
708}
709
710impl HarnessTransferAuthorityRefV1 {
711    pub fn validate(&self) -> Result<(), HarnessValidationError> {
712        match self {
713            Self::ParentGrant { grant_id, revision } => {
714                grant_id.validate()?;
715                revision.validate()
716            }
717            Self::OperatorIssuance { issuance } => issuance.validate(),
718        }
719    }
720}
721
722#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
723#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
724pub enum HarnessLaunchWorktreeSelectionV1 {
725    Existing,
726    Managed {
727        profile_id: HarnessSelectorV1,
728        expected_profile_revision: HarnessSelectorV1,
729    },
730}
731
732impl HarnessLaunchWorktreeSelectionV1 {
733    pub fn validate(&self) -> Result<(), HarnessValidationError> {
734        match self {
735            Self::Existing => Ok(()),
736            Self::Managed {
737                profile_id,
738                expected_profile_revision,
739            } => {
740                profile_id.validate()?;
741                expected_profile_revision.validate()
742            }
743        }
744    }
745}
746
747#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
748#[serde(deny_unknown_fields)]
749pub struct HarnessLaunchTargetSelectionV1 {
750    pub node_id: HarnessSelectorV1,
751    pub source_workspace_id: HarnessSelectorV1,
752    pub worktree: HarnessLaunchWorktreeSelectionV1,
753    pub provider_profile: HarnessSelectorV1,
754    pub mode: HarnessExecutionModeV1,
755}
756
757impl HarnessLaunchTargetSelectionV1 {
758    pub fn validate(&self) -> Result<(), HarnessValidationError> {
759        self.node_id.validate()?;
760        self.source_workspace_id.validate()?;
761        self.worktree.validate()?;
762        self.provider_profile.validate()
763    }
764}
765
766#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
767#[serde(deny_unknown_fields)]
768pub struct HarnessContextSourceSelectionV1 {
769    pub source_run_id: HarnessRunId,
770    pub source_run_revision: HarnessRevision,
771    pub observed_at_unix_ms: u64,
772    pub metadata_digest: HarnessRequestDigest,
773    pub node_id: HarnessSelectorV1,
774    pub node_incarnation: HarnessSelectorV1,
775    pub workspace_id: HarnessSelectorV1,
776    pub session_record_id: HarnessSelectorV1,
777    pub active_session: Option<HarnessRuntimeIdentityV1>,
778    pub message_count: u64,
779    pub message_count_exact: bool,
780    pub completed_turn_count: Option<u64>,
781    pub total_tokens: Option<u64>,
782    pub availability: HarnessContextSourceAvailabilityV1,
783    #[serde(default, skip_serializing_if = "Option::is_none")]
784    pub context_pack: Option<HarnessResolvedContextPackReceiptV1>,
785}
786
787#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
788#[serde(rename_all = "kebab-case")]
789pub enum HarnessContextSourceAvailabilityV1 { Live, Durable }
790
791impl HarnessContextSourceSelectionV1 {
792    pub fn validate(&self) -> Result<(), HarnessValidationError> {
793        self.source_run_id.validate()?;
794        self.source_run_revision.validate()?;
795        if self.observed_at_unix_ms == 0 {
796            return Err(HarnessValidationError::InvalidTimestamps);
797        }
798        self.metadata_digest.validate()?;
799        self.node_id.validate()?;
800        self.node_incarnation.validate()?;
801        self.workspace_id.validate()?;
802        self.session_record_id.validate()?;
803        if let Some(session) = &self.active_session { session.validate()?; }
804        match self.availability {
805            HarnessContextSourceAvailabilityV1::Live => {
806                if self.active_session.is_none() || self.context_pack.is_some() {
807                    return Err(HarnessValidationError::InvalidContextSourceCounts);
808                }
809            }
810            HarnessContextSourceAvailabilityV1::Durable => {
811                let Some(pack) = &self.context_pack else {
812                    return Err(HarnessValidationError::InvalidContextSourceCounts);
813                };
814                pack.validate()?;
815            }
816        }
817        // A `Live` source's counts are best-effort: the Node's own pack
818        // export never reads them (it asks the live session's adapter for
819        // its transcript directly), so a source whose history was never
820        // observed still carries the same "not observed" shape
821        // `HarnessRunContextSourceObservationV1::validate` already accepts
822        // -- `message_count: 0`, `message_count_exact: false`, no turn/token
823        // counts. Any other combination (an exact-zero count, a nonzero
824        // count claimed inexact-and-empty, `Durable`'s pack-backed count
825        // being zero, etc.) stays rejected below exactly as before.
826        let unobserved_live = self.availability == HarnessContextSourceAvailabilityV1::Live
827            && self.message_count == 0
828            && !self.message_count_exact
829            && self.completed_turn_count.is_none()
830            && self.total_tokens.is_none();
831        if !unobserved_live
832            && (self.message_count == 0
833                || self.completed_turn_count.is_some_and(|count| count > self.message_count))
834        {
835            return Err(HarnessValidationError::InvalidContextSourceCounts);
836        }
837        Ok(())
838    }
839}
840
841#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
842#[serde(rename_all = "kebab-case")]
843pub enum HarnessDeliveryComponentKindV1 {
844    Skill,
845    PluginManifest,
846    Prompt,
847    Instructions,
848    AgentDefinition,
849    Command,
850    File,
851    Template,
852    McpDeclaration,
853}
854
855#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
856#[serde(deny_unknown_fields)]
857pub struct HarnessDeliveryComponentCountV1 {
858    pub kind: HarnessDeliveryComponentKindV1,
859    pub workspace_count: u32,
860    pub session_count: u32,
861}
862
863impl HarnessDeliveryComponentCountV1 {
864    pub fn validate(&self) -> Result<(), HarnessValidationError> {
865        let count = self.workspace_count.checked_add(self.session_count)
866            .ok_or(HarnessValidationError::InvalidDeliveryComponentCount)?;
867        if count == 0 || count > HARNESS_DELIVERY_COMPONENTS_MAX {
868            return Err(HarnessValidationError::InvalidDeliveryComponentCount);
869        }
870        Ok(())
871    }
872}
873
874#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
875#[serde(deny_unknown_fields)]
876pub struct HarnessDeliveryBundleSelectionV1 {
877    pub bundle: HarnessDeliveryBundleV1,
878    pub component_counts: Vec<HarnessDeliveryComponentCountV1>,
879}
880
881impl HarnessDeliveryBundleSelectionV1 {
882    pub fn validate(&self) -> Result<(), HarnessValidationError> {
883        self.bundle.validate()?;
884        if self.component_counts.is_empty() {
885            return Err(HarnessValidationError::InvalidDeliveryComponentCount);
886        }
887        if self.component_counts.len() > HARNESS_DELIVERY_COMPONENT_KINDS_MAX {
888            return Err(HarnessValidationError::CollectionTooLarge {
889                field: "delivery component counts",
890                maximum: HARNESS_DELIVERY_COMPONENT_KINDS_MAX,
891            });
892        }
893        if self.component_counts.windows(2).any(|pair| pair[0].kind >= pair[1].kind) {
894            return Err(HarnessValidationError::CollectionNotCanonical {
895                field: "delivery component counts",
896            });
897        }
898        let mut total = 0_u32;
899        for component in &self.component_counts {
900            component.validate()?;
901            total = total.checked_add(component.workspace_count)
902                .and_then(|count| count.checked_add(component.session_count))
903                .ok_or(HarnessValidationError::InvalidDeliveryComponentCount)?;
904        }
905        if total > HARNESS_DELIVERY_COMPONENTS_MAX {
906            return Err(HarnessValidationError::InvalidDeliveryComponentCount);
907        }
908        Ok(())
909    }
910}
911
912#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
913#[serde(deny_unknown_fields)]
914pub struct HarnessTaskLaunchIssuanceV1 {
915    pub issuance_id: HarnessTaskLaunchIssuanceId,
916    pub revision: HarnessRevision,
917    /// Supplied by the service after canonical encoding of the bounded issuance.
918    /// No provider data or unbounded content participates in this record.
919    pub digest: HarnessRequestDigest,
920    pub task_id: HarnessTaskId,
921    pub task_revision: HarnessRevision,
922    pub plan: HarnessLaunchPlanRefV1,
923    pub target: HarnessLaunchTargetSelectionV1,
924    pub context_source: Option<HarnessContextSourceSelectionV1>,
925    pub delivery: Option<HarnessDeliveryBundleSelectionV1>,
926    pub policy_digest: HarnessRequestDigest,
927    pub created_at_unix_ms: u64,
928    pub updated_at_unix_ms: u64,
929}
930
931impl HarnessTaskLaunchIssuanceV1 {
932    pub fn reference(&self) -> HarnessTaskLaunchIssuanceRefV1 {
933        HarnessTaskLaunchIssuanceRefV1 {
934            issuance_id: self.issuance_id.clone(),
935            revision: self.revision,
936            digest: self.digest.clone(),
937        }
938    }
939
940    pub fn validate(&self) -> Result<(), HarnessValidationError> {
941        self.issuance_id.validate()?;
942        self.revision.validate()?;
943        self.digest.validate()?;
944        self.task_id.validate()?;
945        self.task_revision.validate()?;
946        self.plan.validate()?;
947        self.target.validate()?;
948        if let Some(context_source) = &self.context_source {
949            context_source.validate()?;
950        }
951        if let Some(delivery) = &self.delivery {
952            delivery.validate()?;
953        }
954        self.policy_digest.validate()?;
955        validate_timestamps(self.created_at_unix_ms, self.updated_at_unix_ms)
956    }
957}
958
959#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
960#[serde(rename_all = "kebab-case")]
961pub enum HarnessTaskReviewPolicyV1 {
962    OperatorReview,
963}
964
965#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
966#[serde(deny_unknown_fields)]
967pub struct HarnessTaskExecutionSpecInputV1 {
968    pub scheduled_launch: HarnessScheduledLaunchRefV2,
969    pub review_policy: HarnessTaskReviewPolicyV1,
970}
971
972impl HarnessTaskExecutionSpecInputV1 {
973    pub fn validate(&self) -> Result<(), HarnessValidationError> {
974        self.scheduled_launch.validate()
975    }
976}
977
978#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
979#[serde(deny_unknown_fields)]
980pub struct HarnessTaskExecutionSpecV1 {
981    pub execution_spec_id: HarnessExecutionSpecId,
982    pub revision: HarnessRevision,
983    pub task_id: HarnessTaskId,
984    pub scheduled_launch: HarnessScheduledLaunchRefV2,
985    pub scheduled_launch_digest: HarnessRequestDigest,
986    pub review_policy: HarnessTaskReviewPolicyV1,
987    pub created_at_unix_ms: u64,
988    pub updated_at_unix_ms: u64,
989}
990
991#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
992#[serde(deny_unknown_fields)]
993pub struct HarnessTaskExecutionSpecInputV2 {
994    pub launch_issuance: HarnessTaskLaunchIssuanceRefV1,
995    pub review_policy: HarnessTaskReviewPolicyV1,
996}
997
998impl HarnessTaskExecutionSpecInputV2 {
999    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1000        self.launch_issuance.validate()
1001    }
1002}
1003
1004#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1005#[serde(deny_unknown_fields)]
1006pub struct HarnessTaskExecutionSpecV2 {
1007    pub execution_spec_id: HarnessExecutionSpecId,
1008    pub revision: HarnessRevision,
1009    pub task_id: HarnessTaskId,
1010    pub launch_issuance: HarnessTaskLaunchIssuanceRefV1,
1011    pub review_policy: HarnessTaskReviewPolicyV1,
1012    pub created_at_unix_ms: u64,
1013    pub updated_at_unix_ms: u64,
1014}
1015
1016impl HarnessTaskExecutionSpecV2 {
1017    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1018        self.execution_spec_id.validate()?;
1019        self.revision.validate()?;
1020        self.task_id.validate()?;
1021        self.launch_issuance.validate()?;
1022        validate_timestamps(self.created_at_unix_ms, self.updated_at_unix_ms)
1023    }
1024}
1025
1026impl HarnessTaskExecutionSpecV1 {
1027    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1028        self.execution_spec_id.validate()?;
1029        self.revision.validate()?;
1030        self.task_id.validate()?;
1031        self.scheduled_launch.validate()?;
1032        self.scheduled_launch_digest.validate()?;
1033        validate_timestamps(self.created_at_unix_ms, self.updated_at_unix_ms)
1034    }
1035}
1036
1037#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1038#[serde(tag = "kind", content = "revision", rename_all = "kebab-case", deny_unknown_fields)]
1039pub enum HarnessExpectedExecutionSpecRevisionV1 {
1040    Absent,
1041    Exact(HarnessRevision),
1042}
1043
1044impl HarnessExpectedExecutionSpecRevisionV1 {
1045    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1046        match self {
1047            Self::Absent => Ok(()),
1048            Self::Exact(revision) => revision.validate(),
1049        }
1050    }
1051}
1052
1053#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1054#[serde(deny_unknown_fields)]
1055pub struct HarnessReplaceTaskExecutionSpecRequestV1 {
1056    pub authority: HarnessOperatorAuthorityV1,
1057    pub task_id: HarnessTaskId,
1058    pub expected_task_revision: HarnessRevision,
1059    pub expected_execution_spec_revision: HarnessExpectedExecutionSpecRevisionV1,
1060    pub spec: HarnessTaskExecutionSpecInputV1,
1061}
1062
1063impl HarnessReplaceTaskExecutionSpecRequestV1 {
1064    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1065        self.authority.validate()?;
1066        self.task_id.validate()?;
1067        self.expected_task_revision.validate()?;
1068        self.expected_execution_spec_revision.validate()?;
1069        self.spec.validate()
1070    }
1071}
1072
1073#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1074#[serde(deny_unknown_fields)]
1075pub struct HarnessStartTaskRequestV1 {
1076    pub authority: HarnessOperatorAuthorityV1,
1077    pub task_id: HarnessTaskId,
1078    pub expected_task_revision: HarnessRevision,
1079    pub expected_execution_spec_revision: HarnessRevision,
1080    pub expected_scheduled_launch_digest: HarnessRequestDigest,
1081}
1082
1083impl HarnessStartTaskRequestV1 {
1084    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1085        self.authority.validate()?;
1086        self.task_id.validate()?;
1087        self.expected_task_revision.validate()?;
1088        self.expected_execution_spec_revision.validate()?;
1089        self.expected_scheduled_launch_digest.validate()
1090    }
1091}
1092
1093impl HarnessScheduleRequestV1 {
1094    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1095        self.operation_id.validate()?;
1096        self.idempotency_ref.validate()?;
1097        self.actor.validate()?;
1098        self.run_id.validate()?;
1099        if self.parent_run_id.as_ref() == Some(&self.run_id) {
1100            return Err(HarnessValidationError::SelfLink { field: "parent_run_id" });
1101        }
1102        if let Some(parent_run_id) = &self.parent_run_id {
1103            parent_run_id.validate()?;
1104        }
1105        self.intent.validate()?;
1106        if self.now_unix_ms == 0 {
1107            return Err(HarnessValidationError::InvalidTimestamps);
1108        }
1109        Ok(())
1110    }
1111}
1112
1113#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1114#[serde(deny_unknown_fields)]
1115pub struct HarnessDispatchIntentV1 {
1116    pub task_id: HarnessTaskId,
1117    pub task_revision: HarnessRevision,
1118    pub run_id: HarnessRunId,
1119    pub run_revision: HarnessRevision,
1120    pub operation_id: HarnessOperationId,
1121    pub operation_revision: HarnessRevision,
1122    pub idempotency_ref: HarnessIdempotencyRef,
1123    pub parent_run_id: Option<HarnessRunId>,
1124    pub intent: HarnessRunIntentV1,
1125}
1126
1127impl HarnessDispatchIntentV1 {
1128    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1129        self.task_id.validate()?;
1130        self.task_revision.validate()?;
1131        self.run_id.validate()?;
1132        self.run_revision.validate()?;
1133        self.operation_id.validate()?;
1134        self.operation_revision.validate()?;
1135        self.idempotency_ref.validate()?;
1136        if self.parent_run_id.as_ref() == Some(&self.run_id) {
1137            return Err(HarnessValidationError::SelfLink { field: "parent_run_id" });
1138        }
1139        if let Some(parent_run_id) = &self.parent_run_id {
1140            parent_run_id.validate()?;
1141        }
1142        self.intent.validate()
1143    }
1144}
1145
1146#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1147#[serde(tag = "outcome", content = "dispatch", rename_all = "kebab-case")]
1148pub enum HarnessScheduleOutcomeV1 {
1149    Idle,
1150    Dispatch(HarnessDispatchIntentV1),
1151}
1152
1153#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1154#[serde(deny_unknown_fields)]
1155pub struct HarnessTaskStartOutcomeV1 {
1156    pub dispatch: HarnessDispatchIntentV1,
1157    pub replayed: bool,
1158}
1159
1160impl HarnessTaskStartOutcomeV1 {
1161    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1162        self.dispatch.validate()
1163    }
1164}
1165
1166#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1167#[serde(deny_unknown_fields)]
1168pub struct HarnessRuntimeIdentityV1 {
1169    pub instance_id: u64,
1170    pub generation: u64,
1171}
1172
1173impl HarnessRuntimeIdentityV1 {
1174    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1175        if self.instance_id == 0 || self.generation == 0 {
1176            return Err(HarnessValidationError::InvalidSessionIdentity);
1177        }
1178        Ok(())
1179    }
1180}
1181
1182#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1183#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
1184pub enum HarnessSessionIdentityV1 {
1185    Managed { record_id: HarnessSelectorV1, active_session: Option<HarnessRuntimeIdentityV1> },
1186    Inline { inline_ref: HarnessInlineRef },
1187}
1188
1189impl HarnessSessionIdentityV1 {
1190    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1191        match self {
1192            Self::Managed { record_id, active_session } => {
1193                record_id.validate()?;
1194                if let Some(active_session) = active_session {
1195                    active_session.validate()?;
1196                }
1197                Ok(())
1198            }
1199            Self::Inline { inline_ref } => inline_ref.validate(),
1200        }
1201    }
1202}
1203
1204#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1205#[serde(deny_unknown_fields)]
1206pub struct HarnessSessionBindingV1 {
1207    pub node_id: HarnessSelectorV1,
1208    pub node_incarnation: HarnessSelectorV1,
1209    pub workspace_id: HarnessSelectorV1,
1210    pub session: HarnessSessionIdentityV1,
1211}
1212
1213macro_rules! delivery_resource_id {
1214    ($name:ident, $label:literal) => {
1215        #[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
1216        #[serde(transparent)]
1217        pub struct $name(String);
1218
1219        impl $name {
1220            pub fn new(value: impl Into<String>) -> Result<Self, HarnessValidationError> {
1221                let value = value.into();
1222                validate_delivery_resource_id($label, &value)?;
1223                Ok(Self(value))
1224            }
1225
1226            pub fn as_str(&self) -> &str { &self.0 }
1227
1228            pub fn validate(&self) -> Result<(), HarnessValidationError> {
1229                validate_delivery_resource_id($label, &self.0)
1230            }
1231        }
1232
1233        impl fmt::Debug for $name {
1234            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1235                formatter.debug_tuple(stringify!($name)).field(&self.0).finish()
1236            }
1237        }
1238
1239        impl<'de> Deserialize<'de> for $name {
1240            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1241            where
1242                D: Deserializer<'de>,
1243            {
1244                Self::new(String::deserialize(deserializer)?)
1245                    .map_err(serde::de::Error::custom)
1246            }
1247        }
1248    };
1249}
1250
1251delivery_resource_id!(HarnessDeliveryBundleIdV1, "delivery bundle id");
1252delivery_resource_id!(HarnessDeliveryBundleRevisionV1, "delivery bundle revision");
1253
1254#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
1255#[serde(transparent)]
1256pub struct HarnessDeliveryBundleDigestV1(String);
1257
1258impl HarnessDeliveryBundleDigestV1 {
1259    pub fn new(value: impl Into<String>) -> Result<Self, HarnessValidationError> {
1260        let value = value.into();
1261        validate_delivery_digest(&value)?;
1262        Ok(Self(value))
1263    }
1264
1265    pub fn as_str(&self) -> &str { &self.0 }
1266
1267    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1268        validate_delivery_digest(&self.0)
1269    }
1270}
1271
1272impl fmt::Debug for HarnessDeliveryBundleDigestV1 {
1273    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1274        formatter.debug_tuple("HarnessDeliveryBundleDigestV1").field(&self.0).finish()
1275    }
1276}
1277
1278impl<'de> Deserialize<'de> for HarnessDeliveryBundleDigestV1 {
1279    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1280    where
1281        D: Deserializer<'de>,
1282    {
1283        Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
1284    }
1285}
1286
1287#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
1288#[serde(transparent)]
1289pub struct HarnessDeliveryManifestDigestV2(String);
1290
1291impl HarnessDeliveryManifestDigestV2 {
1292    pub fn new(value: impl Into<String>) -> Result<Self, HarnessValidationError> {
1293        let value = value.into();
1294        validate_delivery_digest(&value)?;
1295        Ok(Self(value))
1296    }
1297
1298    pub fn as_str(&self) -> &str { &self.0 }
1299
1300    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1301        validate_delivery_digest(&self.0)
1302    }
1303}
1304
1305impl fmt::Debug for HarnessDeliveryManifestDigestV2 {
1306    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1307        formatter.debug_tuple("HarnessDeliveryManifestDigestV2").field(&self.0).finish()
1308    }
1309}
1310
1311impl<'de> Deserialize<'de> for HarnessDeliveryManifestDigestV2 {
1312    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1313    where
1314        D: Deserializer<'de>,
1315    {
1316        Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
1317    }
1318}
1319
1320#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1321#[serde(deny_unknown_fields)]
1322pub struct HarnessDeliveryBundleV1 {
1323    pub selector: HarnessSelectorV1,
1324    pub bundle_id: HarnessDeliveryBundleIdV1,
1325    pub revision: HarnessDeliveryBundleRevisionV1,
1326    pub digest: HarnessDeliveryBundleDigestV1,
1327    pub manifest_digest: HarnessDeliveryManifestDigestV2,
1328}
1329
1330impl HarnessDeliveryBundleV1 {
1331    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1332        self.selector.validate()?;
1333        self.bundle_id.validate()?;
1334        self.revision.validate()?;
1335        self.digest.validate()
1336            .and_then(|_| self.manifest_digest.validate())
1337    }
1338}
1339
1340#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
1341#[serde(rename_all = "kebab-case")]
1342pub enum HarnessDeliveryStateV1 { Prepared, Staged, Committed }
1343
1344#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1345#[serde(deny_unknown_fields)]
1346pub struct HarnessDeliveryStageReceiptV1 {
1347    pub node_id: HarnessSelectorV1,
1348    pub node_incarnation: HarnessSelectorV1,
1349    pub workspace_id: HarnessSelectorV1,
1350    pub bundle: HarnessDeliveryBundleV1,
1351    pub staged_at_unix_ms: u64,
1352}
1353
1354impl HarnessDeliveryStageReceiptV1 {
1355    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1356        self.node_id.validate()?;
1357        self.node_incarnation.validate()?;
1358        self.workspace_id.validate()?;
1359        self.bundle.validate()?;
1360        if self.staged_at_unix_ms == 0 {
1361            return Err(HarnessValidationError::InvalidTimestamps);
1362        }
1363        Ok(())
1364    }
1365}
1366
1367#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1368#[serde(try_from = "HarnessDeliveryReceiptV1Wire", into = "HarnessDeliveryReceiptV1Wire")]
1369pub struct HarnessDeliveryReceiptV1 {
1370    pub receipt_ref: HarnessReceiptRef,
1371    pub delivery_ref: HarnessDeliveryRef,
1372    pub authority: HarnessTransferAuthorityRefV1,
1373    pub task_id: HarnessTaskId,
1374    pub run_id: HarnessRunId,
1375    pub operation_id: HarnessOperationId,
1376    pub binding: HarnessSessionBindingV1,
1377    pub bundle: HarnessDeliveryBundleV1,
1378    pub committed_at_unix_ms: u64,
1379}
1380
1381impl HarnessDeliveryReceiptV1 {
1382    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1383        self.receipt_ref.validate()?;
1384        self.delivery_ref.validate()?;
1385        self.authority.validate()?;
1386        self.task_id.validate()?;
1387        self.run_id.validate()?;
1388        self.operation_id.validate()?;
1389        self.binding.validate()?;
1390        self.bundle.validate()?;
1391        if self.committed_at_unix_ms == 0 {
1392            return Err(HarnessValidationError::InvalidTimestamps);
1393        }
1394        Ok(())
1395    }
1396}
1397
1398#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1399#[serde(try_from = "HarnessDeliveryV1Wire", into = "HarnessDeliveryV1Wire")]
1400pub struct HarnessDeliveryV1 {
1401    pub delivery_ref: HarnessDeliveryRef,
1402    pub revision: HarnessRevision,
1403    pub authority: HarnessTransferAuthorityRefV1,
1404    pub task_id: HarnessTaskId,
1405    pub run_id: HarnessRunId,
1406    pub operation_id: HarnessOperationId,
1407    pub bundle: HarnessDeliveryBundleV1,
1408    pub state: HarnessDeliveryStateV1,
1409    pub stage_receipt: Option<HarnessDeliveryStageReceiptV1>,
1410    pub receipt: Option<HarnessDeliveryReceiptV1>,
1411    pub created_at_unix_ms: u64,
1412    pub updated_at_unix_ms: u64,
1413}
1414
1415#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
1416#[serde(rename_all = "kebab-case")]
1417pub enum HarnessContinuationStateV1 {
1418    Prepared,
1419    Exporting,
1420    Exported,
1421    Bound,
1422    OutcomeUnknown,
1423    Expired,
1424}
1425
1426#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
1427#[serde(rename_all = "kebab-case")]
1428pub enum HarnessContinuationOutcomeUnknownReasonV1 {
1429    Transport,
1430    RouteMismatch,
1431    ReceiptMismatch,
1432    UnexpectedResponse,
1433}
1434
1435#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
1436#[serde(rename_all = "kebab-case")]
1437pub enum HarnessContinuationCleanupStateV1 { Retained, NotRequired }
1438
1439#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1440#[serde(deny_unknown_fields)]
1441pub struct HarnessContextPackLineageV1 {
1442    pub source_node_id: HarnessSelectorV1,
1443    pub source_workspace_id: HarnessSelectorV1,
1444    pub source_instance_id: u64,
1445    pub source_generation: u64,
1446    pub source_provider: HarnessSelectorV1,
1447}
1448
1449impl HarnessContextPackLineageV1 {
1450    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1451        self.source_node_id.validate()?;
1452        self.source_workspace_id.validate()?;
1453        self.source_provider.validate()?;
1454        if self.source_instance_id == 0 || self.source_generation == 0 {
1455            return Err(HarnessValidationError::InvalidSessionIdentity);
1456        }
1457        Ok(())
1458    }
1459}
1460
1461#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1462#[serde(deny_unknown_fields)]
1463pub struct HarnessResolvedContextPackReceiptV1 {
1464    pub id: HarnessSelectorV1,
1465    pub digest: String,
1466    pub lineage: HarnessContextPackLineageV1,
1467    pub source_message_count: u64,
1468    pub retained_message_count: u64,
1469    pub byte_len: u32,
1470    pub truncated: bool,
1471}
1472
1473impl HarnessResolvedContextPackReceiptV1 {
1474    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1475        self.id.validate()?;
1476        validate_context_digest(&self.digest)?;
1477        self.lineage.validate()?;
1478        if self.source_message_count == 0
1479            || self.retained_message_count == 0
1480            || self.retained_message_count > self.source_message_count
1481            || self.retained_message_count > HARNESS_CONTEXT_PACK_RETAINED_MESSAGES_MAX
1482            || self.byte_len == 0
1483            || self.byte_len > HARNESS_CONTEXT_PACK_MAX_BYTES
1484            || self.truncated != (self.source_message_count > self.retained_message_count)
1485        {
1486            return Err(HarnessValidationError::InvalidContextPackReceipt);
1487        }
1488        Ok(())
1489    }
1490}
1491
1492#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1493#[serde(try_from = "HarnessContinuationV1Wire", into = "HarnessContinuationV1Wire")]
1494pub struct HarnessContinuationV1 {
1495    pub continuation_ref: HarnessContinuationRef,
1496    pub receipt_ref: HarnessReceiptRef,
1497    pub revision: HarnessRevision,
1498    pub state: HarnessContinuationStateV1,
1499    pub authority: HarnessTransferAuthorityRefV1,
1500    pub source_run_id: HarnessRunId,
1501    pub target_run_id: HarnessRunId,
1502    pub operation_id: HarnessOperationId,
1503    pub node_id: HarnessSelectorV1,
1504    pub node_incarnation: HarnessSelectorV1,
1505    pub workspace_id: HarnessSelectorV1,
1506    pub source_provider: HarnessSelectorV1,
1507    pub source_binding: HarnessSessionBindingV1,
1508    pub context: Option<HarnessResolvedContextPackReceiptV1>,
1509    pub target_binding: Option<HarnessSessionBindingV1>,
1510    pub prepared_at_unix_ms: u64,
1511    pub exporting_at_unix_ms: Option<u64>,
1512    pub exported_at_unix_ms: Option<u64>,
1513    pub bound_at_unix_ms: Option<u64>,
1514    pub expired_at_unix_ms: Option<u64>,
1515    pub outcome_unknown_at_unix_ms: Option<u64>,
1516    pub outcome_unknown_reason: Option<HarnessContinuationOutcomeUnknownReasonV1>,
1517    pub cleanup_state: HarnessContinuationCleanupStateV1,
1518    pub created_at_unix_ms: u64,
1519    pub updated_at_unix_ms: u64,
1520}
1521
1522impl HarnessContinuationV1 {
1523    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1524        self.continuation_ref.validate()?;
1525        self.receipt_ref.validate()?;
1526        self.revision.validate()?;
1527        self.authority.validate()?;
1528        self.source_run_id.validate()?;
1529        self.target_run_id.validate()?;
1530        self.operation_id.validate()?;
1531        self.node_id.validate()?;
1532        self.node_incarnation.validate()?;
1533        self.workspace_id.validate()?;
1534        self.source_provider.validate()?;
1535        self.source_binding.validate()?;
1536        if let Some(context) = &self.context { context.validate()?; }
1537        if let Some(binding) = &self.target_binding { binding.validate()?; }
1538        validate_timestamps(self.created_at_unix_ms, self.updated_at_unix_ms)?;
1539        for (field, value) in [
1540            ("prepared_at_unix_ms", Some(self.prepared_at_unix_ms)),
1541            ("exporting_at_unix_ms", self.exporting_at_unix_ms),
1542            ("exported_at_unix_ms", self.exported_at_unix_ms),
1543            ("bound_at_unix_ms", self.bound_at_unix_ms),
1544            ("expired_at_unix_ms", self.expired_at_unix_ms),
1545            ("outcome_unknown_at_unix_ms", self.outcome_unknown_at_unix_ms),
1546        ] {
1547            validate_optional_timestamp(field, value, self.created_at_unix_ms, self.updated_at_unix_ms)?;
1548        }
1549        let exact_route = self.source_binding.node_id == self.node_id
1550            && self.source_binding.node_incarnation == self.node_incarnation
1551            && self.source_binding.workspace_id == self.workspace_id;
1552        let timestamp_order = self.exporting_at_unix_ms
1553            .is_none_or(|value| value >= self.prepared_at_unix_ms)
1554            && self.exported_at_unix_ms.is_none_or(|value| {
1555                self.exporting_at_unix_ms.is_some_and(|started| value >= started)
1556            })
1557            && self.bound_at_unix_ms.is_none_or(|value| {
1558                self.exported_at_unix_ms.is_some_and(|exported| value >= exported)
1559            })
1560            && self.outcome_unknown_at_unix_ms.is_none_or(|value| {
1561                self.exporting_at_unix_ms.is_some_and(|started| value >= started)
1562            })
1563            && self.expired_at_unix_ms
1564                .is_none_or(|value| value >= self.prepared_at_unix_ms);
1565        let state_fields = match self.state {
1566            HarnessContinuationStateV1::Prepared => self.context.is_none()
1567                && self.target_binding.is_none()
1568                && self.exporting_at_unix_ms.is_none()
1569                && self.exported_at_unix_ms.is_none()
1570                && self.bound_at_unix_ms.is_none()
1571                && self.expired_at_unix_ms.is_none()
1572                && self.outcome_unknown_at_unix_ms.is_none()
1573                && self.outcome_unknown_reason.is_none(),
1574            HarnessContinuationStateV1::Exporting => self.context.is_none()
1575                && self.target_binding.is_none()
1576                && self.exporting_at_unix_ms.is_some()
1577                && self.exported_at_unix_ms.is_none()
1578                && self.bound_at_unix_ms.is_none()
1579                && self.expired_at_unix_ms.is_none()
1580                && self.outcome_unknown_at_unix_ms.is_none()
1581                && self.outcome_unknown_reason.is_none(),
1582            HarnessContinuationStateV1::Exported => self.context.is_some()
1583                && self.target_binding.is_none()
1584                && self.exporting_at_unix_ms.is_some()
1585                && self.exported_at_unix_ms.is_some()
1586                && self.bound_at_unix_ms.is_none()
1587                && self.expired_at_unix_ms.is_none()
1588                && self.outcome_unknown_at_unix_ms.is_none()
1589                && self.outcome_unknown_reason.is_none(),
1590            HarnessContinuationStateV1::Bound => self.context.is_some()
1591                && self.target_binding.is_some()
1592                && self.exporting_at_unix_ms.is_some()
1593                && self.exported_at_unix_ms.is_some()
1594                && self.bound_at_unix_ms.is_some()
1595                && self.expired_at_unix_ms.is_none()
1596                && self.outcome_unknown_at_unix_ms.is_none()
1597                && self.outcome_unknown_reason.is_none(),
1598            HarnessContinuationStateV1::OutcomeUnknown => self.target_binding.is_none()
1599                && self.exporting_at_unix_ms.is_some()
1600                && self.exported_at_unix_ms.is_none()
1601                && self.bound_at_unix_ms.is_none()
1602                && self.expired_at_unix_ms.is_none()
1603                && self.outcome_unknown_at_unix_ms.is_some()
1604                && self.outcome_unknown_reason.is_some(),
1605            HarnessContinuationStateV1::Expired => self.target_binding.is_none()
1606                && self.bound_at_unix_ms.is_none()
1607                && self.expired_at_unix_ms.is_some()
1608                && self.outcome_unknown_at_unix_ms.is_none()
1609                && self.outcome_unknown_reason.is_none(),
1610        };
1611        if self.source_run_id == self.target_run_id
1612            || !exact_route
1613            || !timestamp_order
1614            || !state_fields
1615            || self.prepared_at_unix_ms != self.created_at_unix_ms
1616            || self.cleanup_state != HarnessContinuationCleanupStateV1::Retained
1617        {
1618            return Err(HarnessValidationError::InvalidContinuationLink);
1619        }
1620        if let Some(context) = &self.context {
1621            if context.lineage.source_node_id != self.node_id
1622                || context.lineage.source_workspace_id != self.workspace_id
1623                || context.lineage.source_provider != self.source_provider
1624            {
1625                return Err(HarnessValidationError::InvalidContinuationLink);
1626            }
1627        }
1628        if let Some(binding) = &self.target_binding {
1629            if binding.node_id != self.node_id
1630                || binding.node_incarnation != self.node_incarnation
1631            {
1632                return Err(HarnessValidationError::InvalidContinuationLink);
1633            }
1634        }
1635        Ok(())
1636    }
1637}
1638
1639impl HarnessDeliveryV1 {
1640    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1641        self.delivery_ref.validate()?;
1642        self.revision.validate()?;
1643        self.authority.validate()?;
1644        self.task_id.validate()?;
1645        self.run_id.validate()?;
1646        self.operation_id.validate()?;
1647        self.bundle.validate()?;
1648        if let Some(stage) = &self.stage_receipt { stage.validate()?; }
1649        if let Some(receipt) = &self.receipt { receipt.validate()?; }
1650        validate_timestamps(self.created_at_unix_ms, self.updated_at_unix_ms)?;
1651        let fields_match = match self.state {
1652            HarnessDeliveryStateV1::Prepared => {
1653                self.stage_receipt.is_none() && self.receipt.is_none()
1654            }
1655            HarnessDeliveryStateV1::Staged => {
1656                self.stage_receipt.is_some() && self.receipt.is_none()
1657            }
1658            HarnessDeliveryStateV1::Committed => {
1659                self.stage_receipt.is_some() && self.receipt.is_some()
1660            }
1661        };
1662        if !fields_match {
1663            return Err(HarnessValidationError::InvalidDeliveryStateFields);
1664        }
1665        if let Some(stage) = &self.stage_receipt {
1666            if stage.bundle != self.bundle
1667                || stage.staged_at_unix_ms < self.created_at_unix_ms
1668                || stage.staged_at_unix_ms > self.updated_at_unix_ms
1669            {
1670                return Err(HarnessValidationError::InvalidDeliveryLink);
1671            }
1672        }
1673        if let Some(receipt) = &self.receipt {
1674            let staged_at_unix_ms = self.stage_receipt.as_ref()
1675                .map(|stage| stage.staged_at_unix_ms)
1676                .ok_or(HarnessValidationError::InvalidDeliveryLink)?;
1677            if receipt.delivery_ref != self.delivery_ref
1678                || receipt.authority != self.authority
1679                || receipt.task_id != self.task_id
1680                || receipt.run_id != self.run_id
1681                || receipt.operation_id != self.operation_id
1682                || receipt.bundle != self.bundle
1683                || receipt.committed_at_unix_ms < staged_at_unix_ms
1684                || receipt.committed_at_unix_ms < self.created_at_unix_ms
1685                || receipt.committed_at_unix_ms > self.updated_at_unix_ms
1686            {
1687                return Err(HarnessValidationError::InvalidDeliveryLink);
1688            }
1689        }
1690        Ok(())
1691    }
1692}
1693
1694fn migrate_transfer_authority(
1695    authority: Option<HarnessTransferAuthorityRefV1>,
1696    grant_id: Option<SessionGrantId>,
1697    grant_revision: Option<HarnessRevision>,
1698) -> Result<HarnessTransferAuthorityRefV1, String> {
1699    match (authority, grant_id, grant_revision) {
1700        (Some(authority), None, None) => Ok(authority),
1701        (None, Some(grant_id), Some(revision)) => {
1702            Ok(HarnessTransferAuthorityRefV1::ParentGrant { grant_id, revision })
1703        }
1704        _ => Err("transfer authority must be exactly one canonical authority or one legacy grant pair".to_owned()),
1705    }
1706}
1707
1708#[derive(Clone, Debug, Deserialize, Serialize)]
1709#[serde(deny_unknown_fields)]
1710struct HarnessDeliveryReceiptV1Wire {
1711    receipt_ref: HarnessReceiptRef,
1712    delivery_ref: HarnessDeliveryRef,
1713    #[serde(skip_serializing_if = "Option::is_none")]
1714    authority: Option<HarnessTransferAuthorityRefV1>,
1715    #[serde(skip_serializing_if = "Option::is_none")]
1716    grant_id: Option<SessionGrantId>,
1717    #[serde(skip_serializing_if = "Option::is_none")]
1718    grant_revision: Option<HarnessRevision>,
1719    task_id: HarnessTaskId,
1720    run_id: HarnessRunId,
1721    operation_id: HarnessOperationId,
1722    binding: HarnessSessionBindingV1,
1723    bundle: HarnessDeliveryBundleV1,
1724    committed_at_unix_ms: u64,
1725}
1726
1727impl TryFrom<HarnessDeliveryReceiptV1Wire> for HarnessDeliveryReceiptV1 {
1728    type Error = String;
1729
1730    fn try_from(wire: HarnessDeliveryReceiptV1Wire) -> Result<Self, Self::Error> {
1731        Ok(Self {
1732            receipt_ref: wire.receipt_ref,
1733            delivery_ref: wire.delivery_ref,
1734            authority: migrate_transfer_authority(
1735                wire.authority,
1736                wire.grant_id,
1737                wire.grant_revision,
1738            )?,
1739            task_id: wire.task_id,
1740            run_id: wire.run_id,
1741            operation_id: wire.operation_id,
1742            binding: wire.binding,
1743            bundle: wire.bundle,
1744            committed_at_unix_ms: wire.committed_at_unix_ms,
1745        })
1746    }
1747}
1748
1749impl From<HarnessDeliveryReceiptV1> for HarnessDeliveryReceiptV1Wire {
1750    fn from(value: HarnessDeliveryReceiptV1) -> Self {
1751        Self {
1752            receipt_ref: value.receipt_ref,
1753            delivery_ref: value.delivery_ref,
1754            authority: Some(value.authority),
1755            grant_id: None,
1756            grant_revision: None,
1757            task_id: value.task_id,
1758            run_id: value.run_id,
1759            operation_id: value.operation_id,
1760            binding: value.binding,
1761            bundle: value.bundle,
1762            committed_at_unix_ms: value.committed_at_unix_ms,
1763        }
1764    }
1765}
1766
1767#[derive(Clone, Debug, Deserialize, Serialize)]
1768#[serde(deny_unknown_fields)]
1769struct HarnessDeliveryV1Wire {
1770    delivery_ref: HarnessDeliveryRef,
1771    revision: HarnessRevision,
1772    #[serde(skip_serializing_if = "Option::is_none")]
1773    authority: Option<HarnessTransferAuthorityRefV1>,
1774    #[serde(skip_serializing_if = "Option::is_none")]
1775    grant_id: Option<SessionGrantId>,
1776    #[serde(skip_serializing_if = "Option::is_none")]
1777    grant_revision: Option<HarnessRevision>,
1778    task_id: HarnessTaskId,
1779    run_id: HarnessRunId,
1780    operation_id: HarnessOperationId,
1781    bundle: HarnessDeliveryBundleV1,
1782    state: HarnessDeliveryStateV1,
1783    stage_receipt: Option<HarnessDeliveryStageReceiptV1>,
1784    receipt: Option<HarnessDeliveryReceiptV1>,
1785    created_at_unix_ms: u64,
1786    updated_at_unix_ms: u64,
1787}
1788
1789impl TryFrom<HarnessDeliveryV1Wire> for HarnessDeliveryV1 {
1790    type Error = String;
1791
1792    fn try_from(wire: HarnessDeliveryV1Wire) -> Result<Self, Self::Error> {
1793        Ok(Self {
1794            delivery_ref: wire.delivery_ref,
1795            revision: wire.revision,
1796            authority: migrate_transfer_authority(
1797                wire.authority,
1798                wire.grant_id,
1799                wire.grant_revision,
1800            )?,
1801            task_id: wire.task_id,
1802            run_id: wire.run_id,
1803            operation_id: wire.operation_id,
1804            bundle: wire.bundle,
1805            state: wire.state,
1806            stage_receipt: wire.stage_receipt,
1807            receipt: wire.receipt,
1808            created_at_unix_ms: wire.created_at_unix_ms,
1809            updated_at_unix_ms: wire.updated_at_unix_ms,
1810        })
1811    }
1812}
1813
1814impl From<HarnessDeliveryV1> for HarnessDeliveryV1Wire {
1815    fn from(value: HarnessDeliveryV1) -> Self {
1816        Self {
1817            delivery_ref: value.delivery_ref,
1818            revision: value.revision,
1819            authority: Some(value.authority),
1820            grant_id: None,
1821            grant_revision: None,
1822            task_id: value.task_id,
1823            run_id: value.run_id,
1824            operation_id: value.operation_id,
1825            bundle: value.bundle,
1826            state: value.state,
1827            stage_receipt: value.stage_receipt,
1828            receipt: value.receipt,
1829            created_at_unix_ms: value.created_at_unix_ms,
1830            updated_at_unix_ms: value.updated_at_unix_ms,
1831        }
1832    }
1833}
1834
1835#[derive(Clone, Debug, Deserialize, Serialize)]
1836#[serde(deny_unknown_fields)]
1837struct HarnessContinuationV1Wire {
1838    continuation_ref: HarnessContinuationRef,
1839    receipt_ref: HarnessReceiptRef,
1840    revision: HarnessRevision,
1841    state: HarnessContinuationStateV1,
1842    #[serde(skip_serializing_if = "Option::is_none")]
1843    authority: Option<HarnessTransferAuthorityRefV1>,
1844    #[serde(skip_serializing_if = "Option::is_none")]
1845    grant_id: Option<SessionGrantId>,
1846    #[serde(skip_serializing_if = "Option::is_none")]
1847    grant_revision: Option<HarnessRevision>,
1848    source_run_id: HarnessRunId,
1849    target_run_id: HarnessRunId,
1850    operation_id: HarnessOperationId,
1851    node_id: HarnessSelectorV1,
1852    node_incarnation: HarnessSelectorV1,
1853    workspace_id: HarnessSelectorV1,
1854    source_provider: HarnessSelectorV1,
1855    source_binding: HarnessSessionBindingV1,
1856    context: Option<HarnessResolvedContextPackReceiptV1>,
1857    target_binding: Option<HarnessSessionBindingV1>,
1858    prepared_at_unix_ms: u64,
1859    exporting_at_unix_ms: Option<u64>,
1860    exported_at_unix_ms: Option<u64>,
1861    bound_at_unix_ms: Option<u64>,
1862    expired_at_unix_ms: Option<u64>,
1863    outcome_unknown_at_unix_ms: Option<u64>,
1864    outcome_unknown_reason: Option<HarnessContinuationOutcomeUnknownReasonV1>,
1865    cleanup_state: HarnessContinuationCleanupStateV1,
1866    created_at_unix_ms: u64,
1867    updated_at_unix_ms: u64,
1868}
1869
1870impl TryFrom<HarnessContinuationV1Wire> for HarnessContinuationV1 {
1871    type Error = String;
1872
1873    fn try_from(wire: HarnessContinuationV1Wire) -> Result<Self, Self::Error> {
1874        Ok(Self {
1875            continuation_ref: wire.continuation_ref,
1876            receipt_ref: wire.receipt_ref,
1877            revision: wire.revision,
1878            state: wire.state,
1879            authority: migrate_transfer_authority(
1880                wire.authority,
1881                wire.grant_id,
1882                wire.grant_revision,
1883            )?,
1884            source_run_id: wire.source_run_id,
1885            target_run_id: wire.target_run_id,
1886            operation_id: wire.operation_id,
1887            node_id: wire.node_id,
1888            node_incarnation: wire.node_incarnation,
1889            workspace_id: wire.workspace_id,
1890            source_provider: wire.source_provider,
1891            source_binding: wire.source_binding,
1892            context: wire.context,
1893            target_binding: wire.target_binding,
1894            prepared_at_unix_ms: wire.prepared_at_unix_ms,
1895            exporting_at_unix_ms: wire.exporting_at_unix_ms,
1896            exported_at_unix_ms: wire.exported_at_unix_ms,
1897            bound_at_unix_ms: wire.bound_at_unix_ms,
1898            expired_at_unix_ms: wire.expired_at_unix_ms,
1899            outcome_unknown_at_unix_ms: wire.outcome_unknown_at_unix_ms,
1900            outcome_unknown_reason: wire.outcome_unknown_reason,
1901            cleanup_state: wire.cleanup_state,
1902            created_at_unix_ms: wire.created_at_unix_ms,
1903            updated_at_unix_ms: wire.updated_at_unix_ms,
1904        })
1905    }
1906}
1907
1908impl From<HarnessContinuationV1> for HarnessContinuationV1Wire {
1909    fn from(value: HarnessContinuationV1) -> Self {
1910        Self {
1911            continuation_ref: value.continuation_ref,
1912            receipt_ref: value.receipt_ref,
1913            revision: value.revision,
1914            state: value.state,
1915            authority: Some(value.authority),
1916            grant_id: None,
1917            grant_revision: None,
1918            source_run_id: value.source_run_id,
1919            target_run_id: value.target_run_id,
1920            operation_id: value.operation_id,
1921            node_id: value.node_id,
1922            node_incarnation: value.node_incarnation,
1923            workspace_id: value.workspace_id,
1924            source_provider: value.source_provider,
1925            source_binding: value.source_binding,
1926            context: value.context,
1927            target_binding: value.target_binding,
1928            prepared_at_unix_ms: value.prepared_at_unix_ms,
1929            exporting_at_unix_ms: value.exporting_at_unix_ms,
1930            exported_at_unix_ms: value.exported_at_unix_ms,
1931            bound_at_unix_ms: value.bound_at_unix_ms,
1932            expired_at_unix_ms: value.expired_at_unix_ms,
1933            outcome_unknown_at_unix_ms: value.outcome_unknown_at_unix_ms,
1934            outcome_unknown_reason: value.outcome_unknown_reason,
1935            cleanup_state: value.cleanup_state,
1936            created_at_unix_ms: value.created_at_unix_ms,
1937            updated_at_unix_ms: value.updated_at_unix_ms,
1938        }
1939    }
1940}
1941
1942impl HarnessSessionBindingV1 {
1943    pub fn validate(&self) -> Result<(), HarnessValidationError> {
1944        self.node_id.validate()?;
1945        self.node_incarnation.validate()?;
1946        self.workspace_id.validate()?;
1947        self.session.validate()
1948    }
1949}
1950
1951#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
1952#[serde(rename_all = "kebab-case")]
1953pub enum HarnessRunLifecycleV1 {
1954    Requested,
1955    Preparing,
1956    Dispatching,
1957    OutcomeUnknown,
1958    Running,
1959    Waiting,
1960    Completed,
1961    Failed,
1962    Cancelled,
1963}
1964
1965/// The two, and only two, outcomes `g4a_run_finish` accepts (measured
1966/// 2026-09-09: the agent-facing surface has no verb a session can use to
1967/// report its own work finished, so nothing it does ever reaches `Done` --
1968/// see this type's own call site, `HarnessReadRequestV1::RunFinish`, for the
1969/// full account). `Done` and `Failed` name a definitive self-report, never a
1970/// partial or in-progress state -- there is no third value, by design: a
1971/// session is either closing its own run as succeeded or as failed, not
1972/// asking for anything in between.
1973#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1974#[serde(rename_all = "kebab-case")]
1975pub enum HarnessRunFinishOutcomeV1 {
1976    Done,
1977    Failed,
1978}
1979
1980/// The outcome of one `g4a_run_finish` call: resolves the caller's OWN run
1981/// from its grant binding (`actor_run_id`) -- there is no run-id argument,
1982/// so a session can never finish another session's run. `Finished` names
1983/// the run/task pair `commit_lifecycle_projection` (`gate4agent-harness-
1984/// service`) actually transitioned and the outcome it recorded; `task_id`
1985/// is the run's own task, landed in `Review` (never `Done` -- the review
1986/// gate a session cannot skip) for `done`, or in `Failed` for `failed`.
1987/// `AlreadyFinished` is the same "named refusal, never a silent no-op"
1988/// discipline `HarnessTaskMoveResultV1::RevisionConflict` established for
1989/// `g4a_task_move`: a second `g4a_run_finish` call against a run that is no
1990/// longer `Running`/`Waiting` is refused by name, carrying the lifecycle it
1991/// actually found, rather than answering with the same `Finished` shape a
1992/// first, effective call gets.
1993#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1994#[serde(tag = "outcome", rename_all = "kebab-case", deny_unknown_fields)]
1995pub enum HarnessRunFinishResultV1 {
1996    Finished {
1997        run_id: HarnessRunId,
1998        task_id: HarnessTaskId,
1999        result: HarnessRunFinishOutcomeV1,
2000    },
2001    AlreadyFinished {
2002        run_id: HarnessRunId,
2003        lifecycle: HarnessRunLifecycleV1,
2004    },
2005}
2006
2007impl HarnessRunFinishResultV1 {
2008    pub fn validate(&self) -> Result<(), HarnessValidationError> {
2009        match self {
2010            Self::Finished { run_id, task_id, .. } => {
2011                run_id.validate()?;
2012                task_id.validate()
2013            }
2014            Self::AlreadyFinished { run_id, .. } => run_id.validate(),
2015        }
2016    }
2017}
2018
2019#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
2020#[serde(rename_all = "kebab-case")]
2021pub enum HarnessResultDispositionV1 {
2022    Succeeded,
2023    Failed,
2024    Cancelled,
2025    Detached,
2026}
2027
2028#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
2029#[serde(rename_all = "kebab-case")]
2030pub enum HarnessFailureCategoryV1 {
2031    Validation,
2032    Conflict,
2033    PermissionDenied,
2034    TargetUnavailable,
2035    Transport,
2036    Timeout,
2037    Rejected,
2038    Internal,
2039}
2040
2041#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2042#[serde(deny_unknown_fields)]
2043pub struct HarnessFailureV1 {
2044    pub category: HarnessFailureCategoryV1,
2045    pub retryable: bool,
2046}
2047
2048impl HarnessFailureV1 {
2049    pub fn validate(&self) -> Result<(), HarnessValidationError> {
2050        Ok(())
2051    }
2052}
2053
2054/// Mirror of harness-api's `HarnessGitStatusCodeV1` (field-for-field), kept
2055/// as a narrow, intentional duplication forced by crate layering:
2056/// `hatchery-harness-protocol` has no dependency on `hatchery-harness-api`.
2057#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2058#[serde(rename_all = "kebab-case")]
2059pub enum HarnessRunGitStatusCodeV1 {
2060    Unmodified,
2061    Added,
2062    Modified,
2063    Deleted,
2064    Renamed,
2065    Copied,
2066    Unmerged,
2067    Untracked,
2068    Ignored,
2069    TypeChanged,
2070}
2071
2072#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2073#[serde(deny_unknown_fields)]
2074pub struct HarnessRunGitStatusEntryV1 {
2075    pub index_status: HarnessRunGitStatusCodeV1,
2076    pub worktree_status: HarnessRunGitStatusCodeV1,
2077    pub path: String,
2078    pub previous_path: Option<String>,
2079}
2080
2081impl HarnessRunGitStatusEntryV1 {
2082    pub fn validate(&self) -> Result<(), HarnessValidationError> {
2083        validate_run_git_text(&self.path, HARNESS_RUN_GIT_PATH_MAX_BYTES, true)?;
2084        if let Some(previous_path) = &self.previous_path {
2085            validate_run_git_text(previous_path, HARNESS_RUN_GIT_PATH_MAX_BYTES, true)?;
2086            if previous_path == &self.path {
2087                return Err(HarnessValidationError::InvalidGitSummary);
2088            }
2089        }
2090        let renamed_or_copied = matches!(
2091            self.index_status,
2092            HarnessRunGitStatusCodeV1::Renamed | HarnessRunGitStatusCodeV1::Copied
2093        ) || matches!(
2094            self.worktree_status,
2095            HarnessRunGitStatusCodeV1::Renamed | HarnessRunGitStatusCodeV1::Copied
2096        );
2097        if self.previous_path.is_some() != renamed_or_copied {
2098            return Err(HarnessValidationError::InvalidGitSummary);
2099        }
2100        Ok(())
2101    }
2102}
2103
2104#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2105#[serde(deny_unknown_fields)]
2106pub struct HarnessRunGitCommitSummaryV1 {
2107    /// 40 or 64 lowercase hex characters — validated inline, no new newtype
2108    /// (unlike harness-api's `HarnessGitObjectIdV1`; see crate-layering note).
2109    pub id: String,
2110    pub summary: String,
2111}
2112
2113impl HarnessRunGitCommitSummaryV1 {
2114    pub fn validate(&self) -> Result<(), HarnessValidationError> {
2115        validate_git_commit_id(&self.id)?;
2116        validate_run_git_text(&self.summary, HARNESS_RUN_GIT_SUMMARY_MAX_BYTES, false)
2117    }
2118}
2119
2120#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2121#[serde(deny_unknown_fields)]
2122pub struct HarnessRunGitSummaryV1 {
2123    pub is_repository: bool,
2124    pub branch: Option<String>,
2125    pub status: Vec<HarnessRunGitStatusEntryV1>,
2126    pub recent_commits: Vec<HarnessRunGitCommitSummaryV1>,
2127    pub truncated: bool,
2128}
2129
2130impl HarnessRunGitSummaryV1 {
2131    pub fn validate(&self) -> Result<(), HarnessValidationError> {
2132        if self.status.len() > HARNESS_RUN_GIT_STATUS_ENTRIES_MAX
2133            || self.recent_commits.len() > HARNESS_RUN_GIT_RECENT_COMMITS_MAX
2134            || !self.is_repository
2135                && (self.branch.is_some()
2136                    || !self.status.is_empty()
2137                    || !self.recent_commits.is_empty())
2138        {
2139            return Err(HarnessValidationError::InvalidGitSummary);
2140        }
2141        if let Some(branch) = &self.branch {
2142            validate_run_git_text(branch, HARNESS_RUN_GIT_BRANCH_MAX_BYTES, true)?;
2143        }
2144        for status in &self.status {
2145            status.validate()?;
2146        }
2147        if self.status.windows(2).any(|pair| pair[0].path >= pair[1].path) {
2148            return Err(HarnessValidationError::InvalidGitSummary);
2149        }
2150        for commit in &self.recent_commits {
2151            commit.validate()?;
2152        }
2153        if self.recent_commits.iter().enumerate().any(|(index, commit)| {
2154            self.recent_commits[..index].iter().any(|existing| existing.id == commit.id)
2155        }) {
2156            return Err(HarnessValidationError::InvalidGitSummary);
2157        }
2158        Ok(())
2159    }
2160}
2161
2162/// Terminal outcome of one background git-facts capture attempt against a
2163/// run's workspace. `Unavailable` folds every non-success cause (Node
2164/// unreachable, route mismatch, deadline, rejection) into one permanent,
2165/// non-retried terminal state — see the A3 design (§5.2/§9 risk 2).
2166#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2167#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
2168pub enum HarnessRunGitFactsOutcomeV1 {
2169    Captured(HarnessRunGitSummaryV1),
2170    Unavailable,
2171}
2172
2173#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2174#[serde(deny_unknown_fields)]
2175pub struct HarnessRunGitFactsV1 {
2176    pub captured_at_unix_ms: u64,
2177    pub outcome: HarnessRunGitFactsOutcomeV1,
2178}
2179
2180impl HarnessRunGitFactsV1 {
2181    pub fn validate(&self) -> Result<(), HarnessValidationError> {
2182        if let HarnessRunGitFactsOutcomeV1::Captured(summary) = &self.outcome {
2183            summary.validate()?;
2184        }
2185        Ok(())
2186    }
2187}
2188
2189#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2190#[serde(deny_unknown_fields)]
2191pub struct HarnessRunV1 {
2192    pub run_id: HarnessRunId,
2193    pub revision: HarnessRevision,
2194    pub parent_run_id: Option<HarnessRunId>,
2195    pub task_id: HarnessTaskId,
2196    pub operation_id: HarnessOperationId,
2197    pub intent: HarnessRunIntentV1,
2198    pub delivery_receipt: Option<HarnessReceiptRef>,
2199    pub continuation_receipt: Option<HarnessReceiptRef>,
2200    #[serde(default, skip_serializing_if = "Option::is_none")]
2201    pub context_pack: Option<HarnessResolvedContextPackReceiptV1>,
2202    #[serde(default, skip_serializing_if = "Option::is_none")]
2203    pub git_facts: Option<HarnessRunGitFactsV1>,
2204    pub binding: Option<HarnessSessionBindingV1>,
2205    pub lifecycle: HarnessRunLifecycleV1,
2206    pub result_disposition: Option<HarnessResultDispositionV1>,
2207    pub failure: Option<HarnessFailureV1>,
2208    pub created_at_unix_ms: u64,
2209    pub updated_at_unix_ms: u64,
2210}
2211
2212impl HarnessRunV1 {
2213    pub fn validate(&self) -> Result<(), HarnessValidationError> {
2214        self.run_id.validate()?;
2215        self.revision.validate()?;
2216        if self.parent_run_id.as_ref() == Some(&self.run_id) {
2217            return Err(HarnessValidationError::SelfLink { field: "parent_run_id" });
2218        }
2219        self.task_id.validate()?;
2220        self.operation_id.validate()?;
2221        self.intent.validate()?;
2222        if let Some(receipt) = &self.delivery_receipt { receipt.validate()?; }
2223        if let Some(receipt) = &self.continuation_receipt { receipt.validate()?; }
2224        if let Some(binding) = &self.binding {
2225            binding.validate()?;
2226            let workspace_matches = match &self.intent.worktree {
2227                HarnessWorktreeIntentV1::ManagedProfile { .. } => {
2228                    binding.workspace_id != self.intent.workspace_id
2229                }
2230                HarnessWorktreeIntentV1::Existing
2231                | HarnessWorktreeIntentV1::Managed { .. } => {
2232                    binding.workspace_id == self.intent.workspace_id
2233                }
2234            };
2235            if binding.node_id != self.intent.node_id || !workspace_matches {
2236                return Err(HarnessValidationError::BindingIntentMismatch);
2237            }
2238        }
2239        if let Some(pack) = &self.context_pack {
2240            pack.validate()?;
2241            let Some(binding) = &self.binding else {
2242                return Err(HarnessValidationError::MissingFieldForState { field: "context_pack" });
2243            };
2244            let managed = matches!(&binding.session, HarnessSessionIdentityV1::Managed { .. });
2245            if !managed
2246                || pack.lineage.source_node_id != binding.node_id
2247                || pack.lineage.source_workspace_id != binding.workspace_id
2248                || pack.lineage.source_provider != self.intent.provider_profile
2249            {
2250                return Err(HarnessValidationError::InvalidContextPackReceipt);
2251            }
2252        }
2253        if let Some(facts) = &self.git_facts {
2254            facts.validate()?;
2255            if self.binding.is_none() {
2256                return Err(HarnessValidationError::MissingFieldForState { field: "git_facts" });
2257            }
2258        }
2259        if let Some(failure) = &self.failure { failure.validate()?; }
2260        validate_timestamps(self.created_at_unix_ms, self.updated_at_unix_ms)?;
2261
2262        let needs_binding = matches!(self.lifecycle, HarnessRunLifecycleV1::Running | HarnessRunLifecycleV1::Waiting | HarnessRunLifecycleV1::Completed);
2263        if needs_binding && self.binding.is_none() {
2264            return Err(HarnessValidationError::MissingFieldForState { field: "binding" });
2265        }
2266        if matches!(
2267            self.lifecycle,
2268            HarnessRunLifecycleV1::Requested
2269                | HarnessRunLifecycleV1::Preparing
2270                | HarnessRunLifecycleV1::Dispatching
2271                | HarnessRunLifecycleV1::OutcomeUnknown
2272        ) && self.binding.is_some()
2273        {
2274            return Err(HarnessValidationError::InvalidBindingForState);
2275        }
2276        if matches!(self.lifecycle, HarnessRunLifecycleV1::Running | HarnessRunLifecycleV1::Waiting)
2277            && matches!(
2278                self.binding.as_ref().map(|binding| &binding.session),
2279                Some(HarnessSessionIdentityV1::Managed {
2280                    active_session: None,
2281                    ..
2282                })
2283            )
2284        {
2285            return Err(HarnessValidationError::MissingFieldForState {
2286                field: "binding.active_session",
2287            });
2288        }
2289        let disposition_valid = match self.lifecycle {
2290            HarnessRunLifecycleV1::Completed => matches!(self.result_disposition, Some(HarnessResultDispositionV1::Succeeded | HarnessResultDispositionV1::Detached)),
2291            HarnessRunLifecycleV1::Failed => self.result_disposition == Some(HarnessResultDispositionV1::Failed),
2292            HarnessRunLifecycleV1::Cancelled => self.result_disposition == Some(HarnessResultDispositionV1::Cancelled),
2293            _ => self.result_disposition.is_none(),
2294        };
2295        if !disposition_valid {
2296            return Err(HarnessValidationError::InvalidResultDisposition);
2297        }
2298        if matches!(self.lifecycle, HarnessRunLifecycleV1::Failed) != self.failure.is_some() {
2299            return Err(HarnessValidationError::InvalidFailureForState);
2300        }
2301        Ok(())
2302    }
2303}
2304
2305#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
2306#[serde(rename_all = "kebab-case")]
2307pub enum SessionGrantStateV1 { Active, Revoked }
2308
2309#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
2310#[serde(rename_all = "kebab-case")]
2311pub enum HarnessMonitoringVisibilityV1 { None, Summary, Detail, Timeline }
2312
2313#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
2314#[serde(rename_all = "kebab-case")]
2315/// Entity-local read authority rooted at the grant's actor run.
2316///
2317/// `SelfOnly` means the actor run itself (or an entity directly attributed to
2318/// it); `Descendants` includes the actor run and its run-tree descendants.
2319/// Scopes for tasks, runs, and operations are independent and never widen one
2320/// another through stored cross-entity links.
2321pub enum HarnessEntityReadScopeV1 { None, SelfOnly, Descendants }
2322
2323impl Default for HarnessEntityReadScopeV1 {
2324    fn default() -> Self { Self::None }
2325}
2326
2327#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
2328#[serde(deny_unknown_fields)]
2329/// Independent object-level read scopes. A visible object's references do not
2330/// themselves authorize reading the referenced objects.
2331pub struct HarnessReadPermissionsV1 {
2332    pub tasks: HarnessEntityReadScopeV1,
2333    pub runs: HarnessEntityReadScopeV1,
2334    pub operations: HarnessEntityReadScopeV1,
2335}
2336
2337impl HarnessReadPermissionsV1 {
2338    pub fn validate(&self) -> Result<(), HarnessValidationError> { Ok(()) }
2339}
2340
2341/// `create`/`mutate` gate `g4a_task_create`/`g4a_task_move` (D5, Slice D:
2342/// an agent's own subtree only, never system-wide). `#[serde(default)]` on
2343/// both so a grant persisted before Slice D decodes with `create: false`,
2344/// `mutate: false` rather than failing closed on the whole record -- the
2345/// rule since 2026-09-05: a persisted type gains a field only behind
2346/// `#[serde(default)]`, proven by a test deserializing the pre-Slice-D
2347/// shape (the same discipline `read_permissions` already established).
2348#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2349#[serde(deny_unknown_fields)]
2350pub struct HarnessTaskPermissionsV1 {
2351    pub read: bool,
2352    #[serde(default)]
2353    pub create: bool,
2354    #[serde(default)]
2355    pub mutate: bool,
2356    pub request_run: bool,
2357}
2358
2359impl HarnessTaskPermissionsV1 { pub fn validate(&self) -> Result<(), HarnessValidationError> { Ok(()) } }
2360
2361#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2362#[serde(deny_unknown_fields)]
2363pub struct HarnessContextPermissionsV1 { pub export: bool, pub restore: bool }
2364
2365impl HarnessContextPermissionsV1 { pub fn validate(&self) -> Result<(), HarnessValidationError> { Ok(()) } }
2366
2367#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2368#[serde(deny_unknown_fields)]
2369pub struct HarnessOperationTimeoutsV1 {
2370    pub dispatch_ms: u64,
2371    pub wait_ms: u64,
2372    pub reconciliation_ms: u64,
2373}
2374
2375impl HarnessOperationTimeoutsV1 {
2376    pub fn validate(&self) -> Result<(), HarnessValidationError> {
2377        for (field, value) in [("dispatch_ms", self.dispatch_ms), ("wait_ms", self.wait_ms), ("reconciliation_ms", self.reconciliation_ms)] {
2378            if !(HARNESS_TIMEOUT_MIN_MS..=HARNESS_TIMEOUT_MAX_MS).contains(&value) {
2379                return Err(HarnessValidationError::InvalidTimeout { field });
2380            }
2381        }
2382        Ok(())
2383    }
2384}
2385
2386#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
2387#[serde(deny_unknown_fields)]
2388pub struct HarnessGrantTargetV1 {
2389    pub node_id: HarnessSelectorV1,
2390    pub workspace_id: HarnessSelectorV1,
2391    pub provider_profile: HarnessSelectorV1,
2392    pub mode: HarnessExecutionModeV1,
2393}
2394
2395impl HarnessGrantTargetV1 {
2396    pub fn validate(&self) -> Result<(), HarnessValidationError> {
2397        self.node_id.validate()?;
2398        self.workspace_id.validate()?;
2399        self.provider_profile.validate()
2400    }
2401}
2402
2403#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
2404pub struct SessionGrantV1 {
2405    pub grant_id: SessionGrantId,
2406    pub revision: HarnessRevision,
2407    pub actor_run_id: HarnessRunId,
2408    pub allowed_targets: Vec<HarnessGrantTargetV1>,
2409    pub allowed_delivery_bundles: Vec<HarnessSelectorV1>,
2410    pub maximum_child_count: u16,
2411    pub maximum_child_depth: u16,
2412    pub operation_timeouts: HarnessOperationTimeoutsV1,
2413    pub task_permissions: HarnessTaskPermissionsV1,
2414    #[serde(default)]
2415    pub read_permissions: HarnessReadPermissionsV1,
2416    pub monitoring_visibility: HarnessMonitoringVisibilityV1,
2417    pub context_permissions: HarnessContextPermissionsV1,
2418    pub state: SessionGrantStateV1,
2419    pub created_at_unix_ms: u64,
2420    pub updated_at_unix_ms: u64,
2421}
2422
2423/// Forward migration off the mailbox permission bit (removed 2026-09-17: the
2424/// mailbox moved to its own service, see `gate4agent/CLAUDE.md`). Mirrors
2425/// `SessionGrantV1` field-for-field plus the retired `mail` flag so a grant
2426/// persisted while that field still existed keeps decoding under
2427/// `deny_unknown_fields` instead of failing to load; the value itself is
2428/// read once (to prove it decoded) and then dropped -- `SessionGrantV1` has
2429/// nothing left to carry it in.
2430#[derive(Deserialize)]
2431#[serde(deny_unknown_fields)]
2432struct SessionGrantWireV1 {
2433    grant_id: SessionGrantId,
2434    revision: HarnessRevision,
2435    actor_run_id: HarnessRunId,
2436    allowed_targets: Vec<HarnessGrantTargetV1>,
2437    allowed_delivery_bundles: Vec<HarnessSelectorV1>,
2438    maximum_child_count: u16,
2439    maximum_child_depth: u16,
2440    operation_timeouts: HarnessOperationTimeoutsV1,
2441    task_permissions: HarnessTaskPermissionsV1,
2442    #[serde(default)]
2443    read_permissions: HarnessReadPermissionsV1,
2444    monitoring_visibility: HarnessMonitoringVisibilityV1,
2445    context_permissions: HarnessContextPermissionsV1,
2446    #[serde(default)]
2447    mail: bool,
2448    state: SessionGrantStateV1,
2449    created_at_unix_ms: u64,
2450    updated_at_unix_ms: u64,
2451}
2452
2453impl<'de> Deserialize<'de> for SessionGrantV1 {
2454    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2455    where
2456        D: Deserializer<'de>,
2457    {
2458        let wire = SessionGrantWireV1::deserialize(deserializer)?;
2459        let _retired_mail_bit = wire.mail;
2460        Ok(Self {
2461            grant_id: wire.grant_id,
2462            revision: wire.revision,
2463            actor_run_id: wire.actor_run_id,
2464            allowed_targets: wire.allowed_targets,
2465            allowed_delivery_bundles: wire.allowed_delivery_bundles,
2466            maximum_child_count: wire.maximum_child_count,
2467            maximum_child_depth: wire.maximum_child_depth,
2468            operation_timeouts: wire.operation_timeouts,
2469            task_permissions: wire.task_permissions,
2470            read_permissions: wire.read_permissions,
2471            monitoring_visibility: wire.monitoring_visibility,
2472            context_permissions: wire.context_permissions,
2473            state: wire.state,
2474            created_at_unix_ms: wire.created_at_unix_ms,
2475            updated_at_unix_ms: wire.updated_at_unix_ms,
2476        })
2477    }
2478}
2479
2480impl SessionGrantV1 {
2481    pub fn validate(&self) -> Result<(), HarnessValidationError> {
2482        self.grant_id.validate()?;
2483        self.revision.validate()?;
2484        self.actor_run_id.validate()?;
2485        validate_sorted_ids("allowed_targets", &self.allowed_targets, HARNESS_ALLOWED_TARGETS_MAX)?;
2486        for target in &self.allowed_targets {
2487            target.validate()?;
2488        }
2489        validate_sorted_ids("allowed_delivery_bundles", &self.allowed_delivery_bundles, HARNESS_ALLOWED_TARGETS_MAX)?;
2490        if self.allowed_targets.is_empty() {
2491            return Err(HarnessValidationError::EmptyGrantScope);
2492        }
2493        if self.maximum_child_count > HARNESS_CHILD_COUNT_MAX {
2494            return Err(HarnessValidationError::InvalidChildLimit { field: "maximum_child_count" });
2495        }
2496        if self.maximum_child_depth > HARNESS_CHILD_DEPTH_MAX {
2497            return Err(HarnessValidationError::InvalidChildLimit { field: "maximum_child_depth" });
2498        }
2499        self.operation_timeouts.validate()?;
2500        self.task_permissions.validate()?;
2501        self.read_permissions.validate()?;
2502        if !self.task_permissions.read
2503            && self.read_permissions.tasks != HarnessEntityReadScopeV1::None
2504        {
2505            return Err(HarnessValidationError::ReadScopeRequiresTaskRead);
2506        }
2507        self.context_permissions.validate()?;
2508        validate_timestamps(self.created_at_unix_ms, self.updated_at_unix_ms)
2509    }
2510
2511    pub fn allows_target(
2512        &self,
2513        node_id: &HarnessSelectorV1,
2514        workspace_id: &HarnessSelectorV1,
2515        provider_profile: &HarnessSelectorV1,
2516        mode: HarnessExecutionModeV1,
2517    ) -> bool {
2518        if self.state != SessionGrantStateV1::Active {
2519            return false;
2520        }
2521        self.allowed_targets.binary_search_by(|target| {
2522            (
2523                &target.node_id,
2524                &target.workspace_id,
2525                &target.provider_profile,
2526                target.mode,
2527            )
2528                .cmp(&(node_id, workspace_id, provider_profile, mode))
2529        }).is_ok()
2530    }
2531
2532    pub fn allows_delivery_bundle(&self, selector: &HarnessSelectorV1) -> bool {
2533        self.state == SessionGrantStateV1::Active
2534            && self.allowed_delivery_bundles.binary_search(selector).is_ok()
2535    }
2536
2537    /// The default grant a dispatch mints for its own run when harness MCP
2538    /// is requested and no exact grant was supplied
2539    /// (`HarnessGrantPolicyV1::Operator`, gate4agent-arc-mailbox-and-task-layer
2540    /// Slice A(i)). Read-only, own run's lineage only: `SelfOnly` on every
2541    /// `read_permissions` axis (never `Descendants` — this grant never sees
2542    /// outside the one run it was minted for) plus `Timeline` monitoring, so
2543    /// every currently-shipped `g4a_*` read tool resolves (see
2544    /// `allowed_tool_ids`); the task's own sibling runs still surface through
2545    /// `g4a_context_get`, which derives them independently of this scope.
2546    /// `task_permissions.create`/`.mutate` are open (D5, Slice D: the owner's
2547    /// mandate reaches `g4a_task_create`/`g4a_task_move` in the caller's own
2548    /// subtree) but `request_run` stays closed -- spawning runs stays
2549    /// operator-only. No context writes, no delivery bundles, no child runs.
2550    /// `operation_timeouts` and the child limits are this crate's own
2551    /// `valid_grant()` test fixture values, not invented here.
2552    pub fn default_for_run(
2553        grant_id: SessionGrantId,
2554        actor_run_id: HarnessRunId,
2555        target: HarnessGrantTargetV1,
2556        now_unix_ms: u64,
2557    ) -> Self {
2558        Self {
2559            grant_id,
2560            revision: HarnessRevision::new(1)
2561                .expect("literal revision 1 is always a valid harness revision"),
2562            actor_run_id,
2563            allowed_targets: vec![target],
2564            allowed_delivery_bundles: Vec::new(),
2565            maximum_child_count: 0,
2566            maximum_child_depth: 0,
2567            operation_timeouts: HarnessOperationTimeoutsV1 {
2568                dispatch_ms: 30_000,
2569                wait_ms: 60_000,
2570                reconciliation_ms: 30_000,
2571            },
2572            task_permissions: HarnessTaskPermissionsV1 {
2573                read: true,
2574                // D5, Slice D: the owner opened create/move in the
2575                // caller's own subtree for every default-granted run;
2576                // `request_run` stays closed -- spawning remains
2577                // operator-only.
2578                create: true,
2579                mutate: true,
2580                request_run: false,
2581            },
2582            read_permissions: HarnessReadPermissionsV1 {
2583                tasks: HarnessEntityReadScopeV1::SelfOnly,
2584                runs: HarnessEntityReadScopeV1::SelfOnly,
2585                operations: HarnessEntityReadScopeV1::SelfOnly,
2586            },
2587            monitoring_visibility: HarnessMonitoringVisibilityV1::Timeline,
2588            context_permissions: HarnessContextPermissionsV1 { export: false, restore: false },
2589            state: SessionGrantStateV1::Active,
2590            created_at_unix_ms: now_unix_ms,
2591            updated_at_unix_ms: now_unix_ms,
2592        }
2593    }
2594}
2595
2596#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
2597#[serde(rename_all = "kebab-case")]
2598pub enum HarnessOperationKindV1 {
2599    CreateTask,
2600    MutateTask,
2601    MutateExecutionSpec,
2602    CreateRun,
2603    BindRun,
2604    MutateRun,
2605    CreateGrant,
2606    MutateGrant,
2607    RevokeGrant,
2608    Reconcile,
2609    RecordRunContextPack,
2610    RecordRunGitFacts,
2611    RecordTaskResultRef,
2612}
2613
2614#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
2615#[serde(rename_all = "kebab-case")]
2616pub enum HarnessOperationStateV1 { Prepared, Dispatching, OutcomeUnknown, Succeeded, Failed, Reconciled }
2617
2618#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
2619#[serde(rename_all = "kebab-case")]
2620pub enum HarnessOutcomeUnknownReasonV1 { TransportClosed, Timeout, ReplyLost }
2621
2622#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
2623#[serde(rename_all = "kebab-case")]
2624pub enum HarnessReconciliationOutcomeV1 { Succeeded, Failed, NoEffect }
2625
2626#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
2627#[serde(transparent)]
2628pub struct HarnessRequestDigest(String);
2629
2630impl HarnessRequestDigest {
2631    pub fn new(value: impl Into<String>) -> Result<Self, HarnessValidationError> {
2632        let value = value.into();
2633        validate_lower_hex("request digest", &value, 64)?;
2634        Ok(Self(value))
2635    }
2636    pub fn as_str(&self) -> &str { &self.0 }
2637    pub fn validate(&self) -> Result<(), HarnessValidationError> { validate_lower_hex("request digest", &self.0, 64) }
2638}
2639
2640impl fmt::Debug for HarnessRequestDigest {
2641    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.debug_tuple("HarnessRequestDigest").field(&self.0).finish() }
2642}
2643
2644impl<'de> Deserialize<'de> for HarnessRequestDigest {
2645    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de> {
2646        Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
2647    }
2648}
2649
2650#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2651#[serde(deny_unknown_fields)]
2652pub struct HarnessOperationV1 {
2653    pub operation_id: HarnessOperationId,
2654    pub revision: HarnessRevision,
2655    pub actor: HarnessActorV1,
2656    pub kind: HarnessOperationKindV1,
2657    pub state: HarnessOperationStateV1,
2658    pub task_id: Option<HarnessTaskId>,
2659    pub run_id: Option<HarnessRunId>,
2660    pub grant_id: Option<SessionGrantId>,
2661    pub reconciles_operation_id: Option<HarnessOperationId>,
2662    pub expected_revision: Option<HarnessRevision>,
2663    pub request_digest: HarnessRequestDigest,
2664    pub idempotency_ref: HarnessIdempotencyRef,
2665    pub failure: Option<HarnessFailureV1>,
2666    pub outcome_unknown_reason: Option<HarnessOutcomeUnknownReasonV1>,
2667    pub reconciliation_outcome: Option<HarnessReconciliationOutcomeV1>,
2668    pub created_at_unix_ms: u64,
2669    pub updated_at_unix_ms: u64,
2670    pub dispatched_at_unix_ms: Option<u64>,
2671    pub finished_at_unix_ms: Option<u64>,
2672}
2673
2674impl HarnessOperationV1 {
2675    pub fn validate(&self) -> Result<(), HarnessValidationError> {
2676        self.operation_id.validate()?;
2677        self.revision.validate()?;
2678        self.actor.validate()?;
2679        if let Some(task_id) = &self.task_id { task_id.validate()?; }
2680        if let Some(run_id) = &self.run_id { run_id.validate()?; }
2681        if let Some(grant_id) = &self.grant_id { grant_id.validate()?; }
2682        if let Some(operation_id) = &self.reconciles_operation_id {
2683            operation_id.validate()?;
2684            if operation_id == &self.operation_id {
2685                return Err(HarnessValidationError::SelfLink { field: "reconciles_operation_id" });
2686            }
2687        }
2688        if let Some(revision) = self.expected_revision { revision.validate()?; }
2689        self.request_digest.validate()?;
2690        self.idempotency_ref.validate()?;
2691        if let Some(failure) = &self.failure { failure.validate()?; }
2692        validate_timestamps(self.created_at_unix_ms, self.updated_at_unix_ms)?;
2693        validate_optional_timestamp("dispatched_at_unix_ms", self.dispatched_at_unix_ms, self.created_at_unix_ms, self.updated_at_unix_ms)?;
2694        validate_optional_timestamp("finished_at_unix_ms", self.finished_at_unix_ms, self.created_at_unix_ms, self.updated_at_unix_ms)?;
2695        if matches!((self.dispatched_at_unix_ms, self.finished_at_unix_ms), (Some(dispatched), Some(finished)) if finished < dispatched) {
2696            return Err(HarnessValidationError::InvalidOperationStateFields);
2697        }
2698        self.validate_kind_targets()?;
2699
2700        match self.state {
2701            HarnessOperationStateV1::Prepared => {
2702                require_state_fields(self.dispatched_at_unix_ms.is_none() && self.finished_at_unix_ms.is_none() && self.failure.is_none() && self.outcome_unknown_reason.is_none() && self.reconciliation_outcome.is_none())?;
2703            }
2704            HarnessOperationStateV1::Dispatching => {
2705                require_state_fields(self.dispatched_at_unix_ms.is_some() && self.finished_at_unix_ms.is_none() && self.failure.is_none() && self.outcome_unknown_reason.is_none() && self.reconciliation_outcome.is_none())?;
2706            }
2707            HarnessOperationStateV1::OutcomeUnknown => {
2708                require_state_fields(self.dispatched_at_unix_ms.is_some() && self.finished_at_unix_ms.is_none() && self.failure.is_none() && self.outcome_unknown_reason.is_some() && self.reconciliation_outcome.is_none())?;
2709            }
2710            HarnessOperationStateV1::Succeeded => {
2711                require_state_fields(self.finished_at_unix_ms.is_some() && self.failure.is_none() && self.outcome_unknown_reason.is_none() && self.reconciliation_outcome.is_none())?;
2712                if self.kind == HarnessOperationKindV1::CreateRun && self.dispatched_at_unix_ms.is_none() {
2713                    return Err(HarnessValidationError::InvalidOperationStateFields);
2714                }
2715            }
2716            HarnessOperationStateV1::Failed => {
2717                require_state_fields(self.finished_at_unix_ms.is_some() && self.failure.is_some() && self.outcome_unknown_reason.is_none() && self.reconciliation_outcome.is_none())?;
2718            }
2719            HarnessOperationStateV1::Reconciled => {
2720                require_state_fields(self.dispatched_at_unix_ms.is_some() && self.finished_at_unix_ms.is_some() && self.outcome_unknown_reason.is_none() && self.reconciliation_outcome.is_some())?;
2721                if self.reconciliation_outcome == Some(HarnessReconciliationOutcomeV1::Failed) && self.failure.is_none() {
2722                    return Err(HarnessValidationError::InvalidFailureForState);
2723                }
2724                if self.reconciliation_outcome != Some(HarnessReconciliationOutcomeV1::Failed) && self.failure.is_some() {
2725                    return Err(HarnessValidationError::InvalidFailureForState);
2726                }
2727            }
2728        }
2729        Ok(())
2730    }
2731
2732    fn validate_kind_targets(&self) -> Result<(), HarnessValidationError> {
2733        let valid = match self.kind {
2734            HarnessOperationKindV1::CreateTask => self.task_id.is_some() && self.run_id.is_none() && self.grant_id.is_none() && self.reconciles_operation_id.is_none() && self.expected_revision.is_none(),
2735            HarnessOperationKindV1::MutateTask | HarnessOperationKindV1::MutateExecutionSpec => self.task_id.is_some() && self.run_id.is_none() && self.grant_id.is_none() && self.reconciles_operation_id.is_none() && self.expected_revision.is_some(),
2736            HarnessOperationKindV1::CreateRun => self.task_id.is_some() && self.run_id.is_some() && self.grant_id.is_none() && self.reconciles_operation_id.is_none() && self.expected_revision.is_some(),
2737            HarnessOperationKindV1::BindRun | HarnessOperationKindV1::MutateRun | HarnessOperationKindV1::RecordRunContextPack | HarnessOperationKindV1::RecordRunGitFacts => self.task_id.is_none() && self.run_id.is_some() && self.grant_id.is_none() && self.reconciles_operation_id.is_none() && self.expected_revision.is_some(),
2738            HarnessOperationKindV1::RecordTaskResultRef => self.task_id.is_some() && self.run_id.is_none() && self.grant_id.is_none() && self.reconciles_operation_id.is_none() && self.expected_revision.is_some(),
2739            HarnessOperationKindV1::CreateGrant => self.task_id.is_none() && self.run_id.is_none() && self.grant_id.is_some() && self.reconciles_operation_id.is_none() && self.expected_revision.is_none(),
2740            HarnessOperationKindV1::MutateGrant | HarnessOperationKindV1::RevokeGrant => self.task_id.is_none() && self.run_id.is_none() && self.grant_id.is_some() && self.reconciles_operation_id.is_none() && self.expected_revision.is_some(),
2741            HarnessOperationKindV1::Reconcile => self.task_id.is_none() && self.run_id.is_some() && self.grant_id.is_none() && self.reconciles_operation_id.is_some() && self.expected_revision.is_some(),
2742        };
2743        if !valid {
2744            return Err(HarnessValidationError::InvalidOperationTarget);
2745        }
2746        Ok(())
2747    }
2748}
2749
2750fn validate_prefixed_hex_id(label: &'static str, value: &str, prefix: &str, hex_len: usize) -> Result<(), HarnessValidationError> {
2751    let Some(hex) = value.strip_prefix(prefix) else { return Err(HarnessValidationError::InvalidOpaqueId { field: label }); };
2752    validate_lower_hex(label, hex, hex_len)
2753}
2754
2755fn validate_lower_hex(label: &'static str, value: &str, expected_len: usize) -> Result<(), HarnessValidationError> {
2756    if value.len() != expected_len || !value.bytes().all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) {
2757        return Err(HarnessValidationError::InvalidOpaqueId { field: label });
2758    }
2759    Ok(())
2760}
2761
2762fn validate_git_commit_id(value: &str) -> Result<(), HarnessValidationError> {
2763    if !matches!(value.len(), 40 | 64) || !value.bytes().all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) {
2764        return Err(HarnessValidationError::InvalidGitSummary);
2765    }
2766    Ok(())
2767}
2768
2769fn validate_run_git_text(value: &str, maximum: usize, required: bool) -> Result<(), HarnessValidationError> {
2770    if value.len() > maximum || required && value.is_empty() || value.chars().any(char::is_control) {
2771        return Err(HarnessValidationError::InvalidGitSummary);
2772    }
2773    Ok(())
2774}
2775
2776fn validate_selector(value: &str) -> Result<(), HarnessValidationError> {
2777    if value.is_empty() || value.len() > HARNESS_SELECTOR_MAX_BYTES || !value.is_ascii()
2778        || !value.bytes().all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'@' | b'+'))
2779    {
2780        return Err(HarnessValidationError::InvalidSelector);
2781    }
2782    Ok(())
2783}
2784
2785fn validate_delivery_resource_id(
2786    field: &'static str,
2787    value: &str,
2788) -> Result<(), HarnessValidationError> {
2789    if value.is_empty()
2790        || value.len() > HARNESS_DELIVERY_RESOURCE_MAX_BYTES
2791        || !value.bytes().all(|byte| {
2792            byte.is_ascii_graphic() && !matches!(byte, b'/' | b'\\')
2793        })
2794    {
2795        return Err(HarnessValidationError::InvalidDeliveryResource { field });
2796    }
2797    Ok(())
2798}
2799
2800fn validate_delivery_digest(value: &str) -> Result<(), HarnessValidationError> {
2801    let Some(digest) = value.strip_prefix("sha256:") else {
2802        return Err(HarnessValidationError::InvalidDeliveryDigest);
2803    };
2804    validate_lower_hex("delivery bundle digest", digest, 64)
2805        .map_err(|_| HarnessValidationError::InvalidDeliveryDigest)
2806}
2807
2808fn validate_context_digest(value: &str) -> Result<(), HarnessValidationError> {
2809    let Some(digest) = value.strip_prefix("sha256:") else {
2810        return Err(HarnessValidationError::InvalidContextPackDigest);
2811    };
2812    validate_lower_hex("context pack digest", digest, 64)
2813        .map_err(|_| HarnessValidationError::InvalidContextPackDigest)
2814}
2815
2816fn validate_title(value: &str) -> Result<(), HarnessValidationError> {
2817    if value.trim() != value || value.is_empty() || value.len() > HARNESS_TITLE_MAX_BYTES || value.chars().any(char::is_control) {
2818        return Err(HarnessValidationError::InvalidTitle);
2819    }
2820    Ok(())
2821}
2822
2823fn validate_body(value: &str) -> Result<(), HarnessValidationError> {
2824    if value.len() > HARNESS_BODY_MAX_BYTES || value.chars().any(|character| character.is_control() && !matches!(character, '\n' | '\t')) {
2825        return Err(HarnessValidationError::InvalidBody);
2826    }
2827    Ok(())
2828}
2829
2830fn validate_task_result_why(value: &str) -> Result<(), HarnessValidationError> {
2831    if value.is_empty() || value.len() > HARNESS_TASK_RESULT_WHY_MAX_BYTES || value.chars().any(char::is_control) {
2832        return Err(HarnessValidationError::InvalidTaskResultWhy);
2833    }
2834    Ok(())
2835}
2836
2837fn validate_sorted_ids<T: Ord>(field: &'static str, values: &[T], maximum: usize) -> Result<(), HarnessValidationError> {
2838    if values.len() > maximum {
2839        return Err(HarnessValidationError::CollectionTooLarge { field, maximum });
2840    }
2841    if values.windows(2).any(|pair| pair[0].cmp(&pair[1]) != Ordering::Less) {
2842        return Err(HarnessValidationError::CollectionNotCanonical { field });
2843    }
2844    Ok(())
2845}
2846
2847fn validate_timestamps(created: u64, updated: u64) -> Result<(), HarnessValidationError> {
2848    if created == 0 || updated == 0 || updated < created {
2849        return Err(HarnessValidationError::InvalidTimestamps);
2850    }
2851    Ok(())
2852}
2853
2854fn validate_optional_timestamp(field: &'static str, value: Option<u64>, created: u64, updated: u64) -> Result<(), HarnessValidationError> {
2855    if value.is_some_and(|value| value < created || value > updated) {
2856        return Err(HarnessValidationError::InvalidOptionalTimestamp { field });
2857    }
2858    Ok(())
2859}
2860
2861fn require_state_fields(valid: bool) -> Result<(), HarnessValidationError> {
2862    if !valid { return Err(HarnessValidationError::InvalidOperationStateFields); }
2863    Ok(())
2864}
2865
2866#[derive(Clone, Debug, Eq, Error, PartialEq)]
2867pub enum HarnessValidationError {
2868    #[error("{field} is not a valid bounded opaque identifier")]
2869    InvalidOpaqueId { field: &'static str },
2870    #[error("harness revision must be nonzero")]
2871    ZeroRevision,
2872    #[error("harness selector is empty, unbounded, non-ASCII, or contains path/control syntax")]
2873    InvalidSelector,
2874    #[error("continuation selector must be an exact HarnessRunId")]
2875    InvalidContinuationSelector,
2876    #[error("task title is empty, unbounded, padded, or contains control characters")]
2877    InvalidTitle,
2878    #[error("task body is unbounded or contains prohibited control characters")]
2879    InvalidBody,
2880    #[error("{field} cannot link the record to itself")]
2881    SelfLink { field: &'static str },
2882    #[error("{field} exceeds its bounded maximum of {maximum}")]
2883    CollectionTooLarge { field: &'static str, maximum: usize },
2884    #[error("{field} must be strictly sorted and duplicate-free")]
2885    CollectionNotCanonical { field: &'static str },
2886    #[error("created/updated timestamps are zero or non-monotonic")]
2887    InvalidTimestamps,
2888    #[error("{field} is outside the record timestamp bounds")]
2889    InvalidOptionalTimestamp { field: &'static str },
2890    #[error("managed or Inline session identity is invalid")]
2891    InvalidSessionIdentity,
2892    #[error("{field} is required by the current state")]
2893    MissingFieldForState { field: &'static str },
2894    #[error("run result disposition does not match lifecycle")]
2895    InvalidResultDisposition,
2896    #[error("run session binding is not authoritative in the current lifecycle")]
2897    InvalidBindingForState,
2898    #[error("run session binding does not match its immutable node/workspace intent")]
2899    BindingIntentMismatch,
2900    #[error("categorical failure does not match lifecycle/state")]
2901    InvalidFailureForState,
2902    #[error("grant target scope must be explicit and nonempty")]
2903    EmptyGrantScope,
2904    #[error("{field} is outside the supported child bound")]
2905    InvalidChildLimit { field: &'static str },
2906    #[error("{field} is outside the supported timeout bound")]
2907    InvalidTimeout { field: &'static str },
2908    #[error("operation timestamps/failure/reconciliation fields do not match state")]
2909    InvalidOperationStateFields,
2910    #[error("operation kind, target identifiers, and expected revision do not form a valid mutation")]
2911    InvalidOperationTarget,
2912    #[error("object-level task read scope requires task read permission")]
2913    ReadScopeRequiresTaskRead,
2914    #[error("{field} is not a valid bounded delivery resource identifier")]
2915    InvalidDeliveryResource { field: &'static str },
2916    #[error("delivery bundle digest must be sha256 followed by lowercase hexadecimal")]
2917    InvalidDeliveryDigest,
2918    #[error("delivery component count is zero, overflowing, or outside the fixed bundle bound")]
2919    InvalidDeliveryComponentCount,
2920    #[error("delivery state does not match staged and committed receipt fields")]
2921    InvalidDeliveryStateFields,
2922    #[error("delivery receipt does not match its immutable authority")]
2923    InvalidDeliveryLink,
2924    #[error("context pack digest must be sha256 followed by lowercase hexadecimal")]
2925    InvalidContextPackDigest,
2926    #[error("context pack receipt is empty, inconsistent, or exceeds protocol limits")]
2927    InvalidContextPackReceipt,
2928    #[error("context source counts are inconsistent with the observed message count")]
2929    InvalidContextSourceCounts,
2930    #[error("continuation authority has invalid state fields or exact links")]
2931    InvalidContinuationLink,
2932    #[error("harness run git summary is empty, unbounded, unsorted, or contains a control character")]
2933    InvalidGitSummary,
2934    #[error("task result `why` value is empty, unbounded, or contains a control character")]
2935    InvalidTaskResultWhy,
2936}
2937
2938#[cfg(test)]
2939mod tests {
2940    use super::*;
2941
2942    fn task_id(hex: char) -> HarnessTaskId {
2943        HarnessTaskId::new(format!("htask_{}", hex.to_string().repeat(24))).unwrap()
2944    }
2945
2946    fn run_id(hex: char) -> HarnessRunId {
2947        HarnessRunId::new(format!("hrun_{}", hex.to_string().repeat(24))).unwrap()
2948    }
2949
2950    fn operation_id(hex: char) -> HarnessOperationId {
2951        HarnessOperationId::new(format!("hop_{}", hex.to_string().repeat(24))).unwrap()
2952    }
2953
2954    fn grant_id(hex: char) -> SessionGrantId {
2955        SessionGrantId::new(format!("hgrant_{}", hex.to_string().repeat(24))).unwrap()
2956    }
2957
2958    fn selector(value: &str) -> HarnessSelectorV1 {
2959        HarnessSelectorV1::new(value).unwrap()
2960    }
2961
2962    fn valid_task() -> HarnessTaskV1 {
2963        HarnessTaskV1 {
2964            task_id: task_id('1'),
2965            revision: HarnessRevision::new(1).unwrap(),
2966            title: "Implement durable operation ledger".to_owned(),
2967            body: "Explicit harness state only.".to_owned(),
2968            creator: HarnessActorV1::User { actor_id: selector("operator") },
2969            parent_task_id: None,
2970            dependencies: vec![task_id('2')],
2971            state: HarnessTaskStateV1::Ready,
2972            run_ids: vec![],
2973            result_refs: vec![],
2974            artifact_refs: vec![],
2975            created_at_unix_ms: 1_000,
2976            updated_at_unix_ms: 1_000,
2977        }
2978    }
2979
2980    fn valid_grant() -> SessionGrantV1 {
2981        SessionGrantV1 {
2982            grant_id: grant_id('3'),
2983            revision: HarnessRevision::new(1).unwrap(),
2984            actor_run_id: run_id('4'),
2985            allowed_targets: vec![HarnessGrantTargetV1 {
2986                node_id: selector("node-a"),
2987                workspace_id: selector("workspace-a"),
2988                provider_profile: selector("claude"),
2989                mode: HarnessExecutionModeV1::Pty,
2990            }],
2991            allowed_delivery_bundles: vec![],
2992            maximum_child_count: 0,
2993            maximum_child_depth: 0,
2994            operation_timeouts: HarnessOperationTimeoutsV1 {
2995                dispatch_ms: 30_000,
2996                wait_ms: 60_000,
2997                reconciliation_ms: 30_000,
2998            },
2999            task_permissions: HarnessTaskPermissionsV1 {
3000                read: true,
3001                create: false,
3002                mutate: false,
3003                request_run: false,
3004            },
3005            read_permissions: HarnessReadPermissionsV1::default(),
3006            monitoring_visibility: HarnessMonitoringVisibilityV1::Summary,
3007            context_permissions: HarnessContextPermissionsV1 { export: false, restore: false },
3008            state: SessionGrantStateV1::Active,
3009            created_at_unix_ms: 1_000,
3010            updated_at_unix_ms: 1_000,
3011        }
3012    }
3013
3014    fn valid_run() -> HarnessRunV1 {
3015        HarnessRunV1 {
3016            run_id: run_id('4'),
3017            revision: HarnessRevision::new(1).unwrap(),
3018            parent_run_id: None,
3019            task_id: task_id('1'),
3020            operation_id: operation_id('5'),
3021            intent: HarnessRunIntentV1 {
3022                node_id: selector("node-a"),
3023                workspace_id: selector("workspace-a"),
3024                worktree: HarnessWorktreeIntentV1::Existing,
3025                provider_profile: selector("claude"),
3026                mode: HarnessExecutionModeV1::Pty,
3027                delivery_bundle: None,
3028                continuation: None,
3029            },
3030            delivery_receipt: None,
3031            continuation_receipt: None,
3032            context_pack: None,
3033            git_facts: None,
3034            binding: None,
3035            lifecycle: HarnessRunLifecycleV1::Requested,
3036            result_disposition: None,
3037            failure: None,
3038            created_at_unix_ms: 1_000,
3039            updated_at_unix_ms: 1_000,
3040        }
3041    }
3042
3043    #[test]
3044    fn harness_task_id_and_bounds_reject_invalid() {
3045        assert!(HarnessTaskId::new("htask_123456789012345678901234").is_ok());
3046        assert!(HarnessTaskId::new("hrun_123456789012345678901234").is_err());
3047        assert!(HarnessTaskId::new("htask_12345678901234567890123").is_err());
3048        assert!(HarnessTaskId::new("htask_12345678901234567890123A").is_err());
3049        assert!(HarnessRunId::new("htask_123456789012345678901234").is_err());
3050        assert!(SessionGrantId::new("hgrant_123456789012345678901234").is_ok());
3051        assert!(HarnessOperationId::new("hop_abcdefabcdefabcdefabcdef").is_ok());
3052        assert!(HarnessRevision::new(0).is_err());
3053
3054        let mut task = valid_task();
3055        task.title = "x".repeat(HARNESS_TITLE_MAX_BYTES + 1);
3056        assert_eq!(task.validate(), Err(HarnessValidationError::InvalidTitle));
3057
3058        let mut task = valid_task();
3059        task.dependencies = vec![task_id('3'), task_id('2')];
3060        assert_eq!(
3061            task.validate(),
3062            Err(HarnessValidationError::CollectionNotCanonical { field: "dependencies" })
3063        );
3064    }
3065
3066    #[test]
3067    fn harness_task_and_grant_validation_is_fail_closed() {
3068        let task = valid_task();
3069        task.validate().unwrap();
3070        let encoded = serde_json::to_value(&task).unwrap();
3071        assert!(encoded.get("observation").is_none());
3072
3073        let mut self_linked = task.clone();
3074        self_linked.parent_task_id = Some(self_linked.task_id.clone());
3075        assert_eq!(
3076            self_linked.validate(),
3077            Err(HarnessValidationError::SelfLink { field: "parent_task_id" })
3078        );
3079
3080        let grant = valid_grant();
3081        grant.validate().unwrap();
3082        let mut unscoped = grant.clone();
3083        unscoped.allowed_targets.clear();
3084        assert_eq!(unscoped.validate(), Err(HarnessValidationError::EmptyGrantScope));
3085
3086        let mut noncanonical = grant.clone();
3087        noncanonical.allowed_targets = vec![
3088            HarnessGrantTargetV1 {
3089                node_id: selector("node-b"),
3090                workspace_id: selector("workspace-a"),
3091                provider_profile: selector("claude"),
3092                mode: HarnessExecutionModeV1::Pty,
3093            },
3094            HarnessGrantTargetV1 {
3095                node_id: selector("node-a"),
3096                workspace_id: selector("workspace-b"),
3097                provider_profile: selector("codex"),
3098                mode: HarnessExecutionModeV1::Inline,
3099            },
3100        ];
3101        assert_eq!(
3102            noncanonical.validate(),
3103            Err(HarnessValidationError::CollectionNotCanonical {
3104                field: "allowed_targets",
3105            })
3106        );
3107
3108        let mut wire = serde_json::to_value(grant).unwrap();
3109        wire.as_object_mut().unwrap().insert("bearer_token".to_owned(), serde_json::json!("secret"));
3110        assert!(serde_json::from_value::<SessionGrantV1>(wire).is_err());
3111
3112        let mut legacy_cross_product = serde_json::to_value(valid_grant()).unwrap();
3113        let object = legacy_cross_product.as_object_mut().unwrap();
3114        object.remove("allowed_targets");
3115        object.insert("allowed_node_ids".to_owned(), serde_json::json!(["node-a"]));
3116        object.insert(
3117            "allowed_workspace_ids".to_owned(),
3118            serde_json::json!(["workspace-a"]),
3119        );
3120        assert!(serde_json::from_value::<SessionGrantV1>(legacy_cross_product).is_err());
3121
3122        let mut task_wire = serde_json::to_value(task).unwrap();
3123        task_wire.as_object_mut().unwrap().insert("monitoring_state".to_owned(), serde_json::json!("done"));
3124        assert!(serde_json::from_value::<HarnessTaskV1>(task_wire).is_err());
3125    }
3126
3127    #[test]
3128    fn harness_grant_target_scope_is_exact_not_cross_product() {
3129        let mut grant = valid_grant();
3130        grant.allowed_targets = vec![
3131            HarnessGrantTargetV1 {
3132                node_id: selector("node-a"),
3133                workspace_id: selector("workspace-a"),
3134                provider_profile: selector("claude"),
3135                mode: HarnessExecutionModeV1::Pty,
3136            },
3137            HarnessGrantTargetV1 {
3138                node_id: selector("node-b"),
3139                workspace_id: selector("workspace-b"),
3140                provider_profile: selector("codex"),
3141                mode: HarnessExecutionModeV1::Inline,
3142            },
3143        ];
3144        grant.validate().unwrap();
3145
3146        assert!(grant.allows_target(
3147            &selector("node-a"),
3148            &selector("workspace-a"),
3149            &selector("claude"),
3150            HarnessExecutionModeV1::Pty,
3151        ));
3152        assert!(!grant.allows_target(
3153            &selector("node-a"),
3154            &selector("workspace-b"),
3155            &selector("claude"),
3156            HarnessExecutionModeV1::Pty,
3157        ));
3158        assert!(!grant.allows_target(
3159            &selector("node-a"),
3160            &selector("workspace-a"),
3161            &selector("codex"),
3162            HarnessExecutionModeV1::Inline,
3163        ));
3164
3165        grant.state = SessionGrantStateV1::Revoked;
3166        assert!(!grant.allows_target(
3167            &selector("node-a"),
3168            &selector("workspace-a"),
3169            &selector("claude"),
3170            HarnessExecutionModeV1::Pty,
3171        ));
3172    }
3173
3174    #[test]
3175    fn harness_read_permissions_are_legacy_safe_and_task_scope_is_bounded() {
3176        let grant = valid_grant();
3177        let mut legacy_wire = serde_json::to_value(&grant).unwrap();
3178        legacy_wire.as_object_mut().unwrap().remove("read_permissions");
3179        let decoded: SessionGrantV1 = serde_json::from_value(legacy_wire).unwrap();
3180        assert_eq!(decoded.read_permissions, HarnessReadPermissionsV1::default());
3181        assert_eq!(
3182            serde_json::to_value(&decoded.read_permissions).unwrap(),
3183            serde_json::json!({
3184                "tasks": "none",
3185                "runs": "none",
3186                "operations": "none",
3187            }),
3188        );
3189
3190        let mut scoped = grant;
3191        scoped.task_permissions.read = false;
3192        scoped.read_permissions.tasks = HarnessEntityReadScopeV1::SelfOnly;
3193        assert_eq!(
3194            scoped.validate(),
3195            Err(HarnessValidationError::ReadScopeRequiresTaskRead),
3196        );
3197    }
3198
3199    /// Forward migration (2026-09-17): the mailbox moved to its own service
3200    /// and `SessionGrantV1` no longer carries a `mail` bit at all, but a
3201    /// grant persisted while that field existed still has it in its stored
3202    /// JSON -- this proves such a record still decodes under
3203    /// `deny_unknown_fields` rather than failing to load.
3204    #[test]
3205    fn session_grant_decodes_a_stored_record_that_still_carries_the_retired_mail_field() {
3206        let grant = valid_grant();
3207        let mut legacy_wire = serde_json::to_value(&grant).unwrap();
3208        legacy_wire.as_object_mut().unwrap().insert("mail".to_owned(), serde_json::json!(true));
3209        let decoded: SessionGrantV1 = serde_json::from_value(legacy_wire).unwrap();
3210        assert_eq!(decoded, grant);
3211    }
3212
3213    #[test]
3214    fn harness_task_create_and_mutate_permission_default_true_and_legacy_shape_deserialises() {
3215        let grant = valid_grant();
3216        assert!(!grant.task_permissions.create, "valid_grant() fixture predates Slice D");
3217        assert!(!grant.task_permissions.mutate, "valid_grant() fixture predates Slice D");
3218        let mut legacy_wire = serde_json::to_value(&grant).unwrap();
3219        let task_permissions = legacy_wire
3220            .as_object_mut()
3221            .unwrap()
3222            .get_mut("task_permissions")
3223            .unwrap()
3224            .as_object_mut()
3225            .unwrap();
3226        task_permissions.remove("create");
3227        task_permissions.remove("mutate");
3228        let decoded: SessionGrantV1 = serde_json::from_value(legacy_wire).unwrap();
3229        assert!(!decoded.task_permissions.create);
3230        assert!(!decoded.task_permissions.mutate);
3231
3232        let default_grant = SessionGrantV1::default_for_run(
3233            grant_id('9'),
3234            run_id('4'),
3235            HarnessGrantTargetV1 {
3236                node_id: selector("node-a"),
3237                workspace_id: selector("workspace-a"),
3238                provider_profile: selector("claude"),
3239                mode: HarnessExecutionModeV1::Acp,
3240            },
3241            1_000,
3242        );
3243        assert!(default_grant.task_permissions.create);
3244        assert!(default_grant.task_permissions.mutate);
3245        assert!(!default_grant.task_permissions.request_run, "spawning stays operator-only");
3246        default_grant.validate().unwrap();
3247    }
3248
3249    #[test]
3250    fn harness_unknown_run_cannot_claim_unverified_session_binding() {
3251        let mut run = valid_run();
3252        run.lifecycle = HarnessRunLifecycleV1::OutcomeUnknown;
3253        run.binding = Some(HarnessSessionBindingV1 {
3254            node_id: selector("node-a"),
3255            node_incarnation: selector("incarnation-a"),
3256            workspace_id: selector("workspace-a"),
3257            session: HarnessSessionIdentityV1::Managed {
3258                record_id: selector("record-a"),
3259                active_session: None,
3260            },
3261        });
3262        assert_eq!(run.validate(), Err(HarnessValidationError::InvalidBindingForState));
3263
3264        run.binding = None;
3265        run.validate().unwrap();
3266    }
3267
3268    #[test]
3269    fn harness_run_binding_must_match_immutable_target_intent() {
3270        let mut run = valid_run();
3271        run.lifecycle = HarnessRunLifecycleV1::Running;
3272        run.binding = Some(HarnessSessionBindingV1 {
3273            node_id: selector("node-b"),
3274            node_incarnation: selector("incarnation-a"),
3275            workspace_id: selector("workspace-a"),
3276            session: HarnessSessionIdentityV1::Managed {
3277                record_id: selector("record-a"),
3278                active_session: Some(HarnessRuntimeIdentityV1 {
3279                    instance_id: 1,
3280                    generation: 1,
3281                }),
3282            },
3283        });
3284        assert_eq!(run.validate(), Err(HarnessValidationError::BindingIntentMismatch));
3285
3286        run.binding.as_mut().unwrap().node_id = selector("node-a");
3287        run.validate().unwrap();
3288
3289        run.intent.worktree = HarnessWorktreeIntentV1::ManagedProfile {
3290            profile_id: selector("review-tree"),
3291            expected_profile_revision: selector("review-tree-r1"),
3292        };
3293        assert_eq!(
3294            run.validate(),
3295            Err(HarnessValidationError::BindingIntentMismatch),
3296        );
3297
3298        run.binding.as_mut().unwrap().workspace_id = selector("workspace-allocated");
3299        run.validate().unwrap();
3300
3301        run.intent.worktree = HarnessWorktreeIntentV1::Existing;
3302        assert_eq!(
3303            run.validate(),
3304            Err(HarnessValidationError::BindingIntentMismatch),
3305        );
3306    }
3307
3308    #[test]
3309    fn harness_run_context_pack_requires_managed_binding_and_matching_lineage() {
3310        let managed_binding = HarnessSessionBindingV1 {
3311            node_id: selector("node-a"),
3312            node_incarnation: selector("incarnation-a"),
3313            workspace_id: selector("workspace-a"),
3314            session: HarnessSessionIdentityV1::Managed {
3315                record_id: selector("record-a"),
3316                active_session: None,
3317            },
3318        };
3319        let pack = HarnessResolvedContextPackReceiptV1 {
3320            id: selector("context-a"),
3321            digest: format!("sha256:{}", "c".repeat(64)),
3322            lineage: HarnessContextPackLineageV1 {
3323                source_node_id: selector("node-a"),
3324                source_workspace_id: selector("workspace-a"),
3325                source_instance_id: 1,
3326                source_generation: 1,
3327                source_provider: selector("claude"),
3328            },
3329            source_message_count: 2,
3330            retained_message_count: 2,
3331            byte_len: 64,
3332            truncated: false,
3333        };
3334
3335        let mut run = valid_run();
3336        run.lifecycle = HarnessRunLifecycleV1::Completed;
3337        run.result_disposition = Some(HarnessResultDispositionV1::Succeeded);
3338        run.binding = Some(managed_binding.clone());
3339        run.context_pack = Some(pack.clone());
3340        run.validate().unwrap();
3341
3342        let mut missing_binding = run.clone();
3343        missing_binding.binding = None;
3344        assert_eq!(
3345            missing_binding.validate(),
3346            Err(HarnessValidationError::MissingFieldForState { field: "context_pack" }),
3347        );
3348
3349        let mut inline_bound = run.clone();
3350        inline_bound.binding = Some(HarnessSessionBindingV1 {
3351            session: HarnessSessionIdentityV1::Inline {
3352                inline_ref: HarnessInlineRef::new(format!("hinline_{}", "a".repeat(24))).unwrap(),
3353            },
3354            ..managed_binding.clone()
3355        });
3356        assert_eq!(
3357            inline_bound.validate(),
3358            Err(HarnessValidationError::InvalidContextPackReceipt),
3359        );
3360
3361        let mut node_mismatch = run.clone();
3362        node_mismatch.context_pack.as_mut().unwrap().lineage.source_node_id = selector("node-b");
3363        assert_eq!(
3364            node_mismatch.validate(),
3365            Err(HarnessValidationError::InvalidContextPackReceipt),
3366        );
3367
3368        let mut workspace_mismatch = run.clone();
3369        workspace_mismatch.context_pack.as_mut().unwrap().lineage.source_workspace_id =
3370            selector("workspace-b");
3371        assert_eq!(
3372            workspace_mismatch.validate(),
3373            Err(HarnessValidationError::InvalidContextPackReceipt),
3374        );
3375
3376        let mut provider_mismatch = run.clone();
3377        provider_mismatch.context_pack.as_mut().unwrap().lineage.source_provider =
3378            selector("codex");
3379        assert_eq!(
3380            provider_mismatch.validate(),
3381            Err(HarnessValidationError::InvalidContextPackReceipt),
3382        );
3383    }
3384
3385    #[test]
3386    fn harness_run_context_pack_is_additive_and_absent_on_pre_a2_wire() {
3387        let run = valid_run();
3388        let mut legacy_wire = serde_json::to_value(&run).unwrap();
3389        assert!(legacy_wire.as_object().unwrap().get("context_pack").is_none());
3390        legacy_wire.as_object_mut().unwrap().remove("context_pack");
3391        let decoded: HarnessRunV1 = serde_json::from_value(legacy_wire).unwrap();
3392        assert_eq!(decoded.context_pack, None);
3393        assert_eq!(decoded, run);
3394    }
3395
3396    #[test]
3397    fn harness_run_git_facts_is_additive_and_absent_on_pre_a3_wire() {
3398        let run = valid_run();
3399        let mut legacy_wire = serde_json::to_value(&run).unwrap();
3400        assert!(legacy_wire.as_object().unwrap().get("git_facts").is_none());
3401        legacy_wire.as_object_mut().unwrap().remove("git_facts");
3402        let decoded: HarnessRunV1 = serde_json::from_value(legacy_wire).unwrap();
3403        assert_eq!(decoded.git_facts, None);
3404        assert_eq!(decoded, run);
3405    }
3406
3407    #[test]
3408    fn harness_run_git_facts_requires_binding_but_not_managed_session() {
3409        let managed_binding = HarnessSessionBindingV1 {
3410            node_id: selector("node-a"),
3411            node_incarnation: selector("incarnation-a"),
3412            workspace_id: selector("workspace-a"),
3413            session: HarnessSessionIdentityV1::Managed {
3414                record_id: selector("record-a"),
3415                active_session: None,
3416            },
3417        };
3418        let facts = HarnessRunGitFactsV1 {
3419            captured_at_unix_ms: 5,
3420            outcome: HarnessRunGitFactsOutcomeV1::Captured(HarnessRunGitSummaryV1 {
3421                is_repository: true,
3422                branch: Some("main".to_owned()),
3423                status: vec![],
3424                recent_commits: vec![],
3425                truncated: false,
3426            }),
3427        };
3428
3429        let mut run = valid_run();
3430        run.lifecycle = HarnessRunLifecycleV1::Completed;
3431        run.result_disposition = Some(HarnessResultDispositionV1::Succeeded);
3432        run.binding = Some(managed_binding.clone());
3433        run.git_facts = Some(facts);
3434        run.validate().unwrap();
3435
3436        let mut missing_binding = run.clone();
3437        missing_binding.binding = None;
3438        assert_eq!(
3439            missing_binding.validate(),
3440            Err(HarnessValidationError::MissingFieldForState { field: "git_facts" }),
3441        );
3442
3443        // Unlike context_pack, git_facts carries no lineage to cross-check,
3444        // so any present binding — not just a Managed one — satisfies it.
3445        let mut inline_bound = run.clone();
3446        inline_bound.binding = Some(HarnessSessionBindingV1 {
3447            session: HarnessSessionIdentityV1::Inline {
3448                inline_ref: HarnessInlineRef::new(format!("hinline_{}", "a".repeat(24))).unwrap(),
3449            },
3450            ..managed_binding
3451        });
3452        inline_bound.validate().unwrap();
3453    }
3454
3455    #[test]
3456    fn harness_run_git_summary_validate_enforces_bounds_and_invariants() {
3457        let empty = HarnessRunGitSummaryV1 {
3458            is_repository: false,
3459            branch: None,
3460            status: vec![],
3461            recent_commits: vec![],
3462            truncated: false,
3463        };
3464        empty.validate().unwrap();
3465
3466        let mut not_repository_with_branch = empty.clone();
3467        not_repository_with_branch.branch = Some("main".to_owned());
3468        assert_eq!(
3469            not_repository_with_branch.validate(),
3470            Err(HarnessValidationError::InvalidGitSummary),
3471        );
3472
3473        let clean_entry = HarnessRunGitStatusEntryV1 {
3474            index_status: HarnessRunGitStatusCodeV1::Unmodified,
3475            worktree_status: HarnessRunGitStatusCodeV1::Modified,
3476            path: "src/lib.rs".to_owned(),
3477            previous_path: None,
3478        };
3479        let commit = HarnessRunGitCommitSummaryV1 {
3480            id: "a".repeat(40),
3481            summary: "initial commit".to_owned(),
3482        };
3483        let summary = HarnessRunGitSummaryV1 {
3484            is_repository: true,
3485            branch: Some("main".to_owned()),
3486            status: vec![clean_entry.clone()],
3487            recent_commits: vec![commit.clone()],
3488            truncated: false,
3489        };
3490        summary.validate().unwrap();
3491
3492        let mut too_many_status = summary.clone();
3493        too_many_status.status = (0..=HARNESS_RUN_GIT_STATUS_ENTRIES_MAX)
3494            .map(|index| HarnessRunGitStatusEntryV1 {
3495                index_status: HarnessRunGitStatusCodeV1::Unmodified,
3496                worktree_status: HarnessRunGitStatusCodeV1::Modified,
3497                path: format!("src/file-{index:05}.rs"),
3498                previous_path: None,
3499            })
3500            .collect();
3501        assert_eq!(
3502            too_many_status.validate(),
3503            Err(HarnessValidationError::InvalidGitSummary),
3504        );
3505
3506        let mut too_many_commits = summary.clone();
3507        too_many_commits.recent_commits = (0..=HARNESS_RUN_GIT_RECENT_COMMITS_MAX)
3508            .map(|index| HarnessRunGitCommitSummaryV1 {
3509                id: format!("{index:040x}"),
3510                summary: "commit".to_owned(),
3511            })
3512            .collect();
3513        assert_eq!(
3514            too_many_commits.validate(),
3515            Err(HarnessValidationError::InvalidGitSummary),
3516        );
3517
3518        let mut unsorted = summary.clone();
3519        unsorted.status = vec![
3520            HarnessRunGitStatusEntryV1 { path: "b.rs".to_owned(), ..clean_entry.clone() },
3521            HarnessRunGitStatusEntryV1 { path: "a.rs".to_owned(), ..clean_entry.clone() },
3522        ];
3523        assert_eq!(unsorted.validate(), Err(HarnessValidationError::InvalidGitSummary));
3524
3525        let mut duplicate_commit_ids = summary.clone();
3526        duplicate_commit_ids.recent_commits = vec![commit.clone(), commit.clone()];
3527        assert_eq!(
3528            duplicate_commit_ids.validate(),
3529            Err(HarnessValidationError::InvalidGitSummary),
3530        );
3531
3532        let mut bad_commit_id = summary.clone();
3533        bad_commit_id.recent_commits = vec![HarnessRunGitCommitSummaryV1 {
3534            id: "not-hex".to_owned(),
3535            summary: "bad".to_owned(),
3536        }];
3537        assert_eq!(
3538            bad_commit_id.validate(),
3539            Err(HarnessValidationError::InvalidGitSummary),
3540        );
3541
3542        let mut rename_without_previous_path = summary.clone();
3543        rename_without_previous_path.status = vec![HarnessRunGitStatusEntryV1 {
3544            index_status: HarnessRunGitStatusCodeV1::Renamed,
3545            worktree_status: HarnessRunGitStatusCodeV1::Unmodified,
3546            path: "new.rs".to_owned(),
3547            previous_path: None,
3548        }];
3549        assert_eq!(
3550            rename_without_previous_path.validate(),
3551            Err(HarnessValidationError::InvalidGitSummary),
3552        );
3553
3554        let mut previous_path_without_rename = summary.clone();
3555        previous_path_without_rename.status = vec![HarnessRunGitStatusEntryV1 {
3556            index_status: HarnessRunGitStatusCodeV1::Modified,
3557            worktree_status: HarnessRunGitStatusCodeV1::Unmodified,
3558            path: "new.rs".to_owned(),
3559            previous_path: Some("old.rs".to_owned()),
3560        }];
3561        assert_eq!(
3562            previous_path_without_rename.validate(),
3563            Err(HarnessValidationError::InvalidGitSummary),
3564        );
3565
3566        let mut control_char_branch = summary.clone();
3567        control_char_branch.branch = Some("main\u{0007}".to_owned());
3568        assert_eq!(
3569            control_char_branch.validate(),
3570            Err(HarnessValidationError::InvalidGitSummary),
3571        );
3572
3573        let facts = HarnessRunGitFactsV1 {
3574            captured_at_unix_ms: 10,
3575            outcome: HarnessRunGitFactsOutcomeV1::Captured(summary.clone()),
3576        };
3577        facts.validate().unwrap();
3578
3579        let mut invalid_facts = facts.clone();
3580        invalid_facts.outcome = HarnessRunGitFactsOutcomeV1::Captured(bad_commit_id);
3581        assert_eq!(
3582            invalid_facts.validate(),
3583            Err(HarnessValidationError::InvalidGitSummary),
3584        );
3585
3586        let unavailable = HarnessRunGitFactsV1 {
3587            captured_at_unix_ms: 10,
3588            outcome: HarnessRunGitFactsOutcomeV1::Unavailable,
3589        };
3590        unavailable.validate().unwrap();
3591    }
3592
3593    #[test]
3594    fn harness_result_ref_for_run_round_trips_and_rejects_malformed() {
3595        let run = run_id('7');
3596        let result_ref = HarnessResultRef::for_run(&run);
3597        assert_eq!(result_ref.as_str(), format!("hresult_{}", "7".repeat(24)));
3598        assert_eq!(result_ref.run_id(), Ok(run.clone()));
3599
3600        // A result reference must carry its own "hresult_" prefix — the
3601        // run's own "hrun_" wire string is not interchangeable with its
3602        // derived reference, even though the hex body is byte-identical.
3603        assert!(HarnessResultRef::new(run.as_str().to_owned()).is_err());
3604        // Malformed hex bodies are rejected the same way any opaque id is.
3605        assert!(HarnessResultRef::new(format!("hresult_{}", "g".repeat(24))).is_err());
3606        assert!(HarnessResultRef::new(format!("hresult_{}", "7".repeat(23))).is_err());
3607    }
3608
3609    #[test]
3610    fn harness_operation_kind_targets_accept_record_run_git_facts_and_record_task_result_ref() {
3611        let git_facts_operation = HarnessOperationV1 {
3612            operation_id: operation_id('8'),
3613            revision: HarnessRevision::new(1).unwrap(),
3614            actor: HarnessActorV1::ParentRun { run_id: run_id('7') },
3615            kind: HarnessOperationKindV1::RecordRunGitFacts,
3616            state: HarnessOperationStateV1::Prepared,
3617            task_id: None,
3618            run_id: Some(run_id('7')),
3619            grant_id: None,
3620            reconciles_operation_id: None,
3621            expected_revision: Some(HarnessRevision::new(1).unwrap()),
3622            request_digest: HarnessRequestDigest::new("a".repeat(64)).unwrap(),
3623            idempotency_ref: HarnessIdempotencyRef::new(format!("hidem_{}", "b".repeat(24))).unwrap(),
3624            failure: None,
3625            outcome_unknown_reason: None,
3626            reconciliation_outcome: None,
3627            created_at_unix_ms: 1_000,
3628            updated_at_unix_ms: 1_000,
3629            dispatched_at_unix_ms: None,
3630            finished_at_unix_ms: None,
3631        };
3632        git_facts_operation.validate().unwrap();
3633
3634        let mut misdirected = git_facts_operation.clone();
3635        misdirected.task_id = Some(task_id('1'));
3636        assert_eq!(misdirected.validate(), Err(HarnessValidationError::InvalidOperationTarget));
3637
3638        let mut result_ref_operation = git_facts_operation.clone();
3639        result_ref_operation.kind = HarnessOperationKindV1::RecordTaskResultRef;
3640        result_ref_operation.task_id = Some(task_id('1'));
3641        result_ref_operation.run_id = None;
3642        result_ref_operation.validate().unwrap();
3643
3644        let mut missing_task = result_ref_operation.clone();
3645        missing_task.task_id = None;
3646        assert_eq!(missing_task.validate(), Err(HarnessValidationError::InvalidOperationTarget));
3647    }
3648
3649    #[test]
3650    fn bound_continuation_allows_exact_managed_target_workspace() {
3651        let source_binding = HarnessSessionBindingV1 {
3652            node_id: selector("node-a"),
3653            node_incarnation: selector("incarnation-a"),
3654            workspace_id: selector("workspace-a"),
3655            session: HarnessSessionIdentityV1::Managed {
3656                record_id: selector("record-source"),
3657                active_session: Some(HarnessRuntimeIdentityV1 {
3658                    instance_id: 1,
3659                    generation: 1,
3660                }),
3661            },
3662        };
3663        let mut continuation = HarnessContinuationV1 {
3664            continuation_ref: HarnessContinuationRef::new(format!(
3665                "hcontinuation_{}",
3666                "a".repeat(24),
3667            )).unwrap(),
3668            receipt_ref: HarnessReceiptRef::new(format!(
3669                "hreceipt_{}",
3670                "b".repeat(24),
3671            )).unwrap(),
3672            revision: HarnessRevision::new(4).unwrap(),
3673            state: HarnessContinuationStateV1::Bound,
3674            authority: HarnessTransferAuthorityRefV1::ParentGrant {
3675                grant_id: grant_id('3'),
3676                revision: HarnessRevision::new(1).unwrap(),
3677            },
3678            source_run_id: run_id('4'),
3679            target_run_id: run_id('5'),
3680            operation_id: operation_id('6'),
3681            node_id: selector("node-a"),
3682            node_incarnation: selector("incarnation-a"),
3683            workspace_id: selector("workspace-a"),
3684            source_provider: selector("claude"),
3685            source_binding,
3686            context: Some(HarnessResolvedContextPackReceiptV1 {
3687                id: selector("context-a"),
3688                digest: format!("sha256:{}", "c".repeat(64)),
3689                lineage: HarnessContextPackLineageV1 {
3690                    source_node_id: selector("node-a"),
3691                    source_workspace_id: selector("workspace-a"),
3692                    source_instance_id: 1,
3693                    source_generation: 1,
3694                    source_provider: selector("claude"),
3695                },
3696                source_message_count: 2,
3697                retained_message_count: 2,
3698                byte_len: 64,
3699                truncated: false,
3700            }),
3701            target_binding: Some(HarnessSessionBindingV1 {
3702                node_id: selector("node-a"),
3703                node_incarnation: selector("incarnation-a"),
3704                workspace_id: selector("workspace-a"),
3705                session: HarnessSessionIdentityV1::Managed {
3706                    record_id: selector("record-target"),
3707                    active_session: Some(HarnessRuntimeIdentityV1 {
3708                        instance_id: 2,
3709                        generation: 1,
3710                    }),
3711                },
3712            }),
3713            prepared_at_unix_ms: 1_000,
3714            exporting_at_unix_ms: Some(1_100),
3715            exported_at_unix_ms: Some(1_200),
3716            bound_at_unix_ms: Some(1_300),
3717            expired_at_unix_ms: None,
3718            outcome_unknown_at_unix_ms: None,
3719            outcome_unknown_reason: None,
3720            cleanup_state: HarnessContinuationCleanupStateV1::Retained,
3721            created_at_unix_ms: 1_000,
3722            updated_at_unix_ms: 1_300,
3723        };
3724
3725        continuation.validate().unwrap();
3726        continuation.target_binding.as_mut().unwrap().workspace_id =
3727            selector("workspace-allocated");
3728        continuation.validate().unwrap();
3729
3730        continuation.target_binding.as_mut().unwrap().node_id = selector("node-b");
3731        assert_eq!(
3732            continuation.validate(),
3733            Err(HarnessValidationError::InvalidContinuationLink),
3734        );
3735        continuation.target_binding.as_mut().unwrap().node_id = selector("node-a");
3736        continuation.target_binding.as_mut().unwrap().node_incarnation =
3737            selector("incarnation-b");
3738        assert_eq!(
3739            continuation.validate(),
3740            Err(HarnessValidationError::InvalidContinuationLink),
3741        );
3742    }
3743
3744    #[test]
3745    fn harness_operation_state_is_bounded_and_private() {
3746        let prepared = HarnessOperationV1 {
3747            operation_id: operation_id('5'),
3748            revision: HarnessRevision::new(1).unwrap(),
3749            actor: HarnessActorV1::User { actor_id: selector("operator") },
3750            kind: HarnessOperationKindV1::CreateTask,
3751            state: HarnessOperationStateV1::Prepared,
3752            task_id: Some(task_id('6')),
3753            run_id: None,
3754            grant_id: None,
3755            reconciles_operation_id: None,
3756            expected_revision: None,
3757            request_digest: HarnessRequestDigest::new("a".repeat(64)).unwrap(),
3758            idempotency_ref: HarnessIdempotencyRef::new(format!("hidem_{}", "b".repeat(24))).unwrap(),
3759            failure: None,
3760            outcome_unknown_reason: None,
3761            reconciliation_outcome: None,
3762            created_at_unix_ms: 1_000,
3763            updated_at_unix_ms: 1_000,
3764            dispatched_at_unix_ms: None,
3765            finished_at_unix_ms: None,
3766        };
3767        prepared.validate().unwrap();
3768
3769        let json = serde_json::to_string(&prepared).unwrap();
3770        for private_name in ["prompt", "transcript", "credential", "bearer", "working_directory", "provider_home"] {
3771            assert!(!json.contains(private_name));
3772        }
3773
3774        let mut unknown = prepared.clone();
3775        unknown.state = HarnessOperationStateV1::OutcomeUnknown;
3776        unknown.updated_at_unix_ms = 1_100;
3777        unknown.dispatched_at_unix_ms = Some(1_050);
3778        unknown.outcome_unknown_reason = Some(HarnessOutcomeUnknownReasonV1::ReplyLost);
3779        unknown.validate().unwrap();
3780
3781        let mut invalid_failed = unknown.clone();
3782        invalid_failed.state = HarnessOperationStateV1::Failed;
3783        invalid_failed.finished_at_unix_ms = Some(1_100);
3784        invalid_failed.outcome_unknown_reason = None;
3785        assert_eq!(invalid_failed.validate(), Err(HarnessValidationError::InvalidOperationStateFields));
3786
3787        let mut invalid_target = prepared.clone();
3788        invalid_target.run_id = Some(run_id('7'));
3789        assert_eq!(invalid_target.validate(), Err(HarnessValidationError::InvalidOperationTarget));
3790
3791        let mut self_reconcile = prepared.clone();
3792        self_reconcile.kind = HarnessOperationKindV1::Reconcile;
3793        self_reconcile.task_id = None;
3794        self_reconcile.run_id = Some(run_id('7'));
3795        self_reconcile.reconciles_operation_id = Some(self_reconcile.operation_id.clone());
3796        self_reconcile.expected_revision = Some(HarnessRevision::new(1).unwrap());
3797        assert_eq!(
3798            self_reconcile.validate(),
3799            Err(HarnessValidationError::SelfLink { field: "reconciles_operation_id" })
3800        );
3801
3802        let mut targetless_reconcile = self_reconcile.clone();
3803        targetless_reconcile.reconciles_operation_id = Some(operation_id('8'));
3804        targetless_reconcile.run_id = None;
3805        assert_eq!(targetless_reconcile.validate(), Err(HarnessValidationError::InvalidOperationTarget));
3806
3807        let mut undispatched_create_run = prepared.clone();
3808        undispatched_create_run.kind = HarnessOperationKindV1::CreateRun;
3809        undispatched_create_run.run_id = Some(run_id('7'));
3810        undispatched_create_run.expected_revision = Some(HarnessRevision::new(1).unwrap());
3811        undispatched_create_run.state = HarnessOperationStateV1::Succeeded;
3812        undispatched_create_run.updated_at_unix_ms = 1_100;
3813        undispatched_create_run.finished_at_unix_ms = Some(1_100);
3814        assert_eq!(
3815            undispatched_create_run.validate(),
3816            Err(HarnessValidationError::InvalidOperationStateFields)
3817        );
3818
3819        let mut dispatched_create_run = undispatched_create_run;
3820        dispatched_create_run.dispatched_at_unix_ms = Some(1_050);
3821        dispatched_create_run.validate().unwrap();
3822
3823        let mut wire = serde_json::to_value(prepared).unwrap();
3824        wire.as_object_mut().unwrap().insert("raw_spawn_spec".to_owned(), serde_json::json!({"prompt":"private"}));
3825        assert!(serde_json::from_value::<HarnessOperationV1>(wire).is_err());
3826    }
3827
3828    #[test]
3829    fn harness_delivery_receipts_are_exact_bounded_and_private() {
3830        let bundle = HarnessDeliveryBundleV1 {
3831            selector: selector("review-kit"),
3832            bundle_id: HarnessDeliveryBundleIdV1::new("bundle.review-kit").unwrap(),
3833            revision: HarnessDeliveryBundleRevisionV1::new("rev-7").unwrap(),
3834            digest: HarnessDeliveryBundleDigestV1::new(format!(
3835                "sha256:{}",
3836                "d".repeat(64),
3837            )).unwrap(),
3838            manifest_digest: HarnessDeliveryManifestDigestV2::new(format!(
3839                "sha256:{}",
3840                "e".repeat(64),
3841            )).unwrap(),
3842        };
3843        let delivery_ref = HarnessDeliveryRef::new(format!(
3844            "hdelivery_{}",
3845            "d".repeat(24),
3846        )).unwrap();
3847        let receipt = HarnessDeliveryReceiptV1 {
3848            receipt_ref: HarnessReceiptRef::new(format!(
3849                "hreceipt_{}",
3850                "e".repeat(24),
3851            )).unwrap(),
3852            delivery_ref: delivery_ref.clone(),
3853            authority: HarnessTransferAuthorityRefV1::ParentGrant {
3854                grant_id: grant_id('4'),
3855                revision: HarnessRevision::new(2).unwrap(),
3856            },
3857            task_id: task_id('1'),
3858            run_id: run_id('2'),
3859            operation_id: operation_id('3'),
3860            binding: HarnessSessionBindingV1 {
3861                node_id: selector("node-a"),
3862                node_incarnation: selector("incarnation-a"),
3863                workspace_id: selector("workspace-a"),
3864                session: HarnessSessionIdentityV1::Managed {
3865                    record_id: selector("record-a"),
3866                    active_session: Some(HarnessRuntimeIdentityV1 {
3867                        instance_id: 7,
3868                        generation: 3,
3869                    }),
3870                },
3871            },
3872            bundle: bundle.clone(),
3873            committed_at_unix_ms: 30,
3874        };
3875        let delivery = HarnessDeliveryV1 {
3876            delivery_ref,
3877            revision: HarnessRevision::new(3).unwrap(),
3878            authority: HarnessTransferAuthorityRefV1::ParentGrant {
3879                grant_id: grant_id('4'),
3880                revision: HarnessRevision::new(2).unwrap(),
3881            },
3882            task_id: task_id('1'),
3883            run_id: run_id('2'),
3884            operation_id: operation_id('3'),
3885            bundle: bundle.clone(),
3886            state: HarnessDeliveryStateV1::Committed,
3887            stage_receipt: Some(HarnessDeliveryStageReceiptV1 {
3888                node_id: selector("node-a"),
3889                node_incarnation: selector("incarnation-a"),
3890                workspace_id: selector("workspace-a"),
3891                bundle,
3892                staged_at_unix_ms: 20,
3893            }),
3894            receipt: Some(receipt),
3895            created_at_unix_ms: 10,
3896            updated_at_unix_ms: 30,
3897        };
3898        delivery.validate().unwrap();
3899        let wire = serde_json::to_string(&delivery).unwrap();
3900        assert!(wire.contains("\"authority\""));
3901        let canonical = serde_json::to_value(&delivery).unwrap();
3902        let canonical_object = canonical.as_object().unwrap();
3903        assert!(!canonical_object.contains_key("grant_id"));
3904        assert!(!canonical_object.contains_key("grant_revision"));
3905        let canonical_receipt = canonical_object.get("receipt").unwrap()
3906            .as_object().unwrap();
3907        assert!(!canonical_receipt.contains_key("grant_id"));
3908        assert!(!canonical_receipt.contains_key("grant_revision"));
3909        for sentinel in ["prompt", "transcript", "bearer", "credential", "provider_home"] {
3910            assert!(!wire.contains(sentinel));
3911        }
3912
3913        let mut legacy = serde_json::to_value(&delivery).unwrap();
3914        let delivery_object = legacy.as_object_mut().unwrap();
3915        delivery_object.remove("authority");
3916        delivery_object.insert(
3917            "grant_id".to_owned(),
3918            serde_json::to_value(grant_id('4')).unwrap(),
3919        );
3920        delivery_object.insert(
3921            "grant_revision".to_owned(),
3922            serde_json::to_value(HarnessRevision::new(2).unwrap()).unwrap(),
3923        );
3924        let receipt_object = delivery_object.get_mut("receipt").unwrap()
3925            .as_object_mut().unwrap();
3926        receipt_object.remove("authority");
3927        receipt_object.insert(
3928            "grant_id".to_owned(),
3929            serde_json::to_value(grant_id('4')).unwrap(),
3930        );
3931        receipt_object.insert(
3932            "grant_revision".to_owned(),
3933            serde_json::to_value(HarnessRevision::new(2).unwrap()).unwrap(),
3934        );
3935        let migrated: HarnessDeliveryV1 = serde_json::from_value(legacy.clone()).unwrap();
3936        assert_eq!(migrated, delivery);
3937        let migrated_wire = serde_json::to_string(&migrated).unwrap();
3938        assert!(migrated_wire.contains("\"authority\""));
3939        let migrated_value = serde_json::to_value(&migrated).unwrap();
3940        assert!(!migrated_value.as_object().unwrap().contains_key("grant_id"));
3941
3942        legacy.as_object_mut().unwrap().insert(
3943            "authority".to_owned(),
3944            serde_json::to_value(HarnessTransferAuthorityRefV1::ParentGrant {
3945                grant_id: grant_id('4'),
3946                revision: HarnessRevision::new(2).unwrap(),
3947            }).unwrap(),
3948        );
3949        assert!(serde_json::from_value::<HarnessDeliveryV1>(legacy).is_err());
3950
3951        let mut changed = delivery;
3952        changed.receipt.as_mut().unwrap().bundle.manifest_digest =
3953            HarnessDeliveryManifestDigestV2::new(format!(
3954                "sha256:{}",
3955                "f".repeat(64),
3956            )).unwrap();
3957        assert_eq!(changed.validate(), Err(HarnessValidationError::InvalidDeliveryLink));
3958    }
3959
3960    #[test]
3961    fn ordinary_operator_execution_spec_round_trip_is_exact_and_private() {
3962        let scheduled_launch = HarnessScheduledLaunchRefV2 {
3963            plan: HarnessLaunchPlanRefV1 {
3964                plan_id: selector("ordinary-codex"),
3965                revision: HarnessRevision::new(3).unwrap(),
3966                digest: HarnessRequestDigest::new("a".repeat(64)).unwrap(),
3967            },
3968            authority: HarnessLaunchAuthorityRefV1::OrdinaryOperator,
3969        };
3970        let spec = HarnessTaskExecutionSpecV1 {
3971            execution_spec_id: HarnessExecutionSpecId::new(format!(
3972                "hespec_{}",
3973                "b".repeat(24),
3974            )).unwrap(),
3975            revision: HarnessRevision::new(2).unwrap(),
3976            task_id: task_id('1'),
3977            scheduled_launch: scheduled_launch.clone(),
3978            scheduled_launch_digest: HarnessRequestDigest::new("c".repeat(64)).unwrap(),
3979            review_policy: HarnessTaskReviewPolicyV1::OperatorReview,
3980            created_at_unix_ms: 10,
3981            updated_at_unix_ms: 20,
3982        };
3983        spec.validate().unwrap();
3984        let encoded = serde_json::to_string(&spec).unwrap();
3985        for sentinel in [
3986            "delivery",
3987            "continuation",
3988            "harness_mcp",
3989            "provider_home",
3990            "credential",
3991            "environment",
3992            "raw_path",
3993        ] {
3994            assert!(!encoded.contains(sentinel));
3995        }
3996        assert_eq!(serde_json::from_str::<HarnessTaskExecutionSpecV1>(&encoded).unwrap(), spec);
3997
3998        let replace = HarnessReplaceTaskExecutionSpecRequestV1 {
3999            authority: HarnessOperatorAuthorityV1 {
4000                operation_id: operation_id('4'),
4001                idempotency_ref: HarnessIdempotencyRef::new(format!(
4002                    "hidem_{}",
4003                    "5".repeat(24),
4004                )).unwrap(),
4005                actor_id: selector("operator"),
4006                now_unix_ms: 30,
4007            },
4008            task_id: task_id('1'),
4009            expected_task_revision: HarnessRevision::new(7).unwrap(),
4010            expected_execution_spec_revision: HarnessExpectedExecutionSpecRevisionV1::Absent,
4011            spec: HarnessTaskExecutionSpecInputV1 {
4012                scheduled_launch,
4013                review_policy: HarnessTaskReviewPolicyV1::OperatorReview,
4014            },
4015        };
4016        replace.validate().unwrap();
4017        let mut wire = serde_json::to_value(replace).unwrap();
4018        wire.as_object_mut().unwrap().insert(
4019            "spawn_spec".to_owned(),
4020            serde_json::json!({"environment":{"TOKEN":"secret"}}),
4021        );
4022        assert!(serde_json::from_value::<HarnessReplaceTaskExecutionSpecRequestV1>(wire).is_err());
4023    }
4024
4025    #[test]
4026    fn task_launch_issuance_is_bounded_exact_and_context_requires_messages() {
4027        let bundle = HarnessDeliveryBundleV1 {
4028            selector: selector("review-kit"),
4029            bundle_id: HarnessDeliveryBundleIdV1::new("bundle.review-kit").unwrap(),
4030            revision: HarnessDeliveryBundleRevisionV1::new("rev-7").unwrap(),
4031            digest: HarnessDeliveryBundleDigestV1::new(format!(
4032                "sha256:{}",
4033                "d".repeat(64),
4034            )).unwrap(),
4035            manifest_digest: HarnessDeliveryManifestDigestV2::new(format!(
4036                "sha256:{}",
4037                "e".repeat(64),
4038            )).unwrap(),
4039        };
4040        let context_source = HarnessContextSourceSelectionV1 {
4041            source_run_id: run_id('2'),
4042            source_run_revision: HarnessRevision::new(4).unwrap(),
4043            observed_at_unix_ms: 30,
4044            metadata_digest: HarnessRequestDigest::new("4".repeat(64)).unwrap(),
4045            node_id: selector("node-a"),
4046            node_incarnation: selector("incarnation-a"),
4047            workspace_id: selector("workspace-a"),
4048            session_record_id: selector("record-a"),
4049            active_session: Some(HarnessRuntimeIdentityV1 {
4050                instance_id: 7,
4051                generation: 3,
4052            }),
4053            message_count: 12,
4054            message_count_exact: true,
4055            completed_turn_count: Some(5),
4056            total_tokens: Some(900),
4057            availability: HarnessContextSourceAvailabilityV1::Live,
4058            context_pack: None,
4059        };
4060        context_source.validate().unwrap();
4061        let issuance = HarnessTaskLaunchIssuanceV1 {
4062            issuance_id: HarnessTaskLaunchIssuanceId::new(format!(
4063                "hissue_{}",
4064                "1".repeat(24),
4065            )).unwrap(),
4066            revision: HarnessRevision::new(1).unwrap(),
4067            digest: HarnessRequestDigest::new("1".repeat(64)).unwrap(),
4068            task_id: task_id('1'),
4069            task_revision: HarnessRevision::new(7).unwrap(),
4070            plan: HarnessLaunchPlanRefV1 {
4071                plan_id: selector("managed-review"),
4072                revision: HarnessRevision::new(2).unwrap(),
4073                digest: HarnessRequestDigest::new("2".repeat(64)).unwrap(),
4074            },
4075            target: HarnessLaunchTargetSelectionV1 {
4076                node_id: selector("node-a"),
4077                source_workspace_id: selector("workspace-a"),
4078                worktree: HarnessLaunchWorktreeSelectionV1::Managed {
4079                    profile_id: selector("review-tree"),
4080                    expected_profile_revision: selector("revision-7"),
4081                },
4082                provider_profile: selector("claude"),
4083                mode: HarnessExecutionModeV1::Pty,
4084            },
4085            context_source: Some(context_source.clone()),
4086            delivery: Some(HarnessDeliveryBundleSelectionV1 {
4087                bundle,
4088                component_counts: vec![
4089                    HarnessDeliveryComponentCountV1 {
4090                        kind: HarnessDeliveryComponentKindV1::Skill,
4091                        workspace_count: 2,
4092                        session_count: 0,
4093                    },
4094                    HarnessDeliveryComponentCountV1 {
4095                        kind: HarnessDeliveryComponentKindV1::Prompt,
4096                        workspace_count: 0,
4097                        session_count: 1,
4098                    },
4099                ],
4100            }),
4101            policy_digest: HarnessRequestDigest::new("3".repeat(64)).unwrap(),
4102            created_at_unix_ms: 40,
4103            updated_at_unix_ms: 40,
4104        };
4105        issuance.validate().unwrap();
4106        let spec = HarnessTaskExecutionSpecV2 {
4107            execution_spec_id: HarnessExecutionSpecId::new(format!(
4108                "hespec_{}",
4109                "5".repeat(24),
4110            )).unwrap(),
4111            revision: HarnessRevision::new(1).unwrap(),
4112            task_id: task_id('1'),
4113            launch_issuance: issuance.reference(),
4114            review_policy: HarnessTaskReviewPolicyV1::OperatorReview,
4115            created_at_unix_ms: 40,
4116            updated_at_unix_ms: 40,
4117        };
4118        spec.validate().unwrap();
4119        let encoded = serde_json::to_string(&(issuance, spec)).unwrap();
4120        for sentinel in ["raw_path", "prompt_body", "credential", "provider_home"] {
4121            assert!(!encoded.contains(sentinel));
4122        }
4123
4124        let mut empty_context = context_source;
4125        empty_context.message_count = 0;
4126        empty_context.completed_turn_count = Some(0);
4127        empty_context.total_tokens = Some(0);
4128        assert_eq!(
4129            empty_context.validate(),
4130            Err(HarnessValidationError::InvalidContextSourceCounts),
4131        );
4132    }
4133
4134    #[test]
4135    fn context_source_availability_invariants_require_matching_active_session_or_pack() {
4136        let pack = HarnessResolvedContextPackReceiptV1 {
4137            id: selector("context-b"),
4138            digest: format!("sha256:{}", "d".repeat(64)),
4139            lineage: HarnessContextPackLineageV1 {
4140                source_node_id: selector("node-a"),
4141                source_workspace_id: selector("workspace-a"),
4142                source_instance_id: 7,
4143                source_generation: 3,
4144                source_provider: selector("claude"),
4145            },
4146            source_message_count: 12,
4147            retained_message_count: 12,
4148            byte_len: 128,
4149            truncated: false,
4150        };
4151        let base = HarnessContextSourceSelectionV1 {
4152            source_run_id: run_id('2'),
4153            source_run_revision: HarnessRevision::new(4).unwrap(),
4154            observed_at_unix_ms: 30,
4155            metadata_digest: HarnessRequestDigest::new("4".repeat(64)).unwrap(),
4156            node_id: selector("node-a"),
4157            node_incarnation: selector("incarnation-a"),
4158            workspace_id: selector("workspace-a"),
4159            session_record_id: selector("record-a"),
4160            active_session: Some(HarnessRuntimeIdentityV1 {
4161                instance_id: 7,
4162                generation: 3,
4163            }),
4164            message_count: 12,
4165            message_count_exact: true,
4166            completed_turn_count: Some(5),
4167            total_tokens: Some(900),
4168            availability: HarnessContextSourceAvailabilityV1::Live,
4169            context_pack: None,
4170        };
4171        base.validate().unwrap();
4172
4173        let mut live_missing_session = base.clone();
4174        live_missing_session.active_session = None;
4175        assert_eq!(
4176            live_missing_session.validate(),
4177            Err(HarnessValidationError::InvalidContextSourceCounts),
4178        );
4179
4180        let mut live_with_pack = base.clone();
4181        live_with_pack.context_pack = Some(pack.clone());
4182        assert_eq!(
4183            live_with_pack.validate(),
4184            Err(HarnessValidationError::InvalidContextSourceCounts),
4185        );
4186
4187        let mut durable = base.clone();
4188        durable.availability = HarnessContextSourceAvailabilityV1::Durable;
4189        durable.active_session = None;
4190        durable.context_pack = Some(pack.clone());
4191        durable.validate().unwrap();
4192
4193        let mut durable_missing_pack = durable.clone();
4194        durable_missing_pack.context_pack = None;
4195        assert_eq!(
4196            durable_missing_pack.validate(),
4197            Err(HarnessValidationError::InvalidContextSourceCounts),
4198        );
4199    }
4200
4201    #[test]
4202    fn context_source_counts_accept_the_unobserved_live_shape_but_not_incoherent_mixes() {
4203        let base = HarnessContextSourceSelectionV1 {
4204            source_run_id: run_id('3'),
4205            source_run_revision: HarnessRevision::new(4).unwrap(),
4206            observed_at_unix_ms: 30,
4207            metadata_digest: HarnessRequestDigest::new("4".repeat(64)).unwrap(),
4208            node_id: selector("node-a"),
4209            node_incarnation: selector("incarnation-a"),
4210            workspace_id: selector("workspace-a"),
4211            session_record_id: selector("record-a"),
4212            active_session: Some(HarnessRuntimeIdentityV1 {
4213                instance_id: 7,
4214                generation: 3,
4215            }),
4216            message_count: 0,
4217            message_count_exact: false,
4218            completed_turn_count: None,
4219            total_tokens: None,
4220            availability: HarnessContextSourceAvailabilityV1::Live,
4221            context_pack: None,
4222        };
4223        base.validate().unwrap();
4224
4225        let mut exact_zero = base.clone();
4226        exact_zero.message_count_exact = true;
4227        assert_eq!(
4228            exact_zero.validate(),
4229            Err(HarnessValidationError::InvalidContextSourceCounts),
4230        );
4231
4232        let mut leftover_turn_count = base.clone();
4233        leftover_turn_count.completed_turn_count = Some(0);
4234        assert_eq!(
4235            leftover_turn_count.validate(),
4236            Err(HarnessValidationError::InvalidContextSourceCounts),
4237        );
4238
4239        let mut leftover_tokens = base.clone();
4240        leftover_tokens.total_tokens = Some(0);
4241        assert_eq!(
4242            leftover_tokens.validate(),
4243            Err(HarnessValidationError::InvalidContextSourceCounts),
4244        );
4245
4246        // The bypass is Live-only: a Durable source (pack itself perfectly
4247        // valid) whose own `message_count` field is left at zero must still
4248        // be rejected exactly as before -- Durable never gets an unobserved
4249        // shape, its count always comes from the resolved pack.
4250        let mut durable_zero_count = base;
4251        durable_zero_count.availability = HarnessContextSourceAvailabilityV1::Durable;
4252        durable_zero_count.active_session = None;
4253        durable_zero_count.context_pack = Some(HarnessResolvedContextPackReceiptV1 {
4254            id: selector("context-c"),
4255            digest: format!("sha256:{}", "e".repeat(64)),
4256            lineage: HarnessContextPackLineageV1 {
4257                source_node_id: selector("node-a"),
4258                source_workspace_id: selector("workspace-a"),
4259                source_instance_id: 7,
4260                source_generation: 3,
4261                source_provider: selector("claude"),
4262            },
4263            source_message_count: 12,
4264            retained_message_count: 12,
4265            byte_len: 128,
4266            truncated: false,
4267        });
4268        assert_eq!(
4269            durable_zero_count.validate(),
4270            Err(HarnessValidationError::InvalidContextSourceCounts),
4271        );
4272    }
4273
4274    #[test]
4275    fn delivery_component_counts_are_scope_preserving_and_canonical() {
4276        let bundle = HarnessDeliveryBundleV1 {
4277            selector: selector("review-kit"),
4278            bundle_id: HarnessDeliveryBundleIdV1::new("bundle.review-kit").unwrap(),
4279            revision: HarnessDeliveryBundleRevisionV1::new("rev-7").unwrap(),
4280            digest: HarnessDeliveryBundleDigestV1::new(format!(
4281                "sha256:{}",
4282                "d".repeat(64),
4283            )).unwrap(),
4284            manifest_digest: HarnessDeliveryManifestDigestV2::new(format!(
4285                "sha256:{}",
4286                "e".repeat(64),
4287            )).unwrap(),
4288        };
4289        let mut selection = HarnessDeliveryBundleSelectionV1 {
4290            bundle,
4291            component_counts: vec![HarnessDeliveryComponentCountV1 {
4292                kind: HarnessDeliveryComponentKindV1::Skill,
4293                workspace_count: 1,
4294                session_count: 2,
4295            }],
4296        };
4297        selection.validate().unwrap();
4298        selection.component_counts[0].workspace_count = 0;
4299        selection.component_counts[0].session_count = 0;
4300        assert_eq!(
4301            selection.validate(),
4302            Err(HarnessValidationError::InvalidDeliveryComponentCount),
4303        );
4304        selection.component_counts.clear();
4305        assert_eq!(
4306            selection.validate(),
4307            Err(HarnessValidationError::InvalidDeliveryComponentCount),
4308        );
4309    }
4310
4311    #[test]
4312    fn exact_start_request_pins_task_spec_and_scheduled_digest() {
4313        let request = HarnessStartTaskRequestV1 {
4314            authority: HarnessOperatorAuthorityV1 {
4315                operation_id: operation_id('6'),
4316                idempotency_ref: HarnessIdempotencyRef::new(format!(
4317                    "hidem_{}",
4318                    "7".repeat(24),
4319                )).unwrap(),
4320                actor_id: selector("operator"),
4321                now_unix_ms: 40,
4322            },
4323            task_id: task_id('1'),
4324            expected_task_revision: HarnessRevision::new(8).unwrap(),
4325            expected_execution_spec_revision: HarnessRevision::new(2).unwrap(),
4326            expected_scheduled_launch_digest: HarnessRequestDigest::new("d".repeat(64)).unwrap(),
4327        };
4328        request.validate().unwrap();
4329        let encoded = serde_json::to_vec(&request).unwrap();
4330        assert_eq!(serde_json::from_slice::<HarnessStartTaskRequestV1>(&encoded).unwrap(), request);
4331
4332        let mut invalid = serde_json::to_value(request).unwrap();
4333        invalid.as_object_mut().unwrap().insert(
4334            "expected_scheduled_launch_digest".to_owned(),
4335            serde_json::json!("D".repeat(64)),
4336        );
4337        assert!(serde_json::from_value::<HarnessStartTaskRequestV1>(invalid).is_err());
4338    }
4339
4340    #[test]
4341    fn harness_task_create_result_round_trips_and_names_each_refusal() {
4342        let created = HarnessTaskCreateResultV1::Created {
4343            task_id: task_id('1'),
4344            revision: HarnessRevision::new(1).unwrap(),
4345        };
4346        created.validate().unwrap();
4347        assert_eq!(
4348            serde_json::to_value(&created).unwrap(),
4349            serde_json::json!({
4350                "outcome": "created",
4351                "task_id": task_id('1').as_str(),
4352                "revision": 1,
4353            }),
4354        );
4355        let encoded = serde_json::to_vec(&created).unwrap();
4356        assert_eq!(
4357            serde_json::from_slice::<HarnessTaskCreateResultV1>(&encoded).unwrap(),
4358            created,
4359        );
4360
4361        let variants = [
4362            HarnessTaskCreateResultV1::ParentOutsideOwnSubtree {
4363                parent_task_id: task_id('2'),
4364                own_task_id: task_id('3'),
4365            },
4366            HarnessTaskCreateResultV1::ParentTerminal {
4367                parent_task_id: task_id('2'),
4368                state: HarnessTaskStateV1::Done,
4369            },
4370            HarnessTaskCreateResultV1::TitleInvalid { why: "task title is empty".to_owned() },
4371            HarnessTaskCreateResultV1::BodyInvalid { why: "task body is unbounded".to_owned() },
4372            HarnessTaskCreateResultV1::DependenciesInvalid {
4373                why: "dependencies must be strictly sorted and duplicate-free".to_owned(),
4374            },
4375        ];
4376        for variant in variants {
4377            variant.validate().unwrap();
4378            let encoded = serde_json::to_vec(&variant).unwrap();
4379            assert_eq!(
4380                serde_json::from_slice::<HarnessTaskCreateResultV1>(&encoded).unwrap(),
4381                variant,
4382            );
4383        }
4384
4385        let oversized_why = HarnessTaskCreateResultV1::TitleInvalid {
4386            why: "x".repeat(HARNESS_TASK_RESULT_WHY_MAX_BYTES + 1),
4387        };
4388        assert_eq!(oversized_why.validate(), Err(HarnessValidationError::InvalidTaskResultWhy));
4389        let empty_why = HarnessTaskCreateResultV1::BodyInvalid { why: String::new() };
4390        assert_eq!(empty_why.validate(), Err(HarnessValidationError::InvalidTaskResultWhy));
4391        let control_why = HarnessTaskCreateResultV1::DependenciesInvalid { why: "bad\u{0007}why".to_owned() };
4392        assert_eq!(control_why.validate(), Err(HarnessValidationError::InvalidTaskResultWhy));
4393    }
4394
4395    #[test]
4396    fn harness_task_move_result_round_trips_and_names_each_refusal() {
4397        let moved = HarnessTaskMoveResultV1::Moved {
4398            task_id: task_id('1'),
4399            revision: HarnessRevision::new(2).unwrap(),
4400            from: HarnessTaskStateV1::Ready,
4401            to: HarnessTaskStateV1::Running,
4402        };
4403        moved.validate().unwrap();
4404        assert_eq!(
4405            serde_json::to_value(&moved).unwrap(),
4406            serde_json::json!({
4407                "outcome": "moved",
4408                "task_id": task_id('1').as_str(),
4409                "revision": 2,
4410                "from": "ready",
4411                "to": "running",
4412            }),
4413        );
4414        let encoded = serde_json::to_vec(&moved).unwrap();
4415        assert_eq!(serde_json::from_slice::<HarnessTaskMoveResultV1>(&encoded).unwrap(), moved);
4416
4417        let revision_conflict = HarnessTaskMoveResultV1::RevisionConflict {
4418            task_id: task_id('1'),
4419            expected: HarnessRevision::new(2).unwrap(),
4420            current: HarnessRevision::new(3).unwrap(),
4421        };
4422        revision_conflict.validate().unwrap();
4423        assert_eq!(
4424            serde_json::to_value(&revision_conflict).unwrap(),
4425            serde_json::json!({
4426                "outcome": "revision-conflict",
4427                "task_id": task_id('1').as_str(),
4428                "expected": 2,
4429                "current": 3,
4430            }),
4431        );
4432
4433        let task_owned_by_live_run = HarnessTaskMoveResultV1::TaskOwnedByLiveRun {
4434            task_id: task_id('1'),
4435            run_id: run_id('2'),
4436        };
4437        task_owned_by_live_run.validate().unwrap();
4438        assert_eq!(
4439            serde_json::to_value(&task_owned_by_live_run).unwrap(),
4440            serde_json::json!({
4441                "outcome": "task-owned-by-live-run",
4442                "task_id": task_id('1').as_str(),
4443                "run_id": run_id('2').as_str(),
4444            }),
4445        );
4446
4447        let variants = [
4448            HarnessTaskMoveResultV1::TaskIsOwn { task_id: task_id('1') },
4449            HarnessTaskMoveResultV1::TaskOutsideOwnSubtree {
4450                task_id: task_id('1'),
4451                own_task_id: task_id('2'),
4452            },
4453            HarnessTaskMoveResultV1::IllegalTransition {
4454                task_id: task_id('1'),
4455                from: HarnessTaskStateV1::Done,
4456                to: HarnessTaskStateV1::Running,
4457            },
4458            task_owned_by_live_run,
4459            revision_conflict,
4460        ];
4461        for variant in variants {
4462            variant.validate().unwrap();
4463            let encoded = serde_json::to_vec(&variant).unwrap();
4464            assert_eq!(
4465                serde_json::from_slice::<HarnessTaskMoveResultV1>(&encoded).unwrap(),
4466                variant,
4467            );
4468        }
4469    }
4470
4471    #[test]
4472    fn harness_run_finish_outcome_and_result_round_trip() {
4473        assert_eq!(serde_json::to_value(HarnessRunFinishOutcomeV1::Done).unwrap(), "done");
4474        assert_eq!(serde_json::to_value(HarnessRunFinishOutcomeV1::Failed).unwrap(), "failed");
4475        assert!(serde_json::from_value::<HarnessRunFinishOutcomeV1>(
4476            serde_json::json!("in-progress")
4477        ).is_err());
4478
4479        let finished = HarnessRunFinishResultV1::Finished {
4480            run_id: run_id('1'),
4481            task_id: task_id('2'),
4482            result: HarnessRunFinishOutcomeV1::Done,
4483        };
4484        finished.validate().unwrap();
4485        assert_eq!(
4486            serde_json::to_value(&finished).unwrap(),
4487            serde_json::json!({
4488                "outcome": "finished",
4489                "run_id": run_id('1').as_str(),
4490                "task_id": task_id('2').as_str(),
4491                "result": "done",
4492            }),
4493        );
4494        let encoded = serde_json::to_vec(&finished).unwrap();
4495        assert_eq!(serde_json::from_slice::<HarnessRunFinishResultV1>(&encoded).unwrap(), finished);
4496
4497        let already_finished = HarnessRunFinishResultV1::AlreadyFinished {
4498            run_id: run_id('1'),
4499            lifecycle: HarnessRunLifecycleV1::Completed,
4500        };
4501        already_finished.validate().unwrap();
4502        assert_eq!(
4503            serde_json::to_value(&already_finished).unwrap(),
4504            serde_json::json!({
4505                "outcome": "already-finished",
4506                "run_id": run_id('1').as_str(),
4507                "lifecycle": "completed",
4508            }),
4509        );
4510        let encoded = serde_json::to_vec(&already_finished).unwrap();
4511        assert_eq!(
4512            serde_json::from_slice::<HarnessRunFinishResultV1>(&encoded).unwrap(),
4513            already_finished,
4514        );
4515    }
4516}