# Embedded space provisioning

## One-space provisioning

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](/docs/spec/#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](/docs/embedding-daemons/#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](/docs/embedding/#hosted-composition-gaps).

## 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](/docs/upgrading/#issued-authority-stores-after-01110).

## Provisioning authority

`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

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. 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](/docs/identity-and-auth/#the-idp-callout-contract)).

## 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 and live-eviction limitations remain relevant to a hosted composition. See [the detailed contract](/docs/embedding-provisioning/#hosted-composition-gaps).

### Signer process isolation

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

### Shared broker lifecycle

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

### Other identity providers

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