# DD — Metallo request transport (1D groundwork) [Common design](dd.md) · [Tasks and completion status](adr.md). Browser requests reach Metallo through the Go API ([decision 0046](../../../../../docs/decisions/0046-service-communication-and-composition.md)). The API validates the browser's JSON against its OpenAPI contract, adds the authenticated actor and sends a typed Protobuf request over RabbitMQ. This document covers Metallo's side of that path: receiving requests, dispatching them to owner handlers and returning their replies. The [runtime endpoints](runtime-endpoints.md) are the first owners to use it. ## Protocol The Go API's request client ([`client.go`](../../../../api/adaptors/messaging/client.go)) fixes the wire protocol: - Requests are published to `pharos.requests` with routing key `request..query` or `request..command`. Each carries `message_id = correlation_id = request_id`, `app_id = api`, `type = query|command` and `reply_to = reply..`. - Replies are published to `pharos.replies` on that route, with `type = reply` and `app_id = `. The API waits 12 seconds. - Commands carry `CommandMetadata`. Queries that check member access carry `QueryMetadata` with the signed-in member; an owner without it has no actor. [`adaptors/requests.py`](../../../adaptors/requests.py) consumes `ref..queries` and `ref..commands`: 1. Check the delivery properties, size, routing key, reply address and envelope: contract version, owner, request identity, request kind and, for commands, the command, aggregate and correlation identities, expected version and optional actor. A query's optional actor must be a canonical ID. Payloads must belong to the owner's own contract file. 2. Dead-letter invalid deliveries to `ref..delivery` → `.dead` and acknowledge them. Application code never sees them. 3. Dispatch to the handler registered for that query or command. Handlers are registered during composition; `require_complete` refuses to start with an undeclared or missing handler. 4. Business rejections become typed replies: an outcome rejection for commands and a typed error for queries. A rejection that explains each invalid field, such as every invalid action parameter, keeps those messages in `Rejection.fields`. Other failures reply `temporarily_unavailable` without exposing details. 5. Acknowledge only after the reply is confirmed or committed. ## Internal queries Other Metallo contexts ask an owner's internal queries; the API never does. The [accepted-work](accepted-work.md) pages are the first (#765, #766). An owner declares them in its contract as `InternalQuery`, under the request kind `internal_query`: - Requests go to `pharos.requests` with routing key `request..internal_query` and are consumed from `ref..internal_queries`, with the same acknowledgement as API queries. They carry no actor. A refused request is dead-lettered like an API query, but one that expires unanswered is dropped: its caller has stopped waiting and asks again later, so a paused owner does not fill `.dead`. - A Metallo context enrols for an owner by naming itself in that owner's `callers` in `owners.json`. Provisioning gives each enrolled context one user, `pharos__queries`. It may publish only `request..internal_query` to the owners it enrolled for, and declare and read only its own exclusive reply queues, `ref._queries..replies.`, bound to `reply.._queries.`. The owner's requests user may reply there. The API's role covers no internal query, and an owner with only internal queries gets no API bindings. - The owner answers only when the broker-validated `user_id` is `pharos_`, `app_id` names a `_queries` family, `type` is `internal_query` and `reply_to` is a route of that family. Anything else is dead-lettered. - [`adaptors/internal_queries.py`](../../../adaptors/internal_queries.py) is the caller's transport. A refusal, a reply that does not arrive within 20 seconds or a broker failure leaves the caller to ask again. ## Durable command replies A command's reply commits in the same transaction as the outcome it reports. The receiving worker sets a reply intent for the request; the shared `Commands` transaction appends the encoded reply and its API connection route to `pharos.command_replies` in the receiving context's database, including receipt replays and idempotency conflicts. Commands in other contexts' databases, such as owner lookups, never carry it. The worker then acknowledges without publishing, and [`command_replies.relay`](../../../adaptors/command_replies.py) publishes the stored bytes with confirms. A crash between commit and publication therefore cannot lose the reply. Replies are not published after 30 seconds; the API stops waiting after 12. An aggregate whose saved state records a refusal, such as a refused launch, adds the reason to its committed reply. Rejections before any transaction, such as denied access, and query replies are published directly. The reply tables are Metallo's first service-local delivery extension, [`0003_command_replies.sql`](../../../contracts/persistence/0003_command_replies.sql). Administrator migration applies it to every Metallo context database after the shared baseline. Runtime connections verify its checksum at startup. ## Owners and hosting Each owner's registry consumes only the request kinds its contract declares. [`contracts/requests/owners.json`](../../../contracts/requests/owners.json) lists the owners and kinds; Metallo's broker provisioning declares their queues and owner users from it, and the API's broker role covers their queries and commands. A unit test keeps the list equal to the contracts. The API compiles the owner contracts with queries or commands into its own module. Execution Core and Workload Portfolio register their handlers in the dedicated [`metallo-requests` process](runtime-endpoints.md#hosting), with Execution Core's reply relay; Execution Core's internal queries are answered there too. Pipeline Coordination answers its internal queries in the `pipeline-coordinator` process with `PIPELINE_COORDINATION_REQUESTS_BROKER_URL`. The main Metallo HTTP process registers none. ## Verification Completed on 21 October 2026 with a test-only `metallo_fixture` owner: - **25 unit cases** cover owner-scoped reply routes, contract-bound registration, missing handlers, envelope validation, dispatch, typed rejections, hidden failures, dead-lettering and the committed-reply branch. - `pnpm test:integration` passed with the request cases on **real RabbitMQ and PostgreSQL**: two concurrent callers reconnecting under new API connection routes, command replay and actor conflicts with every reply committed and relayed, a reply committed while the relay was stopped and delivered after it started, and a malformed request dead-lettered without side effects. It ran **287 Metallo cases**. The existing persistence case now uses Metallo's own fixture database, because Beto's fixture carries Beto's version 3 delivery schema. - `pnpm verify` passed with **1,872 Metallo tests** and strict Mypy; regenerating the contracts reproduced the committed outputs. - `make metallo-identity-test TARGET=dev-NAME SSH=your-dev-box` passed **1,589 cases** on the dev box. Every Metallo context database received the reply extension through the administrator bootstrap, and startup checksum checks accepted it.