Chio/Docs
LOGIN · JOIN

PlatformLifecycle & Health

Node

Node Preflight

Six ordered probes over toolchain, registry, bundle age, telemetry, kernel metrics, and config. One report, one exit code.

Before the process, not during it

This page covers what an operator can determine about a host before a kernel starts on it: what is installed, what is reachable, what parses. Once the process is running, the equivalent question is answered by Health & Readiness, which reads live in-memory flags and exercises the receipt write path. One caveat on the split: the fifth probe will GET a metrics URL if you give it one, so the doctor is not strictly offline. It reads a body as text and matches a string. It never asks a node a question a node answers.

What chio doctor answers

chio doctor is the preflight. It runs a fixed set of six probes in a fixed order, aggregates their reports, and derives one process exit code from the worst severity any of them observed. It is aimed at a developer workspace and at a CI job about to build against Chio: the first probe shells out to cargo and two of the six read files relative to the working directory. On a production host running a prebuilt binary it still runs, and the toolchain probe still fails it. Read the per-probe sections before wiring it into a runtime health check.

The command is shipped and its JSON envelope is versioned (chio.doctor.v1). Its diagnostics carry a weaker promise. Each urn:chio:error:cli:doctor-* entry in spec/errors/registry.yaml declares its own severity and stability, and the registry reads:

Registry entrySeverityStabilityRegistry summary
urn:chio:error:cli:doctor-chio-yaml-invaliderrorunstablechio doctor schema probe found chio.yaml validation errors.
urn:chio:error:cli:doctor-cosign-stalewarningunstablechio doctor cosign freshness probe found a stale or unverifiable guard-bundle signature.
urn:chio:error:cli:doctor-oci-unreachablewarningunstablechio doctor OCI registry reachability probe could not reach the configured registry.
urn:chio:error:cli:doctor-otel-unresolvedwarningunstablechio doctor OTEL probe could not resolve the OTLP endpoint or kernel runtime metrics.
urn:chio:error:cli:doctor-probe-failederrorunstablechio doctor reported one or more failing probes.
urn:chio:error:cli:doctor-toolchain-mismatcherrorunstablechio doctor toolchain probe found a Rust toolchain mismatch.

The severity column is the registry’s own declaration, and it agrees with the worst severity the corresponding probe can emit. Stability is the column a CI gate has to read: branch on the exit code and the probe names, which integration tests pin, and treat message text as text.

Read the scope narrowly. Each probe reads the local filesystem, the environment, an external HTTP endpoint, or the output of cargo --version. No probe opens a Chio database, reads a policy, or evaluates a guard, and the only write the whole command can make is the --fix scaffold in the sixth probe.

The doctor reads its own configuration vocabulary

CHIO_GUARD_REGISTRY, CHIO_GUARD_REGISTRY_REFERENCE, CHIO_GUARD_BUNDLE, CHIO_KERNEL_METRICS_URL, CHIO_OTEL_RECEIPT_EXPORTER_ENDPOINT, and OTEL_EXPORTER_OTLP_ENDPOINT are each read by exactly one probe and by no other crate under crates/. Same for the file conventions: the two marker keys the sixth probe requires and the registry: line the second one falls back to are both absent from the runtime chio.yaml schema, and nothing in the tree writes .chio/guard-bundle.sigstore.json. Setting these configures the doctor. None of them configures a node. A green run means the operator’s doctor inputs are well-formed, not that a runtime is pointed anywhere in particular.

Probe, report, severity

The whole subsystem is one small trait. A probe has a stable lowercase name and one method that consumes a read-only config and returns exactly one report. Probes are required not to panic: anything that goes wrong comes back as a report carrying a registry code, never as an unwind out of the runner.

crates/products/chio-cli/src/doctor/probe.rsrust
pub trait Probe {
    /// Stable probe identifier. Must be lowercase ASCII, no spaces.
    fn name(&self) -> &'static str;

    /// Execute the probe.
    fn run(&self, config: &ProbeConfig) -> ProbeReport;
}

ProbeConfig is three fields and every probe receives the same one. skip_network is set from --skip-network or the environment, fix_enabled from --fix, and workdir is a test-only override that the binary always leaves as None, so in production every path-resolving probe roots at the current working directory.

ProbeReport is what serializes into the envelope.

