# POT manufacturing CLI

POT turns a configured macOS, Linux, or Windows workstation into an SD-card
manufacturing station. Its primary contract is deliberately narrow: prepare one
trusted golden image, flash it to selected cards, verify it, provision each card
with station settings and Tailnet enrollment material, verify that configuration,
and either eject the exact retained media object or explicitly require the
operator to eject it before removal.

POT never interprets “removable” or “external” as permission to erase a disk.
Only media currently inserted in reader-slot IDs explicitly approved during
`pot setup` can become write targets.

## Operator workflow

1. Insert one disposable card into every physical reader slot that this station
   is allowed to use, then run:

   ```console
   pot setup
   ```

   Setup records the partner, 2.4 GHz Wi-Fi, writer concurrency, and selected
   stable reader IDs. The receiver login is fixed to `operator` / `operator`;
   it is never prompted. Setup validates the partner-gated Cloudflare Tailnet
   enrollment path before saving. Only the Wi-Fi password is prompted, without
   being placed in shell history.

2. Inspect the station and its slots:

   ```console
   pot readers
   pot doctor
   ```

   `readers` labels discovered slots as approved, available but not approved, or
   ignored. `doctor` is read-only and checks station configuration, partner,
   Tailnet, platform tooling, raw-device access, and reader safety evidence. It
   exits nonzero when the station is not ready, so it can be used as a release
   or shift-start gate. Run it from the same Administrator/root-capable context
   that will run manufacturing, with a disposable card inserted in an approved
   reader so raw-device access is proved rather than inferred from elevation.

3. Insert the cards and start the guided batch:

   ```console
   pot setup radxa-cubie-a7z
   ```

   Replace `radxa-cubie-a7z` with the required board hardware key. POT performs
   the complete preflight, displays the exact inserted targets, and asks once
   for `SETUP`. A normal run processes that snapshot and exits. `--yes` is
   available for controlled automation, but it does not bypass any safety
   check. `--dry-run` is an optional diagnostic preview which writes no card;
   it is not a prerequisite because the real run repeats the full preflight.

4. For repeated production, keep the run armed and replace only finished cards
   in the same slots:

   ```console
   pot setup radxa-cubie-a7z --watch
   ```

   The one initial confirmation authorizes the displayed run and its approved
   reader IDs. After a card reaches a terminal state and is removed, a replacement
   in that same slot may start automatically. A newly attached or unapproved
   reader cannot enter the run. With a serial-less reader, leave the completed
   slot empty for at least one polling interval (about one second) before
   inserting its replacement; topology or device-name churn is never treated as
   a new card. Use `--count N` for a bounded watch run.

5. View durable progress from another terminal:

   ```console
   pot status --watch
   ```

## Target selection and fail-closed preflight

Disk discovery classifies all candidates so POT can reject internal, system,
virtual, read-only, ambiguous, or unsupported devices. Discovery is not write
authorization. A card is eligible only when all of these remain true:

- Its physical reader has a stable ID explicitly approved during setup.

  On macOS, USB readers are bound to IORegistry's physical
  `locationID` together with vendor, product, and serial evidence. Readers
  stamped with one duplicate factory serial therefore remain distinct when
  they occupy distinct controller/hub/port paths. Moving a reader to another
  port intentionally produces a new unapproved slot. A genuinely missing or
  ambiguous USB location remains ineligible; `/dev/diskN` is never promoted
  into an approval identity.

  On Windows, POT binds approval to
  `DEVPKEY_Device_LocationPaths`, the physical PnP controller/hub/port path.
  It therefore distinguishes readers carrying the same vendor serial. A
  missing physical location fails closed; a PnP instance ID, model, drive
  number, or duplicated serial never becomes a reader approval by itself.

- It is present, removable, writable, non-system media with sufficient safety
  evidence and supported USB, SD/MMC, or IEEE 1394 topology.

- It is part of the target list shown before confirmation. `--reader` and
  `--only-device` can narrow this list; neither option bypasses approval.

`pot setup --all-readers` explicitly approves every currently listed safe reader
slot. It does not approve every external disk, future reader, or drive on the
machine.

Before asking for the destructive confirmation, `pot setup` fails closed unless:

- station settings and at least one approved reader are valid;

- the partner is live and authorized;

- the Tailnet credential or broker path works;

- the selected image is downloaded, hash-verified, decompressed, and prepared;

- every selected card has capacity for the exact prepared image; and

- writer concurrency is between 1 and 20.

Before showing the destructive confirmation, POT unmounts and opens each
selected raw device read/write without changing bytes, captures its native
media-generation token, and retains that exact operating-system handle while
the prompt is visible and while the card waits for a writer slot. Erase, image
write, device flush, and raw verification all use that same handle; a card
inserted after the displayed selection cannot inherit the approval. Each card
obtains its Tailnet enrollment material before destructive processing. If any
required check is uncertain, that card does not proceed.

