# Service endpoint hosting

## Host operations

A second package-internal module builds service host operations on that writer: register
admission, decide (approve or refuse), revoke, a create-only name binding, and the operation and
proposal reads. It shares the
writer's sealed host, plane-claim hold check and fatal latch, and each invocation opens its own
custody host over a per-invocation effect registry. That host writes only the invocation's scope,
the exact keys discovery located before the effect registry opened, and the effect registry is
handed the same key set so a broker grant can match it. A step that needs a key outside the scope
holds as `scope-mismatch` before anything is issued; replaying the same request discovers what is
stored and scopes it. Each effect registry rides its own connection, whose credential may publish
only on that key set's subjects (subject confinement, not operation confinement; the engine itself
only creates and CASes those keys), never renews or reconnects, and expires at the invocation's original deadline
rounded up to the whole second. With agreeing clocks and timely scheduling the broker's
whole-second timer closes it within about 1999 ms after that deadline; that is granularity, not a
guaranteed close time, and no close proves an earlier write cannot still settle. Losing the
connection fences the invocation. Discovery's credential has no write at all. The module's records and their write order are in SPEC §13.1. Results name their operation once a binding exists or may exist: `pending`, `held` and
`refused` all carry `operationId`. A result can carry `debt`, the capacity releases its operation
still owed as far as that invocation observed. A result without `debt` does not mean nothing is
owed; the operation read is the authoritative view of the debt. A `held` result by itself implies
neither a binding nor an authorized granter: a tripped engine, or a granter without a current
interactive row, gets one `held` answer for a register, decide or revoke before any read of the
request's keys. `denied` means the invocation wrote nothing, and either the request can never bind
or the invocation's own read found no binding. That read is point-in-time; another invocation of the
same request already in flight can still bind it later. A revoke result reports `denyNew` when it
has evidence: an endpoint-gate read, which gives `unknown` when the read failed or found another
assignment's gate row, or an assignment that is already fenced, which gives `landed`. The early `held` answers of a tripped engine and of a
granter without a current interactive row carry no `denyNew`, and a result without `denyNew` says
nothing about deny-new. Completed results
say whether this invocation applied them or returned a stored record. A stored approval returns
unchanged, and its `approvedGateRevision` is null unless its own gate CAS was observed. The index
does not export this module; the typed request, result and view parsers in the service host contract
are public.

## Hosted composition

`openAuthAuthorityPlane` and `startAuthService` take `serviceEndpoints: { startup: { kind: "normal" } }`
to host these operations; it is off by default. `cotal up --user-auth --service-endpoints` sets it
on the stock auth service, and a lane of `auth-service --spaces` sets it when its manifest entry has
`"serviceEndpoints": true`. With it, the plane and the
hosted handle gain `serviceEndpointHost(request)`, which answers `decide`, `revoke`, `name-bind`, the
four caller-policy operations and the instance, proposal, operation and name reads in process.
`instance-read` returns the current `ServiceInstanceView` for the proved owner's selector, including
its approval digest and registration state. `instance-list` walks the native assignment subjects with
a cursor and returns only that owner's instances. Missing and foreign selectors both read absent.
A hosted context rejects it as
`unavailable` while it is not ready. The host authenticates no caller: the embedder must
authenticate the operator or owner granter it passes. The stock service does that on
`POST /service-endpoint-host`: the body is `{ idpToken, request }` with no granter, and the route
verifies the token and derives the granter. A decision, an `owner-request` revoke, the four
caller-policy operations and the reads run as the proved owner on either face; `name-bind`, any
other revoke and the admission operations are the operator's, served on the loopback face under the
exchange capability only, with the proved subject as the audit id (the admission operations have no
engine handler yet and answer `unsupported`). Mint requestIds unpredictably: they share one
space-wide namespace. `cotal service-endpoint` drives that route as the signed-in user
([docs/cli.mdx](/api-reference/cli/#service-endpoint)). Register admission is reachable only through [the service authority route](/docs/embedding-service-access/#authority-routes). `name-bind` needs an operator granter and `expectedRevision: 0`. It creates
the endpoint's name binding once, with your `claimReference`, and only the identical request recovers
it; nothing updates, transfers or releases a name. The host checks no domain control, so verify the
claim, or delegate a namespace you control, before you bind. The host runs over the plane's claim hold, data account and
actor ledger directory. A restore or missing startup refuses the plane open, which unwinds and
releases the claim. If that unwind cannot close an effect connection, it stops there: the claim
stays held, the plane's clients, scanners and cleanup timer stay live, and there is no handle to
retry. Only the process exiting ends them, after which the existing crash reclaim applies. Its discovery connection is an ordinary renewing read-only client: a broker loss
fails its reads until the stock client reconnects, and those calls answer `unavailable` with nothing
written. A renewal failure closes it permanently, and the host stays unavailable until the plane is
opened again. Both plane fatal paths trip the host, and either one trips it even while the
plane closes, without making the plane fatal. Plane close refuses new host calls from the
moment it is called, waits for every call that already entered, from entry through discovery and
effect open, closes discovery and then releases the claim. An effect connection that failed to
close stays owned past its call: close closes it again first, and rejects with the claim still held
while it stays open, so you can retry close. Until a retry succeeds or the process exits, discovery,
the administrative rail, both scanners and the remaining clients stay live, so no successor can
take over the claim.

## Multi-space admission

On a multi-space host the setting is per space. The manifest entry's `serviceEndpoints` is the only
place it is set, and `--service-endpoints` refuses beside `--spaces`
([server daemons](/api-reference/cli/#server-daemons)). Check a manifest you write with
`parseAuthHostManifest(text)` from `@cotal-ai/auth` before you rename it into place: it is the parser
the host applies. A callout replica cannot issue a service-endpoint connection, so it refuses a space
whose admission is open: a single-space replica refuses to start, and a callout host names the space
and retries it every 5 seconds while it answers for the others. The first open with service endpoints
opens the admission and nothing closes it, so keep every space that has ever run with service
endpoints off replica manifests, also after its entry drops the field. `admitsServiceEndpoints({ space,
server, secrets })` from `@cotal-ai/auth` tells you which: it resolves `true` for the spaces a
callout lane refuses for their admission, with the read the lane makes on the lane's own store grant,
and throws when the space's service keys are missing or its stores cannot be read. Nothing turns a
`true` answer back to `false`. To turn service endpoints on for a space a replica lists, remove it from
every replica manifest and wait for each `<manifest>.applied` first: a lane checks at open, so a lane
already open keeps answering and denies the service connects the broker hands it.
