<!-- © TALOS Group Pty Ltd, used under licence by TALOS Defence Pty Ltd. -->
<!-- depth: 2 -->
<!-- Body written by slice D-01. -->

# Changelog

The changelog lists every Hangar release, with what changed and the digests of the published artefacts. The release procedure adds one entry for each release.

## Next release

This entry lists what the next release adds. The release procedure gives the entry its version, its date and its digests.

- The minor version 5.3.0 follows 5.2.10, as one release of the operator tools (the Director, 8 November 2026: one PR). `hangar-ops update` is the one command after a merge: it fast-forwards the Hangar folder, rebuilds the Mac tools and the key-site app only when they are older than main, and prints each cell's last deploy and what the next deploy changes; every other command prints one line when main is newer. `deploy api`, `deploy web` and `cell bootstrap` apply their SSM document themselves when it differs from the commit's host script (after the approval for a deploy), and refuse a `hangar-ops` older than the commit in what it builds or sends; `--version` names the build commit and date. `deploy web` puts the host in Melbourne's web target group and installs the reviewed app start script and unit on every host (Sydney's were made by hand), with Sydney's app cell file for the sign-in hand-over. The cell bootstrap keeps a value `config apply` set, reads `/etc/hangar` with `stat`, and its operator step waits for the API. `dnsfw check` reads Melbourne's allow list too, and `hangar-ops --cell melbourne dnsfw add` adds to it. The key site's status file names its build, and the app and `hangar-keysite status` say when a newer one is installed. CI runs the web app's check and the cell bootstrap's test. The operations screen (`hangar-ops` with no command) shows every cell: Home has one section per cell (version, API units, key site, capacity, newest deploy), "Needs you" names each line's cell, and Deploy, Blocks, Queue and Proposals read each cell. `C` steps the cell filter (every cell, Sydney alone, Melbourne alone), and the header shows it. A cell that cannot be read names the command to run. Capacity lists the Capacity Blocks AWS holds for each cell, adopted or not, with the `block adopt` step for one that is not; blocks whose payment failed show once. The key-site line counts one serving unit of a pair as healthy (the idle unit is not a fault), with the pair rule `hangar-keysite status` uses, and a small clock difference reads `just now`. `--cell sydney|melbourne` aims any `hangar-ops` command at a cell with that cell's own session; an action on a Melbourne row runs with it. The platform version is the release version (decision 154). A `deploy api` that leaves every cell on one release records it as the platform version (the key site signs with the release root, the operator approves); `platform cut` is the override (a catch-up, or a jump such as 6.0.0). A platform version may skip numbers and never goes back. `hangar-ops find <words>` finds a command by its words and fills it: "book trn2 72 hours" gives `hangar-ops block offers --type trn2.3xlarge --hours 72`. No model and no network: words match command names, help and values exactly, by prefix or with a typo, and through a reviewed word table; numbers with units, cells, versions and `cr-` ids fill the flags, and a value not given stays a `<placeholder>`. Nothing runs. In the operations screen the `/` line lists the best five as you type; Tab fills the line, Enter runs the line as it reads. The control plane's code changes only in the platform version rule (`hangar_api::platform_version`): a new platform version is any greater version whose step covers the changes.
- The patch version 5.2.10 follows 5.2.9. The key site and its menu bar app are simple to run. `hangar-keysite start`, `stop`, `restart`, `status` and `help` take `~/hangar-keysite/keysite.json` by default, and `stop` is the safe stop (the stop request, which wipes the keys), never a signal. Each cell host's two API units are one unit pair (`api-a` with `api-b`, `mel-api-a` with `mel-api-b`): a pair is healthy when one of its planes is open, so the app shows a green tick with one unit idle, and the status file names each plane's pair. `hangar-ops tools install` also builds the app and places it in `~/Applications`. The app has Restart, Help and Open at login. The help shows the build's commit and its date, and the date the help was last updated; a test fails when the help changes without its date. No code of the control plane changes.
- The patch version 5.2.9 follows 5.2.8. A new cell's host serves the web app. The cell bootstrap's `config` step makes the system user `hangar-app`, `/etc/hangar-app/cell.env` (the cell's app values), `/usr/local/libexec/hangar/hangar-app-start` and the `hangar-app` unit (the new reviewed files `deploy/production/hangar-app-start` and `deploy/production/systemd/hangar-app.service`), and `secrets` makes the cell's `app-session-secret`; the Director places `tls/web` for the cell's app name. `hangar-ops deploy web` puts the pinned Node (`deploy/production/node-linux-x64.sha256`) in each cell's bucket, and the host installs it when its own Node differs or is missing; the host checks the app under the cell's app name (`app-mel.gethangar.au` in Melbourne). The `hangar-cell-bootstrap` SSM document carries the script gzip-compressed. `--commit` is optional on `deploy api`, `deploy web`, `build control`, `image build`, `image serve`, `tools install` and `cell bootstrap` (decision 156): left out, the command fetches origin, takes origin/main, prints that commit first, and refuses while the local main holds commits origin/main lacks. No code of the control plane changes.
- The patch version 5.2.8 follows 5.2.7. The host deploy script checks a unit's health under the cell's own API name (`api-mel.gethangar.au` in Melbourne), the name on the cell's certificates; it was fixed at `api.gethangar.au`, so the first Melbourne deploy rolled back a unit that served. The cell bootstrap's `config` step writes the `hangar-jobs` unit (`deploy/production/systemd/hangar-jobs.service`, new) when the host has none, so the first deploy can start `hangar-jobs`. A rollback on a host with no jobs unit prints no `systemctl` message. The website archive carries no macOS extended attributes. No code of the control plane changes.
- The patch version 5.2.7 follows 5.2.6. The cell template `deploy/production/cell/hangar.toml.template` sets both checkout return pages on the cell's own app origin (`@@APP_ORIGIN@@/credit/paid` and `@@APP_ORIGIN@@/credit`), the origin `public_origin` names. The template fixed them at `https://gethangar.au`, so `hangar-api` refused the Melbourne configuration at start. A new test fills the template with the Melbourne values and runs the production rules of `hangar-api` on it. No code of the control plane changes.
- The patch version 5.2.6 follows 5.2.5. `infra/aws/cell-mel/c15-hosts.zsh apply` adds a zone of `hangar-mel-nlb` back with its Elastic IP address only after the load balancer's network interface in that subnet is gone. A removed zone leaves the load balancer's description before AWS ends its removal, and AWS refused the add with `ValidationError ... in the process of being removed`. No code of the control plane changes.
- The patch version 5.2.5 follows 5.2.4. Melbourne's API load balancer `hangar-mel-nlb` answers on two Elastic IP addresses, `hangar-mel-nlb-az1` and `hangar-mel-nlb-az3`, one per zone, as Sydney's `hangar-nlb` does. `infra/aws/cell-mel/c15-hosts.zsh` allocates them and binds each zone in place: it removes the zone, then adds it back with its address, so the load balancer keeps its DNS name and its listeners. The key site dials the two fixed IPv4 addresses, and a customer's firewall can list them. No code of the control plane changes.
- The patch version 5.2.4 follows 5.2.3. `hangar-ops deploy api` ships the website's files and the DFAT converter with every release: it builds `web.tar.gz` from the commit (the browser verifier `hangar_cert.wasm`, the hashed files of `tools/assets.py` and their manifest, `verify.html` and `tools/dfat_to_csv.py`), uploads it beside the host files and passes its SHA-256 to the host's `install`. `install` checks the archive's digest and entries before it installs anything, then places `/usr/local/share/hangar/web` (keeping the previous folder as `web.prev`) and `/usr/local/share/hangar/tools/dfat_to_csv.py`. A new cell host had none of these files, so `hangar-api` would not start there, and a release's changed website files had never reached a host. The SSM document `hangar-api-deploy` is applied again for this release. No code of the control plane changes.
- The patch version 5.2.3 follows 5.2.2. The `config` step of `ops/onhost/cell-bootstrap.sh` makes the system user `hangar`, `/var/lib/hangar` (hangar:hangar, 0750) and `/etc/hangar` (root:hangar, 0750) when they are missing, as the Sydney host has them. The units run as `hangar`, and the host image carries none of the three, so the step stopped at `chown: invalid group: root:hangar`. `plan` lists the user. No code of the control plane changes.
- The patch version 5.2.2 follows 5.2.1. The `config` step of `ops/onhost/cell-bootstrap.sh` looks up the cell's three GPU subnets with a filter that has no comma at the end. The earlier filter ended with a comma, so the AWS CLI refused it (`ParamValidation`) and `hangar-ops cell bootstrap melbourne` stopped at `config` with nothing written. The test's stub `aws` now refuses such a filter as the AWS CLI does. No code of the control plane changes.
- The patch version 5.2.1 follows 5.2.0. The SSM document `hangar-api-deploy` carries `ops/onhost/api-deploy.sh` gzip-compressed and base64-encoded, because the script (90 KB) is larger than the 64 KiB that an SSM document holds. The host unpacks the script before it runs it, and `wp7-ssm-deploy-doc.zsh verify` checks the SHA-256 of the unpacked script against the repository's. No code of the control plane changes.
- What a person starts is a job. The API paths are `/v1/jobs` and `/v1/release-jobs`, with the same sub-paths, and a slot lists its jobs at `/v1/slots/{id}/jobs`. The fields are `job_id`, `jobs` and `release_job_id`, and the error codes start with `job_` and `release_job_`, with the same meanings and HTTP statuses. `hangar status` lists jobs, and the web app shows them on the Jobs page. The verifier prints "job certificate" and "Job". `hangar run` keeps its name. The worker routes, the certificate kind `release_run` and the data scope `run_inputs` keep their names. The request key capability `runs` is now `jobs`: the migration changes every existing key, and Hangar refuses a new key with `runs` (`key_request_invalid`).
- A job certificate of the hosted service carries two new facts: key 22, the places where the job's data was processed, staged, stored and recorded, and key 23, the Region policy with its source. A verifier of version 4 refuses a certificate with these keys, so the CLI 5.0.0 and the browser verifier of 5.0.0 go out in the same window as the control plane.
- A client of version 4 that calls an address that this release renamed gets `client_too_old` (410). The detail names the new address and the versions to update to: the CLI 5.0.0 and the Python package `gethangar` 2.0.0. The API refuses the Python package 1.x with `sdk_too_old` (426). The CLI 5.0.0 sends `Hangar-Client: cli/5.0.0` with every request. A CLI of version 4 sends no `Hangar-Client` header, so the API refuses its `POST /v1/instant`, `POST /v1/sandboxes` and `POST /v1/slots/hold` with `client_update_required` (426), before Hangar holds credit or charges. The detail names the CLI version to update to.
- This release is the major version 5.0.0, because the API contract changes and keeps no old name. The deploy has a planned outage: the control plane and the background jobs stop, migration `0052_job_rename.sql` renames the tables and columns, and the new release starts. A client of version 4 updates to the job names.
- The minor version 5.1.0 follows 5.0.0 (D-20 WP9). The deploy has no outage. Migration `0053_block_machines.sql` adds columns with defaults and rebuilds the slot exclusion and the live-session index per machine: it locks the tables `block`, `slot` and `instant_session` for the length of the migration, so the deploy runs when no session is live or starting. Migration `0054_serving_push.sql` is additive: it adds the platform tables `serving_push`, `serving_upload`, `serving_blob` and `platform_image_key`, with no organisation column. Migration `0055_immutable_tags.sql` is additive: it adds the table `image_version`, fills it from the existing images with a version tag, and adds the trigger that keeps each version on its digest. Every existing block has 1 machine, and every existing slot and session is on machine 0. The release adds:
  - **Live block adopt.** `hangar-ops block adopt RESERVATION [--dry-run]` adopts a capacity reservation bought in AWS while the API serves (`POST /v1/ops/blocks/adopt`, proposal kind `block_adopt`, one touch). The new codes are `reservation_unknown` (404), `reservation_not_adoptable` (409) and `reservation_changed` (409).
  - **Machines of a block.** A block holds 1 to 4 machines (`block.instance_count`). Each machine takes its own session, and the free check, the launch and the wipe work on each machine. `hangar status` and the start of a session show the first free machine, or the earliest time a machine is free.
  - **Why a session started late.** `GET /v1/ops/slots/{id}/start` and `hangar-ops check why-start SLOT` show the marks of Hangar beside the CloudTrail `RunInstances` and `TerminateInstances` times, with the lost block minutes.
  - **Serving images belong to the platform** (decisions 149 and 150). A serving image is a platform asset, as a machine image is: no organisation owns it, and TALOS Defence as a user of Hangar is an ordinary organisation. `hangar-ops image serve` gives a release builder a 60-minute grant to the platform repository `platform/quarantine/NAME`, and `hangar upload --image ARCHIVE --name NAME:VERSION --grant -` pushes the image with the grant. The bytes go to the platform's own store under `platform/serving/` in the images bucket, sealed under data keys that the platform image key wraps. `hangar-ops image promote-serve NAME@VERSION --digest sha256:...` promotes the image by digest (proposal kind `serve_promote`, one touch; the card names no organisation). Every record of a serving image is on the platform chain. The new routes are `POST /v1/ops/serving-images/push` and `GET /v1/ops/serving-images`. The new codes are `serving_push_not_found` (404), `serving_digest_mismatch` (409) and `serving_version_taken` (409). Each content path of the platform's store holds one blob or one manifest, written once under the data key its row names: two finishes of one digest store it once, and a blob whose digest is a stored manifest (or the reverse) gets `registry_upload_invalid` (400). An upload that fails removes its parts, and the block watch removes the parts of an upload left past its end. Each cell has its own platform store and key: a Trainium2 serving image is pushed to the Melbourne cell. The directory `images/trn2-serve` is frozen (decision 70).
  - **The platform's store and each organisation's registry are separate namespaces.** A job, image row, pull, tag, package version, sandbox session, release and public fetch of an organisation resolve only in the registry of that organisation. A platform serving name or digest reserves nothing there: an organisation pushes, tags, pulls and runs its own `trn2-serve` as before, and a job that names an image the organisation does not hold gets `image_unresolved` (422).
  - **Sign-up compares the stored ABN.** The check for a number already in use compares the ABN that each organisation stores, so an ACN sign-up of a company whose ABN a live organisation holds is held as `duplicate_identifier`.
  - **Migration 0055 locks `image` first** (SHARE ROW EXCLUSIVE), so no write falls between the backfill and the version trigger.
  - **The wipe gate across a block.** Migration `0056_wipe_gate.sql` is additive: it adds the table `instance_wipe_gate`. On a block of more than one machine, the capacity check reads the instances of the reservation by id (`DescribeInstances` filtered on the capacity reservation). An instance that is ending and that AWS still lists (shutting down) holds no launch on another machine, so each end on a Trainium2 block no longer stops every launch for more than ten minutes. An instance that has no wipe record and that AWS does not list holds every launch on the block, also when an instance that Hangar does not know is in the reservation. A launch writes its gate row as pending in the transaction that adds the instance, releases the capacity lock and answers. A task of its own then settles the row: it reads the list again while AWS lists the new instance as pending, until AWS lists it as running or gone, with a deadline of 600 seconds from the launch (`[blocks] gate_settle_ms`), below the worker's manifest wait of 900 seconds. In the last read, the gate counts an earlier instance with no wipe record only when AWS lists it as running. The gate holds every other one (listed as pending, shutting down or stopping, not listed, or wiped in between) and waits for its wipe record. When the new instance does not run before the deadline, the gate holds every one and raises the alert `block_launch_failed` (`wipe_gate_never_running`). The row records the measured time from the launch to running (`running_after_ms`), and why-start shows it. While the row is pending, the manifest waits. The block watch settles a row that is still pending after `[blocks] gate_sweep_seconds` (the settle's deadline plus two watch intervals; 720 seconds in production) from one read of the reservation by the same rule, and raises `block_launch_failed` (`wipe_gate_swept`) when the row holds an instance with no wipe record. The control plane refuses a configuration where either gate setting is 0, the settle's deadline reaches the worker's manifest wait, or the sweep age is below the settle's deadline plus two watch intervals. A mark that fails after the launch commits is logged and does not fail the launch. `POST /v1/ops/slots/{id}/launch` and the delivery event start the launch as a task of its own, so a caller that goes away does not stop a launch. A launch that ends early leaves no advisory lock on a pooled connection: the connection closes and the lock ends with it. A reboot in place is read by its machine. The gate's answer is written when the manifest first passes it, and the `mounted` movement names the same wipe record. The block watch starts a booked wipe again 25 minutes after its terminate request, or when AWS has ended the instance and no longer counts it. `POST /v1/ops/slots/{id}/launch` passes every launch check of a start and answers `pool_busy` while the block's capacity is not clear. `GET /v1/ops/slots/{id}/start` and `hangar-ops check why-start` list every instance of the block that is ending with no wipe record, and show the wipe gate row of each instance: pending, counted and held, and what settled it. The optional setting `[blocks] capacity_wait_ms` sets how long a start waits for the block's capacity lock (30 000 ms when absent).
  - **Version tags never move.** A version tag is three numbers separated by dots, such as `1.2.0`. Once a version names a digest, it names that digest for good, in every organisation (decision 71). A push completion or a tag that gives a version another digest gets the new code `tag_immutable` (409), also after the image was removed. Push or tag the image as the next version. The same digest again succeeds and leaves the image as it is. `latest` and the other tags move as before.

