For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt, and this page is available as Markdown at /reference/contracts/ssh_access/README.md.

SSH Access contracts

The SSH Access context answers the browser's #678 routes through the API. The routes and their JSON schemas are in http_api/; 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.

RouteOperationSuccess
GET /siteslistHpcSites200 the member's Sites as {id, name}
GET /sites/{siteId}/accessgetHpcAccess200 read model
GET /sites/{siteId}/keyslistHpcKeys200 key list
POST /sites/{siteId}/keyssubmitHpcKey201 key
POST /sites/{siteId}/keys/validatevalidateHpcKey200 preview
GET /sites/{siteId}/keys/{keyId}getHpcKey200 key
POST /sites/{siteId}/keys/{keyId}/revokerevokeHpcKey200 key
POST /sites/{siteId}/keys/{keyId}/resubmitresubmitHpcKey200 key
DELETE /sites/{siteId}/keys/{keyId}deleteHpcKey204
GET /sites/{siteId}/projectiongetHpcProjection200 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 ssh_access/requests.proto, on the shared request transport (contract version 1, context ssh_access, listed in owners.json). 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.

RequestReply
get_access, list_keys, get_keyThe read model, the keys or one key; a typed RequestError such as key_not_found
validate_keyThe 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_keyThe 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.

BoundaryValue
Routing keysrequest.ssh_access.query, request.ssh_access.command
Owner queuesref.ssh_access.queries, ref.ssh_access.commands (quorum, 15-second TTL, delivery limit 5)
Quarantineref.ssh_access.<queue>.dead through ref.ssh_access.delivery
Repliespharos.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

pharos/realtime/v1/ssh_access.proto 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 ../http/site-ssh-access.v1.yaml 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.

Page source

Source snapshot ยท Revision f928834c8.