Ctrl K
ComunidadGuías…

docs/embedding-provisioning.mdx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
---
title: Embedded space provisioning
description: Broker trust, space credentials, durable host state, and current hosting limitations.
kind: Reference
audience: integrators
---

## One-space provisioning [#provisioning-a-space-one-space-reference-shape]

The one-space convenience API combines broker trust and one data account.
A host first creates that trust and renders configuration for a broker it controls:

```ts
import { createSpaceAuth, serverConfig } from "@cotal-ai/core";

const auth = await createSpaceAuth(space);
const config = serverConfig(auth, [auth], { storeDir, transport });
```

`space`, `storeDir`, and `transport` are host-selected values.
`transport` is required: `{ kind: "plaintext" }` or `{ kind: "tls-required", certFile, keyFile }`.
TLS policy applies to the broker listener, not to one space.
The host persists the trust material and starts nats-server with this configuration before opening provisioning connections.
`config` alone does not start a broker.
The following fragment assumes that `brokerUrl` points to that broker and `store` is the injected secret store:

```ts
import {
	ensureDefaultDeliveryClass,
	mintCreds,
	newIdentity,
	setupSpaceStreams,
} from "@cotal-ai/core";
import { deliveryCredsKey } from "@cotal-ai/workspace";

const provisionerCreds = await mintCreds(auth, newIdentity(), "provisioner");
await setupSpaceStreams({ servers: brokerUrl, space, creds: provisionerCreds });
await ensureDefaultDeliveryClass({
	servers: brokerUrl,
	space,
	creds: provisionerCreds,
	deliveryClass: "durable",
});
const deliveryCreds = await mintCreds(auth, newIdentity(), "delivery");
await store.put(deliveryCredsKey(space, { injected: true }), deliveryCreds);
```

