zenkey_fleet/report/blob.rs
1//! The `@blob` plane (RFC 07 §2, issues #58/#68).
2//!
3//! Deliberately **not** feature-gated, and deliberately carrying no `zblob`
4//! type. `zenkey-fleet`'s blob *transport* is optional (the `blob` feature);
5//! its blob *output shape* is not, because a report is a contract:
6//! `zenctl blob locate --format json` must serialize the same document
7//! whether or not the binary was built with the transport, and a frontend
8//! must be able to render a probe it deserialized from somewhere else
9//! entirely.
10
11use super::asked::Asked;
12use super::call::CallError;
13use serde::Serialize;
14
15// ─── the @blob plane (RFC 07 §2, issues #58/#68) ────────────────────────────
16//
17// These are deliberately **not** feature-gated, and deliberately carry no
18// `zblob` type. `zenkey-fleet`'s blob *transport* is optional (the `blob`
19// feature); its blob *output shape* is not, because a report is a contract:
20// `zenctl blob locate --format json` must serialize the same document whether
21// or not the binary was built with the transport, and a frontend must be able
22// to render a probe it deserialized from somewhere else entirely.
23
24/// Where a [`BlobList`]'s rows came from — the O5 provenance line.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
26#[serde(rename_all = "kebab-case")]
27pub enum BlobListSource {
28 /// Introspect slices served by live producers (RFC 08 §6).
29 Bus,
30 /// `--registry <dir>` TOMLs.
31 RegistryDirs,
32 /// Both, unioned.
33 Union,
34}
35
36/// One producer's declaration that it serves one `@blob` tier.
37#[derive(Debug, Clone, Serialize)]
38pub struct BlobTierRow {
39 pub producer: String,
40 pub registry_version: String,
41 /// The tier token **as declared**. A token RFC 07 §2 does not reserve is
42 /// carried verbatim and flagged by `known_tier` rather than dropped: a
43 /// declaration we do not understand is a fact about the fleet (RFC 09
44 /// §5.1 O1), not noise.
45 pub tier: String,
46 /// Whether `tier` is one of the three reserved tokens.
47 pub known_tier: bool,
48 /// Declared endpoints (`artifact` only, RFC 07 §2.2).
49 #[serde(skip_serializing_if = "Vec::is_empty")]
50 pub endpoints: Vec<String>,
51 /// Content-hash algorithm (`store` only).
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub algo: Option<String>,
54 /// The type whose payload carries the content root (RFC 07 §2.1).
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub reference: Option<String>,
57 /// The blob *content*'s encoding, when declared.
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub encoding: Option<String>,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub since: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub description: Option<String>,
64 /// Origins whose liveliness roster names this producer.
65 ///
66 /// `NotAsked` means the roster was never asked — an offline `--registry`
67 /// read learns nothing about who is up, and rendering that as "no origin
68 /// serves this tier" would report a verdict nobody obtained (RFC 09 §5.1
69 /// O4). Even when asked it is a *capability* claim: a producer that
70 /// declares a tier is saying it serves the endpoints, never that it holds
71 /// any particular blob. Only a probe answers that.
72 #[serde(skip_serializing_if = "Asked::is_not_asked", default)]
73 pub origins: Asked<Vec<String>>,
74}
75
76/// Which producers declare which `@blob` tiers (RFC 07 §2.7 / 08 §2).
77#[derive(Debug, Clone, Serialize)]
78pub struct BlobList {
79 pub tiers: Vec<BlobTierRow>,
80 pub source: BlobListSource,
81 /// How many slices were read. Without it an empty `tiers` reads as "nobody
82 /// serves blobs" when it may mean "nothing was asked" (RFC 09 §5.1 O6).
83 pub slices_considered: usize,
84 /// How many of those declared no `@blob` tier at all.
85 pub slices_without_blob: usize,
86}
87
88/// A holder's chunk availability for one artifact (RFC 07 §2.5's `have`).
89#[derive(Debug, Clone, Serialize)]
90pub struct BlobAvailability {
91 pub chunk_count: u32,
92 /// Chunks this holder can serve right now.
93 pub have: u32,
94 pub complete: bool,
95}
96
97/// A holder's manifest for one artifact (RFC 07 §2.2's `manifest`).
98#[derive(Debug, Clone, Serialize)]
99pub struct BlobManifest {
100 pub id: String,
101 /// Advisory only. It is never joined to any path — a remote party does not
102 /// choose where bytes land.
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub filename: Option<String>,
105 pub total_len: u64,
106 pub chunk_size: u32,
107 pub chunk_count: u32,
108 /// The content root, hex — RFC 07 §2.1's integrity anchor.
109 pub root: String,
110 pub created_ms: i64,
111}
112
113/// One origin that answered a probe, and what it said.
114#[derive(Debug, Clone, Serialize)]
115pub struct BlobHolder {
116 /// From the reply's **own** key. `"?"` only when that key neither parsed
117 /// under the base nor had an origin in position 1.
118 pub origin: String,
119 /// The concrete key this origin answered on — the only fetchable form
120 /// (RFC 07 §2.5: probe wide, fetch one).
121 pub key: String,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub availability: Option<BlobAvailability>,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub manifest: Option<BlobManifest>,
126 /// A per-holder observation the counters cannot carry — e.g. a tree
127 /// holder with every chunk but no index (v1.17), which an index fetch
128 /// will fail against despite a full-looking count. Rendered verbatim.
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub note: Option<String>,
131 /// It answered, and we could not read it: the encoding it declared and
132 /// why. Answering unreadably is not not answering (RFC 09 §5.1 O4).
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub unreadable: Option<String>,
135 /// An RFC 05 §3 error envelope.
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub error: Option<CallError>,
138}
139
140/// Who holds an artifact, and at which root (RFC 07 §2.5).
141#[derive(Debug, Clone, Serialize)]
142pub struct BlobProbeReport {
143 /// The target as spelled back: `artifact/<id>`, `tree/<hex>`, …
144 pub target: String,
145 pub tier: String,
146 /// The selectors actually asked. A probe's coverage claim is exactly this
147 /// list and no wider (RFC 09 §5.1 O5).
148 pub asked: Vec<String>,
149 /// Why nothing was asked, when nothing was — a store algorithm the
150 /// reference client does not speak, chiefly, now that every tier has a
151 /// probe endpoint (RFC 07 §2.5, v1.17). Renders instead of a holder
152 /// list; an unasked probe must never read as "no holders".
153 #[serde(skip_serializing_if = "Option::is_none")]
154 pub not_probed: Option<String>,
155 pub holders: Vec<BlobHolder>,
156 pub answered: usize,
157 /// Distinct content roots across holders.
158 ///
159 /// More than one is a **finding, not a tie-break**: the id is a name and
160 /// RFC 07 §2.1's root is what disambiguates it, so a caller facing two
161 /// roots must pin one rather than trust whoever answered first.
162 pub roots: Vec<String>,
163 /// Producers whose slice declares this tier — a capability claim, carried
164 /// so a silent probe stays legible (RFC 05 §3.1: silence is not a verdict,
165 /// and "nobody declares this" and "the declarers are down" are different
166 /// silences).
167 #[serde(skip_serializing_if = "Vec::is_empty")]
168 pub declared_by: Vec<String>,
169 /// How many registry slices the `declared_by` sweep read —
170 /// [`BlobList`]'s own solution, applied here (review finding R7). Without
171 /// it an empty `declared_by` conflates "no slice declares this tier" with
172 /// "no registry was loaded, so nobody was asked" (RFC 09 §5.1 O4) — the
173 /// third silence, beside the two above. Additive.
174 pub slices_considered: usize,
175}
176
177/// A fetch's progress, as the caller may render it.
178///
179/// Engine-owned rather than a re-export of the reference client's progress
180/// type: that one is `#[non_exhaustive]`, and a GUI message enum cannot carry
181/// a non-exhaustive payload without a wildcard arm in every match — which is
182/// how a new variant becomes invisible instead of a compile error.
183#[derive(Debug, Clone, Serialize)]
184#[serde(tag = "event", rename_all = "kebab-case")]
185pub enum BlobProgress {
186 Started {
187 total_len: u64,
188 chunk_count: u32,
189 },
190 /// A partial download resumed from its persisted chunk bitfield.
191 Resumed {
192 received: u32,
193 total: u32,
194 },
195 Chunk {
196 index: u32,
197 received: u32,
198 total: u32,
199 bytes_received: u64,
200 },
201 Verifying,
202 Completed {
203 path: String,
204 },
205 Cancelled {
206 received: u32,
207 total: u32,
208 },
209 Failed {
210 error: String,
211 },
212}
213
214/// What one fetch from one origin cost and proved (RFC 07 §2.1, §2.5, §2.6).
215#[derive(Debug, Clone, Serialize)]
216pub struct BlobFetchReport {
217 pub origin: String,
218 /// The one concrete key fetched from.
219 pub key: String,
220 pub dest: String,
221 pub bytes: u64,
222 pub chunks: u32,
223 /// Chunks a previous attempt had already banked.
224 pub chunks_resumed: u32,
225 /// Replies verification rejected **before disk** (RFC 07 §2.1).
226 pub rejected: u32,
227 pub retries: u32,
228 pub elapsed_ms: u64,
229 pub root: String,
230 /// `false` = trust-on-first-use, which the caller had to ask for out loud.
231 /// RFC 07 §2.1 requires a reference to carry the root; an operator typing
232 /// an id by hand has no reference, so the report says which it was.
233 pub root_pinned: bool,
234 /// The priority the GETs actually rode at (RFC 07 §2.6) — reported rather
235 /// than assumed, and filled from the same constant the client is built
236 /// with, so the sentence cannot drift from the behaviour.
237 pub priority: String,
238}
239
240/// A validated tree-index summary from one origin (RFC 07 §2.3, v1.17):
241/// inspection **without a content store**. The reply chain is untrusted at
242/// every step — index chunks verify against their own addresses and the
243/// reassembled index verifies against the root the caller asked for — so this
244/// is pinned by construction, and browsing a tree costs its index, never its
245/// content.
246#[derive(Debug, Clone, Serialize)]
247pub struct BlobTreeIndexReport {
248 pub origin: String,
249 /// The one concrete key asked.
250 pub key: String,
251 /// The identity fetched — also the pin.
252 pub root: String,
253 /// Directory entries of every kind.
254 pub entries: usize,
255 /// Files among them.
256 pub files: usize,
257 /// Total content bytes the snapshot references.
258 pub total_size: u64,
259 /// Distinct content chunks the snapshot references.
260 pub chunks: usize,
261 pub elapsed_ms: u64,
262 /// See [`BlobFetchReport::priority`].
263 pub priority: String,
264}