FieldTypeMeaning
probe&'static strStable identifier. The six names are asserted in order by doctor_skeleton.rs.
severityProbeSeverityLowercased in JSON. Drives the aggregate.
code&'static strA registry URN, always present. Passing reports use urn:chio:error:cli:other, which the registry marks stability: deprecated; read the severity, not the code, to decide whether a probe passed.
messageStringHuman text. Not a stable contract.
helpOption<String>Remediation. Serialized as null when absent, not omitted.
contextVec<ProbeContext>Ordered key/value pairs, both sides always strings. Omitted from JSON when empty. This is the machine-readable part: cargo_version, msrv, pinned_channel, registry, bundle, age_days, endpoint, source, metrics_url, expected_gauge, path, missing, line, column, created. No key is guaranteed present: several failure branches attach nothing.
repairedboolTrue only when --fix actually wrote something. Exactly one probe can ever set it.

Severity is an ordered enum, and the runner takes a running maximum over it. The ordering matters more than it looks, because Info sorts above Ok: a run in which nothing is configured reports "worst": "info" rather than "ok", and still exits 0.

SeverityMeaningExit code
okChecked and passed.0
infoNot checked, because nothing was configured to check against.0
warningChecked and failed, non-blocking.0
errorChecked and failed, blocking.1
fatalReserved. No probe in the standard set emits it.2

The distinction between info and warning is the one to internalize. info means the probe had nothing to check: no registry configured, no bundle on disk, no endpoint in the environment. It is not a pass. A green chio doctor on an unconfigured host tells you almost nothing.

That claim is easiest to read from a real run against an empty directory: one ok, five info, exit 0. The chio_yaml probe prints an absolute path, so the last report names the directory the command ran in.

doctor · unconfiguredtranscript
$ chio doctor
chio doctor
  [ok] toolchain (urn:chio:error:cli:other): Rust toolchain 1.96.0 satisfies workspace MSRV.
    cargo_version: cargo 1.96.0 (30a34c682 2026-05-25)
  [info] oci (urn:chio:error:cli:other): No guard OCI registry configured. Skipping reachability probe.
    help: Set CHIO_GUARD_REGISTRY or add a `registry:` line to chio.yaml to enable the OCI reachability probe.
  [info] cosign (urn:chio:error:cli:other): No guard bundle configured. Skipping cosign freshness probe.
    help: Set CHIO_GUARD_BUNDLE or place a sigstore bundle at .chio/guard-bundle.sigstore.json to enable this probe.
  [info] otel (urn:chio:error:cli:other): No OTLP endpoint configured. Skipping OTEL probe.
    help: Set OTEL_EXPORTER_OTLP_ENDPOINT (or the receipt-specific CHIO_OTEL_RECEIPT_EXPORTER_ENDPOINT) to enable the probe.
  [info] kernel_runtime (urn:chio:error:cli:other): No kernel metrics URL configured. Skipping chio_kernel_dispatch_inflight gauge probe.
    help: Set CHIO_KERNEL_METRICS_URL to the chio-tower /metrics URL to enable this probe.
  [info] chio_yaml (urn:chio:error:cli:other): No chio.yaml found at ~/chio/chio.yaml.
    help: Run `chio doctor --fix` to scaffold a minimal chio.yaml, or create one by hand.

summary: worst severity = info, exit code = 0
exit 0

Five of the six probes report that they had nothing to reach. The summary line still reads exit code 0, which is the point: this host has passed nothing except its toolchain check.


How a run proceeds

The order is built once, in code, and is not configurable from the command line:

