1use super::*;
7
8#[derive(
9 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
10)]
11#[serde(rename_all = "kebab-case")]
12pub enum OperatorEffectV1 {
13 ReadsTruth,
14 ProjectsTruth,
15 ProposesInference,
16 EmitsReceipt,
17 WidensView,
18 RepairsState,
19 SubtractsStructure,
20 ChangesPromotion,
21 ChangesSchema,
22 CrossesTrustBoundary,
23 AffectsFutureExecution,
24 AffectsUserAgency,
25 ReadsRepository,
26 WritesRepository,
27 RunsCommand,
28 GeneratesPackage,
29 ValidatesPackage,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
33pub struct OperatorContractV1 {
34 pub operator_id: String,
35 pub owner_plane: String,
36 pub input_families: Vec<String>,
37 pub output_families: Vec<String>,
38 pub allowed_effects: BTreeSet<OperatorEffectV1>,
39 pub forbidden_effects: BTreeSet<OperatorEffectV1>,
40 pub preconditions: Vec<String>,
41 pub postconditions: Vec<String>,
42 pub proof_obligations: Vec<String>,
43 pub boundary_profile: String,
44 pub replay_requirements: Vec<String>,
45 pub failure_taxonomy: Vec<String>,
46 pub human_approval: String,
47 #[serde(default, skip_serializing_if = "Vec::is_empty")]
48 pub reason_codes: Vec<String>,
49}
50
51impl OperatorContractV1 {
52 #[allow(clippy::too_many_arguments)]
53 pub fn new(
54 operator_id: impl Into<String>,
55 owner_plane: impl Into<String>,
56 input_families: Vec<&str>,
57 output_families: Vec<&str>,
58 allowed_effects: BTreeSet<OperatorEffectV1>,
59 forbidden_effects: BTreeSet<OperatorEffectV1>,
60 human_approval: impl Into<String>,
61 ) -> Self {
62 Self {
63 operator_id: operator_id.into(),
64 owner_plane: owner_plane.into(),
65 input_families: input_families.into_iter().map(str::to_string).collect(),
66 output_families: output_families.into_iter().map(str::to_string).collect(),
67 allowed_effects,
68 forbidden_effects,
69 preconditions: vec![
70 "input-manifest-present".into(),
71 "execution-context-present".into(),
72 ],
73 postconditions: vec![
74 "output-manifest-present".into(),
75 "invocation-receipt-present".into(),
76 ],
77 proof_obligations: vec!["proof-or-debt-state-declared".into()],
78 boundary_profile: "p28-v11a-strict-json-boundary-profile".into(),
79 replay_requirements: vec![
80 "stable-input-digests".into(),
81 "receipt-bearing-transition".into(),
82 ],
83 failure_taxonomy: vec![
84 "invalid-input".into(),
85 "policy-denied".into(),
86 "tool-failed".into(),
87 "proof-debt".into(),
88 "degraded-output".into(),
89 ],
90 human_approval: human_approval.into(),
91 reason_codes: vec!["operator-contract-declared".into()],
92 }
93 }
94
95 pub fn permits_effects(&self, effects: &BTreeSet<OperatorEffectV1>) -> bool {
96 effects.is_subset(&self.allowed_effects) && self.forbidden_effects.is_disjoint(effects)
97 }
98
99 pub fn failure_taxonomy_is_finite(&self) -> bool {
100 self.failure_taxonomy
101 .iter()
102 .all(|class| MATERIAL_OPERATOR_FAILURE_CLASSES.contains(&class.as_str()))
103 }
104}
105
106pub const MATERIAL_OPERATOR_FAILURE_CLASSES: &[&str] = &[
107 "invalid-input",
108 "policy-denied",
109 "tool-failed",
110 "proof-debt",
111 "degraded-output",
112];
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
115pub struct MaterialOperationRegistryV1 {
116 pub registry_id: ArtifactId,
117 pub contracts: BTreeMap<String, OperatorContractV1>,
118 pub generated_at: DateTime<Utc>,
119 #[serde(default, skip_serializing_if = "Vec::is_empty")]
120 pub reason_codes: Vec<String>,
121}
122
123impl MaterialOperationRegistryV1 {
124 pub fn new(contracts: Vec<OperatorContractV1>) -> Self {
125 let mut map = BTreeMap::new();
126 for contract in contracts {
127 map.insert(contract.operator_id.clone(), contract);
128 }
129 let material = map.keys().cloned().collect::<Vec<_>>().join("|");
130 Self {
131 registry_id: generated_artifact_id_from_material(
132 "material-operation-registry",
133 &material,
134 ),
135 contracts: map,
136 generated_at: Utc::now(),
137 reason_codes: vec!["material-operation-registry-declared".into()],
138 }
139 }
140
141 pub fn contract(&self, operator_id: &str) -> Option<&OperatorContractV1> {
142 self.contracts.get(operator_id)
143 }
144
145 pub fn authorize_effects(
146 &self,
147 operator_id: &str,
148 effects: BTreeSet<OperatorEffectV1>,
149 ) -> Result<(), String> {
150 let contract = self
151 .contract(operator_id)
152 .ok_or_else(|| format!("operator contract missing: {operator_id}"))?;
153 if contract.permits_effects(&effects) {
154 Ok(())
155 } else {
156 Err(format!(
157 "operator effect not declared or forbidden: {operator_id}"
158 ))
159 }
160 }
161
162 pub fn authorize_material_invocation(
163 &self,
164 operator_id: &str,
165 effects: BTreeSet<OperatorEffectV1>,
166 execution_context: &ExecutionContextEnvelopeV1,
167 input_manifest: &ArtifactManifestV1,
168 output_manifest: &ArtifactManifestV1,
169 receipt_refs: &[ArtifactId],
170 ) -> Result<(), String> {
171 let contract = self
172 .contract(operator_id)
173 .ok_or_else(|| format!("operator contract missing: {operator_id}"))?;
174 if !contract.failure_taxonomy_is_finite() {
175 return Err(format!(
176 "operator failure taxonomy is not release-gated: {operator_id}"
177 ));
178 }
179 if !execution_context.terminal_budget_is_enforced() {
180 return Err(format!(
181 "operator terminal state missing budget enforcement: {operator_id}"
182 ));
183 }
184 self.authorize_effects(operator_id, effects)?;
185 if !input_manifest.complete() || !output_manifest.complete() {
186 return Err(format!(
187 "operator manifests incomplete or opaque refs present: {operator_id}"
188 ));
189 }
190 if receipt_refs.is_empty() {
191 return Err(format!(
192 "operator invocation missing material receipts: {operator_id}"
193 ));
194 }
195 Ok(())
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
200pub struct OperationConformanceReportV1 {
201 pub report_id: ArtifactId,
202 pub registry_id: ArtifactId,
203 pub checked_operator_ids: Vec<String>,
204 pub missing_operator_ids: Vec<String>,
205 pub effect_violations: Vec<String>,
206 pub passed: bool,
207 pub generated_at: DateTime<Utc>,
208 #[serde(default, skip_serializing_if = "Vec::is_empty")]
209 pub reason_codes: Vec<String>,
210}
211
212impl OperationConformanceReportV1 {
213 pub fn for_required_operators(
214 registry: &MaterialOperationRegistryV1,
215 required_operator_ids: &[&str],
216 ) -> Self {
217 let missing_operator_ids = required_operator_ids
218 .iter()
219 .filter(|operator_id| !registry.contracts.contains_key(**operator_id))
220 .map(|operator_id| (*operator_id).to_string())
221 .collect::<Vec<_>>();
222 let effect_violations = registry
223 .contracts
224 .values()
225 .filter(|contract| {
226 !contract
227 .forbidden_effects
228 .is_disjoint(&contract.allowed_effects)
229 || !contract.failure_taxonomy_is_finite()
230 })
231 .map(|contract| contract.operator_id.clone())
232 .collect::<Vec<_>>();
233 let passed = missing_operator_ids.is_empty() && effect_violations.is_empty();
234 Self {
235 report_id: generated_artifact_id_from_material(
236 "operation-conformance-report",
237 &format!(
238 "{}|{}",
239 registry.registry_id.0,
240 required_operator_ids.join("|")
241 ),
242 ),
243 registry_id: registry.registry_id.clone(),
244 checked_operator_ids: required_operator_ids
245 .iter()
246 .map(|id| (*id).to_string())
247 .collect(),
248 missing_operator_ids,
249 effect_violations,
250 passed,
251 generated_at: Utc::now(),
252 reason_codes: if passed {
253 vec!["operation-contracts-conformant".into()]
254 } else {
255 vec!["operation-contracts-missing-or-invalid".into()]
256 },
257 }
258 }
259}
260
261pub fn p28_required_operator_ids() -> Vec<&'static str> {
262 vec![
263 "aidens.agent.validate",
264 "aidens.agent.doctor",
265 "aidens.runner.turn",
266 "aidens.provider.route",
267 "aidens.tool.repo_read",
268 "aidens.tool.repo_list",
269 "aidens.tool.file_stat",
270 "aidens.tool.repo_search",
271 "aidens.tool.patch_propose",
272 "aidens.tool.patch_apply",
273 "aidens.tool.run_checks",
274 "aidens.package.generate",
275 "aidens.package.validate",
276 "aidens.package.self_replay",
277 "aidens.report.final_done",
278 ]
279}
280
281pub fn p28_declared_material_operation_registry() -> MaterialOperationRegistryV1 {
282 use OperatorEffectV1::*;
283 let read = BTreeSet::from([ReadsRepository, EmitsReceipt]);
284 let validate = BTreeSet::from([EmitsReceipt, AffectsFutureExecution]);
285 let run = BTreeSet::from([EmitsReceipt, AffectsFutureExecution, AffectsUserAgency]);
286 let write = BTreeSet::from([WritesRepository, EmitsReceipt, AffectsFutureExecution]);
287 let command = BTreeSet::from([RunsCommand, EmitsReceipt, AffectsFutureExecution]);
288 let package = BTreeSet::from([GeneratesPackage, EmitsReceipt]);
289 let package_validate = BTreeSet::from([ValidatesPackage, EmitsReceipt]);
290 let forbidden_cloud = BTreeSet::from([CrossesTrustBoundary, ChangesSchema, SubtractsStructure]);
291 MaterialOperationRegistryV1::new(vec![
292 OperatorContractV1::new(
293 "aidens.agent.validate",
294 "runtime",
295 vec!["agent-spec"],
296 vec!["agent-validation-report"],
297 validate.clone(),
298 forbidden_cloud.clone(),
299 "not-required",
300 ),
301 OperatorContractV1::new(
302 "aidens.agent.doctor",
303 "runtime",
304 vec!["agent-spec"],
305 vec!["doctor-report"],
306 validate.clone(),
307 forbidden_cloud.clone(),
308 "not-required",
309 ),
310 OperatorContractV1::new(
311 "aidens.runner.turn",
312 "runtime",
313 vec!["agent-spec", "task"],
314 vec!["run-report", "run-bundle"],
315 run,
316 forbidden_cloud.clone(),
317 "operator-supervised",
318 ),
319 OperatorContractV1::new(
320 "aidens.provider.route",
321 "tool-execution",
322 vec!["provider-policy"],
323 vec!["provider-route-report"],
324 BTreeSet::from([EmitsReceipt]),
325 forbidden_cloud.clone(),
326 "not-required",
327 ),
328 OperatorContractV1::new(
329 "aidens.tool.repo_read",
330 "tool-execution",
331 vec!["repo-path"],
332 vec!["repo-read-receipt"],
333 read.clone(),
334 BTreeSet::from([WritesRepository, RunsCommand, CrossesTrustBoundary]),
335 "not-required",
336 ),
337 OperatorContractV1::new(
338 "aidens.tool.repo_list",
339 "tool-execution",
340 vec!["repo-path"],
341 vec!["repo-list-receipt"],
342 read.clone(),
343 BTreeSet::from([WritesRepository, RunsCommand, CrossesTrustBoundary]),
344 "not-required",
345 ),
346 OperatorContractV1::new(
347 "aidens.tool.file_stat",
348 "tool-execution",
349 vec!["repo-path"],
350 vec!["file-stat-receipt"],
351 read.clone(),
352 BTreeSet::from([WritesRepository, RunsCommand, CrossesTrustBoundary]),
353 "not-required",
354 ),
355 OperatorContractV1::new(
356 "aidens.tool.repo_search",
357 "tool-execution",
358 vec!["repo-query"],
359 vec!["repo-search-receipt"],
360 read,
361 BTreeSet::from([WritesRepository, RunsCommand, CrossesTrustBoundary]),
362 "not-required",
363 ),
364 OperatorContractV1::new(
365 "aidens.tool.patch_propose",
366 "tool-execution",
367 vec!["patch-diff"],
368 vec!["patch-proposal"],
369 BTreeSet::from([ProposesInference, EmitsReceipt]),
370 BTreeSet::from([WritesRepository, RunsCommand, CrossesTrustBoundary]),
371 "not-required",
372 ),
373 OperatorContractV1::new(
374 "aidens.tool.patch_apply",
375 "tool-execution",
376 vec!["patch-diff", "permit"],
377 vec!["patch-apply-receipt"],
378 write,
379 BTreeSet::from([RunsCommand, CrossesTrustBoundary]),
380 "required",
381 ),
382 OperatorContractV1::new(
383 "aidens.tool.run_checks",
384 "tool-execution",
385 vec!["command", "permit"],
386 vec!["command-run-receipt"],
387 command,
388 BTreeSet::from([WritesRepository, CrossesTrustBoundary]),
389 "required",
390 ),
391 OperatorContractV1::new(
392 "aidens.package.generate",
393 "package",
394 vec!["repo-tree"],
395 vec!["package-manifest"],
396 package,
397 forbidden_cloud.clone(),
398 "not-required",
399 ),
400 OperatorContractV1::new(
401 "aidens.package.validate",
402 "package",
403 vec!["package-manifest"],
404 vec!["package-validation-report"],
405 package_validate.clone(),
406 forbidden_cloud.clone(),
407 "not-required",
408 ),
409 OperatorContractV1::new(
410 "aidens.package.self_replay",
411 "package",
412 vec!["package"],
413 vec!["self-replay-receipt"],
414 package_validate,
415 forbidden_cloud.clone(),
416 "not-required",
417 ),
418 OperatorContractV1::new(
419 "aidens.report.final_done",
420 "governance",
421 vec!["phase-reports", "receipts"],
422 vec!["final-report"],
423 BTreeSet::from([EmitsReceipt, ChangesPromotion]),
424 BTreeSet::from([CrossesTrustBoundary, WritesRepository, RunsCommand]),
425 "operator-reviewed",
426 ),
427 ])
428}