--- title: Service endpoint hosting description: Owner approvals, native host operations, registration, and lifecycle custody. kind: Reference audience: integrators --- ## Host operations [#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 [#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](cli.mdx#service-endpoint)). Register admission is reachable only through [the service authority route](embedding-service-access.mdx#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 [#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](cli.mdx#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 `.applied` first: a lane checks at open, so a lane already open keeps answering and denies the service connects the broker hands it.