- The minor version 5.2.0 follows 5.1.0 (D-20 Section 13.19, WP10a and WP10b). It adds capabilities and breaks no contract. The deploy has no outage. Migrations `0057_upload_completion.sql`, `0058_platform_finding.sql`, `0059_ops_read_token.sql` and `0060_release_image_snapshots.sql` are additive, so the 5.1.0 release keeps serving beside them during the switch. The SSM document `hangar-api-deploy` is applied again in both Regions before the deploy: its host script gains the steps `config`, `web` and `web-rollback`, and the switch checks the digests of the approved deploy. The Python package `gethangar` is 2.1.0: it waits for an upload to complete. `hangar-ops` is the one operator tool: help for every command, six classes of authority, the queue and `do`, the deploy group, the operations screen, and the commands that replace the setup guide's blocks. The entries below give each part.
- Upload completion (D-20 Section 13.19, decisions 75 and 76). The minor version 5.2.0 adds a capability and breaks no contract. The deploy has no outage. Migration `0057_upload_completion.sql` is additive: it adds nullable columns and `hashed_through` (default `-1`) to `upload`, `stored_at` to `upload_chunk` (filled for the existing rows), a wider check on `upload.status` with the state `completing`, and the index `upload_completion_due`. The release adds:
  - **A chunk size for each upload.** A file that needs more than 9,000 chunks of `limits.chunk_bytes` gets larger chunks, in whole MiB, so the file stays under the S3 limit of 10,000 parts: 43 MiB for 400 GB, 59 MiB for 512 GiB. A file above 70.3 GiB was refused at the last step before. The body limit of the chunk upload alone follows the largest chunk that `limits.max_object_bytes` gives; every other request keeps the limit of 8 MiB and 64 KiB. The start refuses a `max_object_bytes` that needs a part above 5 GiB.
  - **The compose in parallel.** Hangar copies up to 32 parts at once inside S3, and removes the staging chunks with one call for each 1,000 keys.
  - **Completion as a job.** `POST /v1/uploads/{id}/complete` queues the check and the store. With `Prefer: respond-async` it answers `202` at once; the new route `GET /v1/uploads/{id}` gives the state, the chunks checked, and the object or the error. Without the header the request is held up to `limits.completion_hold_secs` (300 s) and answers `201`, or `idempotency_in_progress`: the CLI 5.x asks again with the same key, and the final answer for that key is the `201` object. A job holds a lease of 60 s on each upload, so a stopped process's upload is finished by another. The movement record names the person or the key that completed the upload.
  - **The digest as the chunks arrive.** Each chunk adds to the whole-file digest when the chunk arrives in order, so the completion has normally nothing left to read.
  - **A new error code**: `upload_completion_failed` (503, retryable). The chunks stay, and a new completion with a new key stores the file. `upload_incomplete` no longer keeps its idempotency key, so the same key completes after the missing chunks are sent.
  - **The Python package `gethangar` 2.1.0** waits for the completion. `GET /v1/versions` shows 2.1.0 as current; the oldest served stays 2.0.0.
  - **A rollback to 5.1.0** is a point-in-time restore of the database to a time before the migration, because 5.1.0 does not start on a database that holds `0057_upload_completion.sql`. Follow `docs/runbooks/upload_completion_rollback.md`. The Director raises `limits.max_object_bytes` to 512 GiB only after the completion tests pass (decision 76).

