1use std::{collections::BTreeMap, fmt};
10
11pub use hatchery_harness_protocol::{
12 HarnessActorV1, HarnessApprovalLevelV1,
13 HarnessArtifactRef, HarnessEntityReadScopeV1, HarnessExecutionModeV1,
14 HarnessFailureCategoryV1, HarnessIdempotencyRef, HarnessMonitoringVisibilityV1,
15 HarnessOperationId,
16 HarnessOperationKindV1, HarnessOperationStateV1, HarnessOutcomeUnknownReasonV1,
17 HarnessCancelTaskRequestV1, HarnessCreateTaskRequestV1, HarnessDispatchIntentV1,
18 HarnessExpectedExecutionSpecRevisionV1, HarnessExecutionSpecId,
19 HarnessContinuationOutcomeUnknownReasonV1, HarnessContinuationRef,
20 HarnessContinuationStateV1, HarnessDeliveryBundleDigestV1,
21 HarnessDeliveryBundleIdV1, HarnessDeliveryBundleRevisionV1, HarnessDeliveryBundleV1,
22 HarnessDeliveryBundleSelectionV1, HarnessDeliveryComponentCountV1,
23 HarnessDeliveryComponentKindV1, HarnessContextSourceSelectionV1,
24 HarnessContextSourceAvailabilityV1,
25 HarnessDeliveryManifestDigestV2, HarnessDeliveryRef, HarnessDeliveryStateV1,
26 HarnessGrantTargetV1,
27 HarnessLaunchAuthorityRefV1, HarnessLaunchPlanRefV1, HarnessMoveTaskRequestV1,
28 HarnessOperatorAuthorityV1, HarnessReplaceTaskExecutionSpecRequestV1,
29 HarnessReplaceTaskRequestV1, HarnessRequestDigest, HarnessRetryTaskRequestV1,
30 HarnessScheduledLaunchRefV2, HarnessScheduleNextRequestV1, HarnessScheduleOutcomeV1,
31 HarnessStartTaskRequestV1, HarnessTaskExecutionSpecInputV1,
32 HarnessTaskExecutionSpecV1, HarnessTaskExecutionSpecV2,
33 HarnessTaskLaunchIssuanceId, HarnessTaskLaunchIssuanceRefV1,
34 HarnessTaskReviewPolicyV1,
35 HarnessTaskStartOutcomeV1,
36 HarnessContextPackLineageV1,
37 HarnessReadPermissionsV1, HarnessReconciliationOutcomeV1, HarnessResolvedContextPackReceiptV1,
38 HarnessResultDispositionV1,
39 HarnessInlineRef, HarnessReceiptRef, HarnessResultRef, HarnessRevision, HarnessRunId, HarnessRunIntentV1,
40 HarnessRunFinishOutcomeV1, HarnessRunFinishResultV1,
41 HarnessRunGitFactsV1, HarnessRunLifecycleV1,
42 HarnessRuntimeIdentityV1, HarnessSelectorV1, HarnessTaskId, HarnessTaskStateV1,
43 HarnessTaskCreateResultV1, HarnessTaskMoveResultV1,
44 HarnessValidationError, HarnessWorktreeIntentV1, SessionGrantId,
45 HARNESS_ARTIFACTS_MAX, HARNESS_BODY_MAX_BYTES,
46 HARNESS_CHILD_COUNT_MAX, HARNESS_CHILD_DEPTH_MAX, HARNESS_DEPENDENCIES_MAX,
47 HARNESS_CONTEXT_PACK_MAX_BYTES, HARNESS_CONTEXT_PACK_RETAINED_MESSAGES_MAX,
48 HARNESS_LINKS_MAX, HARNESS_RESULTS_MAX, HARNESS_TITLE_MAX_BYTES,
49 HARNESS_RUN_GIT_PATH_MAX_BYTES,
50};
51use serde::{Deserialize, Deserializer, Serialize};
52use thiserror::Error;
53
54pub const HARNESS_READ_REQUEST_MAX_BYTES: usize = 64 * 1024;
55pub const HARNESS_READ_RESPONSE_MAX_BYTES: usize = 1024 * 1024;
56pub const HARNESS_READ_LIMIT_MAX: u16 = 256;
57pub const HARNESS_ENTITY_PAGE_LIMIT_MAX: u16 = 64;
58pub const HARNESS_TIMELINE_PAGE_LIMIT_MAX: u16 = 128;
59pub const HARNESS_MONITOR_FACTS_MAX: usize = 128;
60pub const HARNESS_OBSERVATION_LABEL_MAX_BYTES: usize = 64;
61pub const HARNESS_OBSERVATION_TODO_TEXT_MAX_BYTES: usize = 256;
62pub const HARNESS_OBSERVATION_PATH_MAX_BYTES: usize = 1_024;
63pub const HARNESS_OBSERVATION_BLOCK_LABEL_MAX_BYTES: usize = 256;
72pub const HARNESS_READ_CREDENTIAL_MAX_BYTES: usize = 8 * 1024;
73pub const HARNESS_MCP_AUDIENCE: &str = "gate4agent-harness-mcp-read-v1";
79pub use gate4agent_build_stamp::BUILD_STAMP;
95pub const HARNESS_TERMINAL_SUBSCRIPTION_SESSIONS_MAX: usize = 32;
101pub const HARNESS_AGENT_STREAM_SUBSCRIPTION_SESSIONS_MAX: usize = 32;
108pub const HARNESS_OPERATOR_REQUEST_MAX_BYTES: usize = 64 * 1024;
109pub const HARNESS_OPERATOR_RESPONSE_MAX_BYTES: usize = 1024 * 1024;
110pub const HARNESS_OPERATOR_CREDENTIAL_MAX_BYTES: usize = 256;
111pub const HARNESS_RUNTIME_INVENTORY_PAGE_LIMIT_MAX: u16 = 64;
112pub const HARNESS_NATIVE_SESSION_CATALOG_LIMIT_MAX: u16 = 64;
113pub const HARNESS_NATIVE_SESSION_PREVIEW_MESSAGE_LIMIT_MAX: u16 = 24;
114pub const HARNESS_NATIVE_SESSION_PREVIEW_TEXT_MAX_BYTES: usize = 4_096;
115pub const HARNESS_SESSION_RECORD_DISPLAY_NAME_MAX_BYTES: usize = 256;
116pub const HARNESS_SESSION_RECORD_TASK_ID_MAX_BYTES: usize = 128;
117pub const HARNESS_PROVIDER_SESSION_ID_MAX_BYTES: usize = 512;
118pub const HARNESS_PROVIDER_SESSION_TRANSCRIPT_PATH_MAX_BYTES: usize = 32_768;
119pub const HARNESS_LAUNCH_PLAN_PAGE_LIMIT_MAX: u16 = 64;
120pub const HARNESS_TASK_LAUNCH_OPTIONS_MAX: usize = 64;
121pub const HARNESS_CONTEXT_SOURCE_EXCLUSIONS_MAX: usize = 32;
126pub const CONTEXT_SOURCE_EXCLUSION_KNOWN_INCARNATIONS_MAX: usize = 8;
130pub const HARNESS_RUNTIME_SPAWN_PROFILES_MAX: usize = 64;
131pub const HARNESS_RUNTIME_LAUNCH_BUNDLES_MAX: usize = 128;
132pub const HARNESS_REPOSITORY_PATH_MAX_BYTES: usize = 1_024;
133pub const HARNESS_WORKSPACE_FILE_MAX_BYTES: usize = 256 * 1_024;
134pub const HARNESS_WORKSPACE_TREE_ENTRIES_MAX: usize = 512;
135pub const HARNESS_GIT_STATUS_ENTRIES_MAX: usize = 128;
136pub const HARNESS_GIT_RECENT_COMMITS_MAX: usize = 12;
137pub const HARNESS_GIT_HISTORY_LIMIT_MAX: u16 = 50;
138pub const HARNESS_GIT_DIFF_MAX_BYTES: usize = 512 * 1_024;
139pub const HARNESS_GIT_COMMIT_PARENTS_MAX: usize = 32;
140pub const HARNESS_GIT_SUMMARY_MAX_BYTES: usize = 1_024;
141pub const HARNESS_GIT_IDENTITY_MAX_BYTES: usize = 512;
142pub const HARNESS_GIT_TIMESTAMP_MAX_BYTES: usize = 128;
143pub const HARNESS_GIT_SIGNER_MAX_BYTES: usize = 1_024;
144pub const HARNESS_REVERSE_ATTRIBUTION_LINKS_MAX: usize = 64;
145pub const HARNESS_TERMINAL_PAGE_LIMIT_MAX: u16 = 64;
146pub const HARNESS_TERMINAL_SCROLLBACK_LINES_MAX: usize = 512;
147pub const HARNESS_TERMINAL_FRAME_MAX_BYTES: usize = 2 * 1_024 * 1_024;
153pub const HARNESS_SESSION_INPUT_MAX_BYTES: usize = 32 * 1_024;
161pub const HARNESS_SESSION_BYTES_MAX_BYTES: usize = 64;
167pub const HARNESS_HOST_PATH_MAX_BYTES: usize = 32 * 1_024;
174pub const HARNESS_HOST_DIRECTORY_ENTRIES_MAX: usize = 256;
177pub const HARNESS_HOST_DIRECTORY_DISPLAY_NAME_MAX_BYTES: usize = 1_024;
178pub const HARNESS_AGENT_STREAM_ID_MAX_BYTES: usize = 512;
185pub const HARNESS_AGENT_STREAM_TEXT_MAX_BYTES: usize = 262_144;
191pub const HARNESS_AGENT_STREAM_INTERACTION_OPTIONS_MAX: usize = 256;
197pub const HARNESS_AGENT_STREAM_CATALOG_ENTRIES_MAX: usize = 256;
201pub const HARNESS_ACP_INTERACTION_RESPONSE_MAX_BYTES: usize = 32_768;
208
209pub const HARNESS_READ_TOOL_IDS: [&str; 8] = [
210 "g4a_context_get",
211 "g4a_monitor_get",
212 "g4a_timeline_read",
213 "g4a_tasks_list",
214 "g4a_tasks_get",
215 "g4a_runs_list",
216 "g4a_runs_get",
217 "g4a_operation_get",
218];
219
220pub const HARNESS_WRITE_TOOL_IDS: [&str; 3] = [
236 "g4a_run_finish",
237 "g4a_task_create",
238 "g4a_task_move",
239];
240
241const TOKEN_PREFIX: &str = "g4ah2_";
242const OPERATOR_TOKEN_PREFIX: &str = "g4aho_";
243const OPERATOR_REQUEST_REF_PREFIX: &str = "hireq_";
244
245#[derive(Clone, Eq, PartialEq)]
246pub struct HarnessReadCredential(String);
247
248impl HarnessReadCredential {
249 pub fn parse(value: impl Into<String>) -> Result<Self, HarnessReadApiError> {
250 let value = value.into();
251 validate_credential(&value)?;
252 Ok(Self(value))
253 }
254
255 pub fn expose(&self) -> &str { &self.0 }
256}
257
258impl fmt::Debug for HarnessReadCredential {
259 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
260 formatter.write_str("HarnessReadCredential([REDACTED])")
261 }
262}
263
264impl Serialize for HarnessReadCredential {
265 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
266 where S: serde::Serializer {
267 serializer.serialize_str(&self.0)
268 }
269}
270
271impl<'de> Deserialize<'de> for HarnessReadCredential {
272 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
273 where D: Deserializer<'de> {
274 Self::parse(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
275 }
276}
277
278#[derive(Clone, Eq, PartialEq)]
279pub struct HarnessOperatorCredential(String);
280
281impl HarnessOperatorCredential {
282 pub fn parse(value: impl Into<String>) -> Result<Self, HarnessOperatorApiError> {
283 let value = value.into();
284 validate_operator_credential(&value)?;
285 Ok(Self(value))
286 }
287
288 pub fn expose(&self) -> &str { &self.0 }
289}
290
291impl fmt::Debug for HarnessOperatorCredential {
292 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
293 formatter.write_str("HarnessOperatorCredential([REDACTED])")
294 }
295}
296
297impl Serialize for HarnessOperatorCredential {
298 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
299 where S: serde::Serializer {
300 serializer.serialize_str(&self.0)
301 }
302}
303
304impl<'de> Deserialize<'de> for HarnessOperatorCredential {
305 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
306 where D: Deserializer<'de> {
307 Self::parse(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
308 }
309}
310
311#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
312#[serde(deny_unknown_fields)]
313pub struct HarnessOperatorEnvelopeV1 {
314 pub build_stamp: String,
315 pub credential: HarnessOperatorCredential,
316 pub request: HarnessOperatorRequestV1,
317}
318
319impl HarnessOperatorEnvelopeV1 {
320 pub fn new(credential: HarnessOperatorCredential, request: HarnessOperatorRequestV1) -> Self {
325 Self {
326 build_stamp: BUILD_STAMP.to_string(),
327 credential,
328 request,
329 }
330 }
331
332 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
333 if self.build_stamp != BUILD_STAMP {
334 return Err(HarnessOperatorApiError::BuildStampMismatch {
335 expected: BUILD_STAMP.to_string(),
336 received: self.build_stamp.clone(),
337 });
338 }
339 validate_operator_credential(self.credential.expose())?;
340 self.request.validate()
341 }
342}
343
344#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
345#[serde(transparent)]
346pub struct HarnessOperatorRequestRefV1(String);
347
348impl HarnessOperatorRequestRefV1 {
349 pub fn new(value: impl Into<String>) -> Result<Self, HarnessOperatorApiError> {
350 let value = value.into();
351 validate_operator_request_ref(&value)?;
352 Ok(Self(value))
353 }
354
355 pub fn as_str(&self) -> &str { &self.0 }
356
357 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
358 validate_operator_request_ref(&self.0)
359 }
360}
361
362impl<'de> Deserialize<'de> for HarnessOperatorRequestRefV1 {
363 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
364 where
365 D: Deserializer<'de>,
366 {
367 Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
368 }
369}
370
371#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
372#[serde(deny_unknown_fields)]
373pub struct HarnessOperatorIntentV1 {
374 pub request_ref: HarnessOperatorRequestRefV1,
375 pub submitted_at_unix_ms: u64,
376 pub action: HarnessOperatorActionV1,
377}
378
379impl HarnessOperatorIntentV1 {
380 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
381 self.request_ref.validate()?;
382 if self.submitted_at_unix_ms == 0 {
383 return Err(HarnessOperatorApiError::InvalidSubmittedAt);
384 }
385 let authority = HarnessOperatorAuthorityV1 {
386 operation_id: HarnessOperationId::new(format!("hop_{}", "0".repeat(24)))
387 .map_err(HarnessOperatorApiError::Protocol)?,
388 idempotency_ref: HarnessIdempotencyRef::new(format!("hidem_{}", "0".repeat(24)))
389 .map_err(HarnessOperatorApiError::Protocol)?,
390 actor_id: HarnessSelectorV1::new("harness-operator")
391 .map_err(HarnessOperatorApiError::Protocol)?,
392 now_unix_ms: self.submitted_at_unix_ms,
393 };
394 let task_id = HarnessTaskId::new(format!("htask_{}", "0".repeat(24)))
395 .map_err(HarnessOperatorApiError::Protocol)?;
396 self.action.clone().authorize(authority, task_id).validate()
397 }
398
399 pub fn authorize(
400 self,
401 authority: HarnessOperatorAuthorityV1,
402 create_task_id: HarnessTaskId,
403 ) -> HarnessOperatorRequestV1 {
404 self.action.authorize(authority, create_task_id)
405 }
406}
407
408#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
409#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
410pub enum HarnessOperatorActionV1 {
411 CreateTask {
412 title: String,
413 body: String,
414 parent_task_id: Option<HarnessTaskId>,
415 dependencies: Vec<HarnessTaskId>,
416 initial_state: HarnessTaskStateV1,
417 },
418 ReplaceTask {
419 task_id: HarnessTaskId,
420 expected_revision: HarnessRevision,
421 title: String,
422 body: String,
423 parent_task_id: Option<HarnessTaskId>,
424 dependencies: Vec<HarnessTaskId>,
425 },
426 MoveTask {
427 task_id: HarnessTaskId,
428 expected_revision: HarnessRevision,
429 state: HarnessTaskStateV1,
430 },
431 CancelTask {
432 task_id: HarnessTaskId,
433 expected_revision: HarnessRevision,
434 },
435 RetryTask {
436 task_id: HarnessTaskId,
437 expected_revision: HarnessRevision,
438 },
439 ScheduleNext {
440 plan_id: Option<HarnessSelectorV1>,
441 },
442 ReplaceTaskExecutionSpec {
443 task_id: HarnessTaskId,
444 expected_task_revision: HarnessRevision,
445 expected_execution_spec_revision: HarnessExpectedExecutionSpecRevisionV1,
446 spec: HarnessTaskExecutionSpecInputV1,
447 },
448 StartTask {
449 task_id: HarnessTaskId,
450 expected_task_revision: HarnessRevision,
451 expected_execution_spec_revision: HarnessRevision,
452 expected_scheduled_launch_digest: HarnessRequestDigest,
453 },
454 ReplaceTaskExecutionSpecV2 {
455 task_id: HarnessTaskId,
456 expected_task_revision: HarnessRevision,
457 expected_execution_spec_revision: HarnessExpectedExecutionSpecRevisionV1,
458 selection: HarnessReviewedTaskLaunchSelectionV1,
459 },
460 StartTaskV2 {
461 task_id: HarnessTaskId,
462 expected_task_revision: HarnessRevision,
463 expected_execution_spec_revision: HarnessRevision,
464 expected_launch_issuance: HarnessTaskLaunchIssuanceRefV1,
465 },
466}
467
468impl HarnessOperatorActionV1 {
469 pub fn authorize(
470 self,
471 authority: HarnessOperatorAuthorityV1,
472 create_task_id: HarnessTaskId,
473 ) -> HarnessOperatorRequestV1 {
474 match self {
475 Self::CreateTask {
476 title,
477 body,
478 parent_task_id,
479 dependencies,
480 initial_state,
481 } => HarnessOperatorRequestV1::CreateTask {
482 request: HarnessCreateTaskRequestV1 {
483 authority,
484 task_id: create_task_id,
485 title,
486 body,
487 parent_task_id,
488 dependencies,
489 initial_state,
490 },
491 },
492 Self::ReplaceTask {
493 task_id,
494 expected_revision,
495 title,
496 body,
497 parent_task_id,
498 dependencies,
499 } => HarnessOperatorRequestV1::ReplaceTask {
500 request: HarnessReplaceTaskRequestV1 {
501 authority,
502 task_id,
503 expected_revision,
504 title,
505 body,
506 parent_task_id,
507 dependencies,
508 },
509 },
510 Self::MoveTask { task_id, expected_revision, state } => {
511 HarnessOperatorRequestV1::MoveTask {
512 request: HarnessMoveTaskRequestV1 {
513 authority,
514 task_id,
515 expected_revision,
516 state,
517 },
518 }
519 }
520 Self::CancelTask { task_id, expected_revision } => {
521 HarnessOperatorRequestV1::CancelTask {
522 request: HarnessCancelTaskRequestV1 {
523 authority,
524 task_id,
525 expected_revision,
526 },
527 }
528 }
529 Self::RetryTask { task_id, expected_revision } => {
530 HarnessOperatorRequestV1::RetryTask {
531 request: HarnessRetryTaskRequestV1 {
532 authority,
533 task_id,
534 expected_revision,
535 },
536 }
537 }
538 Self::ScheduleNext { plan_id } => HarnessOperatorRequestV1::ScheduleNext {
539 request: HarnessScheduleNextRequestV1 { authority, plan_id },
540 },
541 Self::ReplaceTaskExecutionSpec {
542 task_id,
543 expected_task_revision,
544 expected_execution_spec_revision,
545 spec,
546 } => HarnessOperatorRequestV1::ReplaceTaskExecutionSpec {
547 request: HarnessReplaceTaskExecutionSpecRequestV1 {
548 authority,
549 task_id,
550 expected_task_revision,
551 expected_execution_spec_revision,
552 spec,
553 },
554 },
555 Self::StartTask {
556 task_id,
557 expected_task_revision,
558 expected_execution_spec_revision,
559 expected_scheduled_launch_digest,
560 } => HarnessOperatorRequestV1::StartTask {
561 request: HarnessStartTaskRequestV1 {
562 authority,
563 task_id,
564 expected_task_revision,
565 expected_execution_spec_revision,
566 expected_scheduled_launch_digest,
567 },
568 },
569 Self::ReplaceTaskExecutionSpecV2 {
570 task_id,
571 expected_task_revision,
572 expected_execution_spec_revision,
573 selection,
574 } => HarnessOperatorRequestV1::ReplaceTaskExecutionSpecV2 {
575 request: HarnessReplaceTaskExecutionSpecRequestV2 {
576 authority,
577 task_id,
578 expected_task_revision,
579 expected_execution_spec_revision,
580 selection,
581 },
582 },
583 Self::StartTaskV2 {
584 task_id,
585 expected_task_revision,
586 expected_execution_spec_revision,
587 expected_launch_issuance,
588 } => HarnessOperatorRequestV1::StartTaskV2 {
589 request: HarnessStartTaskRequestV2 {
590 authority,
591 task_id,
592 expected_task_revision,
593 expected_execution_spec_revision,
594 expected_launch_issuance,
595 },
596 },
597 }
598 }
599
600}
601
602#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
603#[serde(deny_unknown_fields)]
604pub struct HarnessLaunchPlanSummaryV1 {
605 pub scheduled_launch: HarnessScheduledLaunchRefV2,
606 pub node_id: HarnessSelectorV1,
607 pub workspace_id: HarnessSelectorV1,
608 pub worktree: HarnessWorktreeIntentV1,
609 pub provider_profile: HarnessSelectorV1,
610 pub provider_id: HarnessSelectorV1,
611 pub mode: HarnessExecutionModeV1,
612}
613
614impl HarnessLaunchPlanSummaryV1 {
615 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
616 self.scheduled_launch.validate().map_err(HarnessOperatorApiError::Protocol)?;
617 self.node_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
618 self.workspace_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
619 self.worktree.validate().map_err(HarnessOperatorApiError::Protocol)?;
620 self.provider_profile.validate().map_err(HarnessOperatorApiError::Protocol)?;
621 self.provider_id.validate().map_err(HarnessOperatorApiError::Protocol)
622 }
623}
624
625#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
626#[serde(deny_unknown_fields)]
627pub struct HarnessLaunchPlanPageV1 {
628 pub plans: Vec<HarnessLaunchPlanSummaryV1>,
629 pub next_plan_id: Option<HarnessSelectorV1>,
630}
631
632impl HarnessLaunchPlanPageV1 {
633 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
634 if self.plans.len() > usize::from(HARNESS_LAUNCH_PLAN_PAGE_LIMIT_MAX) {
635 return Err(HarnessOperatorApiError::InvalidLaunchPlans);
636 }
637 for plan in &self.plans { plan.validate()?; }
638 if self.plans.windows(2).any(|plans| {
639 plans[0].scheduled_launch.plan.plan_id.as_str()
640 >= plans[1].scheduled_launch.plan.plan_id.as_str()
641 }) {
642 return Err(HarnessOperatorApiError::InvalidLaunchPlans);
643 }
644 if let Some(next_plan_id) = &self.next_plan_id {
645 next_plan_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
646 if self.plans.last().map(|plan| &plan.scheduled_launch.plan.plan_id)
647 != Some(next_plan_id)
648 {
649 return Err(HarnessOperatorApiError::InvalidLaunchPlans);
650 }
651 }
652 Ok(())
653 }
654}
655
656#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
657#[serde(deny_unknown_fields)]
658pub struct HarnessOrdinaryLaunchPlanOptionV1 {
659 pub plan: HarnessLaunchPlanRefV1,
660 pub node_id: HarnessSelectorV1,
661 pub source_workspace_id: HarnessSelectorV1,
662 pub provider_profile: HarnessSelectorV1,
663 pub provider_id: HarnessSelectorV1,
664 pub mode: HarnessExecutionModeV1,
665}
666
667impl HarnessOrdinaryLaunchPlanOptionV1 {
668 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
669 self.plan.validate().map_err(HarnessOperatorApiError::Protocol)?;
670 self.node_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
671 self.source_workspace_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
672 self.provider_profile.validate().map_err(HarnessOperatorApiError::Protocol)?;
673 self.provider_id.validate().map_err(HarnessOperatorApiError::Protocol)
674 }
675}
676
677#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
678#[serde(rename_all = "kebab-case")]
679pub enum HarnessManagedWorktreeRetentionV1 {
680 RemoveWhenReleased,
681 Retain,
682}
683
684#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
685#[serde(deny_unknown_fields)]
686pub struct HarnessManagedWorktreeProfileOptionV1 {
687 pub node_id: HarnessSelectorV1,
688 pub node_incarnation: HarnessSelectorV1,
689 pub source_workspace_id: HarnessSelectorV1,
690 pub profile_id: HarnessSelectorV1,
691 pub profile_revision: HarnessSelectorV1,
692 pub retention: HarnessManagedWorktreeRetentionV1,
693 pub observed_at_unix_ms: u64,
694}
695
696impl HarnessManagedWorktreeProfileOptionV1 {
697 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
698 self.node_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
699 self.node_incarnation.validate().map_err(HarnessOperatorApiError::Protocol)?;
700 self.source_workspace_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
701 self.profile_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
702 self.profile_revision.validate().map_err(HarnessOperatorApiError::Protocol)?;
703 if self.observed_at_unix_ms == 0 {
704 return Err(HarnessOperatorApiError::InvalidTaskLaunchOptions);
705 }
706 Ok(())
707 }
708}
709
710#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
711#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
712pub enum HarnessReviewedWorktreeSelectionV1 {
713 Existing,
714 Managed { profile: HarnessManagedWorktreeProfileOptionV1 },
715}
716
717impl HarnessReviewedWorktreeSelectionV1 {
718 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
719 match self {
720 Self::Existing => Ok(()),
721 Self::Managed { profile } => profile.validate(),
722 }
723 }
724}
725
726#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
727#[serde(deny_unknown_fields)]
728pub struct HarnessReviewedTaskLaunchSelectionV1 {
729 pub plan: HarnessOrdinaryLaunchPlanOptionV1,
730 pub worktree: HarnessReviewedWorktreeSelectionV1,
731 pub context_source: Option<HarnessContextSourceSelectionV1>,
732 pub delivery: Option<HarnessDeliveryBundleSelectionV1>,
733 pub review_policy: HarnessTaskReviewPolicyV1,
734}
735
736impl HarnessReviewedTaskLaunchSelectionV1 {
737 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
738 self.plan.validate()?;
739 self.worktree.validate()?;
740 if let HarnessReviewedWorktreeSelectionV1::Managed { profile } = &self.worktree {
741 if profile.node_id != self.plan.node_id
742 || profile.source_workspace_id != self.plan.source_workspace_id
743 {
744 return Err(HarnessOperatorApiError::InvalidTaskLaunchSelection);
745 }
746 }
747 if let Some(context_source) = &self.context_source {
748 context_source.validate().map_err(HarnessOperatorApiError::Protocol)?;
749 }
750 if let Some(delivery) = &self.delivery {
751 delivery.validate().map_err(HarnessOperatorApiError::Protocol)?;
752 }
753 Ok(())
754 }
755}
756
757#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
758#[serde(deny_unknown_fields)]
759pub struct HarnessIssuedExecutionSpecSummaryV1 {
760 pub task_id: HarnessTaskId,
761 pub execution_spec_id: HarnessExecutionSpecId,
762 pub revision: HarnessRevision,
763 pub launch_issuance: HarnessTaskLaunchIssuanceRefV1,
764 pub review_policy: HarnessTaskReviewPolicyV1,
765 pub created_at_unix_ms: u64,
766 pub updated_at_unix_ms: u64,
767}
768
769impl HarnessIssuedExecutionSpecSummaryV1 {
770 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
771 if self.launch_issuance.revision != self.revision {
772 return Err(HarnessOperatorApiError::InvalidTaskLaunchOptions);
773 }
774 HarnessTaskExecutionSpecV2 {
775 execution_spec_id: self.execution_spec_id.clone(),
776 revision: self.revision,
777 task_id: self.task_id.clone(),
778 launch_issuance: self.launch_issuance.clone(),
779 review_policy: self.review_policy,
780 created_at_unix_ms: self.created_at_unix_ms,
781 updated_at_unix_ms: self.updated_at_unix_ms,
782 }.validate().map_err(HarnessOperatorApiError::Protocol)
783 }
784}
785
786#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
796#[serde(tag = "reason", rename_all = "kebab-case", deny_unknown_fields)]
797pub enum ContextSourceExclusionV1 {
798 LifecycleNotLive { lifecycle: HarnessRunLifecycleV1 },
802 NoBinding,
804 NoActiveManagedSession,
807 NodeIncarnationUnknown {
814 node_id: HarnessSelectorV1,
815 node_incarnation: HarnessSelectorV1,
816 known_incarnations: Vec<HarnessSelectorV1>,
817 },
818 ManagedSessionRecordMismatch {
824 record_id: HarnessSelectorV1,
825 workspace_id: HarnessSelectorV1,
826 instance_id: u64,
827 generation: u64,
828 node_has_records: u32,
829 },
830 ManagedSessionsPageTruncated {
837 record_id: HarnessSelectorV1,
838 page_len: u32,
839 total: u32,
840 },
841 MonitorUnavailable,
844 ProjectionNotLive {
847 availability: ProjectionAvailabilityV1,
848 freshness: ProjectionFreshnessV1,
849 transport_incomplete: bool,
850 },
851 DurableNodeUnknown { node_id: HarnessSelectorV1 },
855}
856
857impl ContextSourceExclusionV1 {
858 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
859 match self {
860 Self::LifecycleNotLive { .. }
861 | Self::NoBinding
862 | Self::NoActiveManagedSession
863 | Self::MonitorUnavailable
864 | Self::ProjectionNotLive { .. } => Ok(()),
865 Self::NodeIncarnationUnknown { node_id, node_incarnation, known_incarnations } => {
866 node_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
867 node_incarnation.validate().map_err(HarnessOperatorApiError::Protocol)?;
868 if known_incarnations.len() > CONTEXT_SOURCE_EXCLUSION_KNOWN_INCARNATIONS_MAX {
869 return Err(HarnessOperatorApiError::InvalidTaskLaunchOptions);
870 }
871 for incarnation in known_incarnations {
872 incarnation.validate().map_err(HarnessOperatorApiError::Protocol)?;
873 }
874 Ok(())
875 }
876 Self::ManagedSessionRecordMismatch {
877 record_id, workspace_id, instance_id, generation, ..
878 } => {
879 record_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
880 workspace_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
881 if *instance_id == 0 || *generation == 0 {
882 return Err(HarnessOperatorApiError::InvalidTaskLaunchOptions);
883 }
884 Ok(())
885 }
886 Self::ManagedSessionsPageTruncated { record_id, page_len, total } => {
887 record_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
888 if total < page_len {
889 return Err(HarnessOperatorApiError::InvalidTaskLaunchOptions);
890 }
891 Ok(())
892 }
893 Self::DurableNodeUnknown { node_id } => {
894 node_id.validate().map_err(HarnessOperatorApiError::Protocol)
895 }
896 }
897 }
898}
899
900#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
903#[serde(deny_unknown_fields)]
904pub struct ContextSourceExclusionEntryV1 {
905 pub run_id: HarnessRunId,
906 pub exclusion: ContextSourceExclusionV1,
907}
908
909impl ContextSourceExclusionEntryV1 {
910 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
911 self.run_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
912 self.exclusion.validate()
913 }
914}
915
916#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
917#[serde(deny_unknown_fields)]
918pub struct HarnessTaskLaunchOptionsV1 {
919 pub task_id: HarnessTaskId,
920 pub task_revision: HarnessRevision,
921 pub policy_digest: HarnessRequestDigest,
922 pub plans: Vec<HarnessOrdinaryLaunchPlanOptionV1>,
923 pub managed_worktree_profiles: Vec<HarnessManagedWorktreeProfileOptionV1>,
924 pub context_sources: Vec<HarnessContextSourceSelectionV1>,
925 pub delivery_bundles: Vec<HarnessDeliveryBundleSelectionV1>,
926 pub current_issued_spec: Option<HarnessIssuedExecutionSpecSummaryV1>,
927 pub truncated: bool,
928 #[serde(default)]
935 pub next_after: Option<HarnessSelectorV1>,
936 #[serde(default)]
946 pub context_source_exclusions: Vec<ContextSourceExclusionEntryV1>,
947}
948
949impl HarnessTaskLaunchOptionsV1 {
950 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
951 self.task_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
952 self.task_revision.validate().map_err(HarnessOperatorApiError::Protocol)?;
953 self.policy_digest.validate().map_err(HarnessOperatorApiError::Protocol)?;
954 if self.plans.len() > HARNESS_TASK_LAUNCH_OPTIONS_MAX
955 || self.managed_worktree_profiles.len() > HARNESS_TASK_LAUNCH_OPTIONS_MAX
956 || self.context_sources.len() > HARNESS_TASK_LAUNCH_OPTIONS_MAX
957 || self.delivery_bundles.len() > HARNESS_TASK_LAUNCH_OPTIONS_MAX
958 || self.context_source_exclusions.len() > HARNESS_CONTEXT_SOURCE_EXCLUSIONS_MAX
959 {
960 return Err(HarnessOperatorApiError::InvalidTaskLaunchOptions);
961 }
962 for plan in &self.plans { plan.validate()?; }
963 for profile in &self.managed_worktree_profiles { profile.validate()?; }
964 for source in &self.context_sources {
965 source.validate().map_err(HarnessOperatorApiError::Protocol)?;
966 }
967 for delivery in &self.delivery_bundles {
968 delivery.validate().map_err(HarnessOperatorApiError::Protocol)?;
969 }
970 for exclusion in &self.context_source_exclusions {
971 exclusion.validate()?;
972 }
973 if self.context_source_exclusions.windows(2).any(|items| {
974 items[0].run_id >= items[1].run_id
975 }) {
976 return Err(HarnessOperatorApiError::InvalidTaskLaunchOptions);
977 }
978 if self.plans.windows(2).any(|items| {
979 (&items[0].plan.plan_id, items[0].plan.revision)
980 >= (&items[1].plan.plan_id, items[1].plan.revision)
981 }) || self.managed_worktree_profiles.windows(2).any(|items| {
982 (
983 &items[0].node_id,
984 &items[0].source_workspace_id,
985 &items[0].profile_id,
986 &items[0].profile_revision,
987 ) >= (
988 &items[1].node_id,
989 &items[1].source_workspace_id,
990 &items[1].profile_id,
991 &items[1].profile_revision,
992 )
993 }) || self.context_sources.windows(2).any(|items| {
994 (&items[0].source_run_id, items[0].source_run_revision)
995 >= (&items[1].source_run_id, items[1].source_run_revision)
996 }) || self.delivery_bundles.windows(2).any(|items| {
997 (&items[0].bundle.bundle_id, &items[0].bundle.revision)
998 >= (&items[1].bundle.bundle_id, &items[1].bundle.revision)
999 }) {
1000 return Err(HarnessOperatorApiError::InvalidTaskLaunchOptions);
1001 }
1002 if let Some(current) = &self.current_issued_spec {
1003 current.validate()?;
1004 if current.task_id != self.task_id {
1005 return Err(HarnessOperatorApiError::InvalidTaskLaunchOptions);
1006 }
1007 }
1008 if let Some(next_after) = &self.next_after {
1009 next_after.validate().map_err(HarnessOperatorApiError::Protocol)?;
1010 if !self.truncated
1011 || self.plans.last().map(|plan| &plan.plan.plan_id) != Some(next_after)
1012 {
1013 return Err(HarnessOperatorApiError::InvalidTaskLaunchOptions);
1014 }
1015 }
1016 Ok(())
1017 }
1018
1019 pub fn validate_for(&self, task_id: &HarnessTaskId) -> Result<(), HarnessOperatorApiError> {
1020 self.validate()?;
1021 if &self.task_id != task_id {
1022 return Err(HarnessOperatorApiError::InvalidTaskLaunchOptions);
1023 }
1024 Ok(())
1025 }
1026}
1027
1028#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1029#[serde(deny_unknown_fields)]
1030pub struct HarnessReplaceTaskExecutionSpecRequestV2 {
1031 pub authority: HarnessOperatorAuthorityV1,
1032 pub task_id: HarnessTaskId,
1033 pub expected_task_revision: HarnessRevision,
1034 pub expected_execution_spec_revision: HarnessExpectedExecutionSpecRevisionV1,
1035 pub selection: HarnessReviewedTaskLaunchSelectionV1,
1036}
1037
1038impl HarnessReplaceTaskExecutionSpecRequestV2 {
1039 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1040 self.authority.validate().map_err(HarnessOperatorApiError::Protocol)?;
1041 self.task_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
1042 self.expected_task_revision.validate().map_err(HarnessOperatorApiError::Protocol)?;
1043 self.expected_execution_spec_revision.validate()
1044 .map_err(HarnessOperatorApiError::Protocol)?;
1045 self.selection.validate()
1046 }
1047}
1048
1049#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1050#[serde(deny_unknown_fields)]
1051pub struct HarnessStartTaskRequestV2 {
1052 pub authority: HarnessOperatorAuthorityV1,
1053 pub task_id: HarnessTaskId,
1054 pub expected_task_revision: HarnessRevision,
1055 pub expected_execution_spec_revision: HarnessRevision,
1056 pub expected_launch_issuance: HarnessTaskLaunchIssuanceRefV1,
1057}
1058
1059impl HarnessStartTaskRequestV2 {
1060 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1061 self.authority.validate().map_err(HarnessOperatorApiError::Protocol)?;
1062 self.task_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
1063 self.expected_task_revision.validate().map_err(HarnessOperatorApiError::Protocol)?;
1064 self.expected_execution_spec_revision.validate()
1065 .map_err(HarnessOperatorApiError::Protocol)?;
1066 self.expected_launch_issuance.validate().map_err(HarnessOperatorApiError::Protocol)?;
1067 if self.expected_launch_issuance.revision != self.expected_execution_spec_revision {
1068 return Err(HarnessOperatorApiError::InvalidTaskLaunchSelection);
1069 }
1070 Ok(())
1071 }
1072}
1073
1074#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
1075#[serde(transparent)]
1076pub struct HarnessRepositoryPathV1(String);
1077
1078impl HarnessRepositoryPathV1 {
1079 pub fn new(value: impl Into<String>) -> Result<Self, HarnessOperatorApiError> {
1080 let value = value.into();
1081 validate_repository_path(&value)?;
1082 Ok(Self(value))
1083 }
1084
1085 pub fn as_str(&self) -> &str { &self.0 }
1086
1087 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1088 validate_repository_path(&self.0)
1089 }
1090}
1091
1092impl<'de> Deserialize<'de> for HarnessRepositoryPathV1 {
1093 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1094 where D: Deserializer<'de> {
1095 Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
1096 }
1097}
1098
1099#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
1113#[serde(transparent)]
1114pub struct HarnessHostPathV1(String);
1115
1116impl HarnessHostPathV1 {
1117 pub fn new(value: impl Into<String>) -> Result<Self, HarnessOperatorApiError> {
1118 let value = value.into();
1119 validate_host_path(&value)?;
1120 Ok(Self(value))
1121 }
1122
1123 pub fn as_str(&self) -> &str { &self.0 }
1124
1125 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1126 validate_host_path(&self.0)
1127 }
1128}
1129
1130impl<'de> Deserialize<'de> for HarnessHostPathV1 {
1131 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1132 where D: Deserializer<'de> {
1133 Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
1134 }
1135}
1136
1137fn validate_host_path(value: &str) -> Result<(), HarnessOperatorApiError> {
1138 if value.is_empty() || value.len() > HARNESS_HOST_PATH_MAX_BYTES || value.contains('\0') {
1139 return Err(HarnessOperatorApiError::InvalidHostPath);
1140 }
1141 Ok(())
1142}
1143
1144#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
1145#[serde(transparent)]
1146pub struct HarnessGitObjectIdV1(String);
1147
1148impl HarnessGitObjectIdV1 {
1149 pub fn new(value: impl Into<String>) -> Result<Self, HarnessOperatorApiError> {
1150 let value = value.into();
1151 validate_git_object_id(&value)?;
1152 Ok(Self(value))
1153 }
1154
1155 pub fn as_str(&self) -> &str { &self.0 }
1156
1157 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1158 validate_git_object_id(&self.0)
1159 }
1160}
1161
1162impl<'de> Deserialize<'de> for HarnessGitObjectIdV1 {
1163 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1164 where D: Deserializer<'de> {
1165 Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
1166 }
1167}
1168
1169#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
1170#[serde(transparent)]
1171pub struct HarnessWorkspaceFileRevisionV1(String);
1172
1173impl HarnessWorkspaceFileRevisionV1 {
1174 pub fn new(value: impl Into<String>) -> Result<Self, HarnessOperatorApiError> {
1175 let value = value.into();
1176 if value.len() != 64 || !value.bytes().all(is_lower_hex) {
1177 return Err(HarnessOperatorApiError::InvalidWorkspaceFile);
1178 }
1179 Ok(Self(value))
1180 }
1181
1182 pub fn as_str(&self) -> &str { &self.0 }
1183
1184 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1185 if self.0.len() != 64 || !self.0.bytes().all(is_lower_hex) {
1186 return Err(HarnessOperatorApiError::InvalidWorkspaceFile);
1187 }
1188 Ok(())
1189 }
1190}
1191
1192impl<'de> Deserialize<'de> for HarnessWorkspaceFileRevisionV1 {
1193 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1194 where D: Deserializer<'de> {
1195 Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
1196 }
1197}
1198
1199#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1200#[serde(deny_unknown_fields)]
1201pub struct HarnessRunWorkspaceOriginV1 {
1202 pub run_id: HarnessRunId,
1203 pub run_revision: HarnessRevision,
1204 pub node_id: HarnessSelectorV1,
1205 pub node_incarnation_id: HarnessNodeIncarnationV1,
1206 pub workspace_id: HarnessSelectorV1,
1207}
1208
1209impl HarnessRunWorkspaceOriginV1 {
1210 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1211 self.run_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
1212 self.run_revision.validate().map_err(HarnessOperatorApiError::Protocol)?;
1213 self.node_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
1214 self.node_incarnation_id.validate()?;
1215 self.workspace_id.validate().map_err(HarnessOperatorApiError::Protocol)
1216 }
1217
1218 pub fn validate_for(&self, run_id: &HarnessRunId) -> Result<(), HarnessOperatorApiError> {
1219 self.validate()?;
1220 if &self.run_id != run_id {
1221 return Err(HarnessOperatorApiError::InvalidWorkspaceOrigin);
1222 }
1223 Ok(())
1224 }
1225}
1226
1227#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1231#[serde(deny_unknown_fields)]
1232pub struct HarnessNodeWorkspaceOriginV1 {
1233 pub node_id: String,
1234 pub node_incarnation_id: String,
1235 pub workspace_id: String,
1236}
1237
1238impl HarnessNodeWorkspaceOriginV1 {
1239 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1240 if !valid_runtime_id(&self.node_id, 128)
1241 || self.node_incarnation_id.len() != 32
1242 || !self.node_incarnation_id.bytes().all(is_lower_hex)
1243 || !valid_runtime_id(&self.workspace_id, 128)
1244 {
1245 return Err(HarnessOperatorApiError::InvalidWorkspaceOrigin);
1246 }
1247 Ok(())
1248 }
1249
1250 pub fn validate_for(
1251 &self,
1252 node_id: &str,
1253 workspace_id: &str,
1254 ) -> Result<(), HarnessOperatorApiError> {
1255 self.validate()?;
1256 if self.node_id != node_id || self.workspace_id != workspace_id {
1257 return Err(HarnessOperatorApiError::InvalidWorkspaceOrigin);
1258 }
1259 Ok(())
1260 }
1261}
1262
1263#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1264#[serde(rename_all = "kebab-case")]
1265pub enum HarnessWorkspaceEntryKindV1 {
1266 File,
1267 Directory,
1268}
1269
1270#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1271#[serde(deny_unknown_fields)]
1272pub struct HarnessWorkspaceTreeEntryV1 {
1273 pub relative_path: HarnessRepositoryPathV1,
1274 pub kind: HarnessWorkspaceEntryKindV1,
1275}
1276
1277impl HarnessWorkspaceTreeEntryV1 {
1278 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1279 self.relative_path.validate()
1280 }
1281}
1282
1283#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1284#[serde(rename_all = "kebab-case")]
1285pub enum HarnessGitStatusCodeV1 {
1286 Unmodified,
1287 Added,
1288 Modified,
1289 Deleted,
1290 Renamed,
1291 Copied,
1292 Unmerged,
1293 Untracked,
1294 Ignored,
1295 TypeChanged,
1296}
1297
1298#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1299#[serde(deny_unknown_fields)]
1300pub struct HarnessGitStatusEntryV1 {
1301 pub index_status: HarnessGitStatusCodeV1,
1302 pub worktree_status: HarnessGitStatusCodeV1,
1303 pub path: HarnessRepositoryPathV1,
1304 pub previous_path: Option<HarnessRepositoryPathV1>,
1305}
1306
1307impl HarnessGitStatusEntryV1 {
1308 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1309 self.path.validate()?;
1310 if let Some(previous_path) = &self.previous_path {
1311 previous_path.validate()?;
1312 if previous_path == &self.path {
1313 return Err(HarnessOperatorApiError::InvalidGitStatus);
1314 }
1315 }
1316 let renamed_or_copied = matches!(self.index_status, HarnessGitStatusCodeV1::Renamed | HarnessGitStatusCodeV1::Copied)
1317 || matches!(self.worktree_status, HarnessGitStatusCodeV1::Renamed | HarnessGitStatusCodeV1::Copied);
1318 if self.previous_path.is_some() != renamed_or_copied {
1319 return Err(HarnessOperatorApiError::InvalidGitStatus);
1320 }
1321 Ok(())
1322 }
1323}
1324
1325#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1326#[serde(deny_unknown_fields)]
1327pub struct HarnessGitCommitSummaryV1 {
1328 pub id: HarnessGitObjectIdV1,
1329 pub summary: String,
1330}
1331
1332impl HarnessGitCommitSummaryV1 {
1333 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1334 self.id.validate()?;
1335 validate_git_single_line(&self.summary, HARNESS_GIT_SUMMARY_MAX_BYTES, false)
1336 }
1337}
1338
1339#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1340#[serde(deny_unknown_fields)]
1341pub struct HarnessGitSummaryV1 {
1342 pub is_repository: bool,
1343 pub branch: Option<String>,
1344 pub status: Vec<HarnessGitStatusEntryV1>,
1345 pub recent_commits: Vec<HarnessGitCommitSummaryV1>,
1346 pub truncated: bool,
1347}
1348
1349impl HarnessGitSummaryV1 {
1350 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1351 if self.status.len() > HARNESS_GIT_STATUS_ENTRIES_MAX
1352 || self.recent_commits.len() > HARNESS_GIT_RECENT_COMMITS_MAX
1353 || !self.is_repository && (self.branch.is_some() || !self.status.is_empty() || !self.recent_commits.is_empty() || self.truncated)
1354 {
1355 return Err(HarnessOperatorApiError::InvalidGitSummary);
1356 }
1357 if let Some(branch) = &self.branch {
1358 validate_git_single_line(branch, HARNESS_REPOSITORY_PATH_MAX_BYTES, true)?;
1359 }
1360 for status in &self.status { status.validate()?; }
1361 if self.status.windows(2).any(|entries| entries[0].path >= entries[1].path) {
1362 return Err(HarnessOperatorApiError::InvalidGitStatus);
1363 }
1364 for commit in &self.recent_commits { commit.validate()?; }
1365 if self.recent_commits.iter().enumerate().any(|(index, commit)| {
1366 self.recent_commits[..index].iter().any(|existing| existing.id == commit.id)
1367 }) {
1368 return Err(HarnessOperatorApiError::InvalidGitSummary);
1369 }
1370 Ok(())
1371 }
1372}
1373
1374#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1382#[serde(deny_unknown_fields)]
1383pub struct HarnessWorkspaceInspectionTruncationV1 {
1384 pub walk_time_budget_exceeded: bool,
1385 pub walk_entry_cap_exceeded: bool,
1386 pub git_time_budget_exceeded: bool,
1387 pub entries_visited: u64,
1388 pub elapsed_ms: u64,
1389}
1390
1391#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1392#[serde(deny_unknown_fields)]
1393pub struct HarnessRunWorkspaceInspectionV1 {
1394 pub origin: HarnessRunWorkspaceOriginV1,
1395 pub entries: Vec<HarnessWorkspaceTreeEntryV1>,
1396 pub tree_truncated: bool,
1397 pub git: HarnessGitSummaryV1,
1398 #[serde(default, skip_serializing_if = "Option::is_none")]
1399 pub truncation: Option<HarnessWorkspaceInspectionTruncationV1>,
1400}
1401
1402impl HarnessRunWorkspaceInspectionV1 {
1403 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1404 self.origin.validate()?;
1405 if self.entries.len() > HARNESS_WORKSPACE_TREE_ENTRIES_MAX {
1406 return Err(HarnessOperatorApiError::InvalidWorkspaceTree);
1407 }
1408 for entry in &self.entries { entry.validate()?; }
1409 if self.entries.windows(2).any(|entries| entries[0].relative_path >= entries[1].relative_path) {
1410 return Err(HarnessOperatorApiError::InvalidWorkspaceTree);
1411 }
1412 self.git.validate()
1413 }
1414
1415 pub fn validate_for(&self, run_id: &HarnessRunId) -> Result<(), HarnessOperatorApiError> {
1416 self.validate()?;
1417 self.origin.validate_for(run_id)
1418 }
1419}
1420
1421#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1423#[serde(deny_unknown_fields)]
1424pub struct HarnessNodeWorkspaceInspectionV1 {
1425 pub origin: HarnessNodeWorkspaceOriginV1,
1426 pub entries: Vec<HarnessWorkspaceTreeEntryV1>,
1427 pub tree_truncated: bool,
1428 pub git: HarnessGitSummaryV1,
1429 #[serde(default, skip_serializing_if = "Option::is_none")]
1430 pub truncation: Option<HarnessWorkspaceInspectionTruncationV1>,
1431}
1432
1433impl HarnessNodeWorkspaceInspectionV1 {
1434 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1435 self.origin.validate()?;
1436 if self.entries.len() > HARNESS_WORKSPACE_TREE_ENTRIES_MAX {
1437 return Err(HarnessOperatorApiError::InvalidWorkspaceTree);
1438 }
1439 for entry in &self.entries { entry.validate()?; }
1440 if self.entries.windows(2).any(|entries| entries[0].relative_path >= entries[1].relative_path) {
1441 return Err(HarnessOperatorApiError::InvalidWorkspaceTree);
1442 }
1443 self.git.validate()
1444 }
1445
1446 pub fn validate_for(
1447 &self,
1448 node_id: &str,
1449 workspace_id: &str,
1450 ) -> Result<(), HarnessOperatorApiError> {
1451 self.validate()?;
1452 self.origin.validate_for(node_id, workspace_id)
1453 }
1454}
1455
1456#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1457#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
1458pub enum HarnessWorkspaceFileContentV1 {
1459 Utf8 { text: String, byte_len: u32 },
1460 NonUtf8 { byte_len: u32 },
1461 TooLarge { limit_bytes: u32 },
1462}
1463
1464impl HarnessWorkspaceFileContentV1 {
1465 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1466 match self {
1467 Self::Utf8 { text, byte_len } => {
1468 if text.len() > HARNESS_WORKSPACE_FILE_MAX_BYTES || usize::try_from(*byte_len).ok() != Some(text.len()) {
1469 return Err(HarnessOperatorApiError::InvalidWorkspaceFile);
1470 }
1471 }
1472 Self::NonUtf8 { byte_len } => {
1473 if usize::try_from(*byte_len).map_or(true, |length| length > HARNESS_WORKSPACE_FILE_MAX_BYTES) {
1474 return Err(HarnessOperatorApiError::InvalidWorkspaceFile);
1475 }
1476 }
1477 Self::TooLarge { limit_bytes } => {
1478 if usize::try_from(*limit_bytes).ok() != Some(HARNESS_WORKSPACE_FILE_MAX_BYTES) {
1479 return Err(HarnessOperatorApiError::InvalidWorkspaceFile);
1480 }
1481 }
1482 }
1483 Ok(())
1484 }
1485}
1486
1487#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1488#[serde(deny_unknown_fields)]
1489pub struct HarnessRunWorkspaceFileV1 {
1490 pub origin: HarnessRunWorkspaceOriginV1,
1491 pub path: HarnessRepositoryPathV1,
1492 pub content: HarnessWorkspaceFileContentV1,
1493 pub revision: Option<HarnessWorkspaceFileRevisionV1>,
1494}
1495
1496impl HarnessRunWorkspaceFileV1 {
1497 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1498 self.origin.validate()?;
1499 self.path.validate()?;
1500 self.content.validate()?;
1501 if let Some(revision) = &self.revision { revision.validate()?; }
1502 if !matches!(&self.content, HarnessWorkspaceFileContentV1::Utf8 { .. }) && self.revision.is_some() {
1503 return Err(HarnessOperatorApiError::InvalidWorkspaceFile);
1504 }
1505 Ok(())
1506 }
1507
1508
1509 pub fn validate_for(
1510 &self,
1511 run_id: &HarnessRunId,
1512 path: &HarnessRepositoryPathV1,
1513 ) -> Result<(), HarnessOperatorApiError> {
1514 self.validate()?;
1515 self.origin.validate_for(run_id)?;
1516 if &self.path != path {
1517 return Err(HarnessOperatorApiError::InvalidWorkspaceFile);
1518 }
1519 Ok(())
1520 }
1521}
1522
1523#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1525#[serde(deny_unknown_fields)]
1526pub struct HarnessNodeWorkspaceFileV1 {
1527 pub origin: HarnessNodeWorkspaceOriginV1,
1528 pub path: HarnessRepositoryPathV1,
1529 pub content: HarnessWorkspaceFileContentV1,
1530 pub revision: Option<HarnessWorkspaceFileRevisionV1>,
1531}
1532
1533impl HarnessNodeWorkspaceFileV1 {
1534 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1535 self.origin.validate()?;
1536 self.path.validate()?;
1537 self.content.validate()?;
1538 if let Some(revision) = &self.revision { revision.validate()?; }
1539 if !matches!(&self.content, HarnessWorkspaceFileContentV1::Utf8 { .. }) && self.revision.is_some() {
1540 return Err(HarnessOperatorApiError::InvalidWorkspaceFile);
1541 }
1542 Ok(())
1543 }
1544
1545 pub fn validate_for(
1546 &self,
1547 node_id: &str,
1548 workspace_id: &str,
1549 path: &HarnessRepositoryPathV1,
1550 ) -> Result<(), HarnessOperatorApiError> {
1551 self.validate()?;
1552 self.origin.validate_for(node_id, workspace_id)?;
1553 if &self.path != path {
1554 return Err(HarnessOperatorApiError::InvalidWorkspaceFile);
1555 }
1556 Ok(())
1557 }
1558}
1559
1560#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1567#[serde(deny_unknown_fields)]
1568pub struct HarnessNodeWorkspaceDirectoryV1 {
1569 pub origin: HarnessNodeWorkspaceOriginV1,
1570 pub entry: HarnessWorkspaceTreeEntryV1,
1571}
1572
1573impl HarnessNodeWorkspaceDirectoryV1 {
1574 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1575 self.origin.validate()?;
1576 self.entry.validate()?;
1577 if self.entry.kind != HarnessWorkspaceEntryKindV1::Directory {
1578 return Err(HarnessOperatorApiError::InvalidWorkspaceTree);
1579 }
1580 Ok(())
1581 }
1582
1583 pub fn validate_for(
1584 &self,
1585 node_id: &str,
1586 workspace_id: &str,
1587 path: &HarnessRepositoryPathV1,
1588 ) -> Result<(), HarnessOperatorApiError> {
1589 self.validate()?;
1590 self.origin.validate_for(node_id, workspace_id)?;
1591 if &self.entry.relative_path != path {
1592 return Err(HarnessOperatorApiError::InvalidWorkspaceTree);
1593 }
1594 Ok(())
1595 }
1596}
1597
1598#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1603#[serde(deny_unknown_fields)]
1604pub struct HarnessHostDirectoryEntryV1 {
1605 pub path: HarnessHostPathV1,
1606 pub display_name: String,
1607 pub is_link: bool,
1608}
1609
1610impl HarnessHostDirectoryEntryV1 {
1611 fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1612 self.path.validate()?;
1613 if self.display_name.is_empty()
1614 || self.display_name.len() > HARNESS_HOST_DIRECTORY_DISPLAY_NAME_MAX_BYTES
1615 || self.display_name.chars().any(char::is_control)
1616 {
1617 return Err(HarnessOperatorApiError::InvalidHostDirectoryBrowseRequest);
1618 }
1619 Ok(())
1620 }
1621}
1622
1623#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1630#[serde(deny_unknown_fields)]
1631pub struct HarnessHostDirectoryListingV1 {
1632 pub directory: Option<HarnessHostPathV1>,
1633 pub parent: Option<HarnessHostPathV1>,
1634 pub entries: Vec<HarnessHostDirectoryEntryV1>,
1635 pub next_after: Option<HarnessHostPathV1>,
1636 pub incomplete: bool,
1639}
1640
1641impl HarnessHostDirectoryListingV1 {
1642 fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1643 if let Some(directory) = &self.directory { directory.validate()?; }
1644 if let Some(parent) = &self.parent { parent.validate()?; }
1645 if self.entries.len() > HARNESS_HOST_DIRECTORY_ENTRIES_MAX {
1646 return Err(HarnessOperatorApiError::InvalidHostDirectoryBrowseRequest);
1647 }
1648 for entry in &self.entries { entry.validate()?; }
1649 if let Some(next_after) = &self.next_after { next_after.validate()?; }
1650 Ok(())
1651 }
1652}
1653
1654#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1656#[serde(rename_all = "kebab-case")]
1657pub enum HarnessWorktreeServiceModeV1 {
1658 Manual,
1659 Managed,
1660 Off,
1661}
1662
1663#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1675#[serde(deny_unknown_fields)]
1676pub struct HarnessWorkspaceSnapshotV1 {
1677 pub workspace_id: String,
1678 pub canonical_root: HarnessHostPathV1,
1679 #[serde(default, skip_serializing_if = "Option::is_none")]
1680 pub worktree_service_mode: Option<HarnessWorktreeServiceModeV1>,
1681}
1682
1683impl HarnessWorkspaceSnapshotV1 {
1684 fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1685 if !valid_runtime_id(&self.workspace_id, 128) {
1686 return Err(HarnessOperatorApiError::InvalidResourceMutationRequest);
1687 }
1688 self.canonical_root.validate()
1689 }
1690}
1691
1692#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1697#[serde(deny_unknown_fields)]
1698pub struct HarnessGitWorktreeSnapshotV1 {
1699 pub path: HarnessHostPathV1,
1700 pub head: String,
1701 pub branch: Option<String>,
1702 pub is_bare: bool,
1703 pub is_main: bool,
1704 pub locked: bool,
1705 pub prunable: bool,
1706 pub workspace_id: Option<String>,
1707}
1708
1709impl HarnessGitWorktreeSnapshotV1 {
1710 fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1711 self.path.validate()?;
1712 if !valid_native_single_line(&self.head, HARNESS_GIT_SUMMARY_MAX_BYTES, true) {
1713 return Err(HarnessOperatorApiError::InvalidResourceMutationRequest);
1714 }
1715 if let Some(branch) = &self.branch {
1716 if !valid_native_single_line(branch, HARNESS_REPOSITORY_PATH_MAX_BYTES, false) {
1717 return Err(HarnessOperatorApiError::InvalidResourceMutationRequest);
1718 }
1719 }
1720 if let Some(workspace_id) = &self.workspace_id {
1721 if !valid_runtime_id(workspace_id, 128) {
1722 return Err(HarnessOperatorApiError::InvalidResourceMutationRequest);
1723 }
1724 }
1725 Ok(())
1726 }
1727}
1728
1729#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1730#[serde(rename_all = "kebab-case")]
1731pub enum HarnessGitSignatureStatusV1 {
1732 Good,
1733 Bad,
1734 UnknownValidity,
1735 ExpiredSignature,
1736 ExpiredKey,
1737 RevokedKey,
1738 CannotCheck,
1739 NoSignature,
1740}
1741
1742#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1743#[serde(deny_unknown_fields)]
1744pub struct HarnessGitCommitV1 {
1745 pub id: HarnessGitObjectIdV1,
1746 pub parents: Vec<HarnessGitObjectIdV1>,
1747 pub subject: String,
1748 pub author_name: String,
1749 pub authored_at: String,
1750 pub committer_name: String,
1751 pub committed_at: String,
1752 pub signature_status: HarnessGitSignatureStatusV1,
1753 pub signer: Option<String>,
1754}
1755
1756impl HarnessGitCommitV1 {
1757 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1758 self.id.validate()?;
1759 if self.parents.len() > HARNESS_GIT_COMMIT_PARENTS_MAX {
1760 return Err(HarnessOperatorApiError::InvalidGitHistory);
1761 }
1762 for parent in &self.parents { parent.validate()?; }
1763 if self.parents.iter().any(|parent| parent == &self.id) {
1764 return Err(HarnessOperatorApiError::InvalidGitHistory);
1765 }
1766 validate_git_single_line(&self.subject, HARNESS_GIT_SUMMARY_MAX_BYTES, false)?;
1767 validate_git_single_line(&self.author_name, HARNESS_GIT_IDENTITY_MAX_BYTES, false)?;
1768 validate_git_single_line(&self.authored_at, HARNESS_GIT_TIMESTAMP_MAX_BYTES, true)?;
1769 validate_git_single_line(&self.committer_name, HARNESS_GIT_IDENTITY_MAX_BYTES, false)?;
1770 validate_git_single_line(&self.committed_at, HARNESS_GIT_TIMESTAMP_MAX_BYTES, true)?;
1771 if let Some(signer) = &self.signer {
1772 validate_git_single_line(signer, HARNESS_GIT_SIGNER_MAX_BYTES, true)?;
1773 }
1774 Ok(())
1775 }
1776}
1777
1778#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1779#[serde(deny_unknown_fields)]
1780pub struct HarnessRunGitHistoryPageV1 {
1781 pub origin: HarnessRunWorkspaceOriginV1,
1782 pub path: Option<HarnessRepositoryPathV1>,
1783 pub commits: Vec<HarnessGitCommitV1>,
1784 pub next_before: Option<HarnessGitObjectIdV1>,
1785 pub truncated: bool,
1786}
1787
1788impl HarnessRunGitHistoryPageV1 {
1789 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1790 self.origin.validate()?;
1791 if let Some(path) = &self.path { path.validate()?; }
1792 if self.commits.len() > usize::from(HARNESS_GIT_HISTORY_LIMIT_MAX) {
1793 return Err(HarnessOperatorApiError::InvalidGitHistory);
1794 }
1795 for commit in &self.commits { commit.validate()?; }
1796 if self.commits.iter().enumerate().any(|(index, commit)| {
1797 self.commits[..index].iter().any(|existing| existing.id == commit.id)
1798 }) {
1799 return Err(HarnessOperatorApiError::InvalidGitHistory);
1800 }
1801 if let Some(next_before) = &self.next_before {
1802 next_before.validate()?;
1803 if self.commits.last().map(|commit| &commit.id) != Some(next_before) {
1804 return Err(HarnessOperatorApiError::InvalidGitHistory);
1805 }
1806 }
1807 Ok(())
1808 }
1809
1810
1811 pub fn validate_for(
1812 &self,
1813 run_id: &HarnessRunId,
1814 path: Option<&HarnessRepositoryPathV1>,
1815 limit: u16,
1816 ) -> Result<(), HarnessOperatorApiError> {
1817 self.validate()?;
1818 self.origin.validate_for(run_id)?;
1819 if self.path.as_ref() != path || self.commits.len() > usize::from(limit) {
1820 return Err(HarnessOperatorApiError::InvalidGitHistory);
1821 }
1822 Ok(())
1823 }
1824}
1825
1826#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1828#[serde(deny_unknown_fields)]
1829pub struct HarnessNodeGitHistoryPageV1 {
1830 pub origin: HarnessNodeWorkspaceOriginV1,
1831 pub path: Option<HarnessRepositoryPathV1>,
1832 pub commits: Vec<HarnessGitCommitV1>,
1833 pub next_before: Option<HarnessGitObjectIdV1>,
1834 pub truncated: bool,
1835}
1836
1837impl HarnessNodeGitHistoryPageV1 {
1838 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1839 self.origin.validate()?;
1840 if let Some(path) = &self.path { path.validate()?; }
1841 if self.commits.len() > usize::from(HARNESS_GIT_HISTORY_LIMIT_MAX) {
1842 return Err(HarnessOperatorApiError::InvalidGitHistory);
1843 }
1844 for commit in &self.commits { commit.validate()?; }
1845 if self.commits.iter().enumerate().any(|(index, commit)| {
1846 self.commits[..index].iter().any(|existing| existing.id == commit.id)
1847 }) {
1848 return Err(HarnessOperatorApiError::InvalidGitHistory);
1849 }
1850 if let Some(next_before) = &self.next_before {
1851 next_before.validate()?;
1852 if self.commits.last().map(|commit| &commit.id) != Some(next_before) {
1853 return Err(HarnessOperatorApiError::InvalidGitHistory);
1854 }
1855 }
1856 Ok(())
1857 }
1858
1859 pub fn validate_for(
1860 &self,
1861 node_id: &str,
1862 workspace_id: &str,
1863 path: Option<&HarnessRepositoryPathV1>,
1864 limit: u16,
1865 ) -> Result<(), HarnessOperatorApiError> {
1866 self.validate()?;
1867 self.origin.validate_for(node_id, workspace_id)?;
1868 if self.path.as_ref() != path || self.commits.len() > usize::from(limit) {
1869 return Err(HarnessOperatorApiError::InvalidGitHistory);
1870 }
1871 Ok(())
1872 }
1873}
1874
1875#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1876#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
1877pub enum HarnessGitDiffModeV1 {
1878 Working,
1879 Staged,
1880 Commit { revision: HarnessGitObjectIdV1 },
1881}
1882
1883impl HarnessGitDiffModeV1 {
1884 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1885 match self {
1886 Self::Working | Self::Staged => Ok(()),
1887 Self::Commit { revision } => revision.validate(),
1888 }
1889 }
1890}
1891
1892#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1893#[serde(deny_unknown_fields)]
1894pub struct HarnessRunGitDiffV1 {
1895 pub origin: HarnessRunWorkspaceOriginV1,
1896 pub mode: HarnessGitDiffModeV1,
1897 pub path: Option<HarnessRepositoryPathV1>,
1898 pub text: String,
1899 pub truncated: bool,
1900}
1901
1902impl HarnessRunGitDiffV1 {
1903 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1904 self.origin.validate()?;
1905 self.mode.validate()?;
1906 if let Some(path) = &self.path { path.validate()?; }
1907 if self.text.len() > HARNESS_GIT_DIFF_MAX_BYTES {
1908 return Err(HarnessOperatorApiError::InvalidGitDiff);
1909 }
1910 Ok(())
1911 }
1912
1913
1914 pub fn validate_for(
1915 &self,
1916 run_id: &HarnessRunId,
1917 mode: &HarnessGitDiffModeV1,
1918 path: Option<&HarnessRepositoryPathV1>,
1919 ) -> Result<(), HarnessOperatorApiError> {
1920 self.validate()?;
1921 self.origin.validate_for(run_id)?;
1922 if &self.mode != mode || self.path.as_ref() != path {
1923 return Err(HarnessOperatorApiError::InvalidGitDiff);
1924 }
1925 Ok(())
1926 }
1927}
1928
1929#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1931#[serde(deny_unknown_fields)]
1932pub struct HarnessNodeGitDiffV1 {
1933 pub origin: HarnessNodeWorkspaceOriginV1,
1934 pub mode: HarnessGitDiffModeV1,
1935 pub path: Option<HarnessRepositoryPathV1>,
1936 pub text: String,
1937 pub truncated: bool,
1938}
1939
1940impl HarnessNodeGitDiffV1 {
1941 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1942 self.origin.validate()?;
1943 self.mode.validate()?;
1944 if let Some(path) = &self.path { path.validate()?; }
1945 if self.text.len() > HARNESS_GIT_DIFF_MAX_BYTES {
1946 return Err(HarnessOperatorApiError::InvalidGitDiff);
1947 }
1948 Ok(())
1949 }
1950
1951 pub fn validate_for(
1952 &self,
1953 node_id: &str,
1954 workspace_id: &str,
1955 mode: &HarnessGitDiffModeV1,
1956 path: Option<&HarnessRepositoryPathV1>,
1957 ) -> Result<(), HarnessOperatorApiError> {
1958 self.validate()?;
1959 self.origin.validate_for(node_id, workspace_id)?;
1960 if &self.mode != mode || self.path.as_ref() != path {
1961 return Err(HarnessOperatorApiError::InvalidGitDiff);
1962 }
1963 Ok(())
1964 }
1965}
1966
1967#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
1968#[serde(deny_unknown_fields)]
1969pub struct HarnessReverseAttributionWorkspaceV1 {
1970 pub node_id: HarnessSelectorV1,
1971 pub node_incarnation_id: HarnessNodeIncarnationV1,
1972 pub workspace_id: HarnessSelectorV1,
1973}
1974
1975impl HarnessReverseAttributionWorkspaceV1 {
1976 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
1977 self.node_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
1978 self.node_incarnation_id.validate()?;
1979 self.workspace_id.validate().map_err(HarnessOperatorApiError::Protocol)
1980 }
1981}
1982
1983#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
1984#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
1985pub enum HarnessReverseAttributionSubjectV1 {
1986 ManagedRecord {
1987 workspace: HarnessReverseAttributionWorkspaceV1,
1988 record_id: HarnessSelectorV1,
1989 },
1990 RuntimeSession {
1991 workspace: HarnessReverseAttributionWorkspaceV1,
1992 instance_id: u64,
1993 generation: u64,
1994 },
1995 Workspace {
1996 workspace: HarnessReverseAttributionWorkspaceV1,
1997 },
1998 FileScope {
1999 workspace: HarnessReverseAttributionWorkspaceV1,
2000 relative_path: HarnessRepositoryPathV1,
2001 },
2002 CommitScope {
2003 workspace: HarnessReverseAttributionWorkspaceV1,
2004 object_id: HarnessGitObjectIdV1,
2005 },
2006}
2007
2008impl HarnessReverseAttributionSubjectV1 {
2009 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
2010 let workspace = match self {
2011 Self::ManagedRecord { workspace, record_id } => {
2012 record_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
2013 workspace
2014 }
2015 Self::RuntimeSession { workspace, instance_id, generation } => {
2016 if *instance_id == 0 || *generation == 0 {
2017 return Err(HarnessOperatorApiError::InvalidReverseAttribution);
2018 }
2019 workspace
2020 }
2021 Self::Workspace { workspace } => workspace,
2022 Self::FileScope { workspace, relative_path } => {
2023 relative_path.validate()?;
2024 workspace
2025 }
2026 Self::CommitScope { workspace, object_id } => {
2027 object_id.validate()?;
2028 workspace
2029 }
2030 };
2031 workspace.validate()
2032 }
2033
2034 fn workspace(&self) -> &HarnessReverseAttributionWorkspaceV1 {
2035 match self {
2036 Self::ManagedRecord { workspace, .. }
2037 | Self::RuntimeSession { workspace, .. }
2038 | Self::Workspace { workspace }
2039 | Self::FileScope { workspace, .. }
2040 | Self::CommitScope { workspace, .. } => workspace,
2041 }
2042 }
2043}
2044
2045#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
2046#[serde(rename_all = "kebab-case")]
2047pub enum HarnessReverseAttributionRelationV1 {
2048 ManagedRecordBinding,
2049 RuntimeSessionBinding,
2050 WorkspaceBinding,
2051 WorkspaceScope,
2052}
2053
2054#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
2055#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
2056pub enum HarnessReverseAttributionBindingV1 {
2057 ManagedRecord {
2058 workspace: HarnessReverseAttributionWorkspaceV1,
2059 record_id: HarnessSelectorV1,
2060 active_instance_id: Option<u64>,
2061 active_generation: Option<u64>,
2062 },
2063 RuntimeSession {
2064 workspace: HarnessReverseAttributionWorkspaceV1,
2065 instance_id: u64,
2066 generation: u64,
2067 },
2068 Workspace {
2069 workspace: HarnessReverseAttributionWorkspaceV1,
2070 },
2071}
2072
2073impl HarnessReverseAttributionBindingV1 {
2074 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
2075 let workspace = match self {
2076 Self::ManagedRecord {
2077 workspace,
2078 record_id,
2079 active_instance_id,
2080 active_generation,
2081 } => {
2082 record_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
2083 if active_instance_id.is_some() != active_generation.is_some()
2084 || active_instance_id == &Some(0)
2085 || active_generation == &Some(0)
2086 {
2087 return Err(HarnessOperatorApiError::InvalidReverseAttribution);
2088 }
2089 workspace
2090 }
2091 Self::RuntimeSession { workspace, instance_id, generation } => {
2092 if *instance_id == 0 || *generation == 0 {
2093 return Err(HarnessOperatorApiError::InvalidReverseAttribution);
2094 }
2095 workspace
2096 }
2097 Self::Workspace { workspace } => workspace,
2098 };
2099 workspace.validate()
2100 }
2101
2102 fn workspace(&self) -> &HarnessReverseAttributionWorkspaceV1 {
2103 match self {
2104 Self::ManagedRecord { workspace, .. }
2105 | Self::RuntimeSession { workspace, .. }
2106 | Self::Workspace { workspace } => workspace,
2107 }
2108 }
2109}
2110
2111#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
2112#[serde(deny_unknown_fields)]
2113pub struct HarnessReverseAttributionLinkV1 {
2114 pub task_id: HarnessTaskId,
2115 pub run_id: HarnessRunId,
2116 pub run_revision: HarnessRevision,
2117 pub binding: HarnessReverseAttributionBindingV1,
2118 pub relation: HarnessReverseAttributionRelationV1,
2119}
2120
2121impl HarnessReverseAttributionLinkV1 {
2122 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
2123 self.task_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
2124 self.run_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
2125 self.run_revision.validate().map_err(HarnessOperatorApiError::Protocol)?;
2126 self.binding.validate()?;
2127 let relation_matches = matches!(
2128 (&self.relation, &self.binding),
2129 (
2130 HarnessReverseAttributionRelationV1::ManagedRecordBinding,
2131 HarnessReverseAttributionBindingV1::ManagedRecord { .. },
2132 ) | (
2133 HarnessReverseAttributionRelationV1::RuntimeSessionBinding,
2134 HarnessReverseAttributionBindingV1::RuntimeSession { .. },
2135 ) | (
2136 HarnessReverseAttributionRelationV1::WorkspaceBinding
2137 | HarnessReverseAttributionRelationV1::WorkspaceScope,
2138 HarnessReverseAttributionBindingV1::Workspace { .. },
2139 )
2140 );
2141 if !relation_matches {
2142 return Err(HarnessOperatorApiError::InvalidReverseAttribution);
2143 }
2144 Ok(())
2145 }
2146}
2147
2148#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2149#[serde(rename_all = "kebab-case")]
2150pub enum HarnessReverseAttributionOutcomeV1 {
2151 Attributed,
2152 Unattributed,
2153}
2154
2155#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2156#[serde(deny_unknown_fields)]
2157pub struct HarnessReverseAttributionV1 {
2158 pub subject: HarnessReverseAttributionSubjectV1,
2159 pub outcome: HarnessReverseAttributionOutcomeV1,
2160 pub links: Vec<HarnessReverseAttributionLinkV1>,
2161}
2162
2163impl HarnessReverseAttributionV1 {
2164 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
2165 self.subject.validate()?;
2166 if self.links.len() > HARNESS_REVERSE_ATTRIBUTION_LINKS_MAX
2167 || matches!(self.outcome, HarnessReverseAttributionOutcomeV1::Attributed)
2168 != !self.links.is_empty()
2169 || self.links.windows(2).any(|pair| pair[0] >= pair[1])
2170 {
2171 return Err(HarnessOperatorApiError::InvalidReverseAttribution);
2172 }
2173 for link in &self.links {
2174 link.validate()?;
2175 if link.binding.workspace() != self.subject.workspace()
2176 || !self.link_matches_subject(link)
2177 {
2178 return Err(HarnessOperatorApiError::InvalidReverseAttribution);
2179 }
2180 }
2181 Ok(())
2182 }
2183
2184 pub fn validate_for(
2185 &self,
2186 subject: &HarnessReverseAttributionSubjectV1,
2187 ) -> Result<(), HarnessOperatorApiError> {
2188 self.validate()?;
2189 if &self.subject != subject {
2190 return Err(HarnessOperatorApiError::InvalidReverseAttribution);
2191 }
2192 Ok(())
2193 }
2194
2195 fn link_matches_subject(&self, link: &HarnessReverseAttributionLinkV1) -> bool {
2196 match (&self.subject, &link.relation, &link.binding) {
2197 (
2198 HarnessReverseAttributionSubjectV1::ManagedRecord { record_id, .. },
2199 HarnessReverseAttributionRelationV1::ManagedRecordBinding,
2200 HarnessReverseAttributionBindingV1::ManagedRecord {
2201 record_id: binding_record_id,
2202 ..
2203 },
2204 ) => record_id == binding_record_id,
2205 (
2206 HarnessReverseAttributionSubjectV1::RuntimeSession {
2207 instance_id,
2208 generation,
2209 ..
2210 },
2211 HarnessReverseAttributionRelationV1::RuntimeSessionBinding,
2212 HarnessReverseAttributionBindingV1::RuntimeSession {
2213 instance_id: binding_instance_id,
2214 generation: binding_generation,
2215 ..
2216 },
2217 ) => instance_id == binding_instance_id && generation == binding_generation,
2218 (
2219 HarnessReverseAttributionSubjectV1::Workspace { .. },
2220 HarnessReverseAttributionRelationV1::WorkspaceBinding,
2221 HarnessReverseAttributionBindingV1::Workspace { .. },
2222 )
2223 | (
2224 HarnessReverseAttributionSubjectV1::FileScope { .. }
2225 | HarnessReverseAttributionSubjectV1::CommitScope { .. },
2226 HarnessReverseAttributionRelationV1::WorkspaceScope,
2227 HarnessReverseAttributionBindingV1::Workspace { .. },
2228 ) => true,
2229 _ => false,
2230 }
2231 }
2232}
2233
2234#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2238#[serde(rename_all = "kebab-case")]
2239pub enum HarnessProviderSessionKeyV1 {
2240 SessionId,
2241 ConversationId,
2242}
2243
2244#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2251#[serde(deny_unknown_fields)]
2252pub struct HarnessProviderSessionIdentityV1 {
2253 pub key: HarnessProviderSessionKeyV1,
2254 pub id: String,
2255 pub transcript_path: Option<String>,
2256}
2257
2258impl HarnessProviderSessionIdentityV1 {
2259 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
2260 if self.id.is_empty()
2261 || self.id.len() > HARNESS_PROVIDER_SESSION_ID_MAX_BYTES
2262 || self.id.starts_with('-')
2263 || self.id.chars().any(char::is_control)
2264 || self.transcript_path.as_ref().is_some_and(|path| {
2265 path.is_empty()
2266 || path.len() > HARNESS_PROVIDER_SESSION_TRANSCRIPT_PATH_MAX_BYTES
2267 || path.chars().any(char::is_control)
2268 })
2269 {
2270 return Err(HarnessOperatorApiError::InvalidSessionRecordRequest);
2271 }
2272 Ok(())
2273 }
2274}
2275
2276#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2286#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
2287pub enum HarnessSessionTaskTargetV1 {
2288 New,
2289 Existing { task_id: String },
2290 Clear,
2291}
2292
2293impl HarnessSessionTaskTargetV1 {
2294 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
2295 if let Self::Existing { task_id } = self {
2296 if !valid_runtime_id(task_id, HARNESS_SESSION_RECORD_TASK_ID_MAX_BYTES) {
2297 return Err(HarnessOperatorApiError::InvalidSessionRecordRequest);
2298 }
2299 }
2300 Ok(())
2301 }
2302}
2303
2304#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
2311#[serde(rename_all = "kebab-case")]
2312pub enum HarnessProviderInteractionKindV1 {
2313 Approval,
2314 Question,
2315}
2316
2317#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2332#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
2333pub enum HarnessProviderInteractionResponseV1 {
2334 ApproveOnce,
2335 Deny,
2336 Answer { text: String },
2337}
2338
2339impl HarnessProviderInteractionResponseV1 {
2340 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
2341 match self {
2342 Self::ApproveOnce | Self::Deny => Ok(()),
2343 Self::Answer { text } => {
2344 if text.len() > HARNESS_ACP_INTERACTION_RESPONSE_MAX_BYTES
2345 || contains_unsafe_control_bytes(text)
2346 {
2347 return Err(HarnessOperatorApiError::InvalidSessionControl);
2348 }
2349 Ok(())
2350 }
2351 }
2352 }
2353}
2354
2355#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
2356#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
2357pub enum HarnessOperatorRequestV1 {
2358 MonitorGet { run_id: HarnessRunId },
2359 TimelineRead {
2360 run_id: HarnessRunId,
2361 after_sequence: Option<u64>,
2362 limit: u16,
2363 },
2364 TasksList {
2365 after_task_id: Option<HarnessTaskId>,
2366 state: Option<HarnessTaskStateV1>,
2367 #[serde(default)]
2374 parent_task_id: Option<HarnessTaskId>,
2375 limit: u16,
2376 },
2377 TaskGet { task_id: HarnessTaskId },
2378 TaskOperations { task_id: HarnessTaskId, limit: u16 },
2385 RunsList {
2386 task_id: Option<HarnessTaskId>,
2387 after_run_id: Option<HarnessRunId>,
2388 lifecycle: Option<HarnessRunLifecycleV1>,
2389 #[serde(default)]
2393 parent_run_id: Option<HarnessRunId>,
2394 limit: u16,
2395 },
2396 RunGet { run_id: HarnessRunId },
2397 RunCorrelationGet { run_id: HarnessRunId },
2398 RunTransferGet { run_id: HarnessRunId },
2399 ReverseAttributionGet { subject: HarnessReverseAttributionSubjectV1 },
2400 ObserveRunContextSource { run_id: HarnessRunId },
2401 InspectRunWorkspace { run_id: HarnessRunId },
2402 ReadRunWorkspaceFile {
2403 run_id: HarnessRunId,
2404 path: HarnessRepositoryPathV1,
2405 },
2406 ReadRunGitHistory {
2407 run_id: HarnessRunId,
2408 path: Option<HarnessRepositoryPathV1>,
2409 before: Option<HarnessGitObjectIdV1>,
2410 limit: u16,
2411 },
2412 ReadRunGitDiff {
2413 run_id: HarnessRunId,
2414 mode: HarnessGitDiffModeV1,
2415 path: Option<HarnessRepositoryPathV1>,
2416 },
2417 InspectNodeWorkspace { node_id: String, workspace_id: String },
2422 ReadNodeWorkspaceFile {
2423 node_id: String,
2424 workspace_id: String,
2425 path: HarnessRepositoryPathV1,
2426 },
2427 ReadNodeGitHistory {
2428 node_id: String,
2429 workspace_id: String,
2430 path: Option<HarnessRepositoryPathV1>,
2431 before: Option<HarnessGitObjectIdV1>,
2432 limit: u16,
2433 },
2434 ReadNodeGitDiff {
2435 node_id: String,
2436 workspace_id: String,
2437 mode: HarnessGitDiffModeV1,
2438 path: Option<HarnessRepositoryPathV1>,
2439 },
2440 WriteNodeWorkspaceFile {
2458 node_id: String,
2459 workspace_id: String,
2460 path: HarnessRepositoryPathV1,
2461 content: String,
2462 expected_revision: HarnessWorkspaceFileRevisionV1,
2463 },
2464 CreateNodeWorkspaceFile {
2465 node_id: String,
2466 workspace_id: String,
2467 path: HarnessRepositoryPathV1,
2468 },
2469 CreateNodeWorkspaceDirectory {
2470 node_id: String,
2471 workspace_id: String,
2472 path: HarnessRepositoryPathV1,
2473 },
2474 LaunchPlansList {
2475 after_plan_id: Option<HarnessSelectorV1>,
2476 limit: u16,
2477 },
2478 TaskExecutionSpecGet { task_id: HarnessTaskId },
2479 TaskLaunchOptionsGet {
2486 task_id: HarnessTaskId,
2487 #[serde(default)]
2488 provider: Option<HarnessSelectorV1>,
2489 #[serde(default)]
2490 workspace: Option<HarnessSelectorV1>,
2491 #[serde(default)]
2492 plan_id: Option<HarnessSelectorV1>,
2493 #[serde(default)]
2494 after: Option<HarnessSelectorV1>,
2495 },
2496 RuntimeInventoryList {
2497 after_node_id: Option<String>,
2498 limit: u16,
2499 },
2500 TerminalRead {
2501 session: HarnessRuntimeSessionAddressV1,
2502 after_sequence: Option<u64>,
2503 limit: u16,
2504 },
2505 SpawnSession {
2513 node_id: String,
2514 workspace_id: String,
2515 provider: String,
2516 provider_profile: String,
2517 mode: HarnessExecutionModeV1,
2518 terminal_size: HarnessRuntimeTerminalSizeV1,
2519 #[serde(default)]
2529 approval_level: Option<HarnessApprovalLevelV1>,
2530 },
2531 WriteSessionInput {
2532 session: HarnessRuntimeSessionAddressV1,
2533 text: String,
2534 },
2535 PromptSession {
2574 session: HarnessRuntimeSessionAddressV1,
2575 text: String,
2576 },
2577 ResizeSession {
2578 session: HarnessRuntimeSessionAddressV1,
2579 terminal_size: HarnessRuntimeTerminalSizeV1,
2580 },
2581 StopSession {
2582 session: HarnessRuntimeSessionAddressV1,
2583 force: bool,
2584 },
2585 ControlSession {
2606 session: HarnessRuntimeSessionAddressV1,
2607 control: HarnessTerminalControlV1,
2608 },
2609 WriteSessionBytes {
2610 session: HarnessRuntimeSessionAddressV1,
2611 bytes: Vec<u8>,
2612 },
2613 PasteSession {
2614 session: HarnessRuntimeSessionAddressV1,
2615 text: String,
2616 },
2617 RemoveSession {
2618 session: HarnessRuntimeSessionAddressV1,
2619 },
2620 ResumeSession {
2621 session: HarnessRuntimeSessionAddressV1,
2622 terminal_size: HarnessRuntimeTerminalSizeV1,
2623 },
2624 ResolveInteraction {
2650 session: HarnessRuntimeSessionAddressV1,
2651 correlation_id: String,
2652 response: HarnessProviderInteractionResponseV1,
2653 },
2654 SetSessionMode {
2655 session: HarnessRuntimeSessionAddressV1,
2656 mode_id: String,
2657 },
2658 SetSessionConfigOption {
2659 session: HarnessRuntimeSessionAddressV1,
2660 option_id: String,
2661 value_json: String,
2662 },
2663 SetSessionModel {
2664 session: HarnessRuntimeSessionAddressV1,
2665 model_id: String,
2666 },
2667 CatalogNativeSessions {
2668 route: HarnessNativeSessionRouteV1,
2669 limit: u16,
2670 },
2671 PageNativeSessions {
2672 route: HarnessNativeSessionRouteV1,
2673 window: HarnessNativeSessionCatalogWindowV1,
2674 catalog_revision: u64,
2675 recent_cutoff_unix_ms: u64,
2676 after_selection_id: Option<String>,
2677 limit: u16,
2678 },
2679 PreviewNativeSession {
2680 selection: HarnessNativeSessionSelectionV1,
2681 message_limit: u16,
2682 },
2683 PreviewSessionRecord {
2696 node_id: String,
2697 record_id: String,
2698 message_limit: u16,
2699 },
2700 ResumeSessionRecord {
2714 node_id: String,
2715 record_id: String,
2716 terminal_size: HarnessRuntimeTerminalSizeV1,
2717 initial_prompt: Option<String>,
2718 },
2719 RenameSessionRecord {
2720 node_id: String,
2721 record_id: String,
2722 display_name: String,
2723 },
2724 SetSessionTask {
2725 node_id: String,
2726 record_id: String,
2727 expected_revision: u64,
2728 target: HarnessSessionTaskTargetV1,
2729 },
2730 ForgetSessionRecord {
2731 node_id: String,
2732 record_id: String,
2733 },
2734 IndexProviderSession {
2735 node_id: String,
2736 workspace_id: String,
2737 provider: String,
2738 identity: HarnessProviderSessionIdentityV1,
2739 display_name: String,
2740 },
2741 IndexNativeSession {
2750 selection: HarnessNativeSessionSelectionV1,
2751 display_name: String,
2752 },
2753 CreateTask { request: HarnessCreateTaskRequestV1 },
2754 ReplaceTask { request: HarnessReplaceTaskRequestV1 },
2755 MoveTask { request: HarnessMoveTaskRequestV1 },
2756 CancelTask { request: HarnessCancelTaskRequestV1 },
2757 RetryTask { request: HarnessRetryTaskRequestV1 },
2758 ScheduleNext { request: HarnessScheduleNextRequestV1 },
2759 ReplaceTaskExecutionSpec { request: HarnessReplaceTaskExecutionSpecRequestV1 },
2760 StartTask { request: HarnessStartTaskRequestV1 },
2761 ReplaceTaskExecutionSpecV2 { request: HarnessReplaceTaskExecutionSpecRequestV2 },
2762 StartTaskV2 { request: HarnessStartTaskRequestV2 },
2763 MintHarnessMcpGrant { request: HarnessStartTaskRequestV2 },
2770 SubmitIntent { intent: HarnessOperatorIntentV1 },
2771 BrowseHostDirectories {
2782 node_id: String,
2783 directory: Option<HarnessHostPathV1>,
2784 after: Option<HarnessHostPathV1>,
2785 },
2786 RegisterWorkspace {
2802 node_id: String,
2803 workspace_id: String,
2804 root: HarnessHostPathV1,
2805 },
2806 UnregisterWorkspace {
2807 node_id: String,
2808 workspace_id: String,
2809 },
2810 CreateStandaloneWorkspace {
2811 node_id: String,
2812 workspace_id: String,
2813 root: HarnessHostPathV1,
2814 initial_branch: Option<String>,
2815 },
2816 CreateWorktree {
2817 node_id: String,
2818 source_workspace_id: String,
2819 workspace_id: String,
2820 target_root: HarnessHostPathV1,
2821 branch: String,
2822 base: Option<String>,
2823 },
2824 RemoveWorktree {
2825 node_id: String,
2826 source_workspace_id: String,
2827 target_root: HarnessHostPathV1,
2828 },
2829 ExportContextPack {
2841 session: HarnessRuntimeSessionAddressV1,
2842 },
2843 ForgetContextPack {
2848 node_id: String,
2849 context_id: HarnessSelectorV1,
2850 },
2851 SubscribeEvents {},
2855 SubscribeTerminal { sessions: Vec<HarnessRuntimeSessionAddressV1> },
2864 SubscribeAgentStream { sessions: Vec<HarnessRuntimeSessionAddressV1> },
2875}
2876
2877impl HarnessOperatorRequestV1 {
2878 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
2879 match self {
2880 Self::MonitorGet { run_id } => {
2881 run_id.validate().map_err(HarnessOperatorApiError::Protocol)
2882 }
2883 Self::TimelineRead { run_id, after_sequence, limit } => {
2884 run_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
2885 if after_sequence == &Some(0) {
2886 return Err(HarnessOperatorApiError::InvalidCursor);
2887 }
2888 validate_operator_timeline_limit(*limit)
2889 }
2890 Self::TasksList { after_task_id, parent_task_id, limit, .. } => {
2891 if let Some(task_id) = after_task_id {
2892 task_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
2893 }
2894 if let Some(task_id) = parent_task_id {
2895 task_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
2896 }
2897 validate_operator_limit(*limit)
2898 }
2899 Self::TaskGet { task_id } => {
2900 task_id.validate().map_err(HarnessOperatorApiError::Protocol)
2901 }
2902 Self::TaskOperations { task_id, limit } => {
2903 task_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
2904 validate_operator_limit(*limit)
2905 }
2906 Self::RunsList { task_id, after_run_id, parent_run_id, limit, .. } => {
2907 if let Some(task_id) = task_id {
2908 task_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
2909 }
2910 if let Some(run_id) = after_run_id {
2911 run_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
2912 }
2913 if let Some(run_id) = parent_run_id {
2914 run_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
2915 }
2916 validate_operator_limit(*limit)
2917 }
2918 Self::RunGet { run_id }
2919 | Self::RunCorrelationGet { run_id }
2920 | Self::RunTransferGet { run_id }
2921 | Self::ObserveRunContextSource { run_id }
2922 | Self::InspectRunWorkspace { run_id } => {
2923 run_id.validate().map_err(HarnessOperatorApiError::Protocol)
2924 }
2925 Self::ReverseAttributionGet { subject } => subject.validate(),
2926 Self::ReadRunWorkspaceFile { run_id, path } => {
2927 run_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
2928 path.validate()
2929 }
2930 Self::ReadRunGitHistory { run_id, path, before, limit } => {
2931 run_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
2932 if let Some(path) = path { path.validate()?; }
2933 if let Some(before) = before { before.validate()?; }
2934 if !(1..=HARNESS_GIT_HISTORY_LIMIT_MAX).contains(limit) {
2935 return Err(HarnessOperatorApiError::InvalidLimit);
2936 }
2937 Ok(())
2938 }
2939 Self::ReadRunGitDiff { run_id, mode, path } => {
2940 run_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
2941 mode.validate()?;
2942 if let Some(path) = path { path.validate()?; }
2943 Ok(())
2944 }
2945 Self::InspectNodeWorkspace { node_id, workspace_id } => {
2946 validate_node_workspace_route(node_id, workspace_id)
2947 }
2948 Self::ReadNodeWorkspaceFile { node_id, workspace_id, path } => {
2949 validate_node_workspace_route(node_id, workspace_id)?;
2950 path.validate()
2951 }
2952 Self::ReadNodeGitHistory { node_id, workspace_id, path, before, limit } => {
2953 validate_node_workspace_route(node_id, workspace_id)?;
2954 if let Some(path) = path { path.validate()?; }
2955 if let Some(before) = before { before.validate()?; }
2956 if !(1..=HARNESS_GIT_HISTORY_LIMIT_MAX).contains(limit) {
2957 return Err(HarnessOperatorApiError::InvalidLimit);
2958 }
2959 Ok(())
2960 }
2961 Self::ReadNodeGitDiff { node_id, workspace_id, mode, path } => {
2962 validate_node_workspace_route(node_id, workspace_id)?;
2963 mode.validate()?;
2964 if let Some(path) = path { path.validate()?; }
2965 Ok(())
2966 }
2967 Self::WriteNodeWorkspaceFile {
2968 node_id, workspace_id, path, content, expected_revision,
2969 } => {
2970 validate_node_workspace_route(node_id, workspace_id)?;
2971 path.validate()?;
2972 expected_revision.validate()?;
2973 if content.len() > HARNESS_WORKSPACE_FILE_MAX_BYTES {
2974 return Err(HarnessOperatorApiError::InvalidWorkspaceFile);
2975 }
2976 Ok(())
2977 }
2978 Self::CreateNodeWorkspaceFile { node_id, workspace_id, path }
2979 | Self::CreateNodeWorkspaceDirectory { node_id, workspace_id, path } => {
2980 validate_node_workspace_route(node_id, workspace_id)?;
2981 path.validate()
2982 }
2983 Self::LaunchPlansList { after_plan_id, limit } => {
2984 if let Some(plan_id) = after_plan_id {
2985 plan_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
2986 }
2987 if !(1..=HARNESS_LAUNCH_PLAN_PAGE_LIMIT_MAX).contains(limit) {
2988 return Err(HarnessOperatorApiError::InvalidLimit);
2989 }
2990 Ok(())
2991 }
2992 Self::TaskExecutionSpecGet { task_id } => {
2993 task_id.validate().map_err(HarnessOperatorApiError::Protocol)
2994 }
2995 Self::TaskLaunchOptionsGet { task_id, provider, workspace, plan_id, after } => {
2996 task_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
2997 if let Some(provider) = provider {
2998 provider.validate().map_err(HarnessOperatorApiError::Protocol)?;
2999 }
3000 if let Some(workspace) = workspace {
3001 workspace.validate().map_err(HarnessOperatorApiError::Protocol)?;
3002 }
3003 if let Some(plan_id) = plan_id {
3004 plan_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
3005 }
3006 if let Some(after) = after {
3007 after.validate().map_err(|_| HarnessOperatorApiError::InvalidCursor)?;
3008 }
3009 Ok(())
3010 }
3011 Self::RuntimeInventoryList { after_node_id, limit } => {
3012 if !(1..=HARNESS_RUNTIME_INVENTORY_PAGE_LIMIT_MAX).contains(limit) {
3013 return Err(HarnessOperatorApiError::InvalidLimit);
3014 }
3015 if after_node_id.as_deref().is_some_and(|node_id| {
3016 !valid_runtime_id(node_id, 128)
3017 }) {
3018 return Err(HarnessOperatorApiError::InvalidCursor);
3019 }
3020 Ok(())
3021 }
3022 Self::TerminalRead { session, limit, .. } => {
3023 session.validate()?;
3026 validate_operator_terminal_limit(*limit)
3027 }
3028 Self::SpawnSession { node_id, workspace_id, provider, provider_profile, terminal_size, .. } => {
3029 validate_node_workspace_route(node_id, workspace_id)?;
3030 if !valid_runtime_id(provider, 128) || !valid_runtime_id(provider_profile, 128) {
3031 return Err(HarnessOperatorApiError::InvalidSessionSpawn);
3032 }
3033 if terminal_size.rows == 0 || terminal_size.columns == 0 {
3034 return Err(HarnessOperatorApiError::InvalidSessionSpawn);
3035 }
3036 Ok(())
3037 }
3038 Self::WriteSessionInput { session, text } => {
3039 session.validate()?;
3040 if text.len() > HARNESS_SESSION_INPUT_MAX_BYTES {
3041 return Err(HarnessOperatorApiError::InvalidSessionControl);
3042 }
3043 Ok(())
3044 }
3045 Self::PromptSession { session, text } => {
3052 session.validate()?;
3053 if text.len() > HARNESS_SESSION_INPUT_MAX_BYTES {
3054 return Err(HarnessOperatorApiError::InvalidSessionControl);
3055 }
3056 Ok(())
3057 }
3058 Self::ResizeSession { session, terminal_size } => {
3059 session.validate()?;
3060 if terminal_size.rows == 0 || terminal_size.columns == 0 {
3061 return Err(HarnessOperatorApiError::InvalidSessionControl);
3062 }
3063 Ok(())
3064 }
3065 Self::StopSession { session, .. } => session.validate(),
3066 Self::ControlSession { session, .. } => session.validate(),
3067 Self::WriteSessionBytes { session, bytes } => {
3068 session.validate()?;
3069 if bytes.is_empty() || bytes.len() > HARNESS_SESSION_BYTES_MAX_BYTES {
3070 return Err(HarnessOperatorApiError::InvalidSessionControl);
3071 }
3072 Ok(())
3073 }
3074 Self::PasteSession { session, text } => {
3075 session.validate()?;
3076 if text.len() > HARNESS_SESSION_INPUT_MAX_BYTES {
3077 return Err(HarnessOperatorApiError::InvalidSessionControl);
3078 }
3079 Ok(())
3080 }
3081 Self::RemoveSession { session } => session.validate(),
3082 Self::ResumeSession { session, terminal_size } => {
3083 session.validate()?;
3084 if terminal_size.rows == 0 || terminal_size.columns == 0 {
3085 return Err(HarnessOperatorApiError::InvalidSessionControl);
3086 }
3087 Ok(())
3088 }
3089 Self::ResolveInteraction { session, correlation_id, response } => {
3090 session.validate()?;
3091 if !valid_agent_stream_correlation_id(correlation_id) {
3092 return Err(HarnessOperatorApiError::InvalidSessionControl);
3093 }
3094 response.validate()
3095 }
3096 Self::SetSessionMode { session, mode_id } => {
3097 session.validate()?;
3098 if !valid_agent_stream_id(mode_id) {
3099 return Err(HarnessOperatorApiError::InvalidSessionControl);
3100 }
3101 Ok(())
3102 }
3103 Self::SetSessionConfigOption { session, option_id, value_json } => {
3104 session.validate()?;
3105 if !valid_agent_stream_id(option_id)
3106 || value_json.is_empty()
3107 || value_json.len() > HARNESS_AGENT_STREAM_TEXT_MAX_BYTES
3108 || contains_unsafe_control_bytes(value_json)
3109 {
3110 return Err(HarnessOperatorApiError::InvalidSessionControl);
3111 }
3112 Ok(())
3113 }
3114 Self::SetSessionModel { session, model_id } => {
3115 session.validate()?;
3116 if !valid_agent_stream_id(model_id) {
3117 return Err(HarnessOperatorApiError::InvalidSessionControl);
3118 }
3119 Ok(())
3120 }
3121 Self::CatalogNativeSessions { route, limit } => {
3122 route.validate()?;
3123 validate_native_session_catalog_limit(*limit)
3124 }
3125 Self::PageNativeSessions {
3126 route,
3127 catalog_revision,
3128 after_selection_id,
3129 limit,
3130 ..
3131 } => {
3132 route.validate()?;
3133 if *catalog_revision == 0
3134 || after_selection_id.as_deref().is_some_and(|value| {
3135 !valid_native_selection_id(value)
3136 })
3137 {
3138 return Err(HarnessOperatorApiError::InvalidNativeHistory);
3139 }
3140 validate_native_session_catalog_limit(*limit)
3141 }
3142 Self::PreviewNativeSession { selection, message_limit } => {
3143 selection.validate()?;
3144 if !(1..=HARNESS_NATIVE_SESSION_PREVIEW_MESSAGE_LIMIT_MAX)
3145 .contains(message_limit)
3146 {
3147 return Err(HarnessOperatorApiError::InvalidLimit);
3148 }
3149 Ok(())
3150 }
3151 Self::PreviewSessionRecord { node_id, record_id, message_limit } => {
3152 if !valid_runtime_id(node_id, 128) || !valid_runtime_id(record_id, 128) {
3153 return Err(HarnessOperatorApiError::InvalidSessionRecordRequest);
3154 }
3155 if !(1..=HARNESS_NATIVE_SESSION_PREVIEW_MESSAGE_LIMIT_MAX)
3156 .contains(message_limit)
3157 {
3158 return Err(HarnessOperatorApiError::InvalidLimit);
3159 }
3160 Ok(())
3161 }
3162 Self::ResumeSessionRecord { node_id, record_id, terminal_size, initial_prompt } => {
3163 if !valid_runtime_id(node_id, 128)
3164 || !valid_runtime_id(record_id, 128)
3165 || terminal_size.rows == 0
3166 || terminal_size.columns == 0
3167 {
3168 return Err(HarnessOperatorApiError::InvalidSessionRecordRequest);
3169 }
3170 if initial_prompt.as_deref().is_some_and(|prompt| {
3171 prompt.is_empty() || prompt.len() > HARNESS_BODY_MAX_BYTES
3172 }) {
3173 return Err(HarnessOperatorApiError::InvalidSessionRecordRequest);
3174 }
3175 Ok(())
3176 }
3177 Self::RenameSessionRecord { node_id, record_id, display_name } => {
3178 if !valid_runtime_id(node_id, 128)
3179 || !valid_runtime_id(record_id, 128)
3180 || display_name.is_empty()
3181 || display_name.len() > HARNESS_SESSION_RECORD_DISPLAY_NAME_MAX_BYTES
3182 || display_name.chars().any(char::is_control)
3183 {
3184 return Err(HarnessOperatorApiError::InvalidSessionRecordRequest);
3185 }
3186 Ok(())
3187 }
3188 Self::SetSessionTask { node_id, record_id, target, .. } => {
3189 if !valid_runtime_id(node_id, 128) || !valid_runtime_id(record_id, 128) {
3190 return Err(HarnessOperatorApiError::InvalidSessionRecordRequest);
3191 }
3192 target.validate()
3193 }
3194 Self::ForgetSessionRecord { node_id, record_id } => {
3195 if !valid_runtime_id(node_id, 128) || !valid_runtime_id(record_id, 128) {
3196 return Err(HarnessOperatorApiError::InvalidSessionRecordRequest);
3197 }
3198 Ok(())
3199 }
3200 Self::IndexProviderSession { node_id, workspace_id, provider, identity, display_name } => {
3201 if !valid_runtime_id(node_id, 128)
3202 || !valid_runtime_id(workspace_id, 128)
3203 || !valid_runtime_id(provider, 128)
3204 || display_name.is_empty()
3205 || display_name.len() > HARNESS_SESSION_RECORD_DISPLAY_NAME_MAX_BYTES
3206 || display_name.chars().any(char::is_control)
3207 {
3208 return Err(HarnessOperatorApiError::InvalidSessionRecordRequest);
3209 }
3210 identity.validate()
3211 }
3212 Self::IndexNativeSession { selection, display_name } => {
3213 selection.validate()?;
3214 if display_name.is_empty()
3215 || display_name.len() > HARNESS_SESSION_RECORD_DISPLAY_NAME_MAX_BYTES
3216 || display_name.chars().any(char::is_control)
3217 {
3218 return Err(HarnessOperatorApiError::InvalidSessionRecordRequest);
3219 }
3220 Ok(())
3221 }
3222 Self::CreateTask { request } => {
3223 request.validate().map_err(HarnessOperatorApiError::Protocol)
3224 }
3225 Self::ReplaceTask { request } => {
3226 request.validate().map_err(HarnessOperatorApiError::Protocol)
3227 }
3228 Self::MoveTask { request } => {
3229 request.validate().map_err(HarnessOperatorApiError::Protocol)
3230 }
3231 Self::CancelTask { request } => {
3232 request.validate().map_err(HarnessOperatorApiError::Protocol)
3233 }
3234 Self::RetryTask { request } => {
3235 request.validate().map_err(HarnessOperatorApiError::Protocol)
3236 }
3237 Self::ScheduleNext { request } => {
3238 request.validate().map_err(HarnessOperatorApiError::Protocol)
3239 }
3240 Self::ReplaceTaskExecutionSpec { request } => {
3241 request.validate().map_err(HarnessOperatorApiError::Protocol)
3242 }
3243 Self::StartTask { request } => {
3244 request.validate().map_err(HarnessOperatorApiError::Protocol)
3245 }
3246 Self::ReplaceTaskExecutionSpecV2 { request } => request.validate(),
3247 Self::StartTaskV2 { request } => request.validate(),
3248 Self::MintHarnessMcpGrant { request } => request.validate(),
3249 Self::SubmitIntent { intent } => intent.validate(),
3250 Self::BrowseHostDirectories { node_id, directory, after } => {
3251 if !valid_runtime_id(node_id, 128) {
3252 return Err(HarnessOperatorApiError::InvalidHostDirectoryBrowseRequest);
3253 }
3254 if let Some(directory) = directory { directory.validate()?; }
3255 if let Some(after) = after { after.validate()?; }
3256 Ok(())
3257 }
3258 Self::RegisterWorkspace { node_id, workspace_id, root } => {
3259 if !valid_runtime_id(node_id, 128) || !valid_runtime_id(workspace_id, 128) {
3260 return Err(HarnessOperatorApiError::InvalidResourceMutationRequest);
3261 }
3262 root.validate()
3263 }
3264 Self::UnregisterWorkspace { node_id, workspace_id } => {
3265 if !valid_runtime_id(node_id, 128) || !valid_runtime_id(workspace_id, 128) {
3266 return Err(HarnessOperatorApiError::InvalidResourceMutationRequest);
3267 }
3268 Ok(())
3269 }
3270 Self::CreateStandaloneWorkspace { node_id, workspace_id, root, initial_branch } => {
3271 if !valid_runtime_id(node_id, 128) || !valid_runtime_id(workspace_id, 128) {
3272 return Err(HarnessOperatorApiError::InvalidResourceMutationRequest);
3273 }
3274 root.validate()?;
3275 if let Some(branch) = initial_branch {
3276 if !valid_native_single_line(branch, HARNESS_REPOSITORY_PATH_MAX_BYTES, true) {
3277 return Err(HarnessOperatorApiError::InvalidResourceMutationRequest);
3278 }
3279 }
3280 Ok(())
3281 }
3282 Self::CreateWorktree {
3283 node_id, source_workspace_id, workspace_id, target_root, branch, base,
3284 } => {
3285 if !valid_runtime_id(node_id, 128)
3286 || !valid_runtime_id(source_workspace_id, 128)
3287 || !valid_runtime_id(workspace_id, 128)
3288 || !valid_native_single_line(branch, HARNESS_REPOSITORY_PATH_MAX_BYTES, true)
3289 {
3290 return Err(HarnessOperatorApiError::InvalidResourceMutationRequest);
3291 }
3292 if let Some(base) = base {
3293 if !valid_native_single_line(base, HARNESS_REPOSITORY_PATH_MAX_BYTES, true) {
3294 return Err(HarnessOperatorApiError::InvalidResourceMutationRequest);
3295 }
3296 }
3297 target_root.validate()
3298 }
3299 Self::RemoveWorktree { node_id, source_workspace_id, target_root } => {
3300 if !valid_runtime_id(node_id, 128) || !valid_runtime_id(source_workspace_id, 128) {
3301 return Err(HarnessOperatorApiError::InvalidResourceMutationRequest);
3302 }
3303 target_root.validate()
3304 }
3305 Self::ExportContextPack { session } => session.validate(),
3306 Self::ForgetContextPack { node_id, context_id } => {
3307 if !valid_runtime_id(node_id, 128) {
3308 return Err(HarnessOperatorApiError::InvalidResourceMutationRequest);
3309 }
3310 context_id.validate().map_err(HarnessOperatorApiError::Protocol)
3311 }
3312 Self::SubscribeEvents {} => Ok(()),
3313 Self::SubscribeTerminal { sessions } => {
3314 if sessions.is_empty()
3315 || sessions.len() > HARNESS_TERMINAL_SUBSCRIPTION_SESSIONS_MAX
3316 {
3317 return Err(HarnessOperatorApiError::InvalidTerminalPage);
3318 }
3319 for session in sessions { session.validate()?; }
3320 let mut seen = std::collections::HashSet::with_capacity(sessions.len());
3321 if !sessions.iter().all(|session| seen.insert(session)) {
3322 return Err(HarnessOperatorApiError::InvalidTerminalPage);
3323 }
3324 Ok(())
3325 }
3326 Self::SubscribeAgentStream { sessions } => {
3327 if sessions.is_empty()
3328 || sessions.len() > HARNESS_AGENT_STREAM_SUBSCRIPTION_SESSIONS_MAX
3329 {
3330 return Err(HarnessOperatorApiError::InvalidAgentStream);
3331 }
3332 for session in sessions { session.validate()?; }
3333 let mut seen = std::collections::HashSet::with_capacity(sessions.len());
3334 if !sessions.iter().all(|session| seen.insert(session)) {
3335 return Err(HarnessOperatorApiError::InvalidAgentStream);
3336 }
3337 Ok(())
3338 }
3339 }
3340 }
3341
3342}
3343
3344#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3345#[serde(tag = "status", rename_all = "kebab-case", deny_unknown_fields)]
3346pub enum HarnessOperatorReplyV1 {
3347 Ok { response: HarnessOperatorResponseV1 },
3348 Error { error: HarnessOperatorHostErrorV1 },
3349}
3350
3351impl HarnessOperatorReplyV1 {
3352 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
3353 match self {
3354 Self::Ok { response } => response.validate(),
3355 Self::Error { .. } => Ok(()),
3356 }
3357 }
3358}
3359
3360#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3372#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
3373pub enum HarnessOperatorEventV1 {
3374 SnapshotBaseline {
3379 sequence: u64,
3380 tasks: Vec<RedactedTaskV1>,
3381 runs: Vec<RedactedRunV1>,
3382 nodes: Vec<HarnessRuntimeNodeInventoryV1>,
3383 },
3384 TaskChanged { sequence: u64, task: RedactedTaskV1 },
3385 RunChanged { sequence: u64, run: RedactedRunV1 },
3386 RuntimeInventoryChanged { sequence: u64, node: HarnessRuntimeNodeInventoryV1 },
3387 RuntimeInventoryRemoved { sequence: u64, node_id: String },
3388 Lagged { sequence: u64 },
3395 Ping { sequence: u64 },
3414}
3415
3416impl HarnessOperatorEventV1 {
3417 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
3418 match self {
3419 Self::SnapshotBaseline { tasks, runs, nodes, .. } => {
3420 for task in tasks { task.validate().map_err(HarnessOperatorApiError::Read)?; }
3421 if tasks.windows(2).any(|pair| pair[0].task_id >= pair[1].task_id) {
3422 return Err(HarnessOperatorApiError::Read(HarnessReadApiError::InvalidCollection));
3423 }
3424 for run in runs { run.validate().map_err(HarnessOperatorApiError::Read)?; }
3425 if runs.windows(2).any(|pair| pair[0].run_id >= pair[1].run_id) {
3426 return Err(HarnessOperatorApiError::Read(HarnessReadApiError::InvalidCollection));
3427 }
3428 for node in nodes { node.validate()?; }
3429 if nodes.windows(2).any(|pair| pair[0].node_id >= pair[1].node_id) {
3430 return Err(HarnessOperatorApiError::InvalidRuntimeInventory);
3431 }
3432 Ok(())
3433 }
3434 Self::TaskChanged { task, .. } => task.validate().map_err(HarnessOperatorApiError::Read),
3435 Self::RunChanged { run, .. } => run.validate().map_err(HarnessOperatorApiError::Read),
3436 Self::RuntimeInventoryChanged { node, .. } => node.validate(),
3437 Self::RuntimeInventoryRemoved { node_id, .. } => {
3438 if !valid_runtime_id(node_id, 128) {
3439 return Err(HarnessOperatorApiError::InvalidRuntimeInventory);
3440 }
3441 Ok(())
3442 }
3443 Self::Lagged { .. } => Ok(()),
3444 Self::Ping { .. } => Ok(()),
3445 }
3446 }
3447}
3448
3449#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3459#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
3460pub enum HarnessOperatorTerminalEventV1 {
3461 TerminalFrame {
3469 sequence: u64,
3470 session: HarnessRuntimeSessionAddressV1,
3471 frame: HarnessRuntimeTerminalFrameV1,
3472 coalesced_since_last: u32,
3477 },
3478 Ping { sequence: u64 },
3482}
3483
3484impl HarnessOperatorTerminalEventV1 {
3485 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
3486 match self {
3487 Self::TerminalFrame { session, frame, .. } => {
3488 session.validate()?;
3489 frame.validate()
3490 }
3491 Self::Ping { .. } => Ok(()),
3492 }
3493 }
3494}
3495
3496#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3503#[serde(deny_unknown_fields)]
3504pub struct HarnessAgentStreamNamedIdV1 {
3505 pub id: String,
3506 pub name: String,
3507 pub description: Option<String>,
3508}
3509
3510#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3515#[serde(deny_unknown_fields)]
3516pub struct HarnessAgentStreamInteractionOptionV1 {
3517 pub option_id: String,
3518 pub name: String,
3519 pub kind: String,
3520}
3521
3522#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
3524#[serde(rename_all = "kebab-case")]
3525pub enum HarnessProviderConfigOptionKindV1 {
3526 Select,
3527 Boolean,
3528 Unknown,
3529}
3530
3531#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3533#[serde(deny_unknown_fields)]
3534pub struct HarnessProviderConfigChoiceV1 {
3535 pub value_json: String,
3536 pub label: Option<String>,
3537}
3538
3539#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3545#[serde(deny_unknown_fields)]
3546pub struct HarnessProviderConfigOptionV1 {
3547 pub id: String,
3548 pub name: String,
3549 pub description: Option<String>,
3550 pub category: Option<String>,
3551 pub kind: HarnessProviderConfigOptionKindV1,
3552 pub value_json: String,
3553 pub choices: Vec<HarnessProviderConfigChoiceV1>,
3554}
3555
3556#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
3564#[serde(rename_all = "kebab-case")]
3565pub enum HarnessBlockAuthorityV1 {
3566 HarnessGate,
3567 HarnessPolicy,
3568 HarnessDeadline,
3569 Operator,
3570 ProviderClassifier,
3571 ProviderPermissionRule,
3572 ProviderSandbox,
3573 ProviderRefusal,
3574 ProviderHook,
3575 UserRejected,
3576 ProviderQuota,
3580 Unknown,
3581}
3582
3583#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3595#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
3596pub enum HarnessAgentStreamChunkKindV1 {
3597 Text { text: String, is_delta: bool },
3598 Thinking { text: String },
3599 InteractionPrompt {
3603 correlation_id: String,
3604 interaction_kind: HarnessProviderInteractionKindV1,
3605 tool_name: String,
3606 title: Option<String>,
3607 prompt: String,
3608 options: Vec<HarnessAgentStreamInteractionOptionV1>,
3609 },
3610 ModeCatalog {
3611 current: Option<String>,
3612 available: Vec<HarnessAgentStreamNamedIdV1>,
3613 },
3614 ConfigOptions { options: Vec<HarnessProviderConfigOptionV1> },
3615 ModelCatalog {
3616 current: Option<String>,
3617 available: Vec<HarnessAgentStreamNamedIdV1>,
3618 },
3619 Blocked {
3626 correlation_id: Option<String>,
3627 tool_class: String,
3628 authority: HarnessBlockAuthorityV1,
3629 reason_kind: Option<String>,
3630 reason: String,
3631 help: Option<String>,
3632 },
3633}
3634
3635impl HarnessAgentStreamChunkKindV1 {
3636 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
3637 match self {
3638 Self::Text { text, .. } | Self::Thinking { text } => {
3639 if text.len() > HARNESS_AGENT_STREAM_TEXT_MAX_BYTES
3640 || contains_unsafe_control_bytes(text)
3641 {
3642 return Err(HarnessOperatorApiError::InvalidAgentStream);
3643 }
3644 Ok(())
3645 }
3646 Self::InteractionPrompt { correlation_id, tool_name, title, prompt, options, .. } => {
3647 if !valid_agent_stream_correlation_id(correlation_id) {
3648 return Err(HarnessOperatorApiError::InvalidAgentStream);
3649 }
3650 if !valid_agent_stream_id(tool_name) {
3651 return Err(HarnessOperatorApiError::InvalidAgentStream);
3652 }
3653 if let Some(title) = title {
3654 if title.len() > HARNESS_AGENT_STREAM_TEXT_MAX_BYTES
3655 || contains_unsafe_control_bytes(title)
3656 {
3657 return Err(HarnessOperatorApiError::InvalidAgentStream);
3658 }
3659 }
3660 if prompt.len() > HARNESS_AGENT_STREAM_TEXT_MAX_BYTES
3661 || contains_unsafe_control_bytes(prompt)
3662 {
3663 return Err(HarnessOperatorApiError::InvalidAgentStream);
3664 }
3665 if options.len() > HARNESS_AGENT_STREAM_INTERACTION_OPTIONS_MAX {
3666 return Err(HarnessOperatorApiError::InvalidAgentStream);
3667 }
3668 for option in options {
3669 if !valid_agent_stream_id(&option.option_id)
3670 || !valid_agent_stream_id(&option.name)
3671 || !valid_agent_stream_id(&option.kind)
3672 {
3673 return Err(HarnessOperatorApiError::InvalidAgentStream);
3674 }
3675 }
3676 Ok(())
3677 }
3678 Self::ModeCatalog { current, available } | Self::ModelCatalog { current, available } => {
3679 if let Some(current) = current {
3680 if !valid_agent_stream_id(current) {
3681 return Err(HarnessOperatorApiError::InvalidAgentStream);
3682 }
3683 }
3684 validate_agent_stream_catalog_entries(available)
3685 }
3686 Self::ConfigOptions { options } => {
3687 if options.len() > HARNESS_AGENT_STREAM_CATALOG_ENTRIES_MAX {
3688 return Err(HarnessOperatorApiError::InvalidAgentStream);
3689 }
3690 for option in options {
3691 if !valid_agent_stream_id(&option.id) || !valid_agent_stream_id(&option.name) {
3692 return Err(HarnessOperatorApiError::InvalidAgentStream);
3693 }
3694 if let Some(description) = &option.description {
3695 if description.len() > HARNESS_AGENT_STREAM_TEXT_MAX_BYTES
3696 || contains_unsafe_control_bytes(description)
3697 {
3698 return Err(HarnessOperatorApiError::InvalidAgentStream);
3699 }
3700 }
3701 if let Some(category) = &option.category {
3702 if !valid_agent_stream_id(category) {
3703 return Err(HarnessOperatorApiError::InvalidAgentStream);
3704 }
3705 }
3706 if option.value_json.len() > HARNESS_AGENT_STREAM_TEXT_MAX_BYTES
3707 || contains_unsafe_control_bytes(&option.value_json)
3708 {
3709 return Err(HarnessOperatorApiError::InvalidAgentStream);
3710 }
3711 if option.choices.len() > HARNESS_AGENT_STREAM_INTERACTION_OPTIONS_MAX {
3712 return Err(HarnessOperatorApiError::InvalidAgentStream);
3713 }
3714 for choice in &option.choices {
3715 if choice.value_json.len() > HARNESS_AGENT_STREAM_TEXT_MAX_BYTES
3716 || contains_unsafe_control_bytes(&choice.value_json)
3717 {
3718 return Err(HarnessOperatorApiError::InvalidAgentStream);
3719 }
3720 if let Some(label) = &choice.label {
3721 if label.len() > HARNESS_AGENT_STREAM_TEXT_MAX_BYTES
3722 || contains_unsafe_control_bytes(label)
3723 {
3724 return Err(HarnessOperatorApiError::InvalidAgentStream);
3725 }
3726 }
3727 }
3728 }
3729 Ok(())
3730 }
3731 Self::Blocked { correlation_id, tool_class, reason_kind, reason, help, .. } => {
3732 if let Some(correlation_id) = correlation_id {
3733 if !valid_agent_stream_correlation_id(correlation_id) {
3734 return Err(HarnessOperatorApiError::InvalidAgentStream);
3735 }
3736 }
3737 if !valid_agent_stream_id(tool_class) {
3738 return Err(HarnessOperatorApiError::InvalidAgentStream);
3739 }
3740 if let Some(reason_kind) = reason_kind {
3741 if !valid_agent_stream_id(reason_kind) {
3742 return Err(HarnessOperatorApiError::InvalidAgentStream);
3743 }
3744 }
3745 if reason.is_empty()
3746 || reason.len() > HARNESS_AGENT_STREAM_TEXT_MAX_BYTES
3747 || contains_unsafe_control_bytes(reason)
3748 {
3749 return Err(HarnessOperatorApiError::InvalidAgentStream);
3750 }
3751 if let Some(help) = help {
3752 if help.is_empty()
3753 || help.len() > HARNESS_AGENT_STREAM_TEXT_MAX_BYTES
3754 || contains_unsafe_control_bytes(help)
3755 {
3756 return Err(HarnessOperatorApiError::InvalidAgentStream);
3757 }
3758 }
3759 Ok(())
3760 }
3761 }
3762 }
3763}
3764
3765fn validate_agent_stream_catalog_entries(
3766 entries: &[HarnessAgentStreamNamedIdV1],
3767) -> Result<(), HarnessOperatorApiError> {
3768 if entries.len() > HARNESS_AGENT_STREAM_CATALOG_ENTRIES_MAX {
3769 return Err(HarnessOperatorApiError::InvalidAgentStream);
3770 }
3771 for entry in entries {
3772 if !valid_agent_stream_id(&entry.id) || !valid_agent_stream_id(&entry.name) {
3773 return Err(HarnessOperatorApiError::InvalidAgentStream);
3774 }
3775 if let Some(description) = &entry.description {
3776 if description.len() > HARNESS_AGENT_STREAM_TEXT_MAX_BYTES
3777 || contains_unsafe_control_bytes(description)
3778 {
3779 return Err(HarnessOperatorApiError::InvalidAgentStream);
3780 }
3781 }
3782 }
3783 Ok(())
3784}
3785
3786#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3794#[serde(deny_unknown_fields)]
3795pub struct HarnessAgentStreamChunkV1 {
3796 pub source_sequence: u64,
3797 pub kind: HarnessAgentStreamChunkKindV1,
3798}
3799
3800impl HarnessAgentStreamChunkV1 {
3801 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
3802 self.kind.validate()
3803 }
3804}
3805
3806#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3844#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
3845pub enum HarnessOperatorAgentEventV1 {
3846 AgentChunk {
3847 sequence: u64,
3848 session: HarnessRuntimeSessionAddressV1,
3849 chunk: HarnessAgentStreamChunkV1,
3850 published_at_ms: u64,
3858 },
3859 Lagged {
3865 sequence: u64,
3866 session: HarnessRuntimeSessionAddressV1,
3867 dropped: u64,
3868 },
3869 ReplayBoundary {
3886 session: HarnessRuntimeSessionAddressV1,
3887 replayed: u32,
3888 dropped_before_replay: u64,
3889 },
3890 Ping { sequence: u64 },
3895}
3896
3897impl HarnessOperatorAgentEventV1 {
3898 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
3899 match self {
3900 Self::AgentChunk { session, chunk, .. } => {
3901 session.validate()?;
3902 chunk.validate()
3903 }
3904 Self::Lagged { session, .. } => session.validate(),
3905 Self::ReplayBoundary { session, .. } => session.validate(),
3906 Self::Ping { .. } => Ok(()),
3907 }
3908 }
3909}
3910
3911#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3912#[serde(deny_unknown_fields)]
3913pub struct HarnessMcpGrantMintV1 {
3914 pub grant_id: SessionGrantId,
3915 pub revision: HarnessRevision,
3916 pub dispatch: HarnessDispatchIntentV1,
3919 pub replayed: bool,
3920}
3921
3922impl HarnessMcpGrantMintV1 {
3923 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
3924 self.grant_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
3925 self.revision.validate().map_err(HarnessOperatorApiError::Protocol)?;
3926 self.dispatch.validate().map_err(HarnessOperatorApiError::Protocol)?;
3927 Ok(())
3928 }
3929}
3930
3931#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
3932#[serde(tag = "kind", content = "value", rename_all = "kebab-case", deny_unknown_fields)]
3933pub enum HarnessOperatorResponseV1 {
3934 Monitor(SessionMonitorV1),
3935 Timeline(TimelinePageV1),
3936 Tasks(TaskPageV1),
3937 Task(RedactedTaskV1),
3938 TaskOperations(Vec<HarnessOperationLedgerEntryV1>),
3939 Runs(RunPageV1),
3940 Run(RedactedRunV1),
3941 RunCorrelation(HarnessRunCorrelationV1),
3942 RunTransfer(HarnessRunTransferSummaryV1),
3943 ReverseAttribution(HarnessReverseAttributionV1),
3944 RunContextSourceObserved(HarnessRunContextSourceObservationV1),
3945 RunWorkspaceInspected(HarnessRunWorkspaceInspectionV1),
3946 RunWorkspaceFileRead(HarnessRunWorkspaceFileV1),
3947 RunGitHistoryRead(HarnessRunGitHistoryPageV1),
3948 RunGitDiffRead(HarnessRunGitDiffV1),
3949 NodeWorkspaceInspected(HarnessNodeWorkspaceInspectionV1),
3950 NodeWorkspaceFileRead(HarnessNodeWorkspaceFileV1),
3951 NodeGitHistoryRead(HarnessNodeGitHistoryPageV1),
3952 NodeGitDiffRead(HarnessNodeGitDiffV1),
3953 NodeWorkspaceFileWritten(HarnessNodeWorkspaceFileV1),
3958 NodeWorkspaceFileCreated(HarnessNodeWorkspaceFileV1),
3959 NodeWorkspaceDirectoryCreated(HarnessNodeWorkspaceDirectoryV1),
3960 LaunchPlans(HarnessLaunchPlanPageV1),
3961 TaskExecutionSpec(Option<HarnessTaskExecutionSpecV1>),
3962 TaskLaunchOptions(HarnessTaskLaunchOptionsV1),
3963 RuntimeInventory(HarnessRuntimeInventoryPageV1),
3964 TerminalRead(HarnessRuntimeTerminalPageV1),
3965 SessionSpawned(HarnessRuntimeSessionAddressV1),
3966 SessionInputWritten,
3967 SessionResized,
3968 SessionStopped,
3969 SessionControlled,
3970 SessionBytesWritten,
3971 SessionPasted,
3972 SessionRemoved,
3973 SessionResumed,
3974 SessionPrompted,
3979 InteractionResolved,
3983 SessionModeSet,
3984 SessionConfigOptionSet,
3985 SessionModelSet,
3986 NativeSessionsCataloged(HarnessNativeSessionsCatalogedV1),
3987 NativeSessionsPaged(HarnessNativeSessionsPagedV1),
3988 NativeSessionPreviewed(HarnessNativeSessionPreviewedV1),
3989 SessionRecordPreviewed(HarnessSessionRecordPreviewedV1),
3990 SessionRecordResumed(HarnessSessionRecordResumedV1),
3991 SessionRecordUpdated(HarnessRuntimeManagedSessionV1),
3992 SessionRecordForgotten { record_id: String },
3993 ProviderSessionIndexed(HarnessRuntimeManagedSessionV1),
3994 NativeSessionIndexed(HarnessNativeSessionIndexedV1),
3995 HostDirectoriesBrowsed(HarnessHostDirectoryListingV1),
3996 WorkspaceRegistered(HarnessWorkspaceSnapshotV1),
3997 StandaloneWorkspaceCreated(HarnessWorkspaceSnapshotV1),
3998 WorkspaceUnregistered { workspace_id: String },
3999 WorktreeCreated {
4000 worktree: HarnessGitWorktreeSnapshotV1,
4001 workspace: HarnessWorkspaceSnapshotV1,
4002 },
4003 WorktreeRemoved {
4004 target_root: HarnessHostPathV1,
4005 workspace_id: Option<String>,
4006 },
4007 ContextPackExported(HarnessResolvedContextPackReceiptV1),
4008 ContextPackForgotten { context_id: String },
4009 Mutation(HarnessOperatorMutationOutcomeV1),
4010 ExecutionSpecMutation(HarnessOperatorMutationOutcomeV1),
4011 Schedule(HarnessScheduleOutcomeV1),
4012 TaskStarted(HarnessTaskStartOutcomeV1),
4013 HarnessMcpGrantMinted(HarnessMcpGrantMintV1),
4017}
4018
4019impl HarnessOperatorResponseV1 {
4020 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4021 match self {
4022 Self::Monitor(value) => value.validate().map_err(HarnessOperatorApiError::Read),
4023 Self::Timeline(value) => value.validate().map_err(HarnessOperatorApiError::Read),
4024 Self::Tasks(value) => value.validate().map_err(HarnessOperatorApiError::Read),
4025 Self::Task(value) => value.validate().map_err(HarnessOperatorApiError::Read),
4026 Self::TaskOperations(entries) => {
4027 if entries.len() > usize::from(HARNESS_ENTITY_PAGE_LIMIT_MAX) {
4028 return Err(HarnessOperatorApiError::InvalidOperationLedger);
4029 }
4030 for entry in entries {
4031 entry.validate()?;
4032 }
4033 Ok(())
4034 }
4035 Self::Runs(value) => value.validate().map_err(HarnessOperatorApiError::Read),
4036 Self::Run(value) => value.validate().map_err(HarnessOperatorApiError::Read),
4037 Self::RunCorrelation(value) => value.validate(),
4038 Self::RunTransfer(value) => value.validate(),
4039 Self::ReverseAttribution(value) => value.validate(),
4040 Self::RunContextSourceObserved(value) => value.validate(),
4041 Self::RunWorkspaceInspected(value) => value.validate(),
4042 Self::RunWorkspaceFileRead(value) => value.validate(),
4043 Self::RunGitHistoryRead(value) => value.validate(),
4044 Self::RunGitDiffRead(value) => value.validate(),
4045 Self::NodeWorkspaceInspected(value) => value.validate(),
4046 Self::NodeWorkspaceFileRead(value) => value.validate(),
4047 Self::NodeGitHistoryRead(value) => value.validate(),
4048 Self::NodeGitDiffRead(value) => value.validate(),
4049 Self::NodeWorkspaceFileWritten(value) | Self::NodeWorkspaceFileCreated(value) => {
4050 value.validate()
4051 }
4052 Self::NodeWorkspaceDirectoryCreated(value) => value.validate(),
4053 Self::LaunchPlans(value) => value.validate(),
4054 Self::TaskExecutionSpec(value) => {
4055 if let Some(value) = value {
4056 value.validate().map_err(HarnessOperatorApiError::Protocol)?;
4057 }
4058 Ok(())
4059 }
4060 Self::TaskLaunchOptions(value) => value.validate(),
4061 Self::RuntimeInventory(value) => value.validate(),
4062 Self::TerminalRead(value) => value.validate(),
4063 Self::SessionSpawned(value) => value.validate(),
4064 Self::SessionInputWritten
4065 | Self::SessionResized
4066 | Self::SessionStopped
4067 | Self::SessionControlled
4068 | Self::SessionBytesWritten
4069 | Self::SessionPasted
4070 | Self::SessionRemoved
4071 | Self::SessionResumed
4072 | Self::SessionPrompted
4073 | Self::InteractionResolved
4074 | Self::SessionModeSet
4075 | Self::SessionConfigOptionSet
4076 | Self::SessionModelSet => Ok(()),
4077 Self::NativeSessionsCataloged(value) => value.validate(),
4078 Self::NativeSessionsPaged(value) => value.validate(),
4079 Self::NativeSessionPreviewed(value) => value.validate(),
4080 Self::SessionRecordPreviewed(value) => value.validate(),
4081 Self::SessionRecordResumed(value) => value.validate(),
4082 Self::SessionRecordUpdated(value) | Self::ProviderSessionIndexed(value) => {
4083 value.validate()
4084 }
4085 Self::SessionRecordForgotten { record_id } => {
4086 if !valid_runtime_id(record_id, 128) {
4087 return Err(HarnessOperatorApiError::InvalidRuntimeInventory);
4088 }
4089 Ok(())
4090 }
4091 Self::NativeSessionIndexed(value) => value.validate(),
4092 Self::HostDirectoriesBrowsed(value) => value.validate(),
4093 Self::WorkspaceRegistered(value) | Self::StandaloneWorkspaceCreated(value) => {
4094 value.validate()
4095 }
4096 Self::WorkspaceUnregistered { workspace_id } => {
4097 if !valid_runtime_id(workspace_id, 128) {
4098 return Err(HarnessOperatorApiError::InvalidResourceMutationRequest);
4099 }
4100 Ok(())
4101 }
4102 Self::WorktreeCreated { worktree, workspace } => {
4103 worktree.validate()?;
4104 workspace.validate()
4105 }
4106 Self::WorktreeRemoved { target_root, workspace_id } => {
4107 target_root.validate()?;
4108 if let Some(workspace_id) = workspace_id {
4109 if !valid_runtime_id(workspace_id, 128) {
4110 return Err(HarnessOperatorApiError::InvalidResourceMutationRequest);
4111 }
4112 }
4113 Ok(())
4114 }
4115 Self::ContextPackExported(value) => {
4116 value.validate().map_err(HarnessOperatorApiError::Protocol)
4117 }
4118 Self::ContextPackForgotten { context_id } => {
4119 if !valid_runtime_id(context_id, 128) {
4120 return Err(HarnessOperatorApiError::InvalidResourceMutationRequest);
4121 }
4122 Ok(())
4123 }
4124 Self::Mutation(_) | Self::ExecutionSpecMutation(_) => Ok(()),
4125 Self::Schedule(HarnessScheduleOutcomeV1::Idle) => Ok(()),
4126 Self::Schedule(HarnessScheduleOutcomeV1::Dispatch(value)) => {
4127 value.validate().map_err(HarnessOperatorApiError::Protocol)
4128 }
4129 Self::TaskStarted(value) => {
4130 value.validate().map_err(HarnessOperatorApiError::Protocol)
4131 }
4132 Self::HarnessMcpGrantMinted(value) => value.validate(),
4133 }
4134 }
4135}
4136
4137#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
4138#[serde(transparent)]
4139pub struct HarnessNodeIncarnationV1(HarnessSelectorV1);
4140
4141impl HarnessNodeIncarnationV1 {
4142 pub fn new(value: impl Into<String>) -> Result<Self, HarnessOperatorApiError> {
4143 let value = value.into();
4144 if value.len() != 32 || !value.bytes().all(is_lower_hex) {
4145 return Err(HarnessOperatorApiError::InvalidRunCorrelation);
4146 }
4147 HarnessSelectorV1::new(value)
4148 .map(Self)
4149 .map_err(HarnessOperatorApiError::Protocol)
4150 }
4151
4152 pub fn as_str(&self) -> &str { self.0.as_str() }
4153
4154 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4155 self.0.validate().map_err(HarnessOperatorApiError::Protocol)?;
4156 if self.0.as_str().len() != 32 || !self.0.as_str().bytes().all(is_lower_hex) {
4157 return Err(HarnessOperatorApiError::InvalidRunCorrelation);
4158 }
4159 Ok(())
4160 }
4161}
4162
4163impl<'de> Deserialize<'de> for HarnessNodeIncarnationV1 {
4164 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4165 where
4166 D: Deserializer<'de>,
4167 {
4168 Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
4169 }
4170}
4171
4172#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4173#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
4174pub enum HarnessRunWorktreeViewV1 {
4175 Existing,
4176 Managed { worktree_ref: HarnessSelectorV1 },
4177}
4178
4179impl HarnessRunWorktreeViewV1 {
4180 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4181 match self {
4182 Self::Existing => Ok(()),
4183 Self::Managed { worktree_ref } => {
4184 worktree_ref.validate().map_err(HarnessOperatorApiError::Protocol)
4185 }
4186 }
4187 }
4188}
4189
4190#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4191#[serde(deny_unknown_fields)]
4192pub struct HarnessManagedRunSessionV1 {
4193 pub record_id: HarnessSelectorV1,
4194 pub active_session: Option<HarnessRuntimeIdentityV1>,
4195}
4196
4197impl HarnessManagedRunSessionV1 {
4198 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4199 self.record_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
4200 if let Some(active_session) = &self.active_session {
4201 active_session.validate().map_err(HarnessOperatorApiError::Protocol)?;
4202 }
4203 Ok(())
4204 }
4205}
4206
4207#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4208#[serde(deny_unknown_fields)]
4209pub struct HarnessInlineRunSessionV1 {
4210 pub inline_ref: HarnessInlineRef,
4211}
4212
4213impl HarnessInlineRunSessionV1 {
4214 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4215 self.inline_ref.validate().map_err(HarnessOperatorApiError::Protocol)
4216 }
4217}
4218
4219#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4220#[serde(tag = "kind", content = "value", rename_all = "kebab-case", deny_unknown_fields)]
4221pub enum HarnessRunSessionViewV1 {
4222 Managed(HarnessManagedRunSessionV1),
4223 Inline(HarnessInlineRunSessionV1),
4224}
4225
4226impl HarnessRunSessionViewV1 {
4227 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4228 match self {
4229 Self::Managed(value) => value.validate(),
4230 Self::Inline(value) => value.validate(),
4231 }
4232 }
4233}
4234
4235#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
4236#[serde(rename_all = "kebab-case")]
4237pub enum HarnessRunCorrelationAvailabilityV1 {
4238 Available,
4239 Dormant,
4240 Unavailable,
4241 NotObserved,
4242 StaleIncarnation,
4243}
4244
4245#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4246#[serde(deny_unknown_fields)]
4247pub struct HarnessRunCorrelationV1 {
4248 pub run_id: HarnessRunId,
4249 pub run_revision: HarnessRevision,
4250 pub task_id: HarnessTaskId,
4251 pub node_id: HarnessSelectorV1,
4252 pub node_incarnation_id: HarnessNodeIncarnationV1,
4253 pub workspace_id: HarnessSelectorV1,
4254 pub provider_profile: HarnessSelectorV1,
4255 pub mode: HarnessExecutionModeV1,
4256 pub worktree: HarnessRunWorktreeViewV1,
4257 pub session: HarnessRunSessionViewV1,
4258 pub availability: HarnessRunCorrelationAvailabilityV1,
4259 pub observed_at_unix_ms: Option<u64>,
4260}
4261
4262impl HarnessRunCorrelationV1 {
4263 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4264 self.run_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
4265 self.run_revision.validate().map_err(HarnessOperatorApiError::Protocol)?;
4266 self.task_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
4267 self.node_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
4268 self.node_incarnation_id.validate()?;
4269 self.workspace_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
4270 self.provider_profile.validate().map_err(HarnessOperatorApiError::Protocol)?;
4271 self.worktree.validate()?;
4272 self.session.validate()?;
4273 if matches!(&self.session, HarnessRunSessionViewV1::Inline(_))
4274 && self.mode != HarnessExecutionModeV1::Inline
4275 {
4276 return Err(HarnessOperatorApiError::InvalidRunCorrelation);
4277 }
4278 if matches!(self.availability, HarnessRunCorrelationAvailabilityV1::NotObserved)
4279 != self.observed_at_unix_ms.is_none()
4280 || self.observed_at_unix_ms == Some(0)
4281 {
4282 return Err(HarnessOperatorApiError::InvalidRunCorrelation);
4283 }
4284 match (&self.session, self.availability) {
4285 (
4286 HarnessRunSessionViewV1::Managed(HarnessManagedRunSessionV1 {
4287 active_session: Some(_),
4288 ..
4289 }),
4290 HarnessRunCorrelationAvailabilityV1::Dormant,
4291 )
4292 | (
4293 HarnessRunSessionViewV1::Managed(HarnessManagedRunSessionV1 {
4294 active_session: None,
4295 ..
4296 }),
4297 HarnessRunCorrelationAvailabilityV1::Available,
4298 )
4299 | (
4300 HarnessRunSessionViewV1::Inline(_),
4301 HarnessRunCorrelationAvailabilityV1::Available
4302 | HarnessRunCorrelationAvailabilityV1::Dormant,
4303 ) => Err(HarnessOperatorApiError::InvalidRunCorrelation),
4304 _ => Ok(()),
4305 }
4306 }
4307}
4308
4309#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4310#[serde(deny_unknown_fields)]
4311pub struct HarnessRunDeliveryTransferV1 {
4312 pub delivery_ref: HarnessDeliveryRef,
4313 pub revision: HarnessRevision,
4314 pub state: HarnessDeliveryStateV1,
4315 pub selector: HarnessSelectorV1,
4316 pub bundle_id: HarnessDeliveryBundleIdV1,
4317 pub bundle_revision: HarnessDeliveryBundleRevisionV1,
4318 pub bundle_digest: HarnessDeliveryBundleDigestV1,
4319 pub manifest_digest: HarnessDeliveryManifestDigestV2,
4320 pub receipt_ref: Option<HarnessReceiptRef>,
4321 pub created_at_unix_ms: u64,
4322 pub updated_at_unix_ms: u64,
4323 pub staged_at_unix_ms: Option<u64>,
4324 pub committed_at_unix_ms: Option<u64>,
4325}
4326
4327impl HarnessRunDeliveryTransferV1 {
4328 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4329 self.delivery_ref.validate().map_err(HarnessOperatorApiError::Protocol)?;
4330 self.revision.validate().map_err(HarnessOperatorApiError::Protocol)?;
4331 self.selector.validate().map_err(HarnessOperatorApiError::Protocol)?;
4332 self.bundle_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
4333 self.bundle_revision.validate().map_err(HarnessOperatorApiError::Protocol)?;
4334 self.bundle_digest.validate().map_err(HarnessOperatorApiError::Protocol)?;
4335 self.manifest_digest.validate().map_err(HarnessOperatorApiError::Protocol)?;
4336 if let Some(receipt_ref) = &self.receipt_ref {
4337 receipt_ref.validate().map_err(HarnessOperatorApiError::Protocol)?;
4338 }
4339 if !valid_transfer_timestamps(
4340 self.created_at_unix_ms,
4341 self.updated_at_unix_ms,
4342 [self.staged_at_unix_ms, self.committed_at_unix_ms],
4343 ) {
4344 return Err(HarnessOperatorApiError::InvalidRunTransfer);
4345 }
4346 let state_fields = match self.state {
4347 HarnessDeliveryStateV1::Prepared => {
4348 self.staged_at_unix_ms.is_none()
4349 && self.committed_at_unix_ms.is_none()
4350 && self.receipt_ref.is_none()
4351 }
4352 HarnessDeliveryStateV1::Staged => {
4353 self.staged_at_unix_ms.is_some()
4354 && self.committed_at_unix_ms.is_none()
4355 && self.receipt_ref.is_none()
4356 }
4357 HarnessDeliveryStateV1::Committed => {
4358 self.staged_at_unix_ms.is_some()
4359 && self.committed_at_unix_ms.is_some()
4360 && self.receipt_ref.is_some()
4361 }
4362 };
4363 if !state_fields
4364 || matches!(
4365 (self.staged_at_unix_ms, self.committed_at_unix_ms),
4366 (Some(staged), Some(committed)) if committed < staged
4367 )
4368 {
4369 return Err(HarnessOperatorApiError::InvalidRunTransfer);
4370 }
4371 Ok(())
4372 }
4373}
4374
4375#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4376#[serde(deny_unknown_fields)]
4377pub struct HarnessRunContextTransferV1 {
4378 pub context_ref: HarnessSelectorV1,
4379 pub digest: String,
4380 pub source_message_count: u64,
4381 pub retained_message_count: u64,
4382 pub byte_len: u32,
4383 pub truncated: bool,
4384}
4385
4386impl HarnessRunContextTransferV1 {
4387 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4388 self.context_ref.validate().map_err(HarnessOperatorApiError::Protocol)?;
4389 let valid_digest = self.digest.strip_prefix("sha256:").is_some_and(|digest| {
4390 digest.len() == 64 && digest.bytes().all(is_lower_hex)
4391 });
4392 if !valid_digest
4393 || self.source_message_count == 0
4394 || self.retained_message_count == 0
4395 || self.retained_message_count > self.source_message_count
4396 || self.retained_message_count > HARNESS_CONTEXT_PACK_RETAINED_MESSAGES_MAX
4397 || self.byte_len == 0
4398 || self.byte_len > HARNESS_CONTEXT_PACK_MAX_BYTES
4399 || self.truncated != (self.source_message_count > self.retained_message_count)
4400 {
4401 return Err(HarnessOperatorApiError::InvalidRunTransfer);
4402 }
4403 Ok(())
4404 }
4405}
4406
4407#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4408#[serde(deny_unknown_fields)]
4409pub struct HarnessRunContinuationTransferV1 {
4410 pub continuation_ref: HarnessContinuationRef,
4411 pub receipt_ref: HarnessReceiptRef,
4412 pub revision: HarnessRevision,
4413 pub state: HarnessContinuationStateV1,
4414 pub source_run_id: HarnessRunId,
4415 pub target_run_id: HarnessRunId,
4416 pub source_provider: HarnessSelectorV1,
4417 pub context: Option<HarnessRunContextTransferV1>,
4418 pub prepared_at_unix_ms: u64,
4419 pub exporting_at_unix_ms: Option<u64>,
4420 pub exported_at_unix_ms: Option<u64>,
4421 pub bound_at_unix_ms: Option<u64>,
4422 pub expired_at_unix_ms: Option<u64>,
4423 pub outcome_unknown_at_unix_ms: Option<u64>,
4424 pub outcome_unknown_reason: Option<HarnessContinuationOutcomeUnknownReasonV1>,
4425 pub created_at_unix_ms: u64,
4426 pub updated_at_unix_ms: u64,
4427}
4428
4429impl HarnessRunContinuationTransferV1 {
4430 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4431 self.continuation_ref.validate().map_err(HarnessOperatorApiError::Protocol)?;
4432 self.receipt_ref.validate().map_err(HarnessOperatorApiError::Protocol)?;
4433 self.revision.validate().map_err(HarnessOperatorApiError::Protocol)?;
4434 self.source_run_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
4435 self.target_run_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
4436 self.source_provider.validate().map_err(HarnessOperatorApiError::Protocol)?;
4437 if let Some(context) = &self.context { context.validate()?; }
4438 if self.source_run_id == self.target_run_id
4439 || self.prepared_at_unix_ms != self.created_at_unix_ms
4440 || !valid_transfer_timestamps(
4441 self.created_at_unix_ms,
4442 self.updated_at_unix_ms,
4443 [
4444 Some(self.prepared_at_unix_ms),
4445 self.exporting_at_unix_ms,
4446 self.exported_at_unix_ms,
4447 self.bound_at_unix_ms,
4448 self.expired_at_unix_ms,
4449 self.outcome_unknown_at_unix_ms,
4450 ],
4451 )
4452 {
4453 return Err(HarnessOperatorApiError::InvalidRunTransfer);
4454 }
4455 let ordered = self.exporting_at_unix_ms
4456 .is_none_or(|value| value >= self.prepared_at_unix_ms)
4457 && self.exported_at_unix_ms.is_none_or(|value| {
4458 self.exporting_at_unix_ms.is_some_and(|started| value >= started)
4459 })
4460 && self.bound_at_unix_ms.is_none_or(|value| {
4461 self.exported_at_unix_ms.is_some_and(|exported| value >= exported)
4462 })
4463 && self.outcome_unknown_at_unix_ms.is_none_or(|value| {
4464 self.exporting_at_unix_ms.is_some_and(|started| value >= started)
4465 })
4466 && self.expired_at_unix_ms
4467 .is_none_or(|value| value >= self.prepared_at_unix_ms);
4468 let state_fields = match self.state {
4469 HarnessContinuationStateV1::Prepared => self.context.is_none()
4470 && self.exporting_at_unix_ms.is_none()
4471 && self.exported_at_unix_ms.is_none()
4472 && self.bound_at_unix_ms.is_none()
4473 && self.expired_at_unix_ms.is_none()
4474 && self.outcome_unknown_at_unix_ms.is_none()
4475 && self.outcome_unknown_reason.is_none(),
4476 HarnessContinuationStateV1::Exporting => self.context.is_none()
4477 && self.exporting_at_unix_ms.is_some()
4478 && self.exported_at_unix_ms.is_none()
4479 && self.bound_at_unix_ms.is_none()
4480 && self.expired_at_unix_ms.is_none()
4481 && self.outcome_unknown_at_unix_ms.is_none()
4482 && self.outcome_unknown_reason.is_none(),
4483 HarnessContinuationStateV1::Exported => self.context.is_some()
4484 && self.exporting_at_unix_ms.is_some()
4485 && self.exported_at_unix_ms.is_some()
4486 && self.bound_at_unix_ms.is_none()
4487 && self.expired_at_unix_ms.is_none()
4488 && self.outcome_unknown_at_unix_ms.is_none()
4489 && self.outcome_unknown_reason.is_none(),
4490 HarnessContinuationStateV1::Bound => self.context.is_some()
4491 && self.exporting_at_unix_ms.is_some()
4492 && self.exported_at_unix_ms.is_some()
4493 && self.bound_at_unix_ms.is_some()
4494 && self.expired_at_unix_ms.is_none()
4495 && self.outcome_unknown_at_unix_ms.is_none()
4496 && self.outcome_unknown_reason.is_none(),
4497 HarnessContinuationStateV1::OutcomeUnknown => self.exporting_at_unix_ms.is_some()
4498 && self.exported_at_unix_ms.is_none()
4499 && self.bound_at_unix_ms.is_none()
4500 && self.expired_at_unix_ms.is_none()
4501 && self.outcome_unknown_at_unix_ms.is_some()
4502 && self.outcome_unknown_reason.is_some(),
4503 HarnessContinuationStateV1::Expired => self.bound_at_unix_ms.is_none()
4504 && self.expired_at_unix_ms.is_some()
4505 && self.outcome_unknown_at_unix_ms.is_none()
4506 && self.outcome_unknown_reason.is_none(),
4507 };
4508 if !ordered || !state_fields {
4509 return Err(HarnessOperatorApiError::InvalidRunTransfer);
4510 }
4511 Ok(())
4512 }
4513}
4514
4515#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4516#[serde(deny_unknown_fields)]
4517pub struct HarnessRunTransferSummaryV1 {
4518 pub run_id: HarnessRunId,
4519 pub run_revision: HarnessRevision,
4520 pub delivery: Option<HarnessRunDeliveryTransferV1>,
4521 pub continuation: Option<HarnessRunContinuationTransferV1>,
4522}
4523
4524impl HarnessRunTransferSummaryV1 {
4525 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4526 self.run_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
4527 self.run_revision.validate().map_err(HarnessOperatorApiError::Protocol)?;
4528 if let Some(delivery) = &self.delivery { delivery.validate()?; }
4529 if let Some(continuation) = &self.continuation {
4530 continuation.validate()?;
4531 if continuation.target_run_id != self.run_id {
4532 return Err(HarnessOperatorApiError::InvalidRunTransfer);
4533 }
4534 }
4535 Ok(())
4536 }
4537
4538 pub fn validate_for(&self, run_id: &HarnessRunId) -> Result<(), HarnessOperatorApiError> {
4539 self.validate()?;
4540 if &self.run_id != run_id {
4541 return Err(HarnessOperatorApiError::InvalidRunTransfer);
4542 }
4543 Ok(())
4544 }
4545}
4546
4547#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4548#[serde(deny_unknown_fields)]
4549pub struct HarnessRunContextSourceObservationV1 {
4550 pub run_id: HarnessRunId,
4551 pub run_revision: HarnessRevision,
4552 pub feature_state: FeatureObservationStateV1,
4553 pub message_count: u64,
4554 pub message_count_exact: bool,
4555 pub completed_turn_count: Option<u64>,
4556 pub total_tokens: Option<u64>,
4557 pub observed_at_unix_ms: Option<u64>,
4558}
4559
4560impl HarnessRunContextSourceObservationV1 {
4561 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4562 self.run_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
4563 self.run_revision.validate().map_err(HarnessOperatorApiError::Protocol)?;
4564 let observed = self.feature_state == FeatureObservationStateV1::Observed;
4565 let observed_fields_valid = self.message_count > 0
4566 && self.message_count_exact
4567 && self.observed_at_unix_ms.is_some_and(|timestamp| timestamp > 0)
4568 && self.completed_turn_count.map_or(true, |count| count <= self.message_count);
4569 let unobserved_fields_empty = self.message_count == 0
4570 && !self.message_count_exact
4571 && self.completed_turn_count.is_none()
4572 && self.total_tokens.is_none()
4573 && self.observed_at_unix_ms.is_none();
4574 if observed && !observed_fields_valid || !observed && !unobserved_fields_empty {
4575 return Err(HarnessOperatorApiError::InvalidRunContextSourceObservation);
4576 }
4577 Ok(())
4578 }
4579
4580 pub fn validate_for(&self, run_id: &HarnessRunId) -> Result<(), HarnessOperatorApiError> {
4581 self.validate()?;
4582 if &self.run_id != run_id {
4583 return Err(HarnessOperatorApiError::InvalidRunContextSourceObservation);
4584 }
4585 Ok(())
4586 }
4587}
4588
4589#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4590#[serde(deny_unknown_fields)]
4591pub struct HarnessRuntimeNodeInventoryV1 {
4592 pub node_id: String,
4593 pub incarnation_id: String,
4594 pub observed_at_unix_ms: u64,
4595 pub event_sequence: u64,
4596 pub inventory: HarnessRuntimeInventoryV1,
4597}
4598
4599impl HarnessRuntimeNodeInventoryV1 {
4600 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4601 if !valid_runtime_id(&self.node_id, 128)
4602 || self.incarnation_id.len() != 32
4603 || !self.incarnation_id.bytes().all(is_lower_hex)
4604 || self.observed_at_unix_ms == 0
4605 {
4606 return Err(HarnessOperatorApiError::InvalidRuntimeInventory);
4607 }
4608 self.inventory.validate()
4609 }
4610}
4611
4612#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4613#[serde(deny_unknown_fields)]
4614pub struct HarnessRuntimeInventoryV1 {
4615 pub enabled_providers: Vec<String>,
4616 pub workspaces: BTreeMap<String, HarnessRuntimeWorkspaceV1>,
4617 pub workspace_count: usize,
4618 pub workspaces_truncated: bool,
4619 pub session_count: usize,
4620 pub sessions_truncated: bool,
4621 pub managed_sessions: Vec<HarnessRuntimeManagedSessionV1>,
4622 pub managed_session_count: usize,
4623 pub managed_sessions_truncated: bool,
4624 #[serde(default)]
4629 pub retired_count: usize,
4630 #[serde(default, skip_serializing_if = "Option::is_none")]
4638 pub launch_inventory: Option<HarnessRuntimeLaunchInventoryV1>,
4639}
4640
4641fn managed_session_liveness_rank(state: &HarnessRuntimeManagedStateV1) -> u8 {
4651 match state {
4652 HarnessRuntimeManagedStateV1::Live => 0,
4653 HarnessRuntimeManagedStateV1::IdentityPending => 1,
4654 HarnessRuntimeManagedStateV1::Dormant => 2,
4655 HarnessRuntimeManagedStateV1::Unavailable => 3,
4656 }
4657}
4658
4659impl HarnessRuntimeInventoryV1 {
4660 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4661 let included_sessions = self.workspaces.values()
4662 .map(|workspace| workspace.sessions.len())
4663 .sum::<usize>();
4664 if self.enabled_providers.len() > 64
4665 || self.enabled_providers.windows(2).any(|pair| pair[0] >= pair[1])
4666 || self.enabled_providers.iter().any(|provider| !valid_runtime_id(provider, 128))
4667 || self.workspaces.len() > 32
4668 || included_sessions > 128
4669 || self.managed_sessions.len() > 128
4670 || self.workspace_count < self.workspaces.len()
4671 || self.session_count < included_sessions
4672 || self.managed_session_count < self.managed_sessions.len()
4673 || self.workspaces_truncated != (self.workspace_count > self.workspaces.len())
4674 || self.sessions_truncated != (self.session_count > included_sessions)
4675 || self.managed_sessions_truncated
4676 != (self.managed_session_count > self.managed_sessions.len())
4677 || self.managed_sessions.windows(2).any(|pair| {
4678 let left = (managed_session_liveness_rank(&pair[0].state), pair[0].record_id.as_str());
4679 let right = (managed_session_liveness_rank(&pair[1].state), pair[1].record_id.as_str());
4680 left >= right
4681 })
4682 {
4683 return Err(HarnessOperatorApiError::InvalidRuntimeInventory);
4684 }
4685 for (workspace_id, workspace) in &self.workspaces {
4686 if workspace_id != &workspace.workspace_id { return Err(HarnessOperatorApiError::InvalidRuntimeInventory); }
4687 workspace.validate()?;
4688 }
4689 for record in &self.managed_sessions { record.validate()?; }
4690 if let Some(launch_inventory) = &self.launch_inventory { launch_inventory.validate()?; }
4691 Ok(())
4692 }
4693}
4694
4695#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4697#[serde(deny_unknown_fields)]
4698pub struct HarnessRuntimeEnvironmentProfileReceiptV1 {
4699 pub profile_id: String,
4700 pub profile_revision: String,
4701}
4702
4703impl HarnessRuntimeEnvironmentProfileReceiptV1 {
4704 fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4705 if !valid_runtime_id(&self.profile_id, 128) || !valid_runtime_id(&self.profile_revision, 128) {
4706 return Err(HarnessOperatorApiError::InvalidRuntimeInventory);
4707 }
4708 Ok(())
4709 }
4710}
4711
4712#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4714#[serde(deny_unknown_fields)]
4715pub struct HarnessRuntimeSpawnProfileSummaryV1 {
4716 pub id: String,
4717 pub revision: String,
4718 #[serde(default, skip_serializing_if = "Option::is_none")]
4719 pub environment_profile: Option<HarnessRuntimeEnvironmentProfileReceiptV1>,
4720}
4721
4722impl HarnessRuntimeSpawnProfileSummaryV1 {
4723 fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4724 if !valid_runtime_id(&self.id, 128) || !valid_runtime_id(&self.revision, 128) {
4725 return Err(HarnessOperatorApiError::InvalidRuntimeInventory);
4726 }
4727 if let Some(environment_profile) = &self.environment_profile {
4728 environment_profile.validate()?;
4729 }
4730 Ok(())
4731 }
4732}
4733
4734#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4736#[serde(deny_unknown_fields)]
4737pub struct HarnessRuntimeBundleReceiptV1 {
4738 pub id: String,
4739 pub revision: String,
4740 pub digest: String,
4741}
4742
4743impl HarnessRuntimeBundleReceiptV1 {
4744 fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4745 if !valid_runtime_id(&self.id, 128)
4746 || !valid_runtime_id(&self.revision, 128)
4747 || !valid_sha256_digest(&self.digest)
4748 {
4749 return Err(HarnessOperatorApiError::InvalidRuntimeInventory);
4750 }
4751 Ok(())
4752 }
4753}
4754
4755#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4757#[serde(deny_unknown_fields)]
4758pub struct HarnessRuntimeLaunchInventoryV1 {
4759 #[serde(default, skip_serializing_if = "Option::is_none")]
4760 pub spawn_profiles: Option<Vec<HarnessRuntimeSpawnProfileSummaryV1>>,
4761 #[serde(default, skip_serializing_if = "Option::is_none")]
4762 pub bundles: Option<Vec<HarnessRuntimeBundleReceiptV1>>,
4763}
4764
4765impl HarnessRuntimeLaunchInventoryV1 {
4766 fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4767 if self.spawn_profiles.is_none() && self.bundles.is_none() {
4768 return Err(HarnessOperatorApiError::InvalidRuntimeInventory);
4769 }
4770 if let Some(profiles) = &self.spawn_profiles {
4771 if profiles.len() > HARNESS_RUNTIME_SPAWN_PROFILES_MAX {
4772 return Err(HarnessOperatorApiError::InvalidRuntimeInventory);
4773 }
4774 for profile in profiles { profile.validate()?; }
4775 if profiles.iter().enumerate().any(|(index, profile)| {
4776 profiles[..index].iter().any(|existing| existing.id == profile.id)
4777 }) {
4778 return Err(HarnessOperatorApiError::InvalidRuntimeInventory);
4779 }
4780 }
4781 if let Some(bundles) = &self.bundles {
4782 if bundles.len() > HARNESS_RUNTIME_LAUNCH_BUNDLES_MAX {
4783 return Err(HarnessOperatorApiError::InvalidRuntimeInventory);
4784 }
4785 for bundle in bundles { bundle.validate()?; }
4786 }
4787 Ok(())
4788 }
4789}
4790
4791#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4792#[serde(deny_unknown_fields)]
4793pub struct HarnessRuntimeWorkspaceV1 {
4794 pub workspace_id: String,
4795 pub display_root: String,
4796 pub display_root_truncated: bool,
4797 pub sessions: Vec<HarnessRuntimeSessionV1>,
4798 pub session_count: usize,
4799 pub sessions_truncated: bool,
4800}
4801
4802impl HarnessRuntimeWorkspaceV1 {
4803 fn validate(&self) -> Result<(), HarnessOperatorApiError> {
4804 if !valid_runtime_id(&self.workspace_id, 128)
4805 || self.display_root.len() > 1024
4806 || self.display_root.chars().any(char::is_control)
4807 || self.sessions.len() > 128
4808 || self.session_count < self.sessions.len()
4809 || self.sessions_truncated != (self.session_count > self.sessions.len())
4810 || self.sessions.windows(2).any(|pair| {
4811 (pair[0].instance_id, pair[0].generation)
4812 >= (pair[1].instance_id, pair[1].generation)
4813 })
4814 {
4815 return Err(HarnessOperatorApiError::InvalidRuntimeInventory);
4816 }
4817 for session in &self.sessions { session.validate()?; }
4818 Ok(())
4819 }
4820}
4821
4822#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
4823#[serde(rename_all = "kebab-case")]
4824pub enum HarnessRuntimeTransportV1 { Pty, Pipe, Acp }
4825
4826#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
4827#[serde(rename_all = "kebab-case")]
4828pub enum HarnessRuntimeSessionStatusV1 { Registered, Starting, Running, Stopping, Exited, Failed }
4829
4830#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
4842#[serde(rename_all = "kebab-case")]
4843pub enum HarnessTerminalControlV1 {
4844 Interrupt,
4845 EndOfFile,
4846 ControlA,
4847 ControlB,
4848 ControlE,
4849 ControlF,
4850 ControlG,
4851 ControlH,
4852 ControlI,
4853 ControlJ,
4854 ControlK,
4855 ControlL,
4856 ControlM,
4857 ControlN,
4858 ControlO,
4859 ControlP,
4860 ControlQ,
4861 ControlR,
4862 ControlS,
4863 ControlT,
4864 ControlU,
4865 ControlV,
4866 ControlW,
4867 ControlX,
4868 ControlY,
4869 ControlZ,
4870 Enter,
4871 LineFeed,
4872 Escape,
4873 Backspace,
4874 Tab,
4875 BackTab,
4876 Insert,
4877 Delete,
4878 Home,
4879 End,
4880 PageUp,
4881 PageDown,
4882 ArrowUp,
4883 ArrowDown,
4884 ArrowRight,
4885 ArrowLeft,
4886 Function1,
4887 Function2,
4888 Function3,
4889 Function4,
4890 Function5,
4891 Function6,
4892 Function7,
4893 Function8,
4894 Function9,
4895 Function10,
4896 Function11,
4897 Function12,
4898}
4899
4900#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
4901#[serde(deny_unknown_fields)]
4902pub struct HarnessRuntimeTerminalSizeV1 {
4903 pub rows: u16,
4904 pub columns: u16,
4905}
4906
4907pub const HARNESS_SCREEN_STATE_PROCESS_MAX_BYTES: usize = 512;
4912pub const HARNESS_SCREEN_STATE_GATE_MAX_BYTES: usize = 128;
4915pub const HARNESS_GATE_OPTIONS_MAX: usize = 8;
4917pub const HARNESS_GATE_OPTION_TEXT_MAX_BYTES: usize = 128;
4919pub const HARNESS_GATE_PATH_MAX_BYTES: usize = 32_768;
4921
4922#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
4924#[serde(rename_all = "kebab-case")]
4925pub enum OperatorGateKindV1 {
4926 WorkspaceTrust,
4927 HookTrust,
4928 Authentication,
4929 VendorUpdate,
4930 Onboarding,
4931 TerminalAppearance,
4932 ConfigurationMigration,
4933}
4934
4935impl OperatorGateKindV1 {
4936 pub fn label(&self) -> &'static str {
4938 match self {
4939 Self::WorkspaceTrust => "workspace trust",
4940 Self::HookTrust => "hook trust review",
4941 Self::Authentication => "authentication",
4942 Self::VendorUpdate => "vendor update",
4943 Self::Onboarding => "onboarding",
4944 Self::TerminalAppearance => "terminal appearance setup",
4945 Self::ConfigurationMigration => "configuration migration",
4946 }
4947 }
4948}
4949
4950#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4952#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
4953pub enum OperatorGateSubjectV1 {
4954 Directory { path: Option<String> },
4955 Hooks { count: Option<u32> },
4956 McpServers,
4957 Account,
4958 ApiKey,
4959 Appearance,
4960 Unknown,
4961}
4962
4963#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
4965#[serde(rename_all = "kebab-case")]
4966pub enum OperatorGateInputV1 {
4967 NumberedList,
4968 ArrowList,
4969 PressEnter,
4970 TextEntry,
4971 Unknown,
4972}
4973
4974#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
4976#[serde(rename_all = "kebab-case")]
4977pub enum OperatorGateOptionSemanticsV1 {
4978 Accept,
4979 Decline,
4980 Inspect,
4981 Exit,
4982 Unknown,
4983}
4984
4985#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
4987#[serde(deny_unknown_fields)]
4988pub struct OperatorGateOptionV1 {
4989 pub text: String,
4990 pub semantics: OperatorGateOptionSemanticsV1,
4991 pub selected: bool,
4992}
4993
4994impl OperatorGateOptionV1 {
4995 fn is_valid(&self) -> bool {
4996 valid_native_single_line(&self.text, HARNESS_GATE_OPTION_TEXT_MAX_BYTES, true)
4997 }
4998}
4999
5000#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5008#[serde(deny_unknown_fields)]
5009pub struct OperatorGateStateV1 {
5010 pub kind: OperatorGateKindV1,
5011 pub subject: OperatorGateSubjectV1,
5012 pub input: OperatorGateInputV1,
5013 pub options: Vec<OperatorGateOptionV1>,
5014}
5015
5016impl OperatorGateStateV1 {
5017 fn is_valid(&self) -> bool {
5018 let subject_valid = match &self.subject {
5019 OperatorGateSubjectV1::Directory { path: Some(path) } => {
5020 valid_native_single_line(path, HARNESS_GATE_PATH_MAX_BYTES, true)
5021 }
5022 OperatorGateSubjectV1::Directory { path: None }
5023 | OperatorGateSubjectV1::Hooks { .. }
5024 | OperatorGateSubjectV1::McpServers
5025 | OperatorGateSubjectV1::Account
5026 | OperatorGateSubjectV1::ApiKey
5027 | OperatorGateSubjectV1::Appearance
5028 | OperatorGateSubjectV1::Unknown => true,
5029 };
5030 subject_valid
5031 && self.options.len() <= HARNESS_GATE_OPTIONS_MAX
5032 && self.options.iter().all(OperatorGateOptionV1::is_valid)
5033 }
5034}
5035
5036#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
5044#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
5045pub enum PtyScreenStateV1 {
5046 #[default]
5047 Unknown,
5048 NotAgent { observed_process: String },
5049 OperatorGate { gate: OperatorGateStateV1 },
5050 Failing { reason: String },
5051 Ready,
5052}
5053
5054impl PtyScreenStateV1 {
5055 pub fn admits_blind_write(&self) -> bool {
5060 !matches!(
5061 self,
5062 Self::OperatorGate { .. } | Self::NotAgent { .. } | Self::Failing { .. }
5063 )
5064 }
5065
5066 fn validate(&self) -> Result<(), HarnessOperatorApiError> {
5067 let ok = match self {
5068 Self::Unknown | Self::Ready => true,
5069 Self::NotAgent { observed_process } => {
5070 valid_native_single_line(observed_process, HARNESS_SCREEN_STATE_PROCESS_MAX_BYTES, true)
5071 }
5072 Self::OperatorGate { gate } => gate.is_valid(),
5073 Self::Failing { reason } => {
5074 valid_native_single_line(reason, HARNESS_SCREEN_STATE_GATE_MAX_BYTES, true)
5075 }
5076 };
5077 if ok { Ok(()) } else { Err(HarnessOperatorApiError::InvalidRuntimeInventory) }
5078 }
5079}
5080
5081#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5082#[serde(deny_unknown_fields)]
5083pub struct HarnessRuntimeSessionV1 {
5084 pub instance_id: u64,
5085 pub generation: u64,
5086 pub provider: String,
5087 pub transport: HarnessRuntimeTransportV1,
5088 pub status: HarnessRuntimeSessionStatusV1,
5089 pub process_id: Option<u32>,
5090 pub terminal_size: Option<HarnessRuntimeTerminalSizeV1>,
5091 pub operation_pending: bool,
5092 pub input_pending: bool,
5093 #[serde(default, skip_serializing_if = "Option::is_none")]
5099 pub screen_state: Option<PtyScreenStateV1>,
5100}
5101
5102impl HarnessRuntimeSessionV1 {
5103 fn validate(&self) -> Result<(), HarnessOperatorApiError> {
5104 if self.instance_id == 0
5105 || self.generation == 0
5106 || !valid_runtime_id(&self.provider, 128)
5107 || self.terminal_size.is_some_and(|size| size.rows == 0 || size.columns == 0)
5108 {
5109 return Err(HarnessOperatorApiError::InvalidRuntimeInventory);
5110 }
5111 if let Some(screen_state) = &self.screen_state {
5112 screen_state.validate()?;
5113 }
5114 Ok(())
5115 }
5116}
5117
5118#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
5119#[serde(rename_all = "kebab-case")]
5120pub enum HarnessRuntimeMouseProtocolEncodingV1 { Default, Utf8, Sgr }
5121
5122#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5123#[serde(deny_unknown_fields)]
5124pub struct HarnessRuntimeTerminalFrameV1 {
5125 pub sequence: u64,
5126 pub size: HarnessRuntimeTerminalSizeV1,
5127 pub cursor_row: u16,
5128 pub cursor_column: u16,
5129 pub formatted: Vec<u8>,
5130 pub scrollback_formatted: Vec<Vec<u8>>,
5131 pub alternate_screen: bool,
5132 pub mouse_protocol_enabled: bool,
5133 pub mouse_protocol_encoding: HarnessRuntimeMouseProtocolEncodingV1,
5134 #[serde(default)]
5143 pub produced_at_unix_ms: u64,
5144 #[serde(default, skip_serializing_if = "Option::is_none")]
5150 pub screen_state: Option<PtyScreenStateV1>,
5151 #[serde(default, skip_serializing_if = "Option::is_none")]
5156 pub bracketed_paste: Option<bool>,
5157}
5158impl HarnessRuntimeTerminalFrameV1 {
5163 fn validate(&self) -> Result<(), HarnessOperatorApiError> {
5164 if self.size.rows == 0 || self.size.columns == 0 {
5165 return Err(HarnessOperatorApiError::InvalidTerminalPage);
5166 }
5167 if self.scrollback_formatted.len() > HARNESS_TERMINAL_SCROLLBACK_LINES_MAX {
5168 return Err(HarnessOperatorApiError::InvalidTerminalPage);
5169 }
5170 let scrollback_bytes = self.scrollback_formatted.iter()
5171 .map(Vec::len)
5172 .fold(0usize, usize::saturating_add);
5173 if self.formatted.len().saturating_add(scrollback_bytes) > HARNESS_TERMINAL_FRAME_MAX_BYTES {
5174 return Err(HarnessOperatorApiError::InvalidTerminalPage);
5175 }
5176 if let Some(screen_state) = &self.screen_state {
5177 screen_state.validate()?;
5178 }
5179 Ok(())
5180 }
5181}
5182
5183#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
5184#[serde(deny_unknown_fields)]
5185pub struct HarnessRuntimeSessionAddressV1 {
5186 pub node_id: String,
5187 pub incarnation_id: String,
5188 pub workspace_id: String,
5189 pub instance_id: u64,
5190 pub generation: u64,
5191}
5192
5193impl HarnessRuntimeSessionAddressV1 {
5194 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
5195 if !valid_runtime_id(&self.node_id, 128)
5196 || self.incarnation_id.len() != 32
5197 || !self.incarnation_id.bytes().all(is_lower_hex)
5198 || !valid_runtime_id(&self.workspace_id, 128)
5199 || self.instance_id == 0
5200 || self.generation == 0
5201 {
5202 return Err(HarnessOperatorApiError::InvalidTerminalPage);
5203 }
5204 Ok(())
5205 }
5206}
5207
5208#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5209#[serde(deny_unknown_fields)]
5210pub struct HarnessRuntimeTerminalPageV1 {
5211 pub session: HarnessRuntimeSessionAddressV1,
5212 pub frames: Vec<HarnessRuntimeTerminalFrameV1>,
5213 pub dropped: u64,
5214 pub transport_incomplete: bool,
5215 pub next_cursor: Option<u64>,
5216}
5217
5218impl HarnessRuntimeTerminalPageV1 {
5219 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
5220 self.session.validate()?;
5221 if self.frames.len() > usize::from(HARNESS_TERMINAL_PAGE_LIMIT_MAX)
5222 || self.frames.windows(2).any(|pair| pair[0].sequence >= pair[1].sequence)
5223 || self.next_cursor == Some(0)
5224 {
5225 return Err(HarnessOperatorApiError::InvalidTerminalPage);
5226 }
5227 if self.next_cursor.is_some()
5228 && self.next_cursor != self.frames.last().map(|frame| frame.sequence)
5229 {
5230 return Err(HarnessOperatorApiError::InvalidTerminalPage);
5231 }
5232 for frame in &self.frames { frame.validate()?; }
5233 Ok(())
5234 }
5235
5236 pub fn validate_for(
5237 &self,
5238 session: &HarnessRuntimeSessionAddressV1,
5239 ) -> Result<(), HarnessOperatorApiError> {
5240 self.validate()?;
5241 if &self.session != session {
5242 return Err(HarnessOperatorApiError::InvalidTerminalPage);
5243 }
5244 Ok(())
5245 }
5246}
5247
5248#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
5249#[serde(rename_all = "kebab-case")]
5250pub enum HarnessRuntimeManagedModeV1 { Pty, Inline, Acp }
5251
5252#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
5253#[serde(rename_all = "kebab-case")]
5254pub enum HarnessRuntimeManagedStateV1 { IdentityPending, Live, Dormant, Unavailable }
5255
5256#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5257#[serde(deny_unknown_fields)]
5258pub struct HarnessRuntimeSessionBindingV1 {
5259 pub workspace_id: String,
5260 pub instance_id: u64,
5261 pub generation: u64,
5262}
5263
5264#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5265#[serde(deny_unknown_fields)]
5266pub struct HarnessRuntimeManagedSessionV1 {
5267 pub record_id: String,
5268 pub display_name: String,
5269 pub display_name_truncated: bool,
5270 pub provider: String,
5271 pub mode: HarnessRuntimeManagedModeV1,
5272 pub state: HarnessRuntimeManagedStateV1,
5273 pub workspace_id: String,
5274 pub active_binding: Option<HarnessRuntimeSessionBindingV1>,
5275 pub provider_identity_present: bool,
5276 pub updated_at_unix_ms: u64,
5277 pub blocked_count: u64,
5285 pub last_blocked_at_ms: Option<u64>,
5288}
5289
5290impl HarnessRuntimeManagedSessionV1 {
5291 fn validate(&self) -> Result<(), HarnessOperatorApiError> {
5292 if !valid_runtime_id(&self.record_id, 128)
5293 || !valid_runtime_id(&self.provider, 128)
5294 || !valid_runtime_id(&self.workspace_id, 128)
5295 || self.display_name.is_empty()
5296 || self.display_name.len() > 256
5297 || self.display_name.chars().any(char::is_control)
5298 || self.updated_at_unix_ms == 0
5299 || self.active_binding.as_ref().is_some_and(|binding| {
5300 !valid_runtime_id(&binding.workspace_id, 128)
5301 || binding.instance_id == 0
5302 || binding.generation == 0
5303 })
5304 || (self.blocked_count == 0) != self.last_blocked_at_ms.is_none()
5305 {
5306 return Err(HarnessOperatorApiError::InvalidRuntimeInventory);
5307 }
5308 Ok(())
5309 }
5310}
5311
5312#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5313#[serde(deny_unknown_fields)]
5314pub struct HarnessRuntimeInventoryPageV1 {
5315 pub nodes: Vec<HarnessRuntimeNodeInventoryV1>,
5316 pub next_cursor: Option<String>,
5317}
5318
5319impl HarnessRuntimeInventoryPageV1 {
5320 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
5321 if self.nodes.len() > usize::from(HARNESS_RUNTIME_INVENTORY_PAGE_LIMIT_MAX)
5322 || self.nodes.windows(2).any(|pair| pair[0].node_id >= pair[1].node_id)
5323 || self.next_cursor.as_ref().is_some_and(|cursor| match self.nodes.last() {
5324 Some(last) => cursor != &last.node_id,
5325 None => true,
5326 })
5327 {
5328 return Err(HarnessOperatorApiError::InvalidRuntimeInventory);
5329 }
5330 for node in &self.nodes { node.validate()?; }
5331 Ok(())
5332 }
5333}
5334
5335#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
5336#[serde(rename_all = "kebab-case")]
5337pub enum HarnessNativeSessionCatalogScopeV1 { Workspace, Unregistered }
5338
5339#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
5340#[serde(rename_all = "kebab-case")]
5341pub enum HarnessNativeSessionCatalogWindowV1 { Recent, Older }
5342
5343#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
5344#[serde(deny_unknown_fields)]
5345pub struct HarnessNativeSessionRouteV1 {
5346 pub node_id: String,
5347 pub incarnation_id: String,
5348 pub scope: HarnessNativeSessionCatalogScopeV1,
5349 pub workspace_id: Option<String>,
5350 pub provider: String,
5351}
5352
5353impl HarnessNativeSessionRouteV1 {
5354 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
5355 let scope_valid = matches!(
5356 (self.scope, self.workspace_id.as_ref()),
5357 (HarnessNativeSessionCatalogScopeV1::Workspace, Some(_))
5358 | (HarnessNativeSessionCatalogScopeV1::Unregistered, None)
5359 );
5360 if !scope_valid
5361 || !valid_runtime_id(&self.node_id, 128)
5362 || self.incarnation_id.len() != 32
5363 || !self.incarnation_id.bytes().all(is_lower_hex)
5364 || !valid_runtime_id(&self.provider, 128)
5365 || self.workspace_id.as_deref().is_some_and(|value| {
5366 !valid_runtime_id(value, 128)
5367 })
5368 {
5369 return Err(HarnessOperatorApiError::InvalidNativeHistory);
5370 }
5371 Ok(())
5372 }
5373}
5374
5375#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5376#[serde(deny_unknown_fields)]
5377pub struct HarnessNativeSessionSelectionV1 {
5378 pub route: HarnessNativeSessionRouteV1,
5379 pub catalog_revision: u64,
5380 pub recent_cutoff_unix_ms: u64,
5381 pub selection_id: String,
5382}
5383
5384impl HarnessNativeSessionSelectionV1 {
5385 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
5386 self.route.validate()?;
5387 if self.catalog_revision == 0 || !valid_native_selection_id(&self.selection_id) {
5388 return Err(HarnessOperatorApiError::InvalidNativeHistory);
5389 }
5390 Ok(())
5391 }
5392}
5393
5394#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
5395#[serde(rename_all = "kebab-case")]
5396pub enum HarnessNativeSessionExternalGroupKindV1 { Project, Global }
5397
5398#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5399#[serde(deny_unknown_fields)]
5400pub struct HarnessNativeSessionExternalGroupV1 {
5401 pub group_id: String,
5402 pub kind: HarnessNativeSessionExternalGroupKindV1,
5403 pub display_name: String,
5404}
5405
5406impl HarnessNativeSessionExternalGroupV1 {
5407 fn validate(&self) -> Result<(), HarnessOperatorApiError> {
5408 if !valid_native_group_id(&self.group_id)
5409 || !valid_native_single_line(&self.display_name, 256, true)
5410 || matches!(self.display_name.as_str(), "." | "..")
5411 || self.display_name.contains('/')
5412 || self.display_name.contains('\\')
5413 || self.display_name.contains(':')
5414 {
5415 return Err(HarnessOperatorApiError::InvalidNativeHistory);
5416 }
5417 Ok(())
5418 }
5419}
5420
5421#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5422#[serde(deny_unknown_fields)]
5423pub struct HarnessNativeSessionCatalogEntryV1 {
5424 pub selection_id: String,
5425 pub title: Option<String>,
5426 pub modified_at_unix_ms: Option<u64>,
5427 pub model: Option<String>,
5428 pub message_count: u64,
5429 pub completed_turn_count: Option<u64>,
5430 pub external_group: Option<HarnessNativeSessionExternalGroupV1>,
5431 pub record_id: Option<String>,
5432}
5433
5434impl HarnessNativeSessionCatalogEntryV1 {
5435 fn validate_for_route(
5436 &self,
5437 route: &HarnessNativeSessionRouteV1,
5438 ) -> Result<(), HarnessOperatorApiError> {
5439 if !valid_native_selection_id(&self.selection_id)
5440 || self.title.as_ref().is_some_and(|value| {
5441 !valid_native_single_line(value, 512, false)
5442 })
5443 || self.model.as_ref().is_some_and(|value| {
5444 !valid_native_single_line(value, 512, true)
5445 })
5446 || self.record_id.as_deref().is_some_and(|value| {
5447 !valid_runtime_id(value, 128)
5448 })
5449 {
5450 return Err(HarnessOperatorApiError::InvalidNativeHistory);
5451 }
5452 if let Some(group) = &self.external_group { group.validate()?; }
5453 let route_fields_valid = match route.scope {
5454 HarnessNativeSessionCatalogScopeV1::Workspace => self.external_group.is_none(),
5455 HarnessNativeSessionCatalogScopeV1::Unregistered => {
5456 self.external_group.is_some() && self.record_id.is_none()
5457 }
5458 };
5459 if !route_fields_valid {
5460 return Err(HarnessOperatorApiError::InvalidNativeHistory);
5461 }
5462 Ok(())
5463 }
5464}
5465
5466#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5467#[serde(deny_unknown_fields)]
5468pub struct HarnessNativeSessionCatalogSummaryV1 {
5469 pub catalog_revision: u64,
5470 pub recent_cutoff_unix_ms: u64,
5471 pub recent_total_count: u32,
5472 pub older_total_count: u32,
5473 pub recent_next_after_selection_id: Option<String>,
5474 pub recent_has_more: bool,
5475}
5476
5477impl HarnessNativeSessionCatalogSummaryV1 {
5478 fn validate(&self, entry_count: usize) -> Result<(), HarnessOperatorApiError> {
5479 if self.catalog_revision == 0
5480 || self.recent_next_after_selection_id.as_deref().is_some_and(|value| {
5481 !valid_native_selection_id(value)
5482 })
5483 || self.recent_has_more != self.recent_next_after_selection_id.is_some()
5484 || usize::try_from(self.recent_total_count).map_or(true, |count| count < entry_count)
5485 || self.recent_has_more != (usize::try_from(self.recent_total_count)
5486 .unwrap_or(usize::MAX) > entry_count)
5487 {
5488 return Err(HarnessOperatorApiError::InvalidNativeHistory);
5489 }
5490 Ok(())
5491 }
5492}
5493
5494#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5495#[serde(deny_unknown_fields)]
5496pub struct HarnessNativeSessionsCatalogedV1 {
5497 pub route: HarnessNativeSessionRouteV1,
5498 pub entries: Vec<HarnessNativeSessionCatalogEntryV1>,
5499 pub summary: Option<HarnessNativeSessionCatalogSummaryV1>,
5500}
5501
5502impl HarnessNativeSessionsCatalogedV1 {
5503 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
5504 self.route.validate()?;
5505 validate_native_entries(&self.route, &self.entries)?;
5506 if let Some(summary) = &self.summary { summary.validate(self.entries.len())?; }
5507 Ok(())
5508 }
5509}
5510
5511#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5512#[serde(deny_unknown_fields)]
5513pub struct HarnessNativeSessionCatalogPageV1 {
5514 pub window: HarnessNativeSessionCatalogWindowV1,
5515 pub revision: u64,
5516 pub entries: Vec<HarnessNativeSessionCatalogEntryV1>,
5517 pub next_after_selection_id: Option<String>,
5518 pub remaining_count: u32,
5519 pub has_more: bool,
5520}
5521
5522impl HarnessNativeSessionCatalogPageV1 {
5523 fn validate_for_route(
5524 &self,
5525 route: &HarnessNativeSessionRouteV1,
5526 ) -> Result<(), HarnessOperatorApiError> {
5527 validate_native_entries(route, &self.entries)?;
5528 if self.revision == 0
5529 || self.next_after_selection_id.as_deref().is_some_and(|value| {
5530 !valid_native_selection_id(value)
5531 })
5532 || self.has_more != self.next_after_selection_id.is_some()
5533 || self.has_more != (self.remaining_count > 0)
5534 {
5535 return Err(HarnessOperatorApiError::InvalidNativeHistory);
5536 }
5537 Ok(())
5538 }
5539}
5540
5541#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5542#[serde(deny_unknown_fields)]
5543pub struct HarnessNativeSessionsPagedV1 {
5544 pub route: HarnessNativeSessionRouteV1,
5545 pub page: HarnessNativeSessionCatalogPageV1,
5546}
5547
5548impl HarnessNativeSessionsPagedV1 {
5549 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
5550 self.route.validate()?;
5551 self.page.validate_for_route(&self.route)
5552 }
5553}
5554
5555#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
5556#[serde(rename_all = "kebab-case")]
5557pub enum HarnessNativeSessionPreviewRoleV1 { User, Assistant }
5558
5559#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5560#[serde(deny_unknown_fields)]
5561pub struct HarnessNativeSessionPreviewMessageV1 {
5562 pub role: HarnessNativeSessionPreviewRoleV1,
5563 pub text: String,
5564}
5565
5566#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5567#[serde(deny_unknown_fields)]
5568pub struct HarnessNativeSessionPreviewV1 {
5569 pub title: Option<String>,
5570 pub modified_at_unix_ms: Option<u64>,
5571 pub model: Option<String>,
5572 pub message_count: u64,
5573 pub message_count_exact: bool,
5574 pub completed_turn_count: Option<u64>,
5575 pub total_tokens: Option<u64>,
5576 pub truncated: bool,
5577 pub messages: Vec<HarnessNativeSessionPreviewMessageV1>,
5578}
5579
5580impl HarnessNativeSessionPreviewV1 {
5581 fn validate(&self) -> Result<(), HarnessOperatorApiError> {
5582 if self.title.as_ref().is_some_and(|value| {
5583 !valid_native_single_line(value, 512, false)
5584 })
5585 || self.model.as_ref().is_some_and(|value| {
5586 !valid_native_single_line(value, 512, true)
5587 })
5588 || self.messages.len()
5589 > usize::from(HARNESS_NATIVE_SESSION_PREVIEW_MESSAGE_LIMIT_MAX)
5590 || self.messages.iter().any(|message| {
5591 message.text.len() > HARNESS_NATIVE_SESSION_PREVIEW_TEXT_MAX_BYTES
5592 || message.text.chars().any(|character| {
5593 character.is_control()
5594 && !matches!(character, '\n' | '\r' | '\t')
5595 })
5596 })
5597 {
5598 return Err(HarnessOperatorApiError::InvalidNativeHistory);
5599 }
5600 Ok(())
5601 }
5602}
5603
5604#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5605#[serde(deny_unknown_fields)]
5606pub struct HarnessNativeSessionPreviewedV1 {
5607 pub selection: HarnessNativeSessionSelectionV1,
5608 pub preview: HarnessNativeSessionPreviewV1,
5609}
5610
5611impl HarnessNativeSessionPreviewedV1 {
5612 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
5613 self.selection.validate()?;
5614 self.preview.validate()
5615 }
5616}
5617
5618#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5625#[serde(deny_unknown_fields)]
5626pub struct HarnessSessionRecordPreviewedV1 {
5627 pub record_id: String,
5628 pub preview: HarnessNativeSessionPreviewV1,
5629}
5630
5631impl HarnessSessionRecordPreviewedV1 {
5632 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
5633 if !valid_runtime_id(&self.record_id, 128) {
5634 return Err(HarnessOperatorApiError::InvalidRuntimeInventory);
5635 }
5636 self.preview.validate()
5637 }
5638}
5639
5640#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5645#[serde(deny_unknown_fields)]
5646pub struct HarnessSessionRecordResumedV1 {
5647 pub record: HarnessRuntimeManagedSessionV1,
5648 pub session: HarnessRuntimeSessionAddressV1,
5649}
5650
5651impl HarnessSessionRecordResumedV1 {
5652 fn validate(&self) -> Result<(), HarnessOperatorApiError> {
5653 self.record.validate()?;
5654 self.session.validate()
5655 }
5656}
5657
5658#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5663#[serde(deny_unknown_fields)]
5664pub struct HarnessNativeSessionIndexedV1 {
5665 pub selection: HarnessNativeSessionSelectionV1,
5666 pub record: HarnessRuntimeManagedSessionV1,
5667}
5668
5669impl HarnessNativeSessionIndexedV1 {
5670 fn validate(&self) -> Result<(), HarnessOperatorApiError> {
5671 self.selection.validate()?;
5672 self.record.validate()
5673 }
5674}
5675
5676#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
5677#[serde(rename_all = "kebab-case")]
5678pub enum HarnessOperatorMutationOutcomeV1 {
5679 Applied,
5680 Replayed,
5681}
5682
5683#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5684#[serde(rename_all = "kebab-case")]
5685pub enum HarnessOperatorHostErrorV1 {
5686 InvalidRequest,
5687 Unauthorized,
5688 NotFound,
5689 Conflict,
5690 TooLarge,
5691 Deadline,
5692 Busy,
5693 Unavailable,
5694 OutcomeUnknown,
5700 UnsupportedTransport {
5716 agent: String,
5717 transport: HarnessRuntimeTransportV1,
5718 },
5719 UnsupportedCapability,
5729 Internal,
5730 Unsupported,
5750 BuildStampMismatch { expected: String, received: String },
5759 EngineRefused { detail: String },
5780 InvalidLaunchSelection {
5787 task_id: HarnessTaskId,
5788 plan_id: Option<HarnessSelectorV1>,
5789 why: String,
5790 },
5791 UnsupportedCheckpointVersion { version: u16 },
5792 InvalidDispatchContext { reason: String },
5793 MutationDigestMismatch,
5794 DispatchFingerprintUnavailable,
5795 NonAtomicRunOperation,
5796 AcceptedSpawnProofRequired,
5797 InvalidAcceptedSpawnProof { reason: String },
5798 DeliveryAuthorityWindowClosed,
5799 DeliveryCompilationInvalid,
5800 InvalidStagedDeliveryProof { reason: String },
5801 AtomicDeliveryCommitRequired,
5802 ContinuationAuthorityWindowClosed,
5803 InvalidContinuationProof { reason: String },
5804 AtomicContinuationBindRequired,
5805 InvalidHarnessMcpReservation { reason: String },
5806 HarnessMcpGrantActorRefused {
5807 actor_kind: String,
5808 parent_run_id: Option<HarnessRunId>,
5809 grant_actor_run_id: HarnessRunId,
5810 },
5811 HarnessMcpGrantOperationLinkRefused {
5812 grant_id: SessionGrantId,
5813 operation_id: HarnessOperationId,
5814 existing_grant_id: SessionGrantId,
5815 },
5816 HarnessMcpGrantRevisionRefused {
5817 grant_id: SessionGrantId,
5818 durable_revision: HarnessRevision,
5819 presented_revision: HarnessRevision,
5820 },
5821 HarnessMcpGrantLinkRefused {
5822 grant_id: SessionGrantId,
5823 grant_actor_run_id: HarnessRunId,
5824 dispatch_actor_run_id: HarnessRunId,
5825 },
5826 HarnessMcpGrantTargetRefused {
5827 grant_id: SessionGrantId,
5828 presented_target: HarnessGrantTargetV1,
5829 allowed_targets: Vec<HarnessGrantTargetV1>,
5830 },
5831 HarnessMcpReplayMismatch,
5832 HarnessMcpProofMismatch,
5833 HarnessMcpArmProofReservationFieldRefused {
5834 field: String,
5835 durable: String,
5836 proof: String,
5837 },
5838 HarnessMcpArmProofRouteRefused {
5839 field: String,
5840 durable: String,
5841 route: String,
5842 },
5843 HarnessMcpArmProofBindingRefused {
5844 field: String,
5845 expected: String,
5846 actual: String,
5847 },
5848 HarnessMcpArmDurableLookupMissing {
5849 missing: String,
5850 operation_id: Option<HarnessOperationId>,
5851 reservation_id: Option<String>,
5852 },
5853 HarnessMcpArmReservationNotReadyRefused {
5854 reservation_id: String,
5855 state: String,
5856 armed_at_unix_ms: u64,
5857 updated_at_unix_ms: u64,
5858 expires_at_unix_ms: u64,
5859 },
5860 HarnessMcpArmRouteInvalid {
5861 operation_id: HarnessOperationId,
5862 field: String,
5863 value: String,
5864 },
5865 HarnessMcpLaunchPolicyRefused {
5866 plan_policy: String,
5867 reservation_present: bool,
5868 },
5869 HarnessMcpLaunchReservationNotArmedRefused {
5870 reservation_id: String,
5871 state: String,
5872 },
5873 HarnessMcpLaunchOperationRefused {
5874 reservation_operation_id: HarnessOperationId,
5875 dispatch_operation_id: HarnessOperationId,
5876 },
5877 HarnessMcpLaunchGrantRefused {
5878 plan_grant_id: SessionGrantId,
5879 plan_grant_revision: HarnessRevision,
5880 reservation_grant_id: SessionGrantId,
5881 reservation_grant_revision: HarnessRevision,
5882 },
5883 HarnessMcpSpecializedTransitionRequired,
5884 OperatorRequestConflict { operation_id: HarnessOperationId },
5885 InvalidOperatorTaskTransition {
5886 from: HarnessTaskStateV1,
5887 to: HarnessTaskStateV1,
5888 },
5889 TaskHasActiveRun,
5890 ExecutionSpecRevisionMismatch {
5891 expected: Option<HarnessRevision>,
5892 actual: Option<HarnessRevision>,
5893 },
5894 ExecutionSpecLaunchMismatch,
5895 IssuedExecutionCasMismatch {
5896 expected: HarnessExpectedExecutionSpecRevisionV1,
5897 spec: Option<HarnessRevision>,
5898 issuance: Option<HarnessRevision>,
5899 },
5900 TaskNotReady,
5901 TaskDependenciesNotDone {
5912 task_id: String,
5913 dependency_ids: Vec<String>,
5914 },
5915 TaskStartBlockedByRun { task_id: String },
5922 SchedulerResourceExhausted,
5923 SchedulerInvalidGraph { reason: String },
5924}
5925
5926#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5927#[serde(deny_unknown_fields)]
5928pub struct HarnessReadEnvelopeV1 {
5929 pub build_stamp: String,
5930 pub credential: HarnessReadCredential,
5931 pub request: HarnessReadRequestV1,
5932}
5933
5934impl HarnessReadEnvelopeV1 {
5935 pub fn new(credential: HarnessReadCredential, request: HarnessReadRequestV1) -> Self {
5940 Self { build_stamp: BUILD_STAMP.to_string(), credential, request }
5941 }
5942
5943 pub fn validate(&self) -> Result<(), HarnessReadApiError> {
5944 if self.build_stamp != BUILD_STAMP {
5945 return Err(HarnessReadApiError::BuildStampMismatch {
5946 expected: BUILD_STAMP.to_string(),
5947 received: self.build_stamp.clone(),
5948 });
5949 }
5950 validate_credential(self.credential.expose())?;
5951 self.request.validate()
5952 }
5953}
5954
5955#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
5956#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
5957pub enum HarnessReadRequestV1 {
5958 ContextGet,
5959 MonitorGet { run_id: Option<HarnessRunId> },
5960 TimelineRead {
5961 run_id: Option<HarnessRunId>,
5962 after_sequence: Option<u64>,
5963 limit: u16,
5964 },
5965 TasksList {
5966 after_task_id: Option<HarnessTaskId>,
5967 state: Option<HarnessTaskStateV1>,
5968 #[serde(default)]
5976 parent_task_id: Option<HarnessTaskId>,
5977 limit: u16,
5978 },
5979 TaskGet { task_id: HarnessTaskId },
5980 RunsList {
5981 task_id: Option<HarnessTaskId>,
5982 after_run_id: Option<HarnessRunId>,
5983 lifecycle: Option<HarnessRunLifecycleV1>,
5984 #[serde(default)]
5989 parent_run_id: Option<HarnessRunId>,
5990 limit: u16,
5991 },
5992 RunGet { run_id: HarnessRunId },
5993 OperationGet { operation_id: HarnessOperationId },
5994 TaskCreate {
6007 title: String,
6008 body: String,
6009 parent_task_id: Option<HarnessTaskId>,
6010 },
6011 TaskMove {
6019 task_id: HarnessTaskId,
6020 expected_revision: HarnessRevision,
6021 to: HarnessTaskStateV1,
6022 },
6023 RunFinish {
6044 outcome: HarnessRunFinishOutcomeV1,
6045 #[serde(default)]
6046 summary: Option<String>,
6047 },
6048}
6049
6050impl HarnessReadRequestV1 {
6051 pub fn validate(&self) -> Result<(), HarnessReadApiError> {
6052 match self {
6053 Self::RunFinish { summary, .. } => {
6054 if let Some(summary) = summary {
6055 if summary.len() > HARNESS_BODY_MAX_BYTES {
6056 return Err(HarnessReadApiError::InvalidText("run finish summary"));
6057 }
6058 }
6059 Ok(())
6060 }
6061 Self::TaskCreate { title, body, parent_task_id } => {
6062 if title.is_empty()
6063 || title.len() > HARNESS_TITLE_MAX_BYTES
6064 || body.len() > HARNESS_BODY_MAX_BYTES
6065 {
6066 return Err(HarnessReadApiError::InvalidTaskCreate);
6067 }
6068 if let Some(parent_task_id) = parent_task_id {
6069 parent_task_id.validate().map_err(HarnessReadApiError::Protocol)?;
6070 }
6071 Ok(())
6072 }
6073 Self::TaskMove { task_id, expected_revision, .. } => {
6074 task_id.validate().map_err(HarnessReadApiError::Protocol)?;
6075 expected_revision.validate().map_err(HarnessReadApiError::Protocol)
6076 }
6077 Self::TimelineRead { run_id, after_sequence, limit } => {
6078 if let Some(run_id) = run_id {
6079 run_id.validate().map_err(HarnessReadApiError::Protocol)?;
6080 }
6081 if after_sequence == &Some(0) {
6082 return Err(HarnessReadApiError::InvalidCursor);
6083 }
6084 validate_limit(*limit, HARNESS_TIMELINE_PAGE_LIMIT_MAX)
6085 }
6086 Self::TasksList { after_task_id, parent_task_id, limit, .. } => {
6087 if let Some(task_id) = after_task_id {
6088 task_id.validate().map_err(HarnessReadApiError::Protocol)?;
6089 }
6090 if let Some(task_id) = parent_task_id {
6091 task_id.validate().map_err(HarnessReadApiError::Protocol)?;
6092 }
6093 validate_limit(*limit, HARNESS_ENTITY_PAGE_LIMIT_MAX)
6094 }
6095 Self::RunsList { task_id, after_run_id, parent_run_id, limit, .. } => {
6096 if let Some(task_id) = task_id {
6097 task_id.validate().map_err(HarnessReadApiError::Protocol)?;
6098 }
6099 if let Some(run_id) = after_run_id {
6100 run_id.validate().map_err(HarnessReadApiError::Protocol)?;
6101 }
6102 if let Some(run_id) = parent_run_id {
6103 run_id.validate().map_err(HarnessReadApiError::Protocol)?;
6104 }
6105 validate_limit(*limit, HARNESS_ENTITY_PAGE_LIMIT_MAX)
6106 }
6107 Self::TaskGet { task_id } => task_id.validate().map_err(HarnessReadApiError::Protocol),
6108 Self::RunGet { run_id } => run_id.validate().map_err(HarnessReadApiError::Protocol),
6109 Self::OperationGet { operation_id } => {
6110 operation_id.validate().map_err(HarnessReadApiError::Protocol)
6111 }
6112 Self::MonitorGet { run_id } => {
6113 if let Some(run_id) = run_id {
6114 run_id.validate().map_err(HarnessReadApiError::Protocol)?;
6115 }
6116 Ok(())
6117 }
6118 Self::ContextGet => Ok(()),
6119 }
6120 }
6121}
6122
6123#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6124#[serde(tag = "status", rename_all = "kebab-case", deny_unknown_fields)]
6125pub enum HarnessReadReplyV1 {
6126 Ok { response: HarnessReadResponseV1 },
6127 Error { error: HarnessReadHostErrorV1 },
6128}
6129
6130impl HarnessReadReplyV1 {
6131 pub fn validate(&self) -> Result<(), HarnessReadApiError> {
6132 match self {
6133 Self::Ok { response } => response.validate(),
6134 Self::Error { .. } => Ok(()),
6135 }
6136 }
6137}
6138
6139#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6140#[serde(tag = "kind", content = "value", rename_all = "kebab-case", deny_unknown_fields)]
6141pub enum HarnessReadResponseV1 {
6142 Context(SessionContextV1),
6143 Monitor(SessionMonitorV1),
6144 Timeline(TimelinePageV1),
6145 Tasks(TaskPageV1),
6146 Task(RedactedTaskV1),
6147 Runs(RunPageV1),
6148 Run(RedactedRunV1),
6149 Operation(RedactedOperationV1),
6150 TaskCreate(HarnessTaskCreateResultV1),
6151 TaskMove(HarnessTaskMoveResultV1),
6152 RunFinish(HarnessRunFinishResultV1),
6153}
6154
6155impl HarnessReadResponseV1 {
6156 pub fn validate(&self) -> Result<(), HarnessReadApiError> {
6157 match self {
6158 Self::Context(value) => value.validate(),
6159 Self::Monitor(value) => value.validate(),
6160 Self::Timeline(value) => value.validate(),
6161 Self::Tasks(value) => value.validate(),
6162 Self::Task(value) => value.validate(),
6163 Self::Runs(value) => value.validate(),
6164 Self::Run(value) => value.validate(),
6165 Self::Operation(value) => value.validate(),
6166 Self::TaskCreate(value) => value.validate().map_err(HarnessReadApiError::Protocol),
6167 Self::TaskMove(value) => value.validate().map_err(HarnessReadApiError::Protocol),
6168 Self::RunFinish(value) => value.validate().map_err(HarnessReadApiError::Protocol),
6169 }
6170 }
6171}
6172
6173#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6174#[serde(rename_all = "kebab-case")]
6175pub enum HarnessReadHostErrorV1 {
6176 InvalidRequest,
6177 Unauthorized,
6178 NotFoundOrDenied,
6179 TooLarge,
6180 Deadline,
6181 Internal,
6182 BuildStampMismatch { expected: String, received: String },
6191}
6192
6193#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6194#[serde(deny_unknown_fields)]
6195pub struct SessionContextV1 {
6196 pub grant_id: SessionGrantId,
6197 pub grant_revision: HarnessRevision,
6198 pub actor_run: CallerRunV1,
6199 pub task: Option<RedactedTaskV1>,
6203 pub sibling_runs: Vec<RedactedRunV1>,
6209 pub read_permissions: HarnessReadPermissionsV1,
6210 pub monitoring_visibility: HarnessMonitoringVisibilityV1,
6211 pub child_task_count: u64,
6223 pub child_task_subtree_depth: u64,
6232 pub task_create: bool,
6235 pub task_mutate: bool,
6238 pub allowed_tool_ids: Vec<String>,
6239 pub history_message_count: Option<u64>,
6240 pub completed_turn_count: Option<u64>,
6241 pub total_tokens: Option<u64>,
6242}
6243
6244impl SessionContextV1 {
6245 pub fn validate(&self) -> Result<(), HarnessReadApiError> {
6246 self.grant_id.validate().map_err(HarnessReadApiError::Protocol)?;
6247 self.grant_revision.validate().map_err(HarnessReadApiError::Protocol)?;
6248 self.actor_run.validate()?;
6249 match &self.task {
6250 Some(task) => {
6251 task.validate()?;
6252 if self.actor_run.task_id.as_ref() != Some(&task.task_id) {
6253 return Err(HarnessReadApiError::InvalidContextTask);
6254 }
6255 }
6256 None if !self.sibling_runs.is_empty() => {
6257 return Err(HarnessReadApiError::InvalidContextTask);
6258 }
6259 None => {}
6260 }
6261 if self.sibling_runs.len() > HARNESS_LINKS_MAX
6262 || self.sibling_runs.windows(2).any(|pair| pair[0].run_id >= pair[1].run_id)
6263 || self.sibling_runs.iter().any(|run| {
6264 run.run_id == self.actor_run.run_id
6265 || run.task_id.as_ref() != self.actor_run.task_id.as_ref()
6266 })
6267 {
6268 return Err(HarnessReadApiError::InvalidContextTask);
6269 }
6270 for run in &self.sibling_runs {
6271 run.validate()?;
6272 }
6273 self.read_permissions.validate().map_err(HarnessReadApiError::Protocol)?;
6274 if self.allowed_tool_ids.len() > HARNESS_READ_TOOL_IDS.len() + HARNESS_WRITE_TOOL_IDS.len()
6275 || self.allowed_tool_ids.windows(2).any(|pair| pair[0] >= pair[1])
6276 || self.allowed_tool_ids.iter().any(|id| {
6277 !HARNESS_READ_TOOL_IDS.contains(&id.as_str())
6278 && !HARNESS_WRITE_TOOL_IDS.contains(&id.as_str())
6279 })
6280 || self.allowed_tool_ids != expected_allowed_tool_ids(self)
6281 {
6282 return Err(HarnessReadApiError::InvalidAllowedTools);
6283 }
6284 Ok(())
6285 }
6286}
6287
6288fn expected_allowed_tool_ids(context: &SessionContextV1) -> Vec<String> {
6289 let mut tools = vec!["g4a_context_get", "g4a_run_finish"];
6293 if context.monitoring_visibility != HarnessMonitoringVisibilityV1::None {
6294 tools.push("g4a_monitor_get");
6295 }
6296 if context.monitoring_visibility == HarnessMonitoringVisibilityV1::Timeline {
6297 tools.push("g4a_timeline_read");
6298 }
6299 if context.read_permissions.tasks != HarnessEntityReadScopeV1::None {
6300 tools.extend(["g4a_tasks_get", "g4a_tasks_list"]);
6301 }
6302 if context.read_permissions.runs != HarnessEntityReadScopeV1::None {
6303 tools.extend(["g4a_runs_get", "g4a_runs_list"]);
6304 }
6305 if context.read_permissions.operations != HarnessEntityReadScopeV1::None {
6306 tools.push("g4a_operation_get");
6307 }
6308 if context.task_create {
6309 tools.push("g4a_task_create");
6310 }
6311 if context.task_mutate {
6312 tools.push("g4a_task_move");
6313 }
6314 tools.sort_unstable();
6315 tools.into_iter().map(str::to_owned).collect()
6316}
6317
6318#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6319#[serde(deny_unknown_fields)]
6320pub struct CallerRunV1 {
6321 pub run_id: HarnessRunId,
6322 pub task_id: Option<HarnessTaskId>,
6323 pub parent_run_id: Option<HarnessRunId>,
6324 pub lifecycle: HarnessRunLifecycleV1,
6325 pub references_redacted: bool,
6326}
6327
6328impl CallerRunV1 {
6329 pub fn validate(&self) -> Result<(), HarnessReadApiError> {
6330 self.run_id.validate().map_err(HarnessReadApiError::Protocol)?;
6331 if let Some(task_id) = &self.task_id {
6332 task_id.validate().map_err(HarnessReadApiError::Protocol)?;
6333 }
6334 if let Some(parent_run_id) = &self.parent_run_id {
6335 parent_run_id.validate().map_err(HarnessReadApiError::Protocol)?;
6336 }
6337 Ok(())
6338 }
6339}
6340
6341#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
6342#[serde(rename_all = "kebab-case")]
6343pub enum ProjectionAvailabilityV1 {
6344 Unknown,
6345 NotObserved,
6346 Current,
6347 Partial,
6348 Frozen,
6349}
6350
6351#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
6352#[serde(rename_all = "kebab-case")]
6353pub enum ProjectionFreshnessV1 {
6354 Unavailable,
6355 Live,
6356 Stale,
6357 IncompleteAfterGap,
6358 LastKnown,
6359 ReplacedIncarnation,
6360}
6361
6362#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
6363#[serde(rename_all = "kebab-case")]
6364pub enum FeatureObservationStateV1 {
6365 Unknown,
6366 NotSupportedByObservedSources,
6367 SupportedNotObserved,
6368 Observed,
6369}
6370
6371#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6372#[serde(deny_unknown_fields)]
6373pub struct MonitorFeatureStatesV1 {
6374 pub todo: FeatureObservationStateV1,
6375 pub tools: FeatureObservationStateV1,
6376 pub subagents: FeatureObservationStateV1,
6377 pub interactions: FeatureObservationStateV1,
6378 pub owned_processes: FeatureObservationStateV1,
6379 pub files: FeatureObservationStateV1,
6380 pub usage: FeatureObservationStateV1,
6381 pub history: FeatureObservationStateV1,
6382}
6383
6384#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6385#[serde(deny_unknown_fields)]
6386pub struct SessionMonitorV1 {
6387 pub run_id: HarnessRunId,
6388 pub visibility: HarnessMonitoringVisibilityV1,
6389 pub availability: ProjectionAvailabilityV1,
6390 pub freshness: ProjectionFreshnessV1,
6391 pub transport_incomplete: bool,
6392 pub features: MonitorFeatureStatesV1,
6393 pub todo_total: u16,
6394 pub todo_completed: u16,
6395 pub active_tools: u16,
6396 pub active_subagents: u16,
6397 pub active_interactions: u16,
6398 #[serde(default)]
6409 pub active_blocks: u16,
6410 pub active_processes: u16,
6411 pub input_tokens: u64,
6412 pub output_tokens: u64,
6413 pub cache_read_tokens: u64,
6414 pub cache_write_tokens: u64,
6415 pub reasoning_tokens: u64,
6416 pub context_window_tokens: Option<u64>,
6417 #[serde(default, skip_serializing_if = "Option::is_none")]
6418 pub history: Option<SessionMonitorHistoryV1>,
6419 pub detail: Option<SessionMonitorDetailV1>,
6420}
6421
6422impl SessionMonitorV1 {
6423 pub fn validate(&self) -> Result<(), HarnessReadApiError> {
6424 self.run_id.validate().map_err(HarnessReadApiError::Protocol)?;
6425 if self.todo_completed > self.todo_total {
6426 return Err(HarnessReadApiError::InvalidMonitorCounts);
6427 }
6428 let detail_allowed = matches!(
6429 self.visibility,
6430 HarnessMonitoringVisibilityV1::Detail | HarnessMonitoringVisibilityV1::Timeline
6431 );
6432 if self.detail.is_some() && !detail_allowed {
6433 return Err(HarnessReadApiError::InvalidMonitorDetail);
6434 }
6435 if matches!(self.availability, ProjectionAvailabilityV1::Unknown | ProjectionAvailabilityV1::NotObserved)
6436 && self.detail.is_some()
6437 {
6438 return Err(HarnessReadApiError::InvalidMonitorDetail);
6439 }
6440 if self.freshness == ProjectionFreshnessV1::IncompleteAfterGap
6441 && !self.transport_incomplete
6442 {
6443 return Err(HarnessReadApiError::InvalidMonitorDetail);
6444 }
6445 if let Some(detail) = &self.detail {
6446 detail.validate(&self.features)?;
6447 }
6448 if self.features.history != FeatureObservationStateV1::Observed
6449 && self.history.is_some()
6450 {
6451 return Err(HarnessReadApiError::InvalidMonitorDetail);
6452 }
6453 validate_feature_counts(self)?;
6454 Ok(())
6455 }
6456
6457 pub fn validate_for(&self, run_id: &HarnessRunId) -> Result<(), HarnessReadApiError> {
6458 self.validate()?;
6459 if &self.run_id != run_id {
6460 return Err(HarnessReadApiError::InvalidRunState);
6461 }
6462 Ok(())
6463 }
6464}
6465
6466#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6467#[serde(deny_unknown_fields)]
6468pub struct SessionMonitorHistoryV1 {
6469 pub message_count: u64,
6470 pub message_count_exact: bool,
6471 pub completed_turn_count: Option<u64>,
6472 pub total_tokens: Option<u64>,
6473}
6474
6475#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6476#[serde(deny_unknown_fields)]
6477pub struct SessionMonitorDetailV1 {
6478 pub todo_facts: Vec<TodoFactV1>,
6479 pub tool_facts: Vec<ActivityFactV1>,
6480 pub subagent_facts: Vec<ActivityFactV1>,
6481 pub interaction_facts: Vec<InteractionFactV1>,
6482 #[serde(default)]
6498 pub block_facts: Vec<BlockFactV1>,
6499 pub process_facts: Vec<ActivityFactV1>,
6500 pub file_facts: Vec<FileFactV1>,
6501}
6502
6503impl SessionMonitorDetailV1 {
6504 pub fn validate(&self, features: &MonitorFeatureStatesV1) -> Result<(), HarnessReadApiError> {
6505 validate_monitor_facts(&self.todo_facts)?;
6506 validate_monitor_facts(&self.tool_facts)?;
6507 validate_monitor_facts(&self.subagent_facts)?;
6508 validate_monitor_facts(&self.interaction_facts)?;
6509 validate_monitor_facts(&self.block_facts)?;
6510 validate_monitor_facts(&self.process_facts)?;
6511 validate_monitor_facts(&self.file_facts)?;
6512 for fact in &self.todo_facts { fact.validate()?; }
6513 for fact in &self.tool_facts { fact.validate()?; }
6514 for fact in &self.subagent_facts { fact.validate()?; }
6515 for fact in &self.interaction_facts { fact.validate()?; }
6516 for fact in &self.block_facts { fact.validate()?; }
6517 for fact in &self.process_facts { fact.validate()?; }
6518 for fact in &self.file_facts { fact.validate()?; }
6519 if self.tool_facts.iter().any(|fact| fact.class != ActivityClassV1::Tool)
6520 || self.subagent_facts.iter().any(|fact| fact.class != ActivityClassV1::Subagent)
6521 || self.process_facts.iter().any(|fact| fact.class != ActivityClassV1::OwnedProcess)
6522 {
6523 return Err(HarnessReadApiError::InvalidMonitorDetail);
6524 }
6525 Ok(())
6526 .and_then(|_| require_observed_or_empty(features.todo, &self.todo_facts))
6527 .and_then(|_| require_observed_or_empty(features.tools, &self.tool_facts))
6528 .and_then(|_| require_observed_or_empty(features.subagents, &self.subagent_facts))
6529 .and_then(|_| require_observed_or_empty(features.interactions, &self.interaction_facts))
6530 .and_then(|_| require_observed_or_empty(features.owned_processes, &self.process_facts))
6531 .and_then(|_| require_observed_or_empty(features.files, &self.file_facts))
6532 }
6533}
6534
6535#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6536#[serde(deny_unknown_fields)]
6537pub struct TodoFactV1 {
6538 pub state: TodoStateV1,
6539 #[serde(default, skip_serializing_if = "Option::is_none")]
6540 pub todo_id: Option<String>,
6541 #[serde(default, skip_serializing_if = "Option::is_none")]
6542 pub label: Option<String>,
6543 pub evidence: ObservationEvidenceV1,
6544}
6545
6546impl TodoFactV1 {
6547 fn validate(&self) -> Result<(), HarnessReadApiError> {
6548 validate_optional_observation_text(
6549 "todo id",
6550 self.todo_id.as_deref(),
6551 HARNESS_OBSERVATION_LABEL_MAX_BYTES,
6552 )?;
6553 validate_optional_observation_text(
6554 "todo label",
6555 self.label.as_deref(),
6556 HARNESS_OBSERVATION_TODO_TEXT_MAX_BYTES,
6557 )
6558 }
6559}
6560
6561#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
6562#[serde(rename_all = "kebab-case")]
6563pub enum TodoStateV1 { Pending, InProgress, Completed, Unknown }
6564
6565#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6566#[serde(deny_unknown_fields)]
6567pub struct ActivityFactV1 {
6568 pub class: ActivityClassV1,
6569 pub state: ActivityStateV1,
6570 #[serde(default, skip_serializing_if = "Option::is_none")]
6571 pub label: Option<String>,
6572 #[serde(default, skip_serializing_if = "Option::is_none")]
6573 pub correlation: Option<u16>,
6574 pub evidence: ObservationEvidenceV1,
6575}
6576
6577impl ActivityFactV1 {
6578 fn validate(&self) -> Result<(), HarnessReadApiError> {
6579 validate_optional_observation_text(
6580 "activity label",
6581 self.label.as_deref(),
6582 HARNESS_OBSERVATION_LABEL_MAX_BYTES,
6583 )?;
6584 validate_observation_correlation(self.correlation)
6585 }
6586}
6587
6588#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
6589#[serde(rename_all = "kebab-case")]
6590pub enum ActivityClassV1 { Tool, Subagent, OwnedProcess }
6591
6592#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
6593#[serde(rename_all = "kebab-case")]
6594pub enum ActivityStateV1 { Started, Active, Waiting, Completed, Failed, UnknownAfterGap }
6595
6596#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6597#[serde(deny_unknown_fields)]
6598pub struct InteractionFactV1 {
6599 pub class: InteractionClassV1,
6600 pub state: InteractionStateV1,
6601 #[serde(default, skip_serializing_if = "Option::is_none")]
6602 pub label: Option<String>,
6603 #[serde(default, skip_serializing_if = "Option::is_none")]
6604 pub correlation: Option<u16>,
6605 pub evidence: ObservationEvidenceV1,
6606}
6607
6608impl InteractionFactV1 {
6609 fn validate(&self) -> Result<(), HarnessReadApiError> {
6610 validate_optional_observation_text(
6611 "interaction label",
6612 self.label.as_deref(),
6613 HARNESS_OBSERVATION_LABEL_MAX_BYTES,
6614 )?;
6615 validate_observation_correlation(self.correlation)
6616 }
6617}
6618
6619#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
6620#[serde(rename_all = "kebab-case")]
6621pub enum InteractionClassV1 { Attention, Approval, UserInput }
6622
6623#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
6624#[serde(rename_all = "kebab-case")]
6625pub enum InteractionStateV1 { Required, Responded, Dismissed, UnknownAfterGap }
6626
6627#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6628#[serde(deny_unknown_fields)]
6629pub struct FileFactV1 {
6630 pub action: FileActionV1,
6631 #[serde(default, skip_serializing_if = "Option::is_none")]
6632 pub relative_path: Option<String>,
6633 pub evidence: ObservationEvidenceV1,
6634}
6635
6636impl FileFactV1 {
6637 fn validate(&self) -> Result<(), HarnessReadApiError> {
6638 if let Some(path) = self.relative_path.as_deref() {
6639 validate_observation_relative_path(path)?;
6640 }
6641 Ok(())
6642 }
6643}
6644
6645#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
6646#[serde(rename_all = "kebab-case")]
6647pub enum FileActionV1 { Changed }
6648
6649#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6656#[serde(deny_unknown_fields)]
6657pub struct BlockFactV1 {
6658 pub state: BlockStateV1,
6659 #[serde(default, skip_serializing_if = "Option::is_none")]
6660 pub label: Option<String>,
6661 #[serde(default, skip_serializing_if = "Option::is_none")]
6662 pub correlation: Option<u16>,
6663 pub evidence: ObservationEvidenceV1,
6664}
6665
6666impl BlockFactV1 {
6667 fn validate(&self) -> Result<(), HarnessReadApiError> {
6668 validate_optional_observation_text(
6669 "block label",
6670 self.label.as_deref(),
6671 HARNESS_OBSERVATION_BLOCK_LABEL_MAX_BYTES,
6672 )?;
6673 validate_observation_correlation(self.correlation)
6674 }
6675}
6676
6677#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
6682#[serde(rename_all = "kebab-case")]
6683pub enum BlockStateV1 { Blocked }
6684
6685#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
6686#[serde(rename_all = "kebab-case")]
6687pub enum ObservationEvidenceV1 {
6688 StructuredProvider,
6689 ManagedHook,
6690 WorkspaceObservation,
6691 NodeLifecycle,
6692 History,
6693 PtyHint,
6694}
6695
6696#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6697#[serde(deny_unknown_fields)]
6698pub struct TimelineEntryV1 {
6699 pub sequence: u64,
6700 pub received_at_ms: u64,
6701 pub category: TimelineCategoryV1,
6702 #[serde(default, skip_serializing_if = "Option::is_none")]
6703 pub label: Option<String>,
6704 #[serde(default)]
6705 pub state: TimelineStateV1,
6706 #[serde(default, skip_serializing_if = "Option::is_none")]
6707 pub correlation: Option<u16>,
6708 pub evidence: ObservationEvidenceV1,
6709}
6710
6711#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6712#[serde(deny_unknown_fields)]
6713pub struct TimelinePageV1 {
6714 pub run_id: HarnessRunId,
6715 pub availability: ProjectionAvailabilityV1,
6716 pub freshness: ProjectionFreshnessV1,
6717 pub transport_incomplete: bool,
6718 pub entries: Vec<TimelineEntryV1>,
6719 pub next_cursor: Option<u64>,
6720}
6721
6722impl TimelinePageV1 {
6723 pub fn validate(&self) -> Result<(), HarnessReadApiError> {
6724 self.run_id.validate().map_err(HarnessReadApiError::Protocol)?;
6725 validate_bounded(&self.entries, HARNESS_TIMELINE_PAGE_LIMIT_MAX, TimelineEntryV1::validate)?;
6726 if self.entries.windows(2).any(|pair| pair[0].sequence >= pair[1].sequence)
6727 || self.next_cursor == Some(0)
6728 {
6729 return Err(HarnessReadApiError::InvalidCursor);
6730 }
6731 if self.next_cursor.is_some()
6732 && self.next_cursor != self.entries.last().map(|entry| entry.sequence)
6733 {
6734 return Err(HarnessReadApiError::InvalidCursor);
6735 }
6736 Ok(())
6737 }
6738
6739 pub fn validate_for(&self, run_id: &HarnessRunId) -> Result<(), HarnessReadApiError> {
6740 self.validate()?;
6741 if &self.run_id != run_id {
6742 return Err(HarnessReadApiError::InvalidRunState);
6743 }
6744 Ok(())
6745 }
6746}
6747
6748#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6749#[serde(deny_unknown_fields)]
6750pub struct TaskPageV1 {
6751 pub tasks: Vec<RedactedTaskV1>,
6752 pub next_cursor: Option<HarnessTaskId>,
6753}
6754
6755impl TaskPageV1 {
6756 pub fn validate(&self) -> Result<(), HarnessReadApiError> {
6757 validate_bounded(&self.tasks, HARNESS_ENTITY_PAGE_LIMIT_MAX, RedactedTaskV1::validate)?;
6758 if self.tasks.windows(2).any(|pair| pair[0].task_id >= pair[1].task_id)
6759 || self.next_cursor.as_ref() != self.tasks.last().map(|task| &task.task_id)
6760 && self.next_cursor.is_some()
6761 {
6762 return Err(HarnessReadApiError::InvalidCursor);
6763 }
6764 Ok(())
6765 }
6766}
6767
6768#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6769#[serde(deny_unknown_fields)]
6770pub struct RunPageV1 {
6771 pub runs: Vec<RedactedRunV1>,
6772 pub next_cursor: Option<HarnessRunId>,
6773}
6774
6775impl RunPageV1 {
6776 pub fn validate(&self) -> Result<(), HarnessReadApiError> {
6777 validate_bounded(&self.runs, HARNESS_ENTITY_PAGE_LIMIT_MAX, RedactedRunV1::validate)?;
6778 if self.runs.windows(2).any(|pair| pair[0].run_id >= pair[1].run_id)
6779 || self.next_cursor.as_ref() != self.runs.last().map(|run| &run.run_id)
6780 && self.next_cursor.is_some()
6781 {
6782 return Err(HarnessReadApiError::InvalidCursor);
6783 }
6784 Ok(())
6785 }
6786}
6787
6788impl TimelineEntryV1 {
6789 pub fn validate(&self) -> Result<(), HarnessReadApiError> {
6790 if self.sequence == 0 || self.received_at_ms == 0 {
6791 return Err(HarnessReadApiError::InvalidTimelineEntry);
6792 }
6793 validate_optional_observation_text(
6794 "timeline label",
6795 self.label.as_deref(),
6796 HARNESS_OBSERVATION_PATH_MAX_BYTES,
6797 )?;
6798 validate_observation_correlation(self.correlation)?;
6799 Ok(())
6800 }
6801}
6802
6803#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
6804#[serde(rename_all = "kebab-case")]
6805pub enum TimelineStateV1 {
6806 Started,
6807 Active,
6808 Waiting,
6809 Required,
6810 Updated,
6811 Changed,
6812 Completed,
6813 Failed,
6814 Dismissed,
6815 Interrupted,
6816 Stale,
6817 UnknownAfterGap,
6818 #[default]
6819 Unknown,
6820}
6821
6822#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
6823#[serde(rename_all = "kebab-case")]
6824pub enum TimelineCategoryV1 {
6825 Lifecycle,
6826 Todo,
6827 Tool,
6828 Subagent,
6829 Interaction,
6830 Process,
6831 File,
6832 Usage,
6833 History,
6834}
6835
6836#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6837#[serde(deny_unknown_fields)]
6838pub struct RedactedTaskV1 {
6839 pub task_id: HarnessTaskId,
6840 pub revision: HarnessRevision,
6841 pub title: String,
6842 pub body: String,
6843 pub creator: TaskCreatorCategoryV1,
6844 pub parent_task_id: Option<HarnessTaskId>,
6845 pub dependency_ids: Vec<HarnessTaskId>,
6846 pub state: HarnessTaskStateV1,
6847 pub run_ids: Vec<HarnessRunId>,
6848 pub references_redacted: bool,
6849 pub result_refs: Vec<HarnessResultRef>,
6850 pub artifact_refs: Vec<HarnessArtifactRef>,
6851 pub created_at_unix_ms: u64,
6852 pub updated_at_unix_ms: u64,
6853}
6854
6855impl RedactedTaskV1 {
6856 pub fn validate(&self) -> Result<(), HarnessReadApiError> {
6857 self.task_id.validate().map_err(HarnessReadApiError::Protocol)?;
6858 self.revision.validate().map_err(HarnessReadApiError::Protocol)?;
6859 validate_text("title", &self.title, HARNESS_TITLE_MAX_BYTES, false)?;
6860 validate_text("body", &self.body, HARNESS_BODY_MAX_BYTES, true)?;
6861 if let Some(parent) = &self.parent_task_id {
6862 parent.validate().map_err(HarnessReadApiError::Protocol)?;
6863 }
6864 validate_bounded_ids_with_max(&self.dependency_ids, HARNESS_DEPENDENCIES_MAX)?;
6865 validate_bounded_ids_with_max(&self.run_ids, HARNESS_LINKS_MAX)?;
6866 validate_bounded_ids_with_max(&self.result_refs, HARNESS_RESULTS_MAX)?;
6867 validate_bounded_ids_with_max(&self.artifact_refs, HARNESS_ARTIFACTS_MAX)?;
6868 validate_timestamps(self.created_at_unix_ms, self.updated_at_unix_ms)
6869 }
6870}
6871
6872#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
6873#[serde(rename_all = "kebab-case")]
6874pub enum TaskCreatorCategoryV1 { User, ParentRun }
6875
6876#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
6877#[serde(rename_all = "kebab-case")]
6878pub enum RedactedWorktreeIntentV1 { Existing, Managed }
6879
6880#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6881#[serde(deny_unknown_fields)]
6882pub struct RedactedRunIntentV1 {
6883 pub mode: HarnessExecutionModeV1,
6884 pub worktree: RedactedWorktreeIntentV1,
6885 pub has_delivery_bundle: bool,
6886 pub has_continuation: bool,
6887}
6888
6889#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6890#[serde(deny_unknown_fields)]
6891pub struct RedactedRunV1 {
6892 pub run_id: HarnessRunId,
6893 pub revision: HarnessRevision,
6894 pub parent_run_id: Option<HarnessRunId>,
6895 pub task_id: Option<HarnessTaskId>,
6896 pub operation_id: Option<HarnessOperationId>,
6897 pub intent: RedactedRunIntentV1,
6898 pub lifecycle: HarnessRunLifecycleV1,
6899 pub binding: RedactedBindingStateV1,
6900 pub result_disposition: Option<HarnessResultDispositionV1>,
6901 pub failure_category: Option<HarnessFailureCategoryV1>,
6902 pub context_pack: Option<HarnessResolvedContextPackReceiptV1>,
6903 pub git_facts: Option<HarnessRunGitFactsV1>,
6904 #[serde(default)]
6909 pub delivery_receipt: Option<HarnessReceiptRef>,
6910 pub references_redacted: bool,
6911 pub created_at_unix_ms: u64,
6912 pub updated_at_unix_ms: u64,
6913}
6914
6915impl RedactedRunV1 {
6916 pub fn validate(&self) -> Result<(), HarnessReadApiError> {
6917 self.run_id.validate().map_err(HarnessReadApiError::Protocol)?;
6918 self.revision.validate().map_err(HarnessReadApiError::Protocol)?;
6919 if let Some(parent) = &self.parent_run_id {
6920 parent.validate().map_err(HarnessReadApiError::Protocol)?;
6921 }
6922 if let Some(task_id) = &self.task_id {
6923 task_id.validate().map_err(HarnessReadApiError::Protocol)?;
6924 }
6925 if let Some(operation_id) = &self.operation_id {
6926 operation_id.validate().map_err(HarnessReadApiError::Protocol)?;
6927 }
6928 if matches!(self.lifecycle, HarnessRunLifecycleV1::Running | HarnessRunLifecycleV1::Waiting)
6929 && self.binding == RedactedBindingStateV1::None
6930 {
6931 return Err(HarnessReadApiError::InvalidRunState);
6932 }
6933 if matches!(self.lifecycle, HarnessRunLifecycleV1::Failed) != self.failure_category.is_some() {
6934 return Err(HarnessReadApiError::InvalidRunState);
6935 }
6936 if let Some(pack) = &self.context_pack {
6937 pack.validate().map_err(HarnessReadApiError::Protocol)?;
6938 }
6939 if let Some(facts) = &self.git_facts {
6940 facts.validate().map_err(HarnessReadApiError::Protocol)?;
6941 }
6942 if let Some(receipt) = &self.delivery_receipt {
6943 receipt.validate().map_err(HarnessReadApiError::Protocol)?;
6944 }
6945 validate_timestamps(self.created_at_unix_ms, self.updated_at_unix_ms)
6946 }
6947}
6948
6949#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
6950#[serde(rename_all = "kebab-case")]
6951pub enum RedactedBindingStateV1 { None, ManagedDormant, ManagedActive, Inline }
6952
6953#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6964#[serde(deny_unknown_fields)]
6965pub struct HarnessOperationLedgerEntryV1 {
6966 pub operation_id: HarnessOperationId,
6967 pub created_at_unix_ms: u64,
6968 pub kind: HarnessOperationKindV1,
6969 pub actor: HarnessActorV1,
6970}
6971
6972impl HarnessOperationLedgerEntryV1 {
6973 pub fn validate(&self) -> Result<(), HarnessOperatorApiError> {
6974 self.operation_id.validate().map_err(HarnessOperatorApiError::Protocol)?;
6975 self.actor.validate().map_err(HarnessOperatorApiError::Protocol)?;
6976 if self.created_at_unix_ms == 0 {
6977 return Err(HarnessOperatorApiError::InvalidOperationLedger);
6978 }
6979 Ok(())
6980 }
6981}
6982
6983#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
6984#[serde(deny_unknown_fields)]
6985pub struct RedactedOperationV1 {
6986 pub operation_id: HarnessOperationId,
6987 pub revision: HarnessRevision,
6988 pub kind: HarnessOperationKindV1,
6989 pub state: HarnessOperationStateV1,
6990 pub task_id: Option<HarnessTaskId>,
6991 pub run_id: Option<HarnessRunId>,
6992 pub reconciles_operation_id: Option<HarnessOperationId>,
6993 pub references_redacted: bool,
6994 pub failure_category: Option<HarnessFailureCategoryV1>,
6995 pub outcome_unknown_reason: Option<HarnessOutcomeUnknownReasonV1>,
6996 pub reconciliation_outcome: Option<HarnessReconciliationOutcomeV1>,
6997 pub created_at_unix_ms: u64,
6998 pub updated_at_unix_ms: u64,
6999 pub dispatched_at_unix_ms: Option<u64>,
7000 pub finished_at_unix_ms: Option<u64>,
7001}
7002
7003impl RedactedOperationV1 {
7004 pub fn validate(&self) -> Result<(), HarnessReadApiError> {
7005 self.operation_id.validate().map_err(HarnessReadApiError::Protocol)?;
7006 self.revision.validate().map_err(HarnessReadApiError::Protocol)?;
7007 if let Some(task_id) = &self.task_id {
7008 task_id.validate().map_err(HarnessReadApiError::Protocol)?;
7009 }
7010 if let Some(run_id) = &self.run_id {
7011 run_id.validate().map_err(HarnessReadApiError::Protocol)?;
7012 }
7013 if let Some(operation_id) = &self.reconciles_operation_id {
7014 operation_id.validate().map_err(HarnessReadApiError::Protocol)?;
7015 }
7016 if (self.state == HarnessOperationStateV1::Failed) != self.failure_category.is_some()
7017 || (self.state == HarnessOperationStateV1::OutcomeUnknown)
7018 != self.outcome_unknown_reason.is_some()
7019 || (self.state == HarnessOperationStateV1::Reconciled)
7020 != self.reconciliation_outcome.is_some()
7021 {
7022 return Err(HarnessReadApiError::InvalidOperationState);
7023 }
7024 validate_timestamps(self.created_at_unix_ms, self.updated_at_unix_ms)?;
7025 for timestamp in [self.dispatched_at_unix_ms, self.finished_at_unix_ms].into_iter().flatten() {
7026 if timestamp < self.created_at_unix_ms || timestamp > self.updated_at_unix_ms {
7027 return Err(HarnessReadApiError::InvalidTimestamp);
7028 }
7029 }
7030 Ok(())
7031 }
7032}
7033
7034#[derive(Debug, Error)]
7035pub enum HarnessReadApiError {
7036 #[error(
7037 "build stamp mismatch: this side was built from tree {expected}, the peer from \
7038 tree {received} -- rebuild and restart the out-of-date side (hatchery-harness \
7039 and its hatchery-harness-client-based peers are built from the same tree and \
7040 must be rolled together)"
7041 )]
7042 BuildStampMismatch { expected: String, received: String },
7043 #[error("harness read credential is malformed")]
7044 MalformedCredential,
7045 #[error("harness read limit is outside the supported range")]
7046 InvalidLimit,
7047 #[error("harness read cursor is invalid")]
7048 InvalidCursor,
7049 #[error("harness read context has invalid allowed tools")]
7050 InvalidAllowedTools,
7051 #[error("harness monitor counts are inconsistent")]
7052 InvalidMonitorCounts,
7053 #[error("harness monitor detail is inconsistent with visibility or availability")]
7054 InvalidMonitorDetail,
7055 #[error("harness timeline entry is invalid")]
7056 InvalidTimelineEntry,
7057 #[error("harness read collection is not canonical or bounded")]
7058 InvalidCollection,
7059 #[error("harness read timestamp is invalid")]
7060 InvalidTimestamp,
7061 #[error("harness redacted run state is invalid")]
7062 InvalidRunState,
7063 #[error("harness redacted operation state is invalid")]
7064 InvalidOperationState,
7065 #[error("harness context task or sibling runs are inconsistent with the actor run")]
7066 InvalidContextTask,
7067 #[error("harness read text field is invalid: {0}")]
7068 InvalidText(&'static str),
7069 #[error("harness task create request is invalid")]
7070 InvalidTaskCreate,
7071 #[error("harness protocol value is invalid: {0}")]
7072 Protocol(#[from] hatchery_harness_protocol::HarnessValidationError),
7073}
7074
7075#[derive(Debug, Error)]
7076pub enum HarnessOperatorApiError {
7077 #[error(
7078 "build stamp mismatch: this side was built from tree {expected}, the peer from \
7079 tree {received} -- rebuild and restart the out-of-date side \
7080 (hatchery-harness and its hatchery-tui / hatchery-tui-light client are built \
7081 from the same tree and must be rolled together)"
7082 )]
7083 BuildStampMismatch { expected: String, received: String },
7084 #[error("harness operator credential is malformed")]
7085 MalformedCredential,
7086 #[error("harness operator limit is outside the supported range")]
7087 InvalidLimit,
7088 #[error("harness operator cursor is invalid")]
7089 InvalidCursor,
7090 #[error("harness operator request reference is malformed")]
7091 MalformedRequestRef,
7092 #[error("harness operator submission timestamp is invalid")]
7093 InvalidSubmittedAt,
7094 #[error("harness runtime inventory is invalid")]
7095 InvalidRuntimeInventory,
7096 #[error("harness run correlation is invalid")]
7097 InvalidRunCorrelation,
7098 #[error("harness run transfer summary is invalid")]
7099 InvalidRunTransfer,
7100 #[error("harness reverse attribution is invalid")]
7101 InvalidReverseAttribution,
7102 #[error("harness run context source observation is invalid")]
7103 InvalidRunContextSourceObservation,
7104 #[error("harness run workspace origin is invalid")]
7105 InvalidWorkspaceOrigin,
7106 #[error("harness repository-relative path is invalid")]
7107 InvalidRepositoryPath,
7108 #[error("harness run workspace tree is invalid")]
7109 InvalidWorkspaceTree,
7110 #[error("harness run workspace file is invalid")]
7111 InvalidWorkspaceFile,
7112 #[error("harness git status is invalid")]
7113 InvalidGitStatus,
7114 #[error("harness git summary is invalid")]
7115 InvalidGitSummary,
7116 #[error("harness git object id is invalid")]
7117 InvalidGitObjectId,
7118 #[error("harness git history is invalid")]
7119 InvalidGitHistory,
7120 #[error("harness git diff is invalid")]
7121 InvalidGitDiff,
7122 #[error("harness launch plan catalog is invalid")]
7123 InvalidLaunchPlans,
7124 #[error("harness task launch options are invalid")]
7125 InvalidTaskLaunchOptions,
7126 #[error("harness reviewed task launch selection is invalid")]
7127 InvalidTaskLaunchSelection,
7128 #[error("harness native history value is invalid")]
7129 InvalidNativeHistory,
7130 #[error("harness terminal page is invalid")]
7131 InvalidTerminalPage,
7132 #[error("harness session spawn request is invalid")]
7133 InvalidSessionSpawn,
7134 #[error("harness session control request is invalid")]
7135 InvalidSessionControl,
7136 #[error("harness agent stream content is invalid")]
7137 InvalidAgentStream,
7138 #[error("harness session record request is invalid")]
7139 InvalidSessionRecordRequest,
7140 #[error("harness host path is invalid")]
7141 InvalidHostPath,
7142 #[error("harness host directory browse request is invalid")]
7143 InvalidHostDirectoryBrowseRequest,
7144 #[error("harness workspace or worktree resource mutation request is invalid")]
7145 InvalidResourceMutationRequest,
7146 #[error("harness task operations ledger entry is invalid")]
7147 InvalidOperationLedger,
7148 #[error("harness operator response is invalid")]
7149 Read(#[source] HarnessReadApiError),
7150 #[error("harness protocol value is invalid: {0}")]
7151 Protocol(#[source] hatchery_harness_protocol::HarnessValidationError),
7152}
7153
7154fn validate_credential(value: &str) -> Result<(), HarnessReadApiError> {
7155 if value.len() > HARNESS_READ_CREDENTIAL_MAX_BYTES || !value.starts_with(TOKEN_PREFIX) {
7156 return Err(HarnessReadApiError::MalformedCredential);
7157 }
7158 let (payload, proof) = value[TOKEN_PREFIX.len()..]
7159 .split_once('.')
7160 .ok_or(HarnessReadApiError::MalformedCredential)?;
7161 if payload.is_empty()
7162 || payload.len() % 2 != 0
7163 || proof.len() != 64
7164 || !payload.bytes().chain(proof.bytes()).all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
7165 {
7166 return Err(HarnessReadApiError::MalformedCredential);
7167 }
7168 Ok(())
7169}
7170
7171fn validate_operator_credential(value: &str) -> Result<(), HarnessOperatorApiError> {
7172 let payload = value.strip_prefix(OPERATOR_TOKEN_PREFIX)
7173 .ok_or(HarnessOperatorApiError::MalformedCredential)?;
7174 if value.len() > HARNESS_OPERATOR_CREDENTIAL_MAX_BYTES
7175 || payload.len() != 64
7176 || !payload.bytes().all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
7177 {
7178 return Err(HarnessOperatorApiError::MalformedCredential);
7179 }
7180 Ok(())
7181}
7182
7183fn validate_operator_request_ref(value: &str) -> Result<(), HarnessOperatorApiError> {
7184 let payload = value.strip_prefix(OPERATOR_REQUEST_REF_PREFIX)
7185 .ok_or(HarnessOperatorApiError::MalformedRequestRef)?;
7186 if payload.len() != 24
7187 || !payload.bytes().all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
7188 {
7189 return Err(HarnessOperatorApiError::MalformedRequestRef);
7190 }
7191 Ok(())
7192}
7193
7194fn valid_runtime_id(value: &str, maximum: usize) -> bool {
7195 !value.is_empty()
7196 && value.len() <= maximum
7197 && value.is_ascii()
7198 && value.bytes().all(|byte| {
7199 byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'@' | b'+')
7200 })
7201}
7202
7203fn is_lower_hex(byte: u8) -> bool {
7204 byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')
7205}
7206
7207fn valid_sha256_digest(value: &str) -> bool {
7208 value.strip_prefix("sha256:")
7209 .is_some_and(|digest| digest.len() == 64 && digest.bytes().all(is_lower_hex))
7210}
7211
7212fn validate_node_workspace_route(
7213 node_id: &str,
7214 workspace_id: &str,
7215) -> Result<(), HarnessOperatorApiError> {
7216 if !valid_runtime_id(node_id, 128) || !valid_runtime_id(workspace_id, 128) {
7217 return Err(HarnessOperatorApiError::InvalidWorkspaceOrigin);
7218 }
7219 Ok(())
7220}
7221
7222fn validate_repository_path(value: &str) -> Result<(), HarnessOperatorApiError> {
7223 if value.is_empty()
7224 || value.len() > HARNESS_REPOSITORY_PATH_MAX_BYTES
7225 || value.starts_with('/')
7226 || value.contains('\\')
7227 || value.contains(':')
7228 || value.chars().any(char::is_control)
7229 || value.split('/').any(|component| {
7230 component.is_empty() || matches!(component, "." | "..")
7231 })
7232 {
7233 return Err(HarnessOperatorApiError::InvalidRepositoryPath);
7234 }
7235 Ok(())
7236}
7237
7238fn validate_git_object_id(value: &str) -> Result<(), HarnessOperatorApiError> {
7239 if !matches!(value.len(), 40 | 64) || !value.bytes().all(is_lower_hex) {
7240 return Err(HarnessOperatorApiError::InvalidGitObjectId);
7241 }
7242 Ok(())
7243}
7244
7245fn validate_git_single_line(
7246 value: &str,
7247 maximum: usize,
7248 required: bool,
7249) -> Result<(), HarnessOperatorApiError> {
7250 if value.len() > maximum
7251 || required && value.is_empty()
7252 || value.chars().any(char::is_control)
7253 {
7254 return Err(HarnessOperatorApiError::InvalidGitSummary);
7255 }
7256 Ok(())
7257}
7258
7259fn validate_native_session_catalog_limit(limit: u16) -> Result<(), HarnessOperatorApiError> {
7260 if !(1..=HARNESS_NATIVE_SESSION_CATALOG_LIMIT_MAX).contains(&limit) {
7261 return Err(HarnessOperatorApiError::InvalidLimit);
7262 }
7263 Ok(())
7264}
7265
7266fn valid_native_selection_id(value: &str) -> bool {
7267 !value.trim().is_empty()
7268 && value.len() <= 1_024
7269 && value.bytes().all(|byte| {
7270 byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')
7271 })
7272}
7273
7274fn valid_native_group_id(value: &str) -> bool {
7275 !value.trim().is_empty()
7276 && value.len() <= 128
7277 && value.bytes().all(|byte| {
7278 byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')
7279 })
7280}
7281
7282fn valid_native_single_line(value: &str, maximum: usize, required: bool) -> bool {
7283 (!required || !value.trim().is_empty())
7284 && value.len() <= maximum
7285 && !value.chars().any(char::is_control)
7286}
7287
7288fn contains_unsafe_control_bytes(value: &str) -> bool {
7297 value.chars().any(|character| {
7298 character.is_control() && !matches!(character, '\n' | '\r' | '\t')
7299 })
7300}
7301
7302fn valid_agent_stream_id(value: &str) -> bool {
7310 !value.is_empty()
7311 && value.len() <= HARNESS_AGENT_STREAM_ID_MAX_BYTES
7312 && !value.chars().any(char::is_control)
7313}
7314
7315fn valid_agent_stream_correlation_id(value: &str) -> bool {
7324 !value.is_empty()
7325 && value.len() <= HARNESS_OBSERVATION_LABEL_MAX_BYTES
7326 && !value.chars().any(char::is_control)
7327}
7328
7329fn validate_native_entries(
7330 route: &HarnessNativeSessionRouteV1,
7331 entries: &[HarnessNativeSessionCatalogEntryV1],
7332) -> Result<(), HarnessOperatorApiError> {
7333 if entries.len() > usize::from(HARNESS_NATIVE_SESSION_CATALOG_LIMIT_MAX) {
7334 return Err(HarnessOperatorApiError::InvalidNativeHistory);
7335 }
7336 for (index, entry) in entries.iter().enumerate() {
7337 entry.validate_for_route(route)?;
7338 if entries[..index].iter().any(|existing| {
7339 existing.selection_id == entry.selection_id
7340 || entry.record_id.is_some() && existing.record_id == entry.record_id
7341 }) {
7342 return Err(HarnessOperatorApiError::InvalidNativeHistory);
7343 }
7344 }
7345 Ok(())
7346}
7347
7348fn validate_operator_limit(limit: u16) -> Result<(), HarnessOperatorApiError> {
7349 if !(1..=HARNESS_ENTITY_PAGE_LIMIT_MAX).contains(&limit) {
7350 return Err(HarnessOperatorApiError::InvalidLimit);
7351 }
7352 Ok(())
7353}
7354
7355fn validate_operator_timeline_limit(limit: u16) -> Result<(), HarnessOperatorApiError> {
7356 if !(1..=HARNESS_TIMELINE_PAGE_LIMIT_MAX).contains(&limit) {
7357 return Err(HarnessOperatorApiError::InvalidLimit);
7358 }
7359 Ok(())
7360}
7361
7362fn validate_operator_terminal_limit(limit: u16) -> Result<(), HarnessOperatorApiError> {
7363 if !(1..=HARNESS_TERMINAL_PAGE_LIMIT_MAX).contains(&limit) {
7364 return Err(HarnessOperatorApiError::InvalidLimit);
7365 }
7366 Ok(())
7367}
7368
7369fn validate_limit(limit: u16, maximum: u16) -> Result<(), HarnessReadApiError> {
7370 if !(1..=maximum).contains(&limit) {
7371 return Err(HarnessReadApiError::InvalidLimit);
7372 }
7373 Ok(())
7374}
7375
7376fn validate_bounded<T>(
7377 values: &[T],
7378 maximum: u16,
7379 validate: impl Fn(&T) -> Result<(), HarnessReadApiError>,
7380) -> Result<(), HarnessReadApiError> {
7381 if values.len() > maximum as usize {
7382 return Err(HarnessReadApiError::InvalidCollection);
7383 }
7384 for value in values { validate(value)?; }
7385 Ok(())
7386}
7387
7388fn validate_bounded_ids_with_max<T: Ord>(
7389 values: &[T],
7390 maximum: usize,
7391) -> Result<(), HarnessReadApiError> {
7392 if values.len() > maximum
7393 || values.windows(2).any(|pair| pair[0] >= pair[1])
7394 {
7395 return Err(HarnessReadApiError::InvalidCollection);
7396 }
7397 Ok(())
7398}
7399
7400fn validate_monitor_facts<T>(values: &[T]) -> Result<(), HarnessReadApiError> {
7401 if values.len() > HARNESS_MONITOR_FACTS_MAX {
7402 return Err(HarnessReadApiError::InvalidCollection);
7403 }
7404 Ok(())
7405}
7406
7407fn validate_optional_observation_text(
7408 field: &'static str,
7409 value: Option<&str>,
7410 maximum: usize,
7411) -> Result<(), HarnessReadApiError> {
7412 let Some(value) = value else { return Ok(()); };
7413 if value.is_empty() || value.len() > maximum || value.chars().any(char::is_control) {
7414 return Err(HarnessReadApiError::InvalidText(field));
7415 }
7416 Ok(())
7417}
7418
7419fn validate_observation_correlation(value: Option<u16>) -> Result<(), HarnessReadApiError> {
7420 if value.is_some_and(|value| value == 0 || usize::from(value) > HARNESS_MONITOR_FACTS_MAX) {
7421 return Err(HarnessReadApiError::InvalidTimelineEntry);
7422 }
7423 Ok(())
7424}
7425
7426fn validate_observation_relative_path(path: &str) -> Result<(), HarnessReadApiError> {
7427 if path.is_empty()
7428 || path.len() > HARNESS_OBSERVATION_PATH_MAX_BYTES
7429 || path.starts_with('/')
7430 || path.contains('\\')
7431 || path.contains(':')
7432 || path.chars().any(char::is_control)
7433 || path.split('/').any(|component| {
7434 component.is_empty() || matches!(component, "." | "..")
7435 })
7436 {
7437 return Err(HarnessReadApiError::InvalidText("relative path"));
7438 }
7439 Ok(())
7440}
7441
7442fn require_observed_or_empty<T>(
7443 state: FeatureObservationStateV1,
7444 values: &[T],
7445) -> Result<(), HarnessReadApiError> {
7446 if state != FeatureObservationStateV1::Observed && !values.is_empty() {
7447 return Err(HarnessReadApiError::InvalidMonitorDetail);
7448 }
7449 Ok(())
7450}
7451
7452fn validate_feature_counts(monitor: &SessionMonitorV1) -> Result<(), HarnessReadApiError> {
7453 if monitor.features.todo != FeatureObservationStateV1::Observed
7454 && (monitor.todo_total != 0 || monitor.todo_completed != 0)
7455 || monitor.features.tools != FeatureObservationStateV1::Observed
7456 && monitor.active_tools != 0
7457 || monitor.features.subagents != FeatureObservationStateV1::Observed
7458 && monitor.active_subagents != 0
7459 || monitor.features.interactions != FeatureObservationStateV1::Observed
7460 && monitor.active_interactions != 0
7461 || monitor.features.owned_processes != FeatureObservationStateV1::Observed
7462 && monitor.active_processes != 0
7463 || monitor.features.usage != FeatureObservationStateV1::Observed
7464 && (monitor.input_tokens != 0
7465 || monitor.output_tokens != 0
7466 || monitor.cache_read_tokens != 0
7467 || monitor.cache_write_tokens != 0
7468 || monitor.reasoning_tokens != 0
7469 || monitor.context_window_tokens.is_some())
7470 {
7471 return Err(HarnessReadApiError::InvalidMonitorDetail);
7472 }
7473 if matches!(monitor.availability, ProjectionAvailabilityV1::Unknown | ProjectionAvailabilityV1::NotObserved)
7474 && [
7475 monitor.features.todo,
7476 monitor.features.tools,
7477 monitor.features.subagents,
7478 monitor.features.interactions,
7479 monitor.features.owned_processes,
7480 monitor.features.files,
7481 monitor.features.usage,
7482 monitor.features.history,
7483 ]
7484 .contains(&FeatureObservationStateV1::Observed)
7485 {
7486 return Err(HarnessReadApiError::InvalidMonitorDetail);
7487 }
7488 Ok(())
7489}
7490
7491fn validate_text(
7492 field: &'static str,
7493 value: &str,
7494 maximum: usize,
7495 allow_empty: bool,
7496) -> Result<(), HarnessReadApiError> {
7497 if value.len() > maximum || !allow_empty && value.trim().is_empty() || value.contains('\0') {
7498 return Err(HarnessReadApiError::InvalidText(field));
7499 }
7500 Ok(())
7501}
7502
7503fn validate_timestamps(created: u64, updated: u64) -> Result<(), HarnessReadApiError> {
7504 if created == 0 || updated < created {
7505 return Err(HarnessReadApiError::InvalidTimestamp);
7506 }
7507 Ok(())
7508}
7509
7510fn valid_transfer_timestamps<const N: usize>(
7511 created: u64,
7512 updated: u64,
7513 optional: [Option<u64>; N],
7514) -> bool {
7515 created != 0
7516 && updated >= created
7517 && optional.into_iter().flatten().all(|timestamp| {
7518 timestamp >= created && timestamp <= updated
7519 })
7520}
7521
7522#[cfg(test)]
7523mod tests {
7524 use super::*;
7525
7526 fn managed_run_correlation() -> HarnessRunCorrelationV1 {
7527 HarnessRunCorrelationV1 {
7528 run_id: HarnessRunId::new(format!("hrun_{}", "a".repeat(24))).unwrap(),
7529 run_revision: HarnessRevision::new(7).unwrap(),
7530 task_id: HarnessTaskId::new(format!("htask_{}", "b".repeat(24))).unwrap(),
7531 node_id: HarnessSelectorV1::new("node-a").unwrap(),
7532 node_incarnation_id: HarnessNodeIncarnationV1::new("07".repeat(16)).unwrap(),
7533 workspace_id: HarnessSelectorV1::new("workspace-a").unwrap(),
7534 provider_profile: HarnessSelectorV1::new("codex-default").unwrap(),
7535 mode: HarnessExecutionModeV1::Pty,
7536 worktree: HarnessRunWorktreeViewV1::Managed {
7537 worktree_ref: HarnessSelectorV1::new("worktree-a").unwrap(),
7538 },
7539 session: HarnessRunSessionViewV1::Managed(HarnessManagedRunSessionV1 {
7540 record_id: HarnessSelectorV1::new("record-a").unwrap(),
7541 active_session: Some(HarnessRuntimeIdentityV1 {
7542 instance_id: 41,
7543 generation: 3,
7544 }),
7545 }),
7546 availability: HarnessRunCorrelationAvailabilityV1::Available,
7547 observed_at_unix_ms: Some(100),
7548 }
7549 }
7550
7551 fn run_transfer_summary() -> HarnessRunTransferSummaryV1 {
7552 let run_id = HarnessRunId::new(format!("hrun_{}", "a".repeat(24))).unwrap();
7553 HarnessRunTransferSummaryV1 {
7554 run_id: run_id.clone(),
7555 run_revision: HarnessRevision::new(9).unwrap(),
7556 delivery: Some(HarnessRunDeliveryTransferV1 {
7557 delivery_ref: HarnessDeliveryRef::new(format!(
7558 "hdelivery_{}",
7559 "b".repeat(24),
7560 )).unwrap(),
7561 revision: HarnessRevision::new(3).unwrap(),
7562 state: HarnessDeliveryStateV1::Committed,
7563 selector: HarnessSelectorV1::new("delivery-safe").unwrap(),
7564 bundle_id: HarnessDeliveryBundleIdV1::new("bundle-safe").unwrap(),
7565 bundle_revision: HarnessDeliveryBundleRevisionV1::new("revision-4").unwrap(),
7566 bundle_digest: HarnessDeliveryBundleDigestV1::new(format!(
7567 "sha256:{}",
7568 "c".repeat(64),
7569 )).unwrap(),
7570 manifest_digest: HarnessDeliveryManifestDigestV2::new(format!(
7571 "sha256:{}",
7572 "d".repeat(64),
7573 )).unwrap(),
7574 receipt_ref: Some(HarnessReceiptRef::new(format!(
7575 "hreceipt_{}",
7576 "e".repeat(24),
7577 )).unwrap()),
7578 created_at_unix_ms: 10,
7579 updated_at_unix_ms: 30,
7580 staged_at_unix_ms: Some(20),
7581 committed_at_unix_ms: Some(30),
7582 }),
7583 continuation: Some(HarnessRunContinuationTransferV1 {
7584 continuation_ref: HarnessContinuationRef::new(format!(
7585 "hcontinuation_{}",
7586 "f".repeat(24),
7587 )).unwrap(),
7588 receipt_ref: HarnessReceiptRef::new(format!(
7589 "hreceipt_{}",
7590 "0".repeat(24),
7591 )).unwrap(),
7592 revision: HarnessRevision::new(4).unwrap(),
7593 state: HarnessContinuationStateV1::Bound,
7594 source_run_id: HarnessRunId::new(format!(
7595 "hrun_{}",
7596 "1".repeat(24),
7597 )).unwrap(),
7598 target_run_id: run_id,
7599 source_provider: HarnessSelectorV1::new("claude").unwrap(),
7600 context: Some(HarnessRunContextTransferV1 {
7601 context_ref: HarnessSelectorV1::new("context-safe").unwrap(),
7602 digest: format!("sha256:{}", "2".repeat(64)),
7603 source_message_count: 12,
7604 retained_message_count: 8,
7605 byte_len: 4096,
7606 truncated: true,
7607 }),
7608 prepared_at_unix_ms: 10,
7609 exporting_at_unix_ms: Some(15),
7610 exported_at_unix_ms: Some(20),
7611 bound_at_unix_ms: Some(25),
7612 expired_at_unix_ms: None,
7613 outcome_unknown_at_unix_ms: None,
7614 outcome_unknown_reason: None,
7615 created_at_unix_ms: 10,
7616 updated_at_unix_ms: 25,
7617 }),
7618 }
7619 }
7620
7621 fn run_workspace_origin() -> HarnessRunWorkspaceOriginV1 {
7622 HarnessRunWorkspaceOriginV1 {
7623 run_id: HarnessRunId::new(format!("hrun_{}", "a".repeat(24))).unwrap(),
7624 run_revision: HarnessRevision::new(7).unwrap(),
7625 node_id: HarnessSelectorV1::new("node-a").unwrap(),
7626 node_incarnation_id: HarnessNodeIncarnationV1::new("07".repeat(16)).unwrap(),
7627 workspace_id: HarnessSelectorV1::new("workspace-a").unwrap(),
7628 }
7629 }
7630
7631 fn node_workspace_origin() -> HarnessNodeWorkspaceOriginV1 {
7632 HarnessNodeWorkspaceOriginV1 {
7633 node_id: "node-a".to_owned(),
7634 node_incarnation_id: "07".repeat(16),
7635 workspace_id: "workspace-a".to_owned(),
7636 }
7637 }
7638
7639 fn reverse_attribution_workspace() -> HarnessReverseAttributionWorkspaceV1 {
7640 HarnessReverseAttributionWorkspaceV1 {
7641 node_id: HarnessSelectorV1::new("node-a").unwrap(),
7642 node_incarnation_id: HarnessNodeIncarnationV1::new("07".repeat(16)).unwrap(),
7643 workspace_id: HarnessSelectorV1::new("workspace-a").unwrap(),
7644 }
7645 }
7646
7647 fn reverse_attribution_link(run_byte: char) -> HarnessReverseAttributionLinkV1 {
7648 HarnessReverseAttributionLinkV1 {
7649 task_id: HarnessTaskId::new(format!("htask_{}", "1".repeat(24))).unwrap(),
7650 run_id: HarnessRunId::new(format!("hrun_{}", run_byte.to_string().repeat(24)))
7651 .unwrap(),
7652 run_revision: HarnessRevision::new(3).unwrap(),
7653 binding: HarnessReverseAttributionBindingV1::Workspace {
7654 workspace: reverse_attribution_workspace(),
7655 },
7656 relation: HarnessReverseAttributionRelationV1::WorkspaceScope,
7657 }
7658 }
7659
7660 fn git_commit(id_byte: char) -> HarnessGitCommitV1 {
7661 HarnessGitCommitV1 {
7662 id: HarnessGitObjectIdV1::new(id_byte.to_string().repeat(40)).unwrap(),
7663 parents: Vec::new(),
7664 subject: "Bounded subject".to_owned(),
7665 author_name: "Author".to_owned(),
7666 authored_at: "2026-08-16T10:00:00Z".to_owned(),
7667 committer_name: "Committer".to_owned(),
7668 committed_at: "2026-08-16T10:00:00Z".to_owned(),
7669 signature_status: HarnessGitSignatureStatusV1::NoSignature,
7670 signer: None,
7671 }
7672 }
7673
7674 fn assert_json_has_no_forbidden_keys(encoded: &str, forbidden: &[&str]) {
7675 fn visit(value: &serde_json::Value, forbidden: &[&str]) {
7676 match value {
7677 serde_json::Value::Object(fields) => {
7678 for (field, value) in fields {
7679 assert!(
7680 !forbidden.iter().any(|forbidden| field == forbidden),
7681 "response exposed structural field {field}",
7682 );
7683 visit(value, forbidden);
7684 }
7685 }
7686 serde_json::Value::Array(values) => {
7687 for value in values { visit(value, forbidden); }
7688 }
7689 _ => {}
7690 }
7691 }
7692
7693 let value: serde_json::Value = serde_json::from_str(encoded).unwrap();
7694 visit(&value, forbidden);
7695 }
7696
7697 #[test]
7698 fn credential_debug_and_errors_never_expose_secret() {
7699 let credential = HarnessReadCredential::parse(format!("g4ah2_aa.{}", "0".repeat(64)))
7700 .expect("credential");
7701 assert_eq!(format!("{credential:?}"), "HarnessReadCredential([REDACTED])");
7702 assert!(!HarnessReadApiError::MalformedCredential.to_string().contains(credential.expose()));
7703 }
7704
7705 #[test]
7706 fn operator_and_agent_credentials_are_strictly_separated_and_redacted() {
7707 let operator = HarnessOperatorCredential::parse(format!(
7708 "g4aho_{}",
7709 "a".repeat(64),
7710 )).expect("operator credential");
7711 let read = format!("g4ah2_aa.{}", "0".repeat(64));
7712 assert!(HarnessOperatorCredential::parse(&read).is_err());
7713 assert!(HarnessReadCredential::parse(operator.expose()).is_err());
7714 assert_eq!(
7715 format!("{operator:?}"),
7716 "HarnessOperatorCredential([REDACTED])",
7717 );
7718 assert!(!HarnessOperatorApiError::MalformedCredential
7719 .to_string().contains(operator.expose()));
7720 }
7721
7722 #[test]
7723 fn operator_frames_enforce_exact_cursor_and_collection_bounds() {
7724 assert!(HarnessOperatorRequestV1::TasksList {
7725 after_task_id: None,
7726 state: None,
7727 parent_task_id: None,
7728 limit: HARNESS_ENTITY_PAGE_LIMIT_MAX,
7729 }.validate().is_ok());
7730 assert!(HarnessOperatorRequestV1::TasksList {
7731 after_task_id: None,
7732 state: None,
7733 parent_task_id: None,
7734 limit: HARNESS_ENTITY_PAGE_LIMIT_MAX + 1,
7735 }.validate().is_err());
7736 let run_id = HarnessRunId::new(format!("hrun_{}", "a".repeat(24))).unwrap();
7737 assert!(HarnessOperatorRequestV1::TimelineRead {
7738 run_id,
7739 after_sequence: Some(0),
7740 limit: 1,
7741 }.validate().is_err());
7742 }
7743
7744 #[test]
7745 fn legacy_raw_schedule_variant_json_is_rejected() {
7746 let legacy = r#"{"kind":"schedule-ready-task","request":{}}"#;
7747 assert!(serde_json::from_str::<HarnessOperatorRequestV1>(legacy).is_err());
7748 }
7749
7750 #[test]
7751 fn operator_intent_is_authority_free() {
7752 let credential = HarnessOperatorCredential::parse(format!(
7753 "g4aho_{}",
7754 "a".repeat(64),
7755 )).unwrap();
7756 let request = HarnessOperatorRequestV1::SubmitIntent {
7757 intent: HarnessOperatorIntentV1 {
7758 request_ref: HarnessOperatorRequestRefV1::new(format!(
7759 "hireq_{}",
7760 "1".repeat(24),
7761 )).unwrap(),
7762 submitted_at_unix_ms: 10,
7763 action: HarnessOperatorActionV1::CreateTask {
7764 title: "Harness-owned identity".to_owned(),
7765 body: "Typed user intent".to_owned(),
7766 parent_task_id: None,
7767 dependencies: Vec::new(),
7768 initial_state: HarnessTaskStateV1::Backlog,
7769 },
7770 },
7771 };
7772 let encoded_request = serde_json::to_string(&request).unwrap();
7773 assert!(!encoded_request.contains("authority"));
7774 assert!(!encoded_request.contains("operation_id"));
7775 assert!(!encoded_request.contains("idempotency_ref"));
7776 assert!(!encoded_request.contains("\"task_id\":"));
7777 let envelope = HarnessOperatorEnvelopeV1 {
7778 build_stamp: BUILD_STAMP.to_string(),
7779 credential,
7780 request,
7781 };
7782 assert!(envelope.validate().is_ok());
7783 let decoded: HarnessOperatorEnvelopeV1 = serde_json::from_slice(
7784 &serde_json::to_vec(&envelope).unwrap(),
7785 ).unwrap();
7786 assert_eq!(decoded, envelope);
7787 }
7788
7789 #[test]
7790 fn operator_run_correlation_is_exact() {
7791 let credential = HarnessOperatorCredential::parse(format!(
7792 "g4aho_{}",
7793 "a".repeat(64),
7794 )).unwrap();
7795 let request = HarnessOperatorRequestV1::RunCorrelationGet {
7796 run_id: HarnessRunId::new(format!("hrun_{}", "c".repeat(24))).unwrap(),
7797 };
7798 let envelope = HarnessOperatorEnvelopeV1 {
7799 build_stamp: BUILD_STAMP.to_string(),
7800 credential,
7801 request,
7802 };
7803 envelope.validate().unwrap();
7804 let decoded: HarnessOperatorEnvelopeV1 = serde_json::from_slice(
7805 &serde_json::to_vec(&envelope).unwrap(),
7806 ).unwrap();
7807 assert_eq!(decoded, envelope);
7808 }
7809
7810 #[test]
7811 fn operator_run_transfer_is_exact_private() {
7812 let credential = HarnessOperatorCredential::parse(format!(
7813 "g4aho_{}",
7814 "a".repeat(64),
7815 )).unwrap();
7816 let summary = run_transfer_summary();
7817 summary.validate().unwrap();
7818 let request = HarnessOperatorRequestV1::RunTransferGet {
7819 run_id: summary.run_id.clone(),
7820 };
7821 HarnessOperatorEnvelopeV1 {
7822 build_stamp: BUILD_STAMP.to_string(),
7823 credential,
7824 request,
7825 }.validate().unwrap();
7826
7827 let response = HarnessOperatorResponseV1::RunTransfer(summary.clone());
7828 response.validate().unwrap();
7829 let encoded = serde_json::to_string(&response).unwrap();
7830 let decoded: HarnessOperatorResponseV1 = serde_json::from_str(&encoded).unwrap();
7831 assert_eq!(decoded, response);
7832 for forbidden in [
7833 "provider_home",
7834 "provider_native",
7835 "session_id",
7836 "source_root",
7837 "component_path",
7838 "component_name",
7839 "blob_bytes",
7840 "prompt",
7841 "node_store",
7842 "credential",
7843 "auth",
7844 "C:\\\\",
7845 ] {
7846 assert!(!encoded.contains(forbidden), "response exposed {forbidden}");
7847 }
7848
7849 let mut wrong_target = summary.clone();
7850 wrong_target.continuation.as_mut().unwrap().target_run_id =
7851 HarnessRunId::new(format!("hrun_{}", "9".repeat(24))).unwrap();
7852 assert!(matches!(
7853 wrong_target.validate(),
7854 Err(HarnessOperatorApiError::InvalidRunTransfer),
7855 ));
7856
7857 let mut invalid_delivery = summary.clone();
7858 invalid_delivery.delivery.as_mut().unwrap().committed_at_unix_ms = None;
7859 assert!(matches!(
7860 invalid_delivery.validate(),
7861 Err(HarnessOperatorApiError::InvalidRunTransfer),
7862 ));
7863
7864 let mut invalid_context = summary;
7865 invalid_context.continuation.as_mut().unwrap()
7866 .context.as_mut().unwrap().truncated = false;
7867 assert!(matches!(
7868 invalid_context.validate(),
7869 Err(HarnessOperatorApiError::InvalidRunTransfer),
7870 ));
7871 }
7872
7873 fn ordinary_launch_plan(plan_id: &str, digest: char) -> HarnessLaunchPlanSummaryV1 {
7874 HarnessLaunchPlanSummaryV1 {
7875 scheduled_launch: HarnessScheduledLaunchRefV2 {
7876 plan: HarnessLaunchPlanRefV1 {
7877 plan_id: HarnessSelectorV1::new(plan_id).unwrap(),
7878 revision: HarnessRevision::new(2).unwrap(),
7879 digest: HarnessRequestDigest::new(digest.to_string().repeat(64)).unwrap(),
7880 },
7881 authority: HarnessLaunchAuthorityRefV1::OrdinaryOperator,
7882 },
7883 node_id: HarnessSelectorV1::new("node-a").unwrap(),
7884 workspace_id: HarnessSelectorV1::new("workspace-a").unwrap(),
7885 worktree: HarnessWorktreeIntentV1::Managed {
7886 worktree_ref: HarnessSelectorV1::new("worktree-a").unwrap(),
7887 },
7888 provider_profile: HarnessSelectorV1::new("codex-default").unwrap(),
7889 provider_id: HarnessSelectorV1::new("codex").unwrap(),
7890 mode: HarnessExecutionModeV1::Pty,
7891 }
7892 }
7893
7894 fn task_launch_options() -> HarnessTaskLaunchOptionsV1 {
7895 let task_id = HarnessTaskId::new(format!("htask_{}", "b".repeat(24))).unwrap();
7896 let legacy_plan = ordinary_launch_plan("plan-a", 'a');
7897 let plan = HarnessOrdinaryLaunchPlanOptionV1 {
7898 plan: legacy_plan.scheduled_launch.plan,
7899 node_id: legacy_plan.node_id,
7900 source_workspace_id: legacy_plan.workspace_id,
7901 provider_profile: legacy_plan.provider_profile,
7902 provider_id: legacy_plan.provider_id,
7903 mode: legacy_plan.mode,
7904 };
7905 let context = HarnessContextSourceSelectionV1 {
7906 source_run_id: HarnessRunId::new(format!("hrun_{}", "c".repeat(24))).unwrap(),
7907 source_run_revision: HarnessRevision::new(7).unwrap(),
7908 observed_at_unix_ms: 90,
7909 metadata_digest: HarnessRequestDigest::new("d".repeat(64)).unwrap(),
7910 node_id: HarnessSelectorV1::new("node-a").unwrap(),
7911 node_incarnation: HarnessSelectorV1::new("07".repeat(16)).unwrap(),
7912 workspace_id: HarnessSelectorV1::new("source-workspace").unwrap(),
7913 session_record_id: HarnessSelectorV1::new("record-a").unwrap(),
7914 active_session: Some(HarnessRuntimeIdentityV1 { instance_id: 41, generation: 3 }),
7915 message_count: 12,
7916 message_count_exact: true,
7917 completed_turn_count: Some(5),
7918 total_tokens: Some(4096),
7919 availability: HarnessContextSourceAvailabilityV1::Live,
7920 context_pack: None,
7921 };
7922 let delivery = HarnessDeliveryBundleSelectionV1 {
7923 bundle: HarnessDeliveryBundleV1 {
7924 selector: HarnessSelectorV1::new("review-kit").unwrap(),
7925 bundle_id: HarnessDeliveryBundleIdV1::new("bundle.review-kit").unwrap(),
7926 revision: HarnessDeliveryBundleRevisionV1::new("revision-7").unwrap(),
7927 digest: HarnessDeliveryBundleDigestV1::new(format!(
7928 "sha256:{}",
7929 "e".repeat(64),
7930 )).unwrap(),
7931 manifest_digest: HarnessDeliveryManifestDigestV2::new(format!(
7932 "sha256:{}",
7933 "f".repeat(64),
7934 )).unwrap(),
7935 },
7936 component_counts: vec![HarnessDeliveryComponentCountV1 {
7937 kind: HarnessDeliveryComponentKindV1::Skill,
7938 workspace_count: 2,
7939 session_count: 1,
7940 }],
7941 };
7942 let issuance = HarnessTaskLaunchIssuanceRefV1 {
7943 issuance_id: HarnessTaskLaunchIssuanceId::new(format!(
7944 "hissue_{}",
7945 "1".repeat(24),
7946 )).unwrap(),
7947 revision: HarnessRevision::new(2).unwrap(),
7948 digest: HarnessRequestDigest::new("2".repeat(64)).unwrap(),
7949 };
7950 HarnessTaskLaunchOptionsV1 {
7951 task_id: task_id.clone(),
7952 task_revision: HarnessRevision::new(5).unwrap(),
7953 policy_digest: HarnessRequestDigest::new("3".repeat(64)).unwrap(),
7954 plans: vec![plan],
7955 managed_worktree_profiles: vec![HarnessManagedWorktreeProfileOptionV1 {
7956 node_id: HarnessSelectorV1::new("node-a").unwrap(),
7957 node_incarnation: HarnessSelectorV1::new("07".repeat(16)).unwrap(),
7958 source_workspace_id: HarnessSelectorV1::new("workspace-a").unwrap(),
7959 profile_id: HarnessSelectorV1::new("review").unwrap(),
7960 profile_revision: HarnessSelectorV1::new("review.r7").unwrap(),
7961 retention: HarnessManagedWorktreeRetentionV1::RemoveWhenReleased,
7962 observed_at_unix_ms: 90,
7963 }],
7964 context_sources: vec![context],
7965 delivery_bundles: vec![delivery],
7966 current_issued_spec: Some(HarnessIssuedExecutionSpecSummaryV1 {
7967 task_id,
7968 execution_spec_id: HarnessExecutionSpecId::new(format!(
7969 "hespec_{}",
7970 "4".repeat(24),
7971 )).unwrap(),
7972 revision: HarnessRevision::new(2).unwrap(),
7973 launch_issuance: issuance,
7974 review_policy: HarnessTaskReviewPolicyV1::OperatorReview,
7975 created_at_unix_ms: 80,
7976 updated_at_unix_ms: 90,
7977 }),
7978 truncated: false,
7979 next_after: None,
7980 context_source_exclusions: vec![ContextSourceExclusionEntryV1 {
7981 run_id: HarnessRunId::new(format!("hrun_{}", "9".repeat(24))).unwrap(),
7982 exclusion: ContextSourceExclusionV1::NodeIncarnationUnknown {
7983 node_id: HarnessSelectorV1::new("node-a").unwrap(),
7984 node_incarnation: HarnessSelectorV1::new("07".repeat(16)).unwrap(),
7985 known_incarnations: vec![HarnessSelectorV1::new("08".repeat(16)).unwrap()],
7986 },
7987 }],
7988 }
7989 }
7990
7991 #[test]
7992 fn operator_task_launch_contract_is_exact_and_private() {
7993 let credential = HarnessOperatorCredential::parse(format!(
7994 "g4aho_{}",
7995 "a".repeat(64),
7996 )).unwrap();
7997 let options = task_launch_options();
7998 options.validate().unwrap();
7999 let task_id = options.task_id.clone();
8000 let authority = HarnessOperatorAuthorityV1 {
8001 operation_id: HarnessOperationId::new(format!("hop_{}", "5".repeat(24))).unwrap(),
8002 idempotency_ref: HarnessIdempotencyRef::new(format!(
8003 "hidem_{}",
8004 "6".repeat(24),
8005 )).unwrap(),
8006 actor_id: HarnessSelectorV1::new("operator").unwrap(),
8007 now_unix_ms: 100,
8008 };
8009 let selection = HarnessReviewedTaskLaunchSelectionV1 {
8010 plan: options.plans[0].clone(),
8011 worktree: HarnessReviewedWorktreeSelectionV1::Managed {
8012 profile: options.managed_worktree_profiles[0].clone(),
8013 },
8014 context_source: Some(options.context_sources[0].clone()),
8015 delivery: Some(options.delivery_bundles[0].clone()),
8016 review_policy: HarnessTaskReviewPolicyV1::OperatorReview,
8017 };
8018 let replace = HarnessOperatorRequestV1::ReplaceTaskExecutionSpecV2 {
8019 request: HarnessReplaceTaskExecutionSpecRequestV2 {
8020 authority: authority.clone(),
8021 task_id: task_id.clone(),
8022 expected_task_revision: options.task_revision,
8023 expected_execution_spec_revision:
8024 HarnessExpectedExecutionSpecRevisionV1::Exact(HarnessRevision::new(2).unwrap()),
8025 selection: selection.clone(),
8026 },
8027 };
8028 let issuance = options.current_issued_spec.as_ref().unwrap().launch_issuance.clone();
8029 let start = HarnessOperatorRequestV1::StartTaskV2 {
8030 request: HarnessStartTaskRequestV2 {
8031 authority: authority.clone(),
8032 task_id: task_id.clone(),
8033 expected_task_revision: options.task_revision,
8034 expected_execution_spec_revision: HarnessRevision::new(2).unwrap(),
8035 expected_launch_issuance: issuance,
8036 },
8037 };
8038 for request in [
8039 HarnessOperatorRequestV1::TaskLaunchOptionsGet {
8040 task_id: task_id.clone(),
8041 provider: None,
8042 workspace: None,
8043 plan_id: None,
8044 after: None,
8045 },
8046 replace.clone(),
8047 start.clone(),
8048 ] {
8049 request.validate().unwrap();
8050 }
8051 HarnessOperatorEnvelopeV1 {
8052 build_stamp: BUILD_STAMP.to_string(),
8053 credential,
8054 request: replace.clone(),
8055 }.validate().unwrap();
8056
8057 let replace_json = serde_json::to_string(&replace).unwrap();
8058 for forbidden in [
8059 "issuance_id",
8060 "policy_digest",
8061 "provider_home",
8062 "credential",
8063 "canonical_root",
8064 "display_root",
8065 "prompt",
8066 ] {
8067 assert!(!replace_json.contains(forbidden), "leaked {forbidden}");
8068 }
8069 let mut unknown = serde_json::to_value(start).unwrap();
8070 unknown["request"]["caller_issuance_digest"] = serde_json::json!("untrusted");
8071 assert!(serde_json::from_value::<HarnessOperatorRequestV1>(unknown).is_err());
8072
8073 let intent = HarnessOperatorIntentV1 {
8074 request_ref: HarnessOperatorRequestRefV1::new(format!(
8075 "hireq_{}",
8076 "7".repeat(24),
8077 )).unwrap(),
8078 submitted_at_unix_ms: 100,
8079 action: HarnessOperatorActionV1::ReplaceTaskExecutionSpecV2 {
8080 task_id,
8081 expected_task_revision: options.task_revision,
8082 expected_execution_spec_revision:
8083 HarnessExpectedExecutionSpecRevisionV1::Exact(HarnessRevision::new(2).unwrap()),
8084 selection,
8085 },
8086 };
8087 let intent_json = serde_json::to_string(&intent).unwrap();
8088 assert!(!intent_json.contains("authority"));
8089 let start_intent = HarnessOperatorIntentV1 {
8090 request_ref: HarnessOperatorRequestRefV1::new(format!(
8091 "hireq_{}",
8092 "8".repeat(24),
8093 )).unwrap(),
8094 submitted_at_unix_ms: 101,
8095 action: HarnessOperatorActionV1::StartTaskV2 {
8096 task_id: options.task_id.clone(),
8097 expected_task_revision: options.task_revision,
8098 expected_execution_spec_revision: HarnessRevision::new(2).unwrap(),
8099 expected_launch_issuance: options.current_issued_spec.as_ref().unwrap()
8100 .launch_issuance.clone(),
8101 },
8102 };
8103 let start_intent_json = serde_json::to_string(&start_intent).unwrap();
8104 assert!(!start_intent_json.contains("authority"));
8105 }
8106
8107 #[test]
8108 fn task_launch_options_are_bounded_canonical_correlated_and_redacted() {
8109 let options = task_launch_options();
8110 options.validate_for(&options.task_id).unwrap();
8111 let encoded = serde_json::to_string(&HarnessOperatorResponseV1::TaskLaunchOptions(
8112 options.clone(),
8113 )).unwrap();
8114 for forbidden in [
8115 "canonical_root",
8116 "display_root",
8117 "provider_session_id",
8118 "provider_home",
8119 "credential",
8120 "auth_token",
8121 "source_path",
8122 "payload",
8123 ] {
8124 assert!(!encoded.contains(forbidden), "leaked {forbidden}");
8125 }
8126
8127 let mut duplicate = options.clone();
8128 duplicate.plans.push(duplicate.plans[0].clone());
8129 assert!(matches!(
8130 duplicate.validate(),
8131 Err(HarnessOperatorApiError::InvalidTaskLaunchOptions),
8132 ));
8133 let mut oversized = options.clone();
8134 oversized.context_sources = vec![
8135 options.context_sources[0].clone();
8136 HARNESS_TASK_LAUNCH_OPTIONS_MAX + 1
8137 ];
8138 assert!(matches!(
8139 oversized.validate(),
8140 Err(HarnessOperatorApiError::InvalidTaskLaunchOptions),
8141 ));
8142 let other_task = HarnessTaskId::new(format!("htask_{}", "8".repeat(24))).unwrap();
8143 assert!(matches!(
8144 options.validate_for(&other_task),
8145 Err(HarnessOperatorApiError::InvalidTaskLaunchOptions),
8146 ));
8147 }
8148
8149 #[test]
8154 fn task_launch_options_paging_fields_are_additive_and_next_after_is_checked() {
8155 let task_id = HarnessTaskId::new(format!("htask_{}", "9".repeat(24))).unwrap();
8156 let legacy_request_json = serde_json::json!({
8157 "kind": "task-launch-options-get",
8158 "task_id": task_id.as_str(),
8159 });
8160 let decoded: HarnessOperatorRequestV1 =
8161 serde_json::from_value(legacy_request_json).unwrap();
8162 assert!(matches!(
8163 decoded,
8164 HarnessOperatorRequestV1::TaskLaunchOptionsGet {
8165 provider: None, workspace: None, plan_id: None, after: None, ..
8166 },
8167 ));
8168
8169 let mut options = task_launch_options();
8170 let legacy_response_json = serde_json::to_value(&options).unwrap();
8171 assert!(legacy_response_json.get("next_after").is_some());
8172 let mut without_next_after = legacy_response_json.clone();
8173 without_next_after.as_object_mut().unwrap().remove("next_after");
8174 let redecoded: HarnessTaskLaunchOptionsV1 =
8175 serde_json::from_value(without_next_after).unwrap();
8176 assert_eq!(redecoded.next_after, None);
8177 redecoded.validate().unwrap();
8178
8179 let plan_id = options.plans[0].plan.plan_id.clone();
8180 options.next_after = Some(plan_id.clone());
8181 assert!(matches!(
8182 options.validate(),
8183 Err(HarnessOperatorApiError::InvalidTaskLaunchOptions),
8184 ), "next_after without truncated must be refused");
8185 options.truncated = true;
8186 options.validate().unwrap();
8187 options.next_after = Some(HarnessSelectorV1::new("not-the-last-plan").unwrap());
8188 assert!(matches!(
8189 options.validate(),
8190 Err(HarnessOperatorApiError::InvalidTaskLaunchOptions),
8191 ), "next_after must name the last plan on the page");
8192 }
8193
8194 #[test]
8199 fn context_source_exclusions_are_additive_bounded_and_ordered() {
8200 let options = task_launch_options();
8201 options.validate_for(&options.task_id).unwrap();
8202 assert_eq!(options.context_source_exclusions.len(), 1);
8203
8204 let mut legacy_json = serde_json::to_value(&options).unwrap();
8205 legacy_json.as_object_mut().unwrap().remove("context_source_exclusions");
8206 let redecoded: HarnessTaskLaunchOptionsV1 = serde_json::from_value(legacy_json).unwrap();
8207 assert!(redecoded.context_source_exclusions.is_empty());
8208 redecoded.validate().unwrap();
8209
8210 let entry = options.context_source_exclusions[0].clone();
8211 let encoded = serde_json::to_string(&entry).unwrap();
8212 assert!(encoded.contains("\"reason\":\"node-incarnation-unknown\""));
8213 let redecoded_entry: ContextSourceExclusionEntryV1 = serde_json::from_str(&encoded).unwrap();
8214 assert_eq!(redecoded_entry, entry);
8215
8216 let mut oversized = options.clone();
8217 oversized.context_source_exclusions =
8218 vec![entry.clone(); HARNESS_CONTEXT_SOURCE_EXCLUSIONS_MAX + 1];
8219 assert!(matches!(
8220 oversized.validate(),
8221 Err(HarnessOperatorApiError::InvalidTaskLaunchOptions),
8222 ));
8223
8224 let mut duplicated = options.clone();
8225 duplicated.context_source_exclusions.push(entry);
8226 assert!(matches!(
8227 duplicated.validate(),
8228 Err(HarnessOperatorApiError::InvalidTaskLaunchOptions),
8229 ), "context_source_exclusions must stay sorted and duplicate-free by run_id");
8230 }
8231
8232 #[test]
8237 fn context_source_exclusion_variants_round_trip_every_shape() {
8238 let samples = [
8239 ContextSourceExclusionV1::LifecycleNotLive {
8240 lifecycle: HarnessRunLifecycleV1::Cancelled,
8241 },
8242 ContextSourceExclusionV1::NoBinding,
8243 ContextSourceExclusionV1::NoActiveManagedSession,
8244 ContextSourceExclusionV1::NodeIncarnationUnknown {
8245 node_id: HarnessSelectorV1::new("node-a").unwrap(),
8246 node_incarnation: HarnessSelectorV1::new("07".repeat(16)).unwrap(),
8247 known_incarnations: vec![HarnessSelectorV1::new("08".repeat(16)).unwrap()],
8248 },
8249 ContextSourceExclusionV1::ManagedSessionRecordMismatch {
8250 record_id: HarnessSelectorV1::new("record-a").unwrap(),
8251 workspace_id: HarnessSelectorV1::new("workspace-a").unwrap(),
8252 instance_id: 7,
8253 generation: 3,
8254 node_has_records: 4,
8255 },
8256 ContextSourceExclusionV1::ManagedSessionsPageTruncated {
8257 record_id: HarnessSelectorV1::new("record-a").unwrap(),
8258 page_len: 128,
8259 total: 185,
8260 },
8261 ContextSourceExclusionV1::MonitorUnavailable,
8262 ContextSourceExclusionV1::ProjectionNotLive {
8263 availability: ProjectionAvailabilityV1::Partial,
8264 freshness: ProjectionFreshnessV1::Stale,
8265 transport_incomplete: true,
8266 },
8267 ContextSourceExclusionV1::DurableNodeUnknown {
8268 node_id: HarnessSelectorV1::new("node-a").unwrap(),
8269 },
8270 ];
8271 for exclusion in samples {
8272 exclusion.validate().unwrap();
8273 let encoded = serde_json::to_string(&exclusion).unwrap();
8274 let decoded: ContextSourceExclusionV1 = serde_json::from_str(&encoded).unwrap();
8275 assert_eq!(decoded, exclusion);
8276 }
8277
8278 let oversized = ContextSourceExclusionV1::NodeIncarnationUnknown {
8279 node_id: HarnessSelectorV1::new("node-a").unwrap(),
8280 node_incarnation: HarnessSelectorV1::new("07".repeat(16)).unwrap(),
8281 known_incarnations: vec![
8282 HarnessSelectorV1::new("08".repeat(16)).unwrap();
8283 CONTEXT_SOURCE_EXCLUSION_KNOWN_INCARNATIONS_MAX + 1
8284 ],
8285 };
8286 assert!(matches!(
8287 oversized.validate(),
8288 Err(HarnessOperatorApiError::InvalidTaskLaunchOptions),
8289 ));
8290 }
8291
8292 #[test]
8293 fn operator_execution_requests_are_exact() {
8294 let credential = HarnessOperatorCredential::parse(format!(
8295 "g4aho_{}",
8296 "a".repeat(64),
8297 )).unwrap();
8298 let authority = HarnessOperatorAuthorityV1 {
8299 operation_id: HarnessOperationId::new(format!("hop_{}", "4".repeat(24))).unwrap(),
8300 idempotency_ref: HarnessIdempotencyRef::new(format!(
8301 "hidem_{}",
8302 "5".repeat(24),
8303 )).unwrap(),
8304 actor_id: HarnessSelectorV1::new("operator").unwrap(),
8305 now_unix_ms: 40,
8306 };
8307 let task_id = HarnessTaskId::new(format!("htask_{}", "b".repeat(24))).unwrap();
8308 let requests = [
8309 HarnessOperatorRequestV1::LaunchPlansList {
8310 after_plan_id: Some(HarnessSelectorV1::new("plan-a").unwrap()),
8311 limit: HARNESS_LAUNCH_PLAN_PAGE_LIMIT_MAX,
8312 },
8313 HarnessOperatorRequestV1::TaskExecutionSpecGet {
8314 task_id: task_id.clone(),
8315 },
8316 HarnessOperatorRequestV1::ReplaceTaskExecutionSpec {
8317 request: HarnessReplaceTaskExecutionSpecRequestV1 {
8318 authority: authority.clone(),
8319 task_id: task_id.clone(),
8320 expected_task_revision: HarnessRevision::new(3).unwrap(),
8321 expected_execution_spec_revision:
8322 HarnessExpectedExecutionSpecRevisionV1::Absent,
8323 spec: HarnessTaskExecutionSpecInputV1 {
8324 scheduled_launch: ordinary_launch_plan("plan-a", 'c').scheduled_launch,
8325 review_policy: HarnessTaskReviewPolicyV1::OperatorReview,
8326 },
8327 },
8328 },
8329 HarnessOperatorRequestV1::StartTask {
8330 request: HarnessStartTaskRequestV1 {
8331 authority,
8332 task_id,
8333 expected_task_revision: HarnessRevision::new(3).unwrap(),
8334 expected_execution_spec_revision: HarnessRevision::new(1).unwrap(),
8335 expected_scheduled_launch_digest: HarnessRequestDigest::new(
8336 "d".repeat(64),
8337 ).unwrap(),
8338 },
8339 },
8340 ];
8341 for request in requests {
8342 assert!(request.validate().is_ok());
8343 assert!(HarnessOperatorEnvelopeV1 {
8344 build_stamp: BUILD_STAMP.to_string(),
8345 credential: credential.clone(),
8346 request,
8347 }.validate().is_ok());
8348 }
8349
8350 assert!(matches!(
8351 HarnessOperatorRequestV1::LaunchPlansList {
8352 after_plan_id: None,
8353 limit: HARNESS_LAUNCH_PLAN_PAGE_LIMIT_MAX + 1,
8354 }.validate(),
8355 Err(HarnessOperatorApiError::InvalidLimit),
8356 ));
8357 }
8358
8359 #[test]
8360 fn launch_plan_page_is_bounded_canonical_and_redacted() {
8361 let first = ordinary_launch_plan("plan-a", 'a');
8362 let second = ordinary_launch_plan("plan-b", 'b');
8363 let page = HarnessLaunchPlanPageV1 {
8364 plans: vec![first.clone(), second.clone()],
8365 next_plan_id: Some(second.scheduled_launch.plan.plan_id.clone()),
8366 };
8367 page.validate().unwrap();
8368 let encoded = serde_json::to_string(&page).unwrap();
8369 for sentinel in [
8370 "delivery",
8371 "continuation",
8372 "harness_mcp",
8373 "spawn_spec",
8374 "environment",
8375 "provider_home",
8376 "credential",
8377 "raw_path",
8378 ] {
8379 assert!(!encoded.contains(sentinel));
8380 }
8381 assert_eq!(serde_json::from_str::<HarnessLaunchPlanPageV1>(&encoded).unwrap(), page);
8382
8383 assert!(matches!(
8384 HarnessLaunchPlanPageV1 {
8385 plans: vec![second, first],
8386 next_plan_id: None,
8387 }.validate(),
8388 Err(HarnessOperatorApiError::InvalidLaunchPlans),
8389 ));
8390 }
8391
8392 #[test]
8393 fn execution_spec_and_start_intents_are_authority_free() {
8394 let intent = HarnessOperatorIntentV1 {
8395 request_ref: HarnessOperatorRequestRefV1::new(format!(
8396 "hireq_{}",
8397 "1".repeat(24),
8398 )).unwrap(),
8399 submitted_at_unix_ms: 50,
8400 action: HarnessOperatorActionV1::StartTask {
8401 task_id: HarnessTaskId::new(format!("htask_{}", "2".repeat(24))).unwrap(),
8402 expected_task_revision: HarnessRevision::new(7).unwrap(),
8403 expected_execution_spec_revision: HarnessRevision::new(3).unwrap(),
8404 expected_scheduled_launch_digest: HarnessRequestDigest::new(
8405 "c".repeat(64),
8406 ).unwrap(),
8407 },
8408 };
8409 intent.validate().unwrap();
8410 let request = HarnessOperatorRequestV1::SubmitIntent { intent };
8411 let encoded = serde_json::to_string(&request).unwrap();
8412 assert!(!encoded.contains("authority"));
8413 assert!(!encoded.contains("operation_id"));
8414 assert!(!encoded.contains("idempotency_ref"));
8415
8416 let replace = HarnessOperatorRequestV1::SubmitIntent {
8417 intent: HarnessOperatorIntentV1 {
8418 request_ref: HarnessOperatorRequestRefV1::new(format!(
8419 "hireq_{}",
8420 "3".repeat(24),
8421 )).unwrap(),
8422 submitted_at_unix_ms: 51,
8423 action: HarnessOperatorActionV1::ReplaceTaskExecutionSpec {
8424 task_id: HarnessTaskId::new(format!(
8425 "htask_{}",
8426 "4".repeat(24),
8427 )).unwrap(),
8428 expected_task_revision: HarnessRevision::new(2).unwrap(),
8429 expected_execution_spec_revision:
8430 HarnessExpectedExecutionSpecRevisionV1::Absent,
8431 spec: HarnessTaskExecutionSpecInputV1 {
8432 scheduled_launch: ordinary_launch_plan("plan-a", 'd').scheduled_launch,
8433 review_policy: HarnessTaskReviewPolicyV1::OperatorReview,
8434 },
8435 },
8436 },
8437 };
8438 replace.validate().unwrap();
8439 }
8440
8441 #[test]
8442 fn run_correlation_round_trip_preserves_atomic_identity_and_redacts() {
8443 let correlation = managed_run_correlation();
8444 correlation.validate().unwrap();
8445 let response = HarnessOperatorResponseV1::RunCorrelation(correlation.clone());
8446 response.validate().unwrap();
8447 let encoded = serde_json::to_vec(&response).unwrap();
8448 let decoded: HarnessOperatorResponseV1 = serde_json::from_slice(&encoded).unwrap();
8449 assert_eq!(decoded, response);
8450 let text = String::from_utf8(encoded.clone()).unwrap();
8451 for forbidden in [
8452 "g4aho_",
8453 "credential",
8454 "provider_identity",
8455 "session_id",
8456 "spawn_spec",
8457 "environment",
8458 "C:\\\\",
8459 ] {
8460 assert!(!text.contains(forbidden));
8461 }
8462 assert!(text.contains("provider_profile"));
8463 assert!(text.contains("node_incarnation_id"));
8464 assert!(text.contains("active_session"));
8465
8466 let mut without_generation: serde_json::Value =
8467 serde_json::from_slice(&encoded).unwrap();
8468 without_generation["value"]["session"]["value"]["active_session"]
8469 .as_object_mut().unwrap().remove("generation");
8470 assert!(serde_json::from_value::<HarnessOperatorResponseV1>(
8471 without_generation,
8472 ).is_err());
8473 }
8474
8475 #[test]
8476 fn run_correlation_rejects_inconsistent_availability_and_unbounded_fields() {
8477 let mut correlation = managed_run_correlation();
8478 correlation.availability = HarnessRunCorrelationAvailabilityV1::NotObserved;
8479 assert!(matches!(
8480 correlation.validate(),
8481 Err(HarnessOperatorApiError::InvalidRunCorrelation),
8482 ));
8483 correlation.observed_at_unix_ms = None;
8484 correlation.validate().unwrap();
8485
8486 correlation.availability = HarnessRunCorrelationAvailabilityV1::Dormant;
8487 correlation.observed_at_unix_ms = Some(101);
8488 assert!(matches!(
8489 correlation.validate(),
8490 Err(HarnessOperatorApiError::InvalidRunCorrelation),
8491 ));
8492
8493 let unknown = r#"{
8494 "run_id":"hrun_aaaaaaaaaaaaaaaaaaaaaaaa",
8495 "run_revision":1,
8496 "task_id":"htask_bbbbbbbbbbbbbbbbbbbbbbbb",
8497 "node_id":"node-a",
8498 "node_incarnation_id":"07070707070707070707070707070707",
8499 "workspace_id":"workspace-a",
8500 "provider_profile":"codex-default",
8501 "mode":"inline",
8502 "worktree":{"kind":"existing"},
8503 "session":{"kind":"inline","value":{"inline_ref":"hinline_cccccccccccccccccccccccc"}},
8504 "availability":"not-observed",
8505 "observed_at_unix_ms":null,
8506 "raw_path":"C:\\\\private"
8507 }"#;
8508 assert!(serde_json::from_str::<HarnessRunCorrelationV1>(unknown).is_err());
8509 assert!(HarnessNodeIncarnationV1::new("x".repeat(129)).is_err());
8510 assert!(HarnessNodeIncarnationV1::new("0".repeat(31)).is_err());
8511 assert!(HarnessNodeIncarnationV1::new("A".repeat(32)).is_err());
8512 }
8513
8514 #[test]
8515 fn runtime_inventory_request_validates() {
8516 let credential = HarnessOperatorCredential::parse(format!(
8517 "g4aho_{}",
8518 "a".repeat(64),
8519 )).unwrap();
8520 let request = HarnessOperatorRequestV1::RuntimeInventoryList {
8521 after_node_id: None,
8522 limit: HARNESS_RUNTIME_INVENTORY_PAGE_LIMIT_MAX,
8523 };
8524 assert!(HarnessOperatorEnvelopeV1 {
8525 build_stamp: BUILD_STAMP.to_string(),
8526 credential,
8527 request,
8528 }.validate().is_ok());
8529 }
8530
8531 #[test]
8540 fn managed_session_blocked_stats_round_trip() {
8541 let mut session = sample_managed_session("record-a");
8542 session.blocked_count = 2;
8543 session.last_blocked_at_ms = Some(2_000);
8544 session.validate().expect("blocked_count with a timestamp is valid");
8545 let encoded = serde_json::to_string(&session).unwrap();
8546 let decoded: HarnessRuntimeManagedSessionV1 = serde_json::from_str(&encoded).unwrap();
8547 assert_eq!(decoded, session);
8548
8549 let mut zero_with_timestamp = sample_managed_session("record-a");
8550 zero_with_timestamp.last_blocked_at_ms = Some(1);
8551 assert!(zero_with_timestamp.validate().is_err());
8552
8553 let mut counted_without_timestamp = sample_managed_session("record-a");
8554 counted_without_timestamp.blocked_count = 1;
8555 assert!(counted_without_timestamp.validate().is_err());
8556 }
8557
8558 fn sample_runtime_inventory(
8559 managed_sessions: Vec<HarnessRuntimeManagedSessionV1>,
8560 ) -> HarnessRuntimeInventoryV1 {
8561 HarnessRuntimeInventoryV1 {
8562 enabled_providers: Vec::new(),
8563 workspaces: BTreeMap::new(),
8564 workspace_count: 0,
8565 workspaces_truncated: false,
8566 session_count: 0,
8567 sessions_truncated: false,
8568 managed_session_count: managed_sessions.len(),
8569 managed_sessions_truncated: false,
8570 managed_sessions,
8571 retired_count: 0,
8572 launch_inventory: None,
8573
8574 }
8575 }
8576
8577 #[test]
8585 fn managed_session_ordering_is_liveness_rank_then_record_id() {
8586 let mut live = sample_managed_session("zzz-live");
8587 live.state = HarnessRuntimeManagedStateV1::Live;
8588 let mut pending = sample_managed_session("aaa-pending");
8589 pending.state = HarnessRuntimeManagedStateV1::IdentityPending;
8590 let mut unavailable = sample_managed_session("mmm-unavailable");
8591 unavailable.state = HarnessRuntimeManagedStateV1::Unavailable;
8592
8593 let ordered = sample_runtime_inventory(vec![
8594 live.clone(),
8595 pending,
8596 unavailable,
8597 ]);
8598 ordered.validate().expect(
8599 "liveness-rank-then-record_id order validates even though record_id is not ascending overall",
8600 );
8601
8602 let duplicate = sample_runtime_inventory(vec![live.clone(), live.clone()]);
8603 assert!(duplicate.validate().is_err());
8604
8605 let mut dormant = sample_managed_session("aaa-dormant");
8606 dormant.state = HarnessRuntimeManagedStateV1::Dormant;
8607 let rank_out_of_order = sample_runtime_inventory(vec![dormant, live]);
8608 assert!(rank_out_of_order.validate().is_err());
8609 }
8610
8611 #[test]
8616 fn runtime_inventory_retired_count_round_trips_and_defaults_to_zero() {
8617 let mut inventory = sample_runtime_inventory(Vec::new());
8618 assert_eq!(inventory.retired_count, 0);
8619 inventory.validate().expect("a fresh sample inventory is valid");
8620
8621 inventory.retired_count = 5;
8622 let encoded = serde_json::to_string(&inventory).unwrap();
8623 assert!(encoded.contains("\"retired_count\":5"));
8624 let decoded: HarnessRuntimeInventoryV1 = serde_json::from_str(&encoded).unwrap();
8625 assert_eq!(decoded, inventory);
8626 decoded.validate().expect("a nonzero retired_count is still a valid inventory");
8627 }
8628
8629 #[test]
8630 fn runtime_inventory_launch_inventory_round_trips_and_fails_closed() {
8631 let node = HarnessRuntimeNodeInventoryV1 {
8632 node_id: "node-a".to_owned(),
8633 incarnation_id: "07".repeat(16),
8634 observed_at_unix_ms: 1_000,
8635 event_sequence: 4,
8636 inventory: HarnessRuntimeInventoryV1 {
8637 enabled_providers: vec!["claude".to_owned()],
8638 workspaces: BTreeMap::new(),
8639 workspace_count: 0,
8640 workspaces_truncated: false,
8641 session_count: 0,
8642 sessions_truncated: false,
8643 managed_sessions: Vec::new(),
8644 managed_session_count: 0,
8645 managed_sessions_truncated: false,
8646 retired_count: 0,
8647 launch_inventory: Some(HarnessRuntimeLaunchInventoryV1 {
8648 spawn_profiles: Some(vec![HarnessRuntimeSpawnProfileSummaryV1 {
8649 id: "default".to_owned(),
8650 revision: "rev-1".to_owned(),
8651 environment_profile: Some(HarnessRuntimeEnvironmentProfileReceiptV1 {
8652 profile_id: "env-a".to_owned(),
8653 profile_revision: "env-rev-1".to_owned(),
8654
8655 }),
8656 }]),
8657 bundles: Some(vec![HarnessRuntimeBundleReceiptV1 {
8658 id: "bundle-a".to_owned(),
8659 revision: "bundle-rev-1".to_owned(),
8660 digest: format!("sha256:{}", "a".repeat(64)),
8661 }]),
8662 }),
8663 },
8664 };
8665 node.validate().expect("valid launch inventory");
8666 let encoded = serde_json::to_string(&node).unwrap();
8667 let decoded: HarnessRuntimeNodeInventoryV1 = serde_json::from_str(&encoded).unwrap();
8668 assert_eq!(decoded, node);
8669
8670 let mut missing_component = node.clone();
8671 missing_component.inventory.launch_inventory = Some(HarnessRuntimeLaunchInventoryV1 {
8672 spawn_profiles: None,
8673 bundles: None,
8674 });
8675 assert!(missing_component.validate().is_err());
8676
8677 let mut duplicate_profile = node.clone();
8678 let profile = duplicate_profile.inventory.launch_inventory.as_ref().unwrap()
8679 .spawn_profiles.as_ref().unwrap()[0].clone();
8680 duplicate_profile.inventory.launch_inventory.as_mut().unwrap()
8681 .spawn_profiles.as_mut().unwrap().push(profile);
8682 assert!(duplicate_profile.validate().is_err());
8683
8684 let mut bad_digest = node.clone();
8685 bad_digest.inventory.launch_inventory.as_mut().unwrap()
8686 .bundles.as_mut().unwrap()[0].digest = "not-a-digest".to_owned();
8687 assert!(bad_digest.validate().is_err());
8688
8689 let mut no_launch_inventory = node;
8690 no_launch_inventory.inventory.launch_inventory = None;
8691 no_launch_inventory.validate().expect("absent launch inventory stays valid");
8692 }
8693
8694 #[test]
8695 fn native_history_is_exact_bounded_and_excludes_sensitive_fields() {
8696 let credential = HarnessOperatorCredential::parse(format!(
8697 "g4aho_{}",
8698 "a".repeat(64),
8699 )).unwrap();
8700 let route = HarnessNativeSessionRouteV1 {
8701 node_id: "node-a".to_owned(),
8702 incarnation_id: "1".repeat(32),
8703 scope: HarnessNativeSessionCatalogScopeV1::Workspace,
8704 workspace_id: Some("workspace-a".to_owned()),
8705 provider: "codex".to_owned(),
8706 };
8707 let selection = HarnessNativeSessionSelectionV1 {
8708 route: route.clone(),
8709 catalog_revision: 7,
8710 recent_cutoff_unix_ms: 9,
8711 selection_id: "selection-a".to_owned(),
8712 };
8713 for request in [
8714 HarnessOperatorRequestV1::CatalogNativeSessions {
8715 route: route.clone(),
8716 limit: HARNESS_NATIVE_SESSION_CATALOG_LIMIT_MAX,
8717 },
8718 HarnessOperatorRequestV1::PageNativeSessions {
8719 route: route.clone(),
8720 window: HarnessNativeSessionCatalogWindowV1::Recent,
8721 catalog_revision: 7,
8722 recent_cutoff_unix_ms: 9,
8723 after_selection_id: Some("selection-a".to_owned()),
8724 limit: 1,
8725 },
8726 HarnessOperatorRequestV1::PreviewNativeSession {
8727 selection: selection.clone(),
8728 message_limit: HARNESS_NATIVE_SESSION_PREVIEW_MESSAGE_LIMIT_MAX,
8729 },
8730 ] {
8731 HarnessOperatorEnvelopeV1 {
8732 build_stamp: BUILD_STAMP.to_string(),
8733 credential: credential.clone(),
8734 request,
8735 }.validate().unwrap();
8736 }
8737 let response = HarnessOperatorResponseV1::NativeSessionPreviewed(
8738 HarnessNativeSessionPreviewedV1 {
8739 selection,
8740 preview: HarnessNativeSessionPreviewV1 {
8741 title: Some("Bounded preview".to_owned()),
8742 modified_at_unix_ms: Some(10),
8743 model: Some("model-a".to_owned()),
8744 message_count: 1,
8745 message_count_exact: true,
8746 completed_turn_count: Some(1),
8747 total_tokens: Some(12),
8748 truncated: false,
8749 messages: vec![HarnessNativeSessionPreviewMessageV1 {
8750 role: HarnessNativeSessionPreviewRoleV1::User,
8751 text: "bounded history preview".to_owned(),
8752 }],
8753 },
8754 },
8755 );
8756 response.validate().unwrap();
8757 let encoded = serde_json::to_string(&response).unwrap();
8758 for forbidden in [
8759 "g4aho_",
8760 "credential",
8761 "provider_identity",
8762 "session_id",
8763 "terminal",
8764 "cwd",
8765 ] {
8766 assert!(!encoded.contains(forbidden));
8767 }
8768 }
8769
8770 #[test]
8771 fn unknown_request_fields_and_unbounded_limits_fail_closed() {
8772 let unknown = r#"{"kind":"tasks-list","limit":10,"cursor":"secret"}"#;
8773 assert!(serde_json::from_str::<HarnessReadRequestV1>(unknown).is_err());
8774 assert!(HarnessReadRequestV1::TasksList {
8775 after_task_id: None, state: None, parent_task_id: None, limit: 0,
8776 }.validate().is_err());
8777 assert!(HarnessReadRequestV1::TasksList {
8778 after_task_id: None, state: None, parent_task_id: None, limit: 65,
8779 }.validate().is_err());
8780 }
8781
8782 #[test]
8783 fn redacted_cross_entity_references_do_not_require_hidden_ids() {
8784 let redacted_run = r#"{
8785 "run_id":"hrun_000000000000000000000001",
8786 "revision":1,
8787 "parent_run_id":null,
8788 "task_id":null,
8789 "operation_id":null,
8790 "intent":{"mode":"inline","worktree":"existing","has_delivery_bundle":false,"has_continuation":false},
8791 "lifecycle":"requested",
8792 "binding":"none",
8793 "result_disposition":null,
8794 "failure_category":null,
8795 "delivery_receipt":null,
8796 "references_redacted":true,
8797 "created_at_unix_ms":1,
8798 "updated_at_unix_ms":1
8799 }"#;
8800 let run: RedactedRunV1 = serde_json::from_str(redacted_run).expect("redacted run");
8801 run.validate().expect("valid redacted run");
8802
8803 let operation_with_grant = r#"{
8804 "operation_id":"hop_000000000000000000000001",
8805 "revision":1,
8806 "kind":"create-task",
8807 "state":"prepared",
8808 "task_id":null,
8809 "run_id":null,
8810 "grant_id":"hgrant_000000000000000000000001",
8811 "reconciles_operation_id":null,
8812 "references_redacted":true,
8813 "failure_category":null,
8814 "outcome_unknown_reason":null,
8815 "reconciliation_outcome":null,
8816 "created_at_unix_ms":1,
8817 "updated_at_unix_ms":1,
8818 "dispatched_at_unix_ms":null,
8819 "finished_at_unix_ms":null
8820 }"#;
8821 assert!(serde_json::from_str::<RedactedOperationV1>(operation_with_grant).is_err());
8822 }
8823
8824 #[test]
8825 fn operator_run_workspace_requests_are_exact_harness_only_round_trips() {
8826 let run_id = run_workspace_origin().run_id;
8827 let path = HarnessRepositoryPathV1::new("src/lib.rs").unwrap();
8828 let object_id = HarnessGitObjectIdV1::new("a".repeat(40)).unwrap();
8829 let requests = vec![
8830 HarnessOperatorRequestV1::InspectRunWorkspace {
8831 run_id: run_id.clone(),
8832 },
8833 HarnessOperatorRequestV1::ReadRunWorkspaceFile {
8834 run_id: run_id.clone(),
8835 path: path.clone(),
8836 },
8837 HarnessOperatorRequestV1::ReadRunGitHistory {
8838 run_id: run_id.clone(),
8839 path: Some(path.clone()),
8840 before: Some(object_id.clone()),
8841 limit: HARNESS_GIT_HISTORY_LIMIT_MAX,
8842 },
8843 HarnessOperatorRequestV1::ReadRunGitDiff {
8844 run_id,
8845 mode: HarnessGitDiffModeV1::Commit { revision: object_id },
8846 path: Some(path),
8847 },
8848 ];
8849 for request in requests {
8850 request.validate().expect("valid workspace request");
8851 let encoded = serde_json::to_string(&request).unwrap();
8852 for forbidden in [
8853 "node_id",
8854 "workspace_id",
8855 "endpoint",
8856 "root",
8857 "workspace_root",
8858 "worktree",
8859 "worktree_path",
8860 "environment",
8861 "spawn_spec",
8862 "provider_home",
8863 ] {
8864 assert!(!encoded.contains(forbidden), "request exposed {forbidden}");
8865 }
8866 let decoded: HarnessOperatorRequestV1 = serde_json::from_str(&encoded).unwrap();
8867 assert_eq!(decoded, request);
8868 }
8869 }
8870
8871 #[test]
8872 fn operator_v4_workspace_replies_round_trip_without_host_or_diagnostic_fields() {
8873 let origin = run_workspace_origin();
8874 let path = HarnessRepositoryPathV1::new("src/lib.rs").unwrap();
8875 let commit = git_commit('a');
8876 let responses = vec![
8877 HarnessOperatorResponseV1::RunWorkspaceInspected(
8878 HarnessRunWorkspaceInspectionV1 {
8879 origin: origin.clone(),
8880 entries: vec![HarnessWorkspaceTreeEntryV1 {
8881 relative_path: path.clone(),
8882 kind: HarnessWorkspaceEntryKindV1::File,
8883 }],
8884 tree_truncated: false,
8885 git: HarnessGitSummaryV1 {
8886 is_repository: true,
8887 branch: Some("main".to_owned()),
8888 status: vec![HarnessGitStatusEntryV1 {
8889 index_status: HarnessGitStatusCodeV1::Unmodified,
8890 worktree_status: HarnessGitStatusCodeV1::Modified,
8891 path: path.clone(),
8892 previous_path: None,
8893 }],
8894 recent_commits: vec![HarnessGitCommitSummaryV1 {
8895 id: commit.id.clone(),
8896 summary: commit.subject.clone(),
8897 }],
8898 truncated: false,
8899 },
8900 truncation: None,
8901 },
8902 ),
8903 HarnessOperatorResponseV1::RunWorkspaceFileRead(HarnessRunWorkspaceFileV1 {
8904 origin: origin.clone(),
8905 path: path.clone(),
8906 content: HarnessWorkspaceFileContentV1::Utf8 {
8907 text: "fn main() {}\n".to_owned(),
8908 byte_len: 13,
8909 },
8910 revision: Some(HarnessWorkspaceFileRevisionV1::new("b".repeat(64)).unwrap()),
8911 }),
8912 HarnessOperatorResponseV1::RunGitHistoryRead(HarnessRunGitHistoryPageV1 {
8913 origin: origin.clone(),
8914 path: Some(path.clone()),
8915 commits: vec![commit],
8916 next_before: None,
8917 truncated: false,
8918 }),
8919 HarnessOperatorResponseV1::RunGitDiffRead(HarnessRunGitDiffV1 {
8920 origin,
8921 mode: HarnessGitDiffModeV1::Working,
8922 path: Some(path),
8923 text: "diff --git a/src/lib.rs b/src/lib.rs\n".to_owned(),
8924 truncated: false,
8925 }),
8926 ];
8927 for response in responses {
8928 response.validate().expect("valid V4 workspace response");
8929 let encoded = serde_json::to_string(&response).unwrap();
8930 assert_json_has_no_forbidden_keys(&encoded, &[
8931 "root",
8932 "worktree",
8933 "endpoint",
8934 "diagnostic",
8935 "author_email",
8936 "committer_email",
8937 "environment",
8938 "spawn_spec",
8939 "provider_home",
8940 ]);
8941 let decoded: HarnessOperatorResponseV1 = serde_json::from_str(&encoded).unwrap();
8942 assert_eq!(decoded, response);
8943 }
8944 }
8945
8946 #[test]
8947 fn operator_v4_repository_paths_and_git_cursors_fail_closed() {
8948 for value in [
8949 "",
8950 ".",
8951 "..",
8952 "../secret",
8953 "src/../secret",
8954 "src//lib.rs",
8955 "/absolute",
8956 "C:/absolute",
8957 r"src\lib.rs",
8958 "src/\nlib.rs",
8959 ] {
8960 assert!(HarnessRepositoryPathV1::new(value).is_err(), "accepted {value:?}");
8961 }
8962 assert!(HarnessRepositoryPathV1::new("x".repeat(HARNESS_REPOSITORY_PATH_MAX_BYTES)).is_ok());
8963 assert!(HarnessRepositoryPathV1::new("x".repeat(HARNESS_REPOSITORY_PATH_MAX_BYTES + 1)).is_err());
8964 assert!(HarnessGitObjectIdV1::new("a".repeat(40)).is_ok());
8965 assert!(HarnessGitObjectIdV1::new("a".repeat(64)).is_ok());
8966 assert!(HarnessGitObjectIdV1::new("A".repeat(40)).is_err());
8967 assert!(HarnessGitObjectIdV1::new("a".repeat(39)).is_err());
8968
8969 let unknown_route = format!(
8970 r#"{{"kind":"read-run-workspace-file","run_id":"hrun_{}","path":"src/lib.rs","workspace_id":"forbidden"}}"#,
8971 "a".repeat(24),
8972 );
8973 assert!(serde_json::from_str::<HarnessOperatorRequestV1>(&unknown_route).is_err());
8974 }
8975
8976 #[test]
8977 fn operator_v4_workspace_payloads_enforce_exact_bounds() {
8978 let origin = run_workspace_origin();
8979 let path = HarnessRepositoryPathV1::new("src/lib.rs").unwrap();
8980 let at_file_limit = HarnessWorkspaceFileContentV1::Utf8 {
8981 text: "x".repeat(HARNESS_WORKSPACE_FILE_MAX_BYTES),
8982 byte_len: HARNESS_WORKSPACE_FILE_MAX_BYTES as u32,
8983 };
8984 assert!(at_file_limit.validate().is_ok());
8985 assert!(HarnessWorkspaceFileContentV1::Utf8 {
8986 text: "x".repeat(HARNESS_WORKSPACE_FILE_MAX_BYTES + 1),
8987 byte_len: (HARNESS_WORKSPACE_FILE_MAX_BYTES + 1) as u32,
8988 }.validate().is_err());
8989 assert!(HarnessRunGitDiffV1 {
8990 origin: origin.clone(),
8991 mode: HarnessGitDiffModeV1::Working,
8992 path: Some(path.clone()),
8993 text: "x".repeat(HARNESS_GIT_DIFF_MAX_BYTES),
8994 truncated: false,
8995 }.validate().is_ok());
8996 assert!(HarnessRunGitDiffV1 {
8997 origin: origin.clone(),
8998 mode: HarnessGitDiffModeV1::Working,
8999 path: Some(path.clone()),
9000 text: "x".repeat(HARNESS_GIT_DIFF_MAX_BYTES + 1),
9001 truncated: true,
9002 }.validate().is_err());
9003 assert!(HarnessOperatorRequestV1::ReadRunGitHistory {
9004 run_id: origin.run_id.clone(),
9005 path: Some(path.clone()),
9006 before: None,
9007 limit: HARNESS_GIT_HISTORY_LIMIT_MAX + 1,
9008 }.validate().is_err());
9009
9010 let entries: Vec<_> = (0..HARNESS_WORKSPACE_TREE_ENTRIES_MAX)
9011 .map(|index| HarnessWorkspaceTreeEntryV1 {
9012 relative_path: HarnessRepositoryPathV1::new(format!("entry-{index:04}")).unwrap(),
9013 kind: HarnessWorkspaceEntryKindV1::File,
9014 })
9015 .collect();
9016 let empty_git = HarnessGitSummaryV1 {
9017 is_repository: false,
9018 branch: None,
9019 status: Vec::new(),
9020 recent_commits: Vec::new(),
9021 truncated: false,
9022 };
9023 assert!(HarnessRunWorkspaceInspectionV1 {
9024 origin: origin.clone(),
9025 entries: entries.clone(),
9026 tree_truncated: false,
9027 git: empty_git.clone(),
9028 truncation: None,
9029 }.validate().is_ok());
9030 let mut too_many_entries = entries;
9031 too_many_entries.push(HarnessWorkspaceTreeEntryV1 {
9032 relative_path: HarnessRepositoryPathV1::new("entry-0512").unwrap(),
9033 kind: HarnessWorkspaceEntryKindV1::File,
9034 });
9035 assert!(HarnessRunWorkspaceInspectionV1 {
9036 origin: origin.clone(),
9037 entries: too_many_entries,
9038 tree_truncated: true,
9039 git: empty_git,
9040 truncation: None,
9041 }.validate().is_err());
9042
9043 let status: Vec<_> = (0..HARNESS_GIT_STATUS_ENTRIES_MAX)
9044 .map(|index| HarnessGitStatusEntryV1 {
9045 index_status: HarnessGitStatusCodeV1::Unmodified,
9046 worktree_status: HarnessGitStatusCodeV1::Modified,
9047 path: HarnessRepositoryPathV1::new(format!("status-{index:04}")).unwrap(),
9048 previous_path: None,
9049 })
9050 .collect();
9051 let recent_commits: Vec<_> = (0..HARNESS_GIT_RECENT_COMMITS_MAX)
9052 .map(|index| HarnessGitCommitSummaryV1 {
9053 id: HarnessGitObjectIdV1::new(format!("{index:040x}")).unwrap(),
9054 summary: format!("commit {index}"),
9055 })
9056 .collect();
9057 assert!(HarnessGitSummaryV1 {
9058 is_repository: true,
9059 branch: Some("main".to_owned()),
9060 status: status.clone(),
9061 recent_commits: recent_commits.clone(),
9062 truncated: false,
9063 }.validate().is_ok());
9064 let mut too_many_status = status;
9065 too_many_status.push(HarnessGitStatusEntryV1 {
9066 index_status: HarnessGitStatusCodeV1::Unmodified,
9067 worktree_status: HarnessGitStatusCodeV1::Modified,
9068 path: HarnessRepositoryPathV1::new("status-0128").unwrap(),
9069 previous_path: None,
9070 });
9071 assert!(HarnessGitSummaryV1 {
9072 is_repository: true,
9073 branch: Some("main".to_owned()),
9074 status: too_many_status,
9075 recent_commits: Vec::new(),
9076 truncated: true,
9077 }.validate().is_err());
9078 let mut too_many_recent = recent_commits;
9079 too_many_recent.push(HarnessGitCommitSummaryV1 {
9080 id: HarnessGitObjectIdV1::new(format!("{:040x}", HARNESS_GIT_RECENT_COMMITS_MAX)).unwrap(),
9081 summary: "one too many".to_owned(),
9082 });
9083 assert!(HarnessGitSummaryV1 {
9084 is_repository: true,
9085 branch: Some("main".to_owned()),
9086 status: Vec::new(),
9087 recent_commits: too_many_recent,
9088 truncated: true,
9089 }.validate().is_err());
9090
9091 let commits: Vec<_> = (0..HARNESS_GIT_HISTORY_LIMIT_MAX)
9092 .map(|index| {
9093 let mut commit = git_commit('a');
9094 commit.id = HarnessGitObjectIdV1::new(format!("{index:040x}")).unwrap();
9095 commit
9096 })
9097 .collect();
9098 assert!(HarnessRunGitHistoryPageV1 {
9099 origin: origin.clone(),
9100 path: Some(path.clone()),
9101 commits: commits.clone(),
9102 next_before: None,
9103 truncated: false,
9104 }.validate().is_ok());
9105 let next_before = commits.last().map(|commit| commit.id.clone());
9106 assert!(HarnessRunGitHistoryPageV1 {
9107 origin: origin.clone(),
9108 path: Some(path.clone()),
9109 commits: commits.clone(),
9110 next_before: next_before.clone(),
9111 truncated: false,
9112 }.validate().is_ok());
9113 assert!(HarnessRunGitHistoryPageV1 {
9114 origin: origin.clone(),
9115 path: Some(path.clone()),
9116 commits: commits.clone(),
9117 next_before: None,
9118 truncated: true,
9119 }.validate().is_ok());
9120 assert!(HarnessRunGitHistoryPageV1 {
9121 origin: origin.clone(),
9122 path: Some(path.clone()),
9123 commits: commits.clone(),
9124 next_before,
9125 truncated: true,
9126 }.validate().is_ok());
9127 let mut too_many_commits = commits;
9128 let mut extra_commit = git_commit('a');
9129 extra_commit.id = HarnessGitObjectIdV1::new(format!("{:040x}", HARNESS_GIT_HISTORY_LIMIT_MAX)).unwrap();
9130 too_many_commits.push(extra_commit);
9131 assert!(HarnessRunGitHistoryPageV1 {
9132 origin,
9133 path: Some(path),
9134 commits: too_many_commits,
9135 next_before: None,
9136 truncated: false,
9137 }.validate().is_err());
9138 }
9139
9140 fn monitor_with_mixed_capabilities() -> SessionMonitorV1 {
9141 SessionMonitorV1 {
9142 run_id: HarnessRunId::new("hrun_000000000000000000000001").unwrap(),
9143 visibility: HarnessMonitoringVisibilityV1::Detail,
9144 availability: ProjectionAvailabilityV1::Frozen,
9145 freshness: ProjectionFreshnessV1::ReplacedIncarnation,
9146 transport_incomplete: false,
9147 features: MonitorFeatureStatesV1 {
9148 todo: FeatureObservationStateV1::Observed,
9149 tools: FeatureObservationStateV1::NotSupportedByObservedSources,
9150 subagents: FeatureObservationStateV1::Unknown,
9151 interactions: FeatureObservationStateV1::SupportedNotObserved,
9152 owned_processes: FeatureObservationStateV1::Observed,
9153 files: FeatureObservationStateV1::Observed,
9154 usage: FeatureObservationStateV1::SupportedNotObserved,
9155 history: FeatureObservationStateV1::Unknown,
9156 },
9157 todo_total: 1,
9158 todo_completed: 0,
9159 active_tools: 0,
9160 active_subagents: 0,
9161 active_interactions: 0,
9162 active_blocks: 0,
9163 active_processes: 0,
9164 input_tokens: 0,
9165 output_tokens: 0,
9166 cache_read_tokens: 0,
9167 cache_write_tokens: 0,
9168 reasoning_tokens: 0,
9169 context_window_tokens: None,
9170 history: None,
9171 detail: Some(SessionMonitorDetailV1 {
9172 todo_facts: vec![TodoFactV1 {
9173 state: TodoStateV1::Unknown,
9174 todo_id: Some("todo-1".to_owned()),
9175 label: Some("review result".to_owned()),
9176 evidence: ObservationEvidenceV1::PtyHint,
9177 }],
9178 tool_facts: Vec::new(),
9179 subagent_facts: Vec::new(),
9180 interaction_facts: Vec::new(),
9181 block_facts: Vec::new(),
9182 process_facts: vec![ActivityFactV1 {
9183 class: ActivityClassV1::OwnedProcess,
9184 state: ActivityStateV1::UnknownAfterGap,
9185 label: Some("compiler".to_owned()),
9186 correlation: Some(1),
9187 evidence: ObservationEvidenceV1::ManagedHook,
9188 }],
9189 file_facts: vec![FileFactV1 {
9190 action: FileActionV1::Changed,
9191 relative_path: Some("src/lib.rs".to_owned()),
9192 evidence: ObservationEvidenceV1::StructuredProvider,
9193 }],
9194 }),
9195 }
9196 }
9197
9198 #[test]
9199 fn monitor_preserves_mixed_capability_and_frozen_replaced_truth() {
9200 let monitor = monitor_with_mixed_capabilities();
9201 monitor.validate().expect("truthful mixed monitor");
9202 let encoded = serde_json::to_string(&monitor).expect("encode monitor");
9203 let decoded: SessionMonitorV1 = serde_json::from_str(&encoded).expect("decode monitor");
9204 assert_eq!(decoded, monitor);
9205 assert_eq!(decoded.availability, ProjectionAvailabilityV1::Frozen);
9206 assert_eq!(decoded.freshness, ProjectionFreshnessV1::ReplacedIncarnation);
9207 assert_eq!(decoded.features.tools, FeatureObservationStateV1::NotSupportedByObservedSources);
9208 let detail = decoded.detail.unwrap();
9209 assert_eq!(detail.todo_facts[0].state, TodoStateV1::Unknown);
9210 assert_eq!(detail.todo_facts[0].todo_id.as_deref(), Some("todo-1"));
9211 assert_eq!(detail.todo_facts[0].label.as_deref(), Some("review result"));
9212 assert_eq!(detail.process_facts[0].correlation, Some(1));
9213 assert_eq!(detail.file_facts[0].relative_path.as_deref(), Some("src/lib.rs"));
9214 }
9215
9216 #[test]
9217 fn monitor_rejects_inference_and_raw_detail_sentinels() {
9218 let mut monitor = monitor_with_mixed_capabilities();
9219 monitor.detail.as_mut().unwrap().tool_facts.push(ActivityFactV1 {
9220 class: ActivityClassV1::Tool,
9221 state: ActivityStateV1::Active,
9222 label: Some("command".to_owned()),
9223 correlation: Some(1),
9224 evidence: ObservationEvidenceV1::StructuredProvider,
9225 });
9226 assert!(monitor.validate().is_err());
9227
9228 let raw_file = r#"{"action":"changed","evidence":"managed-hook","path":"C:\\private\\prompt.txt"}"#;
9229 assert!(serde_json::from_str::<FileFactV1>(raw_file).is_err());
9230 let encoded = serde_json::to_string(&monitor_with_mixed_capabilities()).unwrap();
9231 for sentinel in ["private", "prompt.txt", "provider_id", "correlation_id"] {
9232 assert!(!encoded.contains(sentinel));
9233 }
9234
9235 let mut partial = monitor_with_mixed_capabilities();
9236 partial.availability = ProjectionAvailabilityV1::Partial;
9237 partial.freshness = ProjectionFreshnessV1::IncompleteAfterGap;
9238 partial.transport_incomplete = true;
9239 partial.validate().expect("truthful partial gap");
9240 }
9241
9242 #[test]
9243 fn timeline_structured_fields_round_trip_without_raw_correlation_shape() {
9244 let run_id = HarnessRunId::new("hrun_000000000000000000000001").unwrap();
9245 let page = TimelinePageV1 {
9246 run_id: run_id.clone(),
9247 availability: ProjectionAvailabilityV1::Current,
9248 freshness: ProjectionFreshnessV1::Live,
9249 transport_incomplete: false,
9250 entries: vec![TimelineEntryV1 {
9251 sequence: 7,
9252 received_at_ms: 10,
9253 category: TimelineCategoryV1::Tool,
9254 label: Some("command".to_owned()),
9255 state: TimelineStateV1::Completed,
9256 correlation: Some(1),
9257 evidence: ObservationEvidenceV1::StructuredProvider,
9258 }],
9259 next_cursor: None,
9260 };
9261 page.validate_for(&run_id).unwrap();
9262 let encoded = serde_json::to_string(&page).unwrap();
9263 assert!(encoded.contains("\"label\":\"command\""));
9264 assert!(encoded.contains("\"state\":\"completed\""));
9265 assert!(encoded.contains("\"correlation\":1"));
9266 assert!(!encoded.contains("correlation_id"));
9267 assert_eq!(serde_json::from_str::<TimelinePageV1>(&encoded).unwrap(), page);
9268
9269 let old_entry: TimelineEntryV1 = serde_json::from_str(
9270 r#"{"sequence":1,"received_at_ms":2,"category":"usage","evidence":"managed-hook"}"#,
9271 ).unwrap();
9272 assert_eq!(old_entry.state, TimelineStateV1::Unknown);
9273 assert_eq!(old_entry.correlation, None);
9274 old_entry.validate().unwrap();
9275 }
9276
9277 #[test]
9278 fn monitor_history_summary_matches_categorical_feature_state() {
9279 let mut monitor = monitor_with_mixed_capabilities();
9280 monitor.features.history = FeatureObservationStateV1::Observed;
9281 monitor.history = Some(SessionMonitorHistoryV1 {
9282 message_count: 12,
9283 message_count_exact: true,
9284 completed_turn_count: Some(3),
9285 total_tokens: Some(800),
9286 });
9287 monitor.validate().unwrap();
9288 let encoded = serde_json::to_string(&monitor).unwrap();
9289 let decoded: SessionMonitorV1 = serde_json::from_str(&encoded).unwrap();
9290 assert_eq!(decoded.history, monitor.history);
9291
9292 monitor.history = None;
9293 monitor.validate().expect("redacted observed history remains valid");
9294 monitor.features.history = FeatureObservationStateV1::NotSupportedByObservedSources;
9295 monitor.history = Some(SessionMonitorHistoryV1 {
9296 message_count: 1,
9297 message_count_exact: false,
9298 completed_turn_count: None,
9299 total_tokens: None,
9300 });
9301 assert!(monitor.validate().is_err());
9302 }
9303
9304 #[test]
9305 fn structured_file_fact_rejects_absolute_or_traversing_paths() {
9306 for relative_path in ["C:/private.txt", "/private.txt", "src/../private.txt", "src\\private.txt"] {
9307 let fact = FileFactV1 {
9308 action: FileActionV1::Changed,
9309 relative_path: Some(relative_path.to_owned()),
9310 evidence: ObservationEvidenceV1::WorkspaceObservation,
9311 };
9312 assert!(fact.validate().is_err(), "accepted {relative_path}");
9313 }
9314 }
9315
9316 #[test]
9317 fn operator_reverse_attribution_is_exact_and_private() {
9318 let subject = HarnessReverseAttributionSubjectV1::FileScope {
9319 workspace: reverse_attribution_workspace(),
9320 relative_path: HarnessRepositoryPathV1::new("src/lib.rs").unwrap(),
9321 };
9322 let request = HarnessOperatorRequestV1::ReverseAttributionGet {
9323 subject: subject.clone(),
9324 };
9325 request.validate().unwrap();
9326 let credential = HarnessOperatorCredential::parse(format!(
9327 "g4aho_{}",
9328 "a".repeat(64),
9329 )).unwrap();
9330 HarnessOperatorEnvelopeV1 {
9331 build_stamp: BUILD_STAMP.to_string(),
9332 credential,
9333 request,
9334 }.validate().unwrap();
9335
9336 let response = HarnessReverseAttributionV1 {
9337 subject,
9338 outcome: HarnessReverseAttributionOutcomeV1::Attributed,
9339 links: vec![reverse_attribution_link('2')],
9340 };
9341 response.validate().unwrap();
9342 let encoded = serde_json::to_value(
9343 HarnessOperatorResponseV1::ReverseAttribution(response),
9344 ).unwrap();
9345 assert_eq!(
9346 encoded,
9347 serde_json::json!({
9348 "kind": "reverse-attribution",
9349 "value": {
9350 "subject": {
9351 "kind": "file-scope",
9352 "workspace": {
9353 "node_id": "node-a",
9354 "node_incarnation_id": "07070707070707070707070707070707",
9355 "workspace_id": "workspace-a"
9356 },
9357 "relative_path": "src/lib.rs"
9358 },
9359 "outcome": "attributed",
9360 "links": [{
9361 "task_id": "htask_111111111111111111111111",
9362 "run_id": "hrun_222222222222222222222222",
9363 "run_revision": 3,
9364 "binding": {
9365 "kind": "workspace",
9366 "workspace": {
9367 "node_id": "node-a",
9368 "node_incarnation_id": "07070707070707070707070707070707",
9369 "workspace_id": "workspace-a"
9370 }
9371 },
9372 "relation": "workspace-scope"
9373 }]
9374 }
9375 }),
9376 );
9377 let encoded = encoded.to_string();
9378 for forbidden in [
9379 "produced-by",
9380 "modified-by",
9381 "provider_session",
9382 "provider_profile",
9383 "provider_home",
9384 "credential",
9385 "auth",
9386 "canonical_root",
9387 "display_root",
9388 "source_path",
9389 ] {
9390 assert!(!encoded.contains(forbidden), "leaked {forbidden}");
9391 }
9392
9393 let mut unknown = serde_json::to_value(HarnessOperatorRequestV1::ReverseAttributionGet {
9394 subject: HarnessReverseAttributionSubjectV1::Workspace {
9395 workspace: reverse_attribution_workspace(),
9396 },
9397 }).unwrap();
9398 unknown["subject"]["provider_session_id"] = serde_json::json!("forbidden");
9399 assert!(serde_json::from_value::<HarnessOperatorRequestV1>(unknown).is_err());
9400 }
9401
9402 #[test]
9403 fn reverse_attribution_is_bounded_canonical_and_exactly_correlated() {
9404 let subject = HarnessReverseAttributionSubjectV1::FileScope {
9405 workspace: reverse_attribution_workspace(),
9406 relative_path: HarnessRepositoryPathV1::new("src/lib.rs").unwrap(),
9407 };
9408 let first = reverse_attribution_link('2');
9409 let second = reverse_attribution_link('3');
9410 let exact = HarnessReverseAttributionV1 {
9411 subject: subject.clone(),
9412 outcome: HarnessReverseAttributionOutcomeV1::Attributed,
9413 links: vec![first.clone(), second.clone()],
9414 };
9415 exact.validate_for(&subject).unwrap();
9416
9417 let mut noncanonical = exact.clone();
9418 noncanonical.links.reverse();
9419 assert!(matches!(
9420 noncanonical.validate(),
9421 Err(HarnessOperatorApiError::InvalidReverseAttribution),
9422 ));
9423 let duplicate = HarnessReverseAttributionV1 {
9424 links: vec![first.clone(), first],
9425 ..exact.clone()
9426 };
9427 assert!(duplicate.validate().is_err());
9428 let over_limit = HarnessReverseAttributionV1 {
9429 links: (0..=HARNESS_REVERSE_ATTRIBUTION_LINKS_MAX)
9430 .map(|index| HarnessReverseAttributionLinkV1 {
9431 task_id: HarnessTaskId::new(format!("htask_{index:024x}")).unwrap(),
9432 ..reverse_attribution_link('4')
9433 })
9434 .collect(),
9435 ..exact.clone()
9436 };
9437 assert!(over_limit.validate().is_err());
9438
9439 let unattributed = HarnessReverseAttributionV1 {
9440 subject: subject.clone(),
9441 outcome: HarnessReverseAttributionOutcomeV1::Unattributed,
9442 links: Vec::new(),
9443 };
9444 unattributed.validate().unwrap();
9445 let attributed_empty = HarnessReverseAttributionV1 {
9446 outcome: HarnessReverseAttributionOutcomeV1::Attributed,
9447 ..unattributed.clone()
9448 };
9449 assert!(attributed_empty.validate().is_err());
9450 let unattributed_linked = HarnessReverseAttributionV1 {
9451 outcome: HarnessReverseAttributionOutcomeV1::Unattributed,
9452 links: vec![second],
9453 ..exact.clone()
9454 };
9455 assert!(unattributed_linked.validate().is_err());
9456
9457 let other_subject = HarnessReverseAttributionSubjectV1::FileScope {
9458 workspace: reverse_attribution_workspace(),
9459 relative_path: HarnessRepositoryPathV1::new("src/other.rs").unwrap(),
9460 };
9461 assert!(matches!(
9462 exact.validate_for(&other_subject),
9463 Err(HarnessOperatorApiError::InvalidReverseAttribution),
9464 ));
9465 }
9466
9467 #[test]
9468 fn reverse_attribution_relations_reject_false_file_and_binding_claims() {
9469 let file_subject = HarnessReverseAttributionSubjectV1::FileScope {
9470 workspace: reverse_attribution_workspace(),
9471 relative_path: HarnessRepositoryPathV1::new("src/lib.rs").unwrap(),
9472 };
9473 let mut wrong_relation = reverse_attribution_link('2');
9474 wrong_relation.relation = HarnessReverseAttributionRelationV1::WorkspaceBinding;
9475 assert!(HarnessReverseAttributionV1 {
9476 subject: file_subject,
9477 outcome: HarnessReverseAttributionOutcomeV1::Attributed,
9478 links: vec![wrong_relation],
9479 }.validate().is_err());
9480
9481 let managed_subject = HarnessReverseAttributionSubjectV1::ManagedRecord {
9482 workspace: reverse_attribution_workspace(),
9483 record_id: HarnessSelectorV1::new("record-a").unwrap(),
9484 };
9485 let mismatched_record = HarnessReverseAttributionLinkV1 {
9486 task_id: HarnessTaskId::new(format!("htask_{}", "1".repeat(24))).unwrap(),
9487 run_id: HarnessRunId::new(format!("hrun_{}", "2".repeat(24))).unwrap(),
9488 run_revision: HarnessRevision::new(3).unwrap(),
9489 binding: HarnessReverseAttributionBindingV1::ManagedRecord {
9490 workspace: reverse_attribution_workspace(),
9491 record_id: HarnessSelectorV1::new("record-b").unwrap(),
9492 active_instance_id: Some(41),
9493 active_generation: Some(3),
9494 },
9495 relation: HarnessReverseAttributionRelationV1::ManagedRecordBinding,
9496 };
9497 assert!(HarnessReverseAttributionV1 {
9498 subject: managed_subject,
9499 outcome: HarnessReverseAttributionOutcomeV1::Attributed,
9500 links: vec![mismatched_record],
9501 }.validate().is_err());
9502
9503 let invalid_active_pair = HarnessReverseAttributionBindingV1::ManagedRecord {
9504 workspace: reverse_attribution_workspace(),
9505 record_id: HarnessSelectorV1::new("record-a").unwrap(),
9506 active_instance_id: Some(41),
9507 active_generation: None,
9508 };
9509 assert!(invalid_active_pair.validate().is_err());
9510 }
9511
9512 #[test]
9513 fn operator_node_workspace_requests_are_exact_round_trips() {
9514 let node_id = node_workspace_origin().node_id;
9515 let workspace_id = node_workspace_origin().workspace_id;
9516 let path = HarnessRepositoryPathV1::new("src/lib.rs").unwrap();
9517 let object_id = HarnessGitObjectIdV1::new("a".repeat(40)).unwrap();
9518 let requests = vec![
9519 HarnessOperatorRequestV1::InspectNodeWorkspace {
9520 node_id: node_id.clone(),
9521 workspace_id: workspace_id.clone(),
9522 },
9523 HarnessOperatorRequestV1::ReadNodeWorkspaceFile {
9524 node_id: node_id.clone(),
9525 workspace_id: workspace_id.clone(),
9526 path: path.clone(),
9527 },
9528 HarnessOperatorRequestV1::ReadNodeGitHistory {
9529 node_id: node_id.clone(),
9530 workspace_id: workspace_id.clone(),
9531 path: Some(path.clone()),
9532 before: Some(object_id.clone()),
9533 limit: HARNESS_GIT_HISTORY_LIMIT_MAX,
9534 },
9535 HarnessOperatorRequestV1::ReadNodeGitDiff {
9536 node_id: node_id.clone(),
9537 workspace_id: workspace_id.clone(),
9538 mode: HarnessGitDiffModeV1::Commit { revision: object_id },
9539 path: Some(path),
9540 },
9541 ];
9542 let credential = HarnessOperatorCredential::parse(format!(
9543 "g4aho_{}",
9544 "a".repeat(64),
9545 )).unwrap();
9546 for request in requests {
9547 request.validate().expect("valid node-workspace request");
9548 let encoded = serde_json::to_string(&request).unwrap();
9549 for forbidden in ["run_id", "endpoint", "root", "worktree", "environment"] {
9550 assert!(!encoded.contains(forbidden), "request exposed {forbidden}");
9551 }
9552 let decoded: HarnessOperatorRequestV1 = serde_json::from_str(&encoded).unwrap();
9553 assert_eq!(decoded, request);
9554 HarnessOperatorEnvelopeV1 {
9555 build_stamp: BUILD_STAMP.to_string(),
9556 credential: credential.clone(),
9557 request,
9558 }.validate().unwrap();
9559 }
9560
9561 assert!(HarnessOperatorRequestV1::InspectNodeWorkspace {
9562 node_id: String::new(),
9563 workspace_id,
9564 }.validate().is_err());
9565 assert!(HarnessOperatorRequestV1::InspectNodeWorkspace {
9566 node_id,
9567 workspace_id: String::new(),
9568 }.validate().is_err());
9569 }
9570
9571 #[test]
9572 fn operator_v9_node_workspace_replies_round_trip_without_run_or_diagnostic_fields() {
9573 let origin = node_workspace_origin();
9574 let path = HarnessRepositoryPathV1::new("src/lib.rs").unwrap();
9575 let commit = git_commit('a');
9576 let responses = vec![
9577 HarnessOperatorResponseV1::NodeWorkspaceInspected(
9578 HarnessNodeWorkspaceInspectionV1 {
9579 origin: origin.clone(),
9580 entries: vec![HarnessWorkspaceTreeEntryV1 {
9581 relative_path: path.clone(),
9582 kind: HarnessWorkspaceEntryKindV1::File,
9583 }],
9584 tree_truncated: false,
9585 git: HarnessGitSummaryV1 {
9586 is_repository: true,
9587 branch: Some("main".to_owned()),
9588 status: vec![HarnessGitStatusEntryV1 {
9589 index_status: HarnessGitStatusCodeV1::Unmodified,
9590 worktree_status: HarnessGitStatusCodeV1::Modified,
9591 path: path.clone(),
9592 previous_path: None,
9593 }],
9594 recent_commits: vec![HarnessGitCommitSummaryV1 {
9595 id: commit.id.clone(),
9596 summary: commit.subject.clone(),
9597 }],
9598 truncated: false,
9599 },
9600 truncation: None,
9601 },
9602 ),
9603 HarnessOperatorResponseV1::NodeWorkspaceFileRead(HarnessNodeWorkspaceFileV1 {
9604 origin: origin.clone(),
9605 path: path.clone(),
9606 content: HarnessWorkspaceFileContentV1::Utf8 {
9607 text: "fn main() {}\n".to_owned(),
9608 byte_len: 13,
9609 },
9610 revision: Some(HarnessWorkspaceFileRevisionV1::new("b".repeat(64)).unwrap()),
9611 }),
9612 HarnessOperatorResponseV1::NodeGitHistoryRead(HarnessNodeGitHistoryPageV1 {
9613 origin: origin.clone(),
9614 path: Some(path.clone()),
9615 commits: vec![commit],
9616 next_before: None,
9617 truncated: false,
9618 }),
9619 HarnessOperatorResponseV1::NodeGitDiffRead(HarnessNodeGitDiffV1 {
9620 origin,
9621 mode: HarnessGitDiffModeV1::Working,
9622 path: Some(path),
9623 text: "diff --git a/src/lib.rs b/src/lib.rs\n".to_owned(),
9624 truncated: false,
9625 }),
9626 ];
9627 for response in responses {
9628 response.validate().expect("valid V9 node-workspace response");
9629 let encoded = serde_json::to_string(&response).unwrap();
9630 assert_json_has_no_forbidden_keys(&encoded, &[
9631 "run_id",
9632 "run_revision",
9633 "root",
9634 "worktree",
9635 "endpoint",
9636 "diagnostic",
9637 "author_email",
9638 "committer_email",
9639 "environment",
9640 ]);
9641 let decoded: HarnessOperatorResponseV1 = serde_json::from_str(&encoded).unwrap();
9642 assert_eq!(decoded, response);
9643 }
9644
9645 let mismatched_workspace = HarnessNodeWorkspaceOriginV1 {
9646 workspace_id: "other-workspace".to_owned(),
9647 ..node_workspace_origin()
9648 };
9649 assert!(mismatched_workspace.validate_for("node-a", "workspace-a").is_err());
9650 mismatched_workspace.validate_for("node-a", "other-workspace").unwrap();
9651 }
9652
9653 #[test]
9654 fn operator_node_workspace_write_verbs_are_exact_round_trips() {
9655 let node_id = node_workspace_origin().node_id;
9656 let workspace_id = node_workspace_origin().workspace_id;
9657 let path = HarnessRepositoryPathV1::new("src/lib.rs").unwrap();
9658 let revision = HarnessWorkspaceFileRevisionV1::new("b".repeat(64)).unwrap();
9659 let requests = vec![
9660 HarnessOperatorRequestV1::WriteNodeWorkspaceFile {
9661 node_id: node_id.clone(),
9662 workspace_id: workspace_id.clone(),
9663 path: path.clone(),
9664 content: "fn main() {}\n".to_owned(),
9665 expected_revision: revision.clone(),
9666 },
9667 HarnessOperatorRequestV1::CreateNodeWorkspaceFile {
9668 node_id: node_id.clone(),
9669 workspace_id: workspace_id.clone(),
9670 path: path.clone(),
9671 },
9672 HarnessOperatorRequestV1::CreateNodeWorkspaceDirectory {
9673 node_id: node_id.clone(),
9674 workspace_id: workspace_id.clone(),
9675 path: path.clone(),
9676 },
9677 ];
9678 let credential = HarnessOperatorCredential::parse(format!(
9679 "g4aho_{}",
9680 "a".repeat(64),
9681 )).unwrap();
9682 for request in requests {
9683 request.validate().expect("valid node-workspace write request");
9684 let encoded = serde_json::to_string(&request).unwrap();
9685 for forbidden in ["run_id", "endpoint", "root", "worktree", "environment"] {
9686 assert!(!encoded.contains(forbidden), "request exposed {forbidden}");
9687 }
9688 let decoded: HarnessOperatorRequestV1 = serde_json::from_str(&encoded).unwrap();
9689 assert_eq!(decoded, request);
9690 HarnessOperatorEnvelopeV1 {
9691 build_stamp: BUILD_STAMP.to_string(),
9692 credential: credential.clone(),
9693 request,
9694 }.validate().unwrap();
9695 }
9696
9697 let origin = node_workspace_origin();
9698 let responses = vec![
9699 HarnessOperatorResponseV1::NodeWorkspaceFileWritten(HarnessNodeWorkspaceFileV1 {
9700 origin: origin.clone(),
9701 path: path.clone(),
9702 content: HarnessWorkspaceFileContentV1::Utf8 {
9703 text: "fn main() {}\n".to_owned(),
9704 byte_len: 13,
9705 },
9706 revision: Some(revision.clone()),
9707 }),
9708 HarnessOperatorResponseV1::NodeWorkspaceFileCreated(HarnessNodeWorkspaceFileV1 {
9709 origin: origin.clone(),
9710 path: path.clone(),
9711 content: HarnessWorkspaceFileContentV1::Utf8 {
9712 text: String::new(),
9713 byte_len: 0,
9714 },
9715 revision: Some(revision.clone()),
9716 }),
9717 HarnessOperatorResponseV1::NodeWorkspaceDirectoryCreated(
9718 HarnessNodeWorkspaceDirectoryV1 {
9719 origin: origin.clone(),
9720 entry: HarnessWorkspaceTreeEntryV1 {
9721 relative_path: path.clone(),
9722 kind: HarnessWorkspaceEntryKindV1::Directory,
9723 },
9724 },
9725 ),
9726 ];
9727 for response in responses {
9728 response.validate().expect("valid V11 node-workspace write response");
9729 let encoded = serde_json::to_string(&response).unwrap();
9730 assert_json_has_no_forbidden_keys(&encoded, &[
9731 "run_id",
9732 "run_revision",
9733 "root",
9734 "worktree",
9735 "endpoint",
9736 "diagnostic",
9737 "environment",
9738 ]);
9739 let decoded: HarnessOperatorResponseV1 = serde_json::from_str(&encoded).unwrap();
9740 assert_eq!(decoded, response);
9741 }
9742
9743 let wrong_kind_directory = HarnessNodeWorkspaceDirectoryV1 {
9744 origin,
9745 entry: HarnessWorkspaceTreeEntryV1 {
9746 relative_path: path,
9747 kind: HarnessWorkspaceEntryKindV1::File,
9748 },
9749 };
9750 assert!(wrong_kind_directory.validate().is_err());
9751 }
9752
9753 #[test]
9754 fn node_workspace_write_v11_requests_reject_malformed_fields() {
9755 let node_id = node_workspace_origin().node_id;
9756 let workspace_id = node_workspace_origin().workspace_id;
9757 let path = HarnessRepositoryPathV1::new("src/lib.rs").unwrap();
9758 let revision = HarnessWorkspaceFileRevisionV1::new("b".repeat(64)).unwrap();
9759
9760 let oversized_content = HarnessOperatorRequestV1::WriteNodeWorkspaceFile {
9761 node_id: node_id.clone(),
9762 workspace_id: workspace_id.clone(),
9763 path: path.clone(),
9764 content: "x".repeat(HARNESS_WORKSPACE_FILE_MAX_BYTES + 1),
9765 expected_revision: revision.clone(),
9766 };
9767 assert!(matches!(
9768 oversized_content.validate(),
9769 Err(HarnessOperatorApiError::InvalidWorkspaceFile),
9770 ));
9771 let bounded_content = HarnessOperatorRequestV1::WriteNodeWorkspaceFile {
9772 node_id: node_id.clone(),
9773 workspace_id: workspace_id.clone(),
9774 path: path.clone(),
9775 content: "x".repeat(HARNESS_WORKSPACE_FILE_MAX_BYTES),
9776 expected_revision: revision.clone(),
9777 };
9778 bounded_content.validate().unwrap();
9779
9780 assert!(HarnessWorkspaceFileRevisionV1::new("not-a-sha256-digest").is_err());
9781 assert!(HarnessWorkspaceFileRevisionV1::new("A".repeat(64)).is_err());
9782
9783 let empty_node_write = HarnessOperatorRequestV1::WriteNodeWorkspaceFile {
9784 node_id: String::new(),
9785 workspace_id: workspace_id.clone(),
9786 path: path.clone(),
9787 content: "ok".to_owned(),
9788 expected_revision: revision,
9789 };
9790 assert!(empty_node_write.validate().is_err());
9791
9792 let empty_workspace_create_file = HarnessOperatorRequestV1::CreateNodeWorkspaceFile {
9793 node_id: node_id.clone(),
9794 workspace_id: String::new(),
9795 path: path.clone(),
9796 };
9797 assert!(empty_workspace_create_file.validate().is_err());
9798
9799 let empty_node_create_directory = HarnessOperatorRequestV1::CreateNodeWorkspaceDirectory {
9800 node_id: String::new(),
9801 workspace_id,
9802 path,
9803 };
9804 assert!(empty_node_create_directory.validate().is_err());
9805 }
9806
9807 #[test]
9808 fn operator_context_source_observation_is_exact_and_private() {
9809 let run_id = HarnessRunId::new(format!("hrun_{}", "8".repeat(24))).unwrap();
9810 let request = HarnessOperatorRequestV1::ObserveRunContextSource {
9811 run_id: run_id.clone(),
9812 };
9813 request.validate().unwrap();
9814 let request_json = serde_json::to_value(&request).unwrap();
9815 assert_eq!(
9816 request_json,
9817 serde_json::json!({
9818 "kind": "observe-run-context-source",
9819 "run_id": run_id,
9820 }),
9821 );
9822 for forbidden in ["authority", "operation_id", "idempotency_ref", "task_id"] {
9823 assert!(!request_json.to_string().contains(forbidden), "leaked {forbidden}");
9824 }
9825 let credential = HarnessOperatorCredential::parse(format!(
9826 "g4aho_{}",
9827 "a".repeat(64),
9828 )).unwrap();
9829 HarnessOperatorEnvelopeV1 {
9830 build_stamp: BUILD_STAMP.to_string(),
9831 credential,
9832 request,
9833 }.validate().unwrap();
9834
9835 let response = HarnessRunContextSourceObservationV1 {
9836 run_id: HarnessRunId::new(format!("hrun_{}", "8".repeat(24))).unwrap(),
9837 run_revision: HarnessRevision::new(9).unwrap(),
9838 feature_state: FeatureObservationStateV1::Observed,
9839 message_count: 17,
9840 message_count_exact: true,
9841 completed_turn_count: Some(6),
9842 total_tokens: Some(4_096),
9843 observed_at_unix_ms: Some(1_000),
9844 };
9845 response.validate().unwrap();
9846 let response_json = serde_json::to_value(
9847 HarnessOperatorResponseV1::RunContextSourceObserved(response),
9848 ).unwrap();
9849 assert_eq!(
9850 response_json,
9851 serde_json::json!({
9852 "kind": "run-context-source-observed",
9853 "value": {
9854 "run_id": format!("hrun_{}", "8".repeat(24)),
9855 "run_revision": 9,
9856 "feature_state": "observed",
9857 "message_count": 17,
9858 "message_count_exact": true,
9859 "completed_turn_count": 6,
9860 "total_tokens": 4096,
9861 "observed_at_unix_ms": 1000,
9862 }
9863 }),
9864 );
9865 let response_json = response_json.to_string();
9866 for forbidden in [
9867 "transcript",
9868 "message_text",
9869 "provider_session",
9870 "provider_profile",
9871 "provider_home",
9872 "credential",
9873 "auth",
9874 "path",
9875 "model",
9876 ] {
9877 assert!(!response_json.contains(forbidden), "leaked {forbidden}");
9878 }
9879
9880 let mut unknown = request_json;
9881 unknown["provider_session_id"] = serde_json::json!("forbidden");
9882 assert!(serde_json::from_value::<HarnessOperatorRequestV1>(unknown).is_err());
9883 }
9884
9885 #[test]
9886 fn context_source_observation_rejects_inexact_or_mismatched_eligibility_claims() {
9887 let run_id = HarnessRunId::new(format!("hrun_{}", "8".repeat(24))).unwrap();
9888 let observed = HarnessRunContextSourceObservationV1 {
9889 run_id: run_id.clone(),
9890 run_revision: HarnessRevision::new(9).unwrap(),
9891 feature_state: FeatureObservationStateV1::Observed,
9892 message_count: 17,
9893 message_count_exact: true,
9894 completed_turn_count: Some(6),
9895 total_tokens: Some(4_096),
9896 observed_at_unix_ms: Some(1_000),
9897 };
9898 observed.validate_for(&run_id).unwrap();
9899
9900 let mut inexact = observed.clone();
9901 inexact.message_count_exact = false;
9902 assert!(inexact.validate().is_err());
9903 let mut empty = observed.clone();
9904 empty.message_count = 0;
9905 assert!(empty.validate().is_err());
9906 let mut impossible_turns = observed.clone();
9907 impossible_turns.completed_turn_count = Some(18);
9908 assert!(impossible_turns.validate().is_err());
9909 let unobserved = HarnessRunContextSourceObservationV1 {
9910 feature_state: FeatureObservationStateV1::SupportedNotObserved,
9911 message_count: 0,
9912 message_count_exact: false,
9913 completed_turn_count: None,
9914 total_tokens: None,
9915 observed_at_unix_ms: None,
9916 ..observed.clone()
9917 };
9918 unobserved.validate().unwrap();
9919 let mut leaked_count = unobserved;
9920 leaked_count.total_tokens = Some(1);
9921 assert!(leaked_count.validate().is_err());
9922
9923 let other_run_id = HarnessRunId::new(format!("hrun_{}", "9".repeat(24))).unwrap();
9924 assert!(matches!(
9925 observed.validate_for(&other_run_id),
9926 Err(HarnessOperatorApiError::InvalidRunContextSourceObservation),
9927 ));
9928 }
9929
9930 fn session_address(instance_id: u64, generation: u64) -> HarnessRuntimeSessionAddressV1 {
9931 HarnessRuntimeSessionAddressV1 {
9932 node_id: "node-a".to_owned(),
9933 incarnation_id: "07".repeat(16),
9934 workspace_id: "workspace-a".to_owned(),
9935 instance_id,
9936 generation,
9937 }
9938 }
9939
9940 fn sample_agent_stream_chunk(source_sequence: u64) -> HarnessAgentStreamChunkV1 {
9941 HarnessAgentStreamChunkV1 {
9942 source_sequence,
9943 kind: HarnessAgentStreamChunkKindV1::Text {
9944 text: format!("chunk-{source_sequence}"),
9945 is_delta: true,
9946 },
9947 }
9948 }
9949
9950 fn sample_terminal_frame(sequence: u64) -> HarnessRuntimeTerminalFrameV1 {
9951 HarnessRuntimeTerminalFrameV1 {
9952 sequence,
9953 size: HarnessRuntimeTerminalSizeV1 { rows: 24, columns: 80 },
9954 cursor_row: 0,
9955 cursor_column: 0,
9956 formatted: format!("frame-{sequence}").into_bytes(),
9957 scrollback_formatted: Vec::new(),
9958 alternate_screen: false,
9959 mouse_protocol_enabled: false,
9960 mouse_protocol_encoding: HarnessRuntimeMouseProtocolEncodingV1::Default,
9961 produced_at_unix_ms: 1_000,
9962 screen_state: None,
9963 bracketed_paste: None,
9964 }
9965 }
9966
9967 fn sample_runtime_session(instance_id: u64, generation: u64) -> HarnessRuntimeSessionV1 {
9968 HarnessRuntimeSessionV1 {
9969 instance_id,
9970 generation,
9971 provider: "codex".to_owned(),
9972 transport: HarnessRuntimeTransportV1::Pty,
9973 status: HarnessRuntimeSessionStatusV1::Running,
9974 process_id: Some(1234),
9975 terminal_size: Some(HarnessRuntimeTerminalSizeV1 { rows: 24, columns: 80 }),
9976 operation_pending: false,
9977 input_pending: false,
9978 screen_state: None,
9979 }
9980 }
9981
9982 fn sample_operator_gate_state() -> OperatorGateStateV1 {
9987 OperatorGateStateV1 {
9988 kind: OperatorGateKindV1::HookTrust,
9989 subject: OperatorGateSubjectV1::Hooks { count: Some(6) },
9990 input: OperatorGateInputV1::NumberedList,
9991 options: vec![
9992 OperatorGateOptionV1 {
9993 text: "Trust all and continue".to_owned(),
9994 semantics: OperatorGateOptionSemanticsV1::Accept,
9995 selected: false,
9996 },
9997 OperatorGateOptionV1 {
9998 text: "Continue without trusting".to_owned(),
9999 semantics: OperatorGateOptionSemanticsV1::Decline,
10000 selected: true,
10001 },
10002 ],
10003 }
10004 }
10005
10006 fn every_screen_state_variant() -> Vec<PtyScreenStateV1> {
10011 vec![
10012 PtyScreenStateV1::Unknown,
10013 PtyScreenStateV1::NotAgent { observed_process: "installer.exe".to_owned() },
10014 PtyScreenStateV1::OperatorGate { gate: sample_operator_gate_state() },
10015 PtyScreenStateV1::Failing { reason: "crash-loop".to_owned() },
10016 PtyScreenStateV1::Ready,
10017 ]
10018 }
10019
10020 #[test]
10025 fn a_terminal_frame_with_no_screen_state_serializes_without_the_key_at_all() {
10026 let frame = sample_terminal_frame(1);
10027 assert!(frame.screen_state.is_none());
10028 let value = serde_json::to_value(&frame).unwrap();
10029 assert!(
10030 value.as_object().unwrap().get("screen_state").is_none(),
10031 "expected no screen_state key in {value}",
10032 );
10033 }
10034
10035 #[test]
10039 fn a_runtime_session_with_no_screen_state_serializes_without_the_key_at_all() {
10040 let session = sample_runtime_session(1, 1);
10041 assert!(session.screen_state.is_none());
10042 let value = serde_json::to_value(&session).unwrap();
10043 assert!(
10044 value.as_object().unwrap().get("screen_state").is_none(),
10045 "expected no screen_state key in {value}",
10046 );
10047 }
10048
10049 #[test]
10052 fn some_screen_state_round_trips_exactly_for_every_variant_on_both_carriers() {
10053 for variant in every_screen_state_variant() {
10054 let mut frame = sample_terminal_frame(1);
10055 frame.screen_state = Some(variant.clone());
10056 let decoded_frame: HarnessRuntimeTerminalFrameV1 = serde_json::from_slice(
10057 &serde_json::to_vec(&frame).unwrap(),
10058 ).unwrap();
10059 assert_eq!(decoded_frame, frame);
10060
10061 let mut session = sample_runtime_session(1, 1);
10062 session.screen_state = Some(variant.clone());
10063 let decoded_session: HarnessRuntimeSessionV1 = serde_json::from_slice(
10064 &serde_json::to_vec(&session).unwrap(),
10065 ).unwrap();
10066 assert_eq!(decoded_session, session);
10067 }
10068 }
10069
10070 #[test]
10074 fn validate_rejects_an_oversized_string_inside_a_present_screen_state() {
10075 let oversized_process = "x".repeat(HARNESS_SCREEN_STATE_PROCESS_MAX_BYTES + 1);
10076 let oversized_gate = "x".repeat(HARNESS_SCREEN_STATE_GATE_MAX_BYTES + 1);
10077 let cases = [
10078 PtyScreenStateV1::NotAgent { observed_process: oversized_process },
10079 PtyScreenStateV1::Failing { reason: oversized_gate },
10080 ];
10081 for case in cases {
10082 let mut frame = sample_terminal_frame(1);
10083 frame.screen_state = Some(case.clone());
10084 assert!(
10085 matches!(frame.validate(), Err(HarnessOperatorApiError::InvalidRuntimeInventory)),
10086 "expected frame validate() to reject {case:?}",
10087 );
10088
10089 let mut session = sample_runtime_session(1, 1);
10090 session.screen_state = Some(case.clone());
10091 assert!(
10092 matches!(session.validate(), Err(HarnessOperatorApiError::InvalidRuntimeInventory)),
10093 "expected session validate() to reject {case:?}",
10094 );
10095 }
10096 }
10097
10098 #[test]
10103 fn validate_rejects_an_oversized_field_nested_inside_an_operator_gate() {
10104 let oversized_path = "x".repeat(HARNESS_GATE_PATH_MAX_BYTES + 1);
10105 let oversized_option_text = "x".repeat(HARNESS_GATE_OPTION_TEXT_MAX_BYTES + 1);
10106 let cases = [
10107 PtyScreenStateV1::OperatorGate {
10108 gate: OperatorGateStateV1 {
10109 subject: OperatorGateSubjectV1::Directory { path: Some(oversized_path) },
10110 ..sample_operator_gate_state()
10111 },
10112 },
10113 PtyScreenStateV1::OperatorGate {
10114 gate: OperatorGateStateV1 {
10115 options: vec![OperatorGateOptionV1 {
10116 text: oversized_option_text,
10117 semantics: OperatorGateOptionSemanticsV1::Accept,
10118 selected: false,
10119 }],
10120 ..sample_operator_gate_state()
10121 },
10122 },
10123 ];
10124 for case in cases {
10125 let mut frame = sample_terminal_frame(1);
10126 frame.screen_state = Some(case.clone());
10127 assert!(
10128 matches!(frame.validate(), Err(HarnessOperatorApiError::InvalidRuntimeInventory)),
10129 "expected frame validate() to reject {case:?}",
10130 );
10131
10132 let mut session = sample_runtime_session(1, 1);
10133 session.screen_state = Some(case.clone());
10134 assert!(
10135 matches!(session.validate(), Err(HarnessOperatorApiError::InvalidRuntimeInventory)),
10136 "expected session validate() to reject {case:?}",
10137 );
10138 }
10139 }
10140
10141 #[test]
10142 fn operator_session_verbs_are_exact_round_trips() {
10143 let session = session_address(41, 3);
10144 let requests = vec![
10145 HarnessOperatorRequestV1::SpawnSession {
10146 node_id: "node-a".to_owned(),
10147 workspace_id: "workspace-a".to_owned(),
10148 provider: "claude".to_owned(),
10149 provider_profile: "claude-default".to_owned(),
10150 mode: HarnessExecutionModeV1::Pty,
10151 terminal_size: HarnessRuntimeTerminalSizeV1 { rows: 40, columns: 120 },
10152 approval_level: None,
10153 },
10154 HarnessOperatorRequestV1::WriteSessionInput {
10155 session: session.clone(),
10156 text: "echo hi\n".to_owned(),
10157 },
10158 HarnessOperatorRequestV1::ResizeSession {
10159 session: session.clone(),
10160 terminal_size: HarnessRuntimeTerminalSizeV1 { rows: 50, columns: 160 },
10161 },
10162 HarnessOperatorRequestV1::StopSession {
10163 session: session.clone(),
10164 force: true,
10165 },
10166 ];
10167 let credential = HarnessOperatorCredential::parse(format!(
10168 "g4aho_{}",
10169 "a".repeat(64),
10170 )).unwrap();
10171 for request in requests {
10172 request.validate().expect("valid session verb request");
10173 let encoded = serde_json::to_string(&request).unwrap();
10174 let decoded: HarnessOperatorRequestV1 = serde_json::from_str(&encoded).unwrap();
10175 assert_eq!(decoded, request);
10176 HarnessOperatorEnvelopeV1 {
10177 build_stamp: BUILD_STAMP.to_string(),
10178 credential: credential.clone(),
10179 request,
10180 }.validate().unwrap();
10181 }
10182
10183 let responses = vec![
10184 HarnessOperatorResponseV1::SessionSpawned(session.clone()),
10185 HarnessOperatorResponseV1::SessionInputWritten,
10186 HarnessOperatorResponseV1::SessionResized,
10187 HarnessOperatorResponseV1::SessionStopped,
10188 ];
10189 for response in responses {
10190 response.validate().expect("valid V10 session verb response");
10191 let encoded = serde_json::to_string(&response).unwrap();
10192 let decoded: HarnessOperatorResponseV1 = serde_json::from_str(&encoded).unwrap();
10193 assert_eq!(decoded, response);
10194 }
10195 }
10196
10197 #[test]
10202 fn operator_prompt_session_is_exact_round_trip() {
10203 let session = session_address(41, 3);
10204 let request = HarnessOperatorRequestV1::PromptSession {
10205 session: session.clone(),
10206 text: "please continue".to_owned(),
10207 };
10208 request.validate().expect("valid prompt-session request");
10209 let encoded = serde_json::to_string(&request).unwrap();
10210 let decoded: HarnessOperatorRequestV1 = serde_json::from_str(&encoded).unwrap();
10211 assert_eq!(decoded, request);
10212 let credential = HarnessOperatorCredential::parse(format!(
10213 "g4aho_{}",
10214 "a".repeat(64),
10215 )).unwrap();
10216 HarnessOperatorEnvelopeV1 {
10217 build_stamp: BUILD_STAMP.to_string(),
10218 credential,
10219 request,
10220 }.validate().unwrap();
10221
10222 let response = HarnessOperatorResponseV1::SessionPrompted;
10223 response.validate().expect("valid prompt-session response");
10224 let encoded = serde_json::to_string(&response).unwrap();
10225 let decoded: HarnessOperatorResponseV1 = serde_json::from_str(&encoded).unwrap();
10226 assert_eq!(decoded, response);
10227 }
10228
10229 #[test]
10230 fn session_spawn_and_control_requests_reject_malformed_fields() {
10231 let session = session_address(41, 3);
10232 let valid_spawn = HarnessOperatorRequestV1::SpawnSession {
10233 node_id: "node-a".to_owned(),
10234 workspace_id: "workspace-a".to_owned(),
10235 provider: "claude".to_owned(),
10236 provider_profile: "claude-default".to_owned(),
10237 mode: HarnessExecutionModeV1::Pty,
10238 terminal_size: HarnessRuntimeTerminalSizeV1 { rows: 40, columns: 120 },
10239 approval_level: None,
10240 };
10241 valid_spawn.validate().unwrap();
10242
10243 let mut empty_node = valid_spawn.clone();
10244 if let HarnessOperatorRequestV1::SpawnSession { node_id, .. } = &mut empty_node {
10245 *node_id = String::new();
10246 }
10247 assert!(matches!(
10248 empty_node.validate(),
10249 Err(HarnessOperatorApiError::InvalidWorkspaceOrigin),
10250 ));
10251
10252 let mut bad_provider = valid_spawn.clone();
10253 if let HarnessOperatorRequestV1::SpawnSession { provider, .. } = &mut bad_provider {
10254 *provider = "not a provider id!".to_owned();
10255 }
10256 assert!(matches!(
10257 bad_provider.validate(),
10258 Err(HarnessOperatorApiError::InvalidSessionSpawn),
10259 ));
10260
10261 let mut zero_rows = valid_spawn.clone();
10262 if let HarnessOperatorRequestV1::SpawnSession { terminal_size, .. } = &mut zero_rows {
10263 terminal_size.rows = 0;
10264 }
10265 assert!(matches!(
10266 zero_rows.validate(),
10267 Err(HarnessOperatorApiError::InvalidSessionSpawn),
10268 ));
10269
10270 let oversized_input = HarnessOperatorRequestV1::WriteSessionInput {
10271 session: session.clone(),
10272 text: "x".repeat(HARNESS_SESSION_INPUT_MAX_BYTES + 1),
10273 };
10274 assert!(matches!(
10275 oversized_input.validate(),
10276 Err(HarnessOperatorApiError::InvalidSessionControl),
10277 ));
10278 let bounded_input = HarnessOperatorRequestV1::WriteSessionInput {
10279 session: session.clone(),
10280 text: "x".repeat(HARNESS_SESSION_INPUT_MAX_BYTES),
10281 };
10282 bounded_input.validate().unwrap();
10283
10284 let oversized_prompt = HarnessOperatorRequestV1::PromptSession {
10285 session: session.clone(),
10286 text: "x".repeat(HARNESS_SESSION_INPUT_MAX_BYTES + 1),
10287 };
10288 assert!(matches!(
10289 oversized_prompt.validate(),
10290 Err(HarnessOperatorApiError::InvalidSessionControl),
10291 ));
10292 let bounded_prompt = HarnessOperatorRequestV1::PromptSession {
10293 session: session.clone(),
10294 text: "x".repeat(HARNESS_SESSION_INPUT_MAX_BYTES),
10295 };
10296 bounded_prompt.validate().unwrap();
10297
10298 let zero_resize = HarnessOperatorRequestV1::ResizeSession {
10299 session: session.clone(),
10300 terminal_size: HarnessRuntimeTerminalSizeV1 { rows: 24, columns: 0 },
10301 };
10302 assert!(matches!(
10303 zero_resize.validate(),
10304 Err(HarnessOperatorApiError::InvalidSessionControl),
10305 ));
10306
10307 let mut malformed_session = session.clone();
10308 malformed_session.instance_id = 0;
10309 let bad_stop = HarnessOperatorRequestV1::StopSession {
10310 session: malformed_session,
10311 force: false,
10312 };
10313 assert!(bad_stop.validate().is_err());
10314 }
10315
10316 #[test]
10317 fn operator_session_control_verbs_are_exact_round_trips() {
10318 let session = session_address(41, 3);
10319 let requests = vec![
10320 HarnessOperatorRequestV1::ControlSession {
10321 session: session.clone(),
10322 control: HarnessTerminalControlV1::Enter,
10323 },
10324 HarnessOperatorRequestV1::WriteSessionBytes {
10325 session: session.clone(),
10326 bytes: vec![0x1b, b'[', b'A'],
10327 },
10328 HarnessOperatorRequestV1::PasteSession {
10329 session: session.clone(),
10330 text: "pasted text\n".to_owned(),
10331 },
10332 HarnessOperatorRequestV1::RemoveSession {
10333 session: session.clone(),
10334 },
10335 HarnessOperatorRequestV1::ResumeSession {
10336 session: session.clone(),
10337 terminal_size: HarnessRuntimeTerminalSizeV1 { rows: 24, columns: 80 },
10338 },
10339 ];
10340 let credential = HarnessOperatorCredential::parse(format!(
10341 "g4aho_{}",
10342 "a".repeat(64),
10343 )).unwrap();
10344 for request in requests {
10345 request.validate().expect("valid session control verb request");
10346 let encoded = serde_json::to_string(&request).unwrap();
10347 let decoded: HarnessOperatorRequestV1 = serde_json::from_str(&encoded).unwrap();
10348 assert_eq!(decoded, request);
10349 HarnessOperatorEnvelopeV1 {
10350 build_stamp: BUILD_STAMP.to_string(),
10351 credential: credential.clone(),
10352 request,
10353 }.validate().unwrap();
10354 }
10355
10356 let responses = vec![
10357 HarnessOperatorResponseV1::SessionControlled,
10358 HarnessOperatorResponseV1::SessionBytesWritten,
10359 HarnessOperatorResponseV1::SessionPasted,
10360 HarnessOperatorResponseV1::SessionRemoved,
10361 HarnessOperatorResponseV1::SessionResumed,
10362 ];
10363 for response in responses {
10364 response.validate().expect("valid V11 session control verb response");
10365 let encoded = serde_json::to_string(&response).unwrap();
10366 let decoded: HarnessOperatorResponseV1 = serde_json::from_str(&encoded).unwrap();
10367 assert_eq!(decoded, response);
10368 }
10369 }
10370
10371 #[test]
10372 fn session_control_v11_requests_reject_malformed_fields() {
10373 let session = session_address(41, 3);
10374
10375 let oversized_bytes = HarnessOperatorRequestV1::WriteSessionBytes {
10376 session: session.clone(),
10377 bytes: vec![0u8; HARNESS_SESSION_BYTES_MAX_BYTES + 1],
10378 };
10379 assert!(matches!(
10380 oversized_bytes.validate(),
10381 Err(HarnessOperatorApiError::InvalidSessionControl),
10382 ));
10383 let empty_bytes = HarnessOperatorRequestV1::WriteSessionBytes {
10384 session: session.clone(),
10385 bytes: Vec::new(),
10386 };
10387 assert!(matches!(
10388 empty_bytes.validate(),
10389 Err(HarnessOperatorApiError::InvalidSessionControl),
10390 ));
10391 let bounded_bytes = HarnessOperatorRequestV1::WriteSessionBytes {
10392 session: session.clone(),
10393 bytes: vec![0u8; HARNESS_SESSION_BYTES_MAX_BYTES],
10394 };
10395 bounded_bytes.validate().unwrap();
10396
10397 let oversized_paste = HarnessOperatorRequestV1::PasteSession {
10398 session: session.clone(),
10399 text: "x".repeat(HARNESS_SESSION_INPUT_MAX_BYTES + 1),
10400 };
10401 assert!(matches!(
10402 oversized_paste.validate(),
10403 Err(HarnessOperatorApiError::InvalidSessionControl),
10404 ));
10405 let bounded_paste = HarnessOperatorRequestV1::PasteSession {
10406 session: session.clone(),
10407 text: "x".repeat(HARNESS_SESSION_INPUT_MAX_BYTES),
10408 };
10409 bounded_paste.validate().unwrap();
10410
10411 let zero_resume_size = HarnessOperatorRequestV1::ResumeSession {
10412 session: session.clone(),
10413 terminal_size: HarnessRuntimeTerminalSizeV1 { rows: 0, columns: 80 },
10414 };
10415 assert!(matches!(
10416 zero_resume_size.validate(),
10417 Err(HarnessOperatorApiError::InvalidSessionControl),
10418 ));
10419
10420 let mut malformed_session = session.clone();
10421 malformed_session.instance_id = 0;
10422 let bad_remove = HarnessOperatorRequestV1::RemoveSession {
10423 session: malformed_session.clone(),
10424 };
10425 assert!(bad_remove.validate().is_err());
10426 let bad_control = HarnessOperatorRequestV1::ControlSession {
10427 session: malformed_session,
10428 control: HarnessTerminalControlV1::Interrupt,
10429 };
10430 assert!(bad_control.validate().is_err());
10431 }
10432
10433 fn sample_native_session_selection() -> HarnessNativeSessionSelectionV1 {
10434 HarnessNativeSessionSelectionV1 {
10435 route: HarnessNativeSessionRouteV1 {
10436 node_id: "node-a".to_owned(),
10437 incarnation_id: "1".repeat(32),
10438 scope: HarnessNativeSessionCatalogScopeV1::Workspace,
10439 workspace_id: Some("workspace-a".to_owned()),
10440 provider: "codex".to_owned(),
10441 },
10442 catalog_revision: 7,
10443 recent_cutoff_unix_ms: 9,
10444 selection_id: "selection-a".to_owned(),
10445 }
10446 }
10447
10448 fn sample_managed_session(record_id: &str) -> HarnessRuntimeManagedSessionV1 {
10449 HarnessRuntimeManagedSessionV1 {
10450 record_id: record_id.to_owned(),
10451 display_name: "Session display name".to_owned(),
10452 display_name_truncated: false,
10453 provider: "codex".to_owned(),
10454 mode: HarnessRuntimeManagedModeV1::Pty,
10455 state: HarnessRuntimeManagedStateV1::Dormant,
10456 workspace_id: "workspace-a".to_owned(),
10457 active_binding: None,
10458 provider_identity_present: true,
10459 updated_at_unix_ms: 10,
10460 blocked_count: 0,
10461 last_blocked_at_ms: None,
10462 }
10463 }
10464
10465 #[test]
10466 fn operator_session_record_family_requests_are_exact_round_trips() {
10467 let requests = vec![
10468 HarnessOperatorRequestV1::PreviewSessionRecord {
10469 node_id: "node-a".to_owned(),
10470 record_id: "record-a".to_owned(),
10471 message_limit: HARNESS_NATIVE_SESSION_PREVIEW_MESSAGE_LIMIT_MAX,
10472 },
10473 HarnessOperatorRequestV1::ResumeSessionRecord {
10474 node_id: "node-a".to_owned(),
10475 record_id: "record-a".to_owned(),
10476 terminal_size: HarnessRuntimeTerminalSizeV1 { rows: 24, columns: 80 },
10477 initial_prompt: Some("continue".to_owned()),
10478 },
10479 HarnessOperatorRequestV1::RenameSessionRecord {
10480 node_id: "node-a".to_owned(),
10481 record_id: "record-a".to_owned(),
10482 display_name: "Renamed session".to_owned(),
10483 },
10484 HarnessOperatorRequestV1::SetSessionTask {
10485 node_id: "node-a".to_owned(),
10486 record_id: "record-a".to_owned(),
10487 expected_revision: 3,
10488 target: HarnessSessionTaskTargetV1::Existing {
10489 task_id: format!("task-{}", "a".repeat(24)),
10490 },
10491 },
10492 HarnessOperatorRequestV1::ForgetSessionRecord {
10493 node_id: "node-a".to_owned(),
10494 record_id: "record-a".to_owned(),
10495 },
10496 HarnessOperatorRequestV1::IndexProviderSession {
10497 node_id: "node-a".to_owned(),
10498 workspace_id: "workspace-a".to_owned(),
10499 provider: "codex".to_owned(),
10500 identity: HarnessProviderSessionIdentityV1 {
10501 key: HarnessProviderSessionKeyV1::SessionId,
10502 id: "session-123".to_owned(),
10503 transcript_path: Some("/tmp/transcript.json".to_owned()),
10504 },
10505 display_name: "Indexed session".to_owned(),
10506 },
10507 HarnessOperatorRequestV1::IndexNativeSession {
10508 selection: sample_native_session_selection(),
10509 display_name: "Native indexed".to_owned(),
10510 },
10511 ];
10512 let credential = HarnessOperatorCredential::parse(format!(
10513 "g4aho_{}",
10514 "a".repeat(64),
10515 )).unwrap();
10516 for request in requests {
10517 request.validate().expect("valid session-record family request");
10518 let encoded = serde_json::to_string(&request).unwrap();
10519 let decoded: HarnessOperatorRequestV1 = serde_json::from_str(&encoded).unwrap();
10520 assert_eq!(decoded, request);
10521 HarnessOperatorEnvelopeV1 {
10522 build_stamp: BUILD_STAMP.to_string(),
10523 credential: credential.clone(),
10524 request,
10525 }.validate().unwrap();
10526 }
10527 }
10528
10529 #[test]
10530 fn operator_session_record_family_responses_are_exact_round_trips() {
10531 let record = sample_managed_session("record-a");
10532 let responses = vec![
10533 HarnessOperatorResponseV1::SessionRecordPreviewed(HarnessSessionRecordPreviewedV1 {
10534 record_id: "record-a".to_owned(),
10535 preview: HarnessNativeSessionPreviewV1 {
10536 title: Some("Preview title".to_owned()),
10537 modified_at_unix_ms: Some(10),
10538 model: Some("gpt".to_owned()),
10539 message_count: 1,
10540 message_count_exact: true,
10541 completed_turn_count: Some(1),
10542 total_tokens: Some(100),
10543 truncated: false,
10544 messages: vec![HarnessNativeSessionPreviewMessageV1 {
10545 role: HarnessNativeSessionPreviewRoleV1::User,
10546 text: "hello".to_owned(),
10547 }],
10548 },
10549 }),
10550 HarnessOperatorResponseV1::SessionRecordResumed(HarnessSessionRecordResumedV1 {
10551 record: record.clone(),
10552 session: session_address(41, 3),
10553 }),
10554 HarnessOperatorResponseV1::SessionRecordUpdated(record.clone()),
10555 HarnessOperatorResponseV1::SessionRecordForgotten { record_id: "record-a".to_owned() },
10556 HarnessOperatorResponseV1::ProviderSessionIndexed(record.clone()),
10557 HarnessOperatorResponseV1::NativeSessionIndexed(HarnessNativeSessionIndexedV1 {
10558 selection: sample_native_session_selection(),
10559 record,
10560 }),
10561 ];
10562 for response in responses {
10563 response.validate().expect("valid session-record family response");
10564 let encoded = serde_json::to_string(&response).unwrap();
10565 let decoded: HarnessOperatorResponseV1 = serde_json::from_str(&encoded).unwrap();
10566 assert_eq!(decoded, response);
10567 }
10568 }
10569
10570 #[test]
10571 fn session_record_family_requests_reject_malformed_fields() {
10572 let bad_node_id = HarnessOperatorRequestV1::PreviewSessionRecord {
10573 node_id: String::new(),
10574 record_id: "record-a".to_owned(),
10575 message_limit: 1,
10576 };
10577 assert!(matches!(
10578 bad_node_id.validate(),
10579 Err(HarnessOperatorApiError::InvalidSessionRecordRequest),
10580 ));
10581
10582 let bad_limit = HarnessOperatorRequestV1::PreviewSessionRecord {
10583 node_id: "node-a".to_owned(),
10584 record_id: "record-a".to_owned(),
10585 message_limit: 0,
10586 };
10587 assert!(matches!(bad_limit.validate(), Err(HarnessOperatorApiError::InvalidLimit)));
10588
10589 let empty_display_name = HarnessOperatorRequestV1::RenameSessionRecord {
10590 node_id: "node-a".to_owned(),
10591 record_id: "record-a".to_owned(),
10592 display_name: String::new(),
10593 };
10594 assert!(matches!(
10595 empty_display_name.validate(),
10596 Err(HarnessOperatorApiError::InvalidSessionRecordRequest),
10597 ));
10598
10599 let oversized_display_name = HarnessOperatorRequestV1::RenameSessionRecord {
10600 node_id: "node-a".to_owned(),
10601 record_id: "record-a".to_owned(),
10602 display_name: "x".repeat(HARNESS_SESSION_RECORD_DISPLAY_NAME_MAX_BYTES + 1),
10603 };
10604 assert!(matches!(
10605 oversized_display_name.validate(),
10606 Err(HarnessOperatorApiError::InvalidSessionRecordRequest),
10607 ));
10608
10609 let bounded_display_name = HarnessOperatorRequestV1::RenameSessionRecord {
10610 node_id: "node-a".to_owned(),
10611 record_id: "record-a".to_owned(),
10612 display_name: "x".repeat(HARNESS_SESSION_RECORD_DISPLAY_NAME_MAX_BYTES),
10613 };
10614 bounded_display_name.validate().unwrap();
10615
10616 let zero_resume_size = HarnessOperatorRequestV1::ResumeSessionRecord {
10617 node_id: "node-a".to_owned(),
10618 record_id: "record-a".to_owned(),
10619 terminal_size: HarnessRuntimeTerminalSizeV1 { rows: 0, columns: 80 },
10620 initial_prompt: None,
10621 };
10622 assert!(matches!(
10623 zero_resume_size.validate(),
10624 Err(HarnessOperatorApiError::InvalidSessionRecordRequest),
10625 ));
10626
10627 let empty_initial_prompt = HarnessOperatorRequestV1::ResumeSessionRecord {
10628 node_id: "node-a".to_owned(),
10629 record_id: "record-a".to_owned(),
10630 terminal_size: HarnessRuntimeTerminalSizeV1 { rows: 24, columns: 80 },
10631 initial_prompt: Some(String::new()),
10632 };
10633 assert!(matches!(
10634 empty_initial_prompt.validate(),
10635 Err(HarnessOperatorApiError::InvalidSessionRecordRequest),
10636 ));
10637
10638 let bad_task_id = HarnessOperatorRequestV1::SetSessionTask {
10639 node_id: "node-a".to_owned(),
10640 record_id: "record-a".to_owned(),
10641 expected_revision: 1,
10642 target: HarnessSessionTaskTargetV1::Existing { task_id: String::new() },
10643 };
10644 assert!(matches!(
10645 bad_task_id.validate(),
10646 Err(HarnessOperatorApiError::InvalidSessionRecordRequest),
10647 ));
10648
10649 let valid_clear_task = HarnessOperatorRequestV1::SetSessionTask {
10650 node_id: "node-a".to_owned(),
10651 record_id: "record-a".to_owned(),
10652 expected_revision: 1,
10653 target: HarnessSessionTaskTargetV1::Clear,
10654 };
10655 valid_clear_task.validate().unwrap();
10656
10657 let bad_identity_id = HarnessOperatorRequestV1::IndexProviderSession {
10658 node_id: "node-a".to_owned(),
10659 workspace_id: "workspace-a".to_owned(),
10660 provider: "codex".to_owned(),
10661 identity: HarnessProviderSessionIdentityV1 {
10662 key: HarnessProviderSessionKeyV1::SessionId,
10663 id: String::new(),
10664 transcript_path: None,
10665 },
10666 display_name: "Indexed session".to_owned(),
10667 };
10668 assert!(matches!(
10669 bad_identity_id.validate(),
10670 Err(HarnessOperatorApiError::InvalidSessionRecordRequest),
10671 ));
10672
10673 let mut malformed_selection = sample_native_session_selection();
10674 malformed_selection.catalog_revision = 0;
10675 let bad_index_native = HarnessOperatorRequestV1::IndexNativeSession {
10676 selection: malformed_selection.clone(),
10677 display_name: "Native indexed".to_owned(),
10678 };
10679 assert!(bad_index_native.validate().is_err());
10680 malformed_selection.catalog_revision = 7;
10681 HarnessOperatorRequestV1::IndexNativeSession {
10682 selection: malformed_selection,
10683 display_name: "Native indexed".to_owned(),
10684 }.validate().unwrap();
10685 }
10686
10687 #[test]
10688 fn session_record_family_responses_reject_malformed_fields() {
10689 let mut malformed_record = sample_managed_session("record-a");
10690 malformed_record.record_id = String::new();
10691 assert!(
10692 HarnessOperatorResponseV1::SessionRecordUpdated(malformed_record.clone())
10693 .validate()
10694 .is_err()
10695 );
10696 assert!(
10697 HarnessOperatorResponseV1::ProviderSessionIndexed(malformed_record.clone())
10698 .validate()
10699 .is_err()
10700 );
10701 assert!(
10702 HarnessOperatorResponseV1::SessionRecordResumed(HarnessSessionRecordResumedV1 {
10703 record: malformed_record,
10704 session: session_address(41, 3),
10705 }).validate().is_err()
10706 );
10707
10708 assert!(
10709 HarnessOperatorResponseV1::SessionRecordForgotten { record_id: String::new() }
10710 .validate()
10711 .is_err()
10712 );
10713
10714 let mut malformed_session = session_address(41, 3);
10715 malformed_session.instance_id = 0;
10716 assert!(
10717 HarnessOperatorResponseV1::SessionRecordResumed(HarnessSessionRecordResumedV1 {
10718 record: sample_managed_session("record-a"),
10719 session: malformed_session,
10720 }).validate().is_err()
10721 );
10722
10723 let mut malformed_selection = sample_native_session_selection();
10724 malformed_selection.selection_id = String::new();
10725 assert!(
10726 HarnessOperatorResponseV1::NativeSessionIndexed(HarnessNativeSessionIndexedV1 {
10727 selection: malformed_selection,
10728 record: sample_managed_session("record-a"),
10729 }).validate().is_err()
10730 );
10731 }
10732
10733 fn host_path(value: &str) -> HarnessHostPathV1 {
10734 HarnessHostPathV1::new(value).unwrap()
10735 }
10736
10737 fn sample_workspace_snapshot(workspace_id: &str) -> HarnessWorkspaceSnapshotV1 {
10738 HarnessWorkspaceSnapshotV1 {
10739 workspace_id: workspace_id.to_owned(),
10740 canonical_root: host_path(r"C:\fixtures\workspace"),
10741 worktree_service_mode: Some(HarnessWorktreeServiceModeV1::Manual),
10742 }
10743 }
10744
10745 fn sample_git_worktree_snapshot() -> HarnessGitWorktreeSnapshotV1 {
10746 HarnessGitWorktreeSnapshotV1 {
10747 path: host_path(r"C:\fixtures\worktree"),
10748 head: "a".repeat(40),
10749 branch: Some("feature/resource-mutation".to_owned()),
10750 is_bare: false,
10751 is_main: false,
10752 locked: false,
10753 prunable: false,
10754 workspace_id: Some("worktree-workspace".to_owned()),
10755 }
10756 }
10757
10758 #[test]
10759 fn operator_resource_mutation_family_requests_are_exact_round_trips() {
10760 let requests = vec![
10761 HarnessOperatorRequestV1::BrowseHostDirectories {
10762 node_id: "node-a".to_owned(),
10763 directory: Some(host_path(r"C:\fixtures")),
10764 after: Some(host_path(r"C:\fixtures\a")),
10765 },
10766 HarnessOperatorRequestV1::RegisterWorkspace {
10767 node_id: "node-a".to_owned(),
10768 workspace_id: "workspace-b".to_owned(),
10769 root: host_path(r"C:\fixtures\workspace-b"),
10770 },
10771 HarnessOperatorRequestV1::UnregisterWorkspace {
10772 node_id: "node-a".to_owned(),
10773 workspace_id: "workspace-b".to_owned(),
10774 },
10775 HarnessOperatorRequestV1::CreateStandaloneWorkspace {
10776 node_id: "node-a".to_owned(),
10777 workspace_id: "workspace-c".to_owned(),
10778 root: host_path(r"C:\fixtures\workspace-c"),
10779 initial_branch: Some("main".to_owned()),
10780 },
10781 HarnessOperatorRequestV1::CreateWorktree {
10782 node_id: "node-a".to_owned(),
10783 source_workspace_id: "workspace-a".to_owned(),
10784 workspace_id: "worktree-workspace".to_owned(),
10785 target_root: host_path(r"C:\fixtures\worktree"),
10786 branch: "feature/resource-mutation".to_owned(),
10787 base: Some("main".to_owned()),
10788 },
10789 HarnessOperatorRequestV1::RemoveWorktree {
10790 node_id: "node-a".to_owned(),
10791 source_workspace_id: "workspace-a".to_owned(),
10792 target_root: host_path(r"C:\fixtures\worktree"),
10793 },
10794 HarnessOperatorRequestV1::ExportContextPack { session: session_address(41, 3) },
10795 HarnessOperatorRequestV1::ForgetContextPack {
10796 node_id: "node-a".to_owned(),
10797 context_id: HarnessSelectorV1::new("context-a").unwrap(),
10798 },
10799 ];
10800 let credential = HarnessOperatorCredential::parse(format!(
10801 "g4aho_{}",
10802 "b".repeat(64),
10803 )).unwrap();
10804 for request in requests {
10805 request.validate().expect("valid resource-mutation family request");
10806 let encoded = serde_json::to_string(&request).unwrap();
10807 let decoded: HarnessOperatorRequestV1 = serde_json::from_str(&encoded).unwrap();
10808 assert_eq!(decoded, request);
10809 HarnessOperatorEnvelopeV1 {
10810 build_stamp: BUILD_STAMP.to_string(),
10811 credential: credential.clone(),
10812 request,
10813 }.validate().unwrap();
10814 }
10815 }
10816
10817 #[test]
10818 fn operator_resource_mutation_family_responses_are_exact_round_trips() {
10819 let responses = vec![
10820 HarnessOperatorResponseV1::HostDirectoriesBrowsed(HarnessHostDirectoryListingV1 {
10821 directory: Some(host_path(r"C:\fixtures")),
10822 parent: Some(host_path(r"C:\")),
10823 entries: vec![HarnessHostDirectoryEntryV1 {
10824 path: host_path(r"C:\fixtures\workspace"),
10825 display_name: "workspace".to_owned(),
10826 is_link: false,
10827 }],
10828 next_after: Some(host_path(r"C:\fixtures\workspace")),
10829 incomplete: true,
10830 }),
10831 HarnessOperatorResponseV1::WorkspaceRegistered(sample_workspace_snapshot("workspace-b")),
10832 HarnessOperatorResponseV1::StandaloneWorkspaceCreated(
10833 sample_workspace_snapshot("workspace-c"),
10834 ),
10835 HarnessOperatorResponseV1::WorkspaceUnregistered {
10836 workspace_id: "workspace-b".to_owned(),
10837 },
10838 HarnessOperatorResponseV1::WorktreeCreated {
10839 worktree: sample_git_worktree_snapshot(),
10840 workspace: sample_workspace_snapshot("worktree-workspace"),
10841 },
10842 HarnessOperatorResponseV1::WorktreeRemoved {
10843 target_root: host_path(r"C:\fixtures\worktree"),
10844 workspace_id: Some("worktree-workspace".to_owned()),
10845 },
10846 HarnessOperatorResponseV1::ContextPackExported(HarnessResolvedContextPackReceiptV1 {
10847 id: HarnessSelectorV1::new("context-a").unwrap(),
10848 digest: "sha256:".to_owned() + &"a".repeat(64),
10849 lineage: HarnessContextPackLineageV1 {
10850 source_node_id: HarnessSelectorV1::new("node-a").unwrap(),
10851 source_workspace_id: HarnessSelectorV1::new("workspace-a").unwrap(),
10852 source_instance_id: 41,
10853 source_generation: 3,
10854 source_provider: HarnessSelectorV1::new("codex").unwrap(),
10855 },
10856 source_message_count: 4,
10857 retained_message_count: 4,
10858 byte_len: 128,
10859 truncated: false,
10860 }),
10861 HarnessOperatorResponseV1::ContextPackForgotten { context_id: "context-a".to_owned() },
10862 ];
10863 for response in responses {
10864 response.validate().expect("valid resource-mutation family response");
10865 let encoded = serde_json::to_string(&response).unwrap();
10866 let decoded: HarnessOperatorResponseV1 = serde_json::from_str(&encoded).unwrap();
10867 assert_eq!(decoded, response);
10868 }
10869 }
10870
10871 #[test]
10872 fn resource_mutation_family_requests_reject_malformed_fields() {
10873 let bad_node_id = HarnessOperatorRequestV1::BrowseHostDirectories {
10874 node_id: String::new(),
10875 directory: None,
10876 after: None,
10877 };
10878 assert!(matches!(
10879 bad_node_id.validate(),
10880 Err(HarnessOperatorApiError::InvalidHostDirectoryBrowseRequest),
10881 ));
10882
10883 assert!(matches!(
10884 HarnessHostPathV1::new(""),
10885 Err(HarnessOperatorApiError::InvalidHostPath),
10886 ));
10887 assert!(matches!(
10888 HarnessHostPathV1::new("x".repeat(HARNESS_HOST_PATH_MAX_BYTES + 1)),
10889 Err(HarnessOperatorApiError::InvalidHostPath),
10890 ));
10891 HarnessHostPathV1::new("x".repeat(HARNESS_HOST_PATH_MAX_BYTES)).unwrap();
10892
10893 let bad_workspace_id = HarnessOperatorRequestV1::RegisterWorkspace {
10894 node_id: "node-a".to_owned(),
10895 workspace_id: String::new(),
10896 root: host_path(r"C:\fixtures\workspace-b"),
10897 };
10898 assert!(matches!(
10899 bad_workspace_id.validate(),
10900 Err(HarnessOperatorApiError::InvalidResourceMutationRequest),
10901 ));
10902
10903 let empty_branch = HarnessOperatorRequestV1::CreateWorktree {
10904 node_id: "node-a".to_owned(),
10905 source_workspace_id: "workspace-a".to_owned(),
10906 workspace_id: "worktree-workspace".to_owned(),
10907 target_root: host_path(r"C:\fixtures\worktree"),
10908 branch: String::new(),
10909 base: None,
10910 };
10911 assert!(matches!(
10912 empty_branch.validate(),
10913 Err(HarnessOperatorApiError::InvalidResourceMutationRequest),
10914 ));
10915
10916 let control_char_base = HarnessOperatorRequestV1::CreateWorktree {
10917 node_id: "node-a".to_owned(),
10918 source_workspace_id: "workspace-a".to_owned(),
10919 workspace_id: "worktree-workspace".to_owned(),
10920 target_root: host_path(r"C:\fixtures\worktree"),
10921 branch: "feature/resource-mutation".to_owned(),
10922 base: Some("main\u{0007}".to_owned()),
10923 };
10924 assert!(matches!(
10925 control_char_base.validate(),
10926 Err(HarnessOperatorApiError::InvalidResourceMutationRequest),
10927 ));
10928
10929 let mut malformed_session = session_address(41, 3);
10930 malformed_session.instance_id = 0;
10931 assert!(
10932 HarnessOperatorRequestV1::ExportContextPack { session: malformed_session }
10933 .validate()
10934 .is_err()
10935 );
10936
10937 let bad_context_id = HarnessOperatorRequestV1::ForgetContextPack {
10938 node_id: "node-a".to_owned(),
10939 context_id: HarnessSelectorV1::new("context-a").unwrap(),
10940 };
10941 bad_context_id.validate().unwrap();
10942 let empty_forget_node_id = HarnessOperatorRequestV1::ForgetContextPack {
10943 node_id: String::new(),
10944 context_id: HarnessSelectorV1::new("context-a").unwrap(),
10945 };
10946 assert!(matches!(
10947 empty_forget_node_id.validate(),
10948 Err(HarnessOperatorApiError::InvalidResourceMutationRequest),
10949 ));
10950 }
10951
10952 #[test]
10953 fn resource_mutation_family_responses_reject_malformed_fields() {
10954 assert!(
10955 HarnessOperatorResponseV1::WorkspaceUnregistered { workspace_id: String::new() }
10956 .validate()
10957 .is_err()
10958 );
10959
10960 let mut malformed_workspace = sample_workspace_snapshot("workspace-b");
10961 malformed_workspace.workspace_id = String::new();
10962 assert!(
10963 HarnessOperatorResponseV1::WorkspaceRegistered(malformed_workspace)
10964 .validate()
10965 .is_err()
10966 );
10967
10968 let mut malformed_worktree = sample_git_worktree_snapshot();
10969 malformed_worktree.head = String::new();
10970 assert!(
10971 HarnessOperatorResponseV1::WorktreeCreated {
10972 worktree: malformed_worktree,
10973 workspace: sample_workspace_snapshot("worktree-workspace"),
10974 }.validate().is_err()
10975 );
10976
10977 assert!(
10978 HarnessOperatorResponseV1::WorktreeRemoved {
10979 target_root: host_path(r"C:\fixtures\worktree"),
10980 workspace_id: Some(String::new()),
10981 }.validate().is_err()
10982 );
10983
10984 assert!(
10985 HarnessOperatorResponseV1::ContextPackForgotten { context_id: String::new() }
10986 .validate()
10987 .is_err()
10988 );
10989 }
10990
10991 fn sample_redacted_task(id_byte: char) -> RedactedTaskV1 {
10992 RedactedTaskV1 {
10993 task_id: HarnessTaskId::new(format!("htask_{}", id_byte.to_string().repeat(24)))
10994 .unwrap(),
10995 revision: HarnessRevision::new(1).unwrap(),
10996 title: "Task title".to_owned(),
10997 body: String::new(),
10998 creator: TaskCreatorCategoryV1::User,
10999 parent_task_id: None,
11000 dependency_ids: Vec::new(),
11001 state: HarnessTaskStateV1::Backlog,
11002 run_ids: Vec::new(),
11003 references_redacted: false,
11004 result_refs: Vec::new(),
11005 artifact_refs: Vec::new(),
11006 created_at_unix_ms: 10,
11007 updated_at_unix_ms: 10,
11008 }
11009 }
11010
11011 fn sample_redacted_run(id_byte: char) -> RedactedRunV1 {
11012 RedactedRunV1 {
11013 run_id: HarnessRunId::new(format!("hrun_{}", id_byte.to_string().repeat(24)))
11014 .unwrap(),
11015 revision: HarnessRevision::new(1).unwrap(),
11016 parent_run_id: None,
11017 task_id: None,
11018 operation_id: None,
11019 intent: RedactedRunIntentV1 {
11020 mode: HarnessExecutionModeV1::Pty,
11021 worktree: RedactedWorktreeIntentV1::Existing,
11022 has_delivery_bundle: false,
11023 has_continuation: false,
11024 },
11025 lifecycle: HarnessRunLifecycleV1::Requested,
11026 binding: RedactedBindingStateV1::None,
11027 result_disposition: None,
11028 failure_category: None,
11029 context_pack: None,
11030 git_facts: None,
11031 delivery_receipt: None,
11032 references_redacted: false,
11033 created_at_unix_ms: 10,
11034 updated_at_unix_ms: 10,
11035 }
11036 }
11037
11038 fn simple_node_inventory(node_id: &str) -> HarnessRuntimeNodeInventoryV1 {
11039 HarnessRuntimeNodeInventoryV1 {
11040 node_id: node_id.to_owned(),
11041 incarnation_id: "07".repeat(16),
11042 observed_at_unix_ms: 1_000,
11043 event_sequence: 1,
11044 inventory: HarnessRuntimeInventoryV1 {
11045 enabled_providers: Vec::new(),
11046 workspaces: BTreeMap::new(),
11047 workspace_count: 0,
11048 workspaces_truncated: false,
11049 session_count: 0,
11050 sessions_truncated: false,
11051 managed_sessions: Vec::new(),
11052 managed_session_count: 0,
11053 managed_sessions_truncated: false,
11054 retired_count: 0,
11055 launch_inventory: None,
11056
11057 },
11058 }
11059 }
11060
11061 #[test]
11062 fn operator_subscribe_events_is_exact_round_trip() {
11063 let request = HarnessOperatorRequestV1::SubscribeEvents {};
11064 request.validate().expect("valid subscribe request");
11065 let encoded = serde_json::to_string(&request).unwrap();
11066 let decoded: HarnessOperatorRequestV1 = serde_json::from_str(&encoded).unwrap();
11067 assert_eq!(decoded, request);
11068
11069 let credential = HarnessOperatorCredential::parse(format!(
11070 "g4aho_{}",
11071 "a".repeat(64),
11072 )).unwrap();
11073 HarnessOperatorEnvelopeV1 {
11074 build_stamp: BUILD_STAMP.to_string(),
11075 credential,
11076 request,
11077 }.validate().unwrap();
11078 }
11079
11080 #[test]
11081 fn operator_subscribe_terminal_is_exact_round_trip() {
11082 let request = HarnessOperatorRequestV1::SubscribeTerminal {
11083 sessions: vec![session_address(1, 1), session_address(2, 1)],
11084 };
11085 request.validate().expect("valid subscribe-terminal request");
11086 let encoded = serde_json::to_string(&request).unwrap();
11087 let decoded: HarnessOperatorRequestV1 = serde_json::from_str(&encoded).unwrap();
11088 assert_eq!(decoded, request);
11089
11090 let credential = HarnessOperatorCredential::parse(format!(
11091 "g4aho_{}",
11092 "a".repeat(64),
11093 )).unwrap();
11094 HarnessOperatorEnvelopeV1 {
11095 build_stamp: BUILD_STAMP.to_string(),
11096 credential,
11097 request,
11098 }.validate().unwrap();
11099 }
11100
11101 #[test]
11105 fn envelope_with_a_foreign_build_stamp_is_rejected_naming_both_values() {
11106 let credential = HarnessOperatorCredential::parse(format!(
11107 "g4aho_{}",
11108 "a".repeat(64),
11109 )).unwrap();
11110 let foreign_stamp = "f".repeat(40);
11111 let envelope = HarnessOperatorEnvelopeV1 {
11112 build_stamp: foreign_stamp.clone(),
11113 credential,
11114 request: HarnessOperatorRequestV1::SubscribeEvents {},
11115 };
11116 let error = envelope
11117 .validate()
11118 .expect_err("a foreign build stamp must be rejected");
11119 assert!(matches!(
11120 &error,
11121 HarnessOperatorApiError::BuildStampMismatch { expected, received }
11122 if expected == BUILD_STAMP && received == &foreign_stamp,
11123 ));
11124 let message = error.to_string();
11125 assert!(message.contains(BUILD_STAMP));
11126 assert!(message.contains(&foreign_stamp));
11127 }
11128
11129 #[test]
11130 fn subscribe_terminal_rejects_empty_oversized_and_duplicate_session_lists() {
11131 assert!(matches!(
11132 HarnessOperatorRequestV1::SubscribeTerminal { sessions: Vec::new() }.validate(),
11133 Err(HarnessOperatorApiError::InvalidTerminalPage),
11134 ));
11135
11136 let oversized = (0..=HARNESS_TERMINAL_SUBSCRIPTION_SESSIONS_MAX as u64)
11137 .map(|instance_id| session_address(instance_id + 1, 1))
11138 .collect::<Vec<_>>();
11139 assert!(matches!(
11140 HarnessOperatorRequestV1::SubscribeTerminal { sessions: oversized }.validate(),
11141 Err(HarnessOperatorApiError::InvalidTerminalPage),
11142 ));
11143
11144 let duplicated = vec![session_address(1, 1), session_address(1, 1)];
11145 assert!(matches!(
11146 HarnessOperatorRequestV1::SubscribeTerminal { sessions: duplicated }.validate(),
11147 Err(HarnessOperatorApiError::InvalidTerminalPage),
11148 ));
11149
11150 let mut malformed = session_address(1, 1);
11151 malformed.node_id = String::new();
11152 assert!(HarnessOperatorRequestV1::SubscribeTerminal { sessions: vec![malformed] }
11153 .validate()
11154 .is_err());
11155 }
11156
11157 #[test]
11158 fn operator_event_variants_are_exact_round_trips() {
11159 let task = sample_redacted_task('a');
11160 let run = sample_redacted_run('b');
11161 let node = simple_node_inventory("node-a");
11162 let events = vec![
11163 HarnessOperatorEventV1::SnapshotBaseline {
11164 sequence: 0,
11165 tasks: vec![task.clone()],
11166 runs: vec![run.clone()],
11167 nodes: vec![node.clone()],
11168 },
11169 HarnessOperatorEventV1::TaskChanged { sequence: 1, task: task.clone() },
11170 HarnessOperatorEventV1::RunChanged { sequence: 2, run: run.clone() },
11171 HarnessOperatorEventV1::RuntimeInventoryChanged { sequence: 3, node: node.clone() },
11172 HarnessOperatorEventV1::RuntimeInventoryRemoved {
11173 sequence: 4,
11174 node_id: "node-a".to_owned(),
11175 },
11176 HarnessOperatorEventV1::Lagged { sequence: 5 },
11177 ];
11178 for event in events {
11179 event.validate().expect("valid operator event");
11180 let encoded = serde_json::to_string(&event).unwrap();
11181 let decoded: HarnessOperatorEventV1 = serde_json::from_str(&encoded).unwrap();
11182 assert_eq!(decoded, event);
11183 }
11184 }
11185
11186 #[test]
11187 fn operator_terminal_event_variants_are_exact_round_trips() {
11188 let events = vec![
11189 HarnessOperatorTerminalEventV1::TerminalFrame {
11190 sequence: 0,
11191 session: session_address(1, 1),
11192 frame: sample_terminal_frame(1),
11193 coalesced_since_last: 0,
11194 },
11195 HarnessOperatorTerminalEventV1::TerminalFrame {
11196 sequence: 1,
11197 session: session_address(1, 1),
11198 frame: sample_terminal_frame(2),
11199 coalesced_since_last: 3,
11200 },
11201 HarnessOperatorTerminalEventV1::Ping { sequence: 2 },
11202 ];
11203 for event in events {
11204 event.validate().expect("valid operator terminal event");
11205 let encoded = serde_json::to_string(&event).unwrap();
11206 let decoded: HarnessOperatorTerminalEventV1 = serde_json::from_str(&encoded).unwrap();
11207 assert_eq!(decoded, event);
11208 }
11209 }
11210
11211 #[test]
11212 fn operator_terminal_event_validate_rejects_a_malformed_frame_or_session() {
11213 let mut bad_session = session_address(1, 1);
11214 bad_session.node_id = String::new();
11215 let bad_session_event = HarnessOperatorTerminalEventV1::TerminalFrame {
11216 sequence: 0,
11217 session: bad_session,
11218 frame: sample_terminal_frame(1),
11219 coalesced_since_last: 0,
11220 };
11221 assert!(matches!(
11222 bad_session_event.validate(),
11223 Err(HarnessOperatorApiError::InvalidTerminalPage),
11224 ));
11225
11226 let mut zero_size_frame = sample_terminal_frame(1);
11227 zero_size_frame.size.rows = 0;
11228 let bad_frame_event = HarnessOperatorTerminalEventV1::TerminalFrame {
11229 sequence: 0,
11230 session: session_address(1, 1),
11231 frame: zero_size_frame,
11232 coalesced_since_last: 0,
11233 };
11234 assert!(matches!(
11235 bad_frame_event.validate(),
11236 Err(HarnessOperatorApiError::InvalidTerminalPage),
11237 ));
11238 }
11239
11240 #[test]
11241 fn operator_event_validate_rejects_malformed_frames() {
11242 let task_a = sample_redacted_task('a');
11243 let task_b = sample_redacted_task('b');
11244 let out_of_order = HarnessOperatorEventV1::SnapshotBaseline {
11245 sequence: 0,
11246 tasks: vec![task_b, task_a],
11247 runs: Vec::new(),
11248 nodes: Vec::new(),
11249 };
11250 assert!(out_of_order.validate().is_err());
11251
11252 let bad_node_id = HarnessOperatorEventV1::RuntimeInventoryRemoved {
11253 sequence: 0,
11254 node_id: String::new(),
11255 };
11256 assert!(matches!(
11257 bad_node_id.validate(),
11258 Err(HarnessOperatorApiError::InvalidRuntimeInventory),
11259 ));
11260
11261 let mut malformed_task = sample_redacted_task('c');
11262 malformed_task.title = "\0".to_owned();
11263 let bad_task = HarnessOperatorEventV1::TaskChanged { sequence: 0, task: malformed_task };
11264 assert!(bad_task.validate().is_err());
11265 }
11266
11267 #[test]
11273 fn operator_host_error_unsupported_round_trips() {
11274 let error = HarnessOperatorHostErrorV1::Unsupported;
11275 let encoded = serde_json::to_string(&error).unwrap();
11276 assert_eq!(encoded, "\"unsupported\"");
11277 assert_eq!(serde_json::from_str::<HarnessOperatorHostErrorV1>(&encoded).unwrap(), error);
11278
11279 let reply = HarnessOperatorReplyV1::Error { error };
11280 assert!(reply.validate().is_ok());
11281 let encoded_reply = serde_json::to_vec(&reply).unwrap();
11282 let decoded_reply: HarnessOperatorReplyV1 = serde_json::from_slice(&encoded_reply).unwrap();
11283 assert_eq!(decoded_reply, reply);
11284 }
11285
11286 #[test]
11287 fn operator_acp_control_verbs_are_exact_round_trips() {
11288 let session = session_address(7, 2);
11289 let requests = vec![
11290 HarnessOperatorRequestV1::ResolveInteraction {
11291 session: session.clone(),
11292 correlation_id: "a".repeat(HARNESS_OBSERVATION_LABEL_MAX_BYTES),
11293 response: HarnessProviderInteractionResponseV1::ApproveOnce,
11294 },
11295 HarnessOperatorRequestV1::ResolveInteraction {
11296 session: session.clone(),
11297 correlation_id: "correlation-1".to_owned(),
11298 response: HarnessProviderInteractionResponseV1::Deny,
11299 },
11300 HarnessOperatorRequestV1::ResolveInteraction {
11301 session: session.clone(),
11302 correlation_id: "correlation-2".to_owned(),
11303 response: HarnessProviderInteractionResponseV1::Answer {
11304 text: "yes, proceed".to_owned(),
11305 },
11306 },
11307 HarnessOperatorRequestV1::SetSessionMode {
11308 session: session.clone(),
11309 mode_id: "plan".to_owned(),
11310 },
11311 HarnessOperatorRequestV1::SetSessionConfigOption {
11312 session: session.clone(),
11313 option_id: "reasoning-effort".to_owned(),
11314 value_json: "\"high\"".to_owned(),
11315 },
11316 HarnessOperatorRequestV1::SetSessionModel {
11317 session: session.clone(),
11318 model_id: "grok-4".to_owned(),
11319 },
11320 ];
11321 let credential = HarnessOperatorCredential::parse(format!(
11322 "g4aho_{}",
11323 "a".repeat(64),
11324 )).unwrap();
11325 for request in requests {
11326 request.validate().expect("valid ACP control verb request");
11327 let encoded = serde_json::to_string(&request).unwrap();
11328 let decoded: HarnessOperatorRequestV1 = serde_json::from_str(&encoded).unwrap();
11329 assert_eq!(decoded, request);
11330 HarnessOperatorEnvelopeV1 {
11331 build_stamp: BUILD_STAMP.to_string(),
11332 credential: credential.clone(),
11333 request,
11334 }.validate().unwrap();
11335 }
11336
11337 let responses = vec![
11338 HarnessOperatorResponseV1::InteractionResolved,
11339 HarnessOperatorResponseV1::SessionModeSet,
11340 HarnessOperatorResponseV1::SessionConfigOptionSet,
11341 HarnessOperatorResponseV1::SessionModelSet,
11342 ];
11343 for response in responses {
11344 response.validate().expect("valid ACP control verb response");
11345 let encoded = serde_json::to_string(&response).unwrap();
11346 let decoded: HarnessOperatorResponseV1 = serde_json::from_str(&encoded).unwrap();
11347 assert_eq!(decoded, response);
11348 }
11349 }
11350
11351 #[test]
11352 fn acp_control_requests_reject_malformed_fields() {
11353 let session = session_address(7, 2);
11354
11355 let empty_correlation = HarnessOperatorRequestV1::ResolveInteraction {
11356 session: session.clone(),
11357 correlation_id: String::new(),
11358 response: HarnessProviderInteractionResponseV1::ApproveOnce,
11359 };
11360 assert!(matches!(
11361 empty_correlation.validate(),
11362 Err(HarnessOperatorApiError::InvalidSessionControl),
11363 ));
11364
11365 let oversized_correlation = HarnessOperatorRequestV1::ResolveInteraction {
11366 session: session.clone(),
11367 correlation_id: "a".repeat(HARNESS_OBSERVATION_LABEL_MAX_BYTES + 1),
11368 response: HarnessProviderInteractionResponseV1::Deny,
11369 };
11370 assert!(matches!(
11371 oversized_correlation.validate(),
11372 Err(HarnessOperatorApiError::InvalidSessionControl),
11373 ));
11374
11375 let oversized_answer = HarnessOperatorRequestV1::ResolveInteraction {
11376 session: session.clone(),
11377 correlation_id: "correlation-1".to_owned(),
11378 response: HarnessProviderInteractionResponseV1::Answer {
11379 text: "x".repeat(HARNESS_ACP_INTERACTION_RESPONSE_MAX_BYTES + 1),
11380 },
11381 };
11382 assert!(matches!(
11383 oversized_answer.validate(),
11384 Err(HarnessOperatorApiError::InvalidSessionControl),
11385 ));
11386
11387 let empty_mode_id = HarnessOperatorRequestV1::SetSessionMode {
11388 session: session.clone(),
11389 mode_id: String::new(),
11390 };
11391 assert!(matches!(
11392 empty_mode_id.validate(),
11393 Err(HarnessOperatorApiError::InvalidSessionControl),
11394 ));
11395
11396 let control_char_option_id = HarnessOperatorRequestV1::SetSessionConfigOption {
11397 session: session.clone(),
11398 option_id: "bad\u{0007}id".to_owned(),
11399 value_json: "true".to_owned(),
11400 };
11401 assert!(matches!(
11402 control_char_option_id.validate(),
11403 Err(HarnessOperatorApiError::InvalidSessionControl),
11404 ));
11405
11406 let empty_value_json = HarnessOperatorRequestV1::SetSessionConfigOption {
11407 session: session.clone(),
11408 option_id: "reasoning-effort".to_owned(),
11409 value_json: String::new(),
11410 };
11411 assert!(matches!(
11412 empty_value_json.validate(),
11413 Err(HarnessOperatorApiError::InvalidSessionControl),
11414 ));
11415
11416 let oversized_model_id = HarnessOperatorRequestV1::SetSessionModel {
11417 session: session.clone(),
11418 model_id: "x".repeat(HARNESS_AGENT_STREAM_ID_MAX_BYTES + 1),
11419 };
11420 assert!(matches!(
11421 oversized_model_id.validate(),
11422 Err(HarnessOperatorApiError::InvalidSessionControl),
11423 ));
11424
11425 let mut malformed_session = session.clone();
11426 malformed_session.instance_id = 0;
11427 let bad_session = HarnessOperatorRequestV1::SetSessionModel {
11428 session: malformed_session,
11429 model_id: "grok-4".to_owned(),
11430 };
11431 assert!(bad_session.validate().is_err());
11432 }
11433
11434 #[test]
11435 fn operator_subscribe_agent_stream_is_exact_round_trip() {
11436 let request = HarnessOperatorRequestV1::SubscribeAgentStream {
11437 sessions: vec![session_address(1, 1), session_address(2, 1)],
11438 };
11439 request.validate().expect("valid subscribe-agent-stream request");
11440 let encoded = serde_json::to_string(&request).unwrap();
11441 let decoded: HarnessOperatorRequestV1 = serde_json::from_str(&encoded).unwrap();
11442 assert_eq!(decoded, request);
11443
11444 let credential = HarnessOperatorCredential::parse(format!(
11445 "g4aho_{}",
11446 "a".repeat(64),
11447 )).unwrap();
11448 HarnessOperatorEnvelopeV1 {
11449 build_stamp: BUILD_STAMP.to_string(),
11450 credential,
11451 request,
11452 }.validate().unwrap();
11453 }
11454
11455 #[test]
11456 fn subscribe_agent_stream_rejects_empty_oversized_and_duplicate_session_lists() {
11457 assert!(matches!(
11458 HarnessOperatorRequestV1::SubscribeAgentStream { sessions: Vec::new() }.validate(),
11459 Err(HarnessOperatorApiError::InvalidAgentStream),
11460 ));
11461
11462 let oversized = (0..=HARNESS_AGENT_STREAM_SUBSCRIPTION_SESSIONS_MAX as u64)
11463 .map(|instance_id| session_address(instance_id + 1, 1))
11464 .collect::<Vec<_>>();
11465 assert!(matches!(
11466 HarnessOperatorRequestV1::SubscribeAgentStream { sessions: oversized }.validate(),
11467 Err(HarnessOperatorApiError::InvalidAgentStream),
11468 ));
11469
11470 let duplicated = vec![session_address(1, 1), session_address(1, 1)];
11471 assert!(matches!(
11472 HarnessOperatorRequestV1::SubscribeAgentStream { sessions: duplicated }.validate(),
11473 Err(HarnessOperatorApiError::InvalidAgentStream),
11474 ));
11475
11476 let mut malformed = session_address(1, 1);
11477 malformed.node_id = String::new();
11478 assert!(HarnessOperatorRequestV1::SubscribeAgentStream { sessions: vec![malformed] }
11479 .validate()
11480 .is_err());
11481 }
11482
11483 #[test]
11484 fn operator_agent_event_variants_are_exact_round_trips() {
11485 let session = session_address(1, 1);
11486 let chunks = vec![
11487 HarnessAgentStreamChunkKindV1::Text { text: "hello".to_owned(), is_delta: false },
11488 HarnessAgentStreamChunkKindV1::Thinking { text: "considering options".to_owned() },
11489 HarnessAgentStreamChunkKindV1::InteractionPrompt {
11490 correlation_id: "correlation-1".to_owned(),
11491 interaction_kind: HarnessProviderInteractionKindV1::Approval,
11492 tool_name: "execute".to_owned(),
11493 title: Some("Run command?".to_owned()),
11494 prompt: "rm -rf /tmp/scratch".to_owned(),
11495 options: vec![HarnessAgentStreamInteractionOptionV1 {
11496 option_id: "allow".to_owned(),
11497 name: "Allow".to_owned(),
11498 kind: "allow_once".to_owned(),
11499 }],
11500 },
11501 HarnessAgentStreamChunkKindV1::ModeCatalog {
11502 current: Some("plan".to_owned()),
11503 available: vec![HarnessAgentStreamNamedIdV1 {
11504 id: "plan".to_owned(),
11505 name: "Plan".to_owned(),
11506 description: None,
11507 }],
11508 },
11509 HarnessAgentStreamChunkKindV1::ConfigOptions {
11510 options: vec![HarnessProviderConfigOptionV1 {
11511 id: "reasoning-effort".to_owned(),
11512 name: "Reasoning effort".to_owned(),
11513 description: Some("How hard to think".to_owned()),
11514 category: Some("model".to_owned()),
11515 kind: HarnessProviderConfigOptionKindV1::Select,
11516 value_json: "\"high\"".to_owned(),
11517 choices: vec![HarnessProviderConfigChoiceV1 {
11518 value_json: "\"high\"".to_owned(),
11519 label: Some("High".to_owned()),
11520 }],
11521 }],
11522 },
11523 HarnessAgentStreamChunkKindV1::ModelCatalog {
11524 current: Some("grok-4".to_owned()),
11525 available: vec![HarnessAgentStreamNamedIdV1 {
11526 id: "grok-4".to_owned(),
11527 name: "Grok 4".to_owned(),
11528 description: None,
11529 }],
11530 },
11531 ];
11532
11533 let mut events: Vec<HarnessOperatorAgentEventV1> = chunks
11534 .into_iter()
11535 .enumerate()
11536 .map(|(index, kind)| HarnessOperatorAgentEventV1::AgentChunk {
11537 sequence: index as u64,
11538 session: session.clone(),
11539 chunk: HarnessAgentStreamChunkV1 { source_sequence: index as u64, kind },
11540 published_at_ms: 1_000 + index as u64,
11541 })
11542 .collect();
11543 events.push(HarnessOperatorAgentEventV1::Lagged {
11544 sequence: 100,
11545 session: session.clone(),
11546 dropped: 3,
11547 });
11548 events.push(HarnessOperatorAgentEventV1::ReplayBoundary {
11549 session: session.clone(),
11550 replayed: 6,
11551 dropped_before_replay: 44,
11552 });
11553 events.push(HarnessOperatorAgentEventV1::Ping { sequence: 101 });
11554
11555 for event in events {
11556 event.validate().expect("valid operator agent event");
11557 let encoded = serde_json::to_string(&event).unwrap();
11558 let decoded: HarnessOperatorAgentEventV1 = serde_json::from_str(&encoded).unwrap();
11559 assert_eq!(decoded, event);
11560 }
11561 }
11562
11563 #[test]
11574 fn parent_filters_default_on_old_shape_and_round_trip() {
11575 let task_id = HarnessTaskId::new(format!("htask_{}", "a".repeat(24))).unwrap();
11576 let tasks_request = HarnessReadRequestV1::TasksList {
11577 after_task_id: None,
11578 state: None,
11579 parent_task_id: Some(task_id.clone()),
11580 limit: 10,
11581 };
11582 tasks_request.validate().unwrap();
11583 let encoded_tasks = serde_json::to_string(&tasks_request).unwrap();
11584 assert_eq!(
11585 serde_json::from_str::<HarnessReadRequestV1>(&encoded_tasks).unwrap(),
11586 tasks_request,
11587 );
11588
11589 let old_shape_tasks = serde_json::json!({
11590 "kind": "tasks-list",
11591 "after_task_id": null,
11592 "state": null,
11593 "limit": 10,
11594 });
11595 let decoded_tasks: HarnessReadRequestV1 = serde_json::from_value(old_shape_tasks).unwrap();
11596 match &decoded_tasks {
11597 HarnessReadRequestV1::TasksList { parent_task_id, .. } => assert!(parent_task_id.is_none()),
11598 other => panic!("expected TasksList, got {other:?}"),
11599 }
11600 decoded_tasks.validate().unwrap();
11601 assert!(HarnessTaskId::new("not-a-task-id").is_err());
11602
11603 let run_id = HarnessRunId::new(format!("hrun_{}", "b".repeat(24))).unwrap();
11604 let runs_request = HarnessReadRequestV1::RunsList {
11605 task_id: None,
11606 after_run_id: None,
11607 lifecycle: None,
11608 parent_run_id: Some(run_id.clone()),
11609 limit: 10,
11610 };
11611 runs_request.validate().unwrap();
11612 let encoded_runs = serde_json::to_string(&runs_request).unwrap();
11613 assert_eq!(
11614 serde_json::from_str::<HarnessReadRequestV1>(&encoded_runs).unwrap(),
11615 runs_request,
11616 );
11617
11618 let old_shape_runs = serde_json::json!({
11619 "kind": "runs-list",
11620 "task_id": null,
11621 "after_run_id": null,
11622 "lifecycle": null,
11623 "limit": 10,
11624 });
11625 let decoded_runs: HarnessReadRequestV1 = serde_json::from_value(old_shape_runs).unwrap();
11626 match &decoded_runs {
11627 HarnessReadRequestV1::RunsList { parent_run_id, .. } => assert!(parent_run_id.is_none()),
11628 other => panic!("expected RunsList, got {other:?}"),
11629 }
11630 decoded_runs.validate().unwrap();
11631 assert!(HarnessRunId::new("not-a-run-id").is_err());
11632 }
11633
11634 #[test]
11635 fn operator_agent_event_validate_rejects_malformed_chunks_or_sessions() {
11636 let session = session_address(1, 1);
11637
11638 let mut bad_session = session.clone();
11639 bad_session.node_id = String::new();
11640 let bad_session_event = HarnessOperatorAgentEventV1::AgentChunk {
11641 sequence: 0,
11642 session: bad_session,
11643 chunk: sample_agent_stream_chunk(0),
11644 published_at_ms: 1_000,
11645 };
11646 assert!(bad_session_event.validate().is_err());
11647
11648 let oversized_text_chunk = HarnessAgentStreamChunkV1 {
11649 source_sequence: 0,
11650 kind: HarnessAgentStreamChunkKindV1::Text {
11651 text: "x".repeat(HARNESS_AGENT_STREAM_TEXT_MAX_BYTES + 1),
11652 is_delta: false,
11653 },
11654 };
11655 let bad_chunk_event = HarnessOperatorAgentEventV1::AgentChunk {
11656 sequence: 0,
11657 session: session.clone(),
11658 chunk: oversized_text_chunk,
11659 published_at_ms: 1_000,
11660 };
11661 assert!(matches!(
11662 bad_chunk_event.validate(),
11663 Err(HarnessOperatorApiError::InvalidAgentStream),
11664 ));
11665
11666 let empty_correlation_prompt = HarnessAgentStreamChunkV1 {
11667 source_sequence: 0,
11668 kind: HarnessAgentStreamChunkKindV1::InteractionPrompt {
11669 correlation_id: String::new(),
11670 interaction_kind: HarnessProviderInteractionKindV1::Question,
11671 tool_name: "ask".to_owned(),
11672 title: None,
11673 prompt: "What next?".to_owned(),
11674 options: Vec::new(),
11675 },
11676 };
11677 let bad_prompt_event = HarnessOperatorAgentEventV1::AgentChunk {
11678 sequence: 0,
11679 session: session.clone(),
11680 chunk: empty_correlation_prompt,
11681 published_at_ms: 1_000,
11682 };
11683 assert!(matches!(
11684 bad_prompt_event.validate(),
11685 Err(HarnessOperatorApiError::InvalidAgentStream),
11686 ));
11687 }
11688
11689 #[test]
11694 fn task_create_request_round_trips_and_bounds_title_and_body() {
11695 let request = HarnessReadRequestV1::TaskCreate {
11696 title: "Investigate the flaky retry".to_owned(),
11697 body: "See the timeline for the failing run.".to_owned(),
11698 parent_task_id: Some(HarnessTaskId::new(format!("htask_{}", "a".repeat(24))).unwrap()),
11699 };
11700 request.validate().unwrap();
11701 let encoded = serde_json::to_string(&request).unwrap();
11702 assert_eq!(serde_json::from_str::<HarnessReadRequestV1>(&encoded).unwrap(), request);
11703
11704 let no_parent = HarnessReadRequestV1::TaskCreate {
11705 title: "Child under my own task".to_owned(),
11706 body: String::new(),
11707 parent_task_id: None,
11708 };
11709 no_parent.validate().unwrap();
11710
11711 let empty_title = HarnessReadRequestV1::TaskCreate {
11712 title: String::new(),
11713 body: String::new(),
11714 parent_task_id: None,
11715 };
11716 assert!(matches!(empty_title.validate(), Err(HarnessReadApiError::InvalidTaskCreate)));
11717
11718 let oversized_title = HarnessReadRequestV1::TaskCreate {
11719 title: "x".repeat(HARNESS_TITLE_MAX_BYTES + 1),
11720 body: String::new(),
11721 parent_task_id: None,
11722 };
11723 assert!(matches!(oversized_title.validate(), Err(HarnessReadApiError::InvalidTaskCreate)));
11724
11725 let oversized_body = HarnessReadRequestV1::TaskCreate {
11726 title: "ok".to_owned(),
11727 body: "x".repeat(HARNESS_BODY_MAX_BYTES + 1),
11728 parent_task_id: None,
11729 };
11730 assert!(matches!(oversized_body.validate(), Err(HarnessReadApiError::InvalidTaskCreate)));
11731 }
11732
11733 #[test]
11735 fn task_move_request_round_trips() {
11736 let request = HarnessReadRequestV1::TaskMove {
11737 task_id: HarnessTaskId::new(format!("htask_{}", "b".repeat(24))).unwrap(),
11738 expected_revision: HarnessRevision::new(3).unwrap(),
11739 to: HarnessTaskStateV1::Ready,
11740 };
11741 request.validate().unwrap();
11742 let encoded = serde_json::to_string(&request).unwrap();
11743 assert_eq!(serde_json::from_str::<HarnessReadRequestV1>(&encoded).unwrap(), request);
11744 }
11745
11746 #[test]
11751 fn run_finish_request_round_trips_and_bounds_summary() {
11752 let request = HarnessReadRequestV1::RunFinish {
11753 outcome: HarnessRunFinishOutcomeV1::Done,
11754 summary: Some("handed off cleanly".to_owned()),
11755 };
11756 request.validate().unwrap();
11757 let encoded = serde_json::to_string(&request).unwrap();
11758 assert_eq!(serde_json::from_str::<HarnessReadRequestV1>(&encoded).unwrap(), request);
11759
11760 let without_summary: HarnessReadRequestV1 = serde_json::from_value(serde_json::json!({
11761 "kind": "run-finish",
11762 "outcome": "failed",
11763 })).unwrap();
11764 assert_eq!(
11765 without_summary,
11766 HarnessReadRequestV1::RunFinish { outcome: HarnessRunFinishOutcomeV1::Failed, summary: None },
11767 );
11768 without_summary.validate().unwrap();
11769
11770 let oversized_summary = HarnessReadRequestV1::RunFinish {
11771 outcome: HarnessRunFinishOutcomeV1::Done,
11772 summary: Some("x".repeat(HARNESS_BODY_MAX_BYTES + 1)),
11773 };
11774 assert!(matches!(
11775 oversized_summary.validate(),
11776 Err(HarnessReadApiError::InvalidText("run finish summary")),
11777 ));
11778 }
11779
11780 #[test]
11783 fn run_finish_result_round_trips_through_the_response_wire() {
11784 let finished = HarnessReadResponseV1::RunFinish(HarnessRunFinishResultV1::Finished {
11785 run_id: HarnessRunId::new(format!("hrun_{}", "d".repeat(24))).unwrap(),
11786 task_id: HarnessTaskId::new(format!("htask_{}", "d".repeat(24))).unwrap(),
11787 result: HarnessRunFinishOutcomeV1::Done,
11788 });
11789 finished.validate().unwrap();
11790 let encoded = serde_json::to_string(&finished).unwrap();
11791 assert_eq!(serde_json::from_str::<HarnessReadResponseV1>(&encoded).unwrap(), finished);
11792
11793 let already_finished = HarnessReadResponseV1::RunFinish(HarnessRunFinishResultV1::AlreadyFinished {
11794 run_id: HarnessRunId::new(format!("hrun_{}", "d".repeat(24))).unwrap(),
11795 lifecycle: HarnessRunLifecycleV1::Failed,
11796 });
11797 already_finished.validate().unwrap();
11798 }
11799
11800 #[test]
11804 fn task_create_and_move_results_round_trip_every_variant() {
11805 let created = HarnessReadResponseV1::TaskCreate(HarnessTaskCreateResultV1::Created {
11806 task_id: HarnessTaskId::new(format!("htask_{}", "c".repeat(24))).unwrap(),
11807 revision: HarnessRevision::new(1).unwrap(),
11808 });
11809 created.validate().unwrap();
11810 let encoded = serde_json::to_string(&created).unwrap();
11811 assert_eq!(serde_json::from_str::<HarnessReadResponseV1>(&encoded).unwrap(), created);
11812
11813 let parent_outside = HarnessReadResponseV1::TaskCreate(
11814 HarnessTaskCreateResultV1::ParentOutsideOwnSubtree {
11815 parent_task_id: HarnessTaskId::new(format!("htask_{}", "d".repeat(24))).unwrap(),
11816 own_task_id: HarnessTaskId::new(format!("htask_{}", "e".repeat(24))).unwrap(),
11817 },
11818 );
11819 parent_outside.validate().unwrap();
11820
11821 let parent_terminal = HarnessReadResponseV1::TaskCreate(HarnessTaskCreateResultV1::ParentTerminal {
11822 parent_task_id: HarnessTaskId::new(format!("htask_{}", "d".repeat(24))).unwrap(),
11823 state: HarnessTaskStateV1::Done,
11824 });
11825 parent_terminal.validate().unwrap();
11826
11827 let title_invalid = HarnessReadResponseV1::TaskCreate(HarnessTaskCreateResultV1::TitleInvalid {
11828 why: "task title is empty, unbounded, padded, or contains control characters".to_owned(),
11829 });
11830 title_invalid.validate().unwrap();
11831
11832 let body_invalid = HarnessReadResponseV1::TaskCreate(HarnessTaskCreateResultV1::BodyInvalid {
11833 why: "task body is unbounded".to_owned(),
11834 });
11835 body_invalid.validate().unwrap();
11836
11837 let dependencies_invalid = HarnessReadResponseV1::TaskCreate(
11838 HarnessTaskCreateResultV1::DependenciesInvalid { why: "dependencies must not self-link".to_owned() },
11839 );
11840 dependencies_invalid.validate().unwrap();
11841
11842 let moved = HarnessReadResponseV1::TaskMove(HarnessTaskMoveResultV1::Moved {
11843 task_id: HarnessTaskId::new(format!("htask_{}", "f".repeat(24))).unwrap(),
11844 revision: HarnessRevision::new(2).unwrap(),
11845 from: HarnessTaskStateV1::Backlog,
11846 to: HarnessTaskStateV1::Ready,
11847 });
11848 moved.validate().unwrap();
11849 let encoded = serde_json::to_string(&moved).unwrap();
11850 assert_eq!(serde_json::from_str::<HarnessReadResponseV1>(&encoded).unwrap(), moved);
11851
11852 let task_is_own = HarnessReadResponseV1::TaskMove(HarnessTaskMoveResultV1::TaskIsOwn {
11853 task_id: HarnessTaskId::new(format!("htask_{}", "f".repeat(24))).unwrap(),
11854 });
11855 task_is_own.validate().unwrap();
11856
11857 let outside_subtree = HarnessReadResponseV1::TaskMove(HarnessTaskMoveResultV1::TaskOutsideOwnSubtree {
11858 task_id: HarnessTaskId::new(format!("htask_{}", "f".repeat(24))).unwrap(),
11859 own_task_id: HarnessTaskId::new(format!("htask_{}", "0".repeat(24))).unwrap(),
11860 });
11861 outside_subtree.validate().unwrap();
11862
11863 let illegal_transition = HarnessReadResponseV1::TaskMove(HarnessTaskMoveResultV1::IllegalTransition {
11864 task_id: HarnessTaskId::new(format!("htask_{}", "f".repeat(24))).unwrap(),
11865 from: HarnessTaskStateV1::Done,
11866 to: HarnessTaskStateV1::Ready,
11867 });
11868 illegal_transition.validate().unwrap();
11869
11870 let revision_conflict = HarnessReadResponseV1::TaskMove(HarnessTaskMoveResultV1::RevisionConflict {
11871 task_id: HarnessTaskId::new(format!("htask_{}", "f".repeat(24))).unwrap(),
11872 expected: HarnessRevision::new(2).unwrap(),
11873 current: HarnessRevision::new(3).unwrap(),
11874 });
11875 revision_conflict.validate().unwrap();
11876 }
11877
11878 fn base_session_context() -> SessionContextV1 {
11879 SessionContextV1 {
11880 grant_id: SessionGrantId::new(format!("hgrant_{}", "a".repeat(24))).unwrap(),
11881 grant_revision: HarnessRevision::new(1).unwrap(),
11882 actor_run: CallerRunV1 {
11883 run_id: HarnessRunId::new(format!("hrun_{}", "a".repeat(24))).unwrap(),
11884 task_id: None,
11885 parent_run_id: None,
11886 lifecycle: HarnessRunLifecycleV1::Running,
11887 references_redacted: false,
11888 },
11889 task: None,
11890 sibling_runs: Vec::new(),
11891 read_permissions: HarnessReadPermissionsV1 {
11892 tasks: HarnessEntityReadScopeV1::None,
11893 runs: HarnessEntityReadScopeV1::None,
11894 operations: HarnessEntityReadScopeV1::None,
11895 },
11896 monitoring_visibility: HarnessMonitoringVisibilityV1::None,
11897 child_task_count: 0,
11898 child_task_subtree_depth: 0,
11899 task_create: false,
11900 task_mutate: false,
11901 allowed_tool_ids: vec!["g4a_context_get".to_owned(), "g4a_run_finish".to_owned()],
11902 history_message_count: None,
11903 completed_turn_count: None,
11904 total_tokens: None,
11905 }
11906 }
11907
11908 #[test]
11913 fn expected_allowed_tool_ids_derives_task_tools_from_task_permissions() {
11914 let base = base_session_context();
11915 base.validate().unwrap();
11916
11917 let create_only = SessionContextV1 {
11918 task_create: true,
11919 allowed_tool_ids: vec![
11920 "g4a_context_get".to_owned(),
11921 "g4a_run_finish".to_owned(),
11922 "g4a_task_create".to_owned(),
11923 ],
11924 ..base.clone()
11925 };
11926 create_only.validate().unwrap();
11927
11928 let both = SessionContextV1 {
11929 task_create: true,
11930 task_mutate: true,
11931 allowed_tool_ids: vec![
11932 "g4a_context_get".to_owned(),
11933 "g4a_run_finish".to_owned(),
11934 "g4a_task_create".to_owned(),
11935 "g4a_task_move".to_owned(),
11936 ],
11937 ..base.clone()
11938 };
11939 both.validate().unwrap();
11940
11941 let create_only_claims_move = SessionContextV1 {
11942 task_create: true,
11943 allowed_tool_ids: vec![
11944 "g4a_context_get".to_owned(),
11945 "g4a_task_create".to_owned(),
11946 "g4a_task_move".to_owned(),
11947 ],
11948 ..base
11949 };
11950 assert!(matches!(
11951 create_only_claims_move.validate(),
11952 Err(HarnessReadApiError::InvalidAllowedTools),
11953 ));
11954 }
11955
11956 #[test]
11964 fn child_task_observations_carry_unbounded_values() {
11965 let base = base_session_context();
11966 let many_children = SessionContextV1 {
11967 child_task_count: u64::from(HARNESS_CHILD_COUNT_MAX) * 1_000,
11968 child_task_subtree_depth: u64::from(HARNESS_CHILD_DEPTH_MAX) * 1_000,
11969 ..base
11970 };
11971 many_children.validate().unwrap();
11972 let encoded = serde_json::to_value(&many_children).unwrap();
11973 assert_eq!(
11974 encoded.get("child_task_count").and_then(serde_json::Value::as_u64),
11975 Some(u64::from(HARNESS_CHILD_COUNT_MAX) * 1_000),
11976 );
11977 assert_eq!(
11978 encoded.get("child_task_subtree_depth").and_then(serde_json::Value::as_u64),
11979 Some(u64::from(HARNESS_CHILD_DEPTH_MAX) * 1_000),
11980 );
11981 assert!(encoded.get("maximum_child_count").is_none());
11982 assert!(encoded.get("maximum_child_depth").is_none());
11983 let decoded: SessionContextV1 = serde_json::from_value(encoded).unwrap();
11984 assert_eq!(decoded, many_children);
11985 }
11986}