Metadata-Version: 2.4
Name: voltry-probe
Version: 0.3.4
Summary: Local evidence capture, signing, and offline certificate rendering for data-center GPUs. Scan reads NVML without mutating the device and verifies operator-supplied attestation reports; submission is a separate opt-in command.
Project-URL: Homepage, https://www.voltry.io
Project-URL: Verify, https://verify.voltry.io
Project-URL: Source, https://github.com/Voltry-tech/voltry-probe
Project-URL: Issues, https://github.com/Voltry-tech/voltry-probe/issues
Project-URL: Changelog, https://github.com/Voltry-tech/voltry-probe/blob/main/packages/voltry-probe/CHANGELOG.md
Author: Voltry
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: attestation,certificate,evidence,gpu,nvml,provenance
Classifier: Development Status :: 4 - Beta
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: cryptography<51,>=44
Requires-Dist: pydantic<3,>=2.11
Requires-Dist: rfc8785<0.2,>=0.1.4
Requires-Dist: typer<1,>=0.23
Requires-Dist: voltry-evidence-schema<2,>=1.2.2
Provides-Extra: hardware
Requires-Dist: nvidia-ml-py<13,>=12.560; extra == 'hardware'
Provides-Extra: submit
Requires-Dist: httpx<1,>=0.27; extra == 'submit'
Description-Content-Type: text/markdown

# Voltry Probe

Voltry Probe captures, signs, and renders condition and provenance evidence
for data-center GPUs. `voltry scan` reads device state over NVML, read-only:
the scan path issues only non-mutating reads (no resets, reconfiguration, or
stress); validate against your own driver and GPU models before fleet-wide
rollout. The readings are signed into an evidence bundle, and
`voltry cert` renders that bundle into a self-contained HTML certificate you
can open with no network at all.

Two scoping notes, so the claims match the code you are installing:

- Live capture is NVML only in this release. The bundle format carries DCGM,
  Redfish, and attestation payloads, and `scan` maps them when a captured
  fixture provides them, but there is no live DCGM or Redfish collector yet.
- Attestation is verification, not capture. When a capture carries an
  operator-supplied attestation report, the probe verifies the ECDSA chain
  against a trusted root (and, if you issue a `--challenge`, freshness). The
  probe does not yet pull the report from the device itself.

It records what the hardware is. It never states or implies a price.

## Install

For live scans (needs an NVIDIA GPU and driver):

```bash
pipx install "voltry-probe[hardware]"
```

For everything else (fixture scans, certificate rendering, verification):

```bash
pipx install voltry-probe
```