crates/products/chio-cli/src/doctor/mod.rsrust
/// Build the standard probe set in the canonical order: toolchain,
/// OCI, cosign, OTEL, kernel runtime, chio.yaml.
#[must_use]
pub fn standard(config: ProbeConfig) -> Self {
    Self::new(config)
        .with_probe(Box::new(ToolchainProbe::default()))
        .with_probe(Box::new(OciProbe::default()))
        .with_probe(Box::new(CosignProbe::default()))
        .with_probe(Box::new(OtelProbe::default()))
        .with_probe(Box::new(KernelRuntimeProbe::default()))
        .with_probe(Box::new(ChioYamlProbe::default()))
}
rendering
The six probes DoctorRunner::standard builds, in the order DoctorRunner::run visits them, and the fold from their severities to an exit code. Every probe runs, and none is conditional on an earlier result.
sourcecrates/products/chio-cli/src/doctor/mod.rs:62-115at fe56570
#ProbeReadsNetworkWorst it can emit
1toolchaincargo --version, root Cargo.toml, rust-toolchain.tomlNevererror
2ociCHIO_GUARD_REGISTRY, CHIO_GUARD_REGISTRY_REFERENCE, a registry: line in chio.yamlHEAD, 5s timeoutwarning
3cosignCHIO_GUARD_BUNDLE, else .chio/guard-bundle.sigstore.jsonNeverwarning
4otelCHIO_OTEL_RECEIPT_EXPORTER_ENDPOINT, then OTEL_EXPORTER_OTLP_ENDPOINTNeverwarning
5kernel_runtimeCHIO_KERNEL_METRICS_URLGET, 5s timeoutwarning
6chio_yaml./chio.yamlNevererror

1. toolchain

Shells out to cargo --version, parses the semver out of the second whitespace-delimited field, and compares major and minor only. Then it line-scans the root Cargo.toml for a rust-version key and rust-toolchain.toml for a channel key. Older than the MSRV is an error; a pinned channel whose major and minor differ from the active cargo is an error. All four of its failure paths carry urn:chio:error:cli:doctor-toolchain-mismatch.

This probe is the only one that can fail an otherwise clean host, and it fails hard. Three conditions collapse to the same error: no cargo on PATH, a cargo --version that exits non-zero, and output the parser cannot read. The first two report Failed to invoke `cargo --version`. and exit 1. On a production host running a prebuilt chio binary with no Rust installed, that is the result of a bare chio doctor.

Run against the Chio workspace root, the two values it finds are rust-version = "1.94" in Cargo.toml and channel = "1.94.1" in rust-toolchain.toml, so any cargo outside 1.94 fails the pin comparison and any cargo below 1.94 fails the MSRV comparison.

The rest of the parsing is shallow, and the shallowness is load-bearing. channel = "stable" yields no numeric pair, so the pin comparison is skipped without a word. Both file reads root at the working directory, so running from a subdirectory finds neither file, and with no root manifest the report still reports the active toolchain as satisfying a workspace MSRV it never found. That is the run captured above: the only context key is cargo_version, because there was no manifest and no pin to compare against. The module header calls that case fail-closed; the code passes instead, and the code is the authority. Read the msrv and pinned_channel context keys to know whether a comparison happened, and do not assume cargo_version is always there: the unparseable-output branch attaches neither help nor context.

2. oci

Resolves a registry reference from two environment variables and then a registry: line in chio.yaml, in that order. An environment variable set to the empty string counts as unset and falls through, which is the opposite of how the skip-network variable is read. The file fallback is a line scan, not a parse: it takes the first line whose leading whitespace is trimmed and whose remainder starts with registry:, at any nesting depth, so a key nested three levels down inside an unrelated block resolves the registry. With nothing resolved it reports info and stops. Otherwise it issues one HEAD with a 5 second timeout, to the reference itself if it starts with the four characters http and to https://{registry}/v2/ otherwise, and accepts 2xx, 401, or 404 as reachable.

Read that acceptance set literally. The probe establishes that a host resolves, terminates TLS, and answers; not that the endpoint is an OCI registry, that credentials work, or that your bundle is present. It sends no credential, which is why 401 counts as success. It also appends /v2/ to whatever was resolved, so a reference carrying a repository path (ghcr.io/example/chio) produces a URL that is not the registry API root. Every failure here is a warning and never blocks.

Scope it accordingly. registry is not a key in the runtime chio.yaml schema, and the variable the real pull path reads is a different one, CHIO_GUARD_REGISTRY_PASSWORD in cli/types/runtime.rs. A passing oci probe says nothing about how a bundle will actually be pulled.

3. cosign

Resolves a bundle path from CHIO_GUARD_BUNDLE or, failing that, .chio/guard-bundle.sigstore.json under the working directory if it exists. Only the second candidate is existence-checked. A CHIO_GUARD_BUNDLE pointing at a file that is gone is not info, it is a warning: Cannot read guard bundle {path}: {err} under doctor-cosign-stale. Once a path resolves the probe calls std::fs::metadata, takes modified() with a created() fallback, converts elapsed time to whole days, and warns strictly above 90. Exactly 90 days passes.