## Concurrency, states, and completion

The conservative station default is four simultaneous writers. `pot setup
--parallel N` configures the station default; `pot setup BOARD --parallel N`
can select 1–20 for one card-setup run when the USB topology,
powered hubs, readers, storage, and host have been validated at that load. Ten to
twenty inserted cards may therefore queue behind four writers, or run up to a
higher explicitly configured parallelism. Per-device and per-station locks
prevent two workers or two `pot setup` sessions from controlling the same station.
Normal `pot setup BOARD` overwrites and reads back the complete prepared image and
clears and verifies its temporary binding sector. It leaves all other unused
capacity alone instead of making card setup depend on unrelated sectors at the
physical end of a much larger card. For reused media that carried confidential
recordings, add
`--secure-erase`; POT then zeros and reads back every logical sector beyond the
image. Size a secure-erasure batch for that full-card write-and-read workload.

The primary manufacturing path is:

```text
WAITING -> ERASING -> WRITING -> VERIFYING -> PROVISIONING
        -> CONFIG_VERIFYING -> READY
```

`FAILED` and `REMAKE_REQUIRED` are terminal failures. State is journaled in a
durable SQLite database. If POT exits before a card reaches `READY`, an unfinished
card is reported as `REMAKE_REQUIRED` on the next status read instead of being
assumed usable.

`READY, SAFE TO REMOVE` means the image and provisioned configuration passed
their required readback checks, data was flushed, and POT successfully ejected
the exact retained media object. If the host has no supported exact-handle
eject, POT instead prints `READY, EJECT BEFORE REMOVAL`; use the operating
system's eject action before pulling that card. POT never performs a final
eject by resolving a volatile device path after closing its media-bound
handles. Both READY forms mean valid Tailnet enrollment material was written.
The receiver joins the Tailnet when it boots; `READY` does not claim that the
board has already booted or appeared online.

Stopping a run prevents new work from starting. A card already inside the
destructive section is allowed to finish safely rather than being abandoned
mid-write.

## Verification boundary

Every byte written from the prepared image is read back through the same raw
device handle and compared exactly before that handle is closed. FAT payloads,
boot files, partition metadata, and non-FAT regions all have the same strict
rule: any mismatch fails the card. Normal manufacturing also zero-verifies the
temporary sector just beyond the image; it does not claim to sanitize unused
capacity.
With `--secure-erase`, every logical byte after the aligned image end is zeroed,
flushed, and read back as zero. This prevents ordinary logical recovery of an
older card's trailing data; it does not claim forensic erasure of flash cells
hidden behind an SD controller.

Reader approval and card identity are deliberately separate. `reader_id`
authorizes one physical reader slot. A reader that exposes no media serial may
give the inserted card only a topology or volatile device-name `stable_id`;
POT does not treat either fallback, nor a temporary missing `diskutil`/`lsblk`
sample caused by its own unmount, as proof that the card was swapped. The card
is instead retained through one raw descriptor and checked with the operating
system's media-generation token before erase, after durable callbacks, across
configuration, and around the final `READY` commit. An explicit conflicting
hardware identity may stop the run as supplemental evidence, but cannot replace
those native generation checks.

Host filesystem APIs require the raw writer to close before POT can mount the
new partitions and provision configuration. POT bridges that boundary with a
random, temporary sector outside the image's written region. It verifies the
sector before configuration access, verifies it again after configuration is
flushed, and restores zeros before reporting `READY`. If an image leaves no
spare sector for this binding, POT refuses it before erase. Provisioned
configuration is read back exactly, and a failed filesystem check is never
reported as `READY`.

Primary manufacturing also binds the exact filesystem object that receives
secrets. Native POT images carry a fixed-width `POT_MEDIA_BINDING=` slot plus a
`POT_TAILNET_RETRY=1` attestation. The current SHA-pinned legacy catalog images
(1.5.1 for Pi 3B+, A5E, and Zero 3W; 1.7.1+2026082402 for A7Z) predate those
markers, so POT adapts only an exact allowlisted catalog artifact; an arbitrary
local legacy image is rejected.

