1use std::fmt;
4
5use serde::{Serialize, Serializer};
6use serde_json::{json, Map, Value};
7
8use crate::policy::PolicyViolation;
9use crate::{
10 CloudEventV1, DnsAuthorityDnssecFailed, DnsAuthorityDrift, DnsAuthorityRebindRejected,
11 DnsAuthorityRebindThreshold, DnsQueryEvent, ExecutionCellSpec, ExportReceipt,
12 NetworkFlowDecision, WorkloadIdentity,
13};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
19#[serde(rename_all = "lowercase")]
20pub enum LifecycleDestroyOutcome {
21 Succeeded,
22 Failed,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
43#[serde(rename_all = "lowercase")]
44pub enum LifecycleTerminalState {
45 Clean,
46 Forced,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
66#[non_exhaustive]
67pub enum LifecycleReason {
68 Oom,
70 TtlExceeded,
72 VmmCrashed,
74 BootFailed,
76 SignalKilled,
79 InitCrashed,
81 KernelCannotMountRoot,
84 Other(String),
87}
88
89impl LifecycleReason {
90 pub fn as_wire_str(&self) -> &str {
93 match self {
94 LifecycleReason::Oom => "oom",
95 LifecycleReason::TtlExceeded => "ttl_exceeded",
96 LifecycleReason::VmmCrashed => "vmm_crashed",
97 LifecycleReason::BootFailed => "boot_failed",
98 LifecycleReason::SignalKilled => "signal_killed",
99 LifecycleReason::InitCrashed => "init_crashed",
100 LifecycleReason::KernelCannotMountRoot => "kernel_cannot_mount_root",
101 LifecycleReason::Other(s) => s.as_str(),
102 }
103 }
104}
105
106impl fmt::Display for LifecycleReason {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 f.write_str(self.as_wire_str())
109 }
110}
111
112impl Serialize for LifecycleReason {
113 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
114 serializer.serialize_str(self.as_wire_str())
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
120#[serde(rename_all = "lowercase")]
121pub enum IdentityFailureOperation {
122 Materialize,
123 Revoke,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
153#[serde(rename_all = "camelCase")]
154pub struct Provenance {
155 pub parent: String,
157 pub parent_type: String,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
187#[serde(transparent)]
188pub struct SubjectUrn(String);
189
190#[derive(Debug, Clone, PartialEq, Eq)]
192pub enum SubjectUrnError {
193 MissingUrnScheme,
195 TooFewSegments,
197 EmptySegment,
199 InvalidToolOrKindCharset,
201 ControlOrWhitespace,
203}
204
205impl std::fmt::Display for SubjectUrnError {
206 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207 match self {
208 SubjectUrnError::MissingUrnScheme => f.write_str("subject URN must start with `urn:`"),
209 SubjectUrnError::TooFewSegments => {
210 f.write_str("subject URN must have shape `urn:<tool>:<kind>:<id>`")
211 }
212 SubjectUrnError::EmptySegment => {
213 f.write_str("subject URN tool / kind / id segments must each be non-empty")
214 }
215 SubjectUrnError::InvalidToolOrKindCharset => {
216 f.write_str("subject URN tool and kind must match charset [a-z0-9-]")
217 }
218 SubjectUrnError::ControlOrWhitespace => {
219 f.write_str("subject URN must not contain ASCII control characters or whitespace")
220 }
221 }
222 }
223}
224
225impl std::error::Error for SubjectUrnError {}
226
227impl SubjectUrn {
228 pub fn parse(s: impl Into<String>) -> Result<Self, SubjectUrnError> {
231 let s = s.into();
232
233 if s.bytes()
235 .any(|b| b.is_ascii_control() || (b as char).is_whitespace())
236 {
237 return Err(SubjectUrnError::ControlOrWhitespace);
238 }
239
240 let rest = match s.strip_prefix("urn:") {
242 Some(r) => r,
243 None => return Err(SubjectUrnError::MissingUrnScheme),
244 };
245
246 let mut parts = rest.splitn(3, ':');
250 let tool = parts.next().ok_or(SubjectUrnError::TooFewSegments)?;
251 let kind = parts.next().ok_or(SubjectUrnError::TooFewSegments)?;
252 let id = parts.next().ok_or(SubjectUrnError::TooFewSegments)?;
253
254 if tool.is_empty() || kind.is_empty() || id.is_empty() {
256 return Err(SubjectUrnError::EmptySegment);
257 }
258
259 let ok_segment = |seg: &str| {
261 seg.bytes()
262 .all(|b| matches!(b, b'a'..=b'z' | b'0'..=b'9' | b'-'))
263 };
264 if !ok_segment(tool) || !ok_segment(kind) {
265 return Err(SubjectUrnError::InvalidToolOrKindCharset);
266 }
267
268 Ok(SubjectUrn(s))
269 }
270
271 pub fn as_str(&self) -> &str {
273 &self.0
274 }
275
276 pub fn into_inner(self) -> String {
278 self.0
279 }
280}
281
282impl AsRef<str> for SubjectUrn {
283 fn as_ref(&self) -> &str {
284 &self.0
285 }
286}
287
288impl std::fmt::Display for SubjectUrn {
289 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290 f.write_str(&self.0)
291 }
292}
293
294pub fn cell_subject_urn(cell_id: &str) -> Result<SubjectUrn, SubjectUrnError> {
305 SubjectUrn::parse(format!("urn:cellos:cell:{cell_id}"))
306}
307
308#[allow(clippy::too_many_arguments)]
350pub fn lifecycle_started_data_v1(
351 spec: &ExecutionCellSpec,
352 cell_id: &str,
353 run_id: Option<&str>,
354 derivation_verified: Option<bool>,
355 role_root: Option<&str>,
356 parent_run_id: Option<&str>,
357 spec_hash: Option<&str>,
358 kernel_digest_sha256: Option<&str>,
359 rootfs_digest_sha256: Option<&str>,
360 firecracker_digest_sha256: Option<&str>,
361) -> Result<Value, serde_json::Error> {
362 let mut m = Map::new();
363 m.insert("cellId".to_string(), json!(cell_id));
364 m.insert("specId".to_string(), json!(&spec.id));
365 m.insert("ttlSeconds".to_string(), json!(spec.lifetime.ttl_seconds));
366 if let Some(r) = run_id {
367 m.insert("runId".to_string(), json!(r));
368 }
369 if let Some(verified) = derivation_verified {
370 m.insert("derivationVerified".to_string(), json!(verified));
371 }
372 if let Some(role) = role_root {
373 m.insert("roleRoot".to_string(), json!(role));
374 }
375 if let Some(parent) = parent_run_id {
376 m.insert("parentRunId".to_string(), json!(parent));
377 }
378 if let Some(hash) = spec_hash {
379 m.insert("specHash".to_string(), json!(hash));
380 }
381 if let Some(d) = kernel_digest_sha256 {
382 m.insert("kernelDigestSha256".to_string(), json!(d));
383 }
384 if let Some(d) = rootfs_digest_sha256 {
385 m.insert("rootfsDigestSha256".to_string(), json!(d));
386 }
387 if let Some(d) = firecracker_digest_sha256 {
388 m.insert("firecrackerDigestSha256".to_string(), json!(d));
389 }
390 if let Some(placement) = &spec.placement {
391 let mut placement_map = Map::new();
392 if let Some(pool_id) = &placement.pool_id {
393 placement_map.insert("poolId".to_string(), json!(pool_id));
394 }
395 if let Some(namespace) = &placement.kubernetes_namespace {
396 placement_map.insert("kubernetesNamespace".to_string(), json!(namespace));
397 }
398 if let Some(queue_name) = &placement.queue_name {
399 placement_map.insert("queueName".to_string(), json!(queue_name));
400 }
401 if !placement_map.is_empty() {
402 m.insert("placement".to_string(), Value::Object(placement_map));
403 }
404 }
405 if let Some(c) = &spec.correlation {
406 if let Some(tid) = &c.tenant_id {
407 m.insert("tenantId".to_string(), json!(tid));
408 }
409 m.insert("correlation".to_string(), serde_json::to_value(c)?);
410 }
411 Ok(Value::Object(m))
412}
413
414#[allow(clippy::too_many_arguments)]
453pub fn lifecycle_destroyed_data_v1(
454 spec: &ExecutionCellSpec,
455 cell_id: &str,
456 run_id: Option<&str>,
457 outcome: LifecycleDestroyOutcome,
458 reason: Option<&str>,
459 terminal_state: Option<LifecycleTerminalState>,
460 evidence_bundle_ref: Option<&SubjectUrn>,
461 residue_class: Option<ResidueClass>,
462) -> Result<Value, serde_json::Error> {
463 let mut m = Map::new();
464 m.insert("cellId".to_string(), json!(cell_id));
465 m.insert("specId".to_string(), json!(&spec.id));
466 m.insert("ttlSeconds".to_string(), json!(spec.lifetime.ttl_seconds));
467 m.insert("outcome".to_string(), serde_json::to_value(outcome)?);
468 if let Some(r) = run_id {
469 m.insert("runId".to_string(), json!(r));
470 }
471 if let Some(c) = &spec.correlation {
472 if let Some(tid) = &c.tenant_id {
473 m.insert("tenantId".to_string(), json!(tid));
474 }
475 m.insert("correlation".to_string(), serde_json::to_value(c)?);
476 }
477 if let Some(s) = reason {
478 m.insert("reason".to_string(), json!(s));
479 }
480 if let Some(ts) = terminal_state {
481 m.insert("terminalState".to_string(), serde_json::to_value(ts)?);
482 }
483 if let Some(urn) = evidence_bundle_ref {
484 m.insert("evidenceBundleRef".to_string(), json!(urn));
485 }
486 if let Some(rc) = residue_class {
487 m.insert("residueClass".to_string(), serde_json::to_value(rc)?);
488 }
489 Ok(Value::Object(m))
490}
491
492pub const LIFECYCLE_MANIFEST_FAILED_TYPE: &str =
500 "dev.cellos.events.cell.lifecycle.v1.manifest-failed";
501
502pub fn manifest_failed_data_v1(
515 role: &str,
516 expected_sha256: &str,
517 actual_sha256: &str,
518 manifest_path: &str,
519) -> Result<Value, serde_json::Error> {
520 let mut m = Map::new();
521 m.insert("role".to_string(), json!(role));
522 m.insert("expectedSha256".to_string(), json!(expected_sha256));
523 m.insert("actualSha256".to_string(), json!(actual_sha256));
524 m.insert("manifestPath".to_string(), json!(manifest_path));
525 Ok(Value::Object(m))
526}
527
528#[allow(clippy::too_many_arguments)]
551pub fn lifecycle_destroyed_data_v1_typed(
552 spec: &ExecutionCellSpec,
553 cell_id: &str,
554 run_id: Option<&str>,
555 outcome: LifecycleDestroyOutcome,
556 reason: Option<LifecycleReason>,
557 terminal_state: Option<LifecycleTerminalState>,
558 evidence_bundle_ref: Option<&SubjectUrn>,
559 residue_class: Option<ResidueClass>,
560) -> Result<Value, serde_json::Error> {
561 let reason_str = reason.as_ref().map(|r| r.as_wire_str());
562 lifecycle_destroyed_data_v1(
563 spec,
564 cell_id,
565 run_id,
566 outcome,
567 reason_str,
568 terminal_state,
569 evidence_bundle_ref,
570 residue_class,
571 )
572}
573
574pub fn identity_materialized_data_v1(
578 spec: &ExecutionCellSpec,
579 cell_id: &str,
580 run_id: Option<&str>,
581 identity: &WorkloadIdentity,
582) -> Result<Value, serde_json::Error> {
583 let mut m = Map::new();
584 m.insert("cellId".to_string(), json!(cell_id));
585 m.insert("specId".to_string(), json!(&spec.id));
586 m.insert("identity".to_string(), serde_json::to_value(identity)?);
587 if let Some(r) = run_id {
588 m.insert("runId".to_string(), json!(r));
589 }
590 if let Some(c) = &spec.correlation {
591 m.insert("correlation".to_string(), serde_json::to_value(c)?);
592 }
593 Ok(Value::Object(m))
594}
595
596#[derive(Debug, Clone, Default, PartialEq, Eq)]
608pub struct ProvisioningWitness<'a> {
609 pub provisioned_role_arn: Option<&'a str>,
612 pub session_policy_digest: Option<&'a str>,
616 pub azure_object_id: Option<&'a str>,
619 pub credential_jti: Option<&'a str>,
622}
623
624pub fn identity_materialized_data_v1_provisioned(
637 spec: &ExecutionCellSpec,
638 cell_id: &str,
639 run_id: Option<&str>,
640 identity: &WorkloadIdentity,
641 witness: &ProvisioningWitness<'_>,
642) -> Result<Value, serde_json::Error> {
643 let mut data = identity_materialized_data_v1(spec, cell_id, run_id, identity)?;
644 let m = data
645 .as_object_mut()
646 .expect("identity_materialized_data_v1 returns a JSON object");
647 if let Some(arn) = witness.provisioned_role_arn {
648 m.insert("provisionedRoleArn".to_string(), json!(arn));
649 }
650 if let Some(digest) = witness.session_policy_digest {
651 m.insert("sessionPolicyDigest".to_string(), json!(digest));
652 }
653 if let Some(oid) = witness.azure_object_id {
654 m.insert("azureObjectId".to_string(), json!(oid));
655 }
656 if let Some(jti) = witness.credential_jti {
657 m.insert("credentialJti".to_string(), json!(jti));
658 }
659 Ok(data)
660}
661
662pub fn identity_revoked_data_v1(
672 spec: &ExecutionCellSpec,
673 cell_id: &str,
674 run_id: Option<&str>,
675 identity: &WorkloadIdentity,
676 reason: Option<&str>,
677 provenance: Option<&Provenance>,
678) -> Result<Value, serde_json::Error> {
679 let mut m = Map::new();
680 m.insert("cellId".to_string(), json!(cell_id));
681 m.insert("specId".to_string(), json!(&spec.id));
682 m.insert("identity".to_string(), serde_json::to_value(identity)?);
683 if let Some(r) = run_id {
684 m.insert("runId".to_string(), json!(r));
685 }
686 if let Some(c) = &spec.correlation {
687 m.insert("correlation".to_string(), serde_json::to_value(c)?);
688 }
689 if let Some(s) = reason {
690 m.insert("reason".to_string(), json!(s));
691 }
692 if let Some(p) = provenance {
693 m.insert("provenance".to_string(), serde_json::to_value(p)?);
694 }
695 Ok(Value::Object(m))
696}
697
698pub fn identity_failed_data_v1(
702 spec: &ExecutionCellSpec,
703 cell_id: &str,
704 run_id: Option<&str>,
705 identity: &WorkloadIdentity,
706 operation: IdentityFailureOperation,
707 reason: &str,
708) -> Result<Value, serde_json::Error> {
709 let mut m = Map::new();
710 m.insert("cellId".to_string(), json!(cell_id));
711 m.insert("specId".to_string(), json!(&spec.id));
712 m.insert("identity".to_string(), serde_json::to_value(identity)?);
713 m.insert("operation".to_string(), serde_json::to_value(operation)?);
714 m.insert("reason".to_string(), json!(reason));
715 if let Some(r) = run_id {
716 m.insert("runId".to_string(), json!(r));
717 }
718 if let Some(c) = &spec.correlation {
719 m.insert("correlation".to_string(), serde_json::to_value(c)?);
720 }
721 Ok(Value::Object(m))
722}
723
724pub fn command_completed_data_v1(
728 spec: &ExecutionCellSpec,
729 cell_id: &str,
730 run_id: Option<&str>,
731 argv: &[String],
732 exit_code: i32,
733 duration_ms: u64,
734 spawn_error: Option<&str>,
735) -> Result<Value, serde_json::Error> {
736 let mut m = Map::new();
737 m.insert("cellId".to_string(), json!(cell_id));
738 m.insert("specId".to_string(), json!(&spec.id));
739 m.insert("exitCode".to_string(), json!(exit_code));
740 m.insert("durationMs".to_string(), json!(duration_ms));
741 m.insert("argv".to_string(), json!(argv));
742 if let Some(r) = run_id {
743 m.insert("runId".to_string(), json!(r));
744 }
745 if let Some(c) = &spec.correlation {
746 m.insert("correlation".to_string(), serde_json::to_value(c)?);
747 }
748 if let Some(s) = spawn_error {
749 m.insert("spawnError".to_string(), json!(s));
750 }
751 Ok(Value::Object(m))
752}
753
754pub fn observability_network_scope_data_v1(
758 spec: &ExecutionCellSpec,
759 cell_id: &str,
760 run_id: Option<&str>,
761 egress_rule_count: usize,
762 has_opaque_network_authority: bool,
763) -> Result<Value, serde_json::Error> {
764 let mut m = Map::new();
765 m.insert("cellId".to_string(), json!(cell_id));
766 m.insert("specId".to_string(), json!(&spec.id));
767 m.insert("egressRuleCount".to_string(), json!(egress_rule_count));
768 m.insert(
769 "hasOpaqueNetworkAuthority".to_string(),
770 json!(has_opaque_network_authority),
771 );
772 if let Some(r) = run_id {
773 m.insert("runId".to_string(), json!(r));
774 }
775 if let Some(c) = &spec.correlation {
776 m.insert("correlation".to_string(), serde_json::to_value(c)?);
777 }
778 Ok(Value::Object(m))
779}
780
781pub fn observability_process_spawned_data_v1(
785 spec: &ExecutionCellSpec,
786 cell_id: &str,
787 run_id: Option<&str>,
788 program: &str,
789 argc: usize,
790) -> Result<Value, serde_json::Error> {
791 let mut m = Map::new();
792 m.insert("cellId".to_string(), json!(cell_id));
793 m.insert("specId".to_string(), json!(&spec.id));
794 m.insert("program".to_string(), json!(program));
795 m.insert("argc".to_string(), json!(argc));
796 if let Some(r) = run_id {
797 m.insert("runId".to_string(), json!(r));
798 }
799 if let Some(c) = &spec.correlation {
800 m.insert("correlation".to_string(), serde_json::to_value(c)?);
801 }
802 Ok(Value::Object(m))
803}
804
805pub fn observability_fs_touch_export_data_v1(
809 spec: &ExecutionCellSpec,
810 cell_id: &str,
811 run_id: Option<&str>,
812 source_path: &str,
813 artifact_name: &str,
814) -> Result<Value, serde_json::Error> {
815 let mut m = Map::new();
816 m.insert("cellId".to_string(), json!(cell_id));
817 m.insert("specId".to_string(), json!(&spec.id));
818 m.insert("purpose".to_string(), json!("export"));
819 m.insert("sourcePath".to_string(), json!(source_path));
820 m.insert("artifactName".to_string(), json!(artifact_name));
821 if let Some(r) = run_id {
822 m.insert("runId".to_string(), json!(r));
823 }
824 if let Some(c) = &spec.correlation {
825 m.insert("correlation".to_string(), serde_json::to_value(c)?);
826 }
827 Ok(Value::Object(m))
828}
829
830pub fn export_completed_data_v1(
834 spec: &ExecutionCellSpec,
835 cell_id: &str,
836 run_id: Option<&str>,
837 artifact_name: &str,
838 bytes_written: u64,
839 destination_relative: &str,
840) -> Result<Value, serde_json::Error> {
841 let mut m = Map::new();
842 m.insert("cellId".to_string(), json!(cell_id));
843 m.insert("specId".to_string(), json!(&spec.id));
844 m.insert("artifactName".to_string(), json!(artifact_name));
845 m.insert("bytesWritten".to_string(), json!(bytes_written));
846 m.insert(
847 "destinationRelative".to_string(),
848 json!(destination_relative),
849 );
850 if let Some(r) = run_id {
851 m.insert("runId".to_string(), json!(r));
852 }
853 if let Some(c) = &spec.correlation {
854 m.insert("correlation".to_string(), serde_json::to_value(c)?);
855 }
856 Ok(Value::Object(m))
857}
858
859pub fn export_completed_data_v2(
870 spec: &ExecutionCellSpec,
871 cell_id: &str,
872 run_id: Option<&str>,
873 artifact_name: &str,
874 receipt: &ExportReceipt,
875 provenance: Option<&Provenance>,
876) -> Result<Value, serde_json::Error> {
877 let mut m = Map::new();
878 m.insert("cellId".to_string(), json!(cell_id));
879 m.insert("specId".to_string(), json!(&spec.id));
880 m.insert("artifactName".to_string(), json!(artifact_name));
881 m.insert("receipt".to_string(), serde_json::to_value(receipt)?);
882 if let Some(r) = run_id {
883 m.insert("runId".to_string(), json!(r));
884 }
885 if let Some(c) = &spec.correlation {
886 m.insert("correlation".to_string(), serde_json::to_value(c)?);
887 }
888 if let Some(p) = provenance {
889 m.insert("provenance".to_string(), serde_json::to_value(p)?);
890 }
891 Ok(Value::Object(m))
892}
893
894#[allow(clippy::too_many_arguments)] pub fn export_failed_data_v2(
906 spec: &ExecutionCellSpec,
907 cell_id: &str,
908 run_id: Option<&str>,
909 artifact_name: &str,
910 target_kind: crate::ExportReceiptTargetKind,
911 target_name: Option<&str>,
912 destination: Option<&str>,
913 reason: &str,
914 provenance: Option<&Provenance>,
915) -> Result<Value, serde_json::Error> {
916 let mut m = Map::new();
917 m.insert("cellId".to_string(), json!(cell_id));
918 m.insert("specId".to_string(), json!(&spec.id));
919 m.insert("artifactName".to_string(), json!(artifact_name));
920 m.insert("targetKind".to_string(), serde_json::to_value(target_kind)?);
921 m.insert("reason".to_string(), json!(reason));
922 if let Some(name) = target_name {
923 m.insert("targetName".to_string(), json!(name));
924 }
925 if let Some(dest) = destination {
926 m.insert("destination".to_string(), json!(dest));
927 }
928 if let Some(r) = run_id {
929 m.insert("runId".to_string(), json!(r));
930 }
931 if let Some(c) = &spec.correlation {
932 m.insert("correlation".to_string(), serde_json::to_value(c)?);
933 }
934 if let Some(p) = provenance {
935 m.insert("provenance".to_string(), serde_json::to_value(p)?);
936 }
937 Ok(Value::Object(m))
938}
939
940pub fn observability_network_policy_data_v1(
947 spec: &ExecutionCellSpec,
948 cell_id: &str,
949 run_id: Option<&str>,
950 isolation_mode: &str,
951 egress_rules: &[crate::EgressRule],
952) -> Result<Value, serde_json::Error> {
953 let mut m = Map::new();
954 m.insert("cellId".to_string(), json!(cell_id));
955 m.insert("specId".to_string(), json!(&spec.id));
956 m.insert("isolationMode".to_string(), json!(isolation_mode));
957 m.insert("declaredEgressCount".to_string(), json!(egress_rules.len()));
958 m.insert(
959 "declaredEgress".to_string(),
960 serde_json::to_value(egress_rules)?,
961 );
962 if let Some(r) = run_id {
963 m.insert("runId".to_string(), json!(r));
964 }
965 if let Some(c) = &spec.correlation {
966 m.insert("correlation".to_string(), serde_json::to_value(c)?);
967 }
968 Ok(Value::Object(m))
969}
970
971#[allow(clippy::too_many_arguments)]
978pub fn observability_network_enforcement_data_v1(
979 spec: &ExecutionCellSpec,
980 cell_id: &str,
981 run_id: Option<&str>,
982 nft_rules_applied: bool,
983 declared_egress_rule_count: usize,
984 command_exit_code: i32,
985 spawn_error: Option<&str>,
986) -> Result<Value, serde_json::Error> {
987 let supplementary = nft_rules_applied && declared_egress_rule_count > 0;
988 let mut m = Map::new();
989 m.insert("cellId".to_string(), json!(cell_id));
990 m.insert("specId".to_string(), json!(&spec.id));
991 m.insert("isolationMode".to_string(), json!("clone_newnet"));
992 m.insert("nftRulesApplied".to_string(), json!(nft_rules_applied));
993 m.insert(
994 "declaredEgressRuleCount".to_string(),
995 json!(declared_egress_rule_count),
996 );
997 m.insert(
998 "supplementaryEgressFilterActive".to_string(),
999 json!(supplementary),
1000 );
1001 m.insert("commandExitCode".to_string(), json!(command_exit_code));
1002 if let Some(r) = run_id {
1003 m.insert("runId".to_string(), json!(r));
1004 }
1005 if let Some(s) = spawn_error {
1006 m.insert("spawnError".to_string(), json!(s));
1007 }
1008 if let Some(c) = &spec.correlation {
1009 m.insert("correlation".to_string(), serde_json::to_value(c)?);
1010 }
1011 Ok(Value::Object(m))
1012}
1013
1014pub const TRUST_PLANE_BUILTIN_KEYSET_ID: &str = "cellos:builtin-v0";
1018
1019pub const TRUST_PLANE_BUILTIN_RESOLVER_KID: &str = "cellos-local-resolve-v0";
1021
1022pub const TRUST_PLANE_BUILTIN_L7_KID: &str = "cellos-local-l7-v0";
1024
1025pub const TRUST_PLANE_AGGREGATE_EGRESS_FQDN: &str = "declared-egress.trust.cellos.internal";
1027
1028#[allow(clippy::too_many_arguments)]
1032pub fn observability_dns_resolution_data_v1(
1033 spec: &ExecutionCellSpec,
1034 cell_id: &str,
1035 run_id: Option<&str>,
1036 fqdn: &str,
1037 resolved_at: &str,
1038 targets: &[(&str, &str, Option<u16>)],
1039 ttl_seconds: i64,
1040 policy_digest: &str,
1041 keyset_id: &str,
1042 issuer_kid: &str,
1043 receipt_id: Option<&str>,
1044) -> Result<Value, serde_json::Error> {
1045 let mut rows = Vec::with_capacity(targets.len());
1046 for (addr, family, port) in targets {
1047 let mut row = Map::new();
1048 row.insert("address".to_string(), json!(addr));
1049 row.insert("family".to_string(), json!(family));
1050 if let Some(p) = port {
1051 row.insert("port".to_string(), json!(p));
1052 }
1053 rows.push(Value::Object(row));
1054 }
1055 let mut m = Map::new();
1056 m.insert("cellId".to_string(), json!(cell_id));
1057 m.insert("specId".to_string(), json!(&spec.id));
1058 if let Some(r) = run_id {
1059 m.insert("runId".to_string(), json!(r));
1060 }
1061 if let Some(rid) = receipt_id {
1062 m.insert("receiptId".to_string(), json!(rid));
1063 }
1064 m.insert("fqdn".to_string(), json!(fqdn));
1065 m.insert("resolvedAt".to_string(), json!(resolved_at));
1066 m.insert("targets".to_string(), Value::Array(rows));
1067 m.insert("ttlSeconds".to_string(), json!(ttl_seconds));
1068 m.insert("policyDigest".to_string(), json!(policy_digest));
1069 m.insert("keysetId".to_string(), json!(keyset_id));
1070 m.insert("issuerKid".to_string(), json!(issuer_kid));
1071 if let Some(c) = &spec.correlation {
1072 m.insert("correlation".to_string(), serde_json::to_value(c)?);
1073 }
1074 Ok(Value::Object(m))
1075}
1076
1077#[allow(clippy::too_many_arguments)]
1081pub fn observability_dns_target_set_data_v1(
1082 spec: &ExecutionCellSpec,
1083 cell_id: &str,
1084 run_id: Option<&str>,
1085 fqdn: &str,
1086 previous_digest: &str,
1087 current_digest: &str,
1088 reason: &str,
1089 updated_at: &str,
1090 keyset_id: &str,
1091 issuer_kid: &str,
1092) -> Result<Value, serde_json::Error> {
1093 let mut m = Map::new();
1094 m.insert("cellId".to_string(), json!(cell_id));
1095 m.insert("specId".to_string(), json!(&spec.id));
1096 if let Some(r) = run_id {
1097 m.insert("runId".to_string(), json!(r));
1098 }
1099 m.insert("fqdn".to_string(), json!(fqdn));
1100 m.insert("previousDigest".to_string(), json!(previous_digest));
1101 m.insert("currentDigest".to_string(), json!(current_digest));
1102 m.insert("reason".to_string(), json!(reason));
1103 m.insert("updatedAt".to_string(), json!(updated_at));
1104 m.insert("keysetId".to_string(), json!(keyset_id));
1105 m.insert("issuerKid".to_string(), json!(issuer_kid));
1106 if let Some(c) = &spec.correlation {
1107 m.insert("correlation".to_string(), serde_json::to_value(c)?);
1108 }
1109 Ok(Value::Object(m))
1110}
1111
1112#[allow(clippy::too_many_arguments)]
1122pub fn dns_authority_drift_data_v1(drift: &DnsAuthorityDrift) -> Result<Value, serde_json::Error> {
1123 serde_json::to_value(drift)
1124}
1125
1126pub fn cloud_event_v1_dns_authority_drift(
1137 source: &str,
1138 time: &str,
1139 drift: &DnsAuthorityDrift,
1140) -> Result<CloudEventV1, serde_json::Error> {
1141 Ok(CloudEventV1 {
1142 specversion: "1.0".into(),
1143 id: uuid::Uuid::new_v4().to_string(),
1144 source: source.to_string(),
1145 ty: "dev.cellos.events.cell.observability.v1.dns_authority_drift".into(),
1146 datacontenttype: Some("application/json".into()),
1147 data: Some(dns_authority_drift_data_v1(drift)?),
1148 time: Some(time.to_string()),
1149 traceparent: None,
1150 cex: None,
1151 })
1152}
1153
1154pub fn dns_authority_rebind_threshold_data_v1(
1166 payload: &DnsAuthorityRebindThreshold,
1167) -> Result<Value, serde_json::Error> {
1168 serde_json::to_value(payload)
1169}
1170
1171pub fn cloud_event_v1_dns_authority_rebind_threshold(
1183 source: &str,
1184 time: &str,
1185 payload: &DnsAuthorityRebindThreshold,
1186) -> Result<CloudEventV1, serde_json::Error> {
1187 Ok(CloudEventV1 {
1188 specversion: "1.0".into(),
1189 id: uuid::Uuid::new_v4().to_string(),
1190 source: source.to_string(),
1191 ty: "dev.cellos.events.cell.observability.v1.dns_authority_rebind_threshold".into(),
1192 datacontenttype: Some("application/json".into()),
1193 data: Some(dns_authority_rebind_threshold_data_v1(payload)?),
1194 time: Some(time.to_string()),
1195 traceparent: None,
1196 cex: None,
1197 })
1198}
1199
1200pub fn dns_authority_rebind_rejected_data_v1(
1212 payload: &DnsAuthorityRebindRejected,
1213) -> Result<Value, serde_json::Error> {
1214 serde_json::to_value(payload)
1215}
1216
1217pub fn cloud_event_v1_dns_authority_rebind_rejected(
1224 source: &str,
1225 time: &str,
1226 payload: &DnsAuthorityRebindRejected,
1227) -> Result<CloudEventV1, serde_json::Error> {
1228 Ok(CloudEventV1 {
1229 specversion: "1.0".into(),
1230 id: uuid::Uuid::new_v4().to_string(),
1231 source: source.to_string(),
1232 ty: "dev.cellos.events.cell.observability.v1.dns_authority_rebind_rejected".into(),
1233 datacontenttype: Some("application/json".into()),
1234 data: Some(dns_authority_rebind_rejected_data_v1(payload)?),
1235 time: Some(time.to_string()),
1236 traceparent: None,
1237 cex: None,
1238 })
1239}
1240
1241pub fn dns_authority_dnssec_failed_data_v1(
1253 payload: &DnsAuthorityDnssecFailed,
1254) -> Result<Value, serde_json::Error> {
1255 serde_json::to_value(payload)
1256}
1257
1258pub fn cloud_event_v1_dns_authority_dnssec_failed(
1270 source: &str,
1271 time: &str,
1272 payload: &DnsAuthorityDnssecFailed,
1273) -> Result<CloudEventV1, serde_json::Error> {
1274 Ok(CloudEventV1 {
1275 specversion: "1.0".into(),
1276 id: uuid::Uuid::new_v4().to_string(),
1277 source: source.to_string(),
1278 ty: "dev.cellos.events.cell.observability.v1.dns_authority_dnssec_failed".into(),
1279 datacontenttype: Some("application/json".into()),
1280 data: Some(dns_authority_dnssec_failed_data_v1(payload)?),
1281 time: Some(time.to_string()),
1282 traceparent: None,
1283 cex: None,
1284 })
1285}
1286
1287pub fn dns_query_data_v1(event: &DnsQueryEvent) -> Result<Value, serde_json::Error> {
1296 serde_json::to_value(event)
1297}
1298
1299pub fn cloud_event_v1_dns_query(
1310 source: &str,
1311 time: &str,
1312 event: &DnsQueryEvent,
1313) -> Result<CloudEventV1, serde_json::Error> {
1314 Ok(CloudEventV1 {
1315 specversion: "1.0".into(),
1316 id: uuid::Uuid::new_v4().to_string(),
1317 source: source.to_string(),
1318 ty: "dev.cellos.events.cell.observability.v1.dns_query".into(),
1319 datacontenttype: Some("application/json".into()),
1320 data: Some(dns_query_data_v1(event)?),
1321 time: Some(time.to_string()),
1322 traceparent: None,
1323 cex: None,
1324 })
1325}
1326
1327#[must_use]
1343pub fn dns_query_permitted_data_v1(
1344 qname: &str,
1345 qtype: &str,
1346 cell_id: &str,
1347 resolver: &str,
1348) -> Value {
1349 json!({
1350 "schemaVersion": "1.0.0",
1351 "queryName": qname,
1352 "queryType": qtype,
1353 "cellId": cell_id,
1354 "resolver": resolver,
1355 })
1356}
1357
1358pub fn cloud_event_v1_dns_query_permitted(
1360 source: &str,
1361 time: &str,
1362 qname: &str,
1363 qtype: &str,
1364 cell_id: &str,
1365 resolver: &str,
1366) -> CloudEventV1 {
1367 CloudEventV1 {
1368 specversion: "1.0".into(),
1369 id: uuid::Uuid::new_v4().to_string(),
1370 source: source.to_string(),
1371 ty: "dev.cellos.events.cell.dns.v1.query_permitted".into(),
1372 datacontenttype: Some("application/json".into()),
1373 data: Some(dns_query_permitted_data_v1(qname, qtype, cell_id, resolver)),
1374 time: Some(time.to_string()),
1375 traceparent: None,
1376 cex: None,
1377 }
1378}
1379
1380#[must_use]
1394pub fn dns_query_refused_data_v1(qname: &str, qtype: &str, cell_id: &str, reason: &str) -> Value {
1395 json!({
1396 "schemaVersion": "1.0.0",
1397 "queryName": qname,
1398 "queryType": qtype,
1399 "cellId": cell_id,
1400 "reason": reason,
1401 })
1402}
1403
1404pub fn cloud_event_v1_dns_query_refused(
1406 source: &str,
1407 time: &str,
1408 qname: &str,
1409 qtype: &str,
1410 cell_id: &str,
1411 reason: &str,
1412) -> CloudEventV1 {
1413 CloudEventV1 {
1414 specversion: "1.0".into(),
1415 id: uuid::Uuid::new_v4().to_string(),
1416 source: source.to_string(),
1417 ty: "dev.cellos.events.cell.dns.v1.query_refused".into(),
1418 datacontenttype: Some("application/json".into()),
1419 data: Some(dns_query_refused_data_v1(qname, qtype, cell_id, reason)),
1420 time: Some(time.to_string()),
1421 traceparent: None,
1422 cex: None,
1423 }
1424}
1425
1426pub fn keyset_verified_data_v1(
1439 keyset_id: &str,
1440 payload_digest: &str,
1441 verified_signer_kid: &str,
1442 verified_at: &str,
1443 correlation_id: Option<&str>,
1444) -> Result<Value, serde_json::Error> {
1445 let mut m = Map::new();
1446 m.insert("schemaVersion".to_string(), json!("1.0.0"));
1447 m.insert("keysetId".to_string(), json!(keyset_id));
1448 m.insert("payloadDigest".to_string(), json!(payload_digest));
1449 m.insert("verifiedSignerKid".to_string(), json!(verified_signer_kid));
1450 m.insert("verifiedAt".to_string(), json!(verified_at));
1451 if let Some(cid) = correlation_id {
1452 m.insert("correlationId".to_string(), json!(cid));
1453 }
1454 Ok(Value::Object(m))
1455}
1456
1457pub fn cloud_event_v1_keyset_verified(
1463 source: &str,
1464 time: &str,
1465 keyset_id: &str,
1466 payload_digest: &str,
1467 verified_signer_kid: &str,
1468 verified_at: &str,
1469 correlation_id: Option<&str>,
1470) -> Result<CloudEventV1, serde_json::Error> {
1471 Ok(CloudEventV1 {
1472 specversion: "1.0".into(),
1473 id: uuid::Uuid::new_v4().to_string(),
1474 source: source.to_string(),
1475 ty: "dev.cellos.events.cell.trust.v1.keyset_verified".into(),
1476 datacontenttype: Some("application/json".into()),
1477 data: Some(keyset_verified_data_v1(
1478 keyset_id,
1479 payload_digest,
1480 verified_signer_kid,
1481 verified_at,
1482 correlation_id,
1483 )?),
1484 time: Some(time.to_string()),
1485 traceparent: None,
1486 cex: None,
1487 })
1488}
1489
1490pub fn keyset_verification_failed_data_v1(
1506 attempted_keyset_path: &str,
1507 reason: &str,
1508 failed_at: &str,
1509 correlation_id: Option<&str>,
1510) -> Result<Value, serde_json::Error> {
1511 let mut m = Map::new();
1512 m.insert("schemaVersion".to_string(), json!("1.0.0"));
1513 m.insert(
1514 "attemptedKeysetPath".to_string(),
1515 json!(attempted_keyset_path),
1516 );
1517 m.insert("reason".to_string(), json!(reason));
1518 m.insert("failedAt".to_string(), json!(failed_at));
1519 if let Some(cid) = correlation_id {
1520 m.insert("correlationId".to_string(), json!(cid));
1521 }
1522 Ok(Value::Object(m))
1523}
1524
1525pub fn cloud_event_v1_keyset_verification_failed(
1527 source: &str,
1528 time: &str,
1529 attempted_keyset_path: &str,
1530 reason: &str,
1531 failed_at: &str,
1532 correlation_id: Option<&str>,
1533) -> Result<CloudEventV1, serde_json::Error> {
1534 Ok(CloudEventV1 {
1535 specversion: "1.0".into(),
1536 id: uuid::Uuid::new_v4().to_string(),
1537 source: source.to_string(),
1538 ty: "dev.cellos.events.cell.trust.v1.keyset_verification_failed".into(),
1539 datacontenttype: Some("application/json".into()),
1540 data: Some(keyset_verification_failed_data_v1(
1541 attempted_keyset_path,
1542 reason,
1543 failed_at,
1544 correlation_id,
1545 )?),
1546 time: Some(time.to_string()),
1547 traceparent: None,
1548 cex: None,
1549 })
1550}
1551
1552pub fn network_flow_decision_data_v1(
1566 decision: &NetworkFlowDecision,
1567) -> Result<Value, serde_json::Error> {
1568 serde_json::to_value(decision)
1569}
1570
1571pub fn cloud_event_v1_network_flow_decision(
1582 source: &str,
1583 time: &str,
1584 decision: &NetworkFlowDecision,
1585) -> Result<CloudEventV1, serde_json::Error> {
1586 Ok(CloudEventV1 {
1587 specversion: "1.0".into(),
1588 id: uuid::Uuid::new_v4().to_string(),
1589 source: source.to_string(),
1590 ty: "dev.cellos.events.cell.observability.v1.network_flow_decision".into(),
1591 datacontenttype: Some("application/json".into()),
1592 data: Some(network_flow_decision_data_v1(decision)?),
1593 time: Some(time.to_string()),
1594 traceparent: None,
1595 cex: None,
1596 })
1597}
1598
1599#[allow(clippy::too_many_arguments)]
1603pub fn observability_l7_egress_decision_data_v1(
1604 spec: &ExecutionCellSpec,
1605 cell_id: &str,
1606 run_id: Option<&str>,
1607 decision_id: &str,
1608 action: &str,
1609 sni_host: &str,
1610 policy_digest: &str,
1611 keyset_id: &str,
1612 issuer_kid: &str,
1613 reason_code: &str,
1614 rule_ref: Option<&str>,
1615 stream_id: Option<u32>,
1620) -> Result<Value, serde_json::Error> {
1621 let mut m = Map::new();
1622 m.insert("cellId".to_string(), json!(cell_id));
1623 m.insert("specId".to_string(), json!(&spec.id));
1624 if let Some(r) = run_id {
1625 m.insert("runId".to_string(), json!(r));
1626 }
1627 m.insert("decisionId".to_string(), json!(decision_id));
1628 m.insert("action".to_string(), json!(action));
1629 m.insert("sniHost".to_string(), json!(sni_host));
1630 m.insert("policyDigest".to_string(), json!(policy_digest));
1631 m.insert("keysetId".to_string(), json!(keyset_id));
1632 m.insert("issuerKid".to_string(), json!(issuer_kid));
1633 m.insert("reasonCode".to_string(), json!(reason_code));
1634 if let Some(rr) = rule_ref {
1635 m.insert("ruleRef".to_string(), json!(rr));
1636 }
1637 if let Some(sid) = stream_id {
1638 m.insert("streamId".to_string(), json!(sid));
1639 }
1640 if let Some(c) = &spec.correlation {
1641 m.insert("correlation".to_string(), serde_json::to_value(c)?);
1642 }
1643 Ok(Value::Object(m))
1644}
1645
1646pub fn observability_container_security_data_v1(
1665 cell_id: &str,
1666 run_id: Option<&str>,
1667 cap_eff: Option<&str>,
1668 cap_prm: Option<&str>,
1669 cap_bnd: Option<&str>,
1670 cap_amb: Option<&str>,
1671 cap_inh: Option<&str>,
1672) -> Value {
1673 let mut m = Map::new();
1674 m.insert("cellId".to_string(), json!(cell_id));
1675 if let Some(r) = run_id {
1676 m.insert("runId".to_string(), json!(r));
1677 }
1678 if let (Some(eff), Some(prm), Some(bnd), Some(amb), Some(inh)) =
1679 (cap_eff, cap_prm, cap_bnd, cap_amb, cap_inh)
1680 {
1681 m.insert("capEff".to_string(), json!(eff));
1682 m.insert("capPrm".to_string(), json!(prm));
1683 m.insert("capBnd".to_string(), json!(bnd));
1684 m.insert("capAmb".to_string(), json!(amb));
1685 m.insert("capInh".to_string(), json!(inh));
1686 let privileged = eff == "0000001fffffffff";
1688 m.insert("privileged".to_string(), json!(privileged));
1689 }
1690 Value::Object(m)
1691}
1692
1693pub fn compliance_summary_data_v1(
1712 spec: &ExecutionCellSpec,
1713 cell_id: &str,
1714 run_id: Option<&str>,
1715 command_exit_code: Option<i32>,
1716) -> Result<Value, serde_json::Error> {
1717 compliance_summary_data_v1_with_subjects(spec, cell_id, run_id, command_exit_code, &[])
1718}
1719
1720pub fn compliance_summary_data_v1_with_subjects(
1736 spec: &ExecutionCellSpec,
1737 cell_id: &str,
1738 run_id: Option<&str>,
1739 command_exit_code: Option<i32>,
1740 subject_urns: &[SubjectUrn],
1741) -> Result<Value, serde_json::Error> {
1742 let egress_rule_count = spec
1743 .authority
1744 .egress_rules
1745 .as_ref()
1746 .map(|v| v.len())
1747 .unwrap_or(0);
1748 let export_target_count = spec
1749 .export
1750 .as_ref()
1751 .and_then(|e| e.targets.as_ref())
1752 .map(|t| t.len())
1753 .unwrap_or(0);
1754 let resource_limits_present = spec.run.as_ref().and_then(|r| r.limits.as_ref()).is_some();
1755 let secret_delivery_mode = spec
1756 .run
1757 .as_ref()
1758 .map(|r| serde_json::to_value(&r.secret_delivery))
1759 .transpose()?
1760 .unwrap_or(serde_json::Value::String("env".into()));
1761
1762 let mut m = Map::new();
1763 m.insert("cellId".to_string(), json!(cell_id));
1764 m.insert("specId".to_string(), json!(&spec.id));
1765 m.insert(
1766 "lifetimeTtlSeconds".to_string(),
1767 json!(spec.lifetime.ttl_seconds),
1768 );
1769 m.insert("egressRuleCount".to_string(), json!(egress_rule_count));
1770 m.insert(
1771 "resourceLimitsPresent".to_string(),
1772 json!(resource_limits_present),
1773 );
1774 m.insert("secretDeliveryMode".to_string(), secret_delivery_mode);
1775 m.insert("exportTargetCount".to_string(), json!(export_target_count));
1776
1777 if let Some(policy) = &spec.policy {
1779 if let Some(id) = &policy.pack_id {
1780 m.insert("policyPackId".to_string(), json!(id));
1781 }
1782 if let Some(ver) = &policy.pack_version {
1783 m.insert("policyPackVersion".to_string(), json!(ver));
1784 }
1785 if let Some(digest) = &policy.bundle_digest {
1786 m.insert("policyBundleDigest".to_string(), json!(digest));
1787 }
1788 }
1789
1790 if let Some(placement) = &spec.placement {
1791 let mut placement_map = Map::new();
1792 if let Some(pool_id) = &placement.pool_id {
1793 placement_map.insert("poolId".to_string(), json!(pool_id));
1794 }
1795 if let Some(namespace) = &placement.kubernetes_namespace {
1796 placement_map.insert("kubernetesNamespace".to_string(), json!(namespace));
1797 }
1798 if let Some(queue_name) = &placement.queue_name {
1799 placement_map.insert("queueName".to_string(), json!(queue_name));
1800 }
1801 if !placement_map.is_empty() {
1802 m.insert("placement".to_string(), Value::Object(placement_map));
1803 }
1804 }
1805
1806 if let Some(code) = command_exit_code {
1807 m.insert("commandExitCode".to_string(), json!(code));
1808 }
1809 if let Some(r) = run_id {
1810 m.insert("runId".to_string(), json!(r));
1811 }
1812 if let Some(c) = &spec.correlation {
1813 m.insert("correlation".to_string(), serde_json::to_value(c)?);
1814 }
1815
1816 if !subject_urns.is_empty() {
1820 let urns: Vec<Value> = subject_urns.iter().map(|u| json!(u)).collect();
1821 m.insert("subjectUrns".to_string(), Value::Array(urns));
1822 }
1823
1824 Ok(Value::Object(m))
1825}
1826
1827pub fn policy_rejected_data_v1(
1835 spec: &ExecutionCellSpec,
1836 violations: &[PolicyViolation],
1837) -> Result<Value, serde_json::Error> {
1838 let violation_values: Vec<Value> = violations
1839 .iter()
1840 .map(|v| {
1841 json!({
1842 "rule": v.rule,
1843 "message": v.message,
1844 })
1845 })
1846 .collect();
1847
1848 let mut m = Map::new();
1849 m.insert("specId".to_string(), json!(&spec.id));
1850 m.insert("violationCount".to_string(), json!(violations.len()));
1851 m.insert("violations".to_string(), Value::Array(violation_values));
1852
1853 if let Some(policy) = &spec.policy {
1855 if let Some(id) = &policy.pack_id {
1856 m.insert("policyPackId".to_string(), json!(id));
1857 }
1858 if let Some(ver) = &policy.pack_version {
1859 m.insert("policyPackVersion".to_string(), json!(ver));
1860 }
1861 }
1862
1863 if let Some(c) = &spec.correlation {
1864 m.insert("correlation".to_string(), serde_json::to_value(c)?);
1865 }
1866 Ok(Value::Object(m))
1867}
1868
1869pub const ADMISSION_ALLOWED_TYPE: &str = "dev.cellos.events.cell.admission.v1.allowed";
1876
1877pub const ADMISSION_REJECTED_TYPE: &str = "dev.cellos.events.cell.admission.v1.rejected";
1884
1885pub const AUDIT_RECONCILED_TYPE: &str = "dev.cellos.events.cell.audit.v1.reconciled";
1890
1891#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1906#[serde(rename_all = "snake_case")]
1907pub enum AdmissionDecisionReason {
1908 AuthorityExceedsCeiling,
1910 CeilingManifestUnverified,
1912 CeilingManifestUnparseable,
1914 CeilingManifestWrongPayloadType,
1916 CeilingDimensionUncomparable,
1919 CeilingEpochStale,
1921 CeilingDegradedUnconfigured,
1924 FormationEdgeNotNarrowing,
1926 AdmittedWithinCeiling,
1928}
1929
1930#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1939#[serde(rename_all = "kebab-case")]
1940pub enum EnforcementBinding {
1941 LinuxNftables,
1943 DeclaredAudit,
1946}
1947
1948#[allow(clippy::too_many_arguments)]
1969pub fn admission_decision_data_v1(
1970 spec: &ExecutionCellSpec,
1971 admitted: bool,
1972 reason: AdmissionDecisionReason,
1973 predicate: &str,
1974 spec_hash: &str,
1975 run_id: Option<&str>,
1976 enforcement_binding: EnforcementBinding,
1977 context: &[(&str, &str)],
1978) -> Result<Value, serde_json::Error> {
1979 let mut m = Map::new();
1980 m.insert("specId".to_string(), json!(&spec.id));
1981 m.insert("admitted".to_string(), json!(admitted));
1982 m.insert("reason".to_string(), serde_json::to_value(reason)?);
1983 m.insert("predicate".to_string(), json!(predicate));
1984 m.insert("specHash".to_string(), json!(spec_hash));
1985 m.insert(
1986 "enforcementBinding".to_string(),
1987 serde_json::to_value(enforcement_binding)?,
1988 );
1989
1990 let pid = crate::crypto::provider_identity();
1995 m.insert(
1996 "providerIdentity".to_string(),
1997 json!({
1998 "name": pid.name,
1999 "fipsMode": pid.fips_mode,
2000 "moduleCert": pid.module_cert,
2001 }),
2002 );
2003
2004 if let Some(r) = run_id {
2005 m.insert("runId".to_string(), json!(r));
2006 }
2007
2008 if !context.is_empty() {
2011 let mut ctx = Map::new();
2012 for (k, v) in context {
2013 ctx.insert((*k).to_string(), json!(v));
2014 }
2015 m.insert("context".to_string(), Value::Object(ctx));
2016 }
2017
2018 if let Some(c) = &spec.correlation {
2019 m.insert("correlation".to_string(), serde_json::to_value(c)?);
2020 }
2021 Ok(Value::Object(m))
2022}
2023
2024#[allow(clippy::too_many_arguments)]
2031pub fn audit_reconciled_data_v1(
2032 chain_id: &str,
2033 from_seq: u64,
2034 to_seq: u64,
2035 head_row_hash: &str,
2036 delivered_count: u64,
2037 broker_acked_head: Option<&str>,
2038 integrity: &str,
2039 signed: bool,
2040) -> Result<Value, serde_json::Error> {
2041 let mut m = Map::new();
2042 m.insert("chainId".to_string(), json!(chain_id));
2043 m.insert("fromSeq".to_string(), json!(from_seq));
2044 m.insert("toSeq".to_string(), json!(to_seq));
2045 m.insert("headRowHash".to_string(), json!(head_row_hash));
2046 m.insert("deliveredCount".to_string(), json!(delivered_count));
2047 m.insert("integrity".to_string(), json!(integrity));
2048 m.insert("signed".to_string(), json!(signed));
2049 if let Some(h) = broker_acked_head {
2050 m.insert("brokerAckedHead".to_string(), json!(h));
2051 }
2052 Ok(Value::Object(m))
2053}
2054
2055pub fn authz_rejected_data_v1(
2068 spec: &ExecutionCellSpec,
2069 reason: &str,
2070 message: &str,
2071 denied_pool_id: Option<&str>,
2072 denied_policy_pack_id: Option<&str>,
2073) -> Result<Value, serde_json::Error> {
2074 let mut m = Map::new();
2075 m.insert("specId".to_string(), json!(&spec.id));
2076 m.insert("reason".to_string(), json!(reason));
2077 m.insert("message".to_string(), json!(message));
2078
2079 let tenant_id = spec
2080 .correlation
2081 .as_ref()
2082 .and_then(|c| c.tenant_id.as_deref());
2083 if let Some(t) = tenant_id {
2084 m.insert("tenantId".to_string(), json!(t));
2085 m.insert("subject".to_string(), json!(t));
2090 }
2091
2092 if let Some(p) = denied_pool_id {
2093 m.insert("deniedPoolId".to_string(), json!(p));
2094 }
2095 if let Some(p) = denied_policy_pack_id {
2096 m.insert("deniedPolicyPackId".to_string(), json!(p));
2097 }
2098
2099 if let Some(c) = &spec.correlation {
2100 m.insert("correlation".to_string(), serde_json::to_value(c)?);
2101 }
2102 Ok(Value::Object(m))
2103}
2104
2105pub fn homeostasis_signal_data_v1(
2112 spec: &ExecutionCellSpec,
2113 cell_id: &str,
2114 run_id: Option<&str>,
2115 signal: &crate::HomeostasisSignal,
2116) -> Result<Value, serde_json::Error> {
2117 let mut m = Map::new();
2118 m.insert("cellId".to_string(), json!(cell_id));
2119 m.insert("specId".to_string(), json!(&spec.id));
2120 m.insert("specHash".to_string(), json!(&signal.spec_hash));
2121 m.insert(
2122 "declaredEgressRules".to_string(),
2123 json!(signal.declared_egress_rules),
2124 );
2125 m.insert(
2130 "exercisedEgressConnections".to_string(),
2131 match signal.exercised_egress_connections {
2132 Some(n) => json!(n),
2133 None => Value::Null,
2134 },
2135 );
2136 if let Some(reason) = &signal.exercised_egress_reason {
2137 m.insert("exercisedEgressReason".to_string(), json!(reason));
2138 }
2139 m.insert(
2140 "declaredMountPaths".to_string(),
2141 json!(signal.declared_mount_paths),
2142 );
2143 m.insert(
2144 "accessedMountPaths".to_string(),
2145 json!(signal.accessed_mount_paths),
2146 );
2147 m.insert(
2148 "declaredSecretCount".to_string(),
2149 json!(signal.declared_secret_count),
2150 );
2151 m.insert(
2152 "authorityEfficiency".to_string(),
2153 json!(signal.authority_efficiency),
2154 );
2155 m.insert(
2156 "recommendedRemovals".to_string(),
2157 serde_json::to_value(&signal.recommended_removals)?,
2158 );
2159 if let Some(r) = run_id {
2160 m.insert("runId".to_string(), json!(r));
2161 }
2162 if let Some(c) = &spec.correlation {
2163 m.insert("correlation".to_string(), serde_json::to_value(c)?);
2164 }
2165 Ok(Value::Object(m))
2166}
2167
2168pub fn homeostasis_violation_data_v1(
2183 cell_id: &str,
2184 declared_egress: u64,
2185 exercised_egress: u64,
2186 spec_hash: &str,
2187) -> Value {
2188 let mut m = Map::new();
2189 m.insert("cellId".to_string(), json!(cell_id));
2190 m.insert(
2191 "declaredEgressRuleCount".to_string(),
2192 json!(declared_egress),
2193 );
2194 m.insert(
2195 "exercisedEgressConnections".to_string(),
2196 json!(exercised_egress),
2197 );
2198 m.insert(
2201 "overage".to_string(),
2202 json!(exercised_egress.saturating_sub(declared_egress)),
2203 );
2204 m.insert("specHash".to_string(), json!(spec_hash));
2205 m.insert("severity".to_string(), json!("critical"));
2206 Value::Object(m)
2207}
2208
2209#[allow(clippy::too_many_arguments)]
2234pub fn observability_host_fc_metrics_data_v1(
2235 spec: &ExecutionCellSpec,
2236 cell_id: &str,
2237 run_id: Option<&str>,
2238 spec_signature_hash: &str,
2239 sampled_at_unix_ms: u64,
2240 fc_socket_path: &str,
2241 vcpu_exits_total: Option<u64>,
2242 vsock_tx_bytes: Option<u64>,
2243 vsock_rx_bytes: Option<u64>,
2244 block_read_ops: Option<u64>,
2245 block_write_ops: Option<u64>,
2246 sample_error: Option<&str>,
2247) -> Result<Value, serde_json::Error> {
2248 let mut m = Map::new();
2249 m.insert("cellId".to_string(), json!(cell_id));
2250 m.insert("specId".to_string(), json!(&spec.id));
2251 m.insert("specSignatureHash".to_string(), json!(spec_signature_hash));
2252 m.insert("sampledAtUnixMs".to_string(), json!(sampled_at_unix_ms));
2253 m.insert("fcSocketPath".to_string(), json!(fc_socket_path));
2254 if let Some(v) = vcpu_exits_total {
2255 m.insert("vcpuExitsTotal".to_string(), json!(v));
2256 }
2257 if let Some(v) = vsock_tx_bytes {
2258 m.insert("vsockTxBytes".to_string(), json!(v));
2259 }
2260 if let Some(v) = vsock_rx_bytes {
2261 m.insert("vsockRxBytes".to_string(), json!(v));
2262 }
2263 if let Some(v) = block_read_ops {
2264 m.insert("blockReadOps".to_string(), json!(v));
2265 }
2266 if let Some(v) = block_write_ops {
2267 m.insert("blockWriteOps".to_string(), json!(v));
2268 }
2269 if let Some(e) = sample_error {
2270 m.insert("sampleError".to_string(), json!(e));
2271 }
2272 if let Some(r) = run_id {
2273 m.insert("runId".to_string(), json!(r));
2274 }
2275 if let Some(c) = &spec.correlation {
2276 m.insert("correlation".to_string(), serde_json::to_value(c)?);
2277 }
2278 Ok(Value::Object(m))
2279}
2280
2281#[derive(Debug, Default, Clone)]
2284pub struct CgroupSample<'a> {
2285 pub memory_events: Option<&'a [(&'a str, u64)]>,
2286 pub cpu_stat: Option<&'a [(&'a str, u64)]>,
2287 pub pids_events: Option<&'a [(&'a str, u64)]>,
2288}
2289
2290fn cgroup_section(keys: &[&str], pairs: Option<&[(&str, u64)]>) -> Option<Value> {
2302 let pairs = pairs?;
2303 let mut section = Map::new();
2304 for (k, v) in pairs {
2305 if keys.contains(k) {
2306 section.insert((*k).to_string(), json!(v));
2307 }
2308 }
2309 if section.is_empty() {
2310 None
2311 } else {
2312 Some(Value::Object(section))
2313 }
2314}
2315
2316#[allow(clippy::too_many_arguments)]
2318pub fn observability_host_cgroup_data_v1(
2319 spec: &ExecutionCellSpec,
2320 cell_id: &str,
2321 run_id: Option<&str>,
2322 spec_signature_hash: &str,
2323 sampled_at_unix_ms: u64,
2324 cgroup_path: &str,
2325 sample: &CgroupSample<'_>,
2326 sample_error: Option<&str>,
2327) -> Result<Value, serde_json::Error> {
2328 const MEM_KEYS: &[&str] = &["low", "high", "max", "oom", "oomKill"];
2329 const CPU_KEYS: &[&str] = &[
2330 "usageUsec",
2331 "userUsec",
2332 "systemUsec",
2333 "nrPeriods",
2334 "nrThrottled",
2335 "throttledUsec",
2336 ];
2337 const PIDS_KEYS: &[&str] = &["max"];
2338
2339 let mut m = Map::new();
2340 m.insert("cellId".to_string(), json!(cell_id));
2341 m.insert("specId".to_string(), json!(&spec.id));
2342 m.insert("specSignatureHash".to_string(), json!(spec_signature_hash));
2343 m.insert("sampledAtUnixMs".to_string(), json!(sampled_at_unix_ms));
2344 m.insert("cgroupPath".to_string(), json!(cgroup_path));
2345 if let Some(v) = cgroup_section(MEM_KEYS, sample.memory_events) {
2346 m.insert("memoryEvents".to_string(), v);
2347 }
2348 if let Some(v) = cgroup_section(CPU_KEYS, sample.cpu_stat) {
2349 m.insert("cpuStat".to_string(), v);
2350 }
2351 if let Some(v) = cgroup_section(PIDS_KEYS, sample.pids_events) {
2352 m.insert("pidsEvents".to_string(), v);
2353 }
2354 if let Some(e) = sample_error {
2355 m.insert("sampleError".to_string(), json!(e));
2356 }
2357 if let Some(r) = run_id {
2358 m.insert("runId".to_string(), json!(r));
2359 }
2360 if let Some(c) = &spec.correlation {
2361 m.insert("correlation".to_string(), serde_json::to_value(c)?);
2362 }
2363 Ok(Value::Object(m))
2364}
2365
2366#[derive(Debug, Clone)]
2368pub struct NftRuleCounter<'a> {
2369 pub rule_handle: &'a str,
2370 pub verdict: Option<&'a str>,
2371 pub packets: u64,
2372 pub bytes: u64,
2373 pub r#match: Option<&'a str>,
2374}
2375
2376#[allow(clippy::too_many_arguments)]
2383pub fn observability_host_nftables_data_v1(
2384 spec: &ExecutionCellSpec,
2385 cell_id: &str,
2386 run_id: Option<&str>,
2387 spec_signature_hash: &str,
2388 sampled_at_unix_ms: u64,
2389 table_name: &str,
2390 rule_counters: &[NftRuleCounter<'_>],
2391 sample_error: Option<&str>,
2392) -> Result<Value, serde_json::Error> {
2393 let mut m = Map::new();
2394 m.insert("cellId".to_string(), json!(cell_id));
2395 m.insert("specId".to_string(), json!(&spec.id));
2396 m.insert("specSignatureHash".to_string(), json!(spec_signature_hash));
2397 m.insert("sampledAtUnixMs".to_string(), json!(sampled_at_unix_ms));
2398 m.insert("tableName".to_string(), json!(table_name));
2399 let counters: Vec<Value> = rule_counters
2400 .iter()
2401 .map(|c| {
2402 let mut row = Map::new();
2403 row.insert("ruleHandle".to_string(), json!(c.rule_handle));
2404 if let Some(v) = c.verdict {
2405 row.insert("verdict".to_string(), json!(v));
2406 }
2407 row.insert("packets".to_string(), json!(c.packets));
2408 row.insert("bytes".to_string(), json!(c.bytes));
2409 if let Some(mt) = c.r#match {
2410 row.insert("match".to_string(), json!(mt));
2411 }
2412 Value::Object(row)
2413 })
2414 .collect();
2415 m.insert("ruleCounters".to_string(), Value::Array(counters));
2416 if let Some(e) = sample_error {
2417 m.insert("sampleError".to_string(), json!(e));
2418 }
2419 if let Some(r) = run_id {
2420 m.insert("runId".to_string(), json!(r));
2421 }
2422 if let Some(c) = &spec.correlation {
2423 m.insert("correlation".to_string(), serde_json::to_value(c)?);
2424 }
2425 Ok(Value::Object(m))
2426}
2427
2428#[derive(Debug, Default, Clone, Copy)]
2430pub struct TapStats {
2431 pub rx_packets: Option<u64>,
2432 pub tx_packets: Option<u64>,
2433 pub rx_bytes: Option<u64>,
2434 pub tx_bytes: Option<u64>,
2435 pub rx_errors: Option<u64>,
2436 pub tx_errors: Option<u64>,
2437 pub rx_dropped: Option<u64>,
2438 pub tx_dropped: Option<u64>,
2439}
2440
2441#[allow(clippy::too_many_arguments)]
2448pub fn observability_host_tap_data_v1(
2449 spec: &ExecutionCellSpec,
2450 cell_id: &str,
2451 run_id: Option<&str>,
2452 spec_signature_hash: &str,
2453 sampled_at_unix_ms: u64,
2454 tap_name: &str,
2455 link_state: &str,
2456 stats: &TapStats,
2457 sample_error: Option<&str>,
2458) -> Result<Value, serde_json::Error> {
2459 let mut m = Map::new();
2460 m.insert("cellId".to_string(), json!(cell_id));
2461 m.insert("specId".to_string(), json!(&spec.id));
2462 m.insert("specSignatureHash".to_string(), json!(spec_signature_hash));
2463 m.insert("sampledAtUnixMs".to_string(), json!(sampled_at_unix_ms));
2464 m.insert("tapName".to_string(), json!(tap_name));
2465 m.insert("linkState".to_string(), json!(link_state));
2466 if let Some(v) = stats.rx_packets {
2467 m.insert("rxPackets".to_string(), json!(v));
2468 }
2469 if let Some(v) = stats.tx_packets {
2470 m.insert("txPackets".to_string(), json!(v));
2471 }
2472 if let Some(v) = stats.rx_bytes {
2473 m.insert("rxBytes".to_string(), json!(v));
2474 }
2475 if let Some(v) = stats.tx_bytes {
2476 m.insert("txBytes".to_string(), json!(v));
2477 }
2478 if let Some(v) = stats.rx_errors {
2479 m.insert("rxErrors".to_string(), json!(v));
2480 }
2481 if let Some(v) = stats.tx_errors {
2482 m.insert("txErrors".to_string(), json!(v));
2483 }
2484 if let Some(v) = stats.rx_dropped {
2485 m.insert("rxDropped".to_string(), json!(v));
2486 }
2487 if let Some(v) = stats.tx_dropped {
2488 m.insert("txDropped".to_string(), json!(v));
2489 }
2490 if let Some(e) = sample_error {
2491 m.insert("sampleError".to_string(), json!(e));
2492 }
2493 if let Some(r) = run_id {
2494 m.insert("runId".to_string(), json!(r));
2495 }
2496 if let Some(c) = &spec.correlation {
2497 m.insert("correlation".to_string(), serde_json::to_value(c)?);
2498 }
2499 Ok(Value::Object(m))
2500}
2501
2502#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2505#[serde(rename_all = "snake_case")]
2506pub enum ResidueClass {
2507 None,
2509 DocumentedException,
2512}
2513
2514#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2520#[serde(rename_all = "camelCase")]
2521pub enum LifecycleResidueClass {
2522 None,
2523 MetadataOnly,
2524 DocumentedException,
2525 Unknown,
2526}
2527
2528#[derive(Debug, Default, Clone)]
2537pub struct EvidenceBundleRefs<'a> {
2538 pub started_event_ref: &'a str,
2540 pub cell_destroyed_event_ref: &'a str,
2542 pub command_completed_event_ref: Option<&'a str>,
2543 pub spawned_event_refs: &'a [&'a str],
2544 pub fc_metrics_event_refs: &'a [&'a str],
2545 pub cgroup_event_refs: &'a [&'a str],
2546 pub nftables_event_refs: &'a [&'a str],
2547 pub tap_event_refs: &'a [&'a str],
2548 pub homeostasis_event_ref: Option<&'a str>,
2549 pub compliance_summary_event_ref: Option<&'a str>,
2550 pub guest_event_refs: &'a [(&'a str, &'a str, &'a str)],
2553 pub residue_exception: Option<&'a str>,
2555}
2556
2557pub fn evidence_bundle_emitted_data_v1(
2572 spec: &ExecutionCellSpec,
2573 cell_id: &str,
2574 run_id: Option<&str>,
2575 spec_signature_hash: &str,
2576 emitted_at_unix_ms: u64,
2577 residue_class: ResidueClass,
2578 refs: &EvidenceBundleRefs<'_>,
2579) -> Result<Value, serde_json::Error> {
2580 let mut m = Map::new();
2581 m.insert("cellId".to_string(), json!(cell_id));
2582 m.insert("specId".to_string(), json!(&spec.id));
2583 m.insert("specSignatureHash".to_string(), json!(spec_signature_hash));
2584 m.insert("emittedAtUnixMs".to_string(), json!(emitted_at_unix_ms));
2585
2586 let mut lifecycle = Map::new();
2587 lifecycle.insert("started".to_string(), json!(refs.started_event_ref));
2588 lifecycle.insert(
2589 "destroyed".to_string(),
2590 json!(refs.cell_destroyed_event_ref),
2591 );
2592 if let Some(cc) = refs.command_completed_event_ref {
2593 lifecycle.insert("commandCompleted".to_string(), json!(cc));
2594 }
2595 if !refs.spawned_event_refs.is_empty() {
2596 lifecycle.insert("spawned".to_string(), json!(refs.spawned_event_refs));
2597 }
2598 m.insert("lifecycleEventRefs".to_string(), Value::Object(lifecycle));
2599
2600 m.insert(
2601 "cellDestroyedEventRef".to_string(),
2602 json!(refs.cell_destroyed_event_ref),
2603 );
2604 m.insert(
2605 "residueClass".to_string(),
2606 serde_json::to_value(residue_class)?,
2607 );
2608 if let Some(rx) = refs.residue_exception {
2609 m.insert("residueException".to_string(), json!(rx));
2610 }
2611
2612 let any_host = !refs.fc_metrics_event_refs.is_empty()
2613 || !refs.cgroup_event_refs.is_empty()
2614 || !refs.nftables_event_refs.is_empty()
2615 || !refs.tap_event_refs.is_empty();
2616 if any_host {
2617 let mut host = Map::new();
2618 if !refs.fc_metrics_event_refs.is_empty() {
2619 host.insert("fcMetrics".to_string(), json!(refs.fc_metrics_event_refs));
2620 }
2621 if !refs.cgroup_event_refs.is_empty() {
2622 host.insert("cgroup".to_string(), json!(refs.cgroup_event_refs));
2623 }
2624 if !refs.nftables_event_refs.is_empty() {
2625 host.insert("nftables".to_string(), json!(refs.nftables_event_refs));
2626 }
2627 if !refs.tap_event_refs.is_empty() {
2628 host.insert("tap".to_string(), json!(refs.tap_event_refs));
2629 }
2630 m.insert("hostProbeEventRefs".to_string(), Value::Object(host));
2631 }
2632
2633 if !refs.guest_event_refs.is_empty() {
2634 let rows: Vec<Value> = refs
2635 .guest_event_refs
2636 .iter()
2637 .map(|(id, ty, rc)| {
2638 let mut row = Map::new();
2639 row.insert("eventId".to_string(), json!(id));
2640 row.insert("eventType".to_string(), json!(ty));
2641 row.insert("ruleClass".to_string(), json!(rc));
2642 Value::Object(row)
2643 })
2644 .collect();
2645 m.insert("guestEventRefs".to_string(), Value::Array(rows));
2646 }
2647
2648 if let Some(h) = refs.homeostasis_event_ref {
2649 m.insert("homeostasisEventRef".to_string(), json!(h));
2650 }
2651 if let Some(c) = refs.compliance_summary_event_ref {
2652 m.insert("complianceSummaryEventRef".to_string(), json!(c));
2653 }
2654 if let Some(r) = run_id {
2655 m.insert("runId".to_string(), json!(r));
2656 }
2657 if let Some(c) = &spec.correlation {
2658 m.insert("correlation".to_string(), serde_json::to_value(c)?);
2659 }
2660 Ok(Value::Object(m))
2661}
2662
2663pub fn cortex_dispatched_data_v1(pack_id: &str, cell_id: &str, doctrine_refs: &[String]) -> Value {
2672 let mut m = Map::new();
2673 m.insert("packId".to_string(), json!(pack_id));
2674 m.insert("cellId".to_string(), json!(cell_id));
2675 m.insert("doctrineRefs".to_string(), json!(doctrine_refs));
2676 m.insert("bridgeVersion".to_string(), json!("1.0"));
2677 Value::Object(m)
2678}
2679
2680pub fn firecracker_pool_event_data_v1(cell_id: &str, pool_hit: bool, slot_count: usize) -> Value {
2690 let mut m = Map::new();
2691 m.insert("cellId".to_string(), json!(cell_id));
2692 m.insert("poolHit".to_string(), json!(pool_hit));
2693 m.insert("slotCount".to_string(), json!(slot_count));
2694 Value::Object(m)
2695}
2696
2697pub fn cloud_event_v1_cortex_dispatched(
2704 source: &str,
2705 time: &str,
2706 pack_id: &str,
2707 cell_id: &str,
2708 doctrine_refs: &[String],
2709) -> CloudEventV1 {
2710 CloudEventV1 {
2711 specversion: "1.0".into(),
2712 id: uuid::Uuid::new_v4().to_string(),
2713 source: source.to_string(),
2714 ty: "dev.cellos.events.cell.cortex.v1.dispatched".into(),
2715 datacontenttype: Some("application/json".into()),
2716 data: Some(cortex_dispatched_data_v1(pack_id, cell_id, doctrine_refs)),
2717 time: Some(time.to_string()),
2718 traceparent: None,
2719 cex: None,
2720 }
2721}
2722
2723pub fn cloud_event_v1_firecracker_pool_checkout(
2732 source: &str,
2733 time: &str,
2734 cell_id: &str,
2735 pool_hit: bool,
2736 slot_count: usize,
2737) -> CloudEventV1 {
2738 CloudEventV1 {
2739 specversion: "1.0".into(),
2740 id: uuid::Uuid::new_v4().to_string(),
2741 source: source.to_string(),
2742 ty: "dev.cellos.events.cell.firecracker.v1.pool_checkout".into(),
2743 datacontenttype: Some("application/json".into()),
2744 data: Some(firecracker_pool_event_data_v1(
2745 cell_id, pool_hit, slot_count,
2746 )),
2747 time: Some(time.to_string()),
2748 traceparent: None,
2749 cex: None,
2750 }
2751}
2752
2753pub const FORMATION_CREATED_TYPE: &str = "dev.cellos.events.cell.formation.v1.created";
2770
2771pub const FORMATION_LAUNCHING_TYPE: &str = "dev.cellos.events.cell.formation.v1.launching";
2773
2774pub const FORMATION_RUNNING_TYPE: &str = "dev.cellos.events.cell.formation.v1.running";
2776
2777pub const FORMATION_DEGRADED_TYPE: &str = "dev.cellos.events.cell.formation.v1.degraded";
2779
2780pub const FORMATION_COMPLETED_TYPE: &str = "dev.cellos.events.cell.formation.v1.completed";
2782
2783pub const FORMATION_FAILED_TYPE: &str = "dev.cellos.events.cell.formation.v1.failed";
2785
2786fn formation_data_v1(
2804 formation_id: &str,
2805 formation_name: &str,
2806 cell_count: u32,
2807 failed_cell_ids: &[String],
2808 reason: Option<&str>,
2809) -> Value {
2810 let mut m = Map::new();
2811 m.insert("formationId".to_string(), json!(formation_id));
2812 m.insert("formationName".to_string(), json!(formation_name));
2813 m.insert("cellCount".to_string(), json!(cell_count));
2814 m.insert("failedCellIds".to_string(), json!(failed_cell_ids));
2815 if let Some(r) = reason {
2816 m.insert("reason".to_string(), json!(r));
2817 }
2818 Value::Object(m)
2819}
2820
2821pub fn cloud_event_v1_formation_created(
2828 source: &str,
2829 time: &str,
2830 formation_id: &str,
2831 formation_name: &str,
2832 cell_count: u32,
2833 failed_cell_ids: &[String],
2834 reason: Option<&str>,
2835) -> CloudEventV1 {
2836 CloudEventV1 {
2837 specversion: "1.0".into(),
2838 id: uuid::Uuid::new_v4().to_string(),
2839 source: source.to_string(),
2840 ty: FORMATION_CREATED_TYPE.to_string(),
2841 datacontenttype: Some("application/json".into()),
2842 data: Some(formation_data_v1(
2843 formation_id,
2844 formation_name,
2845 cell_count,
2846 failed_cell_ids,
2847 reason,
2848 )),
2849 time: Some(time.to_string()),
2850 traceparent: None,
2851 cex: None,
2852 }
2853}
2854
2855pub fn cloud_event_v1_formation_launching(
2863 source: &str,
2864 time: &str,
2865 formation_id: &str,
2866 formation_name: &str,
2867 cell_count: u32,
2868 failed_cell_ids: &[String],
2869 reason: Option<&str>,
2870) -> CloudEventV1 {
2871 CloudEventV1 {
2872 specversion: "1.0".into(),
2873 id: uuid::Uuid::new_v4().to_string(),
2874 source: source.to_string(),
2875 ty: FORMATION_LAUNCHING_TYPE.to_string(),
2876 datacontenttype: Some("application/json".into()),
2877 data: Some(formation_data_v1(
2878 formation_id,
2879 formation_name,
2880 cell_count,
2881 failed_cell_ids,
2882 reason,
2883 )),
2884 time: Some(time.to_string()),
2885 traceparent: None,
2886 cex: None,
2887 }
2888}
2889
2890pub fn cloud_event_v1_formation_running(
2896 source: &str,
2897 time: &str,
2898 formation_id: &str,
2899 formation_name: &str,
2900 cell_count: u32,
2901 failed_cell_ids: &[String],
2902 reason: Option<&str>,
2903) -> CloudEventV1 {
2904 CloudEventV1 {
2905 specversion: "1.0".into(),
2906 id: uuid::Uuid::new_v4().to_string(),
2907 source: source.to_string(),
2908 ty: FORMATION_RUNNING_TYPE.to_string(),
2909 datacontenttype: Some("application/json".into()),
2910 data: Some(formation_data_v1(
2911 formation_id,
2912 formation_name,
2913 cell_count,
2914 failed_cell_ids,
2915 reason,
2916 )),
2917 time: Some(time.to_string()),
2918 traceparent: None,
2919 cex: None,
2920 }
2921}
2922
2923pub fn cloud_event_v1_formation_degraded(
2930 source: &str,
2931 time: &str,
2932 formation_id: &str,
2933 formation_name: &str,
2934 cell_count: u32,
2935 failed_cell_ids: &[String],
2936 reason: Option<&str>,
2937) -> CloudEventV1 {
2938 CloudEventV1 {
2939 specversion: "1.0".into(),
2940 id: uuid::Uuid::new_v4().to_string(),
2941 source: source.to_string(),
2942 ty: FORMATION_DEGRADED_TYPE.to_string(),
2943 datacontenttype: Some("application/json".into()),
2944 data: Some(formation_data_v1(
2945 formation_id,
2946 formation_name,
2947 cell_count,
2948 failed_cell_ids,
2949 reason,
2950 )),
2951 time: Some(time.to_string()),
2952 traceparent: None,
2953 cex: None,
2954 }
2955}
2956
2957pub fn cloud_event_v1_formation_completed(
2964 source: &str,
2965 time: &str,
2966 formation_id: &str,
2967 formation_name: &str,
2968 cell_count: u32,
2969 failed_cell_ids: &[String],
2970 reason: Option<&str>,
2971) -> CloudEventV1 {
2972 CloudEventV1 {
2973 specversion: "1.0".into(),
2974 id: uuid::Uuid::new_v4().to_string(),
2975 source: source.to_string(),
2976 ty: FORMATION_COMPLETED_TYPE.to_string(),
2977 datacontenttype: Some("application/json".into()),
2978 data: Some(formation_data_v1(
2979 formation_id,
2980 formation_name,
2981 cell_count,
2982 failed_cell_ids,
2983 reason,
2984 )),
2985 time: Some(time.to_string()),
2986 traceparent: None,
2987 cex: None,
2988 }
2989}
2990
2991pub fn cloud_event_v1_formation_failed(
2998 source: &str,
2999 time: &str,
3000 formation_id: &str,
3001 formation_name: &str,
3002 cell_count: u32,
3003 failed_cell_ids: &[String],
3004 reason: Option<&str>,
3005) -> CloudEventV1 {
3006 CloudEventV1 {
3007 specversion: "1.0".into(),
3008 id: uuid::Uuid::new_v4().to_string(),
3009 source: source.to_string(),
3010 ty: FORMATION_FAILED_TYPE.to_string(),
3011 datacontenttype: Some("application/json".into()),
3012 data: Some(formation_data_v1(
3013 formation_id,
3014 formation_name,
3015 cell_count,
3016 failed_cell_ids,
3017 reason,
3018 )),
3019 time: Some(time.to_string()),
3020 traceparent: None,
3021 cex: None,
3022 }
3023}
3024
3025#[cfg(test)]
3026mod tests {
3027 use super::*;
3028 use crate::{
3029 Correlation, ExecutionCellDocument, ExportReceipt, ExportReceiptTargetKind, Lifetime,
3030 WorkloadIdentity, WorkloadIdentityKind,
3031 };
3032
3033 #[test]
3034 fn admission_receipt_stamps_provider_identity() {
3035 let spec = crate::ExecutionCellSpec::default();
3038 let data = admission_decision_data_v1(
3039 &spec,
3040 true,
3041 AdmissionDecisionReason::AdmittedWithinCeiling,
3042 "ceiling.is_superset_of(spec.authority)",
3043 "sha256:spechash",
3044 Some("run-1"),
3045 EnforcementBinding::DeclaredAudit,
3046 &[],
3047 )
3048 .unwrap();
3049 let pid = data
3050 .get("providerIdentity")
3051 .expect("admission receipt must stamp providerIdentity");
3052 assert!(
3053 pid.get("name").and_then(|v| v.as_str()).is_some(),
3054 "providerIdentity carries a name"
3055 );
3056 assert!(
3057 pid.get("fipsMode").and_then(|v| v.as_bool()).is_some(),
3058 "providerIdentity carries fipsMode (producer claim)"
3059 );
3060 }
3061
3062 #[test]
3063 fn audit_reconciled_data_v1_is_deterministic_and_verifies_offline() {
3064 use crate::{
3067 sign_event_with, verify_signed_event_envelope, CloudEventV1, Signer, SoftwareSigner,
3068 };
3069 use std::collections::HashMap;
3070
3071 let a = audit_reconciled_data_v1(
3072 "chain-A",
3073 0,
3074 41,
3075 "abc123",
3076 42,
3077 Some("abc123"),
3078 "blake3-chain+ed25519-head",
3079 true,
3080 )
3081 .unwrap();
3082 let b = audit_reconciled_data_v1(
3083 "chain-A",
3084 0,
3085 41,
3086 "abc123",
3087 42,
3088 Some("abc123"),
3089 "blake3-chain+ed25519-head",
3090 true,
3091 )
3092 .unwrap();
3093 assert_eq!(
3094 serde_json::to_vec(&a).unwrap(),
3095 serde_json::to_vec(&b).unwrap(),
3096 "builder must be deterministic"
3097 );
3098 assert_eq!(a["toSeq"], serde_json::json!(41));
3099 assert_eq!(a["signed"], serde_json::json!(true));
3100
3101 let event = CloudEventV1 {
3102 specversion: "1.0".into(),
3103 id: "rec-1".into(),
3104 source: "cellos-sink-spool".into(),
3105 ty: AUDIT_RECONCILED_TYPE.into(),
3106 datacontenttype: Some("application/json".into()),
3107 data: Some(a),
3108 time: None,
3109 traceparent: None,
3110 cex: None,
3111 };
3112 let signer = SoftwareSigner::from_seed("org-root-2026", [5u8; 32]).unwrap();
3113 let envelope = sign_event_with(&signer, &event).unwrap();
3114 let mut keys = HashMap::new();
3115 keys.insert("org-root-2026".to_string(), signer.verifying_key());
3116 let hmac: HashMap<String, Vec<u8>> = HashMap::new();
3117 let verified = verify_signed_event_envelope(&envelope, &keys, &hmac).unwrap();
3118 assert_eq!(verified.ty, AUDIT_RECONCILED_TYPE);
3119 }
3120
3121 #[test]
3122 fn lifecycle_started_matches_example_shape() {
3123 let raw =
3124 include_str!("../../../contracts/examples/execution-cell-ci-correlation.valid.json");
3125 let doc: ExecutionCellDocument = serde_json::from_str(raw).unwrap();
3126 let expected: Value = serde_json::from_str(include_str!(
3127 "../../../contracts/examples/cell-lifecycle-started-data.valid.json"
3128 ))
3129 .unwrap();
3130 let data = lifecycle_started_data_v1(
3131 &doc.spec,
3132 "host-cell-abc123",
3133 Some("run-2026-04-06-001"),
3134 None,
3135 None,
3136 None,
3137 None,
3138 None,
3139 None,
3140 None,
3141 )
3142 .unwrap();
3143 assert_eq!(data, expected);
3144 }
3145
3146 #[test]
3147 fn lifecycle_started_without_correlation() {
3148 let spec = ExecutionCellSpec {
3149 id: "s1".into(),
3150 correlation: None,
3151 ingress: None,
3152 environment: None,
3153 placement: None,
3154 policy: None,
3155 identity: None,
3156 run: None,
3157 authority: Default::default(),
3158 lifetime: Lifetime { ttl_seconds: 60 },
3159 export: None,
3160 telemetry: None,
3161 };
3162 let data =
3163 lifecycle_started_data_v1(&spec, "c1", None, None, None, None, None, None, None, None)
3164 .unwrap();
3165 assert!(!data.as_object().unwrap().contains_key("correlation"));
3166 assert!(!data.as_object().unwrap().contains_key("runId"));
3167 assert!(!data.as_object().unwrap().contains_key("derivationVerified"));
3168 assert!(!data.as_object().unwrap().contains_key("roleRoot"));
3169 assert!(!data.as_object().unwrap().contains_key("parentRunId"));
3170 assert!(!data.as_object().unwrap().contains_key("kernelDigestSha256"));
3171 assert!(!data.as_object().unwrap().contains_key("rootfsDigestSha256"));
3172 assert!(!data
3173 .as_object()
3174 .unwrap()
3175 .contains_key("firecrackerDigestSha256"));
3176 }
3177
3178 #[test]
3179 fn lifecycle_started_partial_correlation_serializes() {
3180 let spec = ExecutionCellSpec {
3181 id: "s2".into(),
3182 correlation: Some(Correlation {
3183 platform: Some("custom".into()),
3184 external_run_id: None,
3185 external_job_id: None,
3186 tenant_id: None,
3187 labels: None,
3188 correlation_id: None,
3189 }),
3190 ingress: None,
3191 environment: None,
3192 placement: None,
3193 policy: None,
3194 identity: None,
3195 run: None,
3196 authority: Default::default(),
3197 lifetime: Lifetime { ttl_seconds: 1 },
3198 export: None,
3199 telemetry: None,
3200 };
3201 let data =
3202 lifecycle_started_data_v1(&spec, "c2", None, None, None, None, None, None, None, None)
3203 .unwrap();
3204 assert_eq!(data["correlation"]["platform"], "custom");
3205 }
3206
3207 #[test]
3208 fn lifecycle_started_with_derivation_fields_emits_them() {
3209 let spec = ExecutionCellSpec {
3210 id: "deriv-1".into(),
3211 correlation: None,
3212 ingress: None,
3213 environment: None,
3214 placement: None,
3215 policy: None,
3216 identity: None,
3217 run: None,
3218 authority: Default::default(),
3219 lifetime: Lifetime { ttl_seconds: 60 },
3220 export: None,
3221 telemetry: None,
3222 };
3223 let data = lifecycle_started_data_v1(
3224 &spec,
3225 "cell-deriv",
3226 Some("run-deriv-1"),
3227 Some(false),
3228 Some("role-prod-ci"),
3229 Some("run-parent-001"),
3230 Some("abc123def456"),
3231 None,
3232 None,
3233 None,
3234 )
3235 .unwrap();
3236 assert_eq!(data["derivationVerified"], false);
3237 assert_eq!(data["roleRoot"], "role-prod-ci");
3238 assert_eq!(data["parentRunId"], "run-parent-001");
3239 }
3240
3241 #[test]
3242 fn lifecycle_destroyed_succeeded_shape() {
3243 let raw =
3244 include_str!("../../../contracts/examples/execution-cell-ci-correlation.valid.json");
3245 let doc: ExecutionCellDocument = serde_json::from_str(raw).unwrap();
3246 let data = lifecycle_destroyed_data_v1(
3247 &doc.spec,
3248 "host-xyz",
3249 Some("run-test"),
3250 LifecycleDestroyOutcome::Succeeded,
3251 None,
3252 None,
3253 None,
3254 None,
3255 )
3256 .unwrap();
3257 assert_eq!(data["outcome"], "succeeded");
3258 assert!(!data.as_object().unwrap().contains_key("reason"));
3259 assert!(
3260 !data.as_object().unwrap().contains_key("terminalState"),
3261 "terminalState must be omitted when None for backward-compat"
3262 );
3263 assert!(
3264 !data.as_object().unwrap().contains_key("evidenceBundleRef"),
3265 "evidenceBundleRef must be omitted when None for backward-compat"
3266 );
3267 assert!(
3268 !data.as_object().unwrap().contains_key("residueClass"),
3269 "residueClass must be omitted when None for backward-compat"
3270 );
3271 assert_eq!(data["ttlSeconds"], 3600);
3272 }
3273
3274 #[test]
3275 fn lifecycle_destroyed_failed_includes_reason() {
3276 let spec = ExecutionCellSpec {
3277 id: "s1".into(),
3278 correlation: None,
3279 ingress: None,
3280 environment: None,
3281 placement: None,
3282 policy: None,
3283 identity: None,
3284 run: None,
3285 authority: Default::default(),
3286 lifetime: Lifetime { ttl_seconds: 60 },
3287 export: None,
3288 telemetry: None,
3289 };
3290 let data = lifecycle_destroyed_data_v1(
3291 &spec,
3292 "c1",
3293 None,
3294 LifecycleDestroyOutcome::Failed,
3295 Some("secret resolve: denied"),
3296 None,
3297 None,
3298 None,
3299 )
3300 .unwrap();
3301 assert_eq!(data["outcome"], "failed");
3302 assert_eq!(data["reason"], "secret resolve: denied");
3303 }
3304
3305 #[test]
3306 fn admission_decision_reject_shape_and_determinism() {
3307 let raw =
3308 include_str!("../../../contracts/examples/execution-cell-ci-correlation.valid.json");
3309 let doc: ExecutionCellDocument = serde_json::from_str(raw).unwrap();
3310
3311 let build = || {
3312 admission_decision_data_v1(
3313 &doc.spec,
3314 false,
3315 AdmissionDecisionReason::AuthorityExceedsCeiling,
3316 "spec.authority ⊆ ceiling.authority",
3317 "sha256:abc",
3318 Some("run-001"),
3319 EnforcementBinding::LinuxNftables,
3320 &[("environment", "ci")],
3321 )
3322 .unwrap()
3323 };
3324
3325 let data = build();
3326 assert_eq!(data["admitted"], false);
3327 assert_eq!(data["reason"], "authority_exceeds_ceiling");
3328 assert_eq!(data["predicate"], "spec.authority ⊆ ceiling.authority");
3329 assert_eq!(data["specHash"], "sha256:abc");
3330 assert_eq!(data["runId"], "run-001");
3331 assert_eq!(data["enforcementBinding"], "linux-nftables");
3332 assert_eq!(data["context"]["environment"], "ci");
3333
3334 let closed = [
3336 "authority_exceeds_ceiling",
3337 "ceiling_manifest_unverified",
3338 "ceiling_manifest_unparseable",
3339 "ceiling_manifest_wrong_payload_type",
3340 "ceiling_dimension_uncomparable",
3341 "ceiling_epoch_stale",
3342 "ceiling_degraded_unconfigured",
3343 "formation_edge_not_narrowing",
3344 "admitted_within_ceiling",
3345 ];
3346 assert!(closed.contains(&data["reason"].as_str().unwrap()));
3347
3348 let a = serde_json::to_vec(&build()).unwrap();
3350 let b = serde_json::to_vec(&build()).unwrap();
3351 assert_eq!(a, b);
3352 }
3353
3354 #[test]
3355 fn admission_decision_allow_omits_optional_fields() {
3356 let spec = ExecutionCellSpec {
3357 id: "s1".into(),
3358 correlation: None,
3359 ingress: None,
3360 environment: None,
3361 placement: None,
3362 policy: None,
3363 identity: None,
3364 run: None,
3365 authority: Default::default(),
3366 lifetime: Lifetime { ttl_seconds: 60 },
3367 export: None,
3368 telemetry: None,
3369 };
3370 let data = admission_decision_data_v1(
3371 &spec,
3372 true,
3373 AdmissionDecisionReason::AdmittedWithinCeiling,
3374 "spec.authority ⊆ ceiling.authority",
3375 "sha256:def",
3376 None,
3377 EnforcementBinding::DeclaredAudit,
3378 &[],
3379 )
3380 .unwrap();
3381 assert_eq!(data["admitted"], true);
3382 assert_eq!(data["reason"], "admitted_within_ceiling");
3383 assert_eq!(data["enforcementBinding"], "declared-audit");
3384 let obj = data.as_object().unwrap();
3385 assert!(!obj.contains_key("runId"), "runId omitted when None");
3386 assert!(!obj.contains_key("context"), "context omitted when empty");
3387 assert!(
3388 !obj.contains_key("correlation"),
3389 "correlation omitted when None"
3390 );
3391 }
3392
3393 #[test]
3394 fn lifecycle_destroyed_terminal_state_clean_serializes() {
3395 let spec = ExecutionCellSpec {
3396 id: "term-clean".into(),
3397 correlation: None,
3398 ingress: None,
3399 environment: None,
3400 placement: None,
3401 policy: None,
3402 identity: None,
3403 run: None,
3404 authority: Default::default(),
3405 lifetime: Lifetime { ttl_seconds: 60 },
3406 export: None,
3407 telemetry: None,
3408 };
3409 let data = lifecycle_destroyed_data_v1(
3410 &spec,
3411 "c-clean",
3412 None,
3413 LifecycleDestroyOutcome::Succeeded,
3414 None,
3415 Some(LifecycleTerminalState::Clean),
3416 None,
3417 None,
3418 )
3419 .unwrap();
3420 assert_eq!(data["terminalState"], "clean");
3421 }
3422
3423 #[test]
3424 fn lifecycle_destroyed_terminal_state_forced_serializes() {
3425 let spec = ExecutionCellSpec {
3426 id: "term-forced".into(),
3427 correlation: None,
3428 ingress: None,
3429 environment: None,
3430 placement: None,
3431 policy: None,
3432 identity: None,
3433 run: None,
3434 authority: Default::default(),
3435 lifetime: Lifetime { ttl_seconds: 60 },
3436 export: None,
3437 telemetry: None,
3438 };
3439 let data = lifecycle_destroyed_data_v1(
3440 &spec,
3441 "c-forced",
3442 None,
3443 LifecycleDestroyOutcome::Failed,
3444 Some("in-VM exit bridge: vsock closed"),
3445 Some(LifecycleTerminalState::Forced),
3446 None,
3447 None,
3448 )
3449 .unwrap();
3450 assert_eq!(data["terminalState"], "forced");
3451 assert_eq!(data["outcome"], "failed");
3452 }
3453
3454 #[test]
3455 fn lifecycle_destroyed_evidence_bundle_and_residue_class_serialize_when_populated() {
3456 let spec = ExecutionCellSpec {
3457 id: "f5-populated".into(),
3458 correlation: None,
3459 ingress: None,
3460 environment: None,
3461 placement: None,
3462 policy: None,
3463 identity: None,
3464 run: None,
3465 authority: Default::default(),
3466 lifetime: Lifetime { ttl_seconds: 60 },
3467 export: None,
3468 telemetry: None,
3469 };
3470 let bundle = SubjectUrn::parse("urn:cellos:evidence-bundle:run-1").unwrap();
3471 let data = lifecycle_destroyed_data_v1(
3472 &spec,
3473 "c-f5",
3474 Some("run-1"),
3475 LifecycleDestroyOutcome::Succeeded,
3476 None,
3477 None,
3478 Some(&bundle),
3479 Some(ResidueClass::DocumentedException),
3480 )
3481 .unwrap();
3482 assert_eq!(
3483 data["evidenceBundleRef"],
3484 "urn:cellos:evidence-bundle:run-1"
3485 );
3486 assert_eq!(data["residueClass"], "documented_exception");
3487 }
3488
3489 #[test]
3490 fn lifecycle_destroyed_evidence_bundle_and_residue_class_omitted_when_none() {
3491 let spec = ExecutionCellSpec {
3492 id: "f5-omitted".into(),
3493 correlation: None,
3494 ingress: None,
3495 environment: None,
3496 placement: None,
3497 policy: None,
3498 identity: None,
3499 run: None,
3500 authority: Default::default(),
3501 lifetime: Lifetime { ttl_seconds: 60 },
3502 export: None,
3503 telemetry: None,
3504 };
3505 let data = lifecycle_destroyed_data_v1(
3506 &spec,
3507 "c-f5-omit",
3508 None,
3509 LifecycleDestroyOutcome::Succeeded,
3510 None,
3511 None,
3512 None,
3513 None,
3514 )
3515 .unwrap();
3516 let obj = data.as_object().unwrap();
3517 assert!(
3518 !obj.contains_key("evidenceBundleRef"),
3519 "evidenceBundleRef must be omitted when None"
3520 );
3521 assert!(
3522 !obj.contains_key("residueClass"),
3523 "residueClass must be omitted when None"
3524 );
3525 }
3526
3527 #[test]
3528 fn identity_materialized_matches_example_shape() {
3529 let raw =
3530 include_str!("../../../contracts/examples/execution-cell-github-oidc-s3.valid.json");
3531 let doc: ExecutionCellDocument = serde_json::from_str(raw).unwrap();
3532 let identity = doc.spec.identity.as_ref().expect("identity");
3533 let data = identity_materialized_data_v1(&doc.spec, "host-xyz", Some("run-test"), identity)
3534 .unwrap();
3535 assert_eq!(data["identity"]["kind"], "federatedOidc");
3536 assert_eq!(data["identity"]["provider"], "github-actions");
3537 assert_eq!(data["identity"]["secretRef"], "AWS_WEB_IDENTITY");
3538 assert_eq!(data["runId"], "run-test");
3539 }
3540
3541 #[test]
3542 fn identity_materialized_provisioned_witnesses_aws_role_arn() {
3543 let spec = ExecutionCellSpec {
3544 id: "s-aws".into(),
3545 correlation: None,
3546 ingress: None,
3547 environment: None,
3548 placement: None,
3549 policy: None,
3550 identity: Some(WorkloadIdentity {
3551 kind: WorkloadIdentityKind::AwsAssumeRole {
3552 role_arn: "arn:aws:iam::123456789012:role/cell".into(),
3553 sts_session_policy: None,
3554 },
3555 provider: String::new(),
3556 audience: String::new(),
3557 subject: None,
3558 ttl_seconds: Some(900),
3559 secret_ref: "AWS_WEB_IDENTITY".into(),
3560 }),
3561 run: None,
3562 authority: Default::default(),
3563 lifetime: Lifetime { ttl_seconds: 3600 },
3564 export: None,
3565 telemetry: None,
3566 };
3567 let identity = spec.identity.as_ref().unwrap();
3568 let witness = ProvisioningWitness {
3569 provisioned_role_arn: Some("arn:aws:iam::123456789012:role/cell"),
3570 session_policy_digest: Some(
3571 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
3572 ),
3573 azure_object_id: None,
3574 credential_jti: Some("jti-abc"),
3575 };
3576 let data = identity_materialized_data_v1_provisioned(
3577 &spec,
3578 "host-aws",
3579 Some("run-aws"),
3580 identity,
3581 &witness,
3582 )
3583 .unwrap();
3584 assert_eq!(
3585 data["provisionedRoleArn"],
3586 "arn:aws:iam::123456789012:role/cell"
3587 );
3588 assert_eq!(
3589 data["sessionPolicyDigest"],
3590 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
3591 );
3592 assert_eq!(data["credentialJti"], "jti-abc");
3593 let obj = data.as_object().unwrap();
3594 assert!(
3595 !obj.contains_key("azureObjectId"),
3596 "azureObjectId must be omitted when None"
3597 );
3598 }
3599
3600 #[test]
3601 fn identity_materialized_provisioned_empty_witness_matches_base() {
3602 let raw =
3603 include_str!("../../../contracts/examples/execution-cell-github-oidc-s3.valid.json");
3604 let doc: ExecutionCellDocument = serde_json::from_str(raw).unwrap();
3605 let identity = doc.spec.identity.as_ref().expect("identity");
3606 let base = identity_materialized_data_v1(&doc.spec, "host-xyz", Some("run-test"), identity)
3607 .unwrap();
3608 let provisioned = identity_materialized_data_v1_provisioned(
3609 &doc.spec,
3610 "host-xyz",
3611 Some("run-test"),
3612 identity,
3613 &ProvisioningWitness::default(),
3614 )
3615 .unwrap();
3616 assert_eq!(base, provisioned);
3617 }
3618
3619 #[test]
3620 fn identity_revoked_includes_reason() {
3621 let spec = ExecutionCellSpec {
3622 id: "s3".into(),
3623 correlation: None,
3624 ingress: None,
3625 environment: None,
3626 placement: None,
3627 policy: None,
3628 identity: Some(WorkloadIdentity {
3629 kind: WorkloadIdentityKind::FederatedOidc,
3630 provider: "github-actions".into(),
3631 audience: "sts.amazonaws.com".into(),
3632 subject: None,
3633 ttl_seconds: Some(900),
3634 secret_ref: "AWS_WEB_IDENTITY".into(),
3635 }),
3636 run: None,
3637 authority: Default::default(),
3638 lifetime: Lifetime { ttl_seconds: 3600 },
3639 export: None,
3640 telemetry: None,
3641 };
3642 let identity = spec.identity.as_ref().unwrap();
3643 let data =
3644 identity_revoked_data_v1(&spec, "c3", None, identity, Some("teardown"), None).unwrap();
3645 assert_eq!(data["identity"]["audience"], "sts.amazonaws.com");
3646 assert_eq!(data["reason"], "teardown");
3647 }
3648
3649 #[test]
3650 fn identity_failed_matches_example_shape() {
3651 let raw =
3652 include_str!("../../../contracts/examples/execution-cell-github-oidc-s3.valid.json");
3653 let doc: ExecutionCellDocument = serde_json::from_str(raw).unwrap();
3654 let expected: Value = serde_json::from_str(include_str!(
3655 "../../../contracts/examples/cell-identity-failed-data.valid.json"
3656 ))
3657 .unwrap();
3658 let identity = doc.spec.identity.as_ref().expect("identity");
3659 let data = identity_failed_data_v1(
3660 &doc.spec,
3661 "host-cell-demo",
3662 Some("run-001"),
3663 identity,
3664 IdentityFailureOperation::Materialize,
3665 "oidc exchange denied by upstream federation policy",
3666 )
3667 .unwrap();
3668 assert_eq!(data, expected);
3669 }
3670
3671 #[test]
3672 fn export_completed_v2_matches_example_shape() {
3673 let raw =
3674 include_str!("../../../contracts/examples/execution-cell-github-oidc-s3.valid.json");
3675 let doc: ExecutionCellDocument = serde_json::from_str(raw).unwrap();
3676 let receipt = ExportReceipt {
3677 target_kind: ExportReceiptTargetKind::S3,
3678 target_name: Some("artifact-bucket".into()),
3679 destination: "s3://acme-cellos-artifacts/github/acme/widget/123456789/test-results"
3680 .into(),
3681 bytes_written: 1024,
3682 };
3683 let data = export_completed_data_v2(
3684 &doc.spec,
3685 "host-xyz",
3686 Some("run-test"),
3687 "test-results",
3688 &receipt,
3689 None,
3690 )
3691 .unwrap();
3692 assert_eq!(data["receipt"]["targetKind"], "s3");
3693 assert_eq!(data["receipt"]["targetName"], "artifact-bucket");
3694 assert_eq!(data["receipt"]["bytesWritten"], 1024);
3695 }
3696
3697 #[test]
3698 fn export_completed_v2_http_matches_example_shape() {
3699 let raw = include_str!(
3700 "../../../contracts/examples/execution-cell-github-oidc-multi-export.valid.json"
3701 );
3702 let doc: ExecutionCellDocument = serde_json::from_str(raw).unwrap();
3703 let expected: Value = serde_json::from_str(include_str!(
3704 "../../../contracts/examples/cell-export-v2-completed-data-http.valid.json"
3705 ))
3706 .unwrap();
3707 let receipt = ExportReceipt {
3708 target_kind: ExportReceiptTargetKind::Http,
3709 target_name: Some("artifact-api".into()),
3710 destination: "https://artifacts.acme.internal/upload/host-cell-demo/coverage-summary"
3711 .into(),
3712 bytes_written: 512,
3713 };
3714 let data = export_completed_data_v2(
3715 &doc.spec,
3716 "host-cell-demo",
3717 Some("run-002"),
3718 "coverage-summary",
3719 &receipt,
3720 None,
3721 )
3722 .unwrap();
3723 assert_eq!(data, expected);
3724 }
3725
3726 #[test]
3727 fn export_failed_v2_http_matches_example_shape() {
3728 let raw = include_str!(
3729 "../../../contracts/examples/execution-cell-github-oidc-multi-export.valid.json"
3730 );
3731 let doc: ExecutionCellDocument = serde_json::from_str(raw).unwrap();
3732 let expected: Value = serde_json::from_str(include_str!(
3733 "../../../contracts/examples/cell-export-v2-failed-data.valid.json"
3734 ))
3735 .unwrap();
3736 let data = export_failed_data_v2(
3737 &doc.spec,
3738 "host-cell-demo",
3739 Some("run-002"),
3740 "coverage-summary",
3741 ExportReceiptTargetKind::Http,
3742 Some("artifact-api"),
3743 Some("https://artifacts.acme.internal/upload/host-cell-demo/coverage-summary"),
3744 "http put returned 403 Forbidden",
3745 None,
3746 )
3747 .unwrap();
3748 assert_eq!(data, expected);
3749 }
3750
3751 #[test]
3752 fn compliance_summary_matches_example_shape() {
3753 let raw =
3754 include_str!("../../../contracts/examples/execution-cell-ci-correlation.valid.json");
3755 let doc: ExecutionCellDocument = serde_json::from_str(raw).unwrap();
3756 let expected: Value = serde_json::from_str(include_str!(
3757 "../../../contracts/examples/cell-compliance-summary-data.valid.json"
3758 ))
3759 .unwrap();
3760 let data =
3761 compliance_summary_data_v1(&doc.spec, "host-cell-demo", Some("run-003"), Some(0))
3762 .unwrap();
3763 assert_eq!(data, expected);
3764 }
3765
3766 #[test]
3767 fn compliance_summary_omits_placement_when_absent() {
3768 let spec = ExecutionCellSpec {
3769 id: "compliance-no-placement".into(),
3770 correlation: None,
3771 ingress: None,
3772 environment: None,
3773 placement: None,
3774 policy: None,
3775 identity: None,
3776 run: None,
3777 authority: Default::default(),
3778 lifetime: Lifetime { ttl_seconds: 60 },
3779 export: None,
3780 telemetry: None,
3781 };
3782 let data = compliance_summary_data_v1(&spec, "cell-001", None, None).unwrap();
3783 assert!(!data.as_object().unwrap().contains_key("placement"));
3784 }
3785
3786 #[test]
3791 fn compliance_summary_with_empty_subjects_omits_field() {
3792 let raw =
3793 include_str!("../../../contracts/examples/execution-cell-ci-correlation.valid.json");
3794 let doc: ExecutionCellDocument = serde_json::from_str(raw).unwrap();
3795 let legacy =
3796 compliance_summary_data_v1(&doc.spec, "host-cell-demo", Some("run-003"), Some(0))
3797 .unwrap();
3798 let with_empty = compliance_summary_data_v1_with_subjects(
3799 &doc.spec,
3800 "host-cell-demo",
3801 Some("run-003"),
3802 Some(0),
3803 &[],
3804 )
3805 .unwrap();
3806 assert_eq!(legacy, with_empty);
3807 assert!(!with_empty.as_object().unwrap().contains_key("subjectUrns"));
3808 }
3809
3810 #[test]
3813 fn compliance_summary_with_subjects_matches_example_shape() {
3814 let raw =
3815 include_str!("../../../contracts/examples/execution-cell-ci-correlation.valid.json");
3816 let doc: ExecutionCellDocument = serde_json::from_str(raw).unwrap();
3817 let expected: Value = serde_json::from_str(include_str!(
3818 "../../../contracts/examples/cell-compliance-summary-data-with-subjects.valid.json"
3819 ))
3820 .unwrap();
3821 let subjects: Vec<SubjectUrn> = vec![
3822 SubjectUrn::parse("urn:cellos:cell:host-cell-demo").unwrap(),
3823 SubjectUrn::parse("urn:tsafe:lease:lease-42").unwrap(),
3824 SubjectUrn::parse("urn:cellos:export:run-003%2Fartifact-1").unwrap(),
3825 ];
3826 let data = compliance_summary_data_v1_with_subjects(
3827 &doc.spec,
3828 "host-cell-demo",
3829 Some("run-003"),
3830 Some(0),
3831 &subjects,
3832 )
3833 .unwrap();
3834 assert_eq!(data, expected);
3835 let urns = data["subjectUrns"].as_array().unwrap();
3836 assert_eq!(urns.len(), 3);
3837 assert_eq!(urns[0], "urn:cellos:cell:host-cell-demo");
3838 }
3839
3840 #[test]
3847 fn compliance_summary_invalid_subject_urns_fixture_is_malformed() {
3848 let raw =
3849 include_str!("../../../contracts/examples/cell-compliance-summary-data.invalid.json");
3850 let v: Value = serde_json::from_str(raw).unwrap();
3851 let urns = v["subjectUrns"]
3852 .as_array()
3853 .expect("invalid fixture must carry subjectUrns array");
3854 assert!(!urns.is_empty(), "negative fixture must have entries");
3855
3856 fn matches_schema_shape(s: &str) -> bool {
3863 let parts: Vec<&str> = s.splitn(4, ':').collect();
3864 if parts.len() != 4 {
3865 return false;
3866 }
3867 if parts[0] != "urn" {
3868 return false;
3869 }
3870 let segment_ok = |seg: &str| {
3871 let mut it = seg.chars();
3872 match it.next() {
3873 Some(c) if c.is_ascii_lowercase() || c.is_ascii_digit() => {}
3874 _ => return false,
3875 }
3876 it.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
3877 };
3878 if !segment_ok(parts[1]) || !segment_ok(parts[2]) {
3879 return false;
3880 }
3881 if parts[3].is_empty() {
3882 return false;
3883 }
3884 parts[3]
3885 .chars()
3886 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | ':' | '%' | '-'))
3887 }
3888
3889 for (i, urn) in urns.iter().enumerate() {
3890 let s = urn.as_str().unwrap_or("");
3891 assert!(
3892 !matches_schema_shape(s),
3893 "invalid fixture entry [{i}] {s:?} unexpectedly matches the schema URN regex; \
3894 fixture must remain a negative case"
3895 );
3896 }
3897 }
3898
3899 #[test]
3900 fn network_enforcement_matches_example_shape() {
3901 let raw = include_str!(
3902 "../../../contracts/examples/cell-observability-network-enforcement-data.valid.json"
3903 );
3904 let expected: Value = serde_json::from_str(raw).unwrap();
3905 let spec = ExecutionCellSpec {
3906 id: "net-enforcement-demo".into(),
3907 correlation: None,
3908 ingress: None,
3909 environment: None,
3910 placement: None,
3911 policy: None,
3912 identity: None,
3913 run: None,
3914 authority: Default::default(),
3915 lifetime: Lifetime { ttl_seconds: 60 },
3916 export: None,
3917 telemetry: None,
3918 };
3919 let data = observability_network_enforcement_data_v1(
3920 &spec,
3921 "net-enforcement-demo",
3922 Some("run-local-001"),
3923 true,
3924 1,
3925 1,
3926 None,
3927 )
3928 .unwrap();
3929 assert_eq!(data, expected);
3930 }
3931
3932 #[test]
3933 fn dns_resolution_matches_example_shape() {
3934 let raw = include_str!(
3935 "../../../contracts/examples/cell-observability-dns-resolution-data.valid.json"
3936 );
3937 let expected: Value = serde_json::from_str(raw).unwrap();
3938 let spec = ExecutionCellSpec {
3939 id: "demo-cell-dns".into(),
3940 correlation: None,
3941 ingress: None,
3942 environment: None,
3943 placement: None,
3944 policy: None,
3945 identity: None,
3946 run: None,
3947 authority: Default::default(),
3948 lifetime: Lifetime { ttl_seconds: 60 },
3949 export: None,
3950 telemetry: None,
3951 };
3952 let targets: &[(&str, &str, Option<u16>)] = &[
3953 ("203.0.113.10", "inet", Some(443)),
3954 ("2001:db8::1", "inet6", Some(443)),
3955 ];
3956 let data = observability_dns_resolution_data_v1(
3957 &spec,
3958 "demo-cell-dns",
3959 Some("run-001"),
3960 "api.example.com",
3961 "2026-04-30T12:00:00Z",
3962 targets,
3963 300,
3964 "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
3965 "keyset-demo-001",
3966 "kid-resolver-01",
3967 Some("rcpt-demo-0001"),
3968 )
3969 .unwrap();
3970 assert_eq!(data, expected);
3971 }
3972
3973 #[test]
3974 fn dns_target_set_matches_example_shape() {
3975 let raw = include_str!(
3976 "../../../contracts/examples/cell-observability-dns-target-set-data.valid.json"
3977 );
3978 let expected: Value = serde_json::from_str(raw).unwrap();
3979 let spec = ExecutionCellSpec {
3980 id: "demo-cell-dns".into(),
3981 correlation: None,
3982 ingress: None,
3983 environment: None,
3984 placement: None,
3985 policy: None,
3986 identity: None,
3987 run: None,
3988 authority: Default::default(),
3989 lifetime: Lifetime { ttl_seconds: 60 },
3990 export: None,
3991 telemetry: None,
3992 };
3993 let data = observability_dns_target_set_data_v1(
3994 &spec,
3995 "demo-cell-dns",
3996 Some("run-001"),
3997 "cdn.example.com",
3998 "empty",
3999 "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
4000 "refresh",
4001 "2026-04-30T12:05:00Z",
4002 "keyset-demo-001",
4003 "kid-resolver-01",
4004 )
4005 .unwrap();
4006 assert_eq!(data, expected);
4007 }
4008
4009 #[test]
4010 fn dns_query_data_v1_serializes_allow_path() {
4011 use crate::{DnsQueryDecision, DnsQueryEvent, DnsQueryReasonCode, DnsQueryType};
4012 let ev = DnsQueryEvent {
4013 schema_version: "1.0.0".into(),
4014 cell_id: "demo-cell-dns".into(),
4015 run_id: "run-2026-05-01-001".into(),
4016 query_id: "q-3b58b2a4-e4bb-4f89-9c4f-2a0a2c8b6f01".into(),
4017 query_name: "api.example.com".into(),
4018 query_type: DnsQueryType::A,
4019 decision: DnsQueryDecision::Allow,
4020 reason_code: DnsQueryReasonCode::AllowedByAllowlist,
4021 response_rcode: Some(0),
4022 upstream_resolver_id: Some("resolver-do53-internal".into()),
4023 upstream_latency_ms: Some(4),
4024 response_target_count: Some(2),
4025 keyset_id: Some("keyset-demo-001".into()),
4026 issuer_kid: Some("kid-resolver-01".into()),
4027 policy_digest: Some(
4028 "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".into(),
4029 ),
4030 correlation_id: Some("corr-demo-0001".into()),
4031 observed_at: "2026-05-01T12:34:56Z".into(),
4032 };
4033 let v = dns_query_data_v1(&ev).unwrap();
4034 assert_eq!(v["schemaVersion"], "1.0.0");
4035 assert_eq!(v["queryName"], "api.example.com");
4036 assert_eq!(v["queryType"], "A");
4037 assert_eq!(v["decision"], "allow");
4038 assert_eq!(v["reasonCode"], "allowed_by_allowlist");
4039 assert_eq!(v["upstreamResolverId"], "resolver-do53-internal");
4040 assert_eq!(v["responseTargetCount"], 2);
4041 }
4042
4043 #[test]
4044 fn dns_query_data_v1_omits_optionals_on_deny_path() {
4045 use crate::{DnsQueryDecision, DnsQueryEvent, DnsQueryReasonCode, DnsQueryType};
4046 let ev = DnsQueryEvent {
4047 schema_version: "1.0.0".into(),
4048 cell_id: "demo-cell-dns".into(),
4049 run_id: "run-2026-05-01-001".into(),
4050 query_id: "q-deny-001".into(),
4051 query_name: "blocked.example.com".into(),
4052 query_type: DnsQueryType::AAAA,
4053 decision: DnsQueryDecision::Deny,
4054 reason_code: DnsQueryReasonCode::DeniedNotInAllowlist,
4055 response_rcode: Some(5),
4056 upstream_resolver_id: None,
4057 upstream_latency_ms: None,
4058 response_target_count: Some(0),
4059 keyset_id: None,
4060 issuer_kid: None,
4061 policy_digest: None,
4062 correlation_id: None,
4063 observed_at: "2026-05-01T12:35:00Z".into(),
4064 };
4065 let v = dns_query_data_v1(&ev).unwrap();
4066 let obj = v.as_object().unwrap();
4067 assert!(!obj.contains_key("upstreamResolverId"));
4068 assert!(!obj.contains_key("upstreamLatencyMs"));
4069 assert!(!obj.contains_key("keysetId"));
4070 assert!(!obj.contains_key("issuerKid"));
4071 assert!(!obj.contains_key("policyDigest"));
4072 assert!(!obj.contains_key("correlationId"));
4073 assert_eq!(v["decision"], "deny");
4074 assert_eq!(v["reasonCode"], "denied_not_in_allowlist");
4075 assert_eq!(v["responseRcode"], 5);
4076 }
4077
4078 #[test]
4079 fn cloud_event_v1_dns_query_envelope() {
4080 use crate::{DnsQueryDecision, DnsQueryEvent, DnsQueryReasonCode, DnsQueryType};
4081 let ev = DnsQueryEvent {
4082 schema_version: "1.0.0".into(),
4083 cell_id: "c1".into(),
4084 run_id: "r1".into(),
4085 query_id: "q1".into(),
4086 query_name: "api.example.com".into(),
4087 query_type: DnsQueryType::A,
4088 decision: DnsQueryDecision::Allow,
4089 reason_code: DnsQueryReasonCode::AllowedByAllowlist,
4090 response_rcode: Some(0),
4091 upstream_resolver_id: Some("r-001".into()),
4092 upstream_latency_ms: Some(3),
4093 response_target_count: Some(1),
4094 keyset_id: None,
4095 issuer_kid: None,
4096 policy_digest: None,
4097 correlation_id: None,
4098 observed_at: "2026-05-01T12:34:56Z".into(),
4099 };
4100 let env =
4101 cloud_event_v1_dns_query("cellos-dns-proxy", "2026-05-01T12:34:56Z", &ev).unwrap();
4102 assert_eq!(env.specversion, "1.0");
4103 assert_eq!(env.ty, "dev.cellos.events.cell.observability.v1.dns_query");
4104 assert_eq!(env.source, "cellos-dns-proxy");
4105 assert_eq!(env.datacontenttype.as_deref(), Some("application/json"));
4106 assert!(env.data.is_some());
4107 }
4108
4109 #[test]
4110 fn qtype_mapping_covers_phase1_set() {
4111 use crate::{qtype_to_dns_query_type, DnsQueryType};
4112 assert_eq!(qtype_to_dns_query_type(1), Some(DnsQueryType::A));
4113 assert_eq!(qtype_to_dns_query_type(2), Some(DnsQueryType::NS));
4114 assert_eq!(qtype_to_dns_query_type(5), Some(DnsQueryType::CNAME));
4115 assert_eq!(qtype_to_dns_query_type(12), Some(DnsQueryType::PTR));
4116 assert_eq!(qtype_to_dns_query_type(15), Some(DnsQueryType::MX));
4117 assert_eq!(qtype_to_dns_query_type(16), Some(DnsQueryType::TXT));
4118 assert_eq!(qtype_to_dns_query_type(28), Some(DnsQueryType::AAAA));
4119 assert_eq!(qtype_to_dns_query_type(33), Some(DnsQueryType::SRV));
4120 assert_eq!(qtype_to_dns_query_type(64), Some(DnsQueryType::SVCB));
4121 assert_eq!(qtype_to_dns_query_type(65), Some(DnsQueryType::HTTPS));
4122 assert_eq!(qtype_to_dns_query_type(0), None);
4124 assert_eq!(qtype_to_dns_query_type(99), None);
4125 assert_eq!(qtype_to_dns_query_type(255), None); }
4127
4128 #[test]
4129 fn l7_egress_decision_matches_example_shape() {
4130 let raw = include_str!(
4131 "../../../contracts/examples/cell-observability-l7-egress-decision-data.valid.json"
4132 );
4133 let expected: Value = serde_json::from_str(raw).unwrap();
4134 let spec = ExecutionCellSpec {
4135 id: "demo-cell-dns".into(),
4136 correlation: None,
4137 ingress: None,
4138 environment: None,
4139 placement: None,
4140 policy: None,
4141 identity: None,
4142 run: None,
4143 authority: Default::default(),
4144 lifetime: Lifetime { ttl_seconds: 60 },
4145 export: None,
4146 telemetry: None,
4147 };
4148 let data = observability_l7_egress_decision_data_v1(
4149 &spec,
4150 "demo-cell-dns",
4151 Some("run-001"),
4152 "l7-demo-0002",
4153 "deny",
4154 "blocked.example.com",
4155 "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
4156 "keyset-demo-001",
4157 "kid-l7-01",
4158 "deny_default",
4159 Some("authority.egressRules.default"),
4160 None, )
4162 .unwrap();
4163 assert_eq!(data, expected);
4164 }
4165
4166 #[test]
4167 fn l7_egress_decision_with_stream_id_emits_field() {
4168 let spec = ExecutionCellSpec {
4169 id: "demo-cell-dns".into(),
4170 correlation: None,
4171 ingress: None,
4172 environment: None,
4173 placement: None,
4174 policy: None,
4175 identity: None,
4176 run: None,
4177 authority: Default::default(),
4178 lifetime: Lifetime { ttl_seconds: 60 },
4179 export: None,
4180 telemetry: None,
4181 };
4182 let data = observability_l7_egress_decision_data_v1(
4183 &spec,
4184 "demo-cell-dns",
4185 Some("run-001"),
4186 "l7-demo-0003",
4187 "deny",
4188 "evil.example.com",
4189 "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
4190 "keyset-demo-001",
4191 "kid-l7-01",
4192 "l7_h2_authority_allowlist_miss",
4193 None,
4194 Some(3), )
4196 .unwrap();
4197 assert_eq!(data["streamId"], serde_json::json!(3));
4198 }
4199
4200 fn _seam_g1_g2_minimal_spec(id: &str) -> ExecutionCellSpec {
4203 ExecutionCellSpec {
4204 id: id.into(),
4205 correlation: None,
4206 ingress: None,
4207 environment: None,
4208 placement: None,
4209 policy: None,
4210 identity: None,
4211 run: None,
4212 authority: Default::default(),
4213 lifetime: Lifetime { ttl_seconds: 60 },
4214 export: None,
4215 telemetry: None,
4216 }
4217 }
4218
4219 #[test]
4220 fn seam_g2_identity_revoked_includes_provenance_when_set() {
4221 let mut spec = _seam_g1_g2_minimal_spec("seam-g2-revoke");
4223 spec.identity = Some(WorkloadIdentity {
4224 kind: WorkloadIdentityKind::FederatedOidc,
4225 provider: "github-actions".into(),
4226 audience: "sts.amazonaws.com".into(),
4227 subject: None,
4228 ttl_seconds: Some(900),
4229 secret_ref: "AWS_WEB_IDENTITY".into(),
4230 });
4231 let identity = spec.identity.as_ref().unwrap();
4232 let prov = Provenance {
4233 parent: "urn:cellos:event:00000000-0000-0000-0000-00000000abcd".into(),
4234 parent_type: "dev.cellos.events.cell.lifecycle.v1.started".into(),
4235 };
4236 let data = identity_revoked_data_v1(
4237 &spec,
4238 "cell-seam-g2",
4239 Some("run-seam-g2"),
4240 identity,
4241 Some("teardown"),
4242 Some(&prov),
4243 )
4244 .unwrap();
4245 assert_eq!(
4246 data["provenance"]["parent"],
4247 "urn:cellos:event:00000000-0000-0000-0000-00000000abcd"
4248 );
4249 assert_eq!(
4250 data["provenance"]["parentType"],
4251 "dev.cellos.events.cell.lifecycle.v1.started"
4252 );
4253 }
4254
4255 #[test]
4256 fn seam_g2_identity_revoked_omits_provenance_when_none() {
4257 let mut spec = _seam_g1_g2_minimal_spec("seam-g2-revoke-no-prov");
4260 spec.identity = Some(WorkloadIdentity {
4261 kind: WorkloadIdentityKind::FederatedOidc,
4262 provider: "github-actions".into(),
4263 audience: "sts.amazonaws.com".into(),
4264 subject: None,
4265 ttl_seconds: Some(900),
4266 secret_ref: "AWS_WEB_IDENTITY".into(),
4267 });
4268 let identity = spec.identity.as_ref().unwrap();
4269 let data =
4270 identity_revoked_data_v1(&spec, "cell-x", None, identity, Some("teardown"), None)
4271 .unwrap();
4272 assert!(!data.as_object().unwrap().contains_key("provenance"));
4273 }
4274
4275 #[test]
4276 fn seam_g2_export_completed_v2_includes_provenance_when_set() {
4277 let spec = _seam_g1_g2_minimal_spec("seam-g2-export");
4279 let receipt = ExportReceipt {
4280 target_kind: ExportReceiptTargetKind::Local,
4281 target_name: None,
4282 destination: "/tmp/out/run-1/artifact.json".into(),
4283 bytes_written: 42,
4284 };
4285 let prov = Provenance {
4286 parent: "urn:cellos:event:11111111-1111-1111-1111-111111111111".into(),
4287 parent_type: "dev.cellos.events.cell.lifecycle.v1.started".into(),
4288 };
4289 let data = export_completed_data_v2(
4290 &spec,
4291 "cell-export",
4292 Some("run-export"),
4293 "artifact",
4294 &receipt,
4295 Some(&prov),
4296 )
4297 .unwrap();
4298 assert_eq!(
4299 data["provenance"]["parent"],
4300 "urn:cellos:event:11111111-1111-1111-1111-111111111111"
4301 );
4302 assert_eq!(
4303 data["provenance"]["parentType"],
4304 "dev.cellos.events.cell.lifecycle.v1.started"
4305 );
4306 }
4307
4308 #[test]
4309 fn seam_g2_export_failed_v2_includes_provenance_when_set() {
4310 let spec = _seam_g1_g2_minimal_spec("seam-g2-export-failed");
4312 let prov = Provenance {
4313 parent: "urn:cellos:event:22222222-2222-2222-2222-222222222222".into(),
4314 parent_type: "dev.cellos.events.cell.lifecycle.v1.started".into(),
4315 };
4316 let data = export_failed_data_v2(
4317 &spec,
4318 "cell-fail",
4319 Some("run-fail"),
4320 "artifact",
4321 ExportReceiptTargetKind::S3,
4322 Some("bucket"),
4323 Some("s3://bucket/artifact"),
4324 "denied by policy",
4325 Some(&prov),
4326 )
4327 .unwrap();
4328 assert_eq!(
4329 data["provenance"]["parent"],
4330 "urn:cellos:event:22222222-2222-2222-2222-222222222222"
4331 );
4332 assert_eq!(data["reason"], "denied by policy");
4333 }
4334
4335 #[test]
4336 fn seam_g1_correlation_id_propagates_when_present_in_spec() {
4337 let mut spec = _seam_g1_g2_minimal_spec("seam-g1-corr");
4343 spec.correlation = Some(Correlation {
4344 platform: None,
4345 external_run_id: None,
4346 external_job_id: None,
4347 tenant_id: None,
4348 labels: None,
4349 correlation_id: Some("urn:tsafe:corr:01J".into()),
4350 });
4351 spec.identity = Some(WorkloadIdentity {
4352 kind: WorkloadIdentityKind::FederatedOidc,
4353 provider: "github-actions".into(),
4354 audience: "sts.amazonaws.com".into(),
4355 subject: None,
4356 ttl_seconds: Some(900),
4357 secret_ref: "AWS_WEB_IDENTITY".into(),
4358 });
4359 let identity = spec.identity.as_ref().unwrap();
4360
4361 let data =
4363 identity_revoked_data_v1(&spec, "cell-1", Some("r"), identity, Some("teardown"), None)
4364 .unwrap();
4365 assert_eq!(
4366 data["correlation"]["correlationId"], "urn:tsafe:corr:01J",
4367 "identity.revoked must mirror correlationId from spec"
4368 );
4369
4370 let receipt = ExportReceipt {
4372 target_kind: ExportReceiptTargetKind::Local,
4373 target_name: None,
4374 destination: "/tmp/x".into(),
4375 bytes_written: 1,
4376 };
4377 let data = export_completed_data_v2(&spec, "c", Some("r"), "art", &receipt, None).unwrap();
4378 assert_eq!(
4379 data["correlation"]["correlationId"], "urn:tsafe:corr:01J",
4380 "export.v2.completed must mirror correlationId from spec"
4381 );
4382
4383 let data = export_failed_data_v2(
4385 &spec,
4386 "c",
4387 Some("r"),
4388 "art",
4389 ExportReceiptTargetKind::Local,
4390 None,
4391 None,
4392 "boom",
4393 None,
4394 )
4395 .unwrap();
4396 assert_eq!(
4397 data["correlation"]["correlationId"], "urn:tsafe:corr:01J",
4398 "export.v2.failed must mirror correlationId from spec"
4399 );
4400
4401 let data =
4403 command_completed_data_v1(&spec, "c", Some("r"), &["echo".to_string()], 0, 5, None)
4404 .unwrap();
4405 assert_eq!(
4406 data["correlation"]["correlationId"], "urn:tsafe:corr:01J",
4407 "command.completed must mirror correlationId from spec"
4408 );
4409
4410 let data = compliance_summary_data_v1(&spec, "c", Some("r"), Some(0)).unwrap();
4412 assert_eq!(
4413 data["correlation"]["correlationId"], "urn:tsafe:corr:01J",
4414 "compliance.summary must mirror correlationId from spec"
4415 );
4416 }
4417
4418 #[test]
4427 fn subject_urn_accepts_canonical_cell_form() {
4428 let urn = SubjectUrn::parse("urn:cellos:cell:abc-123").expect("must parse");
4430 assert_eq!(urn.as_str(), "urn:cellos:cell:abc-123");
4431 }
4432
4433 #[test]
4434 fn subject_urn_accepts_id_with_internal_colons() {
4435 let urn = SubjectUrn::parse("urn:cellos:event:abc:01j").expect("must parse");
4438 assert_eq!(urn.as_str(), "urn:cellos:event:abc:01j");
4439 }
4440
4441 #[test]
4442 fn subject_urn_rejects_when_no_urn_scheme() {
4443 let err = SubjectUrn::parse("cell:abc-123").unwrap_err();
4445 assert_eq!(err, SubjectUrnError::MissingUrnScheme);
4446 }
4447
4448 #[test]
4449 fn subject_urn_rejects_three_segment_form() {
4450 let err = SubjectUrn::parse("urn:cellos:cell").unwrap_err();
4452 assert_eq!(err, SubjectUrnError::TooFewSegments);
4453 }
4454
4455 #[test]
4456 fn subject_urn_rejects_empty_id() {
4457 let err = SubjectUrn::parse("urn:cellos:cell:").unwrap_err();
4459 assert_eq!(err, SubjectUrnError::EmptySegment);
4460 }
4461
4462 #[test]
4463 fn subject_urn_rejects_uppercase_tool_or_kind() {
4464 let err = SubjectUrn::parse("urn:CellOS:cell:abc-123").unwrap_err();
4466 assert_eq!(err, SubjectUrnError::InvalidToolOrKindCharset);
4467 }
4468
4469 #[test]
4470 fn subject_urn_rejects_embedded_whitespace() {
4471 let err = SubjectUrn::parse("urn:cellos:cell:abc 123").unwrap_err();
4473 assert_eq!(err, SubjectUrnError::ControlOrWhitespace);
4474 }
4475
4476 #[test]
4477 fn subject_urn_rejects_empty_tool_segment() {
4478 let err = SubjectUrn::parse("urn::cell:abc-123").unwrap_err();
4480 assert_eq!(err, SubjectUrnError::EmptySegment);
4481 }
4482
4483 #[test]
4484 fn cell_subject_urn_helper_round_trips() {
4485 let urn = cell_subject_urn("cell-host-7").expect("helper must accept ASCII id");
4487 assert_eq!(urn.as_str(), "urn:cellos:cell:cell-host-7");
4488 let reparsed = SubjectUrn::parse(urn.as_str()).expect("must reparse");
4490 assert_eq!(reparsed, urn);
4491 }
4492
4493 #[test]
4494 fn cell_subject_urn_helper_rejects_empty_id() {
4495 let err = cell_subject_urn("").unwrap_err();
4496 assert_eq!(err, SubjectUrnError::EmptySegment);
4497 }
4498
4499 #[test]
4502 fn formation_data_v1_shape_happy_path() {
4503 let data = formation_data_v1("f-123", "demo-formation", 3, &[], None);
4504 assert_eq!(data["formationId"], json!("f-123"));
4505 assert_eq!(data["formationName"], json!("demo-formation"));
4506 assert_eq!(data["cellCount"], json!(3));
4507 assert_eq!(data["failedCellIds"], json!([] as [String; 0]));
4508 let obj = data.as_object().unwrap();
4510 assert!(!obj.contains_key("reason"));
4511 }
4512
4513 #[test]
4514 fn formation_data_v1_shape_degraded_path_includes_failed_cells_and_reason() {
4515 let failed = vec!["cell-a".to_string(), "cell-b".to_string()];
4516 let data = formation_data_v1(
4517 "f-123",
4518 "demo-formation",
4519 5,
4520 &failed,
4521 Some("2/5 cells exited non-zero"),
4522 );
4523 assert_eq!(data["failedCellIds"], json!(failed));
4524 assert_eq!(data["reason"], json!("2/5 cells exited non-zero"));
4525 }
4526
4527 #[test]
4528 fn formation_created_envelope_carries_correct_urn() {
4529 let ev = cloud_event_v1_formation_created(
4530 "cellos-supervisor",
4531 "2026-05-16T00:00:00Z",
4532 "f-1",
4533 "demo",
4534 2,
4535 &[],
4536 None,
4537 );
4538 assert_eq!(ev.ty, "dev.cellos.events.cell.formation.v1.created");
4539 assert_eq!(ev.specversion, "1.0");
4540 assert_eq!(ev.source, "cellos-supervisor");
4541 assert!(ev.data.is_some());
4542 }
4543
4544 #[test]
4545 fn formation_launching_envelope_carries_correct_urn() {
4546 let ev = cloud_event_v1_formation_launching(
4547 "cellos-supervisor",
4548 "2026-05-16T00:00:00Z",
4549 "f-1",
4550 "demo",
4551 2,
4552 &[],
4553 None,
4554 );
4555 assert_eq!(ev.ty, "dev.cellos.events.cell.formation.v1.launching");
4556 }
4557
4558 #[test]
4559 fn formation_running_envelope_carries_correct_urn() {
4560 let ev = cloud_event_v1_formation_running(
4561 "cellos-supervisor",
4562 "2026-05-16T00:00:00Z",
4563 "f-1",
4564 "demo",
4565 2,
4566 &[],
4567 None,
4568 );
4569 assert_eq!(ev.ty, "dev.cellos.events.cell.formation.v1.running");
4570 }
4571
4572 #[test]
4573 fn formation_degraded_envelope_carries_correct_urn_and_failed_cells() {
4574 let failed = vec!["cell-a".to_string()];
4575 let ev = cloud_event_v1_formation_degraded(
4576 "cellos-supervisor",
4577 "2026-05-16T00:00:00Z",
4578 "f-1",
4579 "demo",
4580 3,
4581 &failed,
4582 Some("one cell exited 1"),
4583 );
4584 assert_eq!(ev.ty, "dev.cellos.events.cell.formation.v1.degraded");
4585 let data = ev.data.unwrap();
4586 assert_eq!(data["failedCellIds"], json!(failed));
4587 assert_eq!(data["reason"], json!("one cell exited 1"));
4588 }
4589
4590 #[test]
4591 fn formation_completed_envelope_carries_correct_urn() {
4592 let ev = cloud_event_v1_formation_completed(
4593 "cellos-supervisor",
4594 "2026-05-16T00:00:00Z",
4595 "f-1",
4596 "demo",
4597 2,
4598 &[],
4599 None,
4600 );
4601 assert_eq!(ev.ty, "dev.cellos.events.cell.formation.v1.completed");
4602 }
4603
4604 #[test]
4605 fn formation_failed_envelope_carries_correct_urn_and_reason() {
4606 let failed = vec!["cell-a".to_string(), "cell-b".to_string()];
4607 let ev = cloud_event_v1_formation_failed(
4608 "cellos-supervisor",
4609 "2026-05-16T00:00:00Z",
4610 "f-1",
4611 "demo",
4612 2,
4613 &failed,
4614 Some("all cells exited non-zero"),
4615 );
4616 assert_eq!(ev.ty, "dev.cellos.events.cell.formation.v1.failed");
4617 let data = ev.data.unwrap();
4618 assert_eq!(data["failedCellIds"], json!(failed));
4619 assert_eq!(data["reason"], json!("all cells exited non-zero"));
4620 }
4621
4622 #[test]
4623 fn formation_type_constants_match_envelope_urns() {
4624 assert_eq!(
4625 FORMATION_CREATED_TYPE,
4626 "dev.cellos.events.cell.formation.v1.created"
4627 );
4628 assert_eq!(
4629 FORMATION_LAUNCHING_TYPE,
4630 "dev.cellos.events.cell.formation.v1.launching"
4631 );
4632 assert_eq!(
4633 FORMATION_RUNNING_TYPE,
4634 "dev.cellos.events.cell.formation.v1.running"
4635 );
4636 assert_eq!(
4637 FORMATION_DEGRADED_TYPE,
4638 "dev.cellos.events.cell.formation.v1.degraded"
4639 );
4640 assert_eq!(
4641 FORMATION_COMPLETED_TYPE,
4642 "dev.cellos.events.cell.formation.v1.completed"
4643 );
4644 assert_eq!(
4645 FORMATION_FAILED_TYPE,
4646 "dev.cellos.events.cell.formation.v1.failed"
4647 );
4648 }
4649}