The cosign probe does not verify a signature

Despite the name, and despite the module header describing it as consuming chio-attest-verify, the probe imports nothing but std::path::PathBuf and the probe trait. It stats a file. A bundle whose signature is invalid, whose identity is untrusted, or whose contents are an empty JSON object passes as long as the mtime is recent. The age is also floored at zero rather than errored whenever the elapsed computation fails: a future mtime, a clock that moved backwards, and a filesystem that reports neither modified() nor created() all report age_days of 0 and pass. Real verification is chio-attest-verify’s verify_bundle, reached through chio attest and the bundle checks on chio bind, not this. Guard Supply Chain covers what a real bundle check does at load time, including the Rekor inclusion field that is currently never true.

4. otel

Reads CHIO_OTEL_RECEIPT_EXPORTER_ENDPOINT first, then OTEL_EXPORTER_OTLP_ENDPOINT, records which one won in a source context key, and validates the string as a URL, prefixing http:// onto anything that does not already carry an http:// or https:// scheme. Surrounding whitespace is rejected outright, which catches the pasted-with-trailing-space case that otherwise produces a silent export failure much later. It never sends a span and never opens a socket, so --skip-network does not change its behavior at all; the parameter is named _config in the signature.

The check is a parse and nothing more. Once the scheme is filled in, anything url::Url::parse accepts passes: no host reachability, no port sanity, no OTLP path convention. A typo that still parses is a pass here and a silent export failure in production. An empty value counts as unset and reports info.

Neither variable is read by any other crate under crates/, and the workspace has no opentelemetry-otlp dependency that would resolve the standard one inside an SDK. A node’s actual OTLP destination comes from the telemetry.endpoint field of the runtime chio.yaml schema.

5. kernel_runtime

With CHIO_KERNEL_METRICS_URL unset the probe reports info and names the variable in its help text. With it set it GETs the URL on a 5 second timeout and substring-matches the body for one gauge name. It never inspects the HTTP status: a 401, a 404, or a 500 whose body lacks the string all produce the same Metrics endpoint did not expose the chio_kernel_dispatch_inflight gauge. warning as a healthy endpoint that does not export it. Every failure mode here, including a connection failure and a body it cannot read, is a warning.

The gauge it looks for has no emitter, and the URL it names has no route

chio_kernel_dispatch_inflight is a hard-coded constant in doctor/kernel_runtime.rs, and its own doc comment says it is hard-coded so a repo grep gate keeps passing "even when the metric has not yet been merged upstream". Nothing in the tree emits it: a repo-wide grep returns the constant, its own test, three doc comments, and the registry help string. The probe also tells operators to point CHIO_KERNEL_METRICS_URL at "the chio-tower /metrics URL", but chio-tower is a tower::Layer library with no HTTP routes of its own. The route that exists is /metrics on the chio api protect proxy router, and the control plane mounts an equivalent one.

Two things follow. That route sits behind require_sidecar_control_middleware, the same admin gate as the approval routes, and this probe sends no credential. And the body it would get past that gate is composed in handle_metrics from the kernel guard metric families (chio_guard_verdict_total, chio_guard_deny_total, and the rest), the OTEL-drop, signing-queue-block, settlement-unresolved and ambiguous-dispatch families, the receipt watchdog gauges, the http-core mediation-edge families, and the alert pack. None of them is an inflight gauge. Pointed at a live endpoint, credentialed or not, this probe warns. Read Node Observability for what is actually exported.

All four of its failure branches carry urn:chio:error:cli:doctor-otel-unresolved, which the registry entry itself scopes to "the OTLP endpoint or kernel runtime metrics". The collision is deliberate, not a bug, but it means a code-based alert cannot tell probe 4 from probe 5. Split on the probe field.

6. chio_yaml

Reads ./chio.yaml, parses it with serde_yml, requires a top-level mapping, and requires two keys to be present: version and policy. Presence is the whole check. Values are never inspected, extra top-level keys are ignored, and nothing validates that policy names a file that exists. Calling it a schema probe oversells it; it is a two-key marker test that also proves the document parses.

A parse failure carries the parser’s line and column into line and column context keys so an editor can jump to the span, falling back to 0, 0 when serde_yml reports no location; missing keys report line: 1, column: 1 and list the names in missing. Only ErrorKind::NotFound is info. Any other read failure, including a directory at that path or a permission denial, is an error and exits 1.

