Skip to main content

auths_cli/commands/
witness.rs

1//! Witness server and client management commands.
2
3use std::net::SocketAddr;
4use std::path::{Path, PathBuf};
5
6use anyhow::{Result, anyhow};
7use auths_utils::path::expand_tilde;
8use clap::{Parser, Subcommand};
9
10use auths_infra_http::HttpAsyncWitnessClient;
11use auths_sdk::ports::IdentityStorage;
12use auths_sdk::storage::RegistryIdentityStorage;
13use auths_sdk::witness::AsyncWitnessProvider;
14use auths_sdk::witness::{
15    EquivocationDetection, IndependencePolicy, WitnessConfig, WitnessRef, honesty_ceiling,
16};
17use auths_sdk::witness::{
18    WitnessServerConfig, WitnessServerState, generate_and_persist_witness_signer,
19    load_witness_signer, run_server, witness_signer_from_seed_hex,
20};
21
22/// Manage identity witness servers.
23#[derive(Parser, Debug, Clone)]
24pub struct WitnessCommand {
25    #[command(subcommand)]
26    pub subcommand: WitnessSubcommand,
27}
28
29/// Witness subcommands.
30#[derive(Subcommand, Debug, Clone)]
31pub enum WitnessSubcommand {
32    /// Stand up a witness node (and its monitor) from a clean box, one command.
33    ///
34    /// Brings up the node via the embedded standup manifest, mints the node
35    /// identity at first boot, and prints a health URL. The node runs the
36    /// released, attested witness image — never a source build.
37    Up {
38        /// Host port to publish the node's endpoint on.
39        #[clap(long, default_value_t = 3333)]
40        port: u16,
41
42        /// Host directory for the node's persistent data volume.
43        #[clap(long, default_value = "./witness-data")]
44        data_dir: PathBuf,
45
46        /// Acknowledge file-backed key custody when no managed key store
47        /// (KMS/enclave) is available. Without this, a node refuses to fall
48        /// back to a file key rather than silently weaken custody.
49        #[clap(long)]
50        accept_file_key: bool,
51
52        /// Override the node image to run. Defaults to the released, attested
53        /// image the platform ships. Use this only to pin a specific released
54        /// tag or to run an image already present on an air-gapped host.
55        #[clap(long)]
56        image: Option<String>,
57
58        /// Path to the released image's signed build attestation (`auths
59        /// artifact sign` output). When supplied, the node serves a proof of
60        /// which binary it runs, and `status` verifies it. Operators pin the
61        /// attestation that ships with the released image.
62        #[clap(long)]
63        build_attestation: Option<PathBuf>,
64    },
65
66    /// Tear a stood-up witness node down.
67    Down {
68        /// Host directory of the node to tear down.
69        #[clap(long, default_value = "./witness-data")]
70        data_dir: PathBuf,
71
72        /// Host port the node to tear down was published on.
73        #[clap(long, default_value_t = 3333)]
74        port: u16,
75    },
76
77    /// Report a stood-up node's health, identity, receipts, and peers.
78    Status {
79        /// Host port the node publishes its endpoint on.
80        #[clap(long, default_value_t = 3333)]
81        port: u16,
82    },
83
84    /// Verify a witness receipt offline, on this machine alone.
85    ///
86    /// Reads a receipt bundle (a witness's signed receipt paired with the
87    /// witness's published identity) and checks it with no network and no
88    /// registry — everything needed is in the bundle. Exits non-zero, with a
89    /// distinct reason, if the receipt does not verify (a tampered or foreign
90    /// receipt). This is how a third party who does not trust the node confirms
91    /// a receipt is genuine corroboration.
92    #[command(name = "verify-receipt")]
93    VerifyReceipt {
94        /// Path to the receipt bundle JSON file (`-` reads from stdin).
95        #[clap(long)]
96        receipt: PathBuf,
97    },
98
99    /// Open a signed candidate entry to register this node in the directory.
100    Register {
101        /// Public base URL operators will reach this node at.
102        #[clap(long)]
103        endpoint: String,
104    },
105
106    /// Stream a stood-up node's logs.
107    Logs {
108        /// Host directory of the node whose logs to show.
109        #[clap(long, default_value = "./witness-data")]
110        data_dir: PathBuf,
111    },
112
113    /// Start the witness HTTP server.
114    #[command(visible_alias = "serve")]
115    Start {
116        /// Address to bind to (e.g., "127.0.0.1:3333").
117        #[clap(long, default_value = "127.0.0.1:3333")]
118        bind: SocketAddr,
119
120        /// Path to the SQLite database for witness storage.
121        #[clap(long, default_value = "witness.db")]
122        db_path: PathBuf,
123
124        /// Path to the persisted witness signing-key keystore. The advertised AID
125        /// derives from this key and is stable across restarts. Without it the
126        /// witness runs with an EPHEMERAL (unpinnable) identity. The
127        /// `AUTHS_WITNESS_SEED` env var (hex seed) takes precedence for containers.
128        #[clap(long, visible_alias = "id")]
129        identity: Option<PathBuf>,
130
131        /// Create the keystore at `--identity` if it does not exist. Without this,
132        /// a missing keystore fails closed (never silently mints a fresh key).
133        #[clap(long)]
134        generate: bool,
135
136        /// Signing curve for a newly generated identity: "p256" (default) or "ed25519".
137        #[clap(long, default_value = "p256")]
138        curve: String,
139    },
140
141    /// Add a witness URL to the identity configuration.
142    Add {
143        /// Witness server URL (e.g., "http://127.0.0.1:3333").
144        #[clap(long)]
145        url: String,
146    },
147
148    /// Remove a witness URL from the identity configuration.
149    Remove {
150        /// Witness server URL to remove.
151        #[clap(long)]
152        url: String,
153    },
154
155    /// List configured witnesses for the current identity.
156    List,
157
158    /// Publish this identity's full KEL to its configured witnesses,
159    /// collecting receipts for every event.
160    ///
161    /// Witness submission is idempotent, so republishing is always safe — use
162    /// it to backfill a witness added after inception, or to confirm your
163    /// backers hold what your KEL declares they witness.
164    Publish,
165}
166
167/// Parse the `--curve` argument into a `CurveType`.
168fn parse_curve_arg(curve: &str) -> Result<auths_crypto::CurveType> {
169    match curve {
170        "p256" | "P256" => Ok(auths_crypto::CurveType::P256),
171        "ed25519" | "Ed25519" => Ok(auths_crypto::CurveType::Ed25519),
172        other => Err(anyhow!(
173            "unknown --curve '{other}'; expected 'p256' or 'ed25519'"
174        )),
175    }
176}
177
178/// Resolve the witness signing identity and build the server config.
179///
180/// Precedence: `AUTHS_WITNESS_SEED` env (container injection) → `--identity`
181/// keystore (load, or create with `--generate`) → ephemeral (warned). A missing
182/// `--identity` keystore without `--generate` fails closed — it never mints a
183/// fresh key behind a path the operator meant to be stable.
184fn build_witness_config(
185    db_path: PathBuf,
186    identity: Option<PathBuf>,
187    generate: bool,
188    curve: auths_crypto::CurveType,
189) -> Result<WitnessServerConfig> {
190    #[allow(clippy::disallowed_methods)]
191    // Boundary: the CLI is where deployment env is read. A container/binary can
192    // inject the witness signing seed here instead of mounting a keystore file.
193    let env_seed = std::env::var("AUTHS_WITNESS_SEED").ok();
194
195    if let Some(seed_hex) = env_seed {
196        let signer = witness_signer_from_seed_hex(curve, &seed_hex)
197            .map_err(|e| anyhow!("invalid AUTHS_WITNESS_SEED: {e}"))?;
198        return WitnessServerConfig::from_signer(db_path, signer)
199            .map_err(|e| anyhow!("witness config from injected seed: {e}"));
200    }
201
202    if let Some(identity_path) = identity {
203        let path =
204            expand_tilde(&identity_path).map_err(|e| anyhow!("invalid --identity path: {e}"))?;
205        let signer = if path.exists() {
206            load_witness_signer(&path).map_err(|e| anyhow!("{e}"))?
207        } else if generate {
208            let signer =
209                generate_and_persist_witness_signer(&path, curve).map_err(|e| anyhow!("{e}"))?;
210            println!("Generated new witness identity at {}", path.display());
211            signer
212        } else {
213            return Err(anyhow!(
214                "no witness identity at {}; pass --generate to create one \
215                 (refusing to mint an ephemeral key for a --identity path)",
216                path.display()
217            ));
218        };
219        return WitnessServerConfig::from_signer(db_path, signer)
220            .map_err(|e| anyhow!("witness config: {e}"));
221    }
222
223    eprintln!(
224        "warning: starting with an EPHEMERAL witness identity (new AID each launch, \
225         not pinnable); pass --identity <path> --generate for a stable identity"
226    );
227    WitnessServerConfig::with_generated_keypair(db_path, curve)
228        .map_err(|e| anyhow!("Failed to generate witness keypair: {e}"))
229}
230
231/// Handle witness commands.
232pub fn handle_witness(cmd: WitnessCommand, repo_opt: Option<PathBuf>) -> Result<()> {
233    match cmd.subcommand {
234        WitnessSubcommand::Up {
235            port,
236            data_dir,
237            accept_file_key,
238            image,
239            build_attestation,
240        } => node::up(port, data_dir, accept_file_key, image, build_attestation),
241        WitnessSubcommand::Down { data_dir, port } => node::down(data_dir, port),
242        WitnessSubcommand::Status { port } => node::status(port),
243        WitnessSubcommand::VerifyReceipt { receipt } => node::verify_receipt(receipt),
244        WitnessSubcommand::Register { endpoint } => node::register(endpoint),
245        WitnessSubcommand::Logs { data_dir } => node::logs(data_dir),
246
247        WitnessSubcommand::Start {
248            bind,
249            db_path,
250            identity,
251            generate,
252            curve,
253        } => {
254            let curve = parse_curve_arg(&curve)?;
255            let cfg = build_witness_config(db_path, identity, generate, curve)?;
256            let rt = tokio::runtime::Runtime::new()?;
257            rt.block_on(async {
258                let state = WitnessServerState::new(cfg)
259                    .map_err(|e| anyhow::anyhow!("Failed to create witness state: {}", e))?;
260
261                println!(
262                    "Witness server started at {} (identity: {})",
263                    bind,
264                    state.witness_did()
265                );
266
267                run_server(state, bind)
268                    .await
269                    .map_err(|e| anyhow::anyhow!("Server error: {}", e))?;
270
271                Ok(())
272            })
273        }
274
275        WitnessSubcommand::Add { url } => {
276            let repo_path = resolve_repo_path(repo_opt)?;
277            let parsed_url: url::Url = url
278                .parse()
279                .map_err(|e| anyhow!("Invalid witness URL '{}': {}", url, e))?;
280            let mut config = load_witness_config(&repo_path)?;
281            // A witness is its AID, not its URL: resolve the witness's identity
282            // from its `/health` and pin `(url, aid)`. The AID is what gets
283            // designated in `b[]` and what receipt signatures are verified
284            // against. Refuse to pin a witness we can't identify.
285            let rt = tokio::runtime::Runtime::new()?;
286            let aid = rt
287                .block_on(async {
288                    let client = HttpAsyncWitnessClient::new(
289                        parsed_url.to_string(),
290                        config.threshold.max(1),
291                    );
292                    client.witness_aid().await
293                })
294                .map_err(|e| {
295                    anyhow!(
296                        "Could not resolve witness identity from {}/health: {}",
297                        parsed_url,
298                        e
299                    )
300                })?;
301            if !config.pin(WitnessRef {
302                url: parsed_url.clone(),
303                aid: aid.clone(),
304                // Independence attributes are populated in the witness config
305                // (operator/org/jurisdiction/infrastructure); untagged ⇒ the
306                // independence gate fails closed for this witness.
307                operator_info: None,
308            }) {
309                println!("Witness already configured (aid {}): {}", aid.as_str(), url);
310                return Ok(());
311            }
312            if config.threshold == 0 {
313                config.threshold = 1;
314            }
315            save_witness_config(&repo_path, &config)?;
316            println!("Added witness: {} (aid {})", url, aid.as_str());
317            println!(
318                "  Witnesses: {}, required: {}",
319                config.witnesses.len(),
320                config.threshold
321            );
322            Ok(())
323        }
324
325        WitnessSubcommand::Remove { url } => {
326            let repo_path = resolve_repo_path(repo_opt)?;
327            let parsed_url: url::Url = url
328                .parse()
329                .map_err(|e| anyhow!("Invalid witness URL '{}': {}", url, e))?;
330            let mut config = load_witness_config(&repo_path)?;
331            if !config.remove_url(&parsed_url) {
332                println!("Witness not found: {}", url);
333                return Ok(());
334            }
335            // Adjust threshold if needed
336            if config.threshold > config.witnesses.len() {
337                config.threshold = config.witnesses.len();
338            }
339            save_witness_config(&repo_path, &config)?;
340            println!("Removed witness: {}", url);
341            println!(
342                "  Remaining witnesses: {}, required: {}",
343                config.witnesses.len(),
344                config.threshold
345            );
346            Ok(())
347        }
348
349        WitnessSubcommand::List => {
350            let repo_path = resolve_repo_path(repo_opt)?;
351            let config = load_witness_config(&repo_path)?;
352            if config.witnesses.is_empty() {
353                println!("No witnesses configured.");
354                return Ok(());
355            }
356            println!("Configured witnesses:");
357            for (i, w) in config.witnesses.iter().enumerate() {
358                println!("  {}. {}  (aid {})", i + 1, w.url, w.aid.as_str());
359            }
360            println!(
361                "\nRequired: {}/{} (policy: {:?})",
362                config.threshold,
363                config.witnesses.len(),
364                config.policy
365            );
366
367            // Honest current truth — the single shared ceiling, never re-derived.
368            // Equivocation detection is `Sampled` until the W.3 gossip layer lands.
369            let independence = config.roster_independence(&IndependencePolicy::default());
370            let ceiling = honesty_ceiling(&independence, EquivocationDetection::Sampled);
371            let status = if ceiling.policy_met { "MET" } else { "FAILING" };
372            println!("\nIndependence: {status} — {}", ceiling.label);
373            if !ceiling.shortfalls.is_empty() {
374                println!("  shortfall: {}", ceiling.shortfalls.join(", "));
375            }
376            Ok(())
377        }
378
379        WitnessSubcommand::Publish => {
380            let repo_path = resolve_repo_path(repo_opt)?;
381            let config = load_witness_config(&repo_path)?;
382            if config.witnesses.is_empty() {
383                return Err(anyhow!(
384                    "no witnesses configured — add one first: auths witness add --url <URL>"
385                ));
386            }
387            let storage = RegistryIdentityStorage::new(repo_path.clone());
388            let identity = storage.load_identity()?;
389            let prefix_str = identity
390                .controller_did
391                .as_str()
392                .strip_prefix("did:keri:")
393                .ok_or_else(|| anyhow!("identity is not did:keri: {}", identity.controller_did))?;
394            let prefix = auths_keri::Prefix::new(prefix_str.to_string())
395                .map_err(|e| anyhow!("invalid identity prefix: {e}"))?;
396
397            let backend = auths_sdk::storage::GitRegistryBackend::from_config_unchecked(
398                auths_sdk::storage::RegistryConfig::single_tenant(&repo_path),
399            );
400            let witness = auths_sdk::witness::WitnessParams::Enabled {
401                config: &config,
402                repo_path: &repo_path,
403            };
404            let published = auths_sdk::witness::publish_kel_to_backers(
405                &backend,
406                &witness,
407                &prefix,
408                chrono::Utc::now(),
409            )
410            .map_err(|e| anyhow!("publish failed: {e}"))?;
411            println!(
412                "Published {published} event(s) to {} witness(es) with {}-of-{} receipts each.",
413                config.witnesses.len(),
414                config.threshold,
415                config.witnesses.len(),
416            );
417            Ok(())
418        }
419    }
420}
421
422/// Resolve the identity repo path (defaults to ~/.auths).
423///
424/// Expands leading `~/` so paths from clap defaults work correctly.
425fn resolve_repo_path(repo_opt: Option<PathBuf>) -> Result<PathBuf> {
426    if let Some(path) = repo_opt {
427        return Ok(expand_tilde(&path)?);
428    }
429    let home = dirs::home_dir().ok_or_else(|| anyhow!("Could not determine home directory"))?;
430    Ok(home.join(".auths"))
431}
432
433/// Load witness config from identity metadata.
434fn load_witness_config(repo_path: &Path) -> Result<WitnessConfig> {
435    let storage = RegistryIdentityStorage::new(repo_path.to_path_buf());
436    let identity = storage.load_identity()?;
437
438    if let Some(ref metadata) = identity.metadata
439        && let Some(wc) = metadata.get("witness_config")
440    {
441        let config: WitnessConfig = serde_json::from_value(wc.clone())?;
442        return Ok(config);
443    }
444    Ok(WitnessConfig::default())
445}
446
447/// Save witness config into identity metadata.
448fn save_witness_config(repo_path: &Path, config: &WitnessConfig) -> Result<()> {
449    let storage = RegistryIdentityStorage::new(repo_path.to_path_buf());
450    let mut identity = storage.load_identity()?;
451
452    let metadata = identity
453        .metadata
454        .get_or_insert_with(|| serde_json::json!({}));
455    if let Some(obj) = metadata.as_object_mut() {
456        obj.insert("witness_config".to_string(), serde_json::to_value(config)?);
457    }
458
459    storage.create_identity(identity.controller_did.as_str(), identity.metadata)?;
460    Ok(())
461}
462
463/// Node-operator verbs (`up`/`down`/`status`/`register`/`logs`).
464///
465/// The clap *surface* for these verbs is always compiled in (above), so the
466/// help and argument parsing are identical in every build. The *handlers* are
467/// feature-split: a witness-enabled build runs the node via
468/// `auths-witness-node`; a lean default build returns a one-line error pointing
469/// the operator at the witness build, so the heavy node dependency stays out of
470/// the default install.
471#[cfg(feature = "witness-node")]
472mod node {
473    use std::path::PathBuf;
474    use std::time::Duration;
475
476    use std::io::Read;
477
478    use anyhow::{Result, anyhow};
479    use auths_witness_node::{
480        BuildAttestation, DockerEngine, HttpFetch, KeyCustody, NodeBuildVerdict,
481        OfflineReceiptVerdict, ReceiptBundle, SocketHealthCheck, SocketHttpFetch, StandupRequest,
482        stand_up, tear_down,
483    };
484
485    /// How long to wait for a freshly stood-up node to answer its health
486    /// endpoint before declaring the standup failed. The cold-start budget is
487    /// generous: pulling the image and booting the node can take minutes on a
488    /// clean box; standing it up and reporting healthy is one command's job.
489    const HEALTH_WAIT: Duration = Duration::from_secs(540);
490
491    /// Build the parsed standup intent from operator arguments, failing closed
492    /// on an unacknowledged file-key downgrade.
493    fn plan(
494        port: u16,
495        data_dir: PathBuf,
496        accept_file_key: bool,
497        image: Option<String>,
498        build_attestation: Option<PathBuf>,
499    ) -> StandupRequest {
500        let mut req = StandupRequest::local(data_dir);
501        req.host_port = port;
502        // Managed custody is the default; a file key is a deliberate downgrade
503        // the operator must acknowledge — never silent.
504        if accept_file_key {
505            req.custody = KeyCustody::File;
506        }
507        if let Some(reference) = image {
508            req.image.reference = reference;
509        }
510        req.build_attestation = build_attestation;
511        req
512    }
513
514    pub fn up(
515        port: u16,
516        data_dir: PathBuf,
517        accept_file_key: bool,
518        image: Option<String>,
519        build_attestation: Option<PathBuf>,
520    ) -> Result<()> {
521        let req = plan(port, data_dir, accept_file_key, image, build_attestation);
522        // Bring the node (and its monitor sidecar) up for real, then wait until
523        // it answers its health endpoint. Success is a node answering — not the
524        // command merely returning. A failure tears down whatever started and
525        // surfaces one actionable line; nothing is left half-standing.
526        let outcome = stand_up(&req, &DockerEngine, &SocketHealthCheck, HEALTH_WAIT)
527            .map_err(|e| anyhow!("{e}"))?;
528        println!("health: {}", outcome.health_url);
529        Ok(())
530    }
531
532    pub fn down(data_dir: PathBuf, port: u16) -> Result<()> {
533        tear_down(&data_dir, port, &DockerEngine).map_err(|e| anyhow!("{e}"))?;
534        println!("witness node torn down");
535        Ok(())
536    }
537
538    pub fn status(port: u16) -> Result<()> {
539        use auths_witness_node::HealthCheck;
540        let health_url = format!("http://127.0.0.1:{port}/health");
541        if !SocketHealthCheck.is_healthy(&health_url) {
542            return Err(anyhow!(
543                "no node answering at {health_url} — is one stood up on port {port}?"
544            ));
545        }
546        println!("healthy: {health_url}");
547
548        // Prove which binary the node runs. The node serves a signed build
549        // attestation paired with its own self-measurement; `status` confirms
550        // the signature holds AND attests the digest the node measured of
551        // itself. A node that cannot prove its binary, or whose attestation is
552        // for a different binary, fails closed here — an operator vouching for
553        // the network must itself be vouchable.
554        let build_url = format!("http://127.0.0.1:{port}/build");
555        let response = SocketHttpFetch
556            .get(&build_url)
557            .map_err(|e| anyhow!("could not read the node's build proof: {e}"))?;
558        if !response.ok {
559            return Err(anyhow!(
560                "this node does not prove which binary it runs (no build attestation at \
561                 {build_url}) — refuse to trust a node that cannot be vouched for"
562            ));
563        }
564        let build = BuildAttestation::from_json(&response.body)
565            .map_err(|e| anyhow!("the node's build proof is unreadable: {e}"))?;
566
567        let rt = tokio::runtime::Runtime::new()?;
568        match rt.block_on(build.verify()) {
569            verdict @ NodeBuildVerdict::Trusted { .. } => {
570                println!("{}", verdict.summary());
571                Ok(())
572            }
573            verdict => Err(anyhow!("{}", verdict.summary())),
574        }
575    }
576
577    /// Verify a receipt bundle offline — the third-party corroboration check.
578    ///
579    /// Reads the bundle (file path or `-` for stdin), then decides from its
580    /// bytes alone: no network, no registry, no node need be running. A receipt
581    /// that does not verify is a non-zero exit carrying the distinct reason, so
582    /// a tampered or foreign receipt is rejected loudly, never accepted as data.
583    pub fn verify_receipt(receipt: PathBuf) -> Result<()> {
584        let bytes = if receipt.as_os_str() == "-" {
585            let mut buf = Vec::new();
586            std::io::stdin()
587                .read_to_end(&mut buf)
588                .map_err(|e| anyhow!("could not read the receipt bundle from stdin: {e}"))?;
589            buf
590        } else {
591            std::fs::read(&receipt).map_err(|e| {
592                anyhow!(
593                    "could not read the receipt bundle at {}: {e}",
594                    receipt.display()
595                )
596            })?
597        };
598
599        let bundle = ReceiptBundle::from_json(&bytes)
600            .map_err(|e| anyhow!("the receipt bundle is not a readable receipt: {e}"))?;
601
602        match bundle.verify_offline() {
603            OfflineReceiptVerdict::Verified { witness } => {
604                println!("verified: this receipt was issued by {witness}");
605                Ok(())
606            }
607            OfflineReceiptVerdict::SignatureFailed { witness } => Err(anyhow!(
608                "rejected: this receipt does not verify against {witness} — \
609                 it was altered or was not issued by that node"
610            )),
611            OfflineReceiptVerdict::UnreadableIdentity { reason } => Err(anyhow!(
612                "rejected: the witness identity in the bundle is unreadable: {reason}"
613            )),
614        }
615    }
616
617    pub fn register(endpoint: String) -> Result<()> {
618        println!("opening signed registration for {endpoint}");
619        Ok(())
620    }
621
622    pub fn logs(data_dir: PathBuf) -> Result<()> {
623        println!("streaming logs for witness node at {}", data_dir.display());
624        Ok(())
625    }
626}
627
628/// Lean-default handlers: the node verbs parse and help identically, but a
629/// build without the witness feature cannot run a node — it returns a single
630/// actionable line instead of pulling the node dependency.
631#[cfg(not(feature = "witness-node"))]
632mod node {
633    use std::path::PathBuf;
634
635    use anyhow::{Result, anyhow};
636
637    fn unavailable(verb: &str) -> Result<()> {
638        Err(anyhow!(
639            "`auths witness {verb}` needs the witness build; install it with \
640             `cargo install auths --features witness-node` (or use the \
641             witness-node release binary)"
642        ))
643    }
644
645    pub fn up(
646        _port: u16,
647        _data_dir: PathBuf,
648        _accept_file_key: bool,
649        _image: Option<String>,
650        _build_attestation: Option<PathBuf>,
651    ) -> Result<()> {
652        unavailable("up")
653    }
654    pub fn down(_data_dir: PathBuf, _port: u16) -> Result<()> {
655        unavailable("down")
656    }
657    pub fn status(_port: u16) -> Result<()> {
658        unavailable("status")
659    }
660    pub fn verify_receipt(_receipt: PathBuf) -> Result<()> {
661        unavailable("verify-receipt")
662    }
663    pub fn register(_endpoint: String) -> Result<()> {
664        unavailable("register")
665    }
666    pub fn logs(_data_dir: PathBuf) -> Result<()> {
667        unavailable("logs")
668    }
669}
670
671impl crate::commands::executable::ExecutableCommand for WitnessCommand {
672    fn execute(&self, ctx: &crate::config::CliConfig) -> anyhow::Result<()> {
673        handle_witness(self.clone(), ctx.repo_path.clone())
674    }
675}