Skip to main content

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}