Skip to main content

aidens_contracts/
operator.rs

1//! Material-operation operator contracts and effect registry.
2//!
3//! Contracts here constrain AiDENs-local orchestration actions. They do not
4//! authorize new provider, memory, proof, or kernel truth ownership.
5
6use 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}