rightkit_agent_engine_protocol/artifact.rs
1use serde::{Deserialize, Serialize};
2
3/// A verdict attached by a juror. Honored only while `bound_hash` == the artifact's current content hash (A4).
4#[derive(Debug, Clone, Serialize, Deserialize)]
5#[serde(rename_all = "camelCase")]
6pub struct ArtifactVerdict {
7 pub passed: bool,
8 pub juror: String,
9 pub bound_hash: String,
10 pub summary: String,
11}
12
13/// A recorded approval. Honored only while `bound_hash` == the artifact's current content hash.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct ArtifactApproval {
17 pub approver: String,
18 pub bound_hash: String,
19 pub created_at: String,
20}
21
22/// A trust-bearing artifact record (P2 §15). The trust fields (shippable, verdict, approvals) are
23/// only settable through the gated `artifact_trust` tools (A3).
24///
25/// `trust_label` is a storage-level trust propagation field (Row 12 follow-up).
26/// Defaults to `"unreviewed"`; the `artifact_trust` tool family is the only
27/// legitimate way to change it post-creation. Downstream consumers
28/// (compaction, evidence graph, runtime panels) read the label to gate
29/// which contexts may see the artifact body.
30///
31/// Note: `effective_verdict` and `effective_approvals` (A4 hash-binding) are computed properties
32/// in C# marked `[JsonIgnore]` — they are not serialized and have no Rust equivalent here.
33/// Consumers that need hash-binding logic should implement it themselves using `content_hash`.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct ArtifactRow {
37 pub id: String,
38 #[serde(default)]
39 pub path: String,
40 pub kind: String,
41 pub content_hash: String,
42 pub shippable: bool,
43 pub producer: String,
44 #[serde(default)]
45 pub provenance: String,
46 #[serde(default = "default_artifact_trust_label")]
47 pub trust_label: String,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub verdict: Option<ArtifactVerdict>,
50 #[serde(default)]
51 pub approvals: Vec<ArtifactApproval>,
52 #[serde(default)]
53 pub meta: String,
54 #[serde(default)]
55 pub created_at: String,
56}
57
58fn default_artifact_trust_label() -> String {
59 "unreviewed".to_string()
60}
61
62/// CRUD-surface bookkeeping for an [`ArtifactRow`] (ENG-115), carried inside
63/// its opaque `meta` JSON string rather than as new `ArtifactRow` fields.
64///
65/// `ArtifactRow` is constructed via exhaustive struct literals in several
66/// crates outside this lane's file allowlist (`storage::artifacts`,
67/// `host-app/src/api_session_utils_artifacts.rs`, and others) with no
68/// `..Default::default()` spread. Adding a new required field there would be
69/// a breaking change this lane cannot land, since fixing every call site is
70/// out of scope. Piggybacking on `meta` — already a freeform per-artifact
71/// JSON blob other producers (e.g. the browser-artifact writer) use the same
72/// way — keeps `artifact_crud` additive.
73///
74/// A consumer that needs a typed edit counter or "session that produced
75/// this" field on `ArtifactRow` itself should thread it through every
76/// existing constructor in the same change, not add it silently.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78#[serde(rename_all = "camelCase")]
79pub struct ArtifactCrudMeta {
80 /// Monotonic content version. Starts at 1 on creation and is incremented
81 /// by exactly one on every `artifact_update` that lands. Distinct from
82 /// `content_hash`-based trust binding: this is a plain edit counter the
83 /// CRUD surface's optimistic-concurrency check compares cheaply without
84 /// hashing.
85 #[serde(default = "default_artifact_crud_version")]
86 pub version: u64,
87 /// Wall-clock stamp (ISO-8601) of the most recent create/update.
88 #[serde(default)]
89 pub updated_at: String,
90 /// Session that produced this artifact through the CRUD surface.
91 #[serde(default)]
92 pub session_id: String,
93}
94
95fn default_artifact_crud_version() -> u64 {
96 1
97}
98
99impl Default for ArtifactCrudMeta {
100 fn default() -> Self {
101 Self {
102 version: default_artifact_crud_version(),
103 updated_at: String::new(),
104 session_id: String::new(),
105 }
106 }
107}
108
109impl ArtifactCrudMeta {
110 /// Parse from `ArtifactRow::meta`. Any non-JSON or unrecognized shape
111 /// (e.g. another producer's freeform `meta`) yields the version-1
112 /// default rather than an error — this is bookkeeping, not a contract
113 /// other producers must satisfy.
114 pub fn from_meta_str(meta: &str) -> Self {
115 serde_json::from_str(meta).unwrap_or_default()
116 }
117
118 pub fn to_meta_string(&self) -> String {
119 serde_json::to_string(self).unwrap_or_default()
120 }
121}