Skip to main content

aidens_contracts/
agent_bundle.rs

1//! Agent spec and run-bundle envelopes.
2//!
3//! These structures package AiDENs-local run evidence while preserving canonical execution and receipt backpointers.
4
5use super::*;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
8#[serde(rename_all = "kebab-case")]
9pub enum AgentSpecSupportLabelV1 {
10    Supported,
11    SupportedLocal,
12    Partial,
13    Deferred,
14    Unsupported,
15    Scaffold,
16    Failed,
17}
18
19impl fmt::Display for AgentSpecSupportLabelV1 {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        f.write_str(match self {
22            Self::Supported => "supported",
23            Self::SupportedLocal => "supported-local",
24            Self::Partial => "partial",
25            Self::Deferred => "deferred",
26            Self::Unsupported => "unsupported",
27            Self::Scaffold => "scaffold",
28            Self::Failed => "failed",
29        })
30    }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
34#[serde(rename_all = "kebab-case")]
35pub enum AgentProviderModeV1 {
36    Mock,
37    Local,
38    Ollama,
39}
40
41impl fmt::Display for AgentProviderModeV1 {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        f.write_str(match self {
44            Self::Mock => "mock",
45            Self::Local => "local",
46            Self::Ollama => "ollama",
47        })
48    }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
52#[serde(rename_all = "kebab-case")]
53pub enum AgentMemoryModeV1 {
54    Fixture,
55    CanonicalSeam,
56}
57
58impl fmt::Display for AgentMemoryModeV1 {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        f.write_str(match self {
61            Self::Fixture => "fixture",
62            Self::CanonicalSeam => "canonical-seam",
63        })
64    }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
68#[serde(rename_all = "kebab-case")]
69pub enum AgentPermitRuleV1 {
70    OperatorApproved,
71    Forbidden,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
75#[serde(rename_all = "kebab-case")]
76pub enum AgentVerificationCheckV1 {
77    Schema,
78    Sandbox,
79    Digest,
80    SupportClaim,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
84pub struct AgentSpecProviderPolicyV1 {
85    pub provider: AgentProviderModeV1,
86    pub cloud_allowed: bool,
87    pub fallback_allowed: bool,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
91pub struct AgentSpecMemoryPolicyV1 {
92    pub enabled: bool,
93    pub mode: AgentMemoryModeV1,
94    pub requires_view_disclosure: bool,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
98pub struct AgentSpecToolPolicyV1 {
99    pub allowed_tools: Vec<String>,
100    pub write_tools_require_permit: bool,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
104pub struct AgentSpecPermitPolicyV1 {
105    pub writes: AgentPermitRuleV1,
106    pub commands: AgentPermitRuleV1,
107    pub network: AgentPermitRuleV1,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
111pub struct AgentSpecVerificationPolicyV1 {
112    pub required_checks: Vec<AgentVerificationCheckV1>,
113    pub fail_closed: bool,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
117pub struct AgentSpecEvidencePolicyV1 {
118    pub emit_run_bundle: bool,
119    pub emit_tool_receipts: bool,
120    pub emit_permit_receipts: bool,
121    pub emit_abstention_receipts: bool,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
125pub struct AgentSpecBudgetPolicyV1 {
126    pub max_turns: u32,
127    pub max_tool_calls: u32,
128    pub deadline_seconds: u64,
129}
130
131#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
132pub struct AgentSpecV1 {
133    pub schema: String,
134    pub agent_id: String,
135    pub display_name: String,
136    pub support_label: AgentSpecSupportLabelV1,
137    pub profile: String,
138    pub provider_policy: AgentSpecProviderPolicyV1,
139    pub memory_policy: AgentSpecMemoryPolicyV1,
140    pub tool_policy: AgentSpecToolPolicyV1,
141    pub permit_policy: AgentSpecPermitPolicyV1,
142    pub verification_policy: AgentSpecVerificationPolicyV1,
143    pub evidence_policy: AgentSpecEvidencePolicyV1,
144    pub budget_policy: AgentSpecBudgetPolicyV1,
145}
146
147impl AgentSpecV1 {
148    pub const SCHEMA: &'static str = "AgentSpecV1";
149
150    pub fn validate(&self) -> Result<(), Vec<String>> {
151        let mut reason_codes = Vec::new();
152
153        if self.schema.trim() != Self::SCHEMA {
154            reason_codes.push("schema-must-equal-AgentSpecV1".into());
155        }
156        if self.agent_id.trim().is_empty() {
157            reason_codes.push("agent-id-required".into());
158        }
159        if self.display_name.trim().is_empty() {
160            reason_codes.push("display-name-required".into());
161        }
162        if self.profile.trim().is_empty() {
163            reason_codes.push("profile-required".into());
164        } else {
165            let normalized = self.profile.to_ascii_lowercase();
166            if !matches!(
167                normalized.as_str(),
168                "coding" | "memory" | "research" | "custom-local"
169            ) {
170                reason_codes.push("unsupported-profile".into());
171            }
172        }
173        if self.provider_policy.cloud_allowed {
174            reason_codes.push("cloud-providers-unsupported-for-p26-local-agent".into());
175        }
176        if self.tool_policy.allowed_tools.is_empty() {
177            reason_codes.push("tool-policy-must-list-tools".into());
178        } else {
179            // Canonical coding tools (dotted names without namespace prefix).
180            const ALLOWED_TOOLS: [&str; 8] = [
181                "repo.read",
182                "repo.list",
183                "repo.search",
184                "patch.propose",
185                "patch.apply",
186                "checks.run",
187                "run.inspect",
188                "run.replay",
189            ];
190            for tool in &self.tool_policy.allowed_tools {
191                // Accept namespaced tools (e.g. "kirsten:search:1") that are
192                // registered in the ToolRegistryV1 at runtime. Only reject
193                // bare names that aren't in the canonical coding set.
194                let is_namespaced = tool.contains(':');
195                if !is_namespaced && !ALLOWED_TOOLS.contains(&tool.as_str()) {
196                    reason_codes.push(format!("unsupported-tool:{tool}"));
197                }
198            }
199        }
200
201        const WRITE_TOOLS_REQUIRE_PERMIT: [&str; 3] = ["patch.apply", "checks.run", "repo.write"];
202        let write_tools_present = self
203            .tool_policy
204            .allowed_tools
205            .iter()
206            .any(|tool| WRITE_TOOLS_REQUIRE_PERMIT.contains(&tool.as_str()));
207        if write_tools_present && !self.tool_policy.write_tools_require_permit {
208            reason_codes.push("write-tools-must-require-permit".into());
209        }
210        if write_tools_present
211            && !matches!(
212                self.permit_policy.writes,
213                AgentPermitRuleV1::OperatorApproved
214            )
215        {
216            reason_codes.push("write-permit-policy-must-be-operator-approved".into());
217        }
218        if matches!(
219            self.permit_policy.network,
220            AgentPermitRuleV1::OperatorApproved
221        ) {
222            reason_codes.push("network-access-must-be-forbidden-for-local-run".into());
223        }
224        if self.memory_policy.enabled {
225            if !(self.memory_policy.requires_view_disclosure
226                || self.memory_policy.mode == AgentMemoryModeV1::CanonicalSeam)
227            {
228                reason_codes.push("canonical-memory-grounding-must-disclose-or-be-disabled".into());
229            }
230        } else if self.memory_policy.requires_view_disclosure {
231            reason_codes.push("requires_view_disclosure-when-memory-disabled".into());
232        }
233        if !self.evidence_policy.emit_tool_receipts {
234            reason_codes.push("must-emit-tool-receipts".into());
235        }
236        if !self.evidence_policy.emit_permit_receipts {
237            reason_codes.push("must-emit-permit-receipts".into());
238        }
239        if !self.evidence_policy.emit_abstention_receipts {
240            reason_codes.push("must-emit-abstention-receipts".into());
241        }
242        if !self.evidence_policy.emit_run_bundle {
243            reason_codes.push("must-emit-run-bundle".into());
244        }
245        if self.verification_policy.required_checks.is_empty() {
246            reason_codes.push("verification-required-checks-missing".into());
247        }
248        if !self.verification_policy.fail_closed {
249            reason_codes.push("verification-fail-closed-required".into());
250        }
251        if self.budget_policy.max_turns == 0 {
252            reason_codes.push("max-turns-must-be-positive".into());
253        }
254        if self.budget_policy.max_tool_calls == 0 {
255            reason_codes.push("max-tool-calls-must-be-positive".into());
256        }
257        if self.budget_policy.deadline_seconds == 0 {
258            reason_codes.push("deadline-seconds-must-be-positive".into());
259        }
260
261        if reason_codes.is_empty() {
262            Ok(())
263        } else {
264            Err(reason_codes)
265        }
266    }
267}
268
269#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
270pub struct AiDENsRunSupportTierEvidenceV1 {
271    pub support_tier: String,
272    #[serde(default, skip_serializing_if = "Vec::is_empty")]
273    pub supported: Vec<String>,
274    #[serde(default, skip_serializing_if = "Vec::is_empty")]
275    pub partial: Vec<String>,
276    #[serde(default, skip_serializing_if = "Vec::is_empty")]
277    pub deferred: Vec<String>,
278    #[serde(default, skip_serializing_if = "Vec::is_empty")]
279    pub reason_codes: Vec<String>,
280}
281
282#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
283pub struct AiDENsRunBundleV2 {
284    pub schema: String,
285    pub bundle_id: ArtifactId,
286    pub ownership: String,
287    pub run_id: String,
288    pub profile: String,
289    pub created_at: DateTime<Utc>,
290    pub canonical_execution_context: canonical_stack::ForgeExecutionContextV1,
291    pub trace_ctx: StackTraceCtx,
292    pub attempt_id: StackAttemptId,
293    pub trial_id: StackTrialId,
294    pub event_log: AiDENsRunEventLogDigestV1,
295    #[serde(default, skip_serializing_if = "Vec::is_empty")]
296    pub provider_receipts: Vec<String>,
297    #[serde(default, skip_serializing_if = "Vec::is_empty")]
298    pub tool_receipts: Vec<String>,
299    #[serde(default, skip_serializing_if = "Vec::is_empty")]
300    pub permit_receipts: Vec<String>,
301    pub budget: AiDENsRunBudgetDeadlineV1,
302    pub support: AiDENsRunSupportTierEvidenceV1,
303    pub replay: AiDENsRunReplayNormalizationV1,
304    pub failure: AiDENsRunFailureTaxonomyV1,
305    #[serde(default, skip_serializing_if = "Vec::is_empty")]
306    pub outputs: Vec<String>,
307    #[serde(default, skip_serializing_if = "Vec::is_empty")]
308    pub canonical_backpointers: Vec<CanonicalBackpointerV1>,
309    #[serde(default, skip_serializing_if = "Vec::is_empty")]
310    pub blocked_checks: Vec<String>,
311}
312
313impl AiDENsRunBundleV2 {
314    pub const SCHEMA: &'static str = "AiDENsRunBundleV2";
315
316    #[allow(clippy::too_many_arguments)]
317    pub fn new(
318        run_id: impl Into<String>,
319        profile: impl Into<String>,
320        canonical_execution_context: canonical_stack::ForgeExecutionContextV1,
321        event_log: AiDENsRunEventLogDigestV1,
322        budget: AiDENsRunBudgetDeadlineV1,
323        support: AiDENsRunSupportTierEvidenceV1,
324        replay: AiDENsRunReplayNormalizationV1,
325        failure: AiDENsRunFailureTaxonomyV1,
326    ) -> Self {
327        let trace_ctx = canonical_execution_context.trace_ctx.clone();
328        let attempt_id = canonical_execution_context
329            .attempt_id
330            .clone()
331            .unwrap_or_else(StackAttemptId::generate);
332        let trial_id = canonical_execution_context
333            .trial_id
334            .clone()
335            .unwrap_or_else(StackTrialId::generate);
336        Self {
337            schema: Self::SCHEMA.into(),
338            bundle_id: display_only_unstable_id("aidens-run-bundle-v2"),
339            ownership: "AiDENs-local operator run bundle; canonical execution, trace, identity, receipt, memory, and verification semantics remain in owner crates.".into(),
340            run_id: run_id.into(),
341            profile: profile.into(),
342            created_at: Utc::now(),
343            canonical_execution_context,
344            trace_ctx,
345            attempt_id,
346            trial_id,
347            event_log,
348            provider_receipts: Vec::new(),
349            tool_receipts: Vec::new(),
350            permit_receipts: Vec::new(),
351            budget,
352            support,
353            replay,
354            failure,
355            outputs: Vec::new(),
356            canonical_backpointers: vec![
357                CanonicalBackpointerV1::owner_type(
358                    "semantic-memory-forge",
359                    "ExecutionContextV1",
360                    "canonical-execution-context-owner",
361                ),
362                CanonicalBackpointerV1::owner_type(
363                    "stack-ids",
364                    "TraceCtx",
365                    "canonical-trace-owner",
366                ),
367                CanonicalBackpointerV1::owner_type(
368                    "stack-ids",
369                    "AttemptId",
370                    "canonical-attempt-owner",
371                ),
372                CanonicalBackpointerV1::owner_type(
373                    "stack-ids",
374                    "TrialId",
375                    "canonical-trial-owner",
376                ),
377                CanonicalBackpointerV1::owner_type(
378                    "llm-tool-runtime",
379                    "ToolReceipt",
380                    "canonical-tool-receipt-owner",
381                ),
382            ],
383            blocked_checks: Vec::new(),
384        }
385    }
386}
387
388#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
389pub struct AiDENsRunBundleV3 {
390    pub schema: String,
391    pub bundle_id: ArtifactId,
392    pub ownership: String,
393    pub run_id: String,
394    pub profile: String,
395    pub created_at: DateTime<Utc>,
396    pub canonical_execution_context: canonical_stack::ForgeExecutionContextV1,
397    pub trace_ctx: StackTraceCtx,
398    pub attempt_family_id: ArtifactId,
399    pub attempt_id: StackAttemptId,
400    pub trial_id: StackTrialId,
401    pub event_log: AiDENsRunEventLogDigestV1,
402    #[serde(default, skip_serializing_if = "Vec::is_empty")]
403    pub provider_receipts: Vec<String>,
404    #[serde(default, skip_serializing_if = "Vec::is_empty")]
405    pub tool_receipts: Vec<String>,
406    #[serde(default, skip_serializing_if = "Vec::is_empty")]
407    pub permit_receipts: Vec<String>,
408    #[serde(default, skip_serializing_if = "Vec::is_empty")]
409    pub memory_grounding_receipts: Vec<String>,
410    #[serde(default, skip_serializing_if = "Vec::is_empty")]
411    pub verification_receipts: Vec<String>,
412    #[serde(default, skip_serializing_if = "Vec::is_empty")]
413    pub abstention_receipts: Vec<String>,
414    #[serde(default, skip_serializing_if = "Vec::is_empty")]
415    pub repair_plan_receipts: Vec<String>,
416    pub agent_spec_digest: DisplayDigestV1,
417    pub budget: AiDENsRunBudgetDeadlineV1,
418    pub support: AiDENsRunSupportTierEvidenceV1,
419    #[serde(default, skip_serializing_if = "Vec::is_empty")]
420    pub support_labels: Vec<String>,
421    pub replay: AiDENsRunReplayNormalizationV1,
422    #[serde(default, skip_serializing_if = "Vec::is_empty")]
423    pub replay_instructions: Vec<String>,
424    pub failure: AiDENsRunFailureTaxonomyV1,
425    #[serde(default, skip_serializing_if = "Vec::is_empty")]
426    pub outputs: Vec<String>,
427    #[serde(default, skip_serializing_if = "Vec::is_empty")]
428    pub canonical_backpointers: Vec<CanonicalBackpointerV1>,
429    #[serde(default, skip_serializing_if = "Vec::is_empty")]
430    pub blocked_checks: Vec<String>,
431}
432
433impl AiDENsRunBundleV3 {
434    pub const SCHEMA: &'static str = "AiDENsRunBundleV3";
435
436    #[allow(clippy::too_many_arguments)]
437    pub fn new(
438        run_id: impl Into<String>,
439        profile: impl Into<String>,
440        canonical_execution_context: canonical_stack::ForgeExecutionContextV1,
441        event_log: AiDENsRunEventLogDigestV1,
442        budget: AiDENsRunBudgetDeadlineV1,
443        support: AiDENsRunSupportTierEvidenceV1,
444        support_labels: Vec<String>,
445        replay: AiDENsRunReplayNormalizationV1,
446        failure: AiDENsRunFailureTaxonomyV1,
447        attempt_family_id: ArtifactId,
448        attempt_id: StackAttemptId,
449        trial_id: StackTrialId,
450        agent_spec_digest: DisplayDigestV1,
451    ) -> Self {
452        let trace_ctx = canonical_execution_context.trace_ctx.clone();
453        Self {
454            schema: Self::SCHEMA.into(),
455            bundle_id: display_only_unstable_id("aidens-run-bundle-v3"),
456            ownership:
457                "AiDENs-local operator run bundle; canonical execution, trace, identity, receipt, memory, and verification semantics remain in owner crates.".into(),
458            run_id: run_id.into(),
459            profile: profile.into(),
460            created_at: Utc::now(),
461            canonical_execution_context,
462            trace_ctx,
463            attempt_family_id,
464            attempt_id,
465            trial_id,
466            event_log,
467            provider_receipts: Vec::new(),
468            tool_receipts: Vec::new(),
469            permit_receipts: Vec::new(),
470            memory_grounding_receipts: Vec::new(),
471            verification_receipts: Vec::new(),
472            abstention_receipts: Vec::new(),
473            repair_plan_receipts: Vec::new(),
474            agent_spec_digest,
475            budget,
476            support,
477            support_labels,
478            replay,
479            replay_instructions: Vec::new(),
480            failure,
481            outputs: Vec::new(),
482            canonical_backpointers: vec![
483                CanonicalBackpointerV1::owner_type(
484                    "semantic-memory-forge",
485                    "ExecutionContextV1",
486                    "canonical-execution-context-owner",
487                ),
488                CanonicalBackpointerV1::owner_type(
489                    "stack-ids",
490                    "TraceCtx",
491                    "canonical-trace-owner",
492                ),
493                CanonicalBackpointerV1::owner_type(
494                    "stack-ids",
495                    "AttemptFamilyId",
496                    "canonical-attempt-family-owner",
497                ),
498                CanonicalBackpointerV1::owner_type(
499                    "stack-ids",
500                    "AttemptId",
501                    "canonical-attempt-owner",
502                ),
503                CanonicalBackpointerV1::owner_type(
504                    "stack-ids",
505                    "TrialId",
506                    "canonical-trial-owner",
507                ),
508                CanonicalBackpointerV1::owner_type(
509                    "llm-tool-runtime",
510                    "ToolReceipt",
511                    "canonical-tool-receipt-owner",
512                ),
513            ],
514            blocked_checks: Vec::new(),
515        }
516    }
517}