Skip to main content

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}