holger-traits 0.6.8

Holger guards your artifacts at rest. May Allfather Odin watch over every bit.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
//! Foundational contracts and shared types for holger — the seam every backend,
//! upstream, and connector implements, plus the coordinate/format vocabulary they
//! all speak. This crate deliberately sits at the bottom of the dep graph and
//! stays thin: it depends on `znippy-common` (for the source-of-truth type ids)
//! but NOT on the sibling `znippy-plugin-*` crates.
//!
//! The three server-side seams:
//!   * [`RepositoryBackendTrait`] — a repo that serves an ecosystem's native index
//!     from a read-only znippy archive (or disk); `has_archive`/`archive_*` default
//!     to "no archive" so file-backed and writable repos inherit them unchanged.
//!   * [`RemoteUpstream`] — a read-oriented `RepositoryBackendTrait` that pull-through-
//!     caches from Nexus/Artifactory/holger/crates.io; drops into `ProxyBackend`
//!     where `fetch()` is the read and a writable primary write-through-caches the hit.
//!     Its HTTP is impl-side and SYNC (`ureq`), never `reqwest` — the server request
//!     path runs inside the async hyper handler, where a blocked-on `reqwest` panics.
//!   * [`HolgerObject`] — the transport-agnostic facade (in-process engine vs gRPC
//!     client, one `Arc<dyn>` handle), the Rust mirror of `holger.proto`.
//!
//! Agent-side, [`ConnectorTrait`] moves assets between a Source and Target; its
//! [`UpstreamAuth`] cousin here is the *server-side* auth enum, distinct from the
//! agent's `ConnectorAuth`.
//!
//! GOTCHA: znippy is the single source of truth for [`ArtifactFormat::znippy_type_id`]
//! discriminants — most are derived by calling the znippy handler's `type_id()`
//! directly so they can't drift, but `Pip` (2) and `Maven3` (3) stay hardcoded
//! literals because their native handlers live in the plugin crates this crate
//! won't depend on. The `znippy_type_ids_match_*` tests guard those two.