No pipx? A stock Mac ships neither pipx nor a recent enough Python. If you
have [uv](https://docs.astral.sh/uv/) (for example from `brew install uv`), it
installs the tool and fetches a Python for it in one step:

```bash
uv tool install --python 3.12 voltry-probe
```

Extras work the same way with either tool, for example
`uv tool install --python 3.12 "voltry-probe[submit]"`.

Requires Python 3.10 or newer; stock DGX OS and Ubuntu 22.04 hosts work as-is.
`pip install` inside a virtualenv works too. Without `[hardware]`, a live
`voltry scan` exits with a clean error naming the missing extra and the exact
pip command to fix it; every other workflow needs no extra. `voltry submit`
(the opt-in submission client) lives behind the separate `[submit]` extra.

## Sixty seconds to a certificate

With a GPU and the `[hardware]` extra installed:

```bash
# 1. Scan. Read-only, works fully offline and air-gapped.
#    Reads live hardware; signs with your operator key.
voltry scan --signing-key operator.pem --out bundle.json

# 2. Render. A self-contained HTML certificate, no network needed to view.
voltry cert bundle.json --out cert.html
```

That is the whole loop. `voltry submit` exists as a separate, explicit,
opt-in step (see [Submitting to the Voltry registry](#submitting-to-the-voltry-registry));
scanning and rendering never phone home.

## No GPU? Run the same loop on a captured fixture

The test suite ships a real H100 capture. Fetch it, scan it with a throwaway
key, and render:

```bash
curl -LO https://raw.githubusercontent.com/Voltry-tech/voltry-probe/main/packages/voltry-probe/tests/fixtures/h100_read.json
voltry scan --fixture h100_read.json --ephemeral-key --out bundle.json
voltry cert bundle.json --out cert.html
```

This exercises the exact same reader, builder, signer, and renderer as a live
scan; only the source of the raw payloads differs.

## What read-only means

The probe never writes to, resets, reconfigures, or stress-tests a device
during a scan. It does write the local output files you ask for (the bundle
and the certificate).

Functional qualification (which does exercise the device) is a separate,
explicit operation and never part of a scan: it requires importing
`voltry_probe.functional` and giving drain consent at the call. This release
maps functional results captured elsewhere into the bundle; it does not yet
run the live diagnostics itself.

| Operation | Device mutation | Network |
|---|---|---|
| `import voltry_probe` | No | No |
| `voltry scan` | No | No |
| `voltry cert` | No (no device access) | No |
| `voltry submit` | No | Yes: opt-in, consent flag required |
| `voltry fleet manifest` | No (no device access) | No |
| `voltry receipts verify` | No (no device access) | No |
| `voltry_probe.functional` | Mapping only in this release; live runs need drain consent | No |

## What the certificate says, and what it does not

The certificate keeps measured facts and modeled estimates in separate blocks
that never look alike:

- Deterministic gates: authenticity, firmware integrity, functional and
  sanitization results. Pass or fail, no model in the loop.
- Measured condition: ECC and Xid history, retired and remapped pages, spare
  rows remaining, throttle and clock behavior. Raw values against published
  thresholds.
- Provenance: certification history on the device's permanent identity. The
  platform registry may enforce append-only history so failed attempts cannot
  be hidden; this package alone signs individual bundles and does not provide
  that ledger.

It never contains a price, a dollar figure, or a lifetime guarantee. Power-chain
exposure reads "Not Assessed" unless the facility itself was instrumented; it
is never inferred from board power.

## Verifying a bundle yourself

Every bundle is signed (ECDSA P-384 over RFC 8785 canonical JSON) and anyone
with the `voltry-evidence-schema` package can check it. Verify the stored
bytes, not a re-parsed model: parsing materializes the current schema's
defaults, so a bundle signed under an older schema version would
re-canonicalize to different bytes and fail even though nothing was tampered
with. `verify_bundle_json` reproduces exactly the bytes that were signed.

```python
from pathlib import Path

from evidence_schema import verify_bundle_json

assert verify_bundle_json(Path("bundle.json").read_bytes())
```

Be clear about what this check proves. It proves the bundle's bytes have not
been modified since signing, under the public key embedded in the bundle
itself. It does not prove that key belongs to an authorized signer, and it
does not confirm the Signature envelope's `signer` or `signed_at` labels;
those are confirmed against the registry at the platform verify endpoint,
https://verify.voltry.io.

## Submitting to the Voltry registry

`voltry submit` is the only command that touches the network, and it does
nothing unless you pass `--i-consent-to-submit`. It needs the `[submit]`
extra (on a GPU host, `"voltry-probe[hardware,submit]"` gets both; with pipx,
add `--force` if the probe is already installed without it):

```bash
pipx install "voltry-probe[submit]"
voltry submit bundle.json --i-consent-to-submit
```

By default it uploads to the Voltry registry at
`https://api.voltry.io/v1/ingest`. Pass `--url` to send the bundle to a
different platform instead. The URL has to be https; plain http is only
allowed with `--allow-insecure-http`, which is meant for a local test server.

What leaves your machine is the bundle file exactly as `voltry scan` wrote
it: every measured reading, the raw reads it was built from, and the device
identifiers in it (serial number and GPU UUID). Before anything is sent, the
command checks that the file is a valid bundle and that its signature
verifies, and refuses to upload it otherwise. Your consent also travels with
the request, as `consent=true` on the URL, because the registry turns away
any upload that does not carry it. Accepted bundles are appended to the
device's permanent history in the registry; they are not edited or removed
later.

### Your signing key has to be registered first

Right now the public registry route only accepts self-submitted bundles
signed by an operator key that Voltry has registered. That is what stops a
stranger from flooding the registry with records. A bundle signed by any
other key is turned away with `unauthorized_signer` (HTTP 403), and that
includes every bundle made with `--ephemeral-key`, since a throwaway key can
never be registered. (An organization with a workspace on the Voltry
registry has a second path: a key it registers itself, and records that stay
private to it. See the two sections after this one.)

To get set up:

1. Create one persistent P-384 operator key and keep it private. This works
   with the `openssl` that ships on macOS and on Linux:

   ```bash
   openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-384 -out operator.pem
   chmod 600 operator.pem
   ```

2. Export its public half. This file is safe to share:

   ```bash
   openssl pkey -in operator.pem -pubout -out operator.pub.pem
   ```

3. There is no self-service registration for the public route. Contact
   Voltry (https://www.voltry.io) and send them `operator.pub.pem`. Never
   send `operator.pem`. (If your organization has a workspace on the Voltry
   registry, register the key yourself instead, as the next section
   describes, and upload with an upload token.)

4. Once the key is registered, scan with it and submit:

   ```bash
   voltry scan --signing-key operator.pem --out bundle.json
   voltry submit bundle.json --i-consent-to-submit
   ```

Every bundle you sign carries the same public key in its
`signature.public_key_spki_b64` field, and that is what the registry checks
against its list.

If the registry refuses an upload, `voltry submit` prints the reason code
and message it sent back (for example `unauthorized_signer` or
`duplicate_bundle`) and exits with status 4.

## Registering a signing key with your organization

If your organization has a workspace on the Voltry registry, an owner or
admin can register your signing key to it from the Signing keys page of the
Voltry console (https://registry.voltry.io). Registration proves that you
hold the private key, and the proof is made offline on the machine that has
the key:

1. In the console, an owner or admin opens "Add signing key". The page shows
   a one-time challenge that starts with `vsc1.`, the organization id it was
   issued for, and when it expires (ten minutes after it was issued).
2. On the machine that holds the key, run the command the page shows:

   ```bash
   voltry key register --signing-key operator.pem --challenge vsc1...
   ```

   The command makes no network requests. It prints the organization id
   inside the challenge and the key's fingerprint, then asks you to confirm
   before it signs anything. Answer yes only if that id is the one on the
   add-key page you opened yourself. For scripts,
   `--yes-this-is-my-org ORG_ID` confirms without a prompt; it must match
   the id in the challenge, or nothing is signed.
3. The command prints a proof that starts with `vsr1.` on standard output.
   Paste that line into the add-key form, with a label for the key, before
   the challenge expires.

Why the confirmation matters: a key registers to one organization, once, and
can never be moved or registered again, not even after it is revoked. If
someone sends you a challenge and you sign it, your key is bound to their
organization for good. Sign only a challenge you started yourself.

The proof carries the key's public half and an ECDSA P-384 signature over the
challenge (SHA-384, DER-encoded signature). It contains nothing secret, and
the private key never leaves your machine. The key has to be on P-384; both
the command and the platform refuse any other curve.

`voltry key fingerprint --signing-key operator.pem` (or
`--public-key operator.pub.pem`) prints the key's `sha384:` fingerprint, the
same value the console lists for each registered key, so you can tell which
of your keys a registration names. It is offline too.

Once a key is registered, bundles it signs can be uploaded with an
organization upload token (next section). In your organization's own
registry view, records signed with a registered key read as measured by the
operator, including records the organization uploaded before the key was
registered. Revoking the key in the console rewrites nothing; those records
read as anonymous measurement again on the next view. A registered key does
not change what the public route accepts.

## Reviewing what a fleet upload will send

Before a folder of bundles leaves your premises, `voltry fleet manifest`
shows what an organization upload of that folder would send. It reads every
`*.json` file in the folder, applies the same two checks `voltry submit`
applies to a file before uploading it (the file parses as an evidence
bundle, and its signature verifies), and prints the result. Nothing leaves
your machine: the command makes no network requests, and it does not
change, move or delete anything in the folder.

```bash
voltry fleet manifest bundles/ --signing-key operator.pem
```

A trimmed example:

```text
Fleet manifest (offline dry run)
Folder: bundles/ (2 .json files; subfolders not included)

GPUs: 2 distinct identities in 2 valid signed bundles
Models:
  H100-SXM5: 2

Bundles and the identifiers each will send:
  host-a.json
    bundle_id:   be2b02ea-c2a4-437a-8fc0-c712fbd782c6
    ecc384_id:   GPU-0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9
    signer:      sha384:0697ec38...0b41dd7f (label: operator), the key you named
    agent:       voltry-probe 0.3.4
    created_at:  2026-09-24T09:12:03.412870+00:00
  host-b.json
    ...
  Each bundle file goes byte for byte, so everything inside it leaves too: the
  serial number, the GPU UUID, every measured reading and the raw reads.

Signing keys seen:
  sha384:0697ec38...0b41dd7f: 2 bundles (the key you named)

Duplicates: none within the folder.

Every file is a valid signed bundle.

Upload endpoint: https://api.voltry.io/v1/org/upload
  method: POST, one bundle file per request, bytes unmodified
  token header: X-Voltry-Upload-Token (read from an environment variable at upload time; no token was read now)
  consent: consent=true in the query string, added when you pass --i-consent-to-submit
Plan headroom: not fetched (offline manifest; a later release can ask the registry with an upload token)
Nothing was uploaded: this command makes no network requests and does not change the folder.
```

The GPU count is the number of distinct `ecc384_id` values across the valid
bundles, and the model is the same part name the certificate prints. A file
that fails to parse, whose signature does not verify, or that is not a
bundle at all is listed under "Files that would not upload", grouped by
reason, and the rest of the manifest still prints. When the same GPU appears
in more than one bundle the manifest says so: the registry records each as a
separate attempt on that GPU and refuses only an identical bundle id.

`--signing-key operator.pem` (or `--public-key operator.pub.pem`) marks
each bundle as signed by that key or by another key, so you can see at a
glance that every bundle carries the key you registered. The key is read
only to compute its fingerprint. `--recursive` includes subfolders. A hidden
file or subfolder (a name starting with a dot, such as the receipts folder a
fleet upload writes) is never read. `--json` prints the same facts as one
JSON document, with a `schema` field of `voltry.fleet.manifest/v1`, for
scripts.

Exit status: 0 when every file is a valid signed bundle; 1 when at least one
file fails to parse or verify, or is not a bundle (the manifest still prints
everything it could read); 2 for a folder that does not exist or a bad
option. Never a traceback.

The plan headroom line is fixed text in this release. The manifest is
offline, so it does not ask the registry how much of your organization's
upload allowance remains; a later release can, with an upload token.

## Uploading a fleet

`voltry submit --dir` uploads a folder of signed bundles to your organization,
one request per bundle, and saves a receipt for each. It needs the `[submit]`
extra, an upload token in an environment variable (next section), and the
same consent flag as a single upload:

```bash
voltry submit --org-token-env VOLTRY_UPLOAD_TOKEN --dir bundles/ --i-consent-to-submit
```

Every token rule in the next section applies first, in the same order: the
URL is checked before the token is read (its path must end in
`/v1/org/upload`; plain http only to this machine), the token comes from the
named environment variable only, and both are checked before any bundle file
is read. `--dir` always uploads to your organization; it does not take the
public route.

The command walks the folder the way `voltry fleet manifest` does, so it
sends exactly the files the manifest lists as valid, in the same order, and
prints its plan before anything is sent: the bundles to upload, the ones
already receipted, any receipt that disagrees with the file beside it, and
the files that would not upload with the reason for each. A file that fails
to parse, whose signature does not verify, that is not a bundle, or that
carries the same bundle id as an earlier file in the folder is never sent.
Neither is a file larger than the registry accepts (1 MiB; the plan says so
and never reads it), nor one whose name a receipt cannot record (a colon as
the second character of its path, as in `x:y.json`, a backslash, a character
that does not print, or leading whitespace; the plan says to rename it), and a hidden
file or subfolder (a name starting with a dot) is never read at all.
`--dry-run` stops after the plan. `--recursive` includes subfolders. A
trimmed run:

```text
Fleet upload plan
Folder: bundles/ (5 .json files; subfolders not included)
Receipts: bundles/.voltry-receipts
Endpoint: https://api.voltry.io/v1/org/upload (one bundle per request, bytes unmodified)

To upload: 3 bundles
  host-a.json: bundle_id 435207b9-deb3-46ba-ad2b-5190dcdecd21, ecc384_id GPU-0a1b2c3d-...
  host-b.json: bundle_id 501339cc-82db-45a8-ba77-8452d571eec4, ecc384_id GPU-11111111-...
  host-c.json: bundle_id 0783b2bd-7c2f-4125-b6c1-9a627c75c8fc, ecc384_id GPU-22222222-...
Skipped: 2 files (never sent)
  signature does not verify (1):
    tampered.json: the signature does not verify
  not a bundle (1):
    notes.json: a JSON object without the bundle fields schema_version, identity
Batches of 20, at most 60 uploads per minute per token, up to 5 retries per bundle on a 429 or 503 response

[1/3] host-a.json: accepted, HTTP 200, receipt 435207b9-deb3-46ba-ad2b-5190dcdecd21.json
[2/3] host-b.json: duplicate (the registry already holds this bundle), HTTP 409, receipt 501339cc-82db-45a8-ba77-8452d571eec4.json
[3/3] host-c.json: accepted, HTTP 200, receipt 0783b2bd-7c2f-4125-b6c1-9a627c75c8fc.json

Fleet upload summary
Accepted: 2
Duplicate (already recorded; receipt saved): 1
Already receipted before this run: 0
Skipped (would not upload): 2
Receipt mismatch (not sent): 0
Failed: 0
Not attempted: 0
Receipts: bundles/.voltry-receipts
```

### What a receipt holds

Each bundle the registry recorded, or answered that it already holds, gets one
JSON file in the receipts folder (`bundles/.voltry-receipts/` unless you pass
`--receipts DIR`), named by its bundle id and written atomically (a temporary
file in the same folder, then given its final name in one step), so a receipt
file is either complete or absent. It holds the bundle file's path relative
to the folder and the SHA-256 of its bytes (the exact request body), the
bundle id, the outcome (`accepted`, or `duplicate` when the registry answered
`duplicate_bundle`), the endpoint, the HTTP status, the time on your
machine's clock in UTC, the probe version, and the fields the registry
answered with: `attempt_id`, `ecc384_id`, `bundle_id`, `tier`,
`authenticity`, `signer_class`, `submitter_class`, `signer_basis` and
`recorded` for an accepted upload, and only the refusal `code` for a
duplicate (the registry's message is not kept). A receipt holds no request
header, and what the registry answered is redacted of the upload token by
shape and by value before it is saved. A failed upload gets no receipt.

```json
{
  "schema": "voltry.upload.receipt/v1",
  "outcome": "accepted",
  "bundle_id": "435207b9-deb3-46ba-ad2b-5190dcdecd21",
  "file": "host-a.json",
  "sha256": "0a5bb29adc1423123f2d205ad9d0cce0db572e1b4ad56079eaa12105b206e853",
  "endpoint": "https://api.voltry.io/v1/org/upload",
  "http_status": 200,
  "received_at": "2026-09-25T16:23:36.150358+00:00",
  "response": {
    "attempt_id": "1e086ded-d22c-407b-bc1f-88ad2f2afdb2",
    "ecc384_id": "GPU-0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
    "bundle_id": "435207b9-deb3-46ba-ad2b-5190dcdecd21",
    "tier": "BRONZE",
    "authenticity": "NOT_ASSESSED",
    "signer_class": "OPERATOR",
    "submitter_class": "ANONYMOUS",
    "signer_basis": "org_registered",
    "recorded": true
  },
  "probe_version": "0.3.4"
}
```

### Re-runs and resumption

Run the same command again after an interruption or a failure and it resumes.
A bundle whose receipt records the same file SHA-256 is skipped as already
receipted; a bundle without a receipt is sent; a bundle the registry already
holds comes back as `duplicate_bundle` and gets a duplicate receipt, so a file
that was sent just before an interruption completes cleanly. A receipt that
records a different SHA-256 than the file beside it, or that cannot be read,
blocks that file: it is not sent, and the run reports the mismatch. Move or
delete that receipt file and run again; the command never overwrites a
receipt that records a different file. Two runs on the same folder cannot
lose each other's record either: when a receipt for a bundle appears while
the run is on and records the same file, the run keeps it and counts the
bundle as uploaded with what that receipt says, with one exception: an
accepted answer replaces a duplicate receipt for the same file, since it
records strictly more (the registry's `attempt_id`), and a duplicate answer
never replaces an accepted receipt. When the receipt that appeared records
another file, the run leaves it in place and reports that bundle as failed.
Ctrl-C between requests stops the run with its summary (exit status 130),
and the receipts already written stay.

Before the first request leaves, the run creates the receipts folder if it
is missing and checks that it can write a file there; a folder that cannot
take a receipt is a usage error (exit status 2) and nothing is sent, so the
registry never records a bundle this machine has no receipt for. The dry run
does not touch the folder. `--receipts` must not be the bundle folder or a
folder above it (that would exclude every bundle from the walk); a receipts
folder inside the bundle folder, the default, is fine.

### Batches, the rate limit and retries

Bundles go in groups of `--batch N` (default 20) with a pause between groups
sized to the registry's cap of 60 uploads per token per minute. A 429 or 503
response, whatever its body (the registry's `upload_rate_limited`,
`write_conflict` or `registry_unavailable`, or a proxy's page with no
refusal code at all), waits the number of seconds in the response's
`Retry-After` header (at least one second, at most 120; a missing or
unreadable header counts as one) and retries the same bundle, up to
`--max-retries N` times (default 5). When the retries run out the run stops
rather than moving on, since every later bundle would meet the same wait;
wait, then re-run to resume. Any other refusal fails that bundle and the run
goes on to the next, except one after which nothing later would succeed: a
401 of any kind (`invalid_upload_token`, `missing_token`), a 404 or 405,
whatever its body (the path is pinned to `/v1/org/upload`, so a URL that
answers either is wrong for every bundle; check `--url`), `forbidden_role`,
`org_suspended`, `forbidden_tier`, `quota_exceeded`, `consent_required` and
`ambiguous_credentials` stop the run at once, and so do a failed
connection and a receipt that cannot be written (the registry has the bundle
but this machine has no receipt for it; fix the receipts folder and re-run,
and the registry's `duplicate_bundle` answer saves the receipt then). A re-run
resumes from the receipts.

### Exit status and JSON

0 when every valid bundle in the folder is receipted (a skipped file does not
count against it; the plan names it); 4 when any bundle failed, was blocked by
a receipt mismatch, or was not attempted because the run stopped (a dry run
with a receipt mismatch in its plan exits 4 too, since the run would); 130
after Ctrl-C; 2 for a usage error (a missing folder, a bad option, a refused
URL or token, a receipts folder that cannot be written); 3 without the
`[submit]` extra. `--json` prints the summary as one JSON document on
stdout, with a `schema` field of `voltry.upload.summary/v1` and an
`exit_code` field that says the same, and sends the plan and the progress
lines to stderr.

### Checking the receipts

`voltry receipts verify` checks every saved receipt against the bundle file
it names, offline:

```bash
voltry receipts verify bundles/
```

For each receipt: the file has the receipt shape, the bundle file it names is
in the folder with the recorded SHA-256, that file parses as an evidence
bundle, verifies, and carries the receipt's bundle id, and the bundle id the
registry answered with, when it gave one, is that same id (the upload prints
a warning line when it is not, and still saves the receipt). It also lists
the valid bundles that have no receipt yet, and anything else in the
receipts folder (a temporary file an interrupted write left behind, a
subfolder). `--receipts DIR` names the receipts
folder when the upload was given one; `--json` prints the check as one
document with a `schema` field of `voltry.upload.receipts-check/v1`. Exit 0
when every receipt verifies, 1 when at least one does not, 2 for a missing
folder. Nothing is sent and nothing is written.

## Uploading to your organization with an upload token

`voltry submit` can upload to your organization instead of the public
registry route. It needs an upload token, which an owner or admin creates on
the same Signing keys page. The console shows the token once and cannot show
it again. Every token has an expiry, at most a year out, and an owner or
admin can revoke a token at any time.

Put the token in an environment variable on the uploading machine, without
typing it on a command line (a command line ends up in shell history): from
your secret store, or from a file only you can read. Then pass the variable's
name, never the token:

```bash
export VOLTRY_UPLOAD_TOKEN="$(cat ~/.config/voltry/upload-token)"
voltry submit bundle.json --i-consent-to-submit --org-token-env VOLTRY_UPLOAD_TOKEN
```

The token is read from that environment variable only. No option takes the
token itself, and if you type the token where the variable name goes, the
command refuses without echoing it and tells you to revoke it, because it may
now be in your shell history. The value is checked locally for the token
shape (`vot_`, 24 hex characters, a dot, then the secret) before anything is
sent, and it is never printed.

Everything that applies to a public upload still applies first: the consent
flag, the local check that the file is a valid bundle whose signature
verifies, and the https rule. With a token the URL rule is stricter:

- The default endpoint is `https://api.voltry.io/v1/org/upload`, and the
  token travels in the `X-Voltry-Upload-Token` header.
- A `--url` you pass must end in exactly `/v1/org/upload`, with no trailing
  slash. This stops an old script pointed at the public `/v1/ingest` from
  sending the token along and putting the record on the public route.
- Plain http is allowed only to this machine (`localhost`, `127.0.0.1` or
  `::1`), even with `--allow-insecure-http`, so the token never crosses a
  network in clear text.
- The URL is checked before the token is read, so a refused URL never has
  the token in hand.

Any response other than success is a failure (exit status 4), and the reason
code the platform sent is printed. The ones you are most likely to see:

- `unregistered_signer`: the bundle is signed with a key your organization
  has not registered, or has revoked. An upload token only uploads bundles
  signed with a key registered to the same organization. Check which key
  signed the bundle with `voltry key fingerprint --signing-key operator.pem`,
  compare it with the Signing keys page, and register the key if it is
  missing (previous section). Then submit again. A key on Voltry's own list
  for the public route does not count here; the organization has to register
  it too.
- `invalid_upload_token`: the token is malformed, wrong, expired or revoked,
  or the member who created it is no longer an active member of the
  organization. The platform gives one answer for all of these on purpose.
  Create a new token in the console.
- `forbidden_role`: the member who created the token can no longer upload
  (a viewer, for example). The token works again if the role changes back.
- `duplicate_bundle`: this exact bundle is already recorded.
- `upload_rate_limited`: this token has used its per-minute allowance. Retry
  after the interval in the response.
- `quota_exceeded`: the organization has reached its upload allowance. Ask
  an owner.

A token uploads and does nothing else. It cannot register keys, create
tokens, or read anything. It acts as the member who created it: if that
member is removed from the organization or their account is disabled, the
token stops working for good, and restoring the member does not bring it
back.

## What stays private

Records uploaded with an upload token are private to your organization. They
do not appear on the public verify site or in anyone else's registry view.
The public route (`voltry submit` without `--org-token-env`) is the opposite:
it is for records meant to be publicly verifiable.

The probe sends nothing anywhere unless you run `voltry submit`. Scanning,
rendering, the key commands, the fleet manifest and the receipts check work
offline, and `voltry submit` uploads only the bundle file you name (or, with
`--dir`, the valid bundles in the folder you name), only with the consent
flag.

Availability: voltry-probe 0.3.4 includes key commands, token upload, resumable directory submission, fleet manifests and receipt verification.

## Keeps reading

Tracking v0 runs **periodic reads, self-reported (A0)**. It is not continuous monitoring.
The kit reads the default GPU (index 0) on a Linux host once a day. Each successful
scan uses the same persistent signing key and a new bundle ID and file. It then runs
the separate explicit upload:

```sh
voltry scan --signing-key /etc/voltry/operator.pem --out UNIQUE_READ.json
voltry submit --org-token-env VOLTRY_UPLOAD_TOKEN --dir /var/lib/voltry/reads --i-consent-to-submit
```

Run the receipt check manually, offline:

```sh
voltry receipts verify /var/lib/voltry/reads
```

Scanning stays offline and read-only. Only the explicit `submit` uploads the signed
bundle files, including their captured device reads, to your organization. Review
what a bundle contains before opting in. There is no background upload from scan.
Receipts live in `/var/lib/voltry/reads/.voltry-receipts/`, one per bundle ID. A rerun
skips already filed files only when their receipt matches the exact file hash.
Keep both bundles and receipts. Changed files are blocked, never silently replaced.
The receipt verifier works offline. The runner serializes overlapping jobs and keeps
failed scan output in hidden `.pending-*.json` files for local inspection. A scan
failure skips upload; an upload failure keeps the new read for the next run.
The scan timeout is 120 seconds; the submit timeout is 300 seconds. The service
activation limit is 450 seconds. A timeout or busy lock exits nonzero for the
journal or cron notification. The scan child never receives the upload token.
Hidden failed or empty pending files remain local: inspect and archive them under
your retention policy. Never rename or upload a failed pending file.

The installed wheel carries the service, timer, cron and environment example in
`voltry_probe/deploy`. Locate them with your installation's Python:

```sh
/opt/voltry/venv/bin/python -c 'import pathlib, voltry_probe; print(pathlib.Path(voltry_probe.__file__).parent / "deploy")'
```

Administrator installation on Linux:

1. Install the reviewed `voltry-probe[hardware,submit]` release in
   `/opt/voltry/venv`, owned by the administrator. Use a dedicated unprivileged
   `voltry` account. Grant only the GPU device read permissions your driver needs.
   This kit reads index 0 only; multi-GPU scheduling needs separate explicit wiring.
2. Create `/var/lib/voltry/reads`, owned by `voltry:voltry`, mode `0700`.
   Provision `/etc/voltry/operator.pem`, mode `0640`, owner `root:voltry`, from your
   secret store. Register that persistent signing key with your organization.
3. Copy `read.env.example` to `/etc/voltry/read.env`, owner `root:voltry`, mode
   `0640`. Provision `VOLTRY_UPLOAD_TOKEN` there from your secret store. Never put
   its value in the unit or cron command. The environment file must contain plain
   POSIX assignments, with no shell expansions, so either scheduler can read it.
4. Copy the service and timer into `/etc/systemd/system/`. Review them and run
   `systemd-analyze verify` against both files on the target host. The service has
   no elevated capabilities, uses `NoNewPrivileges` and `ProtectSystem=strict`,
   and can write only the read folder; its private temporary directories are read-only. Its executable is
   `/opt/voltry/venv/bin/python -I -m voltry_probe.scheduling`; isolated mode
   excludes the writable read folder and user packages from imports.
5. The local setting `VOLTRY_ENABLE_SCHEDULED_READS=false` keeps every run off.
   After approving the host's uploads, the administrator sets it to `true` and enables
   `voltry-read.timer`. The timer runs daily, adds up to 30 minutes of randomized
   delay, and catches a missed run with `Persistent=true`. Read
   `journalctl -u voltry-read.service` and run the offline receipt check above.
6. For cron instead, install `cron/voltry-read.cron` as `/etc/cron.d/voltry-read`,
   owner `root:root`, mode `0644`. It runs daily at 03:17 as `voltry` and reads the
   same environment file. Use one scheduler per read folder. Cron has no missed-run
   catch-up or service sandbox; restrict that account and its filesystem permissions.

### Stop and uninstall

Set `VOLTRY_ENABLE_SCHEDULED_READS=false` in `/etc/voltry/read.env` first. For
systemd, run these administrator commands:

```sh
sudo systemctl disable --now voltry-read.timer
sudo systemctl stop voltry-read.service
sudo rm /etc/systemd/system/voltry-read.timer /etc/systemd/system/voltry-read.service
sudo systemctl daemon-reload
```

For cron, remove `/etc/cron.d/voltry-read` instead. Revoke the upload token in the
organization console. Keep the signing key, all reads and `.voltry-receipts` while
running `voltry receipts verify /var/lib/voltry/reads`. After successful verification,
archive the reads, receipts and retained pending files under your retention policy.
Do not delete evidence before verifying and archiving it.

Nothing installs, enables a scheduler, or changes a server setting automatically.

## Security

To report a vulnerability, see the repository security policy:
https://github.com/Voltry-tech/voltry-probe/blob/main/SECURITY.md. Please report
suspected signature bypasses or key-handling flaws privately, not in a public
issue.

## Related

Looking for a hardware report on your own Apple silicon Mac? That is `voltry-mac`.

## License

Apache-2.0.