- The operator queue (D-20 WP10b B1 and B2). The minor version 5.2.0 adds the routes and breaks no contract. Migration `0058_platform_finding.sql` is additive: it adds the table `platform_finding`. During the two-unit minute, 5.1.0 still opens one flag for each GuardDuty event; the old flags stay open until a proposal closes them. The release adds:
  - **One item for each GuardDuty finding.** Hangar keeps one `platform_finding` row for each finding id, with the update count and the highest severity. A finding on the instance of a customer's slot opens one flag on that organisation, and the decision of the flag closes the finding. A finding with no organisation is one item of the incidents queue, which `queue_close` closes.
  - **An advisory closes with its pairs.** `advisory_close` clears the `advisory_received` alert and closes the rebuild item in the same transaction. The alert has the clock of its advisory (48 hours or 14 days). The advisory feeds match whole package names. `GET /v1/ops/advisories` lists the open advisories with their count; `?status=closed` and `?status=all` list the others.
  - **Queue items carry their facts.** Each item of `GET /v1/ops/queues` has `ref` (the last 8 hex characters of its id), `organisation`, `org_name`, `subject`, `subject_kind`, `job`, `evidence`, `detail` and `group`. `GET /v1/ops/items/{ref}` resolves a ref or an id to one open item or pending proposal. `GET /v1/ops/organisations?q=` lists organisations by a part of the legal name or the end of the id. `GET /v1/ops/get/{kind}/{id}` also reads a flag, an alert, an organisation and a platform finding.
  - **Expired is a state.** A proposal has `state`, which is `expired` for a pending proposal past its validity. `GET /v1/ops/proposals` lists only the proposals that can still be approved; `?status=expired` lists the expired ones. A vote or a touch on an expired proposal gets `proposal_expired` (409). An expired proposal no longer counts in the day's one-person sum of money.
  - **Subjects are checked when the proposal is made.** A flag, an automatic credit, a held sign-up, an incident and the item of a `queue_close` must exist, be open and belong to the organisation of the card. A refusal comes before any touch.
  - **A new error code**: `ops_subject_state` (409), the one operator refusal for an item that is missing, closed, ambiguous or of another kind. The detail carries `why`, `subject_kind` and `state`; an ambiguous ref also carries `candidates`.

