1use super::*;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
8pub struct RepoReadReportV1 {
9 pub receipt_id: ArtifactId,
10 pub kind: ArtifactKindV1,
11 pub tool_id: String,
12 pub sandbox_root: String,
13 pub requested_path: String,
14 pub resolved_path: String,
15 pub bytes: u64,
16 pub content_digest: DisplayDigestV1,
17 pub allowed: bool,
18 #[serde(default, skip_serializing_if = "Vec::is_empty")]
19 pub reason_codes: Vec<String>,
20 pub read_at: DateTime<Utc>,
21}
22
23impl RepoReadReportV1 {
24 pub fn allowed(
25 sandbox_root: impl Into<String>,
26 requested_path: impl Into<String>,
27 resolved_path: impl Into<String>,
28 bytes: u64,
29 content: &str,
30 ) -> Self {
31 Self {
32 receipt_id: display_only_unstable_id("repo-read"),
33 kind: ArtifactKindV1::RepoRead,
34 tool_id: "aidens:repo-read:1".into(),
35 sandbox_root: sandbox_root.into(),
36 requested_path: requested_path.into(),
37 resolved_path: resolved_path.into(),
38 bytes,
39 content_digest: DisplayDigestV1::for_text(content),
40 allowed: true,
41 reason_codes: vec!["sandbox-read-allowed".into()],
42 read_at: Utc::now(),
43 }
44 }
45
46 pub fn denied(
47 sandbox_root: impl Into<String>,
48 requested_path: impl Into<String>,
49 reason: impl Into<String>,
50 ) -> Self {
51 let reason = reason.into();
52 Self {
53 receipt_id: display_only_unstable_id("repo-read"),
54 kind: ArtifactKindV1::RepoRead,
55 tool_id: "aidens:repo-read:1".into(),
56 sandbox_root: sandbox_root.into(),
57 requested_path: requested_path.into(),
58 resolved_path: String::new(),
59 bytes: 0,
60 content_digest: DisplayDigestV1::for_text(""),
61 allowed: false,
62 reason_codes: vec![reason],
63 read_at: Utc::now(),
64 }
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
69pub struct RepoListEntryV1 {
70 pub path: String,
71 pub entry_kind: String,
72 pub bytes: Option<u64>,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
76pub struct RepoListReportV1 {
77 pub receipt_id: ArtifactId,
78 pub kind: ArtifactKindV1,
79 pub tool_id: String,
80 pub sandbox_root: String,
81 pub requested_path: String,
82 pub entries: Vec<RepoListEntryV1>,
83 pub listing_digest: DisplayDigestV1,
84 #[serde(default)]
85 pub total_entries: usize,
86 #[serde(default)]
87 pub returned_entries: usize,
88 #[serde(default)]
89 pub truncated: bool,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub full_listing_digest: Option<DisplayDigestV1>,
92 pub allowed: bool,
93 #[serde(default, skip_serializing_if = "Vec::is_empty")]
94 pub reason_codes: Vec<String>,
95 pub listed_at: DateTime<Utc>,
96}
97
98impl RepoListReportV1 {
99 pub fn allowed(
100 sandbox_root: impl Into<String>,
101 requested_path: impl Into<String>,
102 entries: Vec<RepoListEntryV1>,
103 ) -> Self {
104 Self::allowed_with_full_listing(
105 sandbox_root,
106 requested_path,
107 entries.clone(),
108 entries.len(),
109 DisplayDigestV1::for_json_value(
110 &serde_json::to_value(&entries).unwrap_or(serde_json::Value::Null),
111 ),
112 )
113 }
114
115 pub fn allowed_with_full_listing(
116 sandbox_root: impl Into<String>,
117 requested_path: impl Into<String>,
118 entries: Vec<RepoListEntryV1>,
119 total_entries: usize,
120 full_listing_digest: DisplayDigestV1,
121 ) -> Self {
122 let listing = serde_json::to_value(&entries).unwrap_or(serde_json::Value::Null);
123 let returned_entries = entries.len();
124 let truncated = returned_entries < total_entries;
125 Self {
126 receipt_id: display_only_unstable_id("repo-list"),
127 kind: ArtifactKindV1::RepoList,
128 tool_id: "aidens:repo-list:1".into(),
129 sandbox_root: sandbox_root.into(),
130 requested_path: requested_path.into(),
131 entries,
132 listing_digest: DisplayDigestV1::for_json_value(&listing),
133 total_entries,
134 returned_entries,
135 truncated,
136 full_listing_digest: Some(full_listing_digest),
137 allowed: true,
138 reason_codes: if truncated {
139 vec![
140 "sandbox-list-allowed".into(),
141 "repo-list-truncated-with-full-digest".into(),
142 ]
143 } else {
144 vec!["sandbox-list-allowed".into()]
145 },
146 listed_at: Utc::now(),
147 }
148 }
149
150 pub fn denied(
151 sandbox_root: impl Into<String>,
152 requested_path: impl Into<String>,
153 reason: impl Into<String>,
154 ) -> Self {
155 Self {
156 receipt_id: display_only_unstable_id("repo-list"),
157 kind: ArtifactKindV1::RepoList,
158 tool_id: "aidens:repo-list:1".into(),
159 sandbox_root: sandbox_root.into(),
160 requested_path: requested_path.into(),
161 entries: Vec::new(),
162 listing_digest: DisplayDigestV1::for_json_value(&serde_json::json!([])),
163 total_entries: 0,
164 returned_entries: 0,
165 truncated: false,
166 full_listing_digest: Some(DisplayDigestV1::for_json_value(&serde_json::json!([]))),
167 allowed: false,
168 reason_codes: vec![reason.into()],
169 listed_at: Utc::now(),
170 }
171 }
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
175pub struct PatchProposalV1 {
176 pub proposal_id: ArtifactId,
177 pub kind: ArtifactKindV1,
178 pub summary: String,
179 pub unified_diff: String,
180 pub touched_paths: Vec<String>,
181 pub diff_digest: DisplayDigestV1,
182 pub mutates_files: bool,
183 #[serde(default, skip_serializing_if = "Vec::is_empty")]
184 pub reason_codes: Vec<String>,
185 pub proposed_at: DateTime<Utc>,
186}
187
188impl PatchProposalV1 {
189 pub fn new(
190 summary: impl Into<String>,
191 unified_diff: impl Into<String>,
192 touched_paths: Vec<String>,
193 ) -> Self {
194 let unified_diff = unified_diff.into();
195 Self {
196 proposal_id: display_only_unstable_id("patch-proposal"),
197 kind: ArtifactKindV1::PatchProposal,
198 summary: summary.into(),
199 diff_digest: DisplayDigestV1::for_text(&unified_diff),
200 unified_diff,
201 touched_paths: sorted_unique_strings(touched_paths),
202 mutates_files: false,
203 reason_codes: vec!["proposal-only-no-file-mutation".into()],
204 proposed_at: Utc::now(),
205 }
206 }
207}
208
209#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
210pub struct PatchApplyReportV1 {
211 pub receipt_id: ArtifactId,
212 pub kind: ArtifactKindV1,
213 pub tool_id: String,
214 pub sandbox_root: String,
215 pub proposal_id: Option<ArtifactId>,
216 pub permit_grant_id: Option<ArtifactId>,
217 pub permit_use_receipt_id: Option<ArtifactId>,
218 pub touched_paths: Vec<String>,
219 pub input_digest: DisplayDigestV1,
220 pub before_digests: BTreeMap<String, DisplayDigestV1>,
221 pub after_digests: BTreeMap<String, DisplayDigestV1>,
222 pub applied: bool,
223 #[serde(default, skip_serializing_if = "Vec::is_empty")]
224 pub changed_files: Vec<String>,
225 #[serde(default)]
226 pub dry_run_checked: bool,
227 #[serde(default, skip_serializing_if = "String::is_empty")]
228 pub semantic_status: String,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub failure_kind: Option<String>,
231 #[serde(default, skip_serializing_if = "Vec::is_empty")]
232 pub rollback_advice: Vec<String>,
233 #[serde(default, skip_serializing_if = "Vec::is_empty")]
234 pub reason_codes: Vec<String>,
235 pub applied_at: DateTime<Utc>,
236}
237
238impl PatchApplyReportV1 {
239 pub fn new(
240 sandbox_root: impl Into<String>,
241 input: &serde_json::Value,
242 touched_paths: Vec<String>,
243 before_digests: BTreeMap<String, DisplayDigestV1>,
244 after_digests: BTreeMap<String, DisplayDigestV1>,
245 permit_grant_id: Option<ArtifactId>,
246 permit_use_receipt_id: Option<ArtifactId>,
247 ) -> Self {
248 Self {
249 receipt_id: display_only_unstable_id("patch-apply"),
250 kind: ArtifactKindV1::PatchApply,
251 tool_id: "aidens:patch-apply:1".into(),
252 sandbox_root: sandbox_root.into(),
253 proposal_id: None,
254 permit_grant_id,
255 permit_use_receipt_id,
256 touched_paths: sorted_unique_strings(touched_paths.clone()),
257 input_digest: DisplayDigestV1::for_json_value(input),
258 before_digests,
259 after_digests,
260 applied: true,
261 changed_files: sorted_unique_strings(touched_paths),
262 dry_run_checked: true,
263 semantic_status: "exact_check".into(),
264 failure_kind: None,
265 rollback_advice: vec![
266 "Use before_digests to verify the pre-apply state before manual rollback.".into(),
267 "Reconstruct prior file contents from version control or the original sandbox fixture.".into(),
268 ],
269 reason_codes: vec!["patch-applied-with-explicit-permit".into()],
270 applied_at: Utc::now(),
271 }
272 }
273
274 pub fn checked(
275 sandbox_root: impl Into<String>,
276 input: &serde_json::Value,
277 touched_paths: Vec<String>,
278 before_digests: BTreeMap<String, DisplayDigestV1>,
279 after_digests: BTreeMap<String, DisplayDigestV1>,
280 permit_grant_id: Option<ArtifactId>,
281 permit_use_receipt_id: Option<ArtifactId>,
282 ) -> Self {
283 Self {
284 receipt_id: display_only_unstable_id("patch-apply"),
285 kind: ArtifactKindV1::PatchApply,
286 tool_id: "aidens:patch-apply:1".into(),
287 sandbox_root: sandbox_root.into(),
288 proposal_id: None,
289 permit_grant_id,
290 permit_use_receipt_id,
291 touched_paths: sorted_unique_strings(touched_paths.clone()),
292 input_digest: DisplayDigestV1::for_json_value(input),
293 before_digests,
294 after_digests,
295 applied: false,
296 changed_files: sorted_unique_strings(touched_paths),
297 dry_run_checked: true,
298 semantic_status: "exact_check".into(),
299 failure_kind: None,
300 rollback_advice: vec![
301 "No rollback required; check_only mode did not mutate files.".into(),
302 ],
303 reason_codes: vec!["patch-validated-without-application".into()],
304 applied_at: Utc::now(),
305 }
306 }
307
308 pub fn denied(
309 sandbox_root: impl Into<String>,
310 input: &serde_json::Value,
311 reason: impl Into<String>,
312 ) -> Self {
313 Self::denied_with_details(
314 sandbox_root,
315 input,
316 reason,
317 "invalid-patch",
318 Vec::new(),
319 None,
320 None,
321 )
322 }
323
324 pub fn denied_with_details(
325 sandbox_root: impl Into<String>,
326 input: &serde_json::Value,
327 reason: impl Into<String>,
328 failure_kind: impl Into<String>,
329 touched_paths: Vec<String>,
330 permit_grant_id: Option<ArtifactId>,
331 permit_use_receipt_id: Option<ArtifactId>,
332 ) -> Self {
333 Self {
334 receipt_id: display_only_unstable_id("patch-apply"),
335 kind: ArtifactKindV1::PatchApply,
336 tool_id: "aidens:patch-apply:1".into(),
337 sandbox_root: sandbox_root.into(),
338 proposal_id: None,
339 permit_grant_id,
340 permit_use_receipt_id,
341 touched_paths: sorted_unique_strings(touched_paths.clone()),
342 input_digest: DisplayDigestV1::for_json_value(input),
343 before_digests: BTreeMap::new(),
344 after_digests: BTreeMap::new(),
345 applied: false,
346 changed_files: sorted_unique_strings(touched_paths),
347 dry_run_checked: true,
348 semantic_status: "failed_exact_check".into(),
349 failure_kind: Some(failure_kind.into()),
350 rollback_advice: vec![
351 "No files were written by this failed-closed patch attempt.".into(),
352 "Regenerate a single-file unified diff with unique removal context before retrying.".into(),
353 ],
354 reason_codes: vec![reason.into()],
355 applied_at: Utc::now(),
356 }
357 }
358}
359
360#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
361pub struct CommandRunReportV1 {
362 pub receipt_id: ArtifactId,
363 pub kind: ArtifactKindV1,
364 pub tool_id: String,
365 pub sandbox_root: String,
366 pub command: Vec<String>,
367 pub permit_grant_id: Option<ArtifactId>,
368 pub permit_use_receipt_id: Option<ArtifactId>,
369 pub allowed_by_policy: bool,
370 pub exit_code: Option<i32>,
371 pub stdout_digest: DisplayDigestV1,
372 pub stderr_digest: DisplayDigestV1,
373 pub stdout_bytes: usize,
374 pub stderr_bytes: usize,
375 pub timed_out: bool,
376 pub succeeded: bool,
377 #[serde(default, skip_serializing_if = "Vec::is_empty")]
378 pub reason_codes: Vec<String>,
379 pub started_at: DateTime<Utc>,
380 pub completed_at: DateTime<Utc>,
381}
382
383impl CommandRunReportV1 {
384 pub fn completed(
385 sandbox_root: impl Into<String>,
386 command: Vec<String>,
387 permit_grant_id: Option<ArtifactId>,
388 permit_use_receipt_id: Option<ArtifactId>,
389 exit_code: Option<i32>,
390 stdout: &str,
391 stderr: &str,
392 ) -> Self {
393 let succeeded = exit_code == Some(0);
394 Self {
395 receipt_id: display_only_unstable_id("command-run"),
396 kind: ArtifactKindV1::CommandRun,
397 tool_id: "aidens:run-checks:1".into(),
398 sandbox_root: sandbox_root.into(),
399 command,
400 permit_grant_id,
401 permit_use_receipt_id,
402 allowed_by_policy: true,
403 exit_code,
404 stdout_digest: DisplayDigestV1::for_text(stdout),
405 stderr_digest: DisplayDigestV1::for_text(stderr),
406 stdout_bytes: stdout.len(),
407 stderr_bytes: stderr.len(),
408 timed_out: false,
409 succeeded,
410 reason_codes: if succeeded {
411 vec!["allowed-check-command-succeeded".into()]
412 } else {
413 vec!["allowed-check-command-failed".into()]
414 },
415 started_at: Utc::now(),
416 completed_at: Utc::now(),
417 }
418 }
419
420 pub fn blocked(
421 sandbox_root: impl Into<String>,
422 command: Vec<String>,
423 reason: impl Into<String>,
424 ) -> Self {
425 let now = Utc::now();
426 Self {
427 receipt_id: display_only_unstable_id("command-run"),
428 kind: ArtifactKindV1::CommandRun,
429 tool_id: "aidens:run-checks:1".into(),
430 sandbox_root: sandbox_root.into(),
431 command,
432 permit_grant_id: None,
433 permit_use_receipt_id: None,
434 allowed_by_policy: false,
435 exit_code: None,
436 stdout_digest: DisplayDigestV1::for_text(""),
437 stderr_digest: DisplayDigestV1::for_text(""),
438 stdout_bytes: 0,
439 stderr_bytes: 0,
440 timed_out: false,
441 succeeded: false,
442 reason_codes: vec![reason.into()],
443 started_at: now,
444 completed_at: now,
445 }
446 }
447}
448
449#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
450pub struct SandboxCapabilityTruthV1 {
451 pub truth_id: ArtifactId,
452 pub kind: ArtifactKindV1,
453 pub sandbox_root: String,
454 pub denied_prefixes: Vec<String>,
455 pub symlink_policy: String,
456 pub env_allowlist: Vec<String>,
457 pub network_policy: String,
458 pub process_timeout_millis: u64,
459 pub write_requires_permit: bool,
460 pub shell_requires_permit: bool,
461 #[serde(default, skip_serializing_if = "Vec::is_empty")]
462 pub reason_codes: Vec<String>,
463 pub checked_at: DateTime<Utc>,
464}
465
466impl SandboxCapabilityTruthV1 {
467 pub fn coding_default(sandbox_root: impl Into<String>) -> Self {
468 Self {
469 truth_id: display_only_unstable_id("sandbox-truth"),
470 kind: ArtifactKindV1::SandboxTruth,
471 sandbox_root: sandbox_root.into(),
472 denied_prefixes: vec![
473 ".ssh".into(),
474 ".gnupg".into(),
475 ".cargo".into(),
476 ".recall".into(),
477 ".password-store".into(),
478 ],
479 symlink_policy: "canonicalize-and-require-within-root".into(),
480 env_allowlist: vec!["PATH".into(), "CARGO_HOME".into(), "RUSTUP_HOME".into()],
481 network_policy: "disabled".into(),
482 process_timeout_millis: 120_000,
483 write_requires_permit: true,
484 shell_requires_permit: true,
485 reason_codes: vec![
486 "no-shell-file-write-or-network-by-default".into(),
487 "side-effects-require-scoped-permit".into(),
488 ],
489 checked_at: Utc::now(),
490 }
491 }
492}
493
494#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
495pub struct CodexPacketV1 {
496 pub packet_id: ArtifactId,
497 pub kind: ArtifactKindV1,
498 pub current_pass: String,
499 pub next_pass: String,
500 pub source_basis: String,
501 pub issue: String,
502 pub source_map: Vec<String>,
503 pub changed_files: Vec<String>,
504 pub commands_run: Vec<CommandRunReportV1>,
505 pub receipt_ids: Vec<ArtifactId>,
506 pub blockers: Vec<String>,
507 pub notes: Vec<String>,
508 pub generated_at: DateTime<Utc>,
509}
510
511#[derive(Debug, Clone, PartialEq, Eq)]
512pub struct CodexPacketInputV1 {
513 pub current_pass: String,
514 pub next_pass: String,
515 pub issue: String,
516 pub source_map: Vec<String>,
517 pub changed_files: Vec<String>,
518 pub commands_run: Vec<CommandRunReportV1>,
519 pub receipt_ids: Vec<ArtifactId>,
520 pub blockers: Vec<String>,
521 pub notes: Vec<String>,
522}
523
524impl CodexPacketV1 {
525 pub fn new(input: CodexPacketInputV1) -> Self {
526 Self {
527 packet_id: display_only_unstable_id("codex-packet"),
528 kind: ArtifactKindV1::CodexPacket,
529 current_pass: input.current_pass,
530 next_pass: input.next_pass,
531 source_basis: "SOURCE_BASIS.md".into(),
532 issue: input.issue,
533 source_map: sorted_unique_strings(input.source_map),
534 changed_files: sorted_unique_strings(input.changed_files),
535 commands_run: input.commands_run,
536 receipt_ids: input.receipt_ids,
537 blockers: input.blockers,
538 notes: input.notes,
539 generated_at: Utc::now(),
540 }
541 }
542
543 pub fn has_resume_context(&self) -> bool {
544 !self.current_pass.trim().is_empty()
545 && !self.next_pass.trim().is_empty()
546 && !self.source_basis.trim().is_empty()
547 && !self.issue.trim().is_empty()
548 && !self.source_map.is_empty()
549 }
550}