1use semver::{Version, VersionReq};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::{BTreeSet, HashSet};
7
8pub mod proposal;
9pub mod usage_telemetry;
10pub mod violations;
11pub use proposal::{
12 CanonicalProposal, DEFAULT_MAX_CONCURRENT_NODES, DEFAULT_MAX_FAN_OUT, DEFAULT_MAX_JOIN_WIDTH,
13 DEFAULT_MAX_QUEUE_DEPTH, ManifestReference, MappingSource, ParallelSchedule,
14 ParallelScheduleError, ParallelScheduleErrorCode, ParallelScheduleFailure,
15 ParallelScheduleLimits, ProposalEdge, ProposalLimits, ProposalMapping, ProposalNode,
16 ProposalValidationError, ProposalValidationErrorCode, ProposalValidationFailure,
17 SnapshotDigests, WorkflowProposal, canonicalize_proposal, compute_parallel_schedule,
18 proposal_digest, proposal_snapshot_digest,
19};
20pub use usage_telemetry::{NoOpUsageTelemetrySink, UsageEvent, UsageEventKind, UsageTelemetrySink};
21pub use violations::ViolationRecord;
22
23const CAPABILITY_CONTRACT_KIND: &str = "capability_contract";
24const EVENT_CONTRACT_KIND: &str = "event_contract";
25const CONNECTOR_CONTRACT_KIND: &str = "connector_contract";
26const SUPPORTED_SCHEMA_VERSION: &str = "1.0.0";
27const GOVERNED_CONTENT_VERSION: &str = "0.1.0";
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct CapabilityContract {
31 pub kind: String,
32 pub schema_version: String,
33 pub id: String,
34 pub namespace: String,
35 pub name: String,
36 pub version: String,
37 pub lifecycle: Lifecycle,
38 pub owner: Owner,
39 pub summary: String,
40 pub description: String,
41 pub inputs: SchemaContainer,
42 pub outputs: SchemaContainer,
43 pub preconditions: Vec<Condition>,
44 pub postconditions: Vec<Condition>,
45 pub side_effects: Vec<SideEffect>,
46 pub emits: Vec<EventReference>,
47 pub consumes: Vec<EventReference>,
48 pub permissions: Vec<IdReference>,
49 pub execution: Execution,
50 pub policies: Vec<IdReference>,
51 pub dependencies: Vec<DependencyReference>,
52 pub provenance: Provenance,
53 pub evidence: Vec<ValidationEvidence>,
54 #[serde(default)]
56 pub service_type: ServiceType,
57 #[serde(default = "default_permitted_targets")]
59 pub permitted_targets: Vec<ExecutionTarget>,
60 #[serde(default)]
62 pub event_trigger: Option<String>,
63 #[serde(default)]
65 pub connector_requirements: Vec<ConnectorRequirement>,
66 #[serde(default)]
68 pub state_schema: Option<Value>,
69 #[serde(default)]
71 pub use_cases: Vec<UseCase>,
72 #[serde(default = "default_risk_metadata")]
76 pub risk: RiskMetadata,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct RiskMetadata {
86 pub effect_class: EffectClass,
87 pub determinism_class: DeterminismClass,
88 #[serde(default)]
89 pub data_flow: DataFlowPolicy,
90 pub reliability: ReliabilityMetadata,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
95#[serde(rename_all = "snake_case")]
96pub enum EffectClass {
97 PureRead,
99 StateWrite,
101 ExternalEffect,
103 IrreversibleEffect,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "snake_case")]
110pub enum DeterminismClass {
111 Deterministic,
112 ExternallyVariable,
113 ModelDerived,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120pub struct DataFlowPolicy {
121 #[serde(default)]
122 pub accepted_data_classifications: Vec<FieldDataClassification>,
123 #[serde(default)]
124 pub produced_data_classifications: Vec<FieldDataClassification>,
125 #[serde(default = "default_egress_policy")]
126 pub egress_policy: EgressPolicy,
127}
128
129impl Default for DataFlowPolicy {
130 fn default() -> Self {
131 Self {
132 accepted_data_classifications: Vec::new(),
133 produced_data_classifications: Vec::new(),
134 egress_policy: default_egress_policy(),
135 }
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142pub struct FieldDataClassification {
143 pub field_path: String,
144 pub classification: DataClassification,
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
148#[serde(rename_all = "snake_case")]
149pub enum DataClassification {
150 Public,
151 Internal,
152 Confidential,
153 Restricted,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "snake_case")]
161pub enum EgressPolicy {
162 Denied,
163 AllowedConnectors(Vec<String>),
164}
165
166fn default_egress_policy() -> EgressPolicy {
167 EgressPolicy::Denied
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
172pub struct ReliabilityMetadata {
173 pub idempotency_required: bool,
174 pub retryable: bool,
175 pub compensation_available: bool,
176}
177
178#[must_use]
183pub fn default_risk_metadata() -> RiskMetadata {
184 RiskMetadata {
185 effect_class: EffectClass::IrreversibleEffect,
186 determinism_class: DeterminismClass::ModelDerived,
187 data_flow: DataFlowPolicy::default(),
188 reliability: ReliabilityMetadata {
189 idempotency_required: true,
190 retryable: false,
191 compensation_available: false,
192 },
193 }
194}
195
196#[must_use]
201pub fn is_automatic_eligible(risk: &RiskMetadata) -> bool {
202 risk.effect_class == EffectClass::PureRead
203 && risk.determinism_class == DeterminismClass::Deterministic
204 && risk.data_flow.egress_policy == EgressPolicy::Denied
205 && !risk.reliability.idempotency_required
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
213pub struct ManifestRiskPolicy {
214 #[serde(default)]
218 pub egress_allowed_connectors: Option<Vec<String>>,
219}
220
221pub fn validate_manifest_risk_policy(
230 risk: &RiskMetadata,
231 policy: &ManifestRiskPolicy,
232) -> Result<(), ValidationFailure> {
233 let mut errors = Vec::new();
234
235 if let Some(declared) = &policy.egress_allowed_connectors {
236 match &risk.data_flow.egress_policy {
237 EgressPolicy::Denied => {
238 if !declared.is_empty() {
239 errors.push(error(
240 ValidationErrorCode::RiskPolicyWeakened,
241 "$.risk_policy.egress_allowed_connectors",
242 "manifest cannot allow egress for a capability whose contract denies all egress",
243 ));
244 }
245 }
246 EgressPolicy::AllowedConnectors(allowed) => {
247 for connector_id in declared {
248 if !allowed.contains(connector_id) {
249 errors.push(error(
250 ValidationErrorCode::RiskPolicyWeakened,
251 "$.risk_policy.egress_allowed_connectors",
252 &format!(
253 "manifest allows connector '{connector_id}' the contract's \
254 egress policy does not permit"
255 ),
256 ));
257 }
258 }
259 }
260 }
261 }
262
263 if errors.is_empty() {
264 Ok(())
265 } else {
266 Err(ValidationFailure { errors })
267 }
268}
269
270#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
272pub struct UseCase {
273 pub scenario: String,
274 pub input_example: Value,
275 pub output_example: Value,
276 pub happy: bool,
277 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub persona_ref: Option<String>,
279}
280
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
282pub struct ConnectorContract {
283 pub kind: String,
284 pub schema_version: String,
285 pub connector_id: String,
286 pub version: String,
287 pub capabilities_provided: Vec<String>,
288 pub required_config_schema: Value,
289 pub operation_envelopes: Vec<ConnectorOperationEnvelope>,
290 #[serde(default = "default_connector_targets")]
291 pub supported_placement_targets: Vec<ExecutionTarget>,
292}
293
294#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
295pub struct ConnectorOperationEnvelope {
296 pub operation_id: String,
297 pub request_schema: Value,
298 pub success_schema: Value,
299 pub failure_classes: Vec<String>,
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
303pub struct ConnectorRequirement {
304 pub connector_id: String,
305 pub version: String,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
309pub struct ConnectorInvocation {
310 pub capability_id: String,
311 pub connector_id: String,
312 pub config: Value,
313 pub input: Value,
314}
315
316#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
317pub struct ConnectorOutput {
318 pub output: Value,
319}
320
321#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
322pub struct ConnectorError {
323 pub code: String,
324 pub message: String,
325}
326
327pub trait ConnectorPlugin: Send + Sync {
328 fn connector_id(&self) -> &str;
329 fn version(&self) -> &str;
330 fn capabilities_provided(&self) -> &[String];
331 fn invoke(&self, invocation: ConnectorInvocation) -> Result<ConnectorOutput, ConnectorError>;
337}
338
339#[must_use]
340#[allow(clippy::too_many_lines)]
341pub fn reference_connector_contracts() -> Vec<ConnectorContract> {
342 vec![
343 reference_connector_contract(
344 "traverse.http",
345 vec!["traverse.http.outbound".to_string()],
346 serde_json::json!({
347 "type": "object",
348 "required": ["base_url"],
349 "properties": {
350 "base_url": {"type": "string"}
351 },
352 "additionalProperties": false
353 }),
354 vec![connector_operation_envelope(
355 "request",
356 serde_json::json!({
357 "type": "object",
358 "required": ["method", "resource_ref", "idempotency_key"],
359 "properties": {
360 "method": {"type": "string"},
361 "resource_ref": {"type": "string"},
362 "headers_ref": {"type": "string"},
363 "body_ref": {"type": "string"},
364 "idempotency_key": {"type": "string"}
365 },
366 "additionalProperties": false
367 }),
368 serde_json::json!({
369 "type": "object",
370 "required": ["status", "body_ref", "result_class"],
371 "properties": {
372 "status": {"type": "integer"},
373 "body_ref": {"type": "string"},
374 "result_class": {"type": "string"}
375 },
376 "additionalProperties": false
377 }),
378 vec!["configuration", "transport", "authorization", "timeout"],
379 )],
380 ),
381 reference_connector_contract(
382 "traverse.fs.read",
383 vec!["traverse.fs.read".to_string()],
384 serde_json::json!({
385 "type": "object",
386 "required": ["root"],
387 "properties": {
388 "root": {"type": "string"}
389 },
390 "additionalProperties": false
391 }),
392 vec![connector_operation_envelope(
393 "read",
394 serde_json::json!({
395 "type": "object",
396 "required": ["resource_ref", "idempotency_key"],
397 "properties": {
398 "resource_ref": {"type": "string"},
399 "max_bytes": {"type": "integer"},
400 "idempotency_key": {"type": "string"}
401 },
402 "additionalProperties": false
403 }),
404 serde_json::json!({
405 "type": "object",
406 "required": ["content_ref", "content_digest", "size", "result_class"],
407 "properties": {
408 "content_ref": {"type": "string"},
409 "content_digest": {"type": "string"},
410 "size": {"type": "integer"},
411 "result_class": {"type": "string"}
412 },
413 "additionalProperties": false
414 }),
415 vec!["configuration", "not_found", "authorization", "too_large"],
416 )],
417 ),
418 reference_connector_contract(
419 "traverse.env",
420 vec!["traverse.env.read".to_string()],
421 serde_json::json!({
422 "type": "object",
423 "required": ["allowed_keys"],
424 "properties": {
425 "allowed_keys": {
426 "type": "array",
427 "items": {"type": "string"}
428 }
429 },
430 "additionalProperties": false
431 }),
432 vec![connector_operation_envelope(
433 "read",
434 serde_json::json!({
435 "type": "object",
436 "required": ["key_ref", "idempotency_key"],
437 "properties": {
438 "key_ref": {"type": "string"},
439 "idempotency_key": {"type": "string"}
440 },
441 "additionalProperties": false
442 }),
443 serde_json::json!({
444 "type": "object",
445 "required": ["value_ref", "result_class"],
446 "properties": {
447 "value_ref": {"type": "string"},
448 "result_class": {"type": "string"}
449 },
450 "additionalProperties": false
451 }),
452 vec!["configuration", "not_found", "authorization"],
453 )],
454 ),
455 reference_connector_contract(
456 "traverse.object-store",
457 vec!["traverse.object_store.put".to_string()],
458 serde_json::json!({
459 "type": "object",
460 "required": ["authority_ref"],
461 "properties": {
462 "authority_ref": {"type": "string"},
463 "retention_classes": {
464 "type": "array",
465 "items": {"type": "string"}
466 }
467 },
468 "additionalProperties": false
469 }),
470 vec![connector_operation_envelope(
471 "put_immutable",
472 serde_json::json!({
473 "type": "object",
474 "required": ["content_ref", "media_type", "idempotency_key"],
475 "properties": {
476 "content_ref": {"type": "string"},
477 "media_type": {"type": "string"},
478 "retention_class": {"type": "string"},
479 "idempotency_key": {"type": "string"}
480 },
481 "additionalProperties": false
482 }),
483 serde_json::json!({
484 "type": "object",
485 "required": ["asset_ref", "content_digest", "size", "result_class"],
486 "properties": {
487 "asset_ref": {"type": "string"},
488 "content_digest": {"type": "string"},
489 "size": {"type": "integer"},
490 "result_class": {"type": "string"}
491 },
492 "additionalProperties": false
493 }),
494 vec!["configuration", "authorization", "quota", "integrity"],
495 )],
496 ),
497 reference_connector_contract(
498 "traverse.state-store",
499 vec!["traverse.state_store.append".to_string()],
500 serde_json::json!({
501 "type": "object",
502 "required": ["authority_ref"],
503 "properties": {
504 "authority_ref": {"type": "string"},
505 "record_type_namespace": {"type": "string"}
506 },
507 "additionalProperties": false
508 }),
509 vec![connector_operation_envelope(
510 "append_transition",
511 serde_json::json!({
512 "type": "object",
513 "required": ["record_refs", "transition", "idempotency_key"],
514 "properties": {
515 "record_refs": {
516 "type": "array",
517 "items": {"type": "string"}
518 },
519 "transition": {"type": "object"},
520 "expected_version": {"type": "integer"},
521 "idempotency_key": {"type": "string"}
522 },
523 "additionalProperties": false
524 }),
525 serde_json::json!({
526 "type": "object",
527 "required": ["result_ref", "version", "replay", "result_class"],
528 "properties": {
529 "result_ref": {"type": "string"},
530 "version": {"type": "integer"},
531 "replay": {"type": "boolean"},
532 "result_class": {"type": "string"}
533 },
534 "additionalProperties": false
535 }),
536 vec!["configuration", "authorization", "conflict", "integrity"],
537 )],
538 ),
539 reference_connector_contract(
540 "traverse.scheduler",
541 vec!["traverse.scheduler.schedule".to_string()],
542 serde_json::json!({
543 "type": "object",
544 "required": ["authority_ref"],
545 "properties": {
546 "authority_ref": {"type": "string"},
547 "allowed_job_kinds": {
548 "type": "array",
549 "items": {"type": "string"}
550 }
551 },
552 "additionalProperties": false
553 }),
554 vec![connector_operation_envelope(
555 "schedule_invocation",
556 serde_json::json!({
557 "type": "object",
558 "required": ["job_kind", "calendar_policy_ref", "logical_deadline", "idempotency_key"],
559 "properties": {
560 "job_kind": {"type": "string"},
561 "calendar_policy_ref": {"type": "string"},
562 "logical_deadline": {"type": "string"},
563 "cancellation_ref": {"type": "string"},
564 "idempotency_key": {"type": "string"}
565 },
566 "additionalProperties": false
567 }),
568 serde_json::json!({
569 "type": "object",
570 "required": ["invocation_ref", "idempotency_key", "result_class"],
571 "properties": {
572 "invocation_ref": {"type": "string"},
573 "idempotency_key": {"type": "string"},
574 "result_class": {"type": "string"}
575 },
576 "additionalProperties": false
577 }),
578 vec!["configuration", "authorization", "deadline", "quota"],
579 )],
580 ),
581 ]
582}
583
584fn reference_connector_contract(
585 connector_id: &str,
586 capabilities_provided: Vec<String>,
587 required_config_schema: Value,
588 operation_envelopes: Vec<ConnectorOperationEnvelope>,
589) -> ConnectorContract {
590 ConnectorContract {
591 kind: CONNECTOR_CONTRACT_KIND.to_string(),
592 schema_version: SUPPORTED_SCHEMA_VERSION.to_string(),
593 connector_id: connector_id.to_string(),
594 version: "1.0.0".to_string(),
595 capabilities_provided,
596 required_config_schema,
597 operation_envelopes,
598 supported_placement_targets: default_connector_targets(),
599 }
600}
601
602fn connector_operation_envelope(
603 operation_id: &str,
604 request_schema: Value,
605 success_schema: Value,
606 failure_classes: Vec<&str>,
607) -> ConnectorOperationEnvelope {
608 ConnectorOperationEnvelope {
609 operation_id: operation_id.to_string(),
610 request_schema,
611 success_schema,
612 failure_classes: failure_classes
613 .into_iter()
614 .map(std::string::ToString::to_string)
615 .collect(),
616 }
617}
618
619#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
620pub struct EventContract {
621 pub kind: String,
622 pub schema_version: String,
623 pub id: String,
624 pub namespace: String,
625 pub name: String,
626 pub version: String,
627 pub lifecycle: Lifecycle,
628 pub owner: Owner,
629 pub summary: String,
630 pub description: String,
631 pub payload: EventPayload,
632 pub classification: EventClassification,
633 pub publishers: Vec<CapabilityReference>,
634 pub subscribers: Vec<CapabilityReference>,
635 pub policies: Vec<IdReference>,
636 pub tags: Vec<String>,
637 pub provenance: EventProvenance,
638 pub evidence: Vec<EventValidationEvidence>,
639}
640
641#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
642pub struct EventPayload {
643 pub schema: Value,
644 pub compatibility: PayloadCompatibility,
645}
646
647#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
648#[serde(rename_all = "kebab-case")]
649pub enum PayloadCompatibility {
650 BackwardCompatible,
651 ForwardCompatible,
652 Breaking,
653}
654
655#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
656pub struct EventClassification {
657 pub domain: String,
658 pub bounded_context: String,
659 pub event_type: EventType,
660 pub tags: Vec<String>,
661}
662
663#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
664#[serde(rename_all = "snake_case")]
665pub enum EventType {
666 Domain,
667 Integration,
668 System,
669}
670
671#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
672pub struct CapabilityReference {
673 pub capability_id: String,
674 pub version: String,
675}
676
677#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
678pub struct EventProvenance {
679 pub source: EventProvenanceSource,
680 pub author: String,
681 pub created_at: String,
682}
683
684#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
685#[serde(rename_all = "kebab-case")]
686pub enum EventProvenanceSource {
687 Greenfield,
688 Brownfield,
689 AiGenerated,
690 Extracted,
691}
692
693#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
694pub struct EventValidationEvidence {
695 pub kind: String,
696 pub r#ref: String,
697}
698
699#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
700#[serde(rename_all = "snake_case")]
701pub enum Lifecycle {
702 Draft,
703 Active,
704 Deprecated,
705 Retired,
706 Archived,
707}
708
709impl Lifecycle {
710 #[must_use]
711 pub fn is_runtime_eligible(&self) -> bool {
712 matches!(self, Self::Active | Self::Deprecated)
713 }
714}
715
716#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
717pub struct Owner {
718 pub team: String,
719 pub contact: String,
720}
721
722#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
723pub struct SchemaContainer {
724 pub schema: Value,
725}
726
727#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
728pub struct Condition {
729 pub id: String,
730 pub description: String,
731}
732
733#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
734pub struct SideEffect {
735 pub kind: SideEffectKind,
736 pub description: String,
737}
738
739#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
740#[serde(rename_all = "snake_case")]
741pub enum SideEffectKind {
742 None,
743 MemoryOnly,
744 EventEmission,
745 ExternalCall,
746 StateChange,
747}
748
749#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
750pub struct EventReference {
751 pub event_id: String,
752 pub version: String,
753}
754
755#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
756pub struct IdReference {
757 pub id: String,
758}
759
760#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
761pub struct Execution {
762 pub binary_format: BinaryFormat,
763 pub entrypoint: Entrypoint,
764 pub preferred_targets: Vec<ExecutionTarget>,
765 pub constraints: ExecutionConstraints,
766}
767
768#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
769#[serde(rename_all = "snake_case")]
770pub enum BinaryFormat {
771 Wasm,
772}
773
774#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
776#[serde(rename_all = "snake_case")]
777pub enum ServiceType {
778 #[default]
780 Stateless,
781 Subscribable,
783 Stateful,
785}
786
787fn default_permitted_targets() -> Vec<ExecutionTarget> {
788 vec![
789 ExecutionTarget::Local,
790 ExecutionTarget::Browser,
791 ExecutionTarget::Edge,
792 ExecutionTarget::Cloud,
793 ExecutionTarget::Worker,
794 ExecutionTarget::Device,
795 ]
796}
797
798fn default_connector_targets() -> Vec<ExecutionTarget> {
799 vec![ExecutionTarget::Local, ExecutionTarget::Cloud]
800}
801
802#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
803pub struct Entrypoint {
804 pub kind: EntrypointKind,
805 pub command: String,
806}
807
808#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
809#[serde(rename_all = "kebab-case")]
810pub enum EntrypointKind {
811 WasiCommand,
812}
813
814#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Ord, PartialOrd)]
815#[serde(rename_all = "snake_case")]
816pub enum ExecutionTarget {
817 Local,
818 Browser,
819 Edge,
820 Cloud,
821 Worker,
822 Device,
823}
824
825#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
826pub struct ExecutionConstraints {
827 pub host_api_access: HostApiAccess,
828 pub network_access: NetworkAccess,
829 pub filesystem_access: FilesystemAccess,
830}
831
832#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
833#[serde(rename_all = "snake_case")]
834pub enum HostApiAccess {
835 None,
836 ExceptionRequired,
837}
838
839#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
840#[serde(rename_all = "snake_case")]
841pub enum NetworkAccess {
842 Forbidden,
843 Required,
844}
845
846#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
847#[serde(rename_all = "snake_case")]
848pub enum FilesystemAccess {
849 None,
850 SandboxOnly,
851}
852
853#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
854pub struct DependencyReference {
855 pub artifact_type: DependencyArtifactType,
856 pub id: String,
857 pub version: String,
858}
859
860#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
861#[serde(rename_all = "snake_case")]
862pub enum DependencyArtifactType {
863 Capability,
864 Event,
865 Policy,
866}
867
868#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
869pub struct Provenance {
870 pub source: ProvenanceSource,
871 pub author: String,
872 pub created_at: String,
873 #[serde(default)]
874 pub spec_ref: Option<String>,
875 #[serde(default)]
876 pub adr_refs: Vec<String>,
877 #[serde(default)]
878 pub exception_refs: Vec<String>,
879}
880
881#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
882#[serde(rename_all = "kebab-case")]
883pub enum ProvenanceSource {
884 Greenfield,
885 BrownfieldExtracted,
886 AiGenerated,
887 AiAssisted,
888}
889
890#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
891pub struct ValidationEvidence {
892 pub evidence_id: String,
893 #[serde(rename = "type")]
894 pub evidence_type: EvidenceType,
895 pub status: EvidenceStatus,
896}
897
898#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
899#[serde(rename_all = "snake_case")]
900pub enum EvidenceType {
901 SpecAlignment,
902 ContractValidation,
903 Compatibility,
904}
905
906#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
907#[serde(rename_all = "snake_case")]
908pub enum EvidenceStatus {
909 Passed,
910 Failed,
911 Superseded,
912}
913
914#[derive(Debug, Clone, PartialEq, Eq)]
915pub struct PublishedContractRecord {
916 pub id: String,
917 pub version: String,
918 pub governed_content_digest: String,
919 pub lifecycle: Lifecycle,
920}
921
922#[derive(Debug, Clone, PartialEq, Eq)]
923pub struct PublishedEventRecord {
924 pub id: String,
925 pub version: String,
926 pub governed_content_digest: String,
927 pub lifecycle: Lifecycle,
928}
929
930#[derive(Debug, Clone, PartialEq, Eq)]
931pub struct ValidationContext<'a> {
932 pub governing_spec: &'a str,
933 pub validator_version: &'a str,
934 pub existing_published: Option<&'a PublishedContractRecord>,
935}
936
937#[derive(Debug, Clone, PartialEq, Eq)]
938pub struct EventValidationContext<'a> {
939 pub governing_spec: &'a str,
940 pub validator_version: &'a str,
941 pub existing_published: Option<&'a PublishedEventRecord>,
942}
943
944#[derive(Debug, Clone, PartialEq, Eq)]
945pub struct ValidationResult {
946 pub normalized: CapabilityContract,
947 pub evidence: ProducedValidationEvidence,
948}
949
950#[derive(Debug, Clone, PartialEq, Eq)]
951pub struct EventValidationResult {
952 pub normalized: EventContract,
953 pub evidence: ProducedValidationEvidence,
954}
955
956#[derive(Debug, Clone, PartialEq, Eq)]
957pub struct ProducedValidationEvidence {
958 pub artifact_id: String,
959 pub artifact_version: String,
960 pub governing_spec: String,
961 pub validator_version: String,
962 pub status: EvidenceStatus,
963}
964
965#[derive(Debug, Clone, PartialEq, Eq)]
966pub struct ValidationFailure {
967 pub errors: Vec<ValidationError>,
968}
969
970#[derive(Debug, Clone, PartialEq, Eq)]
971pub struct ValidationError {
972 pub code: ValidationErrorCode,
973 pub message: String,
974 pub path: String,
975 pub severity: ErrorSeverity,
976}
977
978#[derive(Debug, Clone, PartialEq, Eq)]
979pub enum ValidationErrorCode {
980 MissingRequiredField,
981 InvalidLiteral,
982 InvalidFormat,
983 InvalidSemver,
984 InconsistentIdentity,
985 DuplicateItem,
986 InvalidCapabilityBoundary,
987 InvalidEventBoundary,
988 UnsupportedBinaryFormat,
989 UnsupportedEntrypoint,
990 PortabilityExceptionRequired,
991 ImmutableVersionConflict,
992 InvalidDependencyRef,
993 InvalidPlacementConstraint,
995 MissingEventTrigger,
997 InvalidConnectorContract,
998 InvalidConnectorRequirement,
999 RiskPolicyWeakened,
1002}
1003
1004#[derive(Debug, Clone, PartialEq, Eq)]
1005pub enum ErrorSeverity {
1006 Error,
1007}
1008
1009pub fn parse_contract(json: &str) -> Result<CapabilityContract, ValidationFailure> {
1016 serde_json::from_str::<CapabilityContract>(json).map_err(|error| ValidationFailure {
1017 errors: vec![ValidationError {
1018 code: ValidationErrorCode::InvalidFormat,
1019 message: error.to_string(),
1020 path: "$".to_string(),
1021 severity: ErrorSeverity::Error,
1022 }],
1023 })
1024}
1025
1026pub fn parse_event_contract(json: &str) -> Result<EventContract, ValidationFailure> {
1033 serde_json::from_str::<EventContract>(json).map_err(|error| ValidationFailure {
1034 errors: vec![ValidationError {
1035 code: ValidationErrorCode::InvalidFormat,
1036 message: error.to_string(),
1037 path: "$".to_string(),
1038 severity: ErrorSeverity::Error,
1039 }],
1040 })
1041}
1042
1043pub fn parse_connector_contract(json: &str) -> Result<ConnectorContract, ValidationFailure> {
1050 serde_json::from_str::<ConnectorContract>(json).map_err(|error| ValidationFailure {
1051 errors: vec![ValidationError {
1052 code: ValidationErrorCode::InvalidFormat,
1053 message: error.to_string(),
1054 path: "$".to_string(),
1055 severity: ErrorSeverity::Error,
1056 }],
1057 })
1058}
1059
1060pub fn validate_contract(
1066 mut contract: CapabilityContract,
1067 context: &ValidationContext<'_>,
1068) -> Result<ValidationResult, ValidationFailure> {
1069 let mut errors = Vec::new();
1070
1071 validate_kind(&contract, &mut errors);
1072 validate_schema_version(&contract, &mut errors);
1073 validate_identity(&contract, &mut errors);
1074 validate_semver(&contract.version, "$.version", &mut errors);
1075 validate_owner(&contract.owner, &mut errors);
1076 validate_summary(&contract.summary, "$.summary", &mut errors);
1077 validate_description(&contract.description, "$.description", &mut errors);
1078 validate_schema_container(&contract.inputs, "$.inputs.schema", &mut errors);
1079 validate_schema_container(&contract.outputs, "$.outputs.schema", &mut errors);
1080 validate_conditions(&contract.preconditions, "$.preconditions", &mut errors);
1081 validate_conditions(&contract.postconditions, "$.postconditions", &mut errors);
1082 validate_side_effects(&contract.side_effects, &mut errors);
1083 validate_event_references(&contract.emits, "$.emits", &mut errors);
1084 validate_event_references(&contract.consumes, "$.consumes", &mut errors);
1085 validate_id_references(&contract.permissions, "$.permissions", &mut errors);
1086 validate_execution(&contract.execution, &contract.provenance, &mut errors);
1087 validate_id_references(&contract.policies, "$.policies", &mut errors);
1088 validate_dependencies(&contract.dependencies, &mut errors);
1089 validate_connector_requirements(&contract.connector_requirements, &mut errors);
1090 validate_provenance(&contract.provenance, &mut errors);
1091 validate_evidence(&contract.evidence, &mut errors);
1092 validate_boundary(&contract, &mut errors);
1093 validate_placement_constraints(&contract, &mut errors);
1094 validate_risk_metadata(&contract.risk, &mut errors);
1095 validate_published_record(&contract, context.existing_published, &mut errors);
1096
1097 if !errors.is_empty() {
1098 return Err(ValidationFailure { errors });
1099 }
1100
1101 contract.evidence.clear();
1102
1103 Ok(ValidationResult {
1104 evidence: ProducedValidationEvidence {
1105 artifact_id: contract.id.clone(),
1106 artifact_version: contract.version.clone(),
1107 governing_spec: context.governing_spec.to_string(),
1108 validator_version: context.validator_version.to_string(),
1109 status: EvidenceStatus::Passed,
1110 },
1111 normalized: contract,
1112 })
1113}
1114
1115pub fn validate_connector_contract(
1121 contract: ConnectorContract,
1122) -> Result<ConnectorContract, ValidationFailure> {
1123 let mut errors = Vec::new();
1124
1125 if contract.kind != CONNECTOR_CONTRACT_KIND {
1126 errors.push(error(
1127 ValidationErrorCode::InvalidLiteral,
1128 "$.kind",
1129 "kind must equal connector_contract",
1130 ));
1131 }
1132 if contract.schema_version != SUPPORTED_SCHEMA_VERSION {
1133 errors.push(error(
1134 ValidationErrorCode::InvalidLiteral,
1135 "$.schema_version",
1136 "schema_version must equal 1.0.0",
1137 ));
1138 }
1139 validate_non_empty(&contract.connector_id, "$.connector_id", &mut errors);
1140 validate_semver(&contract.version, "$.version", &mut errors);
1141 validate_unique_strings(
1142 &contract.capabilities_provided,
1143 "$.capabilities_provided",
1144 "capabilities_provided must be unique",
1145 &mut errors,
1146 );
1147 if contract.capabilities_provided.is_empty() {
1148 errors.push(error(
1149 ValidationErrorCode::MissingRequiredField,
1150 "$.capabilities_provided",
1151 "capabilities_provided must contain at least one capability id",
1152 ));
1153 }
1154 validate_schema_value(
1155 &contract.required_config_schema,
1156 "$.required_config_schema",
1157 &mut errors,
1158 );
1159 validate_connector_operation_envelopes(&contract.operation_envelopes, &mut errors);
1160 if contract.supported_placement_targets.is_empty() {
1161 errors.push(error(
1162 ValidationErrorCode::MissingRequiredField,
1163 "$.supported_placement_targets",
1164 "supported_placement_targets must contain at least one target",
1165 ));
1166 }
1167 let unique_targets: BTreeSet<_> = contract
1168 .supported_placement_targets
1169 .iter()
1170 .cloned()
1171 .collect();
1172 if unique_targets.len() != contract.supported_placement_targets.len() {
1173 errors.push(error(
1174 ValidationErrorCode::DuplicateItem,
1175 "$.supported_placement_targets",
1176 "supported_placement_targets must be unique",
1177 ));
1178 }
1179
1180 if !errors.is_empty() {
1181 return Err(ValidationFailure { errors });
1182 }
1183
1184 Ok(contract)
1185}
1186
1187fn validate_connector_operation_envelopes(
1188 operation_envelopes: &[ConnectorOperationEnvelope],
1189 errors: &mut Vec<ValidationError>,
1190) {
1191 if operation_envelopes.is_empty() {
1192 errors.push(error(
1193 ValidationErrorCode::MissingRequiredField,
1194 "$.operation_envelopes",
1195 "operation_envelopes must contain at least one operation",
1196 ));
1197 return;
1198 }
1199
1200 let mut seen = HashSet::new();
1201 for (index, envelope) in operation_envelopes.iter().enumerate() {
1202 let operation_id_path = format!("$.operation_envelopes[{index}].operation_id");
1203 validate_non_empty(&envelope.operation_id, &operation_id_path, errors);
1204 if !seen.insert(envelope.operation_id.clone()) {
1205 errors.push(error(
1206 ValidationErrorCode::DuplicateItem,
1207 &operation_id_path,
1208 "operation_id values must be unique",
1209 ));
1210 }
1211
1212 validate_schema_value(
1213 &envelope.request_schema,
1214 &format!("$.operation_envelopes[{index}].request_schema"),
1215 errors,
1216 );
1217 validate_schema_value(
1218 &envelope.success_schema,
1219 &format!("$.operation_envelopes[{index}].success_schema"),
1220 errors,
1221 );
1222
1223 let failure_classes_path = format!("$.operation_envelopes[{index}].failure_classes");
1224 if envelope.failure_classes.is_empty() {
1225 errors.push(error(
1226 ValidationErrorCode::MissingRequiredField,
1227 &failure_classes_path,
1228 "failure_classes must contain at least one stable failure class",
1229 ));
1230 }
1231 validate_unique_strings(
1232 &envelope.failure_classes,
1233 &failure_classes_path,
1234 "failure_classes must be unique",
1235 errors,
1236 );
1237 for (failure_index, failure_class) in envelope.failure_classes.iter().enumerate() {
1238 validate_non_empty(
1239 failure_class,
1240 &format!("$.operation_envelopes[{index}].failure_classes[{failure_index}]"),
1241 errors,
1242 );
1243 }
1244 }
1245}
1246
1247pub fn validate_event_contract(
1253 mut contract: EventContract,
1254 context: &EventValidationContext<'_>,
1255) -> Result<EventValidationResult, ValidationFailure> {
1256 let mut errors = Vec::new();
1257
1258 validate_event_kind(&contract, &mut errors);
1259 validate_event_schema_version(&contract, &mut errors);
1260 validate_event_identity(&contract, &mut errors);
1261 validate_semver(&contract.version, "$.version", &mut errors);
1262 validate_owner(&contract.owner, &mut errors);
1263 validate_summary(&contract.summary, "$.summary", &mut errors);
1264 validate_description(&contract.description, "$.description", &mut errors);
1265 validate_event_payload(&contract.payload, &mut errors);
1266 validate_event_classification(&contract.classification, &mut errors);
1267 validate_capability_references(&contract.publishers, "$.publishers", true, &mut errors);
1268 validate_capability_references(&contract.subscribers, "$.subscribers", false, &mut errors);
1269 validate_id_references(&contract.policies, "$.policies", &mut errors);
1270 validate_tags(&contract.tags, "$.tags", true, &mut errors);
1271 validate_event_provenance(&contract.provenance, &mut errors);
1272 validate_event_evidence(&contract.evidence, &mut errors);
1273 validate_event_boundary(&contract, &mut errors);
1274 validate_published_event_record(&contract, context.existing_published, &mut errors);
1275
1276 if !errors.is_empty() {
1277 return Err(ValidationFailure { errors });
1278 }
1279
1280 contract.evidence.clear();
1281
1282 Ok(EventValidationResult {
1283 evidence: ProducedValidationEvidence {
1284 artifact_id: contract.id.clone(),
1285 artifact_version: contract.version.clone(),
1286 governing_spec: context.governing_spec.to_string(),
1287 validator_version: context.validator_version.to_string(),
1288 status: EvidenceStatus::Passed,
1289 },
1290 normalized: contract,
1291 })
1292}
1293
1294#[must_use]
1295pub fn governed_content_digest(contract: &CapabilityContract) -> String {
1296 let mut clone = contract.clone();
1297 clone.evidence.clear();
1298 let json = format!("{clone:?}");
1299 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
1300 for byte in json.as_bytes() {
1301 hash ^= u64::from(*byte);
1302 hash = hash.wrapping_mul(0x0000_0001_0000_01b3);
1303 }
1304 format!("{GOVERNED_CONTENT_VERSION}:{hash:016x}")
1305}
1306
1307#[must_use]
1308pub fn governed_event_content_digest(contract: &EventContract) -> String {
1309 let mut clone = contract.clone();
1310 clone.evidence.clear();
1311 let json = format!("{clone:?}");
1312 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
1313 for byte in json.as_bytes() {
1314 hash ^= u64::from(*byte);
1315 hash = hash.wrapping_mul(0x0000_0001_0000_01b3);
1316 }
1317 format!("{GOVERNED_CONTENT_VERSION}:{hash:016x}")
1318}
1319
1320fn validate_kind(contract: &CapabilityContract, errors: &mut Vec<ValidationError>) {
1321 if contract.kind != CAPABILITY_CONTRACT_KIND {
1322 errors.push(error(
1323 ValidationErrorCode::InvalidLiteral,
1324 "$.kind",
1325 "kind must equal capability_contract",
1326 ));
1327 }
1328}
1329
1330fn validate_event_kind(contract: &EventContract, errors: &mut Vec<ValidationError>) {
1331 if contract.kind != EVENT_CONTRACT_KIND {
1332 errors.push(error(
1333 ValidationErrorCode::InvalidLiteral,
1334 "$.kind",
1335 "kind must equal event_contract",
1336 ));
1337 }
1338}
1339
1340fn validate_schema_version(contract: &CapabilityContract, errors: &mut Vec<ValidationError>) {
1341 if contract.schema_version != SUPPORTED_SCHEMA_VERSION {
1342 errors.push(error(
1343 ValidationErrorCode::InvalidLiteral,
1344 "$.schema_version",
1345 "schema_version must equal 1.0.0",
1346 ));
1347 }
1348}
1349
1350fn validate_event_schema_version(contract: &EventContract, errors: &mut Vec<ValidationError>) {
1351 if contract.schema_version != SUPPORTED_SCHEMA_VERSION {
1352 errors.push(error(
1353 ValidationErrorCode::InvalidLiteral,
1354 "$.schema_version",
1355 "schema_version must equal 1.0.0",
1356 ));
1357 }
1358}
1359
1360fn validate_identity(contract: &CapabilityContract, errors: &mut Vec<ValidationError>) {
1361 if !is_valid_namespace(&contract.namespace) {
1362 errors.push(error(
1363 ValidationErrorCode::InvalidFormat,
1364 "$.namespace",
1365 "namespace must be dot-separated lowercase kebab-case segments",
1366 ));
1367 }
1368
1369 if !is_valid_name(&contract.name) {
1370 errors.push(error(
1371 ValidationErrorCode::InvalidFormat,
1372 "$.name",
1373 "name must be lowercase kebab-case",
1374 ));
1375 }
1376
1377 let expected_id = format!("{}.{}", contract.namespace, contract.name);
1378 if contract.id != expected_id {
1379 errors.push(error(
1380 ValidationErrorCode::InconsistentIdentity,
1381 "$.id",
1382 "id must equal namespace.name",
1383 ));
1384 }
1385}
1386
1387fn validate_event_identity(contract: &EventContract, errors: &mut Vec<ValidationError>) {
1388 if !is_valid_namespace(&contract.namespace) {
1389 errors.push(error(
1390 ValidationErrorCode::InvalidFormat,
1391 "$.namespace",
1392 "namespace must be dot-separated lowercase kebab-case segments",
1393 ));
1394 }
1395
1396 if !is_valid_name(&contract.name) {
1397 errors.push(error(
1398 ValidationErrorCode::InvalidFormat,
1399 "$.name",
1400 "name must be lowercase kebab-case",
1401 ));
1402 }
1403
1404 let expected_id = format!("{}.{}", contract.namespace, contract.name);
1405 if contract.id != expected_id {
1406 errors.push(error(
1407 ValidationErrorCode::InconsistentIdentity,
1408 "$.id",
1409 "id must equal namespace.name",
1410 ));
1411 }
1412}
1413
1414fn validate_semver(value: &str, path: &str, errors: &mut Vec<ValidationError>) {
1415 if Version::parse(value).is_err() {
1416 errors.push(error(
1417 ValidationErrorCode::InvalidSemver,
1418 path,
1419 "version must match MAJOR.MINOR.PATCH",
1420 ));
1421 }
1422}
1423
1424fn validate_owner(owner: &Owner, errors: &mut Vec<ValidationError>) {
1425 validate_non_empty(&owner.team, "$.owner.team", errors);
1426 validate_non_empty(&owner.contact, "$.owner.contact", errors);
1427}
1428
1429fn validate_summary(summary: &str, path: &str, errors: &mut Vec<ValidationError>) {
1430 if summary.trim().len() < 10 || summary.len() > 200 {
1431 errors.push(error(
1432 ValidationErrorCode::InvalidFormat,
1433 path,
1434 "summary length must be between 10 and 200 characters",
1435 ));
1436 }
1437}
1438
1439fn validate_description(description: &str, path: &str, errors: &mut Vec<ValidationError>) {
1440 if description.trim().len() < 20 {
1441 errors.push(error(
1442 ValidationErrorCode::InvalidFormat,
1443 path,
1444 "description must be at least 20 characters",
1445 ));
1446 }
1447}
1448
1449fn validate_schema_container(
1450 container: &SchemaContainer,
1451 path: &str,
1452 errors: &mut Vec<ValidationError>,
1453) {
1454 validate_schema_value(&container.schema, path, errors);
1455}
1456
1457fn validate_schema_value(schema: &Value, path: &str, errors: &mut Vec<ValidationError>) {
1458 if !schema.is_object() {
1459 errors.push(error(
1460 ValidationErrorCode::InvalidFormat,
1461 path,
1462 "schema must be a JSON object",
1463 ));
1464 }
1465}
1466
1467fn validate_event_payload(payload: &EventPayload, errors: &mut Vec<ValidationError>) {
1468 if !payload.schema.is_object() {
1469 errors.push(error(
1470 ValidationErrorCode::InvalidFormat,
1471 "$.payload.schema",
1472 "schema must be a JSON object",
1473 ));
1474 }
1475}
1476
1477fn validate_event_classification(
1478 classification: &EventClassification,
1479 errors: &mut Vec<ValidationError>,
1480) {
1481 validate_min_length(
1482 &classification.domain,
1483 "$.classification.domain",
1484 2,
1485 "domain must be at least 2 characters",
1486 errors,
1487 );
1488 validate_min_length(
1489 &classification.bounded_context,
1490 "$.classification.bounded_context",
1491 2,
1492 "bounded_context must be at least 2 characters",
1493 errors,
1494 );
1495 validate_tags(&classification.tags, "$.classification.tags", true, errors);
1496}
1497
1498fn validate_capability_references(
1499 references: &[CapabilityReference],
1500 path: &str,
1501 require_one: bool,
1502 errors: &mut Vec<ValidationError>,
1503) {
1504 if require_one && references.is_empty() {
1505 errors.push(error(
1506 ValidationErrorCode::MissingRequiredField,
1507 path,
1508 "array must contain at least one item",
1509 ));
1510 }
1511
1512 let mut seen = HashSet::new();
1513 for (index, item) in references.iter().enumerate() {
1514 let id_path = format!("{path}[{index}].capability_id");
1515 let version_path = format!("{path}[{index}].version");
1516 validate_non_empty(&item.capability_id, &id_path, errors);
1517 validate_semver(&item.version, &version_path, errors);
1518 if !seen.insert((item.capability_id.clone(), item.version.clone())) {
1519 errors.push(error(
1520 ValidationErrorCode::DuplicateItem,
1521 &id_path,
1522 "capability references must be unique by id and version",
1523 ));
1524 }
1525 }
1526}
1527
1528fn validate_tags(
1529 tags: &[String],
1530 path: &str,
1531 require_one: bool,
1532 errors: &mut Vec<ValidationError>,
1533) {
1534 if require_one && tags.is_empty() {
1535 errors.push(error(
1536 ValidationErrorCode::MissingRequiredField,
1537 path,
1538 "array must contain at least one item",
1539 ));
1540 }
1541 for (index, tag) in tags.iter().enumerate() {
1542 validate_non_empty(tag, &format!("{path}[{index}]"), errors);
1543 }
1544 validate_unique_strings(tags, path, "values must be unique", errors);
1545}
1546
1547fn validate_event_provenance(provenance: &EventProvenance, errors: &mut Vec<ValidationError>) {
1548 validate_non_empty(&provenance.author, "$.provenance.author", errors);
1549 validate_non_empty(&provenance.created_at, "$.provenance.created_at", errors);
1550}
1551
1552fn validate_event_evidence(
1553 evidence: &[EventValidationEvidence],
1554 errors: &mut Vec<ValidationError>,
1555) {
1556 let mut seen = HashSet::new();
1557 for (index, item) in evidence.iter().enumerate() {
1558 let kind_path = format!("$.evidence[{index}].kind");
1559 let ref_path = format!("$.evidence[{index}].ref");
1560 validate_non_empty(&item.kind, &kind_path, errors);
1561 validate_non_empty(&item.r#ref, &ref_path, errors);
1562 if !seen.insert((item.kind.clone(), item.r#ref.clone())) {
1563 errors.push(error(
1564 ValidationErrorCode::DuplicateItem,
1565 &kind_path,
1566 "evidence entries must be unique by kind and ref",
1567 ));
1568 }
1569 }
1570}
1571
1572fn validate_conditions(conditions: &[Condition], path: &str, errors: &mut Vec<ValidationError>) {
1573 let mut seen = HashSet::new();
1574 for (index, condition) in conditions.iter().enumerate() {
1575 let id_path = format!("{path}[{index}].id");
1576 let description_path = format!("{path}[{index}].description");
1577 validate_non_empty(&condition.id, &id_path, errors);
1578 validate_non_empty(&condition.description, &description_path, errors);
1579 if !seen.insert(condition.id.clone()) {
1580 errors.push(error(
1581 ValidationErrorCode::DuplicateItem,
1582 &id_path,
1583 "condition ids must be unique",
1584 ));
1585 }
1586 }
1587}
1588
1589fn validate_side_effects(side_effects: &[SideEffect], errors: &mut Vec<ValidationError>) {
1590 if side_effects.is_empty() {
1591 errors.push(error(
1592 ValidationErrorCode::MissingRequiredField,
1593 "$.side_effects",
1594 "side_effects must contain at least one item",
1595 ));
1596 }
1597
1598 for (index, side_effect) in side_effects.iter().enumerate() {
1599 validate_non_empty(
1600 &side_effect.description,
1601 &format!("$.side_effects[{index}].description"),
1602 errors,
1603 );
1604 }
1605}
1606
1607fn validate_event_references(
1608 references: &[EventReference],
1609 path: &str,
1610 errors: &mut Vec<ValidationError>,
1611) {
1612 let mut seen = HashSet::new();
1613 for (index, item) in references.iter().enumerate() {
1614 let event_path = format!("{path}[{index}].event_id");
1615 let version_path = format!("{path}[{index}].version");
1616 validate_non_empty(&item.event_id, &event_path, errors);
1617 validate_semver(&item.version, &version_path, errors);
1618 if !seen.insert((item.event_id.clone(), item.version.clone())) {
1619 errors.push(error(
1620 ValidationErrorCode::DuplicateItem,
1621 &event_path,
1622 "event references must be unique by id and version",
1623 ));
1624 }
1625 }
1626}
1627
1628fn validate_id_references(items: &[IdReference], path: &str, errors: &mut Vec<ValidationError>) {
1629 let mut seen = HashSet::new();
1630 for (index, item) in items.iter().enumerate() {
1631 let item_path = format!("{path}[{index}].id");
1632 validate_non_empty(&item.id, &item_path, errors);
1633 if !seen.insert(item.id.clone()) {
1634 errors.push(error(
1635 ValidationErrorCode::DuplicateItem,
1636 &item_path,
1637 "ids must be unique",
1638 ));
1639 }
1640 }
1641}
1642
1643fn validate_execution(
1644 execution: &Execution,
1645 provenance: &Provenance,
1646 errors: &mut Vec<ValidationError>,
1647) {
1648 match execution.binary_format {
1649 BinaryFormat::Wasm => {}
1650 }
1651
1652 match execution.entrypoint.kind {
1653 EntrypointKind::WasiCommand => {}
1654 }
1655
1656 validate_non_empty(
1657 &execution.entrypoint.command,
1658 "$.execution.entrypoint.command",
1659 errors,
1660 );
1661
1662 if execution.preferred_targets.is_empty() {
1663 errors.push(error(
1664 ValidationErrorCode::MissingRequiredField,
1665 "$.execution.preferred_targets",
1666 "preferred_targets must contain at least one item",
1667 ));
1668 }
1669
1670 let unique_targets: BTreeSet<_> = execution.preferred_targets.iter().cloned().collect();
1671 if unique_targets.len() != execution.preferred_targets.len() {
1672 errors.push(error(
1673 ValidationErrorCode::DuplicateItem,
1674 "$.execution.preferred_targets",
1675 "preferred_targets must be unique",
1676 ));
1677 }
1678
1679 if matches!(
1680 execution.constraints.host_api_access,
1681 HostApiAccess::ExceptionRequired
1682 ) && provenance.exception_refs.is_empty()
1683 {
1684 errors.push(error(
1685 ValidationErrorCode::PortabilityExceptionRequired,
1686 "$.execution.constraints.host_api_access",
1687 "host_api_access=exception_required requires provenance.exception_refs",
1688 ));
1689 }
1690}
1691
1692fn validate_dependencies(dependencies: &[DependencyReference], errors: &mut Vec<ValidationError>) {
1693 let mut seen = HashSet::new();
1694 for (index, dependency) in dependencies.iter().enumerate() {
1695 let id_path = format!("$.dependencies[{index}].id");
1696 let version_path = format!("$.dependencies[{index}].version");
1697 validate_non_empty(&dependency.id, &id_path, errors);
1698 validate_semver(&dependency.version, &version_path, errors);
1699 if !seen.insert((
1700 dependency.artifact_type.clone(),
1701 dependency.id.clone(),
1702 dependency.version.clone(),
1703 )) {
1704 errors.push(error(
1705 ValidationErrorCode::DuplicateItem,
1706 &id_path,
1707 "dependencies must be unique by artifact_type, id, and version",
1708 ));
1709 }
1710 }
1711}
1712
1713fn validate_connector_requirements(
1714 requirements: &[ConnectorRequirement],
1715 errors: &mut Vec<ValidationError>,
1716) {
1717 let mut seen = HashSet::new();
1718 for (index, requirement) in requirements.iter().enumerate() {
1719 let id_path = format!("$.connector_requirements[{index}].connector_id");
1720 let version_path = format!("$.connector_requirements[{index}].version");
1721 validate_non_empty(&requirement.connector_id, &id_path, errors);
1722 if VersionReq::parse(&requirement.version).is_err() {
1723 errors.push(error(
1724 ValidationErrorCode::InvalidConnectorRequirement,
1725 &version_path,
1726 "connector requirement version must be a valid semver range",
1727 ));
1728 }
1729 if !seen.insert((
1730 requirement.connector_id.clone(),
1731 requirement.version.clone(),
1732 )) {
1733 errors.push(error(
1734 ValidationErrorCode::DuplicateItem,
1735 &id_path,
1736 "connector_requirements must be unique by connector_id and version",
1737 ));
1738 }
1739 }
1740}
1741
1742fn validate_provenance(provenance: &Provenance, errors: &mut Vec<ValidationError>) {
1743 validate_non_empty(&provenance.author, "$.provenance.author", errors);
1744 validate_non_empty(&provenance.created_at, "$.provenance.created_at", errors);
1745
1746 if let Some(spec_ref) = &provenance.spec_ref {
1747 validate_non_empty(spec_ref, "$.provenance.spec_ref", errors);
1748 }
1749
1750 validate_unique_strings(
1751 &provenance.adr_refs,
1752 "$.provenance.adr_refs",
1753 "adr_refs must be unique",
1754 errors,
1755 );
1756 validate_unique_strings(
1757 &provenance.exception_refs,
1758 "$.provenance.exception_refs",
1759 "exception_refs must be unique",
1760 errors,
1761 );
1762}
1763
1764fn validate_evidence(evidence: &[ValidationEvidence], errors: &mut Vec<ValidationError>) {
1765 let mut seen = HashSet::new();
1766 for (index, item) in evidence.iter().enumerate() {
1767 let id_path = format!("$.evidence[{index}].evidence_id");
1768 validate_non_empty(&item.evidence_id, &id_path, errors);
1769 if !seen.insert(item.evidence_id.clone()) {
1770 errors.push(error(
1771 ValidationErrorCode::DuplicateItem,
1772 &id_path,
1773 "evidence_id values must be unique",
1774 ));
1775 }
1776 }
1777}
1778
1779fn validate_boundary(contract: &CapabilityContract, errors: &mut Vec<ValidationError>) {
1780 let summary = contract.summary.to_ascii_lowercase();
1781 let description = contract.description.to_ascii_lowercase();
1782 let combined = format!("{summary} {description}");
1783 let banned_terms = [
1784 "utility function",
1785 "helper function",
1786 "crud wrapper",
1787 "transport handler",
1788 "database insert",
1789 "full application",
1790 "subsystem",
1791 ];
1792
1793 if banned_terms.iter().any(|term| combined.contains(term)) {
1794 errors.push(error(
1795 ValidationErrorCode::InvalidCapabilityBoundary,
1796 "$.summary",
1797 "capability must represent one meaningful business action",
1798 ));
1799 }
1800}
1801
1802fn validate_event_boundary(contract: &EventContract, errors: &mut Vec<ValidationError>) {
1803 let summary = contract.summary.to_ascii_lowercase();
1804 let description = contract.description.to_ascii_lowercase();
1805 let combined = format!("{summary} {description}");
1806 let banned_terms = [
1807 "kafka topic",
1808 "transport topic",
1809 "websocket channel",
1810 "queue binding",
1811 "broker partition",
1812 "payload wrapper",
1813 ];
1814
1815 if banned_terms.iter().any(|term| combined.contains(term)) {
1816 errors.push(error(
1817 ValidationErrorCode::InvalidEventBoundary,
1818 "$.summary",
1819 "event must describe one governed business event boundary",
1820 ));
1821 }
1822}
1823
1824fn validate_placement_constraints(
1825 contract: &CapabilityContract,
1826 errors: &mut Vec<ValidationError>,
1827) {
1828 if contract.service_type == ServiceType::Stateful
1829 && contract
1830 .permitted_targets
1831 .contains(&ExecutionTarget::Browser)
1832 {
1833 errors.push(ValidationError {
1834 code: ValidationErrorCode::InvalidPlacementConstraint,
1835 message: "Stateful capabilities cannot target Browser environments; browsers cannot \
1836 provide managed persistence guarantees."
1837 .to_string(),
1838 path: "$.permitted_targets".to_string(),
1839 severity: ErrorSeverity::Error,
1840 });
1841 }
1842 if contract.service_type == ServiceType::Subscribable
1843 && match contract.event_trigger.as_deref() {
1844 None => true,
1845 Some(event_trigger) => event_trigger.is_empty(),
1846 }
1847 {
1848 errors.push(ValidationError {
1849 code: ValidationErrorCode::MissingEventTrigger,
1850 message: "Subscribable capabilities must declare a non-empty event_trigger field."
1851 .to_string(),
1852 path: "$.event_trigger".to_string(),
1853 severity: ErrorSeverity::Error,
1854 });
1855 }
1856}
1857
1858fn validate_risk_metadata(risk: &RiskMetadata, errors: &mut Vec<ValidationError>) {
1859 validate_field_classifications(
1860 &risk.data_flow.accepted_data_classifications,
1861 "$.risk.data_flow.accepted_data_classifications",
1862 errors,
1863 );
1864 validate_field_classifications(
1865 &risk.data_flow.produced_data_classifications,
1866 "$.risk.data_flow.produced_data_classifications",
1867 errors,
1868 );
1869}
1870
1871fn validate_field_classifications(
1872 classifications: &[FieldDataClassification],
1873 path: &str,
1874 errors: &mut Vec<ValidationError>,
1875) {
1876 let mut seen = HashSet::new();
1877 for (index, item) in classifications.iter().enumerate() {
1878 let field_path = format!("{path}[{index}].field_path");
1879 validate_non_empty(&item.field_path, &field_path, errors);
1880 if !seen.insert(item.field_path.clone()) {
1881 errors.push(error(
1882 ValidationErrorCode::DuplicateItem,
1883 &field_path,
1884 "field_path values must be unique",
1885 ));
1886 }
1887 }
1888}
1889
1890fn validate_published_record(
1891 contract: &CapabilityContract,
1892 published: Option<&PublishedContractRecord>,
1893 errors: &mut Vec<ValidationError>,
1894) {
1895 let Some(published) = published else {
1896 return;
1897 };
1898
1899 if published.id != contract.id || published.version != contract.version {
1900 return;
1901 }
1902
1903 let digest = governed_content_digest(contract);
1904 if published.governed_content_digest != digest {
1905 errors.push(error(
1906 ValidationErrorCode::ImmutableVersionConflict,
1907 "$.version",
1908 "published contract versions are immutable",
1909 ));
1910 }
1911}
1912
1913fn validate_published_event_record(
1914 contract: &EventContract,
1915 published: Option<&PublishedEventRecord>,
1916 errors: &mut Vec<ValidationError>,
1917) {
1918 let Some(published) = published else {
1919 return;
1920 };
1921
1922 if published.id != contract.id || published.version != contract.version {
1923 return;
1924 }
1925
1926 let digest = governed_event_content_digest(contract);
1927 if published.governed_content_digest != digest {
1928 errors.push(error(
1929 ValidationErrorCode::ImmutableVersionConflict,
1930 "$.version",
1931 "published contract versions are immutable",
1932 ));
1933 }
1934}
1935
1936fn validate_non_empty(value: &str, path: &str, errors: &mut Vec<ValidationError>) {
1937 if value.trim().is_empty() {
1938 errors.push(error(
1939 ValidationErrorCode::MissingRequiredField,
1940 path,
1941 "value must be non-empty",
1942 ));
1943 }
1944}
1945
1946fn validate_min_length(
1947 value: &str,
1948 path: &str,
1949 min_length: usize,
1950 message: &str,
1951 errors: &mut Vec<ValidationError>,
1952) {
1953 if value.trim().len() < min_length {
1954 errors.push(error(ValidationErrorCode::InvalidFormat, path, message));
1955 }
1956}
1957
1958fn validate_unique_strings(
1959 values: &[String],
1960 path: &str,
1961 message: &str,
1962 errors: &mut Vec<ValidationError>,
1963) {
1964 let mut seen = HashSet::new();
1965 for value in values {
1966 if !seen.insert(value.clone()) {
1967 errors.push(error(ValidationErrorCode::DuplicateItem, path, message));
1968 break;
1969 }
1970 }
1971}
1972
1973fn error(code: ValidationErrorCode, path: &str, message: &str) -> ValidationError {
1974 ValidationError {
1975 code,
1976 message: message.to_string(),
1977 path: path.to_string(),
1978 severity: ErrorSeverity::Error,
1979 }
1980}
1981
1982fn is_valid_name(name: &str) -> bool {
1983 let mut parts = name.split('-');
1984 let first = parts.next().unwrap_or_default();
1985 is_valid_segment(first) && parts.all(is_valid_segment)
1986}
1987
1988fn is_valid_namespace(namespace: &str) -> bool {
1989 let mut parts = namespace.split('.');
1990 let first = parts.next().unwrap_or_default();
1991 is_valid_name(first) && parts.all(is_valid_name)
1992}
1993
1994fn is_valid_segment(segment: &str) -> bool {
1995 !segment.is_empty()
1996 && segment
1997 .chars()
1998 .all(|character| character.is_ascii_lowercase() || character.is_ascii_digit())
1999}