- The operator reads, the read-only token and the operator's own keys (D-20 WP10a and WP10b), on the server. The minor version 5.2.0 adds routes and one proposal kind and breaks no contract. Migration `0059_ops_read_token.sql` is additive: it adds the table `ops_read_token`, which names the session that made each token, and the nullable column `credential` to `operator_login` and `operator_session`, which names the hardware key of the sign-in. Migration `0060_release_image_snapshots.sql` is additive: it adds the nullable column `snapshot_ids` to `release_image`. The release adds:
  - **Five operator reads.** `GET /v1/ops/pools`, `GET /v1/ops/operators`, `GET /v1/ops/organisations/{id}/objects` (newest first, at most 200), `GET /v1/ops/blocks/{id}/slots` and `GET /v1/ops/sanctions` answer this cell's rows with ids, states, counts and times, and name the cell. An unknown organisation or block is `ops_subject_state` with the state `not_found`.
  - **A read-only token for agents.** `POST /v1/ops/read-tokens` makes a token `hga_` of the class `read`, with an expiry of at most 30 days and 1 to 16 source ranges. `GET /v1/ops/read-tokens` lists the tokens without their values, and `POST /v1/ops/read-tokens/{id}/revoke` ends one. The session that makes a token must name a live hardware key; a session made before 5.2.0 names none and is refused with `session_ended`. `hangar-ops token create|list|revoke` runs these routes: create and revoke are the Director's own steps, refused in an agent shell, and create prints the value once, single-quoted, in the line `export HANGAR_READ_TOKEN='...'`, the only line on standard output, and refuses a `/0` range. The token reaches only the operator `GET` routes of its allow list, which answer counts and kinds: the new `GET /v1/ops/read/summary`, `/v1/ops/version`, `/v1/ops/deploys`, `/v1/ops/platform`, `/v1/ops/platform/{version}`, `/v1/ops/keys/status` and `/v1/ops/metrics`. Hangar refuses the token with `role_too_low` (403) on every other route and method, before the handler and the idempotency key. Hangar keeps the SHA-384 of the token only.
  - **Revoke a hardware key.** The proposal kind `credential_revoke` (role `security`, one approver; decision D1-R) revokes one of the proposer's own hardware keys, for `hangar-ops key revoke`. A touch of another live key of the same operator approves it: the touch page offers the other keys, and Hangar refuses a touch by the key being revoked with `hardware_key_required`. Hangar keeps the last live key: the proposal and the execution each refuse its revocation with `ops_subject_state` and the state `last_live_key`. Every later sign-in and touch with a revoked key is refused. The revocation also ends, in the same transaction, every session signed in with that key and every read token those sessions made. An operator with several live hardware keys opens a touch with any of them.
  - **The evidence keys of a job.** `GET /v1/ops/jobs/{id}/evidence`, for `hangar-ops evidence run`, gives the job's state, times and image digest, its slot and window, and each machine with its instance id, zone and times. The answer holds ids, digests and times only: no command, environment name, image reference, organisation or output.
  - **The image a block launches.** `POST /v1/ops/releases/{id}/images` takes the snapshot ids of the AMI (`snapshot_ids`, optional). `hangar-ops release register-image` and `hangar-ops image release` send them: they read the ids with `aws ec2 describe-images` and the build account's reader in the image's Region, and `register-image --snapshot-ids` gives them instead. `GET /v1/ops/reservations/{reservation}/image`, the source of `hangar-ops fsr on`, names for one block the promoted image release of its profile, its AMI in the block's Region and the AMI's snapshot ids.

