> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

# SSH Access contracts

The [SSH Access context](/reference/services/metallo/contexts/ssh_access/README.md)
answers the browser's #678 routes through the API. The routes and their JSON
schemas are in <a href="/source/contracts/ssh_access/http_api/ssh_access.paths.json.txt">`http_api/`</a>; `pnpm generate:contracts`
bundles them into the API's OpenAPI document under `/api/v1/ssh-access/sites`.

## HTTP

Every route but the Site list names its Site; one Site per process, and any other
Site answers the same public `404 not_found`.

| Route                                        | Operation          | Success                                  |
| -------------------------------------------- | ------------------ | ---------------------------------------- |
| `GET /sites`                                 | `listHpcSites`     | `200` the member's Sites as `{id, name}` |
| `GET /sites/{siteId}/access`                 | `getHpcAccess`     | `200` read model                         |
| `GET /sites/{siteId}/keys`                   | `listHpcKeys`      | `200` key list                           |
| `POST /sites/{siteId}/keys`                  | `submitHpcKey`     | `201` key                                |
| `POST /sites/{siteId}/keys/validate`         | `validateHpcKey`   | `200` preview                            |
| `GET /sites/{siteId}/keys/{keyId}`           | `getHpcKey`        | `200` key                                |
| `POST /sites/{siteId}/keys/{keyId}/revoke`   | `revokeHpcKey`     | `200` key                                |
| `POST /sites/{siteId}/keys/{keyId}/resubmit` | `resubmitHpcKey`   | `200` key                                |
| `DELETE /sites/{siteId}/keys/{keyId}`        | `deleteHpcKey`     | `204`                                    |
| `GET /sites/{siteId}/projection`             | `getHpcProjection` | `200` `application/x-protobuf` snapshot  |

`siteId` matches `[a-z][a-z0-9-]{0,62}`: the identifier the context configures,
never free text. `GET /sites` lists only Sites where the actor's member has an
account, so the page never builds a Site id itself.

Commands require a UUID `Idempotency-Key`, repeated unchanged on retry; each attempt
uses a fresh request ID. They take no `If-Match`. Writes require a configured
`Origin`, and submission and validation require `application/json` (`415
unsupported_media_type`). The actor comes from the session or CLI key, never from
the request. Reads return `Cache-Control: no-store`.

Owner errors use #678's `{ "error": { "code", "message", "params"? } }`. A gated
`403` lists every failing gate as `{ "errors": [{ "code", "message" }] }` in #678
order. `key_duplicate` carries `params.state` and `params.keyId`. Status codes:
`400` `invalid_id`, `invalid_request`; `403` gates, `member_not_found`; `404`
`key_not_found`; `409` `key_duplicate`, `key_limit_reached`, `key_state_conflict`,
`submission_in_progress`, `idempotency_conflict`; `422` `key_private`,
`key_malformed`, `key_type_unsupported`, `key_too_weak`, `key_name_invalid`; `503`
`temporarily_unavailable`. API-level `401 unauthorised`, `403 origin_denied` and
session `503` keep the API's `{ "code" }` shape. #678 asks for `500 internal_error`
and `403 cross_origin`; the API's existing `503` and `origin_denied` apply instead.
\#678's OpenAPI attachment is unavailable, so the key check codes are this
contract's own until the browser contract confirms them.

A Site this process does not serve answers `404 not_found`, the same body for a
foreign Site, an unknown Site and a route without one: the caller learns nothing
about where other keys live. The context keeps `site_not_found` as an internal,
auditable code. A `keyId` that belongs to another Site stays `key_not_found`.

## Messaging

The request contract is Metallo's
<a href="/source/services/metallo/contracts/requests/proto/pharos/requests/v1/contexts/ssh_access/requests.proto.txt">`ssh_access/requests.proto`</a>,
on the shared <a href="/source/services/metallo/docs/epics/instantiate-and-run/request-transport.md.txt">request transport</a>
(contract version 1, context `ssh_access`, listed in
<a href="/source/services/metallo/contracts/requests/owners.json.txt">`owners.json`</a>). Queries
carry the actor in `QueryMetadata`. Commands carry `CommandMetadata` with the
`Idempotency-Key` as `command_id`, the actor as `actor_id` and `expected_version`
0, since #678 sends no version. The command's aggregate is the key: a new key is
named by its command identity, other commands address the key in the path.
Every member request also carries `site_id`: the API is its only producer, from
the path the session was authorised against, and the process rejects anything
but the Site it serves, so the field is an assertion rather than an authority.
`list_sites` carries none and answers the Sites where the actor's member has an
account.

| Request                                                  | Reply                                                                                                                             |
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `get_access`, `list_keys`, `get_key`                     | The read model, the keys or one key; a typed `RequestError` such as `key_not_found`                                               |
| `validate_key`                                           | The preview, or a `Failure` naming every failing gate or the held duplicate (`state`, `key_id`); other checks as a `RequestError` |
| `submit_key`, `revoke_key`, `resubmit_key`, `delete_key` | The recorded `Outcome`; a gated rejection lists its gates in `Rejection.fields`                                                   |

Command replies commit with the outcome and the reply relay publishes them. The
API reads a changed key back with `get_key`, and names the held key of a refused
duplicate through `validate_key`, which runs the same check.

| Boundary     | Value                                                                                         |
| ------------ | --------------------------------------------------------------------------------------------- |
| Routing keys | `request.ssh_access.query`, `request.ssh_access.command`                                      |
| Owner queues | `ref.ssh_access.queries`, `ref.ssh_access.commands` (quorum, 15-second TTL, delivery limit 5) |
| Quarantine   | `ref.ssh_access.<queue>.dead` through `ref.ssh_access.delivery`                               |
| Replies      | `pharos.replies`, `reply.ssh_access.<API connection UUID>`                                    |

`scripts/metallo_broker.py` provisions the queues and the `pharos_ssh_access` role
from `owners.json`. The role reads only its two queues and publishes only replies
and quarantine transfers. The generated API role may publish both routing keys and
consume its own `ref.api.ssh_access.replies.<UUID>` queues.

## Realtime

<a href="/source/contracts/realtime/proto/pharos/realtime/v1/ssh_access.proto.txt">`pharos/realtime/v1/ssh_access.proto`</a>
defines `MemberSshAccessProjection`, the bytes of a `Publication` with context
`ssh_access` and kind `member_ssh_access`. One publication is addressed to each
authorised actor's principal subject. `GET /projection` returns a
`pharos.realtime.v1.ProjectionSnapshot` of the same bytes with each root's
revision; the API forwards it unchanged. A member without keys has an empty
snapshot.

## Site protocol

The access agent's routes are declared in
<a href="/source/contracts/http/site-ssh-access.v1.yaml.txt">`../http/site-ssh-access.v1.yaml`</a> and served by
the Metallo Site API at `site.<domain>/v1/site/ssh-access/`. The access agent is
configured with the Site API origin only and appends the route itself; the Site API
does not share browser API credentials or backend authority. Native requests do not
enter a browser OIDC callback flow.

<details class="page-source"><summary>Page source</summary>

<a href="/source/contracts/ssh_access/README.md.txt">Source snapshot</a> · Revision `f928834c8`.

</details>