use async_trait::async_trait;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ArtifactId {
    pub namespace: Option<String>,
    pub name: String,
    pub version: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum ArtifactFormat {
    Maven3,
    Pip,
    Rust,
    Go,
    Nuget,
    Npm,
    Gem,
    Deb,
    Rpm,
    Helm,
    Docker,
    Conda,
    Composer,
    /// znippy's own native package format (a merged/signed `.znippy` package
    /// served as the artifact itself, rather than a backing store for some
    /// other ecosystem). The one format that is znippy all the way down.
    Znippy,
    Raw,
}

impl ArtifactFormat {
    /// Canonical znippy `pkg_type` / DenseUnion discriminant for this format.
    ///
    /// znippy is the **single source of truth** for these numbers (see
    /// `znippy-common`'s native + skeleton handlers). Every variant whose handler
    /// is reachable from `znippy-common` calls that handler's
    /// [`ArchiveTypePlugin::type_id`] directly — including `Rust`, whose native
    /// `cargo_native::CargoPlugin` lives in `znippy-common` — so the two can never
    /// drift (the test below asserts the equality as data). Only `Pip` (2) and
    /// `Maven3` (3) remain literals: their native handlers live in the sibling
    /// `znippy-plugin-python` / `znippy-plugin-maven` crates, which this
    /// foundational `traits` crate deliberately does NOT depend on (keeping the
    /// base dep graph thin). The `znippy_type_ids_match_*` tests assert those two
    /// literals still equal znippy's published discriminants.
    pub fn znippy_type_id(&self) -> i8 {
        use znippy_common::plugins::{cargo_native, skeletons};
        use znippy_common::plugin::ArchiveTypePlugin;
        match self {
            // Native cargo handler lives in znippy-common — derive, don't hardcode.
            ArtifactFormat::Rust => cargo_native::CargoPlugin::new().type_id(),
            ArtifactFormat::Pip => 2,
            ArtifactFormat::Maven3 => 3,
            ArtifactFormat::Go => skeletons::GoPlugin.type_id(),
            ArtifactFormat::Nuget => skeletons::NugetPlugin.type_id(),
            // npm is a real native handler now (`plugins::npm_native`), not a skeleton.
            ArtifactFormat::Npm => znippy_common::plugins::npm_native::NpmPlugin.type_id(),
            // rpm is a real native handler now (`plugins::rpm_native`), not a skeleton.
            ArtifactFormat::Rpm => znippy_common::plugins::rpm_native::RpmPlugin.type_id(),
            // deb is a real native handler now (`plugins::deb_native`), not a skeleton.
            ArtifactFormat::Deb => znippy_common::plugins::deb_native::DebPlugin.type_id(),
            // gem is a real native handler now (`plugins::gem_native`), not a skeleton.
            ArtifactFormat::Gem => znippy_common::plugins::gem_native::GemPlugin.type_id(),
            ArtifactFormat::Docker => skeletons::DockerPlugin.type_id(),
            ArtifactFormat::Helm => skeletons::HelmPlugin.type_id(),
            // conda is a real native handler now (`plugins::conda_native`), not a skeleton.
            ArtifactFormat::Conda => znippy_common::plugins::conda_native::CondaPlugin.type_id(),
            ArtifactFormat::Composer => skeletons::ComposerPlugin.type_id(),
            // znippy's own container format — not one of the ecosystem handler
            // discriminants, so it gets a dedicated reserved id.
            ArtifactFormat::Znippy => 100,
            ArtifactFormat::Raw => 0,
        }
    }

    /// Parse a Nexus/Artifactory/CLI format string into an `ArtifactFormat`.
    pub fn from_format_str(s: &str) -> Option<Self> {
        Some(match s.to_lowercase().as_str() {
            "maven2" | "maven" | "maven3" | "java" => ArtifactFormat::Maven3,
            "pip" | "pypi" | "python" => ArtifactFormat::Pip,
            "rust" | "cargo" => ArtifactFormat::Rust,
            "go" | "golang" => ArtifactFormat::Go,
            "nuget" | "dotnet" => ArtifactFormat::Nuget,
            "npm" | "node" => ArtifactFormat::Npm,
            "gem" | "ruby" | "rubygems" => ArtifactFormat::Gem,
            "deb" | "debian" | "apt" => ArtifactFormat::Deb,
            "rpm" | "yum" | "dnf" => ArtifactFormat::Rpm,
            "helm" | "chart" => ArtifactFormat::Helm,
            "docker" | "oci" => ArtifactFormat::Docker,
            "conda" | "anaconda" => ArtifactFormat::Conda,
            "composer" | "php" => ArtifactFormat::Composer,
            "znippy" | "snippy" => ArtifactFormat::Znippy,
            "raw" => ArtifactFormat::Raw,
            _ => return None,
        })
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum StorageType {
    Znippy,
    Rocksdb,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum RepositoryType {
    Rust,
    Pip,
    Maven3,
    Go,
    Nuget,
    Npm,
    Gem,
    Deb,
    Rpm,
    Helm,
    Docker,
    Conda,
    Composer,
    Znippy,
    Raw,
}

impl RepositoryType {
    pub fn endpoint_name(&self) -> &'static str {
        match self {
            RepositoryType::Rust => "rust",
            RepositoryType::Pip => "pip",
            RepositoryType::Maven3 => "maven3",
            RepositoryType::Go => "go",
            RepositoryType::Nuget => "nuget",
            RepositoryType::Npm => "npm",
            RepositoryType::Gem => "gem",
            RepositoryType::Deb => "deb",
            RepositoryType::Rpm => "rpm",
            RepositoryType::Helm => "helm",
            RepositoryType::Docker => "docker",
            RepositoryType::Conda => "conda",
            RepositoryType::Composer => "composer",
            RepositoryType::Znippy => "znippy",
            RepositoryType::Raw => "raw",
        }
    }
}

/// Stats for the znippy archive backing a repository (mirror of the proto
/// `ArchiveInfoResponse`). `archive_path` is the repository/archive name — a
/// human-readable handle, not a filesystem path. Defaults to "no archive" (a
/// backend that isn't archive-backed reports zero files / zero bytes).
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ArchiveInfo {
    pub file_count: u64,
    pub total_uncompressed_bytes: u64,
    pub archive_path: String,
}

#[async_trait]
pub trait RepositoryBackendTrait: Send + Sync {
    fn name(&self) -> &str;
    fn format(&self) -> ArtifactFormat;
    fn is_writable(&self) -> bool;

    fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>>;
    fn put(&self, id: &ArtifactId, data: &[u8]) -> anyhow::Result<()>;

    /// List the raw file paths inside the backing znippy archive, optionally
    /// filtered to those starting with `prefix`. These are archive-raw paths
    /// (format-agnostic), not parsed artifact ids. Default: empty (a backend
    /// with no archive — or that can't enumerate one — returns nothing).
    fn archive_files(&self, prefix: Option<&str>) -> anyhow::Result<Vec<String>> {
        let _ = prefix;
        Ok(Vec::new())
    }

    /// Stats for the backing znippy archive (file count + total uncompressed
    /// bytes + name). Default: the empty [`ArchiveInfo`] (no archive).
    fn archive_info(&self) -> anyhow::Result<ArchiveInfo> {
        Ok(ArchiveInfo::default())
    }

    /// List artifacts this backend holds, newest/any order, optionally filtered
    /// by a substring of the artifact name, capped at `limit`. Default: empty
    /// (a backend that can't enumerate returns nothing — the contract stays
    /// valid, the listing just stays empty).
    fn list(&self, name_filter: Option<&str>, limit: usize) -> anyhow::Result<Vec<ArtifactEntry>> {
        let _ = (name_filter, limit);
        Ok(Vec::new())
    }

    /// Whether this backend is backed by a znippy archive. Defaults to `false`
    /// (file-backed / writable repos carry no archive). Znippy-backed repos
    /// override this to return `true` when their archive reader is loaded.
    fn has_archive(&self) -> bool {
        false
    }

    fn handle_http2_request(
        &self,
        method: &str,
        suburl: &str,
        body: &[u8],
    ) -> anyhow::Result<(u16, Vec<(String, String)>, Vec<u8>)>;
}

// === Remote-cache upstream abstraction (server-side, sync request path) ===
//
// A remote-cache *upstream* lets a holger repository transparently fetch-and-
// cache an artifact from an upstream registry (Nexus, Artifactory, another
// holger, crates.io) on a local miss. It is a read-oriented
// [`RepositoryBackendTrait`] so it drops straight into the existing
// `ProxyBackend` pull-through cache: `fetch()` returns the upstream bytes and a
// writable primary write-through-caches them.
//
// This lives in `traits` (the single source of truth) so every concrete impl —
// `NexusUpstream`, `ArtifactoryUpstream`, … in `holger-server-lib` — shares one
// auth enum, one metadata shape, and one coordinate→URL path mapping. The HTTP
// itself is implemented impl-side with a SYNC, runtime-agnostic client (`ureq`),
// because the server request path runs inside the async hyper handler where a
// blocked-on `reqwest` would panic.

/// How a remote-cache upstream authenticates with its registry. Promoted into
/// `traits` as the single source of truth shared by every upstream impl
/// (Nexus = Basic, Artifactory = Basic/Bearer/API-key, holger = Bearer).
///
/// Distinct from the agent-side `ConnectorAuth` (async/reqwest, agent-only),
/// which keeps its own OIDC-token-fetch semantics; this enum is the server-side
/// upstream contract.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Default)]
pub enum UpstreamAuth {
    /// No authentication.
    #[default]
    None,
    /// HTTP Basic auth (Nexus, Artifactory).
    Basic { username: String, password: String },
    /// Bearer token (holger OIDC, Artifactory access token).
    Bearer { token: String },
    /// Custom API-key header, e.g. Artifactory `X-JFrog-Art-Api-Key`.
    ApiKey { header: String, key: String },
    /// mTLS client cert (built into the HTTP client, no per-request header).
    Mtls { cert_pem: String, key_pem: String },
}


/// Size + checksum(s) + content-type for an upstream artifact. Lets the proxy
/// verify a fetched body before write-through caching and answer index/metadata
/// probes without downloading the body.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ArtifactMeta {
    pub size: Option<u64>,
    pub sha256: Option<String>,
    pub content_type: Option<String>,
}

/// A read-oriented remote-cache upstream: a [`RepositoryBackendTrait`] that
/// knows its upstream base URL + auth and can cheaply probe existence/metadata.
/// Plugs into `ProxyBackend` unchanged — `fetch()` is the read, a writable
/// primary caches the hit.
pub trait RemoteUpstream: RepositoryBackendTrait {
    /// The upstream base, e.g. `https://nexus.corp` or `https://jfrog.corp`.
    fn base_url(&self) -> &str;

    /// Credentials attached to upstream requests.
    fn auth(&self) -> &UpstreamAuth;

    /// Cheap existence probe. Default: a `fetch` that discards the body — impls
    /// should override with a HEAD where the registry supports one.
    fn exists(&self, id: &ArtifactId) -> anyhow::Result<bool> {
        Ok(self.fetch(id)?.is_some())
    }

    /// Size + checksum + content-type, used to verify before write-through
    /// caching. `Ok(None)` when the upstream has no such artifact.
    fn metadata(&self, id: &ArtifactId) -> anyhow::Result<Option<ArtifactMeta>>;
}

/// Map an artifact coordinate to the upstream layout path for `fmt`. This is the
/// per-ecosystem translation Nexus/Artifactory share (maven2/npm/pypi/… layouts
/// relative to the repository root). Joined onto the registry-specific content
/// base (`{base}/repository/{repo}/…` for Nexus, `{base}/artifactory/{repo}/…`
/// for Artifactory) by the impl.
pub fn upstream_path(fmt: ArtifactFormat, id: &ArtifactId) -> String {
    match fmt {
        ArtifactFormat::Rust => format!("{n}/{n}-{v}.crate", n = id.name, v = id.version),
        ArtifactFormat::Maven3 => {
            // group.id → group/id ; classic maven2 layout.
            let g = id.namespace.as_deref().unwrap_or("").replace('.', "/");
            format!("{g}/{a}/{v}/{a}-{v}.jar", a = id.name, v = id.version)
        }
        ArtifactFormat::Pip => {
            format!("packages/source/{n}/{n}-{v}.tar.gz", n = id.name, v = id.version)
        }
        ArtifactFormat::Npm => format!("{n}/-/{n}-{v}.tgz", n = id.name, v = id.version),
        ArtifactFormat::Nuget => {
            let n = id.name.to_lowercase();
            format!("{n}/{v}/{n}.{v}.nupkg", n = n, v = id.version)
        }
        ArtifactFormat::Gem => format!("gems/{n}-{v}.gem", n = id.name, v = id.version),
        _ => format!("{}/{}", id.name, id.version),
    }
}

// === Connector trait for external systems (Nexus, Artifactory, crates.io) ===

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoteRepository {
    pub name: String,
    pub format: String,
    pub repo_type: String,
    pub url: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoteAsset {
    pub path: String,
    pub download_url: String,
    pub content_type: Option<String>,
    pub size: Option<u64>,
}

#[async_trait]
pub trait ConnectorTrait: Send + Sync {
    fn name(&self) -> &str;

    async fn list_repositories(&self) -> anyhow::Result<Vec<RemoteRepository>>;

    async fn list_assets(&self, repository: &str) -> anyhow::Result<Vec<RemoteAsset>>;

    async fn download_asset(&self, asset: &RemoteAsset) -> anyhow::Result<Vec<u8>>;

    async fn upload_asset(
        &self,
        repository: &str,
        path: &str,
        data: &[u8],
    ) -> anyhow::Result<()>;
}


// === HolgerObject: the transport-agnostic facade ===
//
// `HolgerObject` is the single Rust contract an external partner programs
// against. Hold an `Arc<dyn HolgerObject>` and you don't care what's behind the
// vtable — exactly like a Java interface reference. Two transports implement it
// today:
//   * an in-process adapter that calls the local Holger engine directly, and
//   * a remote adapter that forwards each call over gRPC (tonic client).
// The language-neutral contract for non-Rust / remote partners is `holger.proto`
// (the generated client/server stubs); this trait mirrors it for Rust callers.

/// Summary of one configured repository (mirror of the proto `RepositoryInfo`).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RepositoryInfo {
    pub name: String,
    pub repo_type: String,
    pub writable: bool,
    pub has_archive: bool,
}

/// One artifact in a repository listing (mirror of the proto `ArtifactEntry`).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ArtifactEntry {
    pub id: ArtifactId,
    pub size_bytes: i64,
    pub content_type: String,
}

/// Server health (mirror of the proto `HealthResponse`).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Health {
    pub status: String,
    pub version: String,
    pub uptime_seconds: i64,
}

