1use 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 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 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}