For the current FAT images, POT parses the FAT directory and cluster chain to
find the active `stereorx-firstboot.sh` entry, ignoring deleted and stale raw
copies, and requires its pinned SHA-256. It carries the exact GPT/MBR partition
number and size from that authenticated image preflight into post-flash
provisioning, so a temporarily missing FAT label cannot redirect an A7Z write
into its ext4 rootfs. For the current Zero 3W image it uses
the unique, pinned build-config object and verifies the installed firstboot
through the same retained ext4 descriptor. POT places a random 192-bit nonce in
that authenticated object through the retained whole-disk handle, proves the
same nonce through the retained filesystem handle, and only then writes
credentials. It replaces the current one-shot firstboot with a verified wrapper
that moves the key to the root filesystem and installs a persistent systemd
retry timer. The wrapper confirms Tailscale `BackendState=Running` and an
assigned Tailnet address before removing the key and retry machinery. The
temporary binding bytes are removed or restored exactly, configuration is read
back byte-for-byte, and ext4 changes must pass `e2fsck` before `READY`.

## Platform backends

| Host | Discovery and write path | Station/release requirements |
| --- | --- | --- |
| macOS | `diskutil` discovery and raw `/dev/rdisk*` access | Removable-volume/raw-device authorization (or root), owner-only station storage, and packaged or installed ext4 tooling when required |
| Linux | `lsblk` plus sysfs topology and raw block devices | Root or suitable udev/polkit block access, unmount support, owner-only station storage, and e2fsprogs when required |
| Windows | PowerShell/CIM/PnP discovery and retained `PhysicalDrive` handles | Run as Administrator. The current Pi 3B+, Cubie A5E, and Cubie A7Z FAT-config goldens are supported; Zero 3W ext4-only manufacturing fails before erase. Cloud offload and local export use the bundled read-only ext4 parser without WSL or a filesystem driver |

On a macOS station used over SSH, a logged-in Finder session may auto-mount a
card's boot volume and `loginwindow` may dissent from the normal Disk
Arbitration unmount. Destructive POT operations first request a normal
whole-disk unmount, then retry once with macOS's explicit forced whole-disk
unmount. This fallback is restricted to the flash/erase engine after the target
has passed removable-media and approved-reader checks. If macOS refuses both,
POT writes nothing and asks the desktop user to eject the card's volumes in
Finder while leaving the physical card inserted.

The Windows manufacturing backend has no safe inherited-handle write path for ext4
provisioning. POT therefore fails closed for an ext4-only golden image there,
even if ordinary e2fsprogs binaries are installed: those tools would reopen a
volatile device path after image verification. A Windows-qualified golden image
must include the standardized FAT provisioning partition. A future ext4 helper
must accept an already-open native device handle and be qualified on real
hardware before this restriction can be relaxed; image flashing alone is not
sufficient.

## Tailnet issuer release setup

The CLI contains no embedded Tailscale credential. For this release,
manufacturing uses the existing `DEV_TAILNET_KEY` held by the partner-gated
`/pot/tailnet-key` Worker. A successful live probe reports
`source=legacy-secret`, and the credential response explicitly reports
`reusable=true`. Both responses are accepted. The Worker must remain reachable:
mandatory manufacturing does not fall back to an unverified local key when the
broker is offline, even if the Mac Keychain holds the same value.

Provision the Worker secret directly, without putting its value in a profile,
image, repository file, command argument, log, or packaged CLI:

```console
cd infra/ota-worker
npx wrangler secret put DEV_TAILNET_KEY
npm test
npm run deploy
```

The reusable key is a deliberate release exception and has wider blast radius
than a one-use credential. Rotate or revoke it if it may have been exposed.
OAuth-based one-use issuance remains supported by the Worker as a later
hardening path, but it is not required for this release.

No new golden image is required for the current reviewed catalog artifacts: POT
recognizes only their exact artifact, raw-image, and firstboot hashes and injects
the persistent retry wrapper per card. Future or local images must carry the
native binding and retry attestations produced by `infra/sanitize-image.sh`;
POT will not guess that an unrecognized legacy image is safe.

## Offload and local-export key boundary

The standalone CLI does not carry or install the fleet private key. A live
partner code claims a receiver-bound routing token. For each selected recording
session, the CLI sends that session's public-key envelope to the upload Worker;
the Worker unwraps it with its `FLEET_PRIVATE_KEY_PEM` secret and returns only
the one 32-byte session key. Receiver ID, session ID, public-key ID, session-key
ID, and the RSA-OAEP label all have to agree.

POT authenticates and decrypts `.srx` chunks in bounded memory and streams
plaintext directly to the routed organization bucket. It uploads neither the
wrapped envelope nor a local plaintext staging file. The production default is
two concurrent sessions per card. QC for later sessions overlaps the upload of
already-approved sessions, but every session must pass QC before any byte from
that session uploads. `--session-parallel 1` selects conservative serial
session operation. Every concurrent session uses an independent retained,
media-generation-checked descriptor; one seek cursor is never shared across
debugfs or portable ext4 readers. Explicit preflight and recording erase retain
the full-card QC boundary.