/// Server operating profile (mirror of the proto `ServerProfileResponse`).
///
/// This is *server truth*: whether the server accepts writes at all, derived
/// from the live route table (or an explicit sealed-mode override). The UI
/// drives its static "silent running" chrome off this rather than a local
/// toggle.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ServerProfile {
    /// "static" (sealed / read-only) or "dynamic" (writable / proxy).
    pub profile: String,
    /// True when the server accepts no writes on any repository.
    pub read_only: bool,
    /// Human label for the UI chrome / badge.
    pub label: String,
    /// Number of configured repositories that accept writes.
    pub writable_repo_count: i32,
}

impl Default for ServerProfile {
    /// A conservative dynamic (writable) default for transports that don't
    /// report a profile.
    fn default() -> Self {
        Self {
            profile: "dynamic".into(),
            read_only: false,
            label: "DYNAMIC / WRITABLE".into(),
            writable_repo_count: 0,
        }
    }
}

/// Transport-agnostic Holger facade. Callable from the CLI or a UI backend over
/// either transport (direct in-process or gRPC) behind the same handle.
#[async_trait]
pub trait HolgerObject: Send + Sync {
    /// Fetch an artifact by id from a named repository.
    async fn fetch(&self, repository: &str, id: &ArtifactId) -> anyhow::Result<Option<Vec<u8>>>;