- **The deploy commands reach every cell** (D-20 WP10a, decision 110, in 5.2.0). `hangar-ops deploy out --cells melbourne,sydney` deploys the control plane to each cell as its own recorded step (`api:melbourne`, then `api:sydney`), each with its own Deploy proposal and touch, and `deploy resume` deploys only the cells not done. `hangar-ops deploy web --cells melbourne,sydney` builds the web app once and installs it on each cell's API host, from that cell's deploy bucket and through that cell's SSM document. A refusal in one cell stops the cells after it and names each cell's state; no cell is rolled back by itself. `deploy rollback --web --cells` takes the web app back on the named cells. Without `--cells`, each command acts on Sydney. `hangar-ops tools install` checks each tool against the SHA-384 manifest of its own build and says that the digests come from that build.

- **The advisory check reads the web app's npm lock** (D-20 WP10, decision 132, in 5.2.0). The check stage `sca` queries OSV.dev for every package in `app/package-lock.json`, dev dependencies included, as it does for every crate in `Cargo.lock`. An advisory passes only with a VEX statement in `deps/vex.toml` for its exact `pkg:npm/` component.

- A GPU session takes nothing from credit until its job starts. A session whose machine fails before the job starts, at the attestation, the reset, the staging or the creation of the job's container, costs A$0.00. The live view of `hangar run` says that the job never started.

