# Field diagnostics

`hpx doctor --bundle` lets a customer or field engineer hand a maintainer
one sanitised archive instead of a screen share. This page is the design
behind it, for anyone extending the collectors or auditing what leaves the
building; how to produce one is on [Troubleshooting](https://ambiqai.github.io/helia-profiler/guide/troubleshooting/#4-produce-a-support-bundle).

## Diagram

The support bundle: The four phases of collect_support_bundle() and write_support_bundle(). Every collector degrades to an unavailable section with a reason rather than failing the bundle.

- Collectors: one section each
  - checks
  - compatibility
  - dependencies
  - modules
  - config
  - probes
  - ports
  - host
- Redaction: diagnostics/redact.py
  - paths
  - URLs
  - token shapes
  - secret assignments
  - serial numbers
- Manifest: hpx.support-bundle-manifest v1
  - sections
  - artifacts
  - redaction counts
- Archive
  - deterministic zip
  - content fingerprint
  - verify
Text description of the diagram

- Collectors (one section each): checks, compatibility, dependencies, modules, config, probes, ports, host
- Redaction (`diagnostics/redact.py`): paths, URLs, token shapes, secret assignments, serial numbers
- Manifest (`hpx.support-bundle-manifest` v1): sections, artifacts, redaction counts
- Archive: deterministic zip, content fingerprint, verify

## The blocks

### Collectors

`collect_support_bundle(options)` runs one collector per section. None of
them can fail the bundle: a missing `--workspace`, an unresolvable
`--config`, absent hardware or no network each mark only their own section
unavailable, with a `reason`, and the rest is still collected
(`SupportBundleSection.available` in `results/support_bundle.py`).

| Section | Source | Present |
|---|---|---|
| `checks` | `hostenv.doctor.inspect_environment(include_versions=True)` | always |
| `compatibility` | `deps.compatibility.load_compatibility_baseline()` | always, offline |
| `dependencies` and `nsx.lock` | `deps.dependencies.read_dependency_lock_provenance()` plus a redacted copy of the lock text | with `--workspace` |
| `modules` | the baseline's qualified modules, plus the resolved modules from the same provenance read | always; the resolved half with `--workspace` |
| `config` | `config.load_config()` serialised | with `--config` |
| `probes` | `target.probe.jlink.list_connected_probes()` | unless `--no-probes` |
| `ports` | `transport.ports.list_serial_ports()` | unless `--no-ports` |
| `host` | `platform.system()`, `.release()`, `.machine()`, `.python_version()`, `sys.platform` | always, in the manifest |

The dependency collector reuses the pipeline's own provenance reader; it
never parses `nsx.lock` itself and never resolves, synchronises or mutates
a workspace. The hostname (`platform.node()`) and any username are
deliberately not collected: neither helps diagnose a toolchain or build
problem and both identify a person more than the default redaction is
designed to catch.

### What is never read

Models, firmware and generated sources, ELF and binary outputs, raw
proprietary payloads, credentials and secret environment values are never
read, not merely redacted afterwards. The archive writer and
`verify_support_bundle()` enforce one allow-list: every member is named
exactly `nsx.lock` or ends in `.json`, and an absolute path, a `..`
segment, a backslash, a NUL byte or a duplicate name is rejected before
any bytes are trusted.

### Redaction

`diagnostics/redact.py` is conservative and deterministic. Absolute paths
keep only their final component, except a path that is itself a home
directory, whose final component is the account name. URL credentials are
stripped and every query value is redacted except a short allow-list of
plainly harmless names; `file://` paths go through path redaction. Known
token shapes (GitHub PAT, AWS access key, Slack token, JWT, HTTP bearer)
are replaced wherever they appear, including inside a URL. Secret-looking
`KEY=VALUE` and `KEY: VALUE` text is replaced, and any JSON mapping value
whose key looks secret-shaped is always replaced regardless of the value.
Device serial numbers are redacted structurally, by field name (`serial`,
`serial_number`), and then by literal substitution wherever the same value
recurs (a USB `hwid` string, a device path derived from it), so a sibling
field cannot leak what structural redaction caught. `--raw-probe-ids` is
the one opt-out; it keeps real probe and port serials and prints a warning.

Every pass returns a `RedactionCounts`; the manifest's `redaction` object
sums them so a reviewer sees which categories were found and rewritten,
and how often. That is an audit trail of what redaction did, not a
certificate that nothing sensitive remains: review a bundle before sharing
it as you would any diagnostic output.

### Manifest

`SupportBundleManifest` (`hpx.support-bundle-manifest`, schema version 1)
is a distinct contract from the run's `ResultManifest`: a support bundle
has no run status, validity or comparability. It reuses `ResultArtifact`
for its member entries because that shape (path, size, SHA-256) is the
same either way, and carries the host record, the section availability and
reasons, the redaction counts and a `generated_at` timestamp.

### Archive

`write_support_bundle()` writes a `ZIP_STORED` archive: members sorted,
`manifest.json` last, a fixed 1980-01-01 timestamp and `create_system = 0`
on every entry, so the archive encodes neither the host clock nor its
operating system. Storage is uncompressed because DEFLATE's exact bytes
depend on the zlib build doing the compressing, and the members are small
JSON. The file name comes from `content_fingerprint()`, a SHA-256 over
every other member's name and digest, excluding `manifest.json` because
its timestamp differs run to run. Two bundles from identical inputs
therefore have byte-identical members and the same name, so a diff
between two bundles is a diff between two environments.
`verify_support_bundle()` re-derives every artifact's size and digest,
requires the declared and actual member sets to match exactly, and applies
the member-name rules above before trusting anything else in the archive.

## Where the code lives

| Block | Path under `src/helia_profiler/` |
|---|---|
| collectors, archive, verify | `diagnostics/support_bundle.py` |
| redaction | `diagnostics/redact.py` |
| manifest model | `results/support_bundle.py` |
| CLI entry | `cli/inspect_cmds.py` (`hpx doctor --bundle`) |
| sources reused | `hostenv/doctor.py`, `deps/compatibility.py`, `deps/dependencies.py`, `target/probe/jlink.py`, `transport/ports.py` |

## What next

[Troubleshooting](https://ambiqai.github.io/helia-profiler/guide/troubleshooting/) for when to produce a bundle and
what to attach with it; the [issue codes](https://ambiqai.github.io/helia-profiler/reference/issue-codes/)
for the run-side diagnostics the bundle does not carry.
