1use 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;
36pub 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 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 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#[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#[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#[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 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 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 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 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#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1974#[serde(rename_all = "kebab-case")]
1975pub enum HarnessRunFinishOutcomeV1 {
1976 Done,
1977 Failed,
1978}
1979
1980#[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#[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 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#[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")]
2315pub 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)]
2329pub 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#[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#[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 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 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 #[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 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 assert!(HarnessResultRef::new(run.as_str().to_owned()).is_err());
3604 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 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}