    /// Store an artifact (write-enabled repositories only).
    async fn put(&self, repository: &str, id: &ArtifactId, data: &[u8]) -> anyhow::Result<()>;

    /// List all configured repositories.
    async fn list_repositories(&self) -> anyhow::Result<Vec<RepositoryInfo>>;

    /// List artifacts in a repository, optionally filtered by a `name_filter`
    /// substring, capped at `limit`, paged via an opaque `page_token`. Returns
    /// the page of entries plus the `next_page_token` (empty when exhausted).
    ///
    /// The default implementation returns an empty page, so transports that do
    /// not (yet) surface a listing — e.g. the in-process [`HolgerObject`]s whose
    /// server returns empty today — inherit it unchanged. Override it where the
    /// transport carries a real `ListArtifacts` call (the gRPC remote does).
    async fn list_artifacts(
        &self,
        repository: &str,
        name_filter: Option<String>,
        limit: u32,
        page_token: Option<String>,
    ) -> anyhow::Result<(Vec<ArtifactEntry>, String)> {
        let _ = (repository, name_filter, limit, page_token);
        Ok((Vec::new(), String::new()))
    }

    /// List the raw file paths inside the znippy archive backing `repository`,
    /// optionally filtered to those starting with `prefix`. These are
    /// archive-raw paths (format-agnostic), not parsed artifact ids.
    ///
    /// The default returns an empty list, so transports that don't (yet)
    /// surface archive browsing inherit it unchanged. Override it where the
    /// transport carries a real `ListArchiveFiles` call (the gRPC remote and
    /// in-process local both do).
    async fn list_archive_files(
        &self,
        repository: &str,
        prefix: Option<String>,
    ) -> anyhow::Result<Vec<String>> {
        let _ = (repository, prefix);
        Ok(Vec::new())
    }