This is a different schema from the runtime one

The probe’s marker schema and the schema chio_config::load_from_file parses are two separate contracts on the same filename, and they cannot both be satisfied. A runtime chio.yaml carries neither version nor policy, so this probe reports an error and the run exits 1. Adding the two keys to satisfy the probe then breaks the loader, because all eleven structs in chio-config/src/schema.rs carry deny_unknown_fields and ChioConfig knows only kernel, adapters, edges, receipts, logging, telemetry, guards, and wasm_guards. The second probe’s registry: fallback is in the same position: no such key exists in the runtime schema either. If you deploy a runtime config under that name, expect the sixth probe to be red and gate CI on the other five.

The JSON envelope

--json, or the global --json / --format json, switches rendering to a single compact line on stdout. Four top-level fields, with exit_code carrying the same number the process will exit with, so a CI gate that already parsed the payload never needs to inspect the status separately.

crates/products/chio-cli/src/cli/doctor.rsrust
let payload = serde_json::json!({
    "schema": "chio.doctor.v1",
    "worst": run.worst,
    "exit_code": run.exit_code(),
    "reports": run.reports,
});

A run from an empty directory with a registry resolved and the network skipped. serde_json::to_writer emits one compact line, so the capture pipes it through jq to read. Note the key order: it is alphabetical rather than the ProbeReport declaration order, because the workspace does not enable serde_json’s preserve_order feature and serde_json::Map is therefore a BTreeMap. Parse the payload; do not match it as text.

doctor · jsontranscript
$ CHIO_GUARD_REGISTRY=ghcr.io/example/chio \
  chio doctor --json --skip-network | jq .
{
  "exit_code": 0,
  "reports": [
    {
      "code": "urn:chio:error:cli:other",
      "context": [
        {
          "key": "cargo_version",
          "value": "cargo 1.96.0 (30a34c682 2026-05-25)"
        }
      ],
      "help": null,
      "message": "Rust toolchain 1.96.0 satisfies workspace MSRV.",
      "probe": "toolchain",
      "repaired": false,
      "severity": "ok"
    },
    {
      "code": "urn:chio:error:cli:other",
      "context": [
        {
          "key": "registry",
          "value": "ghcr.io/example/chio"
        }
      ],
      "help": null,
      "message": "Registry endpoint ghcr.io/example/chio resolved. Network probe skipped.",
      "probe": "oci",
      "repaired": false,
      "severity": "ok"
    },
    {
      "code": "urn:chio:error:cli:other",
      "help": "Set CHIO_GUARD_BUNDLE or place a sigstore bundle at .chio/guard-bundle.sigstore.json to enable this probe.",
      "message": "No guard bundle configured. Skipping cosign freshness probe.",
      "probe": "cosign",
      "repaired": false,
      "severity": "info"
    },
    {
      "code": "urn:chio:error:cli:other",
      "help": "Set OTEL_EXPORTER_OTLP_ENDPOINT (or the receipt-specific CHIO_OTEL_RECEIPT_EXPORTER_ENDPOINT) to enable the probe.",
      "message": "No OTLP endpoint configured. Skipping OTEL probe.",
      "probe": "otel",
      "repaired": false,
      "severity": "info"
    },
    {
      "code": "urn:chio:error:cli:other",
      "help": "Set CHIO_KERNEL_METRICS_URL to the chio-tower /metrics URL to enable this probe.",
      "message": "No kernel metrics URL configured. Skipping chio_kernel_dispatch_inflight gauge probe.",
      "probe": "kernel_runtime",
      "repaired": false,
      "severity": "info"
    },
    {
      "code": "urn:chio:error:cli:other",
      "help": "Run `chio doctor --fix` to scaffold a minimal chio.yaml, or create one by hand.",
      "message": "No chio.yaml found at ~/chio/chio.yaml.",
      "probe": "chio_yaml",
      "repaired": false,
      "severity": "info"
    }
  ],
  "schema": "chio.doctor.v1",
  "worst": "info"
}
exit 0