Each active session may keep four 16 MiB HTTP parts in flight plus one
prefetched part, for an approximately 160 MiB default per-card payload-memory
ceiling. One forward-only producer authenticates, decrypts, and hashes the
source; HTTP workers never share or seek its cursor. Each part worker owns and
reuses its HTTPS connection; no connection object is shared between threads.
Dead sockets time out after 90 seconds rather than ten minutes. POT emits a
liveness heartbeat every 15 seconds, names every retry/backoff, and separately
reports source and acknowledged-network throughput plus the actual active and
maximum part windows.
The retained media generation is attested before and after the complete source
stream, while the second-pass plaintext digest must equal the authenticated
first-pass digest. This keeps native generation probes out of the part-refill
loop without weakening the exact-card evidence. Small AEAD-authenticated files
may reuse the exact bounded first-pass body; large payloads remain two-pass
because their SHA-256 is bound at multipart creation.

Resume-state mutations are serialized and durable, every cloud object must
match its plaintext size and SHA-256, and the exactly read-back per-session
manifest is the last write. A saved digest is not treated as proof of a newly
inserted source: clone cards and repeated session names exist in the fleet, so
normal reruns reread the card and reuse only acknowledged multipart parts.
`--all-cards`
explicitly selects every readable attached receiver card; `--card-parallel N`
controls cards, `--session-parallel N` controls operations within each card,
and `--parallel N` controls multipart requests within each session. Cards keep
independent device locks and resume journals. A receiver-scoped process lock
prevents legacy clone cards with the same receiver identity from racing the
same remote namespace. Existing cloud objects are reused only when plaintext
size and SHA-256 match exactly; mismatched objects are never overwritten.

`pot export` uses the same authenticated QC and Worker key unwrap, but never
calls the object-storage upload API. Plaintext streams directly to a private
destination staging directory, each file is fsynced and independently reread
for SHA-256, and the session is atomically published with
`pot-export-manifest.json` and `SHA256SUMS`. The source card remains read-only
and `key-envelope.json` is not exported.

## Development and native packaging

For an editable development install:

```console
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -e ".[build]"
pot --help
python -m unittest discover -s tests -p "test_*.py" -v
```

PyInstaller bundles must be built on their target operating system; the build
does not cross-compile. After installing `.[build]`, use:

```console
# macOS
./packaging/build-macos.sh

# Linux
./packaging/build-linux.sh
```

On a Windows development host:

```powershell
py -3 -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install ".[build]"
.\packaging\build-windows.ps1
```

Windows archives must be built and smoke-tested on native Windows; PyInstaller
does not cross-compile. The inexpensive preview channel has no hosted-runner or
code-signing dependency. Its manifest is explicitly `unsigned-preview`, the
installer displays a prominent warning and requires operator consent, and it
still enforces HTTPS, the immutable archive size and SHA-256, safe ZIP paths,
the complete inner `SHA256SUMS` inventory, version output, and atomic install.

Move the native-tested archive to a release Mac or Linux host and run:

```bash
POT_PUBLISH_UNSIGNED_WINDOWS=1 \
  ./packaging/publish-windows-preview.sh /path/to/pot-cli-VERSION-windows-x86_64.zip
```

The local publisher validates the archive, refuses conflicting immutable
objects, uploads directly to Cloudflare R2, deploys the existing Worker route,
publishes the mutable pointer last, then downloads and rehashes the public
release. Local Wrangler login or scoped Cloudflare environment credentials are
the only publication authority. No repository automation is involved.

The same manifest and installer retain a future `public-authenticode` mode. In
that mode every `.exe`, `.dll`, and `.pyd` must carry a valid common Artifact
Signing identity EKU. Until such a release exists, documentation and output
must call Windows an unsigned preview rather than implying publisher trust.

All wrappers call `python packaging/build.py`. A successful build creates an
app-independent onedir bundle containing the executable, native dependencies,
and `SHA256SUMS`. POSIX bundles also contain the privilege-aware `pot` launcher
and `pot-engine`. The versioned archive is named
`pot-cli-<version>-<system>-<architecture>.tar.gz` on POSIX and `.zip` on
Windows. Release
builds set `POT_E2FSPROGS_DIR` to vetted native helpers and `POT_SIGN_ID` on
macOS before the archive manifest is generated. Build and test every target on
its native operating system, then qualify it with actual powered-reader
hardware before using concurrency above the default.

Linux release bundles must require no symbol newer than `GLIBC_2.35`, the
Ubuntu 22.04 deployment boundary. `packaging/build.py` inspects every bundled
ELF and rejects a newer build host/runtime before creating the archive.
