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.
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.
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.
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.