[SPEC §4](../SPEC.md#4-delivery-modes) requires the creation-time delivery class to be discoverable on the wire.
A daemon-backed space records `durable`; it does not rely on a receiver's fallback policy.
The delivery credential key is per-space. The bare `delivery.creds` kind is not its store key.

For a user-auth space, pass the account created by `createCalloutAuth` in `extraAccounts` when calling `serverConfig`.
That callout account is separate from the space data account.
The host must provision auth material and its IdP pin and ledger before starting the [auth service](embedding-daemons.mdx#auth-service).

`maxFileStore` is an optional positive integer cap in bytes.
Other values throw.

### Multiple spaces on a broker

Broker trust and space accounts are separate authorities: `createBrokerAuth` mints the one
operator + system account a broker trusts, `createSpaceAccountAuth(broker, space)` signs each
tenant's data account under it, and `serverConfig(broker, spaces, opts)` renders them all into one
config. A host composition can therefore provision several spaces on one broker today. `cotal up`
renders that config from every tenant the root's auth directory holds, so booting one space keeps
the broker trusting its siblings, and it refuses to render at all while any account record is
unreadable. The rest of the CLI lifecycle is still broker-wide: `down`, `clean` and `backup` refuse
on a multi-space root rather than scoping to one tenant, and the per-space lifecycle is the
remaining multi-space operator layer. See
[Known gaps](embedding.mdx#hosted-composition-gaps).

#### Space accounts without a reload

With the default MEMORY resolver, the broker learns accounts only from its config, so every added
or removed space means rendering the whole config again and reloading the broker. A reload takes
longer with every account already on it, and it re-runs auth callout for live callout sessions.

A host can render the broker with `accountResolver: { kind: "full", directory, limit, intervalMs }`
instead. nats-server then keeps each account's signed claim in `directory`, and a space account is
added at run time:

```ts
import { createSpaceAccountAuth, pushAccountClaim, retireAccountClaim } from "@cotal-ai/core";

const space = await createSpaceAccountAuth(broker, "acme");
// `system` is a connection as a system-account user allowed to publish $SYS.REQ.ACCOUNT.*.CLAIMS.UPDATE.
await pushAccountClaim(system, { accountJwt: space.account.jwt, expectedServerIds: [system.info!.server_id], maxWaitMs: 5000 });

// Later, to stop the space: its live connections close and its users are refused.
const retired = await retireAccountClaim(broker, space.account.jwt, Math.floor(Date.now() / 1000));
await pushAccountClaim(system, { accountJwt: retired, expectedServerIds: [system.info!.server_id], maxWaitMs: 5000 });
```

- **A stock root records the choice.** `cotal up --account-resolver full` starts the broker on the
  full resolver at `<root>/.cotal/account-jwts` and records it in `.cotal/broker-policy.json`. A host
  that renders the same broker passes `brokerAccountResolver(root, readBrokerPolicy(root)?.accountResolver)`
  from `@cotal-ai/workspace`, so both renders name the same directory and limit. nats-server switches
  resolvers only at start, so a reload that changes the resolver is refused.
- **The directory is broker state.** It sits beside `store_dir`: give it the same ownership and
  backup coverage. A pushed claim survives a restart there even when the config does not list it.
- **Keep rendering the config.** List every active space in the preloads as before, so a fresh
  directory boots complete. nats-server keeps a newer pushed claim over an older preload.
- **Removal is a retired claim.** Dropping a space from the preloads does not remove it: its last
  pushed claim stays in the directory and loads again at the next start. Native deletion stays off. The retired claim keeps the account's
  JetStream data in place; deleting the space's streams is a separate step.
- **One writer per account.** The host orders the pushes for one account and owns the set of
  servers it expects to answer. `pushAccountClaim` refuses an answer other than "jwt updated", an
  unexpected server, or a missing answer.
- **Switching resolvers is a restart.** nats-server refuses to change resolvers on a reload. `cotal up`
  keeps using MEMORY.

## Issued-authority stores from before 0.111.0

A space whose `KV_cotal_issued_<space>` and `KV_cotal_accepted_<space>` were created before
0.111.0 is refused by every issuer and resolver that binds them. `cotal up
--recreate-issued-stores` fixes the spaces `cotal up` starts. For a space your own composition
provisions, stop its issuers and resolvers (auth service, callout replicas, managers) and run the
same step yourself:

```ts
import { connect } from "@nats-io/transport-node";
import { mintCreds, newIdentity, removeIssuedStoresBeforeRetention, setupSpaceStreams, standaloneConnectOpts } from "@cotal-ai/core";
const identity = newIdentity();
const inboxPrefix = `_INBOX_${identity.id}`;           // the one inbox the credential may receive on
const creds = await mintCreds(auth, identity, "issued-store-recreate"); // one-shot, 30 minutes
const nc = await connect({ servers: brokerUrl, ...standaloneConnectOpts({ creds, tls: false }), inboxPrefix });
try {
  // backupDir must not exist yet; it is created 0700 with a `nats stream backup` layout per stream
  await removeIssuedStoresBeforeRetention(nc, { space, backupDir, inboxPrefix });
} finally { await nc.drain(); }
await setupSpaceStreams({ servers: brokerUrl, space, creds: provisionerCreds }); // creates both again
```

The credential can read, snapshot and delete those two streams and nothing else. The step backs
up every store that predates bounded retention, checks each backup against its stream's message
count and last sequence, and deletes nothing unless every check passes. It throws
`IssuedStoreRecreateRefusal` before writing anything when neither store predates bounded
retention. The order, the refusals and what a rollback can do are in
[Upgrading](UPGRADING.md#issued-authority-stores-after-01110).

## Provisioning authority [#hazardous-provisioning-primitives]

`mintCreds`, the full `Profile`/`CredentialKind` matrix, `createSpaceAuth`, and `stripSpaceAuth` are
low-level operator primitives. Handle them as account-authority material:

- A holder of a `SpaceAuth` (or a `stripSpaceAuth` bundle, which **keeps** the data signing seed) is
  a fully-trusted tenant-account authority: it can mint `admin`, `provisioner`, and destructive
  profiles, not merely `supervisor`, and mint a DM-reading identity. `createSpaceAuth`'s full result
  holds operator, system, and account seeds in memory.
- Choose `profile` and `MintOpts` from **server-side constants**, never from tenant input. `MintOpts`
  can widen the bounded TTL defaults; cap it at your boundary. `CREDENTIAL_LIFETIMES` is a policy
  record, not an authorization boundary.
- Never log signer material or export it into env. Do not co-locate signer access with an untrusted
  connector/runtime process at the same OS uid (file permissions do not contain a same-uid reader;
  see the manager's isolation note). Segregate per tenant; rotate on compromise
  (`rotateDataAccountSigningKey`).

## Hosted composition gaps [#hosted-composition-gaps]

Three composition limits remain. A host must supply the missing isolation or integration,
or leave the affected capability unavailable.

Membership and live eviction already read their credentials through `SecretStore`.
`startMembership` reads the data-account writer and system-account observer there; eviction
reads the observer and evictor pair on each operation. Both use per-space keys and check
that the observer names the daemon's account. The feed derives that account from the
daemon's credential, without `membership.json`. The host still owns provisioning and
rotation. An absent observer leaves the feed traffic-only; missing eviction authority
refuses eviction. The membership reader can adopt a host-renewed observer with the same
issuer and scope at 75% of its lifetime.

1. **Supervisor signer isolation.** `ManagerOptions.secretStore` now injects the one `SecretStore` the
   manager reads/writes every secret through, including the composed `SpaceAuth`
   signer (the split trust records), its daemon-cred renewal, and its per-agent kinds. What remains is process
   isolation: the manager still decrypts the signer in-process at its uid, so untrusted agent children
   must run under a different uid/container/mount namespace or behind a future remote signer.
2. **Per-space lifecycle on a shared broker.** The trust layer is multi-space
   (`createBrokerAuth` + `createSpaceAccountAuth` + N-space `serverConfig`, persisted as
   `broker.json` + `account.<key>.json`) and `cotal up` renders the whole tenant list. The CLI
   still lacks tenant-account provisioning and per-space teardown/backup/restore on that
   broker. A host composition can add and retire space accounts without a reload through the full
   resolver ([above](#space-accounts-without-a-reload)). Its broker-wide lifecycle verbs refuse
   on a multi-space root, naming the tenants.
   The existing `cotal space add` registers a connection, and `space delete` removes a
   space's streams; neither supplies that tenant-account lifecycle.
3. **A non-Better-Auth production IdP.** The exchange core (`createIdpBridge`) is EdDSA-generic, but
   the stock provider and login client are Better-Auth-endpoint-shaped, `cotalAuthProvider`
   self-registers on import (colliding with a host-owned provider under `resolveAuthProvider`), and
   the login flow speaks Better Auth's device-code endpoints. A different IdP is a host-built auth
   composition on the low-level primitives, not a configuration change (see
   [the IdP callout contract](identity-and-auth.mdx#the-idp-callout-contract)).

## Hosted durability [#hosted-durability]

Space-durable **coordination** state (chat/DM/task history, live presence, membership runtime, the
durable ACL registry, leases) lives in **JetStream**, written by the delivery daemon and the
endpoints. It is broker-resident and needs no host-side durable path.

What is **not** in JetStream, and is hosting-critical, is trust and authorization state a host must
place and keep:

| state | class | where today | hosted injection |
|---|---|---|---|
| full `SpaceAuth` trust chain (`auth/broker.json` + `auth/account.<key>.json`, composed; a stripped signer bundle may instead be mounted at the legacy `auth/auth.json` key) | signing authority | `SecretStore` | `SecretStore` (manager + renewal) |
| auth kinds: callout account/creds/xkey, issuer private keys, owner-derivation secret, data-signer projection | signing/identity authority | four `SecretStore` kinds | `SecretStore` (auth-service) |
| auth plane instance identity (instance id + serve nkey seed) | restart identity | root `.cotal/space.<hex>/auth-instance.json` | `SecretStore` (`startAuthService`) |
| `delivery.creds` | standing scoped cred | `SecretStore` or `--creds` | `SecretStore` (delivery) |
| actor ledger, IdP pin | authorization + trust config | ambient `userAuthStateDir(findCotalRoot(), space)` | none (root-relative; not `store`/`COTAL_HOME`) |
| `membership-rw.creds` | standing scoped cred | `SecretStore` | `SecretStore` (delivery + manager renewal) |
| membership-observer / connection-evictor creds | scoped $SYS creds | per-space `SecretStore` keys | injected `SecretStore`; account identity is checked against the daemon credential, not `membership.json` |
| manager agent creds, actor tokens, sentinel creds | lifecycle authority | `SecretStore` | `SecretStore` (manager `secretStore`) |
| `~/.cotal/meshes/space.<key>.json` record (holds IdP trust pins/root pointers) | non-secret, integrity-critical | machine home | process-global `COTAL_HOME` only |
| auth-health, renewal records | non-secret diagnostics | workspace filesystem | `workspaceRoot` |

The `SpaceAuth` trust chain and the auth-service store kinds are **separate** identities/projections,
never parts of one document. `auth-service.json` (the live exchange capability) is ephemeral runtime
state, not durable, but is sensitive while the daemon runs. `@cotal-ai/workspace` is machine-local
operator tooling by design; personas, PID files, and the `current-mesh` pointer are truly local and
must **not** sit on a hosted durable path. Everything classed above as an authority is what a hosted
composition must provision and persist: signer-bearing server secrets now have `SecretStore` seams;
the remaining non-injectable rows are the explicit ambient `workspaceRoot`/cwd paths above.

## Related contracts

### Membership limitations [#membership-limitations]

Membership and live-eviction limitations remain relevant to a hosted composition. See [the detailed contract](embedding-provisioning.mdx#hosted-composition-gaps).

### Signer process isolation [#signer-process-isolation]

A host must isolate the process that holds signing authority. See [the detailed contract](embedding-provisioning.mdx#hosted-composition-gaps).

### Shared broker lifecycle [#shared-broker-lifecycle]

Trust for several spaces and broker-wide lifecycle operations have different boundaries. See [the detailed contract](embedding-provisioning.mdx#multiple-spaces-on-a-broker).

### Other identity providers [#other-identity-providers]

Production identity providers need the bridge and provider wiring described in the compatibility contract. See [the detailed contract](identity-service-auth.mdx#idp-compatibility).