    /// Stats for the znippy archive backing `repository` (file count + total
    /// uncompressed bytes + name). Default: the empty [`ArchiveInfo`].
    async fn archive_info(&self, repository: &str) -> anyhow::Result<ArchiveInfo> {
        let _ = repository;
        Ok(ArchiveInfo::default())
    }

    /// Server health / version / uptime.
    async fn health(&self) -> anyhow::Result<Health>;

    /// Server operating profile (static/read-only vs dynamic/writable).
    ///
    /// The default returns a dynamic (writable) profile so transports that
    /// don't report one inherit it unchanged. Override it where the transport
    /// can derive real server truth (the gRPC remote via `AdminService.
    /// ServerProfile`, the in-process local from its route table).
    async fn server_profile(&self) -> anyhow::Result<ServerProfile> {
        Ok(ServerProfile::default())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Emit one functional-status row for a real check. Gated behind
    /// `--features testmatrix` so release builds strip it (dep is optional).
    #[cfg(feature = "testmatrix")]
    fn fstatus(component: &str, check: &str, ok: bool, detail: &str) {
        nornir_testmatrix::functional_status(component, check, ok, detail);
    }

    #[test]
    fn znippy_type_ids_match_znippy_skeletons() {
        // znippy is the source of truth — these literals must equal the znippy
        // handler discriminants, or the mapping has drifted.
        assert_eq!(ArtifactFormat::Go.znippy_type_id(), 4);
        assert_eq!(ArtifactFormat::Nuget.znippy_type_id(), 5);
        assert_eq!(ArtifactFormat::Npm.znippy_type_id(), 6);
        assert_eq!(ArtifactFormat::Rpm.znippy_type_id(), 8);
        assert_eq!(ArtifactFormat::Deb.znippy_type_id(), 9);
        assert_eq!(ArtifactFormat::Gem.znippy_type_id(), 11);
        assert_eq!(ArtifactFormat::Docker.znippy_type_id(), 12);
        assert_eq!(ArtifactFormat::Helm.znippy_type_id(), 13);
        assert_eq!(ArtifactFormat::Conda.znippy_type_id(), 14);
        assert_eq!(ArtifactFormat::Composer.znippy_type_id(), 17);
        // natives
        assert_eq!(ArtifactFormat::Rust.znippy_type_id(), 1);
        assert_eq!(ArtifactFormat::Pip.znippy_type_id(), 2);
        assert_eq!(ArtifactFormat::Maven3.znippy_type_id(), 3);

        #[cfg(feature = "testmatrix")]
        {
            let ok = ArtifactFormat::Helm.znippy_type_id() == 13
                && ArtifactFormat::Rust.znippy_type_id() == 1
                && ArtifactFormat::Composer.znippy_type_id() == 17;
            fstatus(
                "traits",
                "znippy_type_ids_match",
                ok,
                &format!(
                    "Rust=1 Pip=2 Maven3=3 Helm=13 Composer=17 (helm={})",
                    ArtifactFormat::Helm.znippy_type_id()
                ),
            );
        }
    }

    /// Drift guard sourced from znippy itself: instead of comparing against
    /// hand-typed literals, assert each format's `znippy_type_id()` equals the
    /// `type_id()` of the corresponding znippy-common handler. If holger's table
    /// (or znippy's handler register) ever drifts, this fails — proving the two
    /// stay a single source of truth, as data rather than by eyeballing.
    ///
    /// Only the handlers reachable from `znippy-common` are covered (every native
    /// + skeleton handler except the maven/python natives, which live in the
    /// sibling plugin crates that `traits` does not depend on — those two stay
    /// guarded by the literal assertions in `znippy_type_ids_match_znippy_skeletons`).
    #[test]
    fn znippy_type_ids_derive_from_znippy_handlers_not_holger_copies() {
        use znippy_common::plugin::ArchiveTypePlugin;
        use znippy_common::plugins::{
            cargo_native::CargoPlugin, conda_native::CondaPlugin, deb_native::DebPlugin,
            gem_native::GemPlugin, npm_native::NpmPlugin, rpm_native::RpmPlugin, skeletons,
        };

        // (format, znippy handler's own type_id) — the right-hand side is znippy's
        // source of truth, never a literal copied into holger.
        let pairs: &[(ArtifactFormat, i8)] = &[
            // Rust is now derived from cargo_native (in znippy-common) rather than
            // a hardcoded `1` — this asserts the derivation, not a copy.
            (ArtifactFormat::Rust, CargoPlugin::new().type_id()),
            (ArtifactFormat::Npm, NpmPlugin.type_id()),
            (ArtifactFormat::Gem, GemPlugin.type_id()),
            (ArtifactFormat::Conda, CondaPlugin.type_id()),
            (ArtifactFormat::Rpm, RpmPlugin.type_id()),
            (ArtifactFormat::Deb, DebPlugin.type_id()),
            (ArtifactFormat::Go, skeletons::GoPlugin.type_id()),
            (ArtifactFormat::Nuget, skeletons::NugetPlugin.type_id()),
            (ArtifactFormat::Docker, skeletons::DockerPlugin.type_id()),
            (ArtifactFormat::Helm, skeletons::HelmPlugin.type_id()),
            (ArtifactFormat::Composer, skeletons::ComposerPlugin.type_id()),
        ];

        let mut all_ok = true;
        for (fmt, znippy_id) in pairs {
            let holger_id = fmt.znippy_type_id();
            if holger_id != *znippy_id {
                all_ok = false;
            }
            assert_eq!(
                holger_id, *znippy_id,
                "{fmt:?}: holger znippy_type_id()={holger_id} drifted from znippy handler type_id()={znippy_id}"
            );
        }

        #[cfg(feature = "testmatrix")]
        fstatus(
            "traits",
            "znippy_type_ids_derive_from_handlers",
            all_ok,
            &format!("{} formats derive their discriminant from znippy handlers", pairs.len()),
        );
        let _ = all_ok;
    }

    #[test]
    fn upstream_path_maps_per_ecosystem_layout() {
        let id = ArtifactId { namespace: None, name: "serde".into(), version: "1.0.0".into() };
        assert_eq!(upstream_path(ArtifactFormat::Rust, &id), "serde/serde-1.0.0.crate");
        assert_eq!(upstream_path(ArtifactFormat::Npm, &id), "serde/-/serde-1.0.0.tgz");
        assert_eq!(upstream_path(ArtifactFormat::Pip, &id), "packages/source/serde/serde-1.0.0.tar.gz");

        let mvn = ArtifactId {
            namespace: Some("com.example".into()),
            name: "lib".into(),
            version: "2.1".into(),
        };
        assert_eq!(upstream_path(ArtifactFormat::Maven3, &mvn), "com/example/lib/2.1/lib-2.1.jar");
    }

    #[test]
    fn upstream_auth_defaults_to_none() {
        assert_eq!(UpstreamAuth::default(), UpstreamAuth::None);
        assert_ne!(
            UpstreamAuth::Bearer { token: "t".into() },
            UpstreamAuth::Basic { username: "u".into(), password: "p".into() }
        );
    }

    #[test]
    fn format_str_roundtrips_aliases() {
        assert_eq!(ArtifactFormat::from_format_str("cargo"), Some(ArtifactFormat::Rust));
        assert_eq!(ArtifactFormat::from_format_str("golang"), Some(ArtifactFormat::Go));
        assert_eq!(ArtifactFormat::from_format_str("dotnet"), Some(ArtifactFormat::Nuget));
        assert_eq!(ArtifactFormat::from_format_str("oci"), Some(ArtifactFormat::Docker));
        assert_eq!(ArtifactFormat::from_format_str("nope"), None);

        #[cfg(feature = "testmatrix")]
        {
            let ok = ArtifactFormat::from_format_str("cargo") == Some(ArtifactFormat::Rust)
                && ArtifactFormat::from_format_str("oci") == Some(ArtifactFormat::Docker)
                && ArtifactFormat::from_format_str("nope").is_none();
            fstatus(
                "traits",
                "format_str_aliases_roundtrip",
                ok,
                "cargo->Rust golang->Go dotnet->Nuget oci->Docker nope->None",
            );
        }
    }
}