- A GPU session whose machine does not report ends as Failed, with the reason in one sentence. `hangar status`, `hangar logs` and the live view of `hangar run` show the reason. Hangar ends the machine, frees the slot at once and charges nothing when no job started. The worker reports a failed step with its name and the error text, and sends its step once a minute. `hangar stop` ends a session at once when no job has started.

- Packages and releases: A vendor describes its application in `hangar_package.json` and signs each release with its own release key. TALOS Defence accepts a release after its checks and a self-test whose signed return verifies. The release has channels, pins, widening approval and revocation, with the signed revocation list. At every job, Hangar measures a widening against the baseline release of the tenant. The signed return carries the baseline, so `hangar verify` recomputes the widening. A revocation stops queued and running work of the release, and stops new jobs.
- Jobs of a release on the CPU pool `hosted-cpu-syd`, for a tenant. Each job stays inside an envelope that TALOS Warrant compiles before the job. Events are newline-delimited JSON. The signed return shows the release, the envelope, the pool and every attempt. `hangar verify` checks the release in a return.
- Tenants, request keys signed with `Hangar-Signature`, the usage report for each tenant, and the Python package `gethangar` 2.0.0, which uses the standard library only.
- A request key with `jobs` can start, follow and stop a GPU now (`hangar run --gpu`, `POST /v1/instant`, `client.instant` in the Python package). The key can also read the jobs, bookings, logs, outputs and certificates of the organisation. The key spends credit only through the jobs it starts, inside the limits of the organisation. The key never books, tops up or approves. Jobs and sessions record `started_by`.
- Pools as an object, with registration, licence, local records contract and offline bundles for the customer-controlled form. Hangar offers the customer-controlled form from 2027.
- The error catalogue marks each code `retryable` or not.
- Hangar checks every image in the request that pushes or registers the image. The checks cover the platform (`linux/amd64` for GPU jobs, `linux/arm64` for the CPU pool), the media types, the size that a slot allows and the CUDA version. For a multi-platform index, Hangar checks each of its `linux/amd64` and `linux/arm64` images, and `preflight_detail.platforms` holds one result for each. `preflight` keeps its meaning: the result for the image that a GPU job takes. A GPU job needs its `linux/amd64` image to pass. A sandbox session needs its `linux/arm64` image to pass. `GET /v1/images` shows the result with the platform, the CUDA version and the entrypoint. When the image fails, `preflight_error` shows the reason and the next step. A tag takes the result of the digest that the tag names. The new codes are `preflight_media_type`, `preflight_image_missing` and `preflight_unreadable`. `hangar upload --image` lists each platform that the image passed for, with the pool that runs the platform.
- A sandbox session checks each image from your registry when the session opens. The image must pass for `linux/arm64` and fit the 8 GiB of the CPU pool. Hangar refuses the session with the code of the failed check, such as `preflight_too_large` or `preflight_architecture`, before an instance starts. The worker checks the platform and the size again when it stages the image. For a pushed image, the worker checks before it fetches a layer.
- The worker of a session renews the presigned URLs of its images when they near the end of their 15 minutes. The worker renews them once more after the store refuses a URL. Staging therefore runs for as long as the bytes take. An upload in parts renews its storage credential between parts, so one large output outlasts one credential.
- Sign-up checks the ABN on the Australian Business Register. Hangar refuses a cancelled ABN with `abn_cancelled` and an unknown ABN with `abn_not_found`. A person checks a name that is not like the names in the register, and a register that does not answer. The organisation records the entity name and the GST registration from the register. A tax invoice of A$1,000 or more uses the GST registration.
- The reference package `hangar_patch_agent` 1.0.0 in `examples/patch-agent`. The envelope of the package requests no destination.