Note what is and is not there. context is omitted entirely on the four info reports because it is empty; help is present as null on the two ok reports, which have none. Every report carries urn:chio:error:cli:other, including the passing ones, which is why severity is the field to branch on. The human renderer prints the same data under a leading chio doctor line, one two-space-indented [ok] probe (code): message per report, then further-indented help: and context lines and a repaired: yes line where it applies, closing with summary: worst severity = ..., exit code = .... The warning marker is [warn], not [warning], which is the one place the human and JSON channels disagree on a spelling.


--fix repairs exactly one thing

The flag is documented as running idempotent repairs and rejecting destructive ones. No runtime check classifies a repair. The rule holds because exactly one probe reads fix_enabled at all, in exactly one branch: chio_yaml, on ErrorKind::NotFound, writes a two-line file.

chio.yaml (scaffolded by --fix)yaml
version: 1
policy: ./policy.yaml

The write uses create_new(true), so it can only ever create. If the file appeared between the failed read and the create, the AlreadyExists arm re-reads and validates instead of clobbering. On success the report is ok with repaired: true, a created: true context key, and the help line Review the scaffolded policy path before running production workloads. The flag’s own clap help says repairs run "after probes complete"; they do not. The repair happens inline, inside the sixth probe, in place of the report it would otherwise have emitted.

What it does not do matters more. It will not repair a chio.yaml that exists and is missing keys, because that path never consults the flag. It will not create the ./policy.yaml the scaffold points at, and nothing ever checks that the target exists. It will not re-pull a stale bundle, install a toolchain, or write an environment variable. On a host where all six probes are already green it is a no-op.

--fix can turn a passing run into a failing one

The one repair is also a new way to exit 1. If the create or the write fails, on a read-only working directory, under a restrictive umask, or on a full disk, the report is error under urn:chio:error:cli:doctor-probe-failed rather than the info the same run would have produced without the flag. Adding --fix to a bootstrap script is safe in the sense that it cannot destroy anything, not in the sense that it cannot change the exit code.

The network skip, and why CI is safe

Two switches set the same bit: --skip-network and the environment variable CHIO_DOCTOR_SKIP_NETWORK. The environment check is presence-only (std::env::var(...).is_ok()), so exporting it empty counts as set; there is no =0 that turns it back off.

It is resolved twice on purpose. cmd_doctor folds both switches into ProbeConfig::skip_network before the runner is built, and the two probes that would reach out also re-check the variable themselves, so a library caller passing skip_network: false still gets the skip when the variable is exported.

Only oci and kernel_runtime build an HTTP client, both with a 5 second timeout, and both carry an explicit egress allowance comment marking them as diagnostic probes against a user-configured endpoint rather than substrate tool egress.

Skipped is not the same as unconfigured

A skipped probe returns ok with the resolved endpoint in context: registry for oci, metrics_url plus expected_gauge for kernel_runtime. An unconfigured probe returns info with empty context. That difference is what makes --skip-network useful as a config assertion in a sandboxed job: it proves the environment resolves a registry and a metrics URL without needing either to answer. tests/doctor_oci.rs and tests/doctor_otel.rs assert exactly this pair of signals.

The CI recipe follows from the exit-code table. In a sandbox with no egress, run chio doctor --json --skip-network and gate on the status. All six probes still run; the two that would reach out resolve their target and report it without opening a socket, so no timeout can stall the job. Dropping the skip does not change the gate, because every network failure the doctor can produce is a warning worth 0. The skip buys the five seconds per probe, not a different verdict.


Guarantees and limits

StatusClaimEvidence
ShippedSix probes, always in the order toolchain, oci, cosign, otel, kernel_runtime, chio_yaml. Every probe runs on every invocation; none is conditional on an earlier result.DoctorRunner::standard and run in doctor/mod.rs
Proved by testThe JSON envelope carries chio.doctor.v1 and the six probe names in canonical order.tests/doctor_skeleton.rs (doctor_json_envelope_carries_schema_and_reports)
Proved by testWorst severity drives the exit code, and a warning alone exits 0.worst_severity_drives_exit_code, warnings_do_not_drive_non_zero_exit
ShippedThe process exit code and the exit_code field agree, because cmd_doctor calls std::process::exit directly rather than letting the generic dispatch error path force 1.cli/doctor.rs
ShippedOnly oci and kernel_runtime perform network I/O, and both honor --skip-network and CHIO_DOCTOR_SKIP_NETWORK.doctor/oci.rs, doctor/kernel_runtime.rs; no other probe constructs a client
Shipped--fix can only create an absent chio.yaml, never overwrite one.scaffold_missing uses create_new(true); fix_enabled has exactly one reader in the workspace
Limit--fix is not exit-code-neutral. A create or write failure reports error under doctor-probe-failed where the same run without the flag would have reported info.The two non-AlreadyExists error arms of scaffold_missing
LimitOnly two probes ever fail a run. Every Error in the tree is one of four in toolchain or seven in chio_yaml; the last of those seven is reachable only under --fix. An unreachable registry, a 91-day-old bundle, a malformed OTLP endpoint, and a missing kernel gauge are all warnings that exit 0.Every ProbeSeverity::Error construction under src/doctor/; DoctorRun::exit_code
LimitThe cosign probe performs no cryptographic verification. It is a file mtime check against a 90-day threshold, and every branch that cannot compute an elapsed time reports an age of 0 and passes.doctor/cosign.rs imports only PathBuf and the probe trait; the .and_then(|t| t.elapsed().ok()) / unwrap_or(0) chain
LimitThe toolchain probe requires cargo on PATH and fails the run without it, which makes a bare chio doctor on a toolchain-free production host exit 1.cargo_version_string returning None
LimitConfig validity means two marker keys are present, not that the document matches the runtime schema. Values are never inspected and extra top-level keys are ignored. The two contracts are mutually exclusive on one file.REQUIRED_KEYS and validate_body in doctor/chio_yaml.rs; deny_unknown_fields on ChioConfig
Not implementedTreating warnings as errors. The severity docs describe a runner "configured to treat warnings as errors"; no such field exists on ProbeConfig or DoctorArgs, and exit_code hard-codes warning to 0.doctor/probe.rs doc comment vs. DoctorRun::exit_code
Not implementedEarly exit on fatal. The docs say subsequent probes may be skipped at the runner’s discretion; the loop has no break, and no standard probe constructs a Fatal report, so exit code 2 is unreachable from DoctorRunner::standard.DoctorRunner::run; no ProbeSeverity::Fatal construction in chio-cli
Not wiredchio_kernel_dispatch_inflight has no emitter anywhere in the repo, so the fifth probe asserts a placeholder rather than a live gauge.Constant defined in doctor/kernel_runtime.rs; only other occurrences are three doc comments, the registry help text, and the probe’s own test
Not wiredThe endpoint the fifth probe names does not exist. Its help text points at "the chio-tower /metrics URL", but chio-tower is a tower::Layer crate with no routes. The route that serves the kernel families is /metrics on the chio api protect proxy router, and it is admin-gated, which the probe cannot satisfy.chio-api-protect/src/proxy/router.rs (handle_metrics behind require_sidecar_control_middleware); no .route call in chio-tower
Not wiredEvery environment variable the doctor resolves is doctor-only: CHIO_GUARD_REGISTRY, CHIO_GUARD_REGISTRY_REFERENCE, CHIO_GUARD_BUNDLE, CHIO_KERNEL_METRICS_URL, and both OTLP variables have exactly one reader each. Runtime telemetry is configured through telemetry.endpoint.Repo-wide grep over crates/; chio-config/src/schema.rs; no opentelemetry-otlp dependency in the workspace manifest
UnstableEvery doctor-* registry entry declares stability: unstable, rendered row by row from the registry in the table above. Passing reports carry the deprecated urn:chio:error:cli:other placeholder instead of any of them.spec/errors/registry.yaml
UnsupportedProbing a running node’s state. The doctor never opens a receipt or authority database and never reads a HealthFlag. The one route it can touch is /metrics, read as text. Selecting or reordering probes from the command line is also unsupported; the set is fixed in code.DoctorArgs is three booleans; DoctorRunner::standard takes no probe list

Next Steps

  • Health & Readiness · the same question asked of a process that is already running, and why liveness and readiness answer differently
  • Node Lifecycle · what happens after preflight passes: startup ordering, and the refusals that stop a boot
  • chio.yaml Configuration · the runtime schema the sixth probe does not validate
  • Node Observability · what a node actually exports on /metrics, and the gate in front of it
  • Guard Supply Chain · what the cosign probe only pretends to check, and the gate that actually checks it
  • CLI Reference · chio doctor alongside the rest of the command set
Node Preflight · Chio Docs