@cotal-ai/pi 0.40.0 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +15 -8
- package/dist/standalone.js +138 -54
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -16316,7 +16316,7 @@ import { isConcreteChannel as isConcreteChannel3, channelInAllow as channelInAll
|
|
|
16316
16316
|
|
|
16317
16317
|
// ../connector-core/dist/docs-bundle.generated.js
|
|
16318
16318
|
var DOCS_BUNDLE = {
|
|
16319
|
-
"version": "0.
|
|
16319
|
+
"version": "0.41.0",
|
|
16320
16320
|
"generatedFrom": "docs/*.md + SPEC.md + spec/cotal-lang.md + spec/cotal.schema.json",
|
|
16321
16321
|
"pages": [
|
|
16322
16322
|
{
|
|
@@ -16359,14 +16359,14 @@ var DOCS_BUNDLE = {
|
|
|
16359
16359
|
"title": "Identity",
|
|
16360
16360
|
"kind": "Concept (informative)",
|
|
16361
16361
|
"summary": "Who can do what on a mesh, and how it is enforced.",
|
|
16362
|
-
"body": "# Identity\n\n> **Concept** (informative) \xB7 **For:** operators and implementers \xB7 **Normative:** [SPEC \xA72](../SPEC.md#2-identity), [\xA79](../SPEC.md#9-nats--jetstream-security-and-authorization), [\xA710](../SPEC.md#10-connection-and-onboarding), [Appendix B](../SPEC.md#appendix-b-profile-acls)\n\nWho can do what on a mesh, and how it is enforced. The design goal: the mesh is a **real\nboundary against untrusted peers in a shared space**; an agent can only speak as itself\nand only where its declared permissions allow, enforced by the broker, not by agent\ngoodwill. What that boundary does and does not protect is the\n[security model](security.md); the exact ACLs are\n[SPEC Appendix B](../SPEC.md#appendix-b-profile-acls).\n\n## On by default\n\n`cotal up` provisions a JWT-authed space; `cotal up --open` runs an unauthenticated dev\nmesh instead. Both bind loopback by default. `--host 0.0.0.0` widens the bind\nindependently, so \"network-reachable\" never silently means \"unauthenticated\". Open mode\nis for quick local experiments and sits outside every security claim\n([SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization)).\n\n## Shared identity\n\nAn agent's wire identity is a **principal**: an `owner.actor` pair, where the owner is\nthe account (a human, or an organization) the agent acts on behalf of, and the actor is\nthe agent's own handle under that owner ([SPEC \xA72](../SPEC.md#2-identity)). The same pair\nis the card id, the sender tokens in every subject it publishes, the presence key, and\nits durable-consumer names. On an open dev mesh the owner is the literal `local`; on a\nper-user-auth mesh it is a derived token (`u_` plus 26 characters, so no PII rides the\nwire). The connection still authenticates with an **nkey**, generated locally (the signer\nonly ever sees the public half), but the nkey is the transport credential, not the\nidentity: it scopes only the per-connection reply inbox.\n\n**The sender is encoded in the subject.** Every publish carries the sender's owner and\nactor in positions the broker's permissions pin to that connection, so an agent *cannot*\nemit as anyone else: not as another owner, and not as a sibling actor under its own\nowner. Receivers verify the payload's `from.id` against the subject sender and reject\nmismatches; sender authenticity is broker-enforced end to end\n([SPEC \xA73](../SPEC.md#3-subject-layout), [\xA75](../SPEC.md#5-envelopes)).\n\n**Account = space, user = agent.** A space is one NATS account, a server-enforced\nisolation boundary. An operator signs the account; an account **signing key** mints\nper-agent user JWTs.\n\n## Provisioner\n\nThe **provisioner** is whoever holds the account signing key. It mints profile-scoped\ncredentials and pre-creates the durables agents may only *bind* (their DM inbox, their\nrole's task queue). The manager hosts it today, but nothing is manager-special about it;\nprivilege attaches to the signer, and a space can run without a manager.\n`cotal mint <name> --profile <agent|observer|admin>` is the out-of-band path; spawn calls\nthe same library ([CLI](cli.md)). Minting static creds is a **static-auth** surface: a\nper-user-auth space refuses it, because agents there join under a logged-in user, never\nvia a handed-out file (see *Per-user auth* below).\n\n## Profiles\n\nEvery credential is a profile: an explicit allow-list built from the same\nsubject/stream/durable builders as the wire layout, so ACLs cannot drift from it. The\nnormative shapes are [SPEC Appendix B](../SPEC.md#appendix-b-profile-acls); in brief:\n\n| Profile | Is |\n|---|---|\n| **agent** | The ordinary peer: publishes as itself to its declared channels, reads within its read ACL + its own DM/task inboxes. Its read-only presence and channel-registry watches may create, inspect, and delete only their own client-managed ordered consumers; those cleanup grants cannot delete KV records or streams. |\n| **observer** | Read-only chat + presence; DMs invisible. What `cotal console` runs. |\n| **admin** | Elevated *read-only* god-view: sees DMs and anycast live, still writes nothing. A deliberate opt-in (`cotal web`). |\n| operator-side | Narrow single-purpose creds for the machinery (supervising, provisioning, teardown, delivery); the reference implementation splits these so no one connection can read every DM *and* delete every stream ([security model](security.md)). |\n\n**An agent's channel scope is three verbs**: `subscribe` (reads at boot),\n`allowSubscribe` (read ACL), `allowPublish` (post ACL, default-deny), declared in its\n[agent file](agent-files.md) or [manifest](manifest.md), minted into its cred. One card\nwith the recipes: [Channels & permissions](channels-and-permissions.md).\n\n**DM confidentiality** holds against peers by construction: deliveries ride per-identity\ninbox prefixes, and the DM/task consumers are provisioner-pre-created and bind-only, so an\nagent cannot create a consumer filtered to someone else's inbox\n([SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization) items 1\u20135).\n\n## Spawn capability\n\nControl-plane power is a **declared capability**, not a default. An agent file carrying\n`capabilities: [spawn]` gets the privileged control subject minted into its cred: spawn,\nplus stop/despawn of its *own* children, plus persona definition. Without it, an agent can\nonly self-despawn. The tool surface mirrors the grant: `cotal_spawn` / `cotal_persona` / `cotal_personas` are\ninjected only where they can actually succeed ([agent files](agent-files.md)). Destructive\noperator ops (history purge, cross-agent stop) live on a third tier no agent credential\nreaches. Persona redefinition separates content from policy; the write path takes only\n`model`/`persona`, so a peer cannot grant itself a capability by redefining a file.\n\n## Per-user authentication\n\n`cotal up --user-auth --idp <auth base URL>` (or manifest `broker.auth: \"user\"`) puts a\n**human identity plane** above the per-agent one: people sign in to an external IdP once,\nand every connect is authorized live against the operator's **actor ledger**. No creds\nfiles to hand out, and revoking a grant actually bites.\n\n**The flow.** Each person runs `cotal login --idp <url>` once per machine. After that,\nany command works: cached IdP session \u2192 fresh IdP proof per connect (so IdP-side\nrevocation bites here too) \u2192 the configured exchange turns it into a short-lived Cotal bearer \u2192\nthe broker's **auth callout** checks the bearer and the ledger at connect time and mints\na scoped credential on the spot. Every bearer also names a **root credential** row in the\nspace's credential ledger, proved live at each connect, so revoking that one credential\nbites at the very next connect. The operator grants access with\n`cotal actor grant <actor> --sub <their id>`; a bare grant is the full envelope (all\nchannels, may spawn), and `--allow-subscribe` / `--allow-publish` / `--scope` narrow it.\nNo ledger row, no access; there is no allow-by-default.\n\n**One auth service per space** hosts both halves: the NATS auth callout and the token\nexchange. Its default HTTP listener remains loopback-only and requires the per-start capability\nstored in the owner-only `auth-service.json` file. An operator may add a second listener with\n`cotal up --user-auth ... --exchange-public-port <port> --exchange-public-url https://auth.example`.\nThat listener still binds `127.0.0.1`; put a reverse proxy in front of it and terminate TLS there.\nIn-process TLS is deliberately not another deployment mode: it would duplicate certificate renewal\nand fork proxy-based deployments.\n\nThe public listener has a closed surface: `GET /health`, `GET /jwks`, `POST /exchange`, and\n`GET /.well-known/cotal-mesh`; every other path is 404. It does **not** require the loopback\ncapability. That capability proves same-uid access to a 0600 local file and has no remote meaning;\non the public face the credential is the proof. A human presents an EdDSA IdP JWT checked against\nthe pinned JWKS, issuer, and audience. An agent presents its spawn-time actor token, whose hash must\nmatch a fresh managed-ledger row. Elevated `view` exchanges stay loopback-only.\n\nThe well-known response contains the IdP pins and the actual deny-all sentinel credential remote\nagents need before the bearer-driven auth callout. The pins ride a `userAuth` arm that names the\nauth provider, and that name is the same one the local arm registers under. A document naming a\ndifferent provider than the one serving it would register an entry nothing can resolve, so both read\none constant. Treat it as bootstrap material: the sentinel cannot publish or subscribe, but\nconsumers must still take the bundle only from the intended HTTPS origin and must verify TLS.\n`--exchange-trusted-proxy` opts into peer attribution by the **last** `X-Forwarded-For` hop; use it\nonly when the listener is reachable solely through a proxy you control.\nWithout it, forwarded headers are ignored and the socket address is the peer key. Public failure\nbuckets are per-source and separate from loopback exchange budgets. The in-process LRU retains at\nmost 1024 peer buckets: that bounds memory and isolates ordinary sources, but an attacker cycling\nmore than 1024 trusted-proxy last hops can evict earlier 429 state. It is not a mint bypass; a valid\ncredential is still required, so use upstream reverse-proxy rate limiting when that throttle-escape\nmatters to the deployment.\n\nThe service starts with the broker, is torn down by `cotal down`, and holds the\ndata-account signing key for the callout (a running manager is the other standing holder, for\nthe creds it mints); the operator seed never enters it. It also owns the space's two authority\nstores (lifecycle records and the credential ledger), provisions them at boot, and refuses\nconnects it cannot credential-check against them; there is no fallback path. If it\ndies while the broker lives, re-running `cotal up` heals it, and a boot whose auth\nservice never became ready exits non-zero, so automation never reads a dead identity\nplane as success. Changing any public-listener flag requires `cotal down` followed by `cotal up`\nwith the new values; a refresh adopts an already-running auth service rather than silently replacing\nits listener policy. \"One per space\" is enforced, not assumed (SPEC \xA713.13): at boot the\nservice takes a broker-backed ownership claim, so a second same-space auth process refuses\nwith instructions instead of silently splitting the plane, and a crashed one's claim is\nreclaimed only once the broker confirms its connections are gone. That verdict is trusted only\non a standalone broker (a clustered one refuses the reclaim, since a partitioned member\ncould still hold them). If the claim's connections die mid-run, the service downs itself\nloudly instead of serving from a half-dead plane.\n\n**Your agents are yours.** `cotal spawn` on a user mesh grants a managed actor under the\n*spawning operator's* owner and launches the agent with a bearer command instead of a\ncreds file. The agent exchanges its spawn-time secret for short bearers (five minutes or\nless) and refreshes ahead of each expiry. Rows are runtime grants: every start rotates\nthe secret, every stop or despawn revokes the row, so a non-running agent holds no\nstanding authority. Manifest deploys (`up -f`) stamp the logged-in owner into the launch,\nso those agents are yours too.\n\n**Despawn tears the lifecycle down, then frees the name.** When you despawn an agent, the manager\ndrives the *full* teardown of that lifecycle: it shreds the local credential files, revokes the\nagent's standing mint authority (its ledger row, so a copied token can no longer mint a fresh\ncredential), deletes its broker footprint (the lifecycle-keyed durables + read-ACL row), and asks\nthe auth service to *retire* the lifecycle (settle in-flight work, evict the departed credentials,\nrecord it retired). The name is held *reserved pending retirement* until **all** of that completes,\nthe broker-footprint cleanup, the standing-authority revoke, **and** the lifecycle retirement, not the\nretirement alone, so a same-name respawn in the gap is refused with\na plain reason and a retry hint rather than quietly handing the alias to a new agent while\nthe old lifecycle's teardown is still running. Only once the broker footprint is gone, the standing\nauthority is revoked, and the retirement is confirmed does the name free, and `cotal spawn <same-name>`\ngives you a fresh agent cleanly. This is what makes reusing an agent's name safe: the old lifecycle is\nfully torn down before the new one takes the alias. If the auth service is unreachable or the\nstanding-authority revoke fails, the despawn still stops the agent and *holds* the name. **A\nsame-name `cotal spawn` re-drives the whole teardown** and finishes it. Retrying the despawn has no\neffect because the agent is already stopped. The operator copy tells you to recover the stack\n(`cotal supervise`) rather than reusing the name over an unretired predecessor.\n\n**A crash mid-retirement resumes at the next boot.** The retirement's last two steps (recording the\nissuance gate terminal, then the lifecycle head terminal) are separate durable writes, and a crash\nbetween them leaves the gate retired while the head is still `retiring`: an alias that can neither\nmint nor be replaced. The auth service's boot crash-resume decides what it owes across *both*\nobjects (the gate *and* the alias head), so the next boot finishes that tail from the durable\noperation intent: nothing is re-revoked or re-drained, and a completed retirement (its head terminal\nlanded, or a successor already took the alias) is left skipped. A retry of the despawn converges on\nthe same recovery.\n\n**Delegation only narrows (the envelope rule).** A user's grant is their envelope:\neverything under their owner (their CLI, every agent they spawn, every agent those\nspawn) stays within its channel lists and its capability scope. Handing a role to a\nspawned agent needs the matching `role:<r>` capability in the spawner's scope. The whole\ndelegation chain is checked, not just the last link, and re-checked at every bearer\nexchange, so narrowing a user's grant reaches their agents within minutes, and revoking\nthe user revokes everything under them, grandchildren included. A spawn beyond the\nenvelope is refused with the exact widening re-grant to ask the operator for.\n\n**Control ops ride your own login**, gated by ledger scope. `spawn` covers launching,\n`ps`, and stop/attach of the agents under **your own owner**: the owner is the\nadministrative boundary of its own subtree, so you (and your agents) manage what you own\nwithout any extra grant. `admin` is the explicit opt-in for touching **other owners'**\nagents; it is never part of a default grant and never accepted from a manifest.\n\n**Elevated operator surfaces ride the same login** through a short-lived *view*: the\nexchange stamps a server-authored view claim into the bearer, and the callout mints that\nconnection as the matching non-agent profile instead of `agent`. `cotal web` and\n`cotal console` ask for the read-only admin view, `clean history` for the purger,\n`channels set/default` for the channel-writer (all gated on ledger scope `admin`);\n`up -f` deploys over the deployer view, gated on `spawn`, because deploying your own team\nis spawn-grade (the manager still refuses a manifest claiming another owner). Views exist\nonly on a signed-in human exchange (an agent's managed exchange never mints one), are\nauthorized against the fresh ledger row at every connect, and expire with the bearer, so\nnarrowing or revoking a grant bites within minutes here too.\n\n### Remote manager authority\n\nA registered user remains an ordinary `agent` bearer by default. Running a detached manager\non a remote user-auth mesh needs the closed server-authored **`manager-service`** view, which\nis distinct from every general-purpose profile. The operator grants it only by adding\n`supervise` to that user's actor-ledger scope. `supervise` is deliberately distinct from\n`spawn` and `admin`: spawn controls your agents, admin permits the separate cross-owner\noperations, and neither grants persistent manager registration authority.\n\nOnly a signed-in human may request this view from the loopback/operator exchange. The public\nexchange and every managed-agent secret exchange refuse it. At exchange and each connection,\nthe auth service re-reads the actor row; revoking or removing `supervise` therefore denies the\nnext view exchange and connection. A grant must carry the whole requested row just like every\nother actor update, so re-grant its channel envelope, role, and all wanted scope tokens, not\nonly `supervise`.\n\nThe service is one opaque manager instance for the user's derived owner and a fixed\nserver-selected manager actor. Its authority is limited to that instance's manager\nregistration, contracts, status, endpoint rails, gate and credential family; it cannot read or\nwrite another owner or instance. It never exposes a signer, static provisioner credential, owner\nsecret, raw stream/KV/consumer authority, or a generic credential-mint API. The host creates the\npublic-nkey JWT material through the typed lifecycle-bound protocol: **prepare \u2192 activate \u2192\nrenew**. Each request is replay-safe and idempotent at its lifecycle/instance operation\ncoordinate; the host writes its credential ledger row and finalizes the gate before it releases\nusable material.\n\nA remote manager can provision only descendants of the same derived owner, and the host\nvalidates that relation and the current manager grant for every provision. It cannot broaden the\nuser's envelope or provision a sibling owner's agent. Renewals are bounded. If login, the\n`supervise` grant, or the host manager authority service is unavailable, the manager reports a\ndegraded state and refuses new agents, restarts, or replacement credentials rather than\nsubstituting local/static authority. Existing live agents remain running only while their own\nvalid authority permits it; recovery requires the host service and a fresh successful renewal.\n\n**User authentication has one path.** On a user-auth space, commands never fall back to\nstatic minting or credless connects: a missing login or a down auth service is one\nsentence naming the exact recovery, and static agent/observer/admin minting is refused\noutright. The refusal is deny-new: a static cred signed before the space flipped stays\nbroker-valid until the signing key is rotated ([security model](security.md)).\n\n## The IdP callout contract\n\nAny OIDC identity provider that issues **EdDSA/Ed25519** JWTs plugs in here directly; a provider that\nissues RS256 or ES256 tokens (many managed OIDC services do) needs a host-side normalization or\nre-issuance adapter first, because the reference bridge pins the token algorithm to EdDSA. The\nreference implementation ships **Better Auth** as a\ndev and test fixture only (it is a `devDependency` of `@cotal-ai/auth`; the only code that imports\nit is the `dev-idp.ts` harness and the smoke tests, never the runtime `src`). The one runtime\ncoupling to an IdP is the `idp.ts` bridge plus the `auth-provider` extension. The bridge core\n(`createIdpBridge`) is IdP-generic for **EdDSA** tokens (issuer, audience, JWKS as configuration).\nThe stock end-to-end flow around it, though, is **Better-Auth-shaped**: `cotalAuthProvider` pins\n`<base>/jwks` and issuer/audience to the IdP origin, and the login client speaks Better Auth's\ndevice-code endpoints (`/device/code`, `/device/token`, `/token`) with an opaque revocable session.\nSo a Better-Auth-shaped EdDSA IdP uses the stock flow directly; **any other production IdP is a\nhosted-composability gap, not a configuration change**. A host integrates it by building its own\nlogin and provider wiring on the low-level primitives (`createIdpBridge`, `createUserTokenIssuer`),\nnot by reusing the stock provider. Note that importing `@cotal-ai/auth` self-registers\n`cotalAuthProvider`, and `resolveAuthProvider()` throws when two providers are registered, so a host\non the registry-resolution path must not also register its own. Whatever the path, never loosen the\nissuer/audience/JWKS pins to force-fit an IdP.\n\nThe bridge (`createIdpBridge`) exchanges a verified IdP token for a Cotal bearer in three steps:\n\n1. **Bearer validation.** Verify the IdP's JWT offline against its **pinned JWKS**, with the token\n algorithm pinned to EdDSA. Keys resolve only through the pinned JWKS: a token carrying embedded\n key material (`jku`/`jwk`/`x5u`/`x5c`) is rejected, so the token can never influence key\n resolution. Issuer and audience are checked, and the minted Cotal bearer is capped to the\n upstream proof's remaining lifetime.\n2. **Owner derivation.** The opaque per-space owner derives deterministically from the JSON-array\n encoding of `[idp issuer, sub]`, namespaced by issuer so no issuer/sub pair can straddle a\n delimiter, and re-login re-lands the same person in the same lanes. The owner-token *format*\n (`u_` followed by 26 base32-lower characters) is normative\n ([SPEC section 2](../SPEC.md#2-identity)). At the contract level the *derivation* from an\n identity is a pluggable edge, but the reference `createIdpBridge` fixes it\n (`deriveOwnerForIdpSubject`) and takes no derivation callback, so what a host configures is the\n IdP, not the derivation. **The encoding is frozen:** changing it, or changing the IdP issuer\n string, re-keys every owner in the space, which is a migration on the order of rotating the space\n secret.\n3. **Actor authorization and mint.** The operator's ledger hook authorizes the `(owner, actor)` pair\n and is the only source of the bearer's `scope`/`parent`; the issuer then mints the Cotal bearer,\n re-asserting every claim shape.\n\nA host wires this with the IdP's own coordinates and nothing from `@cotal-ai/auth` changes:\n\n```ts\nimport { createIdpBridge, pinnedJwksResolver, createUserTokenIssuer } from \"@cotal-ai/auth\";\nconst bridge = createIdpBridge({\n idp: { issuer: idpIssuer, audience, key: pinnedJwksResolver(jwksUri) }, // your production IdP\n space,\n spaceSecret, // identity-plane owner-derivation secret (>=32 bytes), held by the auth service at runtime\n issuer: createUserTokenIssuer({ issuer: cotalIssuer, key: signingKey }), // mints the Cotal bearer\n authorizeActor: (owner, actor) => grantFromLedger(owner, actor), // your ledger, returns an ActorGrant\n});\n```\n\n## Joining\n\nA single **join link** carries server, auth, and space\n([SPEC \xA710](../SPEC.md#10-connection-and-onboarding)):\n\n```\ncotals://<token>@host:4222/<space>?channel=general # cotals:// = TLS required; cotal:// = TLS not required (downgrade-tolerant)\n```\n\nHumans: `cotal join --link \u2026`. Agents: `COTAL_LINK=\u2026 ` in the environment. The connector\nexpands it and auto-joins. Token/user-pass links are the open-mode path; the default\nauthed path threads a minted creds file, and the endpoint adopts the credential's identity\nas its card id. A seat the manager spawned reaches that file through its **launch\nmaterial** rather than through `COTAL_CREDS` in an environment every descendant process\ninherits (see [Configuration](config.md#launch-material)); a session you drive by hand\nstill sets `COTAL_CREDS` itself.\n\n## Honest limitations (v0)\n\n- **The signing key is hot** on the mint/manager box of a static-auth mesh; the \"real\n boundary\" holds given operator-controlled cred distribution. On a per-user-auth mesh\n the data-account signing key is held by the auth service (the callout stage) and by any\n running manager, which loads the trust bundle and self-mints its supervisor cred and\n renewals from it; a copied signing *seed* still stays valid for its identity until the\n signing key is rotated. Rotation remains the revocation lever for trust material.\n- **The two `$SYS` creds renew through rotation.** `membership-observer` and\n `connection-evictor` are signed by the system-account seed, which is never persisted, so no\n running process re-signs them: they carry a 30-day expiry and are renewed by issuing a new\n system account (`cotal down` then `cotal up --rotate-sys`), which leaves the data account,\n every agent cred and the store untouched but does invalidate earlier full backups (they bind to\n the operator JWT and system account they were taken under, so re-run `cotal backup` after). Past that horizon the mesh keeps delivering, but the\n membership feed and live eviction stop; `cotal doctor auth` and the manager warn from the 75%\n point onward.\n- **Static agent creds are long-lived; the machinery's are not.** One-shot command creds\n expire in minutes and the standing daemon creds in 24h with the manager renewing them\n (`cotal doctor auth` is the one diagnosis and repair surface). But a static *agent*\n cred has no TTL yet: `cotal_despawn` cuts a session, not a credential, and a\n compromised agent that copied its creds can reconnect until the signing key is\n rotated. Per-user-auth spaces close this: bearers live minutes, `cotal actor revoke`\n denies the next exchange and the next connect and evicts the principal's live\n connections immediately.\n- **Not non-repudiation.** Authenticity is broker-enforced, not portable proof; it does\n not survive an untrusted relay. Signed envelopes are reserved\n ([SPEC \xA711](../SPEC.md#11-versioning-and-extensibility)).\n- **Chat metadata leaks in-space.** Content reads are ACL-bounded; stream metadata\n (channel names, per-subject counts) is not yet ([security model](security.md)).\n\n**Denials are loud, never silent.** A publish outside an ACL surfaces as a logged denial\n(\"denied, not absent\") on the endpoint's error path; an over-tight ACL never looks like a\nmissing peer ([run a mesh](run-a-mesh.md)).\n"
|
|
16362
|
+
"body": "# Identity\n\n> **Concept** (informative) \xB7 **For:** operators and implementers \xB7 **Normative:** [SPEC \xA72](../SPEC.md#2-identity), [\xA79](../SPEC.md#9-nats--jetstream-security-and-authorization), [\xA710](../SPEC.md#10-connection-and-onboarding), [Appendix B](../SPEC.md#appendix-b-profile-acls)\n\nWho can do what on a mesh, and how it is enforced. The design goal: the mesh is a **real\nboundary against untrusted peers in a shared space**; an agent can only speak as itself\nand only where its declared permissions allow, enforced by the broker, not by agent\ngoodwill. What that boundary does and does not protect is the\n[security model](security.md); the exact ACLs are\n[SPEC Appendix B](../SPEC.md#appendix-b-profile-acls).\n\n## On by default\n\n`cotal up` provisions a JWT-authed space; `cotal up --open` runs an unauthenticated dev\nmesh instead. Both bind loopback by default. `--host 0.0.0.0` widens the bind\nindependently, so \"network-reachable\" never silently means \"unauthenticated\". Open mode\nis for quick local experiments and sits outside every security claim\n([SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization)).\n\n## Shared identity\n\nAn agent's wire identity is a **principal**: an `owner.actor` pair, where the owner is\nthe account (a human, or an organization) the agent acts on behalf of, and the actor is\nthe agent's own handle under that owner ([SPEC \xA72](../SPEC.md#2-identity)). The same pair\nis the card id, the sender tokens in every subject it publishes, the presence key, and\nits durable-consumer names. On an open dev mesh the owner is the literal `local`; on a\nper-user-auth mesh it is a derived token (`u_` plus 26 characters, so no PII rides the\nwire). The connection still authenticates with an **nkey**, generated locally (the signer\nonly ever sees the public half), but the nkey is the transport credential, not the\nidentity: it scopes only the per-connection reply inbox.\n\n**The sender is encoded in the subject.** Every publish carries the sender's owner and\nactor in positions the broker's permissions pin to that connection, so an agent *cannot*\nemit as anyone else: not as another owner, and not as a sibling actor under its own\nowner. Receivers verify the payload's `from.id` against the subject sender and reject\nmismatches; sender authenticity is broker-enforced end to end\n([SPEC \xA73](../SPEC.md#3-subject-layout), [\xA75](../SPEC.md#5-envelopes)).\n\n**Account = space, user = agent.** A space is one NATS account, a server-enforced\nisolation boundary. An operator signs the account; an account **signing key** mints\nper-agent user JWTs.\n\n## Provisioner\n\nThe **provisioner** is whoever holds the account signing key. It mints profile-scoped\ncredentials and pre-creates the durables agents may only *bind* (their DM inbox, their\nrole's task queue). The manager hosts it today, but nothing is manager-special about it;\nprivilege attaches to the signer, and a space can run without a manager.\n`cotal mint <name> --profile <agent|observer|admin>` is the out-of-band path; spawn calls\nthe same library ([CLI](cli.md)). Minting static creds is a **static-auth** surface: a\nper-user-auth space refuses it, because agents there join under a logged-in user, never\nvia a handed-out file (see *Per-user auth* below).\n\nAgent-profile minting resolves one mesh root for the persona ACL, account signer and default\ncredential storage. If the current folder holds trust for a different space or account, mint\nrefuses and names both roots. It never signs one root's persona policy with another root's authority.\n\n## Profiles\n\nEvery credential is a profile: an explicit allow-list built from the same\nsubject/stream/durable builders as the wire layout, so ACLs cannot drift from it. The\nnormative shapes are [SPEC Appendix B](../SPEC.md#appendix-b-profile-acls); in brief:\n\n| Profile | Is |\n|---|---|\n| **agent** | The ordinary peer: publishes as itself to its declared channels, reads within its read ACL + its own DM/task inboxes. Its read-only presence and channel-registry watches may create, inspect, and delete only their own client-managed ordered consumers; those cleanup grants cannot delete KV records or streams. |\n| **observer** | Read-only chat + presence; DMs invisible. What `cotal console` runs. |\n| **admin** | Elevated *read-only* god-view: sees DMs and anycast live, still writes nothing. A deliberate opt-in (`cotal web`). |\n| operator-side | Narrow single-purpose creds for the machinery (supervising, provisioning, teardown, delivery); the reference implementation splits these so no one connection can read every DM *and* delete every stream ([security model](security.md)). |\n\n**An agent's channel scope is three verbs**: `subscribe` (reads at boot),\n`allowSubscribe` (read ACL), `allowPublish` (post ACL, default-deny), declared in its\n[agent file](agent-files.md) or [manifest](manifest.md), minted into its cred. One card\nwith the recipes: [Channels & permissions](channels-and-permissions.md).\n\n**DM confidentiality** holds against peers by construction: deliveries ride per-identity\ninbox prefixes, and the DM/task consumers are provisioner-pre-created and bind-only, so an\nagent cannot create a consumer filtered to someone else's inbox\n([SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization) items 1\u20135).\n\n## Spawn capability\n\nControl-plane power is a **declared capability**, not a default. An agent file carrying\n`capabilities: [spawn]` gets the privileged control subject minted into its cred: spawn,\nplus stop/despawn of its *own* children, plus persona definition. Without it, an agent can\nonly self-despawn. The tool surface mirrors the grant: `cotal_spawn` / `cotal_persona` / `cotal_personas` are\ninjected only where they can actually succeed ([agent files](agent-files.md)). Destructive\noperator ops (history purge, cross-agent stop) live on a third tier no agent credential\nreaches. Persona redefinition separates content from policy; the write path takes only\n`model`/`persona`, so a peer cannot grant itself a capability by redefining a file.\n\n## Per-user authentication\n\n`cotal up --user-auth --idp <auth base URL>` (or manifest `broker.auth: \"user\"`) puts a\n**human identity plane** above the per-agent one: people sign in to an external IdP once,\nand every connect is authorized live against the operator's **actor ledger**. No creds\nfiles to hand out, and revoking a grant actually bites.\n\n**The flow.** Each person runs `cotal login --idp <url>` once per machine. After that,\nany command works: cached IdP session \u2192 fresh IdP proof per connect (so IdP-side\nrevocation bites here too) \u2192 the configured exchange turns it into a short-lived Cotal bearer \u2192\nthe broker's **auth callout** checks the bearer and the ledger at connect time and mints\na scoped credential on the spot. Every bearer also names a **root credential** row in the\nspace's credential ledger, proved live at each connect, so revoking that one credential\nbites at the very next connect. The operator grants access with\n`cotal actor grant <actor> --sub <their id>`; a bare grant is the full envelope (all\nchannels, may spawn), and `--allow-subscribe` / `--allow-publish` / `--scope` narrow it.\nNo ledger row, no access; there is no allow-by-default.\n\n**One auth service per space** hosts both halves: the NATS auth callout and the token\nexchange. Its default HTTP listener remains loopback-only and requires the per-start capability\nstored in the owner-only `auth-service.json` file. An operator may add a second listener with\n`cotal up --user-auth ... --exchange-public-port <port> --exchange-public-url https://auth.example`.\nThat listener still binds `127.0.0.1`; put a reverse proxy in front of it and terminate TLS there.\nIn-process TLS is deliberately not another deployment mode: it would duplicate certificate renewal\nand fork proxy-based deployments.\n\nThe public listener has a closed surface: `GET /health`, `GET /jwks`, `POST /exchange`, and\n`GET /.well-known/cotal-mesh`; every other path is 404. It does **not** require the loopback\ncapability. That capability proves same-uid access to a 0600 local file and has no remote meaning;\non the public face the credential is the proof. A human presents an EdDSA IdP JWT checked against\nthe pinned JWKS, issuer, and audience. An agent presents its spawn-time actor token, whose hash must\nmatch a fresh managed-ledger row. Elevated `view` exchanges stay loopback-only.\n\nThe well-known response contains the IdP pins and the actual deny-all sentinel credential remote\nagents need before the bearer-driven auth callout. The pins ride a `userAuth` arm that names the\nauth provider, and that name is the same one the local arm registers under. A document naming a\ndifferent provider than the one serving it would register an entry nothing can resolve, so both read\none constant. Treat it as bootstrap material: the sentinel cannot publish or subscribe, but\nconsumers must still take the bundle only from the intended HTTPS origin and must verify TLS.\n`--exchange-trusted-proxy` opts into peer attribution by the **last** `X-Forwarded-For` hop; use it\nonly when the listener is reachable solely through a proxy you control.\nWithout it, forwarded headers are ignored and the socket address is the peer key. Public failure\nbuckets are per-source and separate from loopback exchange budgets. The in-process LRU retains at\nmost 1024 peer buckets: that bounds memory and isolates ordinary sources, but an attacker cycling\nmore than 1024 trusted-proxy last hops can evict earlier 429 state. It is not a mint bypass; a valid\ncredential is still required, so use upstream reverse-proxy rate limiting when that throttle-escape\nmatters to the deployment.\n\nThe service starts with the broker, is torn down by `cotal down`, and holds the\ndata-account signing key for the callout (a running manager is the other standing holder, for\nthe creds it mints); the operator seed never enters it. It also owns the space's two authority\nstores (lifecycle records and the credential ledger), provisions them at boot, and refuses\nconnects it cannot credential-check against them; there is no fallback path. If it\ndies while the broker lives, re-running `cotal up` heals it, and a boot whose auth\nservice never became ready exits non-zero, so automation never reads a dead identity\nplane as success. Changing any public-listener flag requires `cotal down` followed by `cotal up`\nwith the new values; a refresh adopts an already-running auth service rather than silently replacing\nits listener policy. \"One per space\" is enforced, not assumed (SPEC \xA713.13): at boot the\nservice takes a broker-backed ownership claim, so a second same-space auth process refuses\nwith instructions instead of silently splitting the plane, and a crashed one's claim is\nreclaimed only once the broker confirms its connections are gone. That verdict is trusted only\non a standalone broker (a clustered one refuses the reclaim, since a partitioned member\ncould still hold them). If the claim's connections die mid-run, the service downs itself\nloudly instead of serving from a half-dead plane.\n\n**Your agents are yours.** `cotal spawn` on a user mesh grants a managed actor under the\n*spawning operator's* owner and launches the agent with a bearer command instead of a\ncreds file. The agent exchanges its spawn-time secret for short bearers (five minutes or\nless) and refreshes ahead of each expiry. Rows are runtime grants: every start rotates\nthe secret, every stop or despawn revokes the row, so a non-running agent holds no\nstanding authority. Manifest deploys (`up -f`) stamp the logged-in owner into the launch,\nso those agents are yours too.\n\n**Despawn tears the lifecycle down, then frees the name.** When you despawn an agent, the manager\ndrives the *full* teardown of that lifecycle: it shreds the local credential files, revokes the\nagent's standing mint authority (its ledger row, so a copied token can no longer mint a fresh\ncredential), deletes its broker footprint (the lifecycle-keyed durables + read-ACL row), and asks\nthe auth service to *retire* the lifecycle (settle in-flight work, evict the departed credentials,\nrecord it retired). The name is held *reserved pending retirement* until **all** of that completes,\nthe broker-footprint cleanup, the standing-authority revoke, **and** the lifecycle retirement, not the\nretirement alone, so a same-name respawn in the gap is refused with\na plain reason and a retry hint rather than quietly handing the alias to a new agent while\nthe old lifecycle's teardown is still running. Only once the broker footprint is gone, the standing\nauthority is revoked, and the retirement is confirmed does the name free, and `cotal spawn <same-name>`\ngives you a fresh agent cleanly. This is what makes reusing an agent's name safe: the old lifecycle is\nfully torn down before the new one takes the alias. If the auth service is unreachable or the\nstanding-authority revoke fails, the despawn still stops the agent and *holds* the name. **A\nsame-name `cotal spawn` re-drives the whole teardown** and finishes it. Retrying the despawn has no\neffect because the agent is already stopped. The operator copy tells you to recover the stack\n(`cotal supervise`) rather than reusing the name over an unretired predecessor.\n\n**A crash mid-retirement resumes at the next boot.** The retirement's last two steps (recording the\nissuance gate terminal, then the lifecycle head terminal) are separate durable writes, and a crash\nbetween them leaves the gate retired while the head is still `retiring`: an alias that can neither\nmint nor be replaced. The auth service's boot crash-resume decides what it owes across *both*\nobjects (the gate *and* the alias head), so the next boot finishes that tail from the durable\noperation intent: nothing is re-revoked or re-drained, and a completed retirement (its head terminal\nlanded, or a successor already took the alias) is left skipped. A retry of the despawn converges on\nthe same recovery.\n\n**Delegation only narrows (the envelope rule).** A user's grant is their envelope:\neverything under their owner (their CLI, every agent they spawn, every agent those\nspawn) stays within its channel lists and its capability scope. Handing a role to a\nspawned agent needs the matching `role:<r>` capability in the spawner's scope. The whole\ndelegation chain is checked, not just the last link, and re-checked at every bearer\nexchange, so narrowing a user's grant reaches their agents within minutes, and revoking\nthe user revokes everything under them, grandchildren included. A spawn beyond the\nenvelope is refused with the exact widening re-grant to ask the operator for.\n\n**Control ops ride your own login**, gated by ledger scope. `spawn` covers launching,\n`ps`, and stop/attach of the agents under **your own owner**: the owner is the\nadministrative boundary of its own subtree, so you (and your agents) manage what you own\nwithout any extra grant. `admin` is the explicit opt-in for touching **other owners'**\nagents; it is never part of a default grant and never accepted from a manifest.\n\n**Elevated operator surfaces ride the same login** through a short-lived *view*: the\nexchange stamps a server-authored view claim into the bearer, and the callout mints that\nconnection as the matching non-agent profile instead of `agent`. `cotal web` and\n`cotal console` ask for the read-only admin view, `clean history` for the purger,\n`channels set/default` for the channel-writer (all gated on ledger scope `admin`);\n`up -f` deploys over the deployer view, gated on `spawn`, because deploying your own team\nis spawn-grade (the manager still refuses a manifest claiming another owner). Views exist\nonly on a signed-in human exchange (an agent's managed exchange never mints one), are\nauthorized against the fresh ledger row at every connect, and expire with the bearer, so\nnarrowing or revoking a grant bites within minutes here too.\n\n### Remote manager authority\n\nA registered user remains an ordinary `agent` bearer by default. Running a detached manager\non a remote user-auth mesh needs the closed server-authored **`manager-service`** view, which\nis distinct from every general-purpose profile. The operator grants it only by adding\n`supervise` to that user's actor-ledger scope. `supervise` is deliberately distinct from\n`spawn` and `admin`: spawn controls your agents, admin permits the separate cross-owner\noperations, and neither grants persistent manager registration authority.\n\nOnly a signed-in human may request this view from the loopback/operator exchange. The public\nexchange and every managed-agent secret exchange refuse it. At exchange and each connection,\nthe auth service re-reads the actor row; revoking or removing `supervise` therefore denies the\nnext view exchange and connection. A grant must carry the whole requested row just like every\nother actor update, so re-grant its channel envelope, role, and all wanted scope tokens, not\nonly `supervise`.\n\nThe service is one opaque manager instance for the user's derived owner and a fixed\nserver-selected manager actor. Its authority is limited to that instance's manager\nregistration, contracts, status, endpoint rails, gate and credential family; it cannot read or\nwrite another owner or instance. It never exposes a signer, static provisioner credential, owner\nsecret, raw stream/KV/consumer authority, or a generic credential-mint API. The host creates the\npublic-nkey JWT material through the typed lifecycle-bound protocol: **prepare \u2192 activate \u2192\nrenew**. Each request is replay-safe and idempotent at its lifecycle/instance operation\ncoordinate; the host writes its credential ledger row and finalizes the gate before it releases\nusable material.\n\nA remote manager can provision only descendants of the same derived owner, and the host\nvalidates that relation and the current manager grant for every provision. It cannot broaden the\nuser's envelope or provision a sibling owner's agent. Renewals are bounded. If login, the\n`supervise` grant, or the host manager authority service is unavailable, the manager reports a\ndegraded state and refuses new agents, restarts, or replacement credentials rather than\nsubstituting local/static authority. Existing live agents remain running only while their own\nvalid authority permits it; recovery requires the host service and a fresh successful renewal.\n\n**User authentication has one path.** On a user-auth space, commands never fall back to\nstatic minting or credless connects: a missing login or a down auth service is one\nsentence naming the exact recovery, and static agent/observer/admin minting is refused\noutright. The refusal is deny-new: a static cred signed before the space flipped stays\nbroker-valid until the signing key is rotated ([security model](security.md)).\n\n## The IdP callout contract\n\nAny OIDC identity provider that issues **EdDSA/Ed25519** JWTs plugs in here directly; a provider that\nissues RS256 or ES256 tokens (many managed OIDC services do) needs a host-side normalization or\nre-issuance adapter first, because the reference bridge pins the token algorithm to EdDSA. The\nreference implementation ships **Better Auth** as a\ndev and test fixture only (it is a `devDependency` of `@cotal-ai/auth`; the only code that imports\nit is the `dev-idp.ts` harness and the smoke tests, never the runtime `src`). The one runtime\ncoupling to an IdP is the `idp.ts` bridge plus the `auth-provider` extension. The bridge core\n(`createIdpBridge`) is IdP-generic for **EdDSA** tokens (issuer, audience, JWKS as configuration).\nThe stock end-to-end flow around it, though, is **Better-Auth-shaped**: `cotalAuthProvider` pins\n`<base>/jwks` and issuer/audience to the IdP origin, and the login client speaks Better Auth's\ndevice-code endpoints (`/device/code`, `/device/token`, `/token`) with an opaque revocable session.\nSo a Better-Auth-shaped EdDSA IdP uses the stock flow directly; **any other production IdP is a\nhosted-composability gap, not a configuration change**. A host integrates it by building its own\nlogin and provider wiring on the low-level primitives (`createIdpBridge`, `createUserTokenIssuer`),\nnot by reusing the stock provider. Note that importing `@cotal-ai/auth` self-registers\n`cotalAuthProvider`, and `resolveAuthProvider()` throws when two providers are registered, so a host\non the registry-resolution path must not also register its own. Whatever the path, never loosen the\nissuer/audience/JWKS pins to force-fit an IdP.\n\nThe bridge (`createIdpBridge`) exchanges a verified IdP token for a Cotal bearer in three steps:\n\n1. **Bearer validation.** Verify the IdP's JWT offline against its **pinned JWKS**, with the token\n algorithm pinned to EdDSA. Keys resolve only through the pinned JWKS: a token carrying embedded\n key material (`jku`/`jwk`/`x5u`/`x5c`) is rejected, so the token can never influence key\n resolution. Issuer and audience are checked, and the minted Cotal bearer is capped to the\n upstream proof's remaining lifetime.\n2. **Owner derivation.** The opaque per-space owner derives deterministically from the JSON-array\n encoding of `[idp issuer, sub]`, namespaced by issuer so no issuer/sub pair can straddle a\n delimiter, and re-login re-lands the same person in the same lanes. The owner-token *format*\n (`u_` followed by 26 base32-lower characters) is normative\n ([SPEC section 2](../SPEC.md#2-identity)). At the contract level the *derivation* from an\n identity is a pluggable edge, but the reference `createIdpBridge` fixes it\n (`deriveOwnerForIdpSubject`) and takes no derivation callback, so what a host configures is the\n IdP, not the derivation. **The encoding is frozen:** changing it, or changing the IdP issuer\n string, re-keys every owner in the space, which is a migration on the order of rotating the space\n secret.\n3. **Actor authorization and mint.** The operator's ledger hook authorizes the `(owner, actor)` pair\n and is the only source of the bearer's `scope`/`parent`; the issuer then mints the Cotal bearer,\n re-asserting every claim shape.\n\nA host wires this with the IdP's own coordinates and nothing from `@cotal-ai/auth` changes:\n\n```ts\nimport { createIdpBridge, pinnedJwksResolver, createUserTokenIssuer } from \"@cotal-ai/auth\";\nconst bridge = createIdpBridge({\n idp: { issuer: idpIssuer, audience, key: pinnedJwksResolver(jwksUri) }, // your production IdP\n space,\n spaceSecret, // identity-plane owner-derivation secret (>=32 bytes), held by the auth service at runtime\n issuer: createUserTokenIssuer({ issuer: cotalIssuer, key: signingKey }), // mints the Cotal bearer\n authorizeActor: (owner, actor) => grantFromLedger(owner, actor), // your ledger, returns an ActorGrant\n});\n```\n\n## Joining\n\nA single **join link** carries server, auth, and space\n([SPEC \xA710](../SPEC.md#10-connection-and-onboarding)):\n\n```\ncotals://<token>@host:4222/<space>?channel=general # cotals:// = TLS required; cotal:// = TLS not required (downgrade-tolerant)\n```\n\nHumans: `cotal join --link \u2026`. Agents: `COTAL_LINK=\u2026 ` in the environment. The connector\nexpands it and auto-joins. Token/user-pass links are the open-mode path; the default\nauthed path threads a minted creds file, and the endpoint adopts the credential's identity\nas its card id. A seat the manager spawned reaches that file through its **launch\nmaterial** rather than through `COTAL_CREDS` in an environment every descendant process\ninherits (see [Configuration](config.md#launch-material)); a session you drive by hand\nstill sets `COTAL_CREDS` itself.\n\n## Honest limitations (v0)\n\n- **The signing key is hot** on the mint/manager box of a static-auth mesh; the \"real\n boundary\" holds given operator-controlled cred distribution. On a per-user-auth mesh\n the data-account signing key is held by the auth service (the callout stage) and by any\n running manager, which loads the trust bundle and self-mints its supervisor cred and\n renewals from it; a copied signing *seed* still stays valid for its identity until the\n signing key is rotated. Rotation remains the revocation lever for trust material.\n- **The two `$SYS` creds renew through rotation.** `membership-observer` and\n `connection-evictor` are signed by the system-account seed, which is never persisted, so no\n running process re-signs them: they carry a 30-day expiry and are renewed by issuing a new\n system account (`cotal down` then `cotal up --rotate-sys`), which leaves the data account,\n every agent cred and the store untouched but does invalidate earlier full backups (they bind to\n the operator JWT and system account they were taken under, so re-run `cotal backup` after). Past that horizon the mesh keeps delivering, but the\n membership feed and live eviction stop; `cotal doctor auth` and the manager warn from the 75%\n point onward.\n- **Static agent creds are long-lived; the machinery's are not.** One-shot command creds\n expire in minutes and the standing daemon creds in 24h with the manager renewing them\n (`cotal doctor auth` is the one diagnosis and repair surface). But a static *agent*\n cred has no TTL yet: `cotal_despawn` cuts a session, not a credential, and a\n compromised agent that copied its creds can reconnect until the signing key is\n rotated. Per-user-auth spaces close this: bearers live minutes, `cotal actor revoke`\n denies the next exchange and the next connect and evicts the principal's live\n connections immediately.\n- **Not non-repudiation.** Authenticity is broker-enforced, not portable proof; it does\n not survive an untrusted relay. Signed envelopes are reserved\n ([SPEC \xA711](../SPEC.md#11-versioning-and-extensibility)).\n- **Chat metadata leaks in-space.** Content reads are ACL-bounded; stream metadata\n (channel names, per-subject counts) is not yet ([security model](security.md)).\n\n**Denials are loud, never silent.** A publish outside an ACL surfaces as a logged denial\n(\"denied, not absent\") on the endpoint's error path; an over-tight ACL never looks like a\nmissing peer ([run a mesh](run-a-mesh.md)).\n"
|
|
16363
16363
|
},
|
|
16364
16364
|
{
|
|
16365
16365
|
"slug": "agent-files",
|
|
16366
16366
|
"title": "Agent files",
|
|
16367
16367
|
"kind": "Reference (the persisted form of an agent's identity + persona, read by every launcher)",
|
|
16368
16368
|
"summary": "An agent's identity and persona live in one Markdown file instead of being passed flag-by-flag, the same shape Claude Code uses for subagents:",
|
|
16369
|
-
"body": "# Agent files\n\n> **Reference** (the persisted form of an agent's identity + persona, read by every launcher) \xB7 **For:** operators \xB7 **ACL semantics:** [SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization), [Appendix B](../SPEC.md#appendix-b-profile-acls)\n\nAn agent's identity and persona live in one Markdown file instead of being passed\nflag-by-flag, the same shape Claude Code uses for subagents:\n\n```markdown\n.cotal/agents/<name>.md\n---\nname: dave # \u2192 COTAL_NAME / card.name\nrole: builder # \u2192 COTAL_ROLE / card.role (presence + anycast address)\ndescription: \u2026 # \u2192 card.description\ntags: [edit, test] # \u2192 card.tags (\"what it can do\")\nsubscribe: [general, team.backend] # channels it reads at boot (omit = none)\nallowSubscribe: [general, team.>] # read ACL (omit = same as subscribe)\nallowPublish: [general, team.backend] # post ACL (omit = none, default-deny)\nmodel: opus # optional model override\nvariant: high # optional connector-defined model variant\ncapabilities: [spawn] # control-plane capabilities (may start/despawn teammates)\n---\nYou are a builder on a shared mesh of peer agents\u2026 \u2190 the body is the persona\n```\n\n**Frontmatter is identity** (an A2A-style `AgentCard`,\n[SPEC \xA76](../SPEC.md#6-presence-and-discovery)); **the body is the persona**, appended to\nthe session's system prompt at launch: the one field that *must* be applied at launch,\nbecause a session cannot change its system prompt afterward. Connectors that use an external\nprompt file write an owner-private temporary copy and pass only its path, so the persona body is not\npublished in the agent process argv.\n\n## Fields\n\nAuthoritative shape: [`agent-file.ts`](../packages/core/src/agent-file.ts).\n\n| Field | Type | Meaning |\n|---|---|---|\n| `name` | string, required | Display name \u2192 `card.name`. A launcher resolves a bare name to `.cotal/agents/<name>.md`. |\n| `role` | string | The addressable **service**: presence label *and* the anycast address ([SPEC \xA73](../SPEC.md#3-subject-layout)). |\n| `kind` | `agent` \\| `endpoint` | Participation class; default `agent`. |\n| `description` | string | One-line summary \u2192 `card.description`. |\n| `tags` | string[] | Capability tags \u2192 `card.tags`. |\n| `subscribe` | string[] | The **active read set**: channels subscribed at boot (mutable at runtime via join/leave). Must be \u2286 `allowSubscribe`. **Omitted \u21D2 no channels**: an agent reads what it lists, and one that lists none joins none (still reachable by DM, anycast and presence). List `general` if you want it. |\n| `allowSubscribe` | string[] | The **read ACL**: channels it *may* read. Wildcard subtrees allowed (`team.>`). Omitted \u21D2 same as `subscribe`. |\n| `allowPublish` | string[] | The **post ACL**: channels it may publish to. **Omitted \u21D2 deny**; posting is the dangerous capability, declare it explicitly. |\n| `quiet` | string[] | Per-channel attention *default*: ambient stays buffered and pull-only until `cotal_inbox`; `@mention`s remain automatic. Concrete channels within the read ACL. |\n| `muted` | string[] | Per-channel attention *default*: dropped on receive, `@mentions` included. |\n| `model` | string | Model override handed to the agent CLI (Claude: `opus` / full id; OpenCode: `provider/model`). |\n| `variant` | string | Connector-defined model variant (e.g. an OpenCode variant, see `cotal models`). |\n| `agent` | string | The connector/harness this persona pins (`claude`, `jcode`, and so on). Precedence: explicit `--agent` > this field > `COTAL_DEFAULT_AGENT` > the product default, the same shape as `model`/`variant`, so the env var stays a *default* and cannot beat a deliberate per-persona pin. A value naming an unregistered connector fails the spawn loudly (no silent fallback). |\n| `launchOptions` | map | Opaque per-connector launch options forwarded **raw** to the harness (Claude flags, OpenCode agent config; Hermes and pi have no option surface and fail loud). A CLI `--opt key=value` overrides a key set here. See [run a mesh](run-a-mesh.md#spawning-agents). |\n| `capabilities` | string[] | Control-plane capabilities minted into the cred. `spawn` grants the privileged control subject (spawn / named stop / persona definition), default-deny when absent, enforced by the broker, not a handler. On a per-user-auth mesh, `role:<r>` additionally lets the agent delegate role `r` when spawning ([identity & auth](identity-and-auth.md)); `admin` is never a persona capability. |\n| `owner` | string | **Policy, not content**: set once by `definePersona` (owner = creator); only the owner (or admin) may redefine the file over the wire. Never write it by hand. |\n| *(any other key)* | string | Kept verbatim in `meta` so a connector can read its own launcher hints without core knowing them. The connector-owned keys are the exception: `connector`, `model`, `variant`, and `host` (the machine the session runs on) are overlaid from the live session, so a file cannot declare a harness or a host it is not on. |\n\nThe three channel verbs on one card, with the common recipes:\n[Channels & permissions](channels-and-permissions.md). Attention semantics (`quiet` /\n`muted` are one-way *defaults*; the runtime toggle is per-instance and resets on restart):\n[Connect Claude](connect-claude.md#attention).\n\n## Persona lookup\n\n- **By name.** A launcher resolves a bare name to
|
|
16369
|
+
"body": "# Agent files\n\n> **Reference** (the persisted form of an agent's identity + persona, read by every launcher) \xB7 **For:** operators \xB7 **ACL semantics:** [SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization), [Appendix B](../SPEC.md#appendix-b-profile-acls)\n\nAn agent's identity and persona live in one Markdown file instead of being passed\nflag-by-flag, the same shape Claude Code uses for subagents:\n\n```markdown\n.cotal/agents/<name>.md\n---\nname: dave # \u2192 COTAL_NAME / card.name\nrole: builder # \u2192 COTAL_ROLE / card.role (presence + anycast address)\ndescription: \u2026 # \u2192 card.description\ntags: [edit, test] # \u2192 card.tags (\"what it can do\")\nsubscribe: [general, team.backend] # channels it reads at boot (omit = none)\nallowSubscribe: [general, team.>] # read ACL (omit = same as subscribe)\nallowPublish: [general, team.backend] # post ACL (omit = none, default-deny)\nmodel: opus # optional model override\nvariant: high # optional connector-defined model variant\ncapabilities: [spawn] # control-plane capabilities (may start/despawn teammates)\n---\nYou are a builder on a shared mesh of peer agents\u2026 \u2190 the body is the persona\n```\n\n**Frontmatter is identity** (an A2A-style `AgentCard`,\n[SPEC \xA76](../SPEC.md#6-presence-and-discovery)); **the body is the persona**, appended to\nthe session's system prompt at launch: the one field that *must* be applied at launch,\nbecause a session cannot change its system prompt afterward. Connectors that use an external\nprompt file write an owner-private temporary copy and pass only its path, so the persona body is not\npublished in the agent process argv.\n\n## Fields\n\nAuthoritative shape: [`agent-file.ts`](../packages/core/src/agent-file.ts).\n\n| Field | Type | Meaning |\n|---|---|---|\n| `name` | string, required | Display name \u2192 `card.name`. A launcher resolves a bare name to `.cotal/agents/<name>.md`. |\n| `role` | string | The addressable **service**: presence label *and* the anycast address ([SPEC \xA73](../SPEC.md#3-subject-layout)). |\n| `kind` | `agent` \\| `endpoint` | Participation class; default `agent`. |\n| `description` | string | One-line summary \u2192 `card.description`. |\n| `tags` | string[] | Capability tags \u2192 `card.tags`. |\n| `subscribe` | string[] | The **active read set**: channels subscribed at boot (mutable at runtime via join/leave). Must be \u2286 `allowSubscribe`. **Omitted \u21D2 no channels**: an agent reads what it lists, and one that lists none joins none (still reachable by DM, anycast and presence). List `general` if you want it. |\n| `allowSubscribe` | string[] | The **read ACL**: channels it *may* read. Wildcard subtrees allowed (`team.>`). Omitted \u21D2 same as `subscribe`. |\n| `allowPublish` | string[] | The **post ACL**: channels it may publish to. **Omitted \u21D2 deny**; posting is the dangerous capability, declare it explicitly. |\n| `quiet` | string[] | Per-channel attention *default*: ambient stays buffered and pull-only until `cotal_inbox`; `@mention`s remain automatic. Concrete channels within the read ACL. |\n| `muted` | string[] | Per-channel attention *default*: dropped on receive, `@mentions` included. |\n| `model` | string | Model override handed to the agent CLI (Claude: `opus` / full id; OpenCode: `provider/model`). |\n| `variant` | string | Connector-defined model variant (e.g. an OpenCode variant, see `cotal models`). |\n| `agent` | string | The connector/harness this persona pins (`claude`, `jcode`, and so on). Precedence: explicit `--agent` > this field > `COTAL_DEFAULT_AGENT` > the product default, the same shape as `model`/`variant`, so the env var stays a *default* and cannot beat a deliberate per-persona pin. A value naming an unregistered connector fails the spawn loudly (no silent fallback). |\n| `launchOptions` | map | Opaque per-connector launch options forwarded **raw** to the harness (Claude flags, OpenCode agent config; Hermes and pi have no option surface and fail loud). A CLI `--opt key=value` overrides a key set here. See [run a mesh](run-a-mesh.md#spawning-agents). |\n| `capabilities` | string[] | Control-plane capabilities minted into the cred. `spawn` grants the privileged control subject (spawn / named stop / persona definition), default-deny when absent, enforced by the broker, not a handler. On a per-user-auth mesh, `role:<r>` additionally lets the agent delegate role `r` when spawning ([identity & auth](identity-and-auth.md)); `admin` is never a persona capability. |\n| `owner` | string | **Policy, not content**: set once by `definePersona` (owner = creator); only the owner (or admin) may redefine the file over the wire. Never write it by hand. |\n| *(any other key)* | string | Kept verbatim in `meta` so a connector can read its own launcher hints without core knowing them. The connector-owned keys are the exception: `connector`, `model`, `variant`, and `host` (the machine the session runs on) are overlaid from the live session, so a file cannot declare a harness or a host it is not on. |\n\nThe three channel verbs on one card, with the common recipes:\n[Channels & permissions](channels-and-permissions.md). Attention semantics (`quiet` /\n`muted` are one-way *defaults*; the runtime toggle is per-instance and resets on restart):\n[Connect Claude](connect-claude.md#attention).\n\n## Persona lookup\n\n- **By name.** A launcher resolves a bare name to `<target-root>/.cotal/agents/<name>.md`. The\n target root comes from the selected mesh, including `cotal use`, `--space`, and `--server`, so\n launchers, `cotal personas`, setup, status, and agent-profile minting use one catalog. This is a\n directory convention, not an HTTP well-known; mesh discovery stays NATS presence. The card built\n from the file is what gets broadcast.\n- **One ref.** The launcher sets `COTAL_AGENT_FILE=<abs path>` (the *who*) the way\n `COTAL_LINK` carries the *where*; the joined session reads its card straight from the\n file. Individual `COTAL_*` vars still override it ([config](config.md)).\n- **Defaults.** A bare `cotal spawn` uses the `default` persona\n (`COTAL_DEFAULT_PERSONA` changes the fallback); the harness comes from `--agent` > the persona's\n `agent:` pin > the invoking CLI's `COTAL_DEFAULT_AGENT` > the manager's own environment, else\n Claude. An explicit flag always wins over the file, and the file wins over either environment\n default, including detached spawns ([run a mesh](run-a-mesh.md)).\n\nEvery launcher consumes the file the same way; they differ only in how they run the spec:\n\n| Launcher | How to point at a file |\n|---|---|\n| Manager (`cotal spawn --detach dave`) | auto-discovers `.cotal/agents/dave.md` in the manager's workspace, or `--config <persona-or-path>`; same grammar as foreground (`--model`, `--variant`, `--cwd`, `--prompt`, ACL overrides, `--share-tools`). |\n| Foreground (`cotal spawn dave`) | same resolution; the real agent TUI takes over this terminal. Works from any directory via the mesh registry. |\n\n`.cotal/` is gitignored (user-local, like `.claude/`); commit persona files you want\nshared some other way. The demo ships committed examples under\n[`examples/01-lateral-coordination/agents/`](../examples/01-lateral-coordination/agents/).\n\n## Persona purpose\n\nExpert-persona prompts (\"you are a world-class\u2026\") do not reliably improve accuracy. Keep\nthe body to what the agent *does* and how it *coordinates*; a persona that needs facts\nshould point at the source (the repo's docs, a URL), not assert them.\n\n## Defining one at runtime\n\n`cotal_persona(name, prompt, model?, announce?)` sends a persona to the manager, which\nwrites the same file; a later `cotal_spawn(name, role?, agent?, model?, variant?)` brings\nit online, so a peer can mint a teammate with no hand-written file\n([tool catalog](mcp-tools.md)). The write path takes **content only** (`model` /\n`persona`); `role`, `allowPublish`, `capabilities`, and `owner` are policy and have no\nslot, so a peer cannot grant itself a capability by redefining a file. A persona with no\n`capabilities:` line (every wire-defined one) therefore spawns **without** `spawn`, and a\nspawn whose effective role is `manager` is **refused at spawn time** rather than joining as\na labelled manager that silently cannot seat workers: either put `capabilities: [spawn]` on\nthe file (an operator edit) or spawn it under another role.\n\n**Defining is silent.** Nothing goes out on the mesh unless you pass `announce: <channel>`,\nand then it goes to that channel only. A peer that did not ask for the persona has no way\nto judge whether spawning it is wanted, and a broadcast soliciting spawns from an\nunfamiliar principal is a thing a peer should be suspicious of, so announcing belongs on\nthe channel your team is working on rather than `general`. The old announcement carried limited discovery. Peers already listening saw the bare name, but\nno prompt, model, or role. Peers joining later saw nothing. No path a peer can\ndeliberately consult is affected: `cotal_personas` lists and shows the catalog over the\nwire (spawn-capability, same ownership as the write), `cotal personas list` reads the\ncatalog within a workspace, and `cotal_spawn` on a name that does not exist fails loud.\n\nThe operator-side counterpart is `cotal personas` (list / show / edit / new / rm); it reads and\nwrites the selected mesh root's files directly, offline, with no broker connection ([CLI](cli.md)).\n"
|
|
16370
16370
|
},
|
|
16371
16371
|
{
|
|
16372
16372
|
"slug": "authoring-a-connector",
|
|
@@ -16387,7 +16387,7 @@ var DOCS_BUNDLE = {
|
|
|
16387
16387
|
"title": "`cotal` CLI reference",
|
|
16388
16388
|
"kind": "Reference: describes the TypeScript reference implementation (the `cotal` CLI), not the wire contract.",
|
|
16389
16389
|
"summary": "cotal is the operator command line for the reference implementation: bring a mesh up, mint identities, launch agents, watch what they do, and tear it all down.",
|
|
16390
|
-
"body": "# `cotal` CLI reference\n\n> **Reference**: describes the TypeScript reference implementation (the `cotal` CLI), not the wire contract. \xB7 **For:** operators \xB7 **Wire contract:** [SPEC](../SPEC.md)\n\n`cotal` is the operator command line for the reference implementation: bring a mesh up, mint\nidentities, launch agents, watch what they do, and tear it all down. It is a thin client over the\nwire contract: the normative subjects and schemas live in the [SPEC](../SPEC.md); this page is\nlookup material for the commands, not a walkthrough; if you are new, start with\n[Getting started](getting-started.md).\n\n## Running it\n\n```bash\nnpm install -g cotal-ai # puts `cotal` on your PATH (needs Node 22+)\ncotal --help # every command, grouped\ncotal --version # cotal-ai version + each installed extension's (also `cotal -v`)\ncotal <command> --help # one command's flags and usage\n```\n\n`npx cotal-ai <command>` runs it without a global install; in a dev clone, `pnpm cotal <command>`\nruns it through `tsx` with no build step. Bare `cotal` prints help. Every command generates its own\n`--help`, usage, and shell completion from its declared flags.\n\nCommands come from the surfaces the binary composes: the base mesh CLI, the manager\n(`supervise`), and the delivery daemon (`deliver`), plus any operator-installed extensions.\n`cotal ext add <npm-package>` installs any registry providers a package contributes: commands,\nruntimes, and local process lifecycle descriptors. The `web` dashboard and optional manager\nruntimes ship this way.\n\n## Commands\n\n| Area | Command | Purpose |\n|---|---|---|\n| Set up & lifecycle | [`setup`](#setup) | Guided, configure-only setup (installs, seeds personas; launches nothing) |\n| Set up & lifecycle | [`update`](#update) | Reconcile first-party extensions and check or opt into a coherent CLI upgrade |\n| Set up & lifecycle | [`up`](#up) | Start a local mesh (nats-server + JetStream), or boot a whole manifest with `-f` |\n| Set up & lifecycle | [`down`](#down) | Stop the whole stack, selected registered components, or a manifest deploy |\n| Set up & lifecycle | [`backup`](#backups) | Create an offline full-space or registry-only artifact from a preserved cut |\n| Set up & lifecycle | [`clean`](#clean) | Configurable cleanup: purge history (live), or wipe the local store / identity (stopped) |\n| Set up & lifecycle | [`meshes`](#mesh-registry) | List the running meshes on this machine |\n| Set up & lifecycle | [`use`](#mesh-registry) | Set the default mesh a bare `cotal spawn` joins |\n| Set up & lifecycle | [`status`](#mesh-registry) | Read-only diagnostics for setup, processes, and the selected mesh |\n| Agents & personas | [`spawn`](#spawn) | Launch an agent from a persona (foreground, or `--detach` via the manager) |\n| Agents & personas | [`models`](#models) | List connector model catalogs and variants from the manager |\n| Agents & personas | [`ps`](#managed-seats) | List managed agents and their mesh status |\n| Agents & personas | [`stop`](#managed-seats) | Ask the manager to stop a managed agent |\n| Agents & personas | [`attach`](#managed-seats) | Stream and drive a managed agent's terminal (pty runtime) |\n| Agents & personas | [`input`](#input) | Type one line into a managed agent's terminal without attaching |\n| Agents & personas | [`personas`](#personas) | List, show, edit, create, or remove local personas |\n| Agents & personas | [`supervise`](#supervise) | Run a manager daemon (the agent supervisor / control plane) |\n| Agents & personas | [`runtimes`](#runtimes) | List the agent runtimes the manager can spawn through and whether each is reachable |\n| Agents & personas | [`reconcile-gate`](#reconcile-gate) | Unfreeze an issuance gate left frozen by a crashed restart when the successor cannot boot-heal it (holder gone, complete CONNZ sweep) |\n| Messaging & watching | [`endpoints`](#endpoints) | List every endpoint in the live presence roster, including infrastructure |\n| Messaging & watching | [`describe` / `invoke`](#endpoint-control) | Resolve a v0.4 service's command surface off the wire; invoke one command by name |\n| Messaging & watching | [`send`](#send) | Send one message, then exit: DM a peer, post a channel, or ask a role |\n| Messaging & watching | [`channels`](#channels) | Inspect or set the channel registry |\n| Messaging & watching | [`history`](#history) | Clear retained message history |\n| Messaging & watching | [`console`](#console) | Live protocol view for a space (TUI, or `--plain` line stream) |\n| Messaging & watching | [`web`](#web) | Browser dashboard (installed as the `@cotal-ai/web` extension) |\n| Auth & meshes | [`mint`](#mint) | Mint a creds file for a space (static auth mode) |\n| Auth & meshes | [`login`](#login) | Sign in to a per-user-auth mesh's IdP (once per machine) |\n| Auth & meshes | [`logout`](#login) | Revoke the IdP session and clear the cached login |\n| Auth & meshes | [`actor`](#actor) | Manage a user-auth space's actor ledger (grant / revoke / list) |\n| Auth & meshes | [`doctor`](#doctor) | Credential-health diagnosis and repair (`doctor auth`) |\n| Auth & meshes | [`join`](#join) | Join a space as your own presence (interactive) |\n| Manifest | [`topology`](#manifest-deploys) | Validate and view a mesh manifest's access graph (read-only) |\n| Extensions & misc | [`ext`](#ext) | Install / remove operator CLI extensions |\n| Extensions & misc | [`completion`](#completion) | Print or install shell completion |\n| Extensions & misc | [`feedback`](#feedback) | Send feedback to the Cotal developers |\n| Extensions & misc | [`deliver`](#server-daemons) | Run the server-side Plane-3 delivery daemon |\n| Workflow runs | [`run`](#run) | Operate durable workflow runs: start, resume, list, inspect, answer a checkpoint |\n| Extensions & misc | [`feedback-intake`](#server-daemons) | Run a self-hosted feedback intake server |\n\nThe manifest modes of `up`, `spawn`, and `down` (`-f <cotal.yaml>`) plus `topology` are covered\ntogether under [Manifest deploys](#manifest-deploys).\n\n## setup\n\n```bash\ncotal setup [--full] [--demo] [--yes] [--skills]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--full` | off | Redo the full guided flow (implies `--demo`) |\n| `--demo` | off | Also seed the guided expert team (`david`, `sven`, `me`) |\n| `--yes`, `-y` | off | Non-interactive accept-all (for agents / CI) |\n| `--skills` | off | Reconcile Cotal skills only through installed connector providers, plus `~/.agents/skills`. Refused with `--full` or `--demo`. |\n\nGuided setup is **configure-only**: it checks prerequisites, invokes installed connectors' declared setup providers, and\nseeds persona files, and it launches nothing (no mesh, no web, no manager). First run gets the\nnarrated flow; later runs print a status card. By default it seeds one `default` persona; the\n`david`/`sven`/`me` team is opt-in via `--demo`. `cotal status` points stale Claude skills and\nout-of-date `.agents` skills at `cotal setup --skills`, not unscoped `setup`. See [Getting started](getting-started.md) and, for\nmaintainers, [setup internals](setup-internals.md).\n\n## update\n\n```bash\ncotal update [--self]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--self` | off | If a newer release exists, install that exact validated `cotal-ai` version globally and reconcile through the newly installed binary |\n\nWithout `--self`, `update` keeps the installed first-party surfaces coherent with the running\nbinary: it force-reconciles the four built-in connectors, then reinstalls other `@cotal-ai/*`\noperator extensions at the binary's exact version. Each extension runs in an isolated child, so one\nfailure cannot poison later replays. It then checks npm; a newer binary is an informational notice\nwith `cotal update --self` as the next command, not an automatic install.\n\nWith `--self`, the npm check happens first. When a newer release exists, Cotal installs the exact\nversion it validated, resolves and verifies that package in npm's global root, then launches that\nbinary to reconcile connectors and first-party extensions to the new generation. An npx or dev-clone\ninvocation therefore installs and continues through a separate global copy; it never claims the\nalready-running process changed. If the binary is current, `--self` performs the normal local\nreconcile without reinstalling it.\n\nThird-party extensions are listed with their installed version and recorded spec but are not\nauto-updated in v1. Floating third-party updates require `@cotal-ai/*` peer-range validation and are\na future follow-up. A failed connector/extension install, npm metadata check, or requested global\ninstall is reported and makes the command exit nonzero. Independent extension attempts continue so\nthe output includes every failure; an unavailable npm registry does not undo a completed local\nreconcile, but the command still exits nonzero because it could not establish that the install is\ncurrent.\n\n## up\n\n```bash\ncotal up [--detach] [--open] [--space <s>] [--server <url>] [--channels <path>] [--runtime <name>]\ncotal up --user-auth --idp <url> [--exchange-public-port <n> --exchange-public-url <https://\u2026> [--exchange-trusted-proxy]]\ncotal up --tls-cert <cert.pem> --tls-key <key.pem> # serve broker TLS (both, or neither)\ncotal up --restore <dir> [--restore-only registry] [--accept-missing-source]\ncotal up -f <cotal.yaml> [--dry-run] [--runtime <name>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--server <url>` | auto (free local port) | Listen URL override |\n| `--host <host>` | none | Bind host override. With no `--server`, the broker URL is derived from it, so `--host <addr>` alone is enough to make a mesh reachable at that address; a `--host`/`--server` pair naming different addresses is refused. A wildcard bind (`0.0.0.0`, `::`) keeps a dialable loopback URL. Recorded on the mesh and reused by every later manager launch, so a repair or resume keeps remote [`attach`](#managed-seats) working |\n| `--space <s>` | the folder's name | Space name |\n| `--store-dir <dir>` | none | JetStream store directory |\n| `--channels <path>` | `.cotal/channels.json` if present | Channel-registry seed file (JSON). An explicit path that is missing is an error |\n| `--restore <dir>` | none | Restore a completed offline backup before exposing the normal listener |\n| `--restore-only registry` | artifact selection | Restore only the registry component |\n| `--accept-missing-source` | off | Explicit disaster consent when the inode-bound preserved source is absent |\n| `--open` | off (auth) | Unauthenticated dev mesh: no JWT, no ACLs |\n| `--user-auth` | off | Per-user auth: people `cotal login`; connects are authorized against the actor ledger |\n| `--idp <url>` | none | With `--user-auth`: the IdP auth base URL to pin on first enable |\n| `--exchange-public-port <n>` | none | With `--user-auth`: add the public exchange face on this loopback port, for an HTTPS reverse proxy to forward to |\n| `--exchange-public-url <https://\u2026>` | none | With `--exchange-public-port`: advertise the reverse proxy's HTTPS URL in discovery |\n| `--exchange-trusted-proxy` | off | With `--exchange-public-port`: attribute public failure buckets to the last `X-Forwarded-For` hop. Enable only when the listener is reachable solely through a trusted proxy; otherwise the socket address is used |\n| `--detach` | off | Run in the background (stop with `cotal down`) |\n| `--tls-cert <path>` | none | PEM certificate to serve TLS with. Must be given together with `--tls-key`. Before starting the broker, Cotal checks readability, private-key mode, key/certificate match, the validity window, and host coverage. `nats-server` accepts an expired certificate and leaves the failure to clients, so Cotal performs these checks first. The decision is recorded; a later bare `cotal up` keeps serving TLS |\n| `--tls-key <path>` | none | PEM private key for `--tls-cert`. Refused if group- or other-readable (tighten to `600`) |\n| `--file <cotal.yaml>`, `-f` | none | Launch a whole mesh from a manifest |\n| `--dry-run` | off | With `-f`: print the plan, mutate nothing |\n| `--runtime <name>` | `pty` (or the manifest's, with `-f`) | Agent runtime for the mesh manager (`pty` built in; others are installed extensions, explicit-only). Resolved + probed before the broker starts; an uninstalled/unreachable runtime fails loud. With `-f`, overrides the manifest's runtime |\n| `--rotate-sys` | off | Rotate the space's system account and re-mint its two `$SYS` creds. Needs a stopped mesh; refused with `--open` |\n\n`cotal up` boots a local nats-server with JetStream and, in auth mode (the default), JWT auth and\nper-agent ACLs; `--detach` records the mesh so `cotal spawn` from any directory can find it. With no\n`--server`, it auto-selects a free port if the default address is taken; an explicit `--server`\nstays fail-loud on collision. `--detach` also brings up the control plane (delivery daemon in auth\nmode, then the manager). The `-f` form is a [manifest deploy](#manifest-deploys); see\n[Run a mesh](run-a-mesh.md).\n\n`--user-auth --idp <url>` starts the space's auth service alongside the broker: the NATS\nauth callout plus its capability-gated local exchange, and optionally the closed public exchange\nface configured by the three `--exchange-*` flags above. The service is torn down with `cotal down`,\nand a re-run of `cotal up` heals a dead service on a running broker. `--user-auth` and `--open`\ncontradict each other and are refused loudly; a running broker cannot change auth mode\nwithout a `cotal down` first. See [identity & auth](identity-and-auth.md).\n\n`--rotate-sys` renews the two `$SYS` credentials (`membership-observer`, `connection-evictor`).\nThey carry a 30-day expiry and nothing re-signs them in place, because the system-account seed is\nnever persisted, so they are renewed by issuing a **new system account** under the same broker\noperator and minting fresh creds against it. A plain re-`up` does **not** do this: it reuses the\nexisting trust record, and its `$SYS` creds along with it.\n\nThe rotation is safe to run on a real space, with one operational cost. The data account, the account\nsigning key, every agent credential minted from it, and the JetStream store are all untouched; what\ndies is the retired system account, and with it any out-of-band copy of the old `$SYS` creds, on every\nbroker that loads the rotated config. The cost is that **earlier full backups stop being restorable**\n(see below), so this is not a no-consequence operation. It needs the broker to restart on the rewritten\nconfig, so it runs as part of a boot:\n\n```bash\ncotal down\ncotal up --rotate-sys --detach # agents reconnect; nothing is re-provisioned\ncotal doctor auth # both $SYS creds healthy again, 30 days out\n```\n\nA rotation is a stopped, fresh boot, and anything that is not one refuses it, all for the same reason\n(the on-disk material and the broker it runs on must never end up on different generations):\n\n- a live mesh, because the running broker would keep serving the retired account;\n- an open mesh, whether that comes from `--open` or from `broker.auth: false` in a manifest, which\n has no system account at all;\n- `--restore`, because reinstating a trust root and superseding it in one command leaves no way to\n say which authority the mesh came up on;\n- an unfinished restore or resume attempt on this root, including one `cotal up` would recover on\n its own, because those paths can adopt a live listener and return without booting a broker;\n- a root that hosts more than one space, because the system account lives in the shared broker\n record and a rotation would retire every tenant's, while the root holds one `$SYS` cred pair\n pinned to one data account.\n\nTwo things to know before you run it:\n\n- **The retirement is config-load-bound.** Old `$SYS` creds are refused by any broker that loads the\n rotated config. A stale `nats-server` still running the *previous* config in memory would keep\n honouring them, so stop every broker for this root first. `--rotate-sys` refuses if this root's\n mesh is recorded as running, if anything unidentified is answering at the address it was given, or\n if the root's pid file names a live (or unreadable) process. Those are Cotal's own ownership\n records, not a scan of the process table: a `nats-server` you started by hand against this root's\n `server.conf` on some other port writes none of them and will not be seen. Do not run one.\n- **It invalidates earlier full backups.** A full artifact binds to the trust chain it was taken\n against, and that commitment covers the operator JWT and the system account. Every full backup\n taken before a rotation refuses to restore afterwards, so take a fresh `cotal backup` once the\n rotated mesh is up. `cotal up --restore` names this case when the data account still matches.\n\nThe commit is not atomic (a trust-record write plus two credential writes), so an interrupted\nrotation leaves the record ahead of the creds. That split is detected rather than silent: every\n`cotal up` on an auth mesh, and every `cotal doctor auth`, compares each `$SYS` cred's issuer against\nthe persisted record and names the retired account. `up` warns rather than refusing, because these\ncreds power the membership graph and live eviction, both of which degrade fail-soft; the mesh is not\nworth taking down over them. Re-running the rotation heals it, at the cost of one generation.\n\nWhile those creds are expired the mesh keeps delivering messages, but the\n[membership feed](delivery-daemon.md) and live connection eviction stay down; `cotal doctor auth`\nand the manager's log both name the credential and this repair.\n\n## down\n\n```bash\ncotal down\ncotal down --preserve-state [--store-dir <dir>]\ncotal down manager [delivery auth web nats ...]\ncotal down web [--space <name>]\ncotal down -f <cotal.yaml> | --run <id> [--dry-run]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--file <cotal.yaml>`, `-f` | none | Tear down this manifest's deploy |\n| `--run <id>` | none | Tear down one `spawn -f` run by id |\n| `--space <name>` | current mesh | With components: the mesh whose target-addressed components (e.g. `web`) to stop |\n| `--dry-run` | off | Print the manifest teardown or selected components, mutate nothing |\n| `--preserve-state` | off | Bare whole stack only: fence the manager, retain principals and durable state, stop and prove the stack down, then publish `ready` |\n| `--store-dir <dir>` | `.cotal/nats` | With `--preserve-state`: the actual store path (required for a custom store) |\n\nBare `cotal down` stops the whole local stack in dependency order. Positional component names stop\nonly those self-registered local processes; for example, `cotal down manager` leaves delivery and\nthe broker running, and `cotal down web` is available when the web extension is installed. A\ncomponent that starts target-resolved (the web dashboard) is stopped the same way: `cotal down web`\nresolves the mesh the same way as `cotal web` (registry current mesh first, `--space` to name one), so\nit works from any directory; the other components always stop under the folder you run it in. The\n`-f` / `--run` forms tear down a [manifest deploy](#manifest-deploys) without stopping the whole mesh\nand cannot be combined with component names. Stopping `nats` alone is refused while an unselected\nregistered daemon is still live; include those components or use bare `cotal down`.\n\nNormal `down` remains destructive at the logical identity/durable layer. `--preserve-state` is a\ndifferent maintenance transition: it suppresses leave/deprovision cleanup, persists the manager's\nsame-principal resume inventory, stops the entire stack without removing run/auth artifacts, and\npublishes a stable inode-bound cut only after every recorded process is proven stopped and the exact\nrecorded NATS endpoint is unreachable. A missing or stale broker pidfile never counts as stopped. The\nattempt is bound durably before the manager is fenced, the resume document and attempt-bound\n`cut-intent` are fsynced before manager commit, and the manager's commitment itself is journaled\n(`cut-committed`) before any process stops. A retry after a crash at any of those boundaries reuses\nthe exact recorded attempt and finishes the remaining stop and endpoint proofs idempotently, without\nneeding the (by then intentionally dead) manager. A partial cut never publishes `ready`. It cannot\nbe combined with component names, manifest teardown, or `--dry-run`.\n\n## clean\n\n```bash\ncotal clean <history|store|all> --force\ncotal clean restore-attempt --attempt <id> --force\ncotal clean restore-fallback --attempt <id> --force\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | `history`: target mesh |\n| `--dms` | off | `history`: also clear DM history |\n| `--store-dir <dir>` | `.cotal/nats` | `store`/`all`: JetStream store directory |\n| `--force` | none | Required: destructive, no prompting |\n| `--attempt <id>` | none | `restore-attempt`: exact stale pre-commit attempt; `restore-fallback`: matching healthy committed restore |\n\nOne configurable cleanup verb; every target requires `--force`.\n\n- `history` purges the retained message backlog on the **running** broker (channels, plus DMs\n with `--dms`). The same operation as [`history clear`](#history), which stays as an alias.\n- `store` deletes the **stopped** mesh's JetStream store (`.cotal/nats`): streams, durable\n consumers, and messages. This is the reset for stale on-disk broker state, e.g. durables\n minted by an older, incompatible Cotal generation surviving a `down`/`up` cycle.\n- `all` is `store` plus the space identity (`.cotal/auth`), the local creds and markers tied to\n it, any crash residue a normal `down` would have swept (stale pidfiles, `run/`), and the mesh's\n registry entry; the next `cotal up` mints a fresh identity.\n\n`history` needs the mesh up; `store` and `all` refuse while any recorded mesh process is still\nalive or any same-root recorded broker endpoint remains reachable (run `cotal down` first). They\nalso refuse outright on a root that holds accounts for several spaces: the store and the broker\ntrust record are shared by every space on the broker, so both targets would take out all of them\nand no `--space` can narrow that. `down`, `backup` and `up --restore` refuse there for the same\nreason. `cotal status` lists the tenants on such a root. Personas\n(`.cotal/agents`) and logs are never touched. A custom\nstore location is not recorded anywhere, so `--store-dir` must repeat whatever the mesh was\nlaunched with. Custom cleanup targets must contain either the Cotal store-generation marker or a\nreal `jetstream/` store directory; filesystem roots, project roots, and Cotal auth/maintenance trees\nare always refused.\n\n`store` and `all` also refuse every maintenance journal state. After a healthy committed restore,\n`restore-fallback` is the only supported way to remove the recorded unchanged old-store inode; it\nnever deletes the active target, requires both the exact attempt id and `--force`, and retires the\ncompleted restore journal so a later `down --preserve-state` can start a new backup cycle.\n\n## Backups\n\n```bash\ncotal down --preserve-state [--store-dir <dir>]\ncotal backup create <dir> [--only full|registry] [--store-dir <dir>]\ncotal up --restore <dir> [--restore-only registry] [--accept-missing-source]\n```\n\nBackup is offline-only. It requires the stable `ready` record from `down --preserve-state`, an exact\nstore match, no live recorded process, and an unreachable exact endpoint from the recorded cut.\nThat endpoint is probed immediately before cloning, so a live broker with a missing or stale pidfile\nis still refused. It claims the cut, reflink/copies the stopped source to a\nprivate attempt clone, and opens only that clone on a random loopback bootstrap broker with an\nindependent parent/deadline watchdog. It validates the canonical stream and pull-consumer inventory,\nwrites native snapshots with consumers excluded, and stores conservative contiguous ACK-floor\ncheckpoints separately. The original store is never opened by the backup broker, and the stack is\nnot restarted implicitly. Artifact destinations must not overlap the preserved source or maintenance\nattempt tree. Restore artifacts and targets likewise cannot nest inside or contain each other, the\npreserved source, or the maintenance attempt tree.\n\nStopped client-managed KV ordered consumers are ephemeral read residue, not backup state. Backup\nignores only the pinned client's exact stopped shapes: ordinary last-value watchers and the\nwhole-bucket scanner that uses all-history delivery to collapse concurrent tombstones. A bound\nconsumer or any lookalike with a different filter, inbox, lifetime, or other config is still refused.\n\n`full` is the default and indivisible: channel registry, CHAT/DM/TASK/INBOX/DLV, ACL, MEMBERS, and\nvalidated durable checkpoints. `registry` is the sole partial artifact. Presence, derived membership\nfeed, leases, native ephemeral/history consumers, credentials, keys, tokens, owner secrets, and actor\nledger files are excluded. `full` means every transferable message and registry stream, not every\nJetStream resource: endpoint submissions/facts/events/timers/workflow state, contract artifacts, and\nthe records/auth/session stores are nonportable control state. Restore recreates those streams empty\nwith their canonical configs before exposing the normal listener, so active endpoint runs,\nlifecycles, and sessions do not cross a backup. Artifacts are exclusively created `0700`;\nsnapshot/checkpoint files and\nthe manifest are `0600`; `manifest.json` is written last with exact sizes and SHA-256 values. The\ndirectory is trusted operator input: hashes detect corruption, not malicious rewriting.\n\nRestore validates and stages the exact allowlisted artifact bytes before moving or creating a store.\nIt requires the same space and existing trust state. The whole pre-commit window holds a journaled\nliveness claim (coordinator, watchdogs, brokers, absolute deadline): ordinary `up` and a repeated\n`up --restore` refuse while the claim is live, and a stale attempt is recovered only after the\ndeadline has elapsed and every recorded owner is proven dead. A retried `up --restore` handles this\nautomatically; an operator can also recover it explicitly with `cotal clean restore-attempt --attempt <id> --force`. Nothing\never rolls back a live attempt. A registry-only artifact restores as registry-only whether or not\n`--restore-only registry` is passed; omitted infrastructure is always created and the exact\npost-restore stream inventory is asserted before commit intent. Ordinary `up` from a preserved cut\nresumes only the exact recorded source store and runtime; a contradicting `--store-dir` or\n`--runtime` fails in preflight. Authenticated restores validate the complete\nspace trust bundle before staging, including nkeys, seed matches, JWTs, signers, and space binding;\nfull restores commit to the validated operator, system-account, data-account, and active-signer root\nchain in addition to the static/user authority fingerprint. Because the system account is part of that\ncommitment, a [`cotal up --rotate-sys`](#up) makes every full artifact taken before it unrestorable\nagainst this root: take a fresh full backup after each rotation. The composed commitment is revalidated\nimmediately before store mutation and never includes secret seeds. Restore never creates fresh auth.\nSame-path restores atomically retain the old\nsource at the journaled fallback path; alternate targets retain it in place; a missing canonical\nsource needs explicit `--accept-missing-source`. Quarantine and target restores use current canonical\nconfigs on isolated random-loopback brokers, never expose native snapshot consumers, and publish a\ncommit-intent immediately before the normal listener starts. Archive bytes never instantiate the real\ntarget: after quarantine validation, every stream is re-snapshotted from the validated quarantine\nstate into attempt-owned sanitized files, and the target is restored solely from those. Before that boundary, failure rolls back\nthe attempt-owned target; after it, ambiguity preserves both stores and records forward-repair\nrecourse. The cooperative maintenance lock excludes Cotal commands, not arbitrary raw NATS processes.\n\nBootstrap brokers in every auth mode, including open, mount the store under a local account with\nrandom operation-specific logins only, each carrying the exact per-phase subject permission matrix;\nnormal static credentials and user-auth sentinel/bearer connections are rejected, and no auth\nservice or callout starts. Open mode differs only in its account label, never in authority. Inventory, each stream snapshot,\nrestore initiation, exact upload id, validation, and each checkpoint recreation use separate exact\nauthorities. Every checkpoint carries the source stream's message/first/last sequence state and must\nmatch its snapshot record before mutation; core then derives and validates the only allowed start\npolicy. TASK is not a CLI exception: the same core checkpoint API recreates its canonical `DeliverAll`\nWorkQueue durable because acknowledged tasks are absent from retention and NATS forbids a\nstart-sequence policy there. Registry-only restore creates every omitted canonical stream and transient\nbucket on the isolated target before the normal listener is exposed. It deliberately does not resume\nretained agents or recreate their DM/DLV/TASK/ACL state; their identity material stays retained and\nstopped rather than being reprovisioned into a partial restore.\n\nAfter listener readiness, the manager starts attempt-bound, validates retained credentials/tokens\nwithout granting or reprovisioning, and resumes the exact persisted principals under cleanup\nsuppression. Registry-only restore uses the same flow with an empty agent set. `commitResume` is an\nidempotent validation barrier only: success must be `awaitingFinalize` with an attempt-bound 64-hex\ncommit token and does not release suppression. Under the workspace lock, the CLI first fsyncs that\nexact evidence as `manager-committed` (restore) or `resume-committed` (ordinary resume), then calls\ntoken-bound `finalizeResume`; only an `active` response for the exact token releases suppression. The\nCLI records the same token in finalization evidence before a restore becomes `active`, or before an\nordinary resume retires and consumes the marker. Re-entry from either committed state skips the prior\nidempotent activation/commit phases, retries finalization with the durable token, and finishes the\nworkspace transition. Failure before finalization preserves the committed state and cleanup\nsuppression; it is not rewritten through a degraded transition. Re-entry between any two earlier\nboundaries reuses the same attempt and may retry the idempotent phases without deleting retained state. A missing or\nchanged per-agent dependency is a named fail-closed result; the journal becomes degraded and remains\navailable for forward repair. A retry from `resume-intent`,\n`resume-active`, or `resume-degraded` reuses the same attempt and inventory after the prior listener is\nproven stopped. Every normal restore listener has an unguessable attempt-bound NATS server name. The\nCLI fsyncs its exact name/nonce, canonical endpoint, process owner, and generation-bound target identity\nimmediately after spawn. Re-entry accepts a surviving listener only when its INFO server name, live PID\nrecord, endpoint, and target identity all match that proof; degraded restore repair then moves through\nthe guarded workspace transition only after manager commit. If an uncommitted bound owner is provably\ndead, recovery retires that exact proof under the maintenance lock and binds a fresh listener for the\nsame attempt, endpoint, and target with a new nonce and server name. A live foreign/mismatched listener\nor ambiguous owner is preserved and refused, never adopted by reachability alone. A reconstructed\ncommit/degraded attempt without either the exact bound proof or a durable dead-listener replacement\nrecord fails closed even when the recorded port is free. A later ordinary startup may pass an `active`\nrestore only when its details prove manager commit and its exact recorded listener is dead.\n\n## Mesh registry\n\n```bash\ncotal meshes\ncotal meshes add # guided, on a terminal\ncotal meshes add <space> --server <url> [--root <dir>] [--mode auth|open|user] [--tls] [--force]\ncotal meshes add <space> --mode user (--user-auth-file <bundle.json> | --from <https url>)\ncotal meshes rm <space> [<space> \u2026] [--force]\ncotal use <space>\ncotal status [--space <s>] [--server <url>] [--components]\n```\n\n`meshes` lists the meshes this machine knows; a `*` marks the `current` default a bare\n`cotal spawn` joins.\n\nRun on a terminal with the space or `--server` missing, **`meshes add` is guided**: it asks for the\none thing that cannot be derived (the broker URL), probes it, and tells you what answered - open or\nrequiring credentials. It then offers the spaces your `--root` already holds credentials for, states\nthe mode as a fact about that broker rather than asking, and shows the exact record before writing\nanything. A broker that does not answer, or a space name already registered, becomes a choice rather\nthan an error. Anything you pass on the command line is taken as given and not asked again. Without\na terminal - a script, an agent, CI - nothing prompts and the flag form's errors stand\n(`COTAL_NO_PROMPT=1` forces that too).\n\n`cotal up` and `cotal down` maintain their own records. `meshes add` registers a mesh they cannot\nspeak for: one running on another machine, a shared broker, a hosted space. `--root` is the folder\nwhose `.cotal/auth` holds that mesh's credentials and whose `.cotal/agents` holds its personas.\nThe default is the project you run it in. The registry stores that path, never a secret. `--mode`\ndefaults to `auth` when the root holds the space's account record and to `open` otherwise. The\nbroker is probed before anything is recorded, so a wrong address, or credentials that mesh will\nnot accept, fails here instead of at the first `spawn`; `--force` records without verifying (and\nreplaces an existing record).\n\nA hostname or public address is registrable only when the connection will **require TLS**. Pass\n`--tls`, or use a `tls://` URL. The scheme is recorded as enforced intent, so every later dial\nthrough the record demands the handshake (and `meshes add tls://\u2026` against a plaintext broker is\nrefused at registration). Without required TLS the fence admits loopback and private-overlay\nliterals only. RFC1918 addresses are refused in both modes because a cafe LAN is private but does not belong to you.\n\nA **user-auth** mesh registers from supplied pinned trust, never guessed: `--user-auth-file`\ntakes the bundle exported where the mesh runs; `--from` asks before it dials the address at all,\nthen fetches its `/.well-known/cotal-mesh` discovery document (HTTPS only), displays the pins, and\nasks again before adopting them. Neither fetch follows redirects: a 302 can move a pinned fetch\nonto plaintext or onto another host, so it is refused rather than followed, and the pinned\nexchange must itself be an `https://` URL, except for an exchange on this machine, where plain\n`http://` is accepted for a loopback *literal* (`127.0.0.1`, `::1`, any spelling of them) but not\nfor `localhost`, which is a name rather than an address. Registration verifies that the exchange\nanswers `/health` and `/jwks` as the pinned issuer. It also verifies that the broker refuses a bare\nconnect; that auth-required refusal is the pass. The sentinel credentials land in a 0600 file under\nthe entry's root; the registry records only the path.\n\n`meshes rm` drops records. It never stops a mesh. For a mesh running on this machine `cotal down`\nis the right verb, and `rm` says so unless you pass `--force`. A hand-added record is removed by\n`meshes rm`, by an `add --force` replacement, or by a `cotal up` that actually starts the broker for that same space, server and root, which becomes that\nmesh and so takes the record over (a `cotal up` for that space anywhere else refuses instead).\nNothing that merely *infers* a record is stale touches it: an\nunreachable broker is listed `offline` and stays, and `cotal down` / `cotal clean all` leave it\nalone even when it shares a root with the project they are tearing down, because nothing on this\nmachine could write it back.\n\n`use <space>` sets that default; the selection applies from every directory,\nincluding inside another mesh's project. `status` is a read-only report: machine prerequisites\n(starting with the installed `cotal-ai` version), the installed extensions and their versions, this\nfolder's `.cotal/`, the recorded meshes, and a live snapshot of the selected mesh (roster, channels,\nmembership feed). Stale Claude skills and out-of-date `.agents` skills recommend `cotal setup --skills`,\nnot unscoped `cotal setup`. `status` takes `--space` / `--server` to pick the mesh to inspect; it starts\nnothing.\n\n`cotal status --components` adds a fail-loud per-component health pass. It reads **each\ncomponent's own control surface**, rather than treating a PID, a lease, or a successful probe of a\nsibling as proof that the component serves. It prints one of `serving`, `absent`, `not-serving`, or\n`refused` for each component and exits `0`, `1`, `2`, or `3` respectively (the highest observed\nstate wins):\n\n- **manager**: local PID record, its liveness-lease holder and PID, then the manager's own typed\n `status` service reachability from this host. Builds without a startup-phase report say\n `phase not reported by this manager build`; that is never a blank green state.\n- **delivery**: local PID record, its ready lease (`ready` is the daemon's own bound-control\n signal), and the latest `renewal.json` adoption verdict. A re-signed credential and a\n broker-accepted adoption stay distinct facts.\n- **web**: local PID record and the dashboard's own loopback `/api/meta` response, which must name\n the same PID and its requested port. A different process on the port, an unreadable PID command,\n or an unrecognizable process record is `refused`, not a green default-port guess.\n- **broker**: the registered mesh URL dialed from this host with its recorded TLS requirement.\n\n`absent` means Cotal has no live local component record (or has a stale record); `not-serving`\nmeans the component record is live but its service/readiness surface did not answer or is not ready.\nThose are intentionally separate exit cases. A failed or unreadable probe is `refused`, never an\nabsent component or a clean zero.\n\n## spawn\n\n```bash\ncotal spawn [<persona>] [--detach] [--name <n>] [--agent <a>] [--model <m>] [--variant <v>] [--prompt <text>] [--cwd <dir>]\ncotal spawn -f <cotal.yaml> [--dry-run]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | resolved mesh | Target space |\n| `--server <url>` | registry entry | Broker URL override |\n| `--creds <path>` | none | Control-caller creds for an off-registry manager (`--detach` only) |\n| `--name <n>` | persona's `name:` | Presence-name override (does not choose the persona) |\n| `--config <persona-or-path>` | none | Persona catalog name or file path; wins over the positional |\n| `--agent <a>` | persona's `agent:`, else `COTAL_DEFAULT_AGENT`, else `claude` | Connector type (`claude`, `opencode`, `jcode`, `hermes`, and so on) |\n| `--role <r>` | persona's `role:` | Role override |\n| `--model <m>` | persona's `model:` | Model override |\n| `--variant <v>` | persona's `variant:` | Model variant override (connector-defined; e.g. OpenCode reasoning tiers) |\n| `--cwd <dir>` | this cwd | Working directory to root the agent at |\n| `--prompt <text>` | none | Initial prompt auto-submitted at start |\n| `--resume <id>` | none | Fork an existing session id into the mesh; only connectors that declare resume support accept it (see [the matrix](connectors.md)) |\n| `--events` / `--no-events` | off | Publish the session's structured event plane to its own event channel |\n| `--share-tools <sel>` | none | Share named operator MCP servers with the agent |\n| `--subscribe <a,b>` | persona's | Channel read-set override |\n| `--allow-subscribe <a,b>` | = subscribe | Read-ACL override |\n| `--allow-publish <a,b>` | deny | Post-ACL override |\n| `--detach`, `-d` | off | Launch via the manager into a detached PTY (reattach with `cotal attach`) |\n| `--on <instance>` | class anycast | With `--detach` only: pin the launch to one manager instance id (the whole id, as `ps` prints it). Refused on a foreground spawn (no manager to pin), with `-f` (a manifest deploy launches through the manager class queue), and when empty |\n| `--file <cotal.yaml>`, `-f` | none | Deploy a manifest onto the running mesh |\n| `--dry-run` | off | With `-f`: print the plan, mutate nothing |\n| `--allow-stale <a,b>` | none | With `-f`: waive named stale agents (apply-only) |\n| `--runtime <name>` | manifest's | With `-f`: override the manifest's runtime |\n\n`--events` turns on the session's **event plane**: a stream of structured events describing what\nthe agent did, rather than the prose it wrote, on a channel of its own. The channel is named after\nthe agent's principal, `events.<owner>.<actor>`, never after its display name, because two live\nagents are allowed to share a display name and would then share a stream. The launch grants publish\nrights on that channel alone, foreground and detached alike, and a connector that does not\npublish an event plane refuses the flag rather than starting a session whose events have nowhere to\ngo.\n\nThe flag and the grant are separate on purpose. Holding publish rights on a channel is not a request\nto publish to it, so writing an event channel into an agent file's `allowPublish` does not turn the\nplane on: only the launch does.\n\nThe persona (`--config` > positional > `COTAL_DEFAULT_PERSONA` > `default`) is loaded from the\ntarget mesh's `.cotal/agents/`; the launch flags override the file. Foreground runs the agent\nattached to your terminal; `--detach` hands the launch to the running manager. Both modes get the\ndurable backstop on a mesh that runs the delivery daemon; `--live-only` skips it for a foreground\nspawn (messages posted while it is disconnected are then not replayed). A foreground exit retires\nthe agent's creds and broker footprint, like a manager despawn. A `--detach` spawn is an\n**action**: the manager accepts it and returns the allocated identity at once, then the launch\nfollows to a terminal outcome rather than blocking (see [the control surface](control-surface.md)).\nSee [Connect Claude Code](connect-claude.md) and [Agent files](agent-files.md); `-f` is a\n[manifest deploy](#manifest-deploys). (`cotal start` was merged into `cotal spawn --detach`.)\n\n## models\n\n```bash\ncotal models [--agent <connector>] [--refresh]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--agent <connector>` | all registered connectors | Connector whose catalog to list |\n| `--refresh` | off | Ask the connector to refresh its provider cache |\n\nAsks the running manager for each connector's model catalog (model ids plus their variants)\nfor connectors that expose one. OpenCode and Codex query harness/provider surfaces; Jcode reads\nproviders that enable `model_catalog = true` in the operator Jcode `config.toml`. Jcode's listed\neffort tiers render as `variants (declared, not provider-verified)`, and launch can still refuse one.\nA connector without a catalog says so. Pick a result with `cotal spawn --model <id> --variant <v>`,\nwhere `<id>` is the model id as the catalog printed it. OpenCode and Codex ids are the full\n`provider/model`; Jcode ids are bare (`opus-5`, not `cliproxy/opus-5`), because the provider is\nselected by the operator's Jcode config and a prefixed id is refused at launch with the bare form\nnamed.\n\n## endpoints\n\n```bash\ncotal endpoints [--space <s>] [--server <url>] [--creds <path>]\n```\n\nLists the mesh presence roster: agents, the manager, and any other protocol endpoint, with each\nendpoint's role, kind, status, and current activity. Unlike `ps`, this is a read-only presence view;\nit is not limited to child processes owned by the manager.\n\n## Endpoint control\n\n```bash\ncotal describe <endpoint> [--space <s>]\ncotal invoke <endpoint> <command> [--args '<json>'] [--space <s>]\ncotal invoke <endpoint> <command> --name <agent> [--admin] [--space <s>]\n```\n\nThe generic v0.4 service surface. `describe` resolves a registered endpoint's command set off the\nwire - the reserved `describe` command answers the registered contract digests, the schemas are\nfetched from the space's content-addressed contract store, recompiled, and verified against those\ndigests - and prints each command with its capability class and targeting shape. `invoke` calls one\ncommand by name: `--args` is a JSON object validated against the fetched input schema *before*\npublish; a targeted command takes `--name <agent>` (resolved to the agent's current principal via\n`ps`) or `--self`. `--admin` uses the admin instrument credential, whose cross-agent reach rides\nthe operator-only `any` authorization mode. Neither command has compile-time knowledge of any\nendpoint's schemas - this is the same trust chain every built-in control command now uses. Needs an\nauth mesh: the manager registers its service on both static and per-user meshes (a signed-in user\nrides their bearer; cross-agent reach needs the `admin` scope). An open mesh has no service\nregistry.\n\n## Managed seats\n\n```bash\ncotal ps [--on <instance>] [--wide | --json] [--space <s>]\ncotal stop --name <n> [--on <instance>] [--space <s>]\ncotal attach --name <n> [--on <instance>] [--no-reconnect] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--name <n>` | none | Managed agent to stop / attach (required) |\n| `--on <instance>` | class anycast (`ps`: class scatter) | Pin to one manager instance id (multi-manager space); takes the whole id as `ps` prints it, not a prefix. An empty value (`--on \"\"`, an unset shell variable) is refused, never treated as absent |\n| `--wide` (`ps`) | off | After each seat's compact row, print extra operational facts the manager records: `cwd`, `pid`, spawner, lifecycle uid, and the owning manager's instance id and host. Model and requested variant stay in the identity row rather than printing twice. A fact the manager did not record (for example a runtime with no real process) prints nothing, never a placeholder |\n| `--json` (`ps`) | off | Machine-readable: one JSON object per seat per line, copied unchanged from the manager row. Instance headers and errors go to stderr, so stdout contains only rows. Mutually exclusive with `--wide` |\n| `--no-reconnect` (`attach`) | off | End the attach when its session ends, instead of re-establishing it. For scripts that want one run and one exit code |\n\nThe human `ps` row is presentation text and is not a stable parsing target. Scripts use `--json`,\nwhich is the machine-readable row contract.\n\nThese are operator clients over the running manager's control plane. The default row includes the\nconnector, model pin, optional requested variant, and runtime as operational descriptors for the\nmanaged row. They do not make a shared display name a unique protocol identity; use `--json` when\nunambiguous owner+actor attribution is required. An omitted variant means no override was requested;\nCotal does not invent an effective provider default it cannot observe. `ps` also prints two state\nfacts per managed agent, because they answer different questions: the process fact from the manager's\nown runtime handle (`running` with its uptime, or `exited` with how long it ran), and the mesh fact\nfrom the roster (`idle` / `working` / `waiting` / `mesh offline`, or `not in roster` when the seat has\nno presence row at all: a seat that has not joined yet, or one that never did). A seat can be\n`running` and `mesh offline` at once: the process is alive and its presence has lapsed. On a user-auth\nmesh `ps` also renders each managed agent's last credential-refresh outcome, fail-closed.\n\n**Mode split (chosen up front, never try-scatter-then-degrade):**\n\n- **Static / open mesh.** Bare `ps` is a **class scatter**: it freezes the live manager class from\n the records registry, merges every registered instance's agents grouped and attributed per\n instance, and a non-answering instance is shown as `registered, no answer within the deadline`\n (never silently omitted). That label is the whole claim: the instance is registered and did not\n answer. It does not say the host is down, because a dead host never deregisters itself and a\n live one can be slow; if it is gone, deregister it.\n `--on <instance>` pins the read to one exact instance id instead. A wrong pin fails loud\n rather than falling through: a well-formed id that no live manager carries is reported as\n `manager instance <id> did not answer` (nothing else is asked), and a credential without that\n instance's rail is reported as refused by the broker, not as an unresponsive manager. A manager\n that answers with a refusal is shown with its own cause; \"no manager reachable\" is said only when\n nothing answered at all. If the scatter's own registry read fails (the freeze or the reconcile),\n `ps` says the manager registry could not be read rather than pronouncing on the managers, which\n may all be up.\n\n**`stop` and `attach` route by seat locality.** A seat can only be stopped or attached by the\nmanager actually running it, and the class queue does not know which one that is. So on a\nstatic/open mesh both verbs first ask every registered instance which one hosts the named seat, then\naddress that instance directly. This happens by default; you do not need `--on`.\n\n`--on <instance>` remains the override, for when you already know where the seat lives or the\nlookup itself is degraded. It is also the **only** route on a **user-auth mesh**: a ledger-scoped\nbearer does not hold the registry-read rows the lookup needs, so there the verbs stay on the class\nqueue unless you pin them yourself.\n\nIf no reachable instance has the seat, the error reports how many managers answered and names\nthose that did not. It does not collapse that state into a bare `no agent <name>`. That distinction matters\nbecause a single manager cannot tell \"hosted elsewhere\" from \"does not exist\": it answers\n`not-found` for both.\n- **User-auth mesh.** `cotal ps` reports what **one** manager knows about your agents (an `ep.one`\n read against the manager's in-memory roster, owner-filtered). It does **not** report other\n manager instances. It cannot tell you that one is down: an unreachable manager is absent\n from the list. Completeness across a multi-manager user-auth space is not claimed.\n A manager that does not answer fails the command outright (exit non-zero), rather than printing\n an empty list that could be read as \"no agents\". Your ledger row needs the `admin` scope to\n reach `ps` at all; `spawn` alone is refused by the broker (the ep tier boundary).\n\n`attach` streams and drives an agent's terminal on the `pty` runtime; detach with the escape key\n(Ctrl-] by default; see [`COTAL_DETACH_KEY`](config.md)). It does so over a one-use, holder-bound\nmesh session ([SPEC](../SPEC.md) \xA713.6): the manager replies with a signed session grant (never a\n`127.0.0.1` URL), the CLI redeems it once over the broker, and the browser console (`cotal console`)\ndrives the same session. `stop` and `attach` need a running manager to talk to. On a static mesh\nthey are cross-agent admin operations. On a user-auth mesh, your own agents (any agent under your\nowner) need only the `spawn` scope; another owner's agent needs `admin` on your ledger row\n([identity & auth](identity-and-auth.md)). Launch detached agents with [`spawn --detach`](#spawn).\n\n**`attach` reconnects when the link dies.** A session lives on a network link, and a laptop that\nsleeps, a VPN that drops or a wifi handover kills it. When that happens `attach` prints\n`[cotal: connection lost, reconnecting]` on stderr and starts asking the manager for a new session:\na fresh grant, a fresh per-session credential, a fresh connection, so every attempt re-runs the same\nauthorization the first attach did. On success it prints `[cotal: reconnected]`, the manager repaints\nthe seat's current screen the way it does for any attach, and you carry on in the same terminal.\nRetries wait 1s, 2s, 5s, 10s, then 30s, for as long as the seat exists. The detach key is read the\nwhole time the loop runs, the waits and the attempts alike, so a reconnect never traps you: press it\nwhile a session is being established and the attach ends there, and a session that lands behind the\npress is handed back to the manager rather than left holding a slot. Everything else you type while\nthere is no session is dropped rather than queued, so keystrokes aimed at a terminal that turned out\nto be frozen, Ctrl-C included, are not delivered to the agent by a reconnect you did not know had\nhappened. That starts before the first session, not at the first reconnect: at a terminal, `attach`\nreads and drops what you type while it is still resolving the mesh, so a key struck at a prompt that\nhas not come up yet does not reach the agent when it does.\n\nA **pipe** carries script input. For example, `printf 'ls\\n' | cotal attach --name web` is\nbuffered until the session opens. Buffering continues across reconnects, so\n`tail -f log | cotal attach --name web` does not lose the part of its feed written while the link was\ndown. Only a terminal gets the reader; `--no-reconnect` keeps the old behaviour on both.\n\nIt stops on its own when reconnecting cannot help, and says why: a manager that refuses the attach\nexits non-zero with the manager's own message, and a reconnect that finds the seat no longer there\n(despawned, or its agent exited while the link was down) exits cleanly with `seat <name> is gone`.\nA refusal that could still pass, such as a manager at its session ceiling, is relayed in the\nmanager's own words while the loop keeps trying, once per refusal rather than once per attempt.\nPressing the detach key, or the agent's process exiting while you are attached, ends the attach as\nit always did. `--no-reconnect` turns all of this off and restores the single-session behaviour,\nwhich is what a script wants.\n\nEach reconnect also hands the abandoned session back to the manager, over the first link that can\ncarry the message, so an attach that flaps does not eat the manager's session slots one outage at a\ntime. If that message never gets a link, the attach says so when it ends.\n\nWhich mesh `attach` resolves also decides **whose trust it redeems with**. Redeeming a session grant\nmeans minting a short-lived, session-scoped credential from the space's seed, and that seed comes\nfrom the root the mesh resolved to, never from a `.cotal` found by walking up from whichever\ndirectory you happen to be standing in. The difference is not hypothetical: `~/.cotal` exists on\nevery install because the mesh registry lives there, so a command run anywhere under your home\ndirectory but outside a project used to mint from your home directory's trust and present it to a\nbroker that trusts a different chain, which surfaced as a bare authorization failure that named\nnothing. A directory that does hold another chain for the same space is now reported on the way\npast, and not obeyed:\n\n```text\n! this directory resolves to /Users/you, whose .cotal/auth holds a DIFFERENT trust chain for space \"team\".\n attach used /Users/you/projects/app, the root this mesh resolved to. The other one is not being used, and is worth a look.\n```\n\nWhen the resolved mesh holds no seed at all, `attach` refuses and names what it resolved, the broker\nand the root, instead of describing a directory it did not use.\n\nTerminal bytes stream over the mesh; the manager's own HTTP/WS face serves the console. That endpoint binds\n**loopback by default**, so nothing is exposed by accident; `cotal up --host <addr>` passes its bind\naddress down, which is what lets you attach to an agent whose manager runs on another machine. A\nbare `cotal supervise` and an embedded manager stay machine-local. Set it directly with\n`supervise --console-host <host>`.\n\nThat address is **recorded on the mesh** and carried forward, because it is a decision rather than\nsomething later commands can work out for themselves (a broker dial address is not a manager bind\naddress). Every later manager launch for the same mesh reuses it, including a same-root `cotal up` repair,\nan adopted preserved or restored listener, and a `spawn -f` manifest deploy. A manager replacement\ndoes not quietly move a reachable attach face back to loopback. Passing `--host` again overrides it,\nso you can widen or narrow exposure whenever you like; a mesh that never asked stays loopback-only\nand records nothing.\n\nBecause that face carries terminal read and write for every managed agent, it is credentialed in two\ntiers. A mesh caller receives a **ticket** bound to the single agent the manager just authorized,\nsingle-use and short-lived, so one authorized attach can never be re-pointed at someone else's\nagent. The **console token** is the operator's own, reaches every agent, and is printed only to the\nmanager's output. The roster, the live feed, and the PTY stream all answer `401` without one; the\nstatic console shell is served openly, since it describes no agent.\n\n## input\n\n```bash\ncotal input --name <n> --text <text> [--no-enter] [--on <instance>] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--name <n>` | | Managed agent to type into (required) |\n| `--text <text>` | | The text to type, taken verbatim (required) |\n| `--no-enter` | off | Type the text and stop there, without pressing Enter |\n| `--on <instance>` | class anycast | Pin to one manager instance id using the same rules as [`attach`](#managed-seats) |\n\nTypes one line into a running agent's terminal, as if you had typed it there, and returns. This is\nthe half of [`attach`](#managed-seats) that a program wants: `attach` is a live stream that holds a\nsession open and expects a terminal on your side, so a script, a cron job or a web UI cannot use it\nto send a single line. `input` is one authorized call.\n\nWhat it is for is **harness commands**. A line beginning with `/` is not chat and not a message: it\nis something the agent's own harness handles, and the only way in is the keyboard.\n\n```bash\ncotal input --name reviewer --text \"/compact\" # ask the harness to compact its context\ncotal input --name reviewer --text \"/model opus\" # switch its model\ncotal input --name reviewer --text \"hold on that PR\" # ordinary typing works too\n```\n\n**Quoting.** `--text` takes a value, so a payload starting with `/` survives as written. A payload\nstarting with a dash needs the `=` form, because the shell-style `--text --foo` is ambiguous and is\nrefused rather than guessed:\n\n```bash\ncotal input --name reviewer --text=--verbose # dash-leading text: use --text=<value>\n```\n\nEnter is pressed by default, since a command typed but never submitted has not been delivered.\n`--no-enter` types the text and leaves it sitting at the prompt, which is how you stage a line and\nsend it later.\n\nNothing comes back but a delivery receipt (`\u2713 sent 9 bytes to reviewer`, counting the trailing\ncarriage return). Whatever the agent does next shows up where its output already goes: the mesh, its\ntranscript, or an `attach`.\n\n**This one is operator-only, and more narrowly than `stop` or `attach`.** Those two are granted to\nanything holding `spawn`, so an agent can stop and attach to seats under its own owner. `input` is\nnot: it is granted only to operator credentials, which on a user-auth mesh means your ledger row\nneeds the `admin` scope, the same scope [`ps`](#managed-seats) already needs there. The reason is\nthat a write into a terminal is control of whatever is running in it, and on a user-auth mesh the\nown-owner rule covers every seat under you, not only the ones you launched: a `spawn`-scoped agent\ncould otherwise type into a sibling it never started. Seat locality is still resolved for you.\n\nOnly the `pty` runtime can be typed into. The external terminal runtimes (`tmux`, `cmux`, `orca`,\n`herdr`) attach to a process they do not own, so they have no input stream for it and the command\nrefuses by name rather than dropping the keystroke.\n\n## personas\n\n```bash\ncotal personas list [-v] [--running]\ncotal personas show <name>\ncotal personas edit <name>\ncotal personas new <name> (--prompt <t> | --from <f>) [--role <r>] [--model <m>]\ncotal personas rm <name> --force\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which mesh's persona catalog |\n| `--role <r>` | none | `new`: the persona's role |\n| `--model <m>` | none | `new`: the persona's model |\n| `--prompt <t>` | none | `new`: the persona's prompt text |\n| `--from <f>` | none | `new`: seed the prompt from a file |\n| `--verbose`, `-v` | off | `list`: include role / model / description |\n| `--running` | off | `list`: mark personas live on the mesh |\n| `--force` | none | `rm`: required, delete without prompting |\n\nPersonas are the local agent files under `.cotal/agents/` that `cotal spawn` launches. See\n[Agent files](agent-files.md) for the file format.\n\n## supervise\n\n```bash\ncotal supervise [--runtime <name>] [--space <s>] [--server <url>] [--spawn <names>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space to supervise |\n| `--server <url>` | hosting mesh, or matching registered mesh | Broker URL. A registered mesh supplies it when omitted; a different explicit value is refused. |\n| `--runtime <name>` | `pty` | Agent runtime (`pty` built in; extension runtimes are explicit-only) |\n| `--console-port <n>` | none | Protocol-console port |\n| `--console-host <host>` | loopback | Bind host for the console + attach endpoint. Loopback keeps it machine-local; `cotal up` passes the address it bound the broker to, which is what lets `cotal attach` reach this manager from another machine |\n| `--roster <file>` | none | Declarative roster to boot at startup |\n| `--launch <spec>` | none | Resolved manifest launch spec (from `up -f` / `spawn -f`) |\n| `--spawn <names>` | none | Comma-separated personas to pre-spawn at startup |\n\nThe manager is the agent supervisor and control plane: it answers `spawn --detach`, `stop`, `ps`,\n`attach`, and the `cotal_*` manager tools. `cotal up --detach` starts one for you; run `supervise`\ndirectly to recover a dead manager or drive a custom runtime. Default runtime is `pty`; install an\noptional provider first (`cotal ext add @cotal-ai/orca`, `@cotal-ai/tmux`, `@cotal-ai/cmux`, or `@cotal-ai/herdr`) and\nselect it explicitly. A missing provider or app fails loudly; there is no fallback. See [Deploy](deploy.md).\n\nOn a normal `SIGINT`/`SIGTERM`, the manager stops every seat and requires the selected runtime to\nprove the seat is gone before it releases the manager lease or service registration. A stop that\ncannot prove exit fails loud and keeps manager authority instead of reporting a clean shutdown while\nan orphan still holds broker rails. After an abrupt manager death, the same logical successor\nterminalizes only its own durable static slots, verify-evicts the predecessor's broker principal,\nrecords that result in the lifecycle's caller-readable audit detail, and only then retires the\nlifecycle and frees the alias. Missing or unverified broker evidence keeps the slot terminalizing.\nDelivery-admin does not terminate the orphan OS process; safe successor process reaping requires\ndurable process start-identity pinning and is tracked separately.\n\nA `meshes add --mode user` entry is a **participant** registration, not hosting authority. A\nparticipant may run `supervise` only when the host advertises the remote manager authority service\nand the signed-in actor has the dedicated `supervise` ledger scope. The CLI obtains the closed,\nloopback-only `manager-service` view; `spawn` and `admin` do not substitute for that scope. The\nhost issues the manager's public-nkey JWT material through its lifecycle-bound prepare \u2192 activate\n\u2192 renew protocol, never by handing the participant a signer or static provisioner credential.\n\nWithout that advertised host service or scope, `supervise` refuses before it starts a manager.\nRun `cotal spawn` without `--detach` to launch a foreground agent, or ask the space host to enable\nthe authority service and grant `supervise` for detached agents. If a running remote manager loses\nrenewal, it reports degraded state and refuses unsafe new starts and restarts; live agents are not\nsilently replaced. Do not run `cotal down` or `cotal up` on a participant machine to repair this\ncondition.\n\n## reconcile-gate\n\n```bash\ncotal reconcile-gate [--space <s>] [--server <url>] [--endpoint <e>] [--instance <id>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space the frozen gate lives in |\n| `--server <url>` | the local mesh | Broker URL |\n| `--endpoint <e>` | `manager` | Endpoint whose gate is frozen |\n| `--instance <id>` | this folder's persisted manager instance | Instance id |\n\n**When you need this.** A manager restart killed after deregistration begins but before the new\nincarnation finishes leaves the endpoint's issuance gate *frozen*, held by a\nprocess that no longer exists. The freeze is what stops two incarnations serving at once, which is\ncorrect. The successor manager now completes that dead registration itself on boot, using the same\nguard this command uses: it acts only when the freeze-holder is affirmatively gone under a complete\nCONNZ sweep (`gone` and `sweepComplete=true`), then abort-reopens the gate at generation+1 with\nprocessEpoch unchanged and continues the normal takeover. Live, unknown, unestablishable, and\nwrong-op-kind still refuse; there is no TTL.\n\nUse this command when the boot path cannot run: the delivery daemon is down, the repair targets a\nnon-manager endpoint, or you want to lift the freeze without starting a manager. It checks that the\nholder really is gone, prints what it found, and then finishes the dead operation the same way as the\ninterrupted restart would have: revoke the old credentials, evict their holders with verification,\nand reopen the gate.\n\nIf verification is interrupted, the command leaves the gate frozen and durably records each holder\nwhose eviction was already verified. A retry still repeats the freeze-holder liveness check, then\nskips only progress bound to the same registration operation, frozen-gate revision, and holder set.\nThe output reports holders completed before this attempt, completed now, and still remaining. A new\nfreeze or changed holder set starts from zero. Cursor cleanup happens only after reopen; a retained\ncursor is harmless because its old gate revision cannot authorize a later freeze.\n\n**It refuses far more often than it acts, on purpose**, and always says which check stopped it:\n\n| Refusal | What it means | What to do |\n|---|---|---|\n| `holder-alive` | The freeze-holder still has a live connection: a manager *is* running | Stop that process first. Reconciling would evict a live manager's credentials |\n| `holder-unknown` | The connection sweep could not prove the holder absent | Not safe to proceed: an unprovable holder is treated as a live one. Re-run once the broker answers completely |\n| `liveness-unestablishable` | The delivery daemon could not be asked at all | Start it (`cotal up` runs it) and re-run. Silence is never read as death |\n| `not-frozen` / `no-gate` | The gate is open, or there is no gate at that coordinate | Nothing to repair: check `--endpoint` / `--instance` |\n| `wrong-op-kind` | Frozen under a takeover or retirement, not a registration | Out of scope for this command; it will not reinterpret another operation's intent |\n| `eviction-unverified` | The holder looked gone but eviction could not be verified | The gate is left frozen, unchanged. Investigate the broker before retrying |\n| `raced` | A newer manager moved the gate mid-repair | Re-run `cotal doctor` and look again |\n\nThere is no `--force`, and no path that discards gate state: the only way this reopens a gate is by\nproving the holder is gone and then completing the operation properly.\n\n## deregister-instance\n\n```bash\ncotal deregister-instance [--space <s>] [--server <url>] [--endpoint <e>] [--instance <id>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space the instance is registered in |\n| `--server <url>` | the local mesh | Broker URL |\n| `--endpoint <e>` | `manager` | Endpoint the instance serves |\n| `--instance <id>` | this folder's persisted manager instance | Instance id, the whole id as `cotal ps` prints it |\n\n**When you need this.** The service registry records *registration*, not liveness, and nothing in\nthe model expires a row. A manager that stops cleanly removes its own registration. One whose host\ndied without writing anything cannot, so its record goes on claiming a live instance forever: every\nclass scatter in that space freezes the dead slot in, and `cotal ps`, `stop` and `attach` each pay\ntheir whole deadline waiting for a machine that is never coming back. A laptop that was reimaged, a\ncontainer that was deleted, a box that will not be back on the network: those registrations have no\nother exit.\n\nThis command is that exit. It asks the instance first, and it removes a record only when the broker\naffirms the instance's own rail is empty: nothing subscribed there. Then it deletes the\nregistration's two records keys, each pinned to the revision it read, and prints what it removed.\n\n**Silence alone never passes.** An unanswered describe is what a dead host, a wedged process and a\nslow one all look like, and a hung process still holds its subscriptions, so the broker sees\ninterest on its rail. That instance is refused and the observation is printed. A dead process holds\nno connection and therefore no subscription, so a real corpse is still removed.\n\n**Every refusal names the failed check:**\n\n| Refusal | What it means | What to do |\n|---|---|---|\n| `instance-answered` | The instance answered a pinned describe. It is alive | Nothing to repair. If it is wedged rather than gone, stop the process first; its own clean stop removes the record |\n| `instance-not-affirmed-gone` | It did not answer, and the broker did not report its rail empty, which is what a held subscription looks like: slow or hung, not affirmed gone | Nothing was removed. Stop the process; its record goes on its own clean stop, or re-run this once it is down |\n| `liveness-unestablishable` | The probe itself failed, so nothing was learned | Fix the probe's path (credential, broker) and re-run. A probe that could not run is never read as death |\n| `not-registered` | No registration at that coordinate | Check `--instance` and `--endpoint`. This takes the whole id, never a prefix |\n| `superseded` | The record moved between the read and the delete | Something is writing to it. Nothing was removed; re-observe before retrying |\n\nThere is no `--force` and no sweep: silence is not death, and a rule that removed rows on silence\nwould eventually remove a live instance that was merely slow. An operator names one instance, the\nbroker's verdict on its rail is what authorizes the removal, and the guard's job is to show them\nthey named a dead one. Removal is not a one way door either. The same instance re-registers over\nthe tombstone on its next start, under the same identity.\n\n## runtimes\n\n```bash\ncotal runtimes\n```\n\nLists every agent runtime the manager can spawn through: the built-in `pty`, the official providers\n(`orca`, `tmux`, `cmux`, `herdr`), and any custom provider installed via `cotal ext add`. Each installed\nprovider is probed so you can see what is actually reachable on this machine before selecting it:\n\n```\npty built in\norca installed \xB7 reachable @cotal-ai/orca\ntmux available \xB7 cotal ext add @cotal-ai/tmux\ncmux available \xB7 cotal ext add @cotal-ai/cmux\nherdr available \xB7 cotal ext add @cotal-ai/herdr\n```\n\n`installed \xB7 reachable` / `unreachable` is the provider's own `available()` probe; `available` means\nit is a known runtime you can add with the shown command. Selecting an unknown or uninstalled runtime\nvia `up`/`spawn --runtime <name>` fails loud and, for a known one, points at the exact `cotal ext add`\npackage. There is no silent fallback to `pty`.\n\n## send\n\n```bash\ncotal send dm <agent> \"<text>\" [--space <s>] [--server <url>] [--creds <path>]\ncotal send msg <channel> \"<text>\"\ncotal send ask <role> \"<text>\"\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which mesh, and (off-registry) which credential |\n\nOne-shot messaging: connect, send a single direct message (`dm`), channel post (`msg`), or role\nask/anycast (`ask`), then exit. For a running conversation, agents use the mesh tools instead\n([MCP tools](mcp-tools.md)).\n\n## channels\n\n```bash\ncotal channels list\ncotal channels set <name> [--replay | --no-replay] [--window <n>] [--desc <s>] [--instructions <s>]\ncotal channels default --replay | --no-replay\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Target mesh |\n| `--replay` / `--no-replay` | none | `set`/`default`: replay history to new joiners, or not |\n| `--window <n>` | none | `set`: replay window size |\n| `--desc <s>` | none | `set`: one-line channel description |\n| `--instructions <s>` | none | `set`: instructions shown to joiners |\n\nInspects and edits the channel registry: replay policy, description, and joiner instructions. ACL\nsemantics (who may read or post) are set at mint / provision time, not here; see\n[Channels and permissions](channels-and-permissions.md). On a user-auth mesh, `list` rides your\nown login as is; `set` and `default` edit the registry over a short-lived\nchannel-writer view, which needs ledger scope `admin` ([Identity & auth](identity-and-auth.md)).\n\n\n## history\n\n```bash\ncotal history clear --force [--dms] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Target mesh |\n| `--dms` | off | Also clear DM history |\n| `--force` | none | Required: clear without prompting |\n\nPurges retained channel history; `--dms` extends it to direct-message history. An alias of\n[`clean history`](#clean). On a user-auth mesh the purge rides a short-lived purger view over\nyour login, which needs ledger scope `admin` ([Identity & auth](identity-and-auth.md)).\n\n## console\n\n```bash\ncotal console [--plain] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Space to watch |\n| `--plain` | off | Line stream instead of the TUI |\n\nA live protocol view for a space: a lazygit-style TUI, or a plain line stream on `--plain`. On a\nuser-auth mesh it rides the read-only admin view over your login, which needs ledger scope\n`admin`. See [Watch a mesh](watch-a-mesh.md).\n\n## web\n\n```bash\ncotal web [--detach] [--host <host>] [--port <n>] [--no-open] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Space to serve |\n| `--host <host>` | `127.0.0.1` | Concrete HTTP bind and browser host; wildcard addresses are refused |\n| `--port <n>` | `7799` | HTTP port |\n| `--detach` | off | Run in the background; stop with `cotal down web` or bare `cotal down` |\n| `--no-open` | off | Don't open the browser |\n\nThe browser observability dashboard: presence, channels, and a live feed. It is **not** part of\n`cotal up`: it ships inside `cotal-ai` as the `@cotal-ai/web` extension, seeded automatically on first\nrun (like the built-in connectors) so it always matches your CLI version. It self-registers `cotal web`\ninto this surface and serves\n`http://cotal.localhost:7799` by default (loopback; `*.localhost` resolves in Chrome/Firefox/Edge; Safari may\nneed `http://127.0.0.1:7799`). On a user-auth mesh the dashboard rides the read-only admin view\nover your login, and a channel purge asks for its own channel-purger view per click; both need\nledger scope `admin`. Detached mode re-execs the current Cotal installation, writes diagnostics to\nthe mesh root's `.cotal/web.log`, and reports success only after the HTTP server answers. It requires\na recorded mesh root, but can be launched from any directory once `cotal up` has recorded the mesh.\nSee [Watch a mesh](watch-a-mesh.md).\n\n## mint\n\n```bash\ncotal mint <name> [--profile <agent|observer|admin>] [--out <path>] [--signer]\ncotal mint <name> --provision [--role <role>] [--space <s>] [--server <url>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--profile <agent\\|observer\\|admin>` | `agent` | Credential profile |\n| `--out <path>` | `.cotal/auth/creds/space.<key>/<name>.creds` | Output path - the default sits under the resolved space's segment (`<key>` is that space's hex encoding, as in [Project files](config.md#project-files)) |\n| `--signer` | off | Emit a stripped account-signing file instead |\n| `--force` | off | With `--signer`: overwrite an existing file |\n| `--allow-subscribe <a,b>` | the agent file's, else subscribe | Read-ACL override, **agent profile only**: `observer` and `admin` carry a fixed read set, and `mint` refuses this flag there rather than narrowing nothing |\n| `--allow-publish <a,b>` | the agent file's, else deny | Post-ACL override, **agent profile only** |\n| `--role <role>` | the agent file's | Agent profile: the anycast task queue the identity pulls (`svc_<role>`) |\n| `--provision` | off | Agent profile: also pre-create the identity's bind-only DM/deliver durables (and its role's task queue) on the live mesh, so the credential can consume |\n| `--space <s>`, `--server <url>` | the resolved mesh | With `--provision`: which mesh to provision on |\n\nMints a NATS creds file for a space in **static** auth mode, scoped to a profile and (optionally)\nexplicit read/post ACLs. `--signer` emits an account-signing file for delegating minting to another\nhost. A per-user-auth space refuses `mint`: agents there join under a logged-in user\n([`login`](#login) + [`actor grant`](#actor)), never via a handed-out creds file. See\n[Identity and auth](identity-and-auth.md).\n\nA plain mint is creds only: the identity can publish within its post ACL at once, but on an authed\nmesh its DM inbox and task queue are provisioner-pre-created and bind-only, so a **consuming**\nconnect fails until they exist. `--provision` performs that pre-create in the same command (a\nprovisioner cred is minted from the space's trust material, used, and dropped), so a long-running\nclient you start yourself can receive DMs and role anycasts like a spawned seat. The command prints\nthe identity's principal (its wire id) and lifecycle uid; a consuming client passes that uid as its\n`lifecycleUid`. Agent profile only; an open mesh needs none of this (peers self-create there). The\nmesh it provisions on must be the one this folder's auth is for - same space and same account key -\nso `--provision` can never quietly mint under another root's trust material.\n\n## Login\n\n```bash\ncotal login --idp <auth base URL> [--client-id <id>]\ncotal logout --idp <auth base URL>\n```\n\nSigns you in to a per-user-auth mesh's IdP (device code flow) and caches the session; run it\nonce per machine. It prints your IdP subject, the id the operator grants against. After a\nlogin, every command on that mesh works under your identity: each connect takes a fresh IdP\nproof, exchanges it locally for a short-lived bearer, and is authorized against the actor\nledger at connect time. `logout` revokes the IdP session and clears the cache. See\n[identity & auth](identity-and-auth.md).\n\n## actor\n\n```bash\n# an upsert of the WHOLE row: a flag left off is the WIDE default below, not \"unchanged\"\ncotal actor grant <actor> --sub <IdP subject> [--scope a,b] [--allow-subscribe a,b] [--allow-publish a,b] [--role <r>] [--label <l>]\ncotal actor revoke <actor> (--sub <IdP subject> | --owner <u_\u2026>)\ncotal actor list\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | the folder's | Space whose ledger to manage |\n| `--sub <subject>` | none | The IdP subject (shown by `cotal login`) the actor belongs to |\n| `--owner <u_\u2026>` | none | The derived owner token (alternative to `--sub`) |\n| `--scope <a,b>` | `spawn,role:default` | Capability scope (`''` = none; `spawn` = may run agents; `role:<r>` = may delegate role r; `admin` = cross-agent control; `supervise` = eligible for the closed remote manager-service view when the host enables it) |\n| `--allow-subscribe <a,b>` | `>` (all channels) | Channel read ACL; the user's envelope, their agents can never read beyond it |\n| `--allow-publish <a,b>` | `>` (all channels) | Channel post ACL; also the envelope for their agents' posting |\n| `--role <r>` | none | Role (scopes the task-queue consumer) |\n| `--label <l>` | none | Display label for `actor list` (never the IdP subject) |\n\nThe actor ledger is the single authorization source of a user-auth space: no row, no access.\nA bare `grant` is the **full** envelope (all channels, may spawn); the flags narrow it. A\nre-grant **replaces the whole row**, not the one field you name, so to add a capability spell\nevery field out: the new scope plus the row's current read set, post set, role and label\n(`cotal actor list` shows what a row holds). A field left off does not stay as it was, it\nreverts to the wide default in the table above, which is how a narrow reader becomes a reader\nof every channel. A re-grant retires the current interactive lifecycle through the running auth\nservice before it rotates the row, so copied bearers cannot cross an authorization update. If that\nretirement cannot be confirmed, the row is left unchanged and the command fails with the recovery\naction. `revoke` uses the same retirement before deleting the row, which lets a later grant create a\nreal successor instead of colliding with a live predecessor. `supervise` is separate from `spawn` and `admin`: it only makes a signed-in\nperson eligible for the host-provided closed remote manager-service view; it does not grant\nmanagement of another owner or a general host profile. `revoke` denies the next exchange and\nthe next connect with no restart, and evicts the principal's live connections. Managed-agent rows\n(written by the spawn path) live in a disjoint row space this command never touches. See\n[identity & auth](identity-and-auth.md).\n\n## doctor\n\n```bash\ncotal doctor auth [--fix]\n```\n\nCredential-health diagnosis and repair for this folder's mesh: renders every managed\ncredential as healthy / near-expiry / expired and ends in `healthy` or the exact next\ncommand; `--fix` applies the repairs it can. The one surface every stale-credential error\npoints at.\n\n## join\n\n```bash\ncotal join --space <s> --name <n> [--role <r>] [--channel <c>]\ncotal join --link <url> | --token <t>\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which mesh, and which credential |\n| `--name <n>` | none | Your presence name |\n| `--role <r>` | none | Your role |\n| `--channel <c>` | none | Channel to join |\n| `--kind <k>` | `agent` | Endpoint kind |\n| `--link <url>` | none | Join link (`cotal://\u2026`) |\n| `--token <t>` | none | Join token |\n| `--lifecycle-uid <uid>` | none | Required with `--creds`: the lifecycle UID minted alongside the credential (`COTAL_LIFECYCLE_UID` works too). A credential's durable grants name exact lifecycle-keyed resources, so `join` refuses to invent one |\n| `--tls` | off | Connect over TLS |\n\nAn interactive presence: join a space under your own name and role, without launching an agent\nharness. A `--link` or `--token` supplies the where and the auth in one value. See\n[Spaces](spaces.md) and [Identity and auth](identity-and-auth.md).\n\n## Manifest deploys\n\nA `cotal.yaml` manifest declares a whole mesh (channels, personas, roles, and ACLs) in one file.\nThree commands consume it, plus a read-only validator:\n\n```bash\ncotal up -f cotal.yaml # boot a fresh mesh from the manifest\ncotal spawn -f cotal.yaml # deploy the manifest additively onto a running mesh\ncotal down -f cotal.yaml # tear that deploy down (or --run <id> for one run)\ncotal topology view -f cotal.yaml # validate + view the access graph, change nothing\n```\n\n`up -f` and `spawn -f` differ in target: `up -f` brings up a new broker and applies the manifest;\n`spawn -f` requires an already-reachable mesh and applies additively (ownership-scoped). On a\nuser-auth mesh, `spawn -f` deploys over your own login (the deployer view, gated on ledger scope\n`spawn`): the manifest's agents land under your owner, a manifest claiming another owner is\nrefused, and seeding new channels additionally needs scope `admin`. Both take\n`--dry-run` to print the plan without mutating anything. `topology` validates the manifest and\nrenders its channel / role / ACL graph. See [Define a team](define-a-team.md) and the\n[manifest reference](manifest.md).\n\n## ext\n\n```bash\ncotal ext # same as `list`\ncotal ext add <npm-package>\ncotal ext remove <name>\ncotal ext list\ncotal ext root # print just the install prefix (scriptable)\ncotal ext seed [--repair|--reset|--force]\n```\n\nOperator-installed extensions: `add` installs an npm package into a cotal-owned prefix and records\nevery registry provider it contributes. Commands appear in help, completion, and dispatch; runtime\nproviders are lazy-loaded by commands such as `supervise`; local process providers participate in\n`status` and selective `down`. `remove` and `list` manage them. The `@cotal-ai/web` dashboard is the\ncanonical command/process example. Installed packages and their location are described in\n[config](config.md).\n\nBare `cotal ext` lists the inventory, headed by the install prefix. That prefix is a cotal-owned npm\nroot kept **separate** from npm's own global tree. These packages never show up in `npm list -g`,\n`cotal ext` (or the Extensions section of `cotal status`) is the canonical inventory. `cotal ext root`\nprints only the path, for scripts. The versions shown are the manifest pin recorded at add time.\n\nRemoving an extension that owns a running local process is refused with the mesh root and its\n`cotal down <component>` command; stop it first so uninstalling the package never strands a process\nwhose lifecycle provider is gone.\n\n### Built-in connectors are seeded extensions\n\nThe first-party agent connectors (`claude`, `opencode`, `codex`, `hermes`, `jcode`, `pi`) are not compiled into\nthe binary. They are seeded on first run through the **same** `ext add` path a third party uses, and\nappear in `cotal ext list` like any other extension. So you can remove one you do not want\n(`cotal ext remove @cotal-ai/connector-hermes`), and a deliberately-removed connector STAYS removed\nacross upgrades. `cotal ext add <your-package>` adds a third-party connector the same way. The web\ndashboard (`@cotal-ai/web`, providing `command:web`) is the seventh built-in seeded on the same path.\n\n`cotal ext seed` is the maintenance entry for that seeding (it runs automatically on the first real\ncommand of each boot, so you rarely call it):\n\n| Flag | Meaning |\n|---|---|\n| (none) | Reconcile: seed any never-seeded built-in, refresh a seeded one whose version the binary bumped, leave a removed one removed. A no-op once current. |\n| `--repair` | Recover after an interrupted seed or a lost authority (rebuilds the interrupted connector; restores the removed-vs-never-seeded record from its durable backup). |\n| `--reset` | Discard the record and re-seed all seven built-ins (the six connectors plus the web dashboard). **Resurrects any you removed.** Rebuilds cleanly over corrupt seed state. |\n| `--force` | Re-seed the built-ins even when the version stamp is current or a downgrade. |\n\nWhen a newer `cotal` advances the operator-global seed store to its generation, it prints one\nmigration line naming the old and new generations, the exact CLI entry that wrote the store, the\ncommit timestamp, and `seed/stamp.json`. That writer and timestamp are kept in the stamp, so a later\nolder CLI refusal can say which executable wrote the generation it will not overwrite and when.\nLegacy generation-only stamps remain readable; their refusal simply has no writer provenance to add.\n\nAn older `cotal` refuses a seed store written by a newer version. When it can verify a sufficient\n`cotal` executable on PATH or at the installer's `~/.local/bin/cotal` location, the refusal names\nthat absolute path so a reduced service PATH does not select the older binary again. Otherwise it\nkeeps the generic newer-version instruction. `--reset` remains the explicit way to rebuild the store\nfor the running older version.\n\nThe default connector for a bare `cotal spawn` (no `--agent`) is the persona's `agent:` pin if it\nhas one, else `claude`; set `COTAL_DEFAULT_AGENT` (e.g. `opencode`) to change the fallback. It is\na default, so a persona that pins its harness still wins over it. An `--agent` naming a removed\nconnector fails loud with the exact\n`cotal ext add` to restore it. Set `COTAL_SKIP_CONNECTOR_SEED=1` to turn off the automatic first-run\nseed/refresh entirely (for a controlled or offline setup that manages connectors by hand); `cotal ext\nseed` still runs on request.\n\n## completion\n\n```bash\ncotal completion <bash|zsh|fish|powershell> # print a stub to eval / source\ncotal completion install [shell] # install it persistently\n```\n\nPrints or installs shell completion. Completion candidates come from each command's declared flags\nand, where useful, live mesh state (spaces, personas, managed agents) resolved offline.\n\n## feedback\n\n```bash\ncotal feedback \"<summary>\" [--type <t>] [--email <e>] [--details <text>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--type <t>` | none | `bug` \\| `idea` \\| `friction` \\| `praise` \\| `other` |\n| `--details <text>` | none | Longer free-form details |\n| `--severity <s>` | none | `low` \\| `medium` \\| `high` |\n| `--area <a>` | none | The part of Cotal this concerns |\n| `--email <e>` | git email | Contact email (required on the keyless public path) |\n| `--name <n>` | none | Your name (optional) |\n| `--url <url>` | keyed / public intake | Intake URL override |\n| `--key <k>` | `COTAL_FEEDBACK_KEY` | Feedback key |\n\nSends feedback to the Cotal developers. With a key (`--key` / `COTAL_FEEDBACK_KEY`) it routes to the\nkeyed beta intake; without one it goes to the public `cotal.ai` intake and requires a contact email\n(`--email` / `COTAL_FEEDBACK_EMAIL`, else your git email). Run a self-hosted intake with\n[`feedback-intake`](#server-daemons).\n\n## run\n\nOperate durable workflow runs (cotal-lang programs) from the terminal.\n\n```bash\ncotal run start --file <program> [--timeout <dur>] [--endpoint <ep>]\ncotal run resume <runId> --file <program>\ncotal run ps\ncotal run journal <runId>\ncotal run answer <runId> <stepKey> --by <who> [--value <json>] [--artifact <ref>]\n```\n\n`start` mints the run id (the record never takes a caller-supplied one), prints it, and drives the\nrun to quiescence. `resume` takes an existing run over and continues it from its step journal.\n`ps` lists the run records on the endpoint and `journal` renders one run's durable records; both\nonly inspect, driving nothing. `answer` resolves an open checkpoint, presenting as the holder that\narmed it, with `--by` naming the answerer inside the resolution. `--timeout` sets the default\ncheckpoint timeout for a drive (default 1h). The guide is [workflows](workflows.md).\n\n## Server daemons\n\nTwo long-lived infra roles ship with the CLI. They are not part of everyday operation; the delivery\ndaemon comes up automatically with `cotal up --detach` in auth mode.\n\n```bash\ncotal deliver --space <s> [--server <url>] [--creds <file>]\ncotal auth-service --space <s> --server <url> [--port <n>] [--exchange-public-port <n>] [--exchange-public-url <https://\u2026>] [--exchange-trusted-proxy]\ncotal feedback-intake --keys <keys.json> [--port <n>] [--creds <file>]\n```\n\n`auth-service` runs a user-auth space's identity plane: the NATS auth callout, the\ncapability-gated local exchange and JWKS, and, when `--exchange-public-port` is set, the closed public\nexchange/discovery face forwarded by an HTTPS reverse proxy. `--exchange-public-url` is the proxy URL\nadvertised to clients; `--exchange-trusted-proxy` opts into last-hop `X-Forwarded-For` attribution.\n`cotal up --user-auth` starts and supervises the service for you, so you run it directly only to\nrecover one by hand.\n\n`deliver` runs the server-side Plane-3 delivery daemon: the durable backstop and membership/ACL\nauthority. It is auth-mode-only and single-instance (`--shard`/`--shards` accept only `N=1`);\n`--dev-mint` mints a scoped cred from the local signer for standalone dev. See the\n[delivery daemon](delivery-daemon.md). `feedback-intake` runs a self-hosted feedback server\n(requires `--keys` and a scoped `--creds`), announcing submissions into a space channel; flags\ninclude `--host`/`--port`, `--store`, `--space`/`--channel`, `--max-bytes`, and `--rate-limit`.\n\n## Plumbing\n\n`cotal __complete <words\u2026>` is the internal entry the shell-completion stubs call to emit candidates\nfor the current command line; you never run it directly. `cotal agent-bearer` is machine-facing\nplumbing on user-auth meshes: spawned agents exec it to print a fresh short-lived bearer from their\nspawn-time secret; you never run it directly either. Its local arm uses `--dir` to discover the\ncapability-gated loopback service. A remotely enrolled, already-granted agent instead receives\n`--exchange-url <https://base>` in its launch argv: that arm sends `{owner, actor, actorToken}` to the\npinned public exchange with no local capability, follows no redirects, and refuses every non-HTTPS\nURL because the actor token is the credential in the request body. (`cotal start` is a removed tombstone: it\nerrors and points you to `cotal spawn --detach`.)\n"
|
|
16390
|
+
"body": "# `cotal` CLI reference\n\n> **Reference**: describes the TypeScript reference implementation (the `cotal` CLI), not the wire contract. \xB7 **For:** operators \xB7 **Wire contract:** [SPEC](../SPEC.md)\n\n`cotal` is the operator command line for the reference implementation: bring a mesh up, mint\nidentities, launch agents, watch what they do, and tear it all down. It is a thin client over the\nwire contract: the normative subjects and schemas live in the [SPEC](../SPEC.md); this page is\nlookup material for the commands, not a walkthrough; if you are new, start with\n[Getting started](getting-started.md).\n\n## Running it\n\n```bash\nnpm install -g cotal-ai # puts `cotal` on your PATH (needs Node 22+)\ncotal --help # every command, grouped\ncotal --version # cotal-ai version + each installed extension's (also `cotal -v`)\ncotal <command> --help # one command's flags and usage\n```\n\n`npx cotal-ai <command>` runs it without a global install; in a dev clone, `pnpm cotal <command>`\nruns it through `tsx` with no build step. Bare `cotal` prints help. Every command generates its own\n`--help`, usage, and shell completion from its declared flags.\n\nCommands come from the surfaces the binary composes: the base mesh CLI, the manager\n(`supervise`), and the delivery daemon (`deliver`), plus any operator-installed extensions.\n`cotal ext add <npm-package>` installs any registry providers a package contributes: commands,\nruntimes, and local process lifecycle descriptors. The `web` dashboard and optional manager\nruntimes ship this way.\n\n## Commands\n\n| Area | Command | Purpose |\n|---|---|---|\n| Set up & lifecycle | [`setup`](#setup) | Guided, configure-only setup (installs, seeds personas; launches nothing) |\n| Set up & lifecycle | [`update`](#update) | Reconcile first-party extensions and check or opt into a coherent CLI upgrade |\n| Set up & lifecycle | [`up`](#up) | Start a local mesh (nats-server + JetStream), or boot a whole manifest with `-f` |\n| Set up & lifecycle | [`down`](#down) | Stop the whole stack, selected registered components, or a manifest deploy |\n| Set up & lifecycle | [`backup`](#backups) | Create an offline full-space or registry-only artifact from a preserved cut |\n| Set up & lifecycle | [`clean`](#clean) | Configurable cleanup: purge history (live), or wipe the local store / identity (stopped) |\n| Set up & lifecycle | [`meshes`](#mesh-registry) | List the running meshes on this machine |\n| Set up & lifecycle | [`use`](#mesh-registry) | Set the default mesh a bare `cotal spawn` joins |\n| Set up & lifecycle | [`status`](#mesh-registry) | Read-only diagnostics for setup, processes, and the selected mesh |\n| Agents & personas | [`spawn`](#spawn) | Launch an agent from a persona (foreground, or `--detach` via the manager) |\n| Agents & personas | [`models`](#models) | List connector model catalogs and variants from the manager |\n| Agents & personas | [`ps`](#managed-seats) | List managed agents and their mesh status |\n| Agents & personas | [`stop`](#managed-seats) | Ask the manager to stop a managed agent |\n| Agents & personas | [`attach`](#managed-seats) | Stream and drive a managed agent's terminal (pty runtime) |\n| Agents & personas | [`input`](#input) | Type one line into a managed agent's terminal without attaching |\n| Agents & personas | [`personas`](#personas) | List, show, edit, create, or remove local personas |\n| Agents & personas | [`supervise`](#supervise) | Run a manager daemon (the agent supervisor / control plane) |\n| Agents & personas | [`runtimes`](#runtimes) | List the agent runtimes the manager can spawn through and whether each is reachable |\n| Agents & personas | [`reconcile-gate`](#reconcile-gate) | Unfreeze an issuance gate left frozen by a crashed restart when the successor cannot boot-heal it (holder gone, complete CONNZ sweep) |\n| Messaging & watching | [`endpoints`](#endpoints) | List every endpoint in the live presence roster, including infrastructure |\n| Messaging & watching | [`describe` / `invoke`](#endpoint-control) | Resolve a v0.4 service's command surface off the wire; invoke one command by name |\n| Messaging & watching | [`send`](#send) | Send one message, then exit: DM a peer, post a channel, or ask a role |\n| Messaging & watching | [`channels`](#channels) | Inspect or set the channel registry |\n| Messaging & watching | [`history`](#history) | Clear retained message history |\n| Messaging & watching | [`console`](#console) | Live protocol view for a space (TUI, or `--plain` line stream) |\n| Messaging & watching | [`web`](#web) | Browser dashboard (installed as the `@cotal-ai/web` extension) |\n| Auth & meshes | [`mint`](#mint) | Mint a creds file for a space (static auth mode) |\n| Auth & meshes | [`login`](#login) | Sign in to a per-user-auth mesh's IdP (once per machine) |\n| Auth & meshes | [`logout`](#login) | Revoke the IdP session and clear the cached login |\n| Auth & meshes | [`actor`](#actor) | Manage a user-auth space's actor ledger (grant / revoke / list) |\n| Auth & meshes | [`doctor`](#doctor) | Credential-health diagnosis and repair (`doctor auth`) |\n| Auth & meshes | [`join`](#join) | Join a space as your own presence (interactive) |\n| Manifest | [`topology`](#manifest-deploys) | Validate and view a mesh manifest's access graph (read-only) |\n| Extensions & misc | [`ext`](#ext) | Install / remove operator CLI extensions |\n| Extensions & misc | [`completion`](#completion) | Print or install shell completion |\n| Extensions & misc | [`feedback`](#feedback) | Send feedback to the Cotal developers |\n| Extensions & misc | [`deliver`](#server-daemons) | Run the server-side Plane-3 delivery daemon |\n| Workflow runs | [`run`](#run) | Operate durable workflow runs: start, resume, list, inspect, answer a checkpoint |\n| Extensions & misc | [`feedback-intake`](#server-daemons) | Run a self-hosted feedback intake server |\n\nThe manifest modes of `up`, `spawn`, and `down` (`-f <cotal.yaml>`) plus `topology` are covered\ntogether under [Manifest deploys](#manifest-deploys).\n\n## setup\n\n```bash\ncotal setup [--full] [--demo] [--yes] [--skills]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--full` | off | Redo the full guided flow (implies `--demo`) |\n| `--demo` | off | Also seed the guided expert team (`david`, `sven`, `me`) |\n| `--yes`, `-y` | off | Non-interactive accept-all (for agents / CI) |\n| `--skills` | off | Reconcile Cotal skills only through installed connector providers, plus `~/.agents/skills`. Refused with `--full` or `--demo`. |\n\nGuided setup is **configure-only**: it checks prerequisites, invokes installed connectors' declared setup providers, and\nseeds persona files, and it launches nothing (no mesh, no web, no manager). First run gets the\nnarrated flow; later runs print a status card. By default it seeds one `default` persona; the\n`david`/`sven`/`me` team is opt-in via `--demo`. `cotal status` points stale Claude skills and\nout-of-date `.agents` skills at `cotal setup --skills`, not unscoped `setup`. See [Getting started](getting-started.md) and, for\nmaintainers, [setup internals](setup-internals.md).\n\nWhen a mesh resolves, setup seeds that mesh's recorded `.cotal/agents` catalog, the same catalog a\nfollowing `cotal spawn` reads. It prints the absolute destination. On a fresh machine with no mesh it\nuses this folder and says why; when several meshes are available and none is selected, it refuses\nrather than choosing a catalog.\n\n## update\n\n```bash\ncotal update [--self]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--self` | off | If a newer release exists, install that exact validated `cotal-ai` version globally and reconcile through the newly installed binary |\n\nWithout `--self`, `update` keeps the installed first-party surfaces coherent with the running\nbinary: it force-reconciles the four built-in connectors, then reinstalls other `@cotal-ai/*`\noperator extensions at the binary's exact version. Each extension runs in an isolated child, so one\nfailure cannot poison later replays. It then checks npm; a newer binary is an informational notice\nwith `cotal update --self` as the next command, not an automatic install.\n\nWith `--self`, the npm check happens first. When a newer release exists, Cotal installs the exact\nversion it validated, resolves and verifies that package in npm's global root, then launches that\nbinary to reconcile connectors and first-party extensions to the new generation. An npx or dev-clone\ninvocation therefore installs and continues through a separate global copy; it never claims the\nalready-running process changed. If the binary is current, `--self` performs the normal local\nreconcile without reinstalling it.\n\nThird-party extensions are listed with their installed version and recorded spec but are not\nauto-updated in v1. Floating third-party updates require `@cotal-ai/*` peer-range validation and are\na future follow-up. A failed connector/extension install, npm metadata check, or requested global\ninstall is reported and makes the command exit nonzero. Independent extension attempts continue so\nthe output includes every failure; an unavailable npm registry does not undo a completed local\nreconcile, but the command still exits nonzero because it could not establish that the install is\ncurrent.\n\n## up\n\n```bash\ncotal up [--detach] [--open] [--space <s>] [--server <url>] [--channels <path>] [--runtime <name>]\ncotal up --user-auth --idp <url> [--exchange-public-port <n> --exchange-public-url <https://\u2026> [--exchange-trusted-proxy]]\ncotal up --tls-cert <cert.pem> --tls-key <key.pem> # serve broker TLS (both, or neither)\ncotal up --restore <dir> [--restore-only registry] [--accept-missing-source]\ncotal up -f <cotal.yaml> [--dry-run] [--runtime <name>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--server <url>` | auto (free local port) | Listen URL override |\n| `--host <host>` | none | Bind host override. With no `--server`, the broker URL is derived from it, so `--host <addr>` alone is enough to make a mesh reachable at that address; a `--host`/`--server` pair naming different addresses is refused. A wildcard bind (`0.0.0.0`, `::`) keeps a dialable loopback URL. Recorded on the mesh and reused by every later manager launch, so a repair or resume keeps remote [`attach`](#managed-seats) working |\n| `--space <s>` | the folder's name | Space name |\n| `--store-dir <dir>` | none | JetStream store directory |\n| `--channels <path>` | `.cotal/channels.json` if present | Channel-registry seed file (JSON). An explicit path that is missing is an error |\n| `--restore <dir>` | none | Restore a completed offline backup before exposing the normal listener |\n| `--restore-only registry` | artifact selection | Restore only the registry component |\n| `--accept-missing-source` | off | Explicit disaster consent when the inode-bound preserved source is absent |\n| `--open` | off (auth) | Unauthenticated dev mesh: no JWT, no ACLs |\n| `--user-auth` | off | Per-user auth: people `cotal login`; connects are authorized against the actor ledger |\n| `--idp <url>` | none | With `--user-auth`: the IdP auth base URL to pin on first enable |\n| `--exchange-public-port <n>` | none | With `--user-auth`: add the public exchange face on this loopback port, for an HTTPS reverse proxy to forward to |\n| `--exchange-public-url <https://\u2026>` | none | With `--exchange-public-port`: advertise the reverse proxy's HTTPS URL in discovery |\n| `--exchange-trusted-proxy` | off | With `--exchange-public-port`: attribute public failure buckets to the last `X-Forwarded-For` hop. Enable only when the listener is reachable solely through a trusted proxy; otherwise the socket address is used |\n| `--detach` | off | Run in the background (stop with `cotal down`) |\n| `--tls-cert <path>` | none | PEM certificate to serve TLS with. Must be given together with `--tls-key`. Before starting the broker, Cotal checks readability, private-key mode, key/certificate match, the validity window, and host coverage. `nats-server` accepts an expired certificate and leaves the failure to clients, so Cotal performs these checks first. The decision is recorded; a later bare `cotal up` keeps serving TLS |\n| `--tls-key <path>` | none | PEM private key for `--tls-cert`. Refused if group- or other-readable (tighten to `600`) |\n| `--file <cotal.yaml>`, `-f` | none | Launch a whole mesh from a manifest |\n| `--dry-run` | off | With `-f`: print the plan, mutate nothing |\n| `--runtime <name>` | `pty` (or the manifest's, with `-f`) | Agent runtime for the mesh manager (`pty` built in; others are installed extensions, explicit-only). Resolved + probed before the broker starts; an uninstalled/unreachable runtime fails loud. With `-f`, overrides the manifest's runtime |\n| `--rotate-sys` | off | Rotate the space's system account and re-mint its two `$SYS` creds. Needs a stopped mesh; refused with `--open` |\n\n`cotal up` boots a local nats-server with JetStream and, in auth mode (the default), JWT auth and\nper-agent ACLs; `--detach` records the mesh so `cotal spawn` from any directory can find it. With no\n`--server`, it auto-selects a free port if the default address is taken; an explicit `--server`\nstays fail-loud on collision. `--detach` also brings up the control plane (delivery daemon in auth\nmode, then the manager). The `-f` form is a [manifest deploy](#manifest-deploys); see\n[Run a mesh](run-a-mesh.md).\n\n`--user-auth --idp <url>` starts the space's auth service alongside the broker: the NATS\nauth callout plus its capability-gated local exchange, and optionally the closed public exchange\nface configured by the three `--exchange-*` flags above. The service is torn down with `cotal down`,\nand a re-run of `cotal up` heals a dead service on a running broker. `--user-auth` and `--open`\ncontradict each other and are refused loudly; a running broker cannot change auth mode\nwithout a `cotal down` first. See [identity & auth](identity-and-auth.md).\n\n`--rotate-sys` renews the two `$SYS` credentials (`membership-observer`, `connection-evictor`).\nThey carry a 30-day expiry and nothing re-signs them in place, because the system-account seed is\nnever persisted, so they are renewed by issuing a **new system account** under the same broker\noperator and minting fresh creds against it. A plain re-`up` does **not** do this: it reuses the\nexisting trust record, and its `$SYS` creds along with it.\n\nThe rotation is safe to run on a real space, with one operational cost. The data account, the account\nsigning key, every agent credential minted from it, and the JetStream store are all untouched; what\ndies is the retired system account, and with it any out-of-band copy of the old `$SYS` creds, on every\nbroker that loads the rotated config. The cost is that **earlier full backups stop being restorable**\n(see below), so this is not a no-consequence operation. It needs the broker to restart on the rewritten\nconfig, so it runs as part of a boot:\n\n```bash\ncotal down\ncotal up --rotate-sys --detach # agents reconnect; nothing is re-provisioned\ncotal doctor auth # both $SYS creds healthy again, 30 days out\n```\n\nA rotation is a stopped, fresh boot, and anything that is not one refuses it, all for the same reason\n(the on-disk material and the broker it runs on must never end up on different generations):\n\n- a live mesh, because the running broker would keep serving the retired account;\n- an open mesh, whether that comes from `--open` or from `broker.auth: false` in a manifest, which\n has no system account at all;\n- `--restore`, because reinstating a trust root and superseding it in one command leaves no way to\n say which authority the mesh came up on;\n- an unfinished restore or resume attempt on this root, including one `cotal up` would recover on\n its own, because those paths can adopt a live listener and return without booting a broker;\n- a root that hosts more than one space, because the system account lives in the shared broker\n record and a rotation would retire every tenant's, while the root holds one `$SYS` cred pair\n pinned to one data account.\n\nTwo things to know before you run it:\n\n- **The retirement is config-load-bound.** Old `$SYS` creds are refused by any broker that loads the\n rotated config. A stale `nats-server` still running the *previous* config in memory would keep\n honouring them, so stop every broker for this root first. `--rotate-sys` refuses if this root's\n mesh is recorded as running, if anything unidentified is answering at the address it was given, or\n if the root's pid file names a live (or unreadable) process. Those are Cotal's own ownership\n records, not a scan of the process table: a `nats-server` you started by hand against this root's\n `server.conf` on some other port writes none of them and will not be seen. Do not run one.\n- **It invalidates earlier full backups.** A full artifact binds to the trust chain it was taken\n against, and that commitment covers the operator JWT and the system account. Every full backup\n taken before a rotation refuses to restore afterwards, so take a fresh `cotal backup` once the\n rotated mesh is up. `cotal up --restore` names this case when the data account still matches.\n\nThe commit is not atomic (a trust-record write plus two credential writes), so an interrupted\nrotation leaves the record ahead of the creds. That split is detected rather than silent: every\n`cotal up` on an auth mesh, and every `cotal doctor auth`, compares each `$SYS` cred's issuer against\nthe persisted record and names the retired account. `up` warns rather than refusing, because these\ncreds power the membership graph and live eviction, both of which degrade fail-soft; the mesh is not\nworth taking down over them. Re-running the rotation heals it, at the cost of one generation.\n\nWhile those creds are expired the mesh keeps delivering messages, but the\n[membership feed](delivery-daemon.md) and live connection eviction stay down; `cotal doctor auth`\nand the manager's log both name the credential and this repair.\n\n## down\n\n```bash\ncotal down\ncotal down --preserve-state [--store-dir <dir>]\ncotal down manager [delivery auth web nats ...]\ncotal down web [--space <name>]\ncotal down -f <cotal.yaml> | --run <id> [--dry-run]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--file <cotal.yaml>`, `-f` | none | Tear down this manifest's deploy |\n| `--run <id>` | none | Tear down one `spawn -f` run by id |\n| `--space <name>` | current mesh | With components: the mesh whose target-addressed components (e.g. `web`) to stop |\n| `--dry-run` | off | Print the manifest teardown or selected components, mutate nothing |\n| `--preserve-state` | off | Bare whole stack only: fence the manager, retain principals and durable state, stop and prove the stack down, then publish `ready` |\n| `--store-dir <dir>` | `.cotal/nats` | With `--preserve-state`: the actual store path (required for a custom store) |\n\nBare `cotal down` stops the whole local stack in dependency order. Positional component names stop\nonly those self-registered local processes; for example, `cotal down manager` leaves delivery and\nthe broker running, and `cotal down web` is available when the web extension is installed. A\ncomponent that starts target-resolved (the web dashboard) is stopped the same way: `cotal down web`\nresolves the mesh the same way as `cotal web` (registry current mesh first, `--space` to name one), so\nit works from any directory; the other components always stop under the folder you run it in. The\n`-f` / `--run` forms tear down a [manifest deploy](#manifest-deploys) without stopping the whole mesh\nand cannot be combined with component names. Stopping `nats` alone is refused while an unselected\nregistered daemon is still live; include those components or use bare `cotal down`.\n\n**Teardown verifies pinned process identity before signalling.** PIDs are recycled by every OS,\nso a recorded pid alone is not a durable target identity. `up` records each stack process's\ncreation identity in a sibling `<pidfile>.identity` pin, which holds the pid and the process start\nreported by the OS. Every stop path, including `down` for the broker, web and extension components,\nand the manager, delivery and auth-service stops, applies the same rule. A pin that names a different\nstart means the pid was reused, so teardown refuses and preserves it. A torn or unreadable pin also\nrefuses. Once the recorded process is stopped, rerunning teardown clears the stale record\nautomatically.\n\nThe first teardown after upgrading a running pre-pin stack has a narrower guarantee. A live record\nwith no identity pin is signalled after a loud warning that it predates identity pinning. Restarting\nthe component writes the pin, so later teardowns receive full match and mismatch protection. The\nsame warning applies on platforms where no stable start token is available.\n\nNormal `down` remains destructive at the logical identity/durable layer. `--preserve-state` is a\ndifferent maintenance transition: it suppresses leave/deprovision cleanup, persists the manager's\nsame-principal resume inventory, stops the entire stack without removing run/auth artifacts, and\npublishes a stable inode-bound cut only after every recorded process is proven stopped and the exact\nrecorded NATS endpoint is unreachable. A missing or stale broker pidfile never counts as stopped. The\nattempt is bound durably before the manager is fenced, the resume document and attempt-bound\n`cut-intent` are fsynced before manager commit, and the manager's commitment itself is journaled\n(`cut-committed`) before any process stops. A retry after a crash at any of those boundaries reuses\nthe exact recorded attempt and finishes the remaining stop and endpoint proofs idempotently, without\nneeding the (by then intentionally dead) manager. A partial cut never publishes `ready`. It cannot\nbe combined with component names, manifest teardown, or `--dry-run`.\n\n## clean\n\n```bash\ncotal clean <history|store|all> --force\ncotal clean restore-attempt --attempt <id> --force\ncotal clean restore-fallback --attempt <id> --force\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | `history`: target mesh |\n| `--dms` | off | `history`: also clear DM history |\n| `--store-dir <dir>` | `.cotal/nats` | `store`/`all`: JetStream store directory |\n| `--force` | none | Required: destructive, no prompting |\n| `--attempt <id>` | none | `restore-attempt`: exact stale pre-commit attempt; `restore-fallback`: matching healthy committed restore |\n\nOne configurable cleanup verb; every target requires `--force`.\n\n- `history` purges the retained message backlog on the **running** broker (channels, plus DMs\n with `--dms`). The same operation as [`history clear`](#history), which stays as an alias.\n- `store` deletes the **stopped** mesh's JetStream store (`.cotal/nats`): streams, durable\n consumers, and messages. This is the reset for stale on-disk broker state, e.g. durables\n minted by an older, incompatible Cotal generation surviving a `down`/`up` cycle.\n- `all` is `store` plus the space identity (`.cotal/auth`), the local creds and markers tied to\n it, any crash residue a normal `down` would have swept (stale pidfiles, `run/`), and the mesh's\n registry entry; the next `cotal up` mints a fresh identity.\n\n`history` needs the mesh up; `store` and `all` refuse while any recorded mesh process is still\nalive or any same-root recorded broker endpoint remains reachable (run `cotal down` first). They\nalso refuse outright on a root that holds accounts for several spaces: the store and the broker\ntrust record are shared by every space on the broker, so both targets would take out all of them\nand no `--space` can narrow that. `down`, `backup` and `up --restore` refuse there for the same\nreason. `cotal status` lists the tenants on such a root. Personas\n(`.cotal/agents`) and logs are never touched. A custom\nstore location is not recorded anywhere, so `--store-dir` must repeat whatever the mesh was\nlaunched with. Custom cleanup targets must contain either the Cotal store-generation marker or a\nreal `jetstream/` store directory; filesystem roots, project roots, and Cotal auth/maintenance trees\nare always refused.\n\n`store` and `all` also refuse every maintenance journal state. After a healthy committed restore,\n`restore-fallback` is the only supported way to remove the recorded unchanged old-store inode; it\nnever deletes the active target, requires both the exact attempt id and `--force`, and retires the\ncompleted restore journal so a later `down --preserve-state` can start a new backup cycle.\n\n## Backups\n\n```bash\ncotal down --preserve-state [--store-dir <dir>]\ncotal backup create <dir> [--only full|registry] [--store-dir <dir>]\ncotal up --restore <dir> [--restore-only registry] [--accept-missing-source]\n```\n\nBackup is offline-only. It requires the stable `ready` record from `down --preserve-state`, an exact\nstore match, no live recorded process, and an unreachable exact endpoint from the recorded cut.\nThat endpoint is probed immediately before cloning, so a live broker with a missing or stale pidfile\nis still refused. It claims the cut, reflink/copies the stopped source to a\nprivate attempt clone, and opens only that clone on a random loopback bootstrap broker with an\nindependent parent/deadline watchdog. It validates the canonical stream and pull-consumer inventory,\nwrites native snapshots with consumers excluded, and stores conservative contiguous ACK-floor\ncheckpoints separately. The original store is never opened by the backup broker, and the stack is\nnot restarted implicitly. Artifact destinations must not overlap the preserved source or maintenance\nattempt tree. Restore artifacts and targets likewise cannot nest inside or contain each other, the\npreserved source, or the maintenance attempt tree.\n\nStopped client-managed KV ordered consumers are ephemeral read residue, not backup state. Backup\nignores only the pinned client's exact stopped shapes: ordinary last-value watchers and the\nwhole-bucket scanner that uses all-history delivery to collapse concurrent tombstones. A bound\nconsumer or any lookalike with a different filter, inbox, lifetime, or other config is still refused.\n\n`full` is the default and indivisible: channel registry, CHAT/DM/TASK/INBOX/DLV, ACL, MEMBERS, and\nvalidated durable checkpoints. `registry` is the sole partial artifact. Presence, derived membership\nfeed, leases, native ephemeral/history consumers, credentials, keys, tokens, owner secrets, and actor\nledger files are excluded. `full` means every transferable message and registry stream, not every\nJetStream resource: endpoint submissions/facts/events/timers/workflow state, contract artifacts, and\nthe records/auth/session stores are nonportable control state. Restore recreates those streams empty\nwith their canonical configs before exposing the normal listener, so active endpoint runs,\nlifecycles, and sessions do not cross a backup. Artifacts are exclusively created `0700`;\nsnapshot/checkpoint files and\nthe manifest are `0600`; `manifest.json` is written last with exact sizes and SHA-256 values. The\ndirectory is trusted operator input: hashes detect corruption, not malicious rewriting.\n\nRestore validates and stages the exact allowlisted artifact bytes before moving or creating a store.\nIt requires the same space and existing trust state. The whole pre-commit window holds a journaled\nliveness claim (coordinator, watchdogs, brokers, absolute deadline): ordinary `up` and a repeated\n`up --restore` refuse while the claim is live, and a stale attempt is recovered only after the\ndeadline has elapsed and every recorded owner is proven dead. A retried `up --restore` handles this\nautomatically; an operator can also recover it explicitly with `cotal clean restore-attempt --attempt <id> --force`. Nothing\never rolls back a live attempt. A registry-only artifact restores as registry-only whether or not\n`--restore-only registry` is passed; omitted infrastructure is always created and the exact\npost-restore stream inventory is asserted before commit intent. Ordinary `up` from a preserved cut\nresumes only the exact recorded source store and runtime; a contradicting `--store-dir` or\n`--runtime` fails in preflight. Authenticated restores validate the complete\nspace trust bundle before staging, including nkeys, seed matches, JWTs, signers, and space binding;\nfull restores commit to the validated operator, system-account, data-account, and active-signer root\nchain in addition to the static/user authority fingerprint. Because the system account is part of that\ncommitment, a [`cotal up --rotate-sys`](#up) makes every full artifact taken before it unrestorable\nagainst this root: take a fresh full backup after each rotation. The composed commitment is revalidated\nimmediately before store mutation and never includes secret seeds. Restore never creates fresh auth.\nSame-path restores atomically retain the old\nsource at the journaled fallback path; alternate targets retain it in place; a missing canonical\nsource needs explicit `--accept-missing-source`. Quarantine and target restores use current canonical\nconfigs on isolated random-loopback brokers, never expose native snapshot consumers, and publish a\ncommit-intent immediately before the normal listener starts. Archive bytes never instantiate the real\ntarget: after quarantine validation, every stream is re-snapshotted from the validated quarantine\nstate into attempt-owned sanitized files, and the target is restored solely from those. Before that boundary, failure rolls back\nthe attempt-owned target; after it, ambiguity preserves both stores and records forward-repair\nrecourse. The cooperative maintenance lock excludes Cotal commands, not arbitrary raw NATS processes.\n\nBootstrap brokers in every auth mode, including open, mount the store under a local account with\nrandom operation-specific logins only, each carrying the exact per-phase subject permission matrix;\nnormal static credentials and user-auth sentinel/bearer connections are rejected, and no auth\nservice or callout starts. Open mode differs only in its account label, never in authority. Inventory, each stream snapshot,\nrestore initiation, exact upload id, validation, and each checkpoint recreation use separate exact\nauthorities. Every checkpoint carries the source stream's message/first/last sequence state and must\nmatch its snapshot record before mutation; core then derives and validates the only allowed start\npolicy. TASK is not a CLI exception: the same core checkpoint API recreates its canonical `DeliverAll`\nWorkQueue durable because acknowledged tasks are absent from retention and NATS forbids a\nstart-sequence policy there. Registry-only restore creates every omitted canonical stream and transient\nbucket on the isolated target before the normal listener is exposed. It deliberately does not resume\nretained agents or recreate their DM/DLV/TASK/ACL state; their identity material stays retained and\nstopped rather than being reprovisioned into a partial restore.\n\nAfter listener readiness, the manager starts attempt-bound, validates retained credentials/tokens\nwithout granting or reprovisioning, and resumes the exact persisted principals under cleanup\nsuppression. Registry-only restore uses the same flow with an empty agent set. `commitResume` is an\nidempotent validation barrier only: success must be `awaitingFinalize` with an attempt-bound 64-hex\ncommit token and does not release suppression. Under the workspace lock, the CLI first fsyncs that\nexact evidence as `manager-committed` (restore) or `resume-committed` (ordinary resume), then calls\ntoken-bound `finalizeResume`; only an `active` response for the exact token releases suppression. The\nCLI records the same token in finalization evidence before a restore becomes `active`, or before an\nordinary resume retires and consumes the marker. Re-entry from either committed state skips the prior\nidempotent activation/commit phases, retries finalization with the durable token, and finishes the\nworkspace transition. Failure before finalization preserves the committed state and cleanup\nsuppression; it is not rewritten through a degraded transition. Re-entry between any two earlier\nboundaries reuses the same attempt and may retry the idempotent phases without deleting retained state. A missing or\nchanged per-agent dependency is a named fail-closed result; the journal becomes degraded and remains\navailable for forward repair. A retry from `resume-intent`,\n`resume-active`, or `resume-degraded` reuses the same attempt and inventory after the prior listener is\nproven stopped. Every normal restore listener has an unguessable attempt-bound NATS server name. The\nCLI fsyncs its exact name/nonce, canonical endpoint, process owner, and generation-bound target identity\nimmediately after spawn. Re-entry accepts a surviving listener only when its INFO server name, live PID\nrecord, endpoint, and target identity all match that proof; degraded restore repair then moves through\nthe guarded workspace transition only after manager commit. If an uncommitted bound owner is provably\ndead, recovery retires that exact proof under the maintenance lock and binds a fresh listener for the\nsame attempt, endpoint, and target with a new nonce and server name. A live foreign/mismatched listener\nor ambiguous owner is preserved and refused, never adopted by reachability alone. A reconstructed\ncommit/degraded attempt without either the exact bound proof or a durable dead-listener replacement\nrecord fails closed even when the recorded port is free. A later ordinary startup may pass an `active`\nrestore only when its details prove manager commit and its exact recorded listener is dead.\n\n## Mesh registry\n\n```bash\ncotal meshes\ncotal meshes add # guided, on a terminal\ncotal meshes add <space> --server <url> [--root <dir>] [--mode auth|open|user] [--tls] [--force]\ncotal meshes add <space> --mode user (--user-auth-file <bundle.json> | --from <https url>)\ncotal meshes rm <space> [<space> \u2026] [--force]\ncotal use <space>\ncotal status [--space <s>] [--server <url>] [--components]\n```\n\n`meshes` lists the meshes this machine knows; a `*` marks the `current` default a bare\n`cotal spawn` joins.\n\nRun on a terminal with the space or `--server` missing, **`meshes add` is guided**: it asks for the\none thing that cannot be derived (the broker URL), probes it, and tells you what answered - open or\nrequiring credentials. It then offers the spaces your `--root` already holds credentials for, states\nthe mode as a fact about that broker rather than asking, and shows the exact record before writing\nanything. A broker that does not answer, or a space name already registered, becomes a choice rather\nthan an error. Anything you pass on the command line is taken as given and not asked again. Without\na terminal - a script, an agent, CI - nothing prompts and the flag form's errors stand\n(`COTAL_NO_PROMPT=1` forces that too).\n\n`cotal up` and `cotal down` maintain their own records. `meshes add` registers a mesh they cannot\nspeak for: one running on another machine, a shared broker, a hosted space. `--root` is the folder\nwhose `.cotal/auth` holds that mesh's credentials and whose `.cotal/agents` holds its personas.\nThe default is the project you run it in. The registry stores that path, never a secret. `--mode`\ndefaults to `auth` when the root holds the space's account record and to `open` otherwise. The\nbroker is probed before anything is recorded, so a wrong address, or credentials that mesh will\nnot accept, fails here instead of at the first `spawn`; `--force` records without verifying (and\nreplaces an existing record).\n\nA hostname or public address is registrable only when the connection will **require TLS**. Pass\n`--tls`, or use a `tls://` URL. The scheme is recorded as enforced intent, so every later dial\nthrough the record demands the handshake (and `meshes add tls://\u2026` against a plaintext broker is\nrefused at registration). Without required TLS the fence admits loopback and private-overlay\nliterals only. RFC1918 addresses are refused in both modes because a cafe LAN is private but does not belong to you.\n\nA **user-auth** mesh registers from supplied pinned trust, never guessed: `--user-auth-file`\ntakes the bundle exported where the mesh runs; `--from` asks before it dials the address at all,\nthen fetches its `/.well-known/cotal-mesh` discovery document (HTTPS only), displays the pins, and\nasks again before adopting them. Neither fetch follows redirects: a 302 can move a pinned fetch\nonto plaintext or onto another host, so it is refused rather than followed, and the pinned\nexchange must itself be an `https://` URL, except for an exchange on this machine, where plain\n`http://` is accepted for a loopback *literal* (`127.0.0.1`, `::1`, any spelling of them) but not\nfor `localhost`, which is a name rather than an address. Registration verifies that the exchange\nanswers `/health` and `/jwks` as the pinned issuer. It also verifies that the broker refuses a bare\nconnect; that auth-required refusal is the pass. The sentinel credentials land in a 0600 file under\nthe entry's root; the registry records only the path.\n\n`meshes rm` drops records. It never stops a mesh. For a mesh running on this machine `cotal down`\nis the right verb, and `rm` says so unless you pass `--force`. A hand-added record is removed by\n`meshes rm`, by an `add --force` replacement, or by a `cotal up` that actually starts the broker for that same space, server and root, which becomes that\nmesh and so takes the record over (a `cotal up` for that space anywhere else refuses instead).\nNothing that merely *infers* a record is stale touches it: an\nunreachable broker is listed `offline` and stays, and `cotal down` / `cotal clean all` leave it\nalone even when it shares a root with the project they are tearing down, because nothing on this\nmachine could write it back.\n\n`use <space>` sets that default; the selection applies from every directory,\nincluding inside another mesh's project. `status` is a read-only report: machine prerequisites\n(starting with the installed `cotal-ai` version), the installed extensions and their versions, this\nfolder's `.cotal/`, the recorded meshes, and a live snapshot of the selected mesh (roster, channels,\nmembership feed). Stale Claude skills and out-of-date `.agents` skills recommend `cotal setup --skills`,\nnot unscoped `cotal setup`. `status` takes `--space` / `--server` to pick the mesh to inspect; it starts\nnothing.\n\nPersona rows name the catalog they describe. If this folder and the selected mesh use different\ncatalogs, status names both and marks which one spawn launches from. A green `default` means the file\npasses the same agent-file loader spawn uses; a present but invalid file is reported as invalid.\n\n`cotal status --components` adds a fail-loud per-component health pass. It reads **each\ncomponent's own control surface**, rather than treating a PID, a lease, or a successful probe of a\nsibling as proof that the component serves. It prints one of `serving`, `absent`, `not-serving`, or\n`refused` for each component and exits `0`, `1`, `2`, or `3` respectively (the highest observed\nstate wins):\n\n- **manager**: local PID record, its liveness-lease holder and PID, then the manager's own typed\n `status` service reachability from this host. Builds without a startup-phase report say\n `phase not reported by this manager build`; that is never a blank green state.\n- **delivery**: local PID record, its ready lease (`ready` is the daemon's own bound-control\n signal), and the latest `renewal.json` adoption verdict. A re-signed credential and a\n broker-accepted adoption stay distinct facts.\n- **web**: local PID record and the dashboard's own loopback `/api/meta` response, which must name\n the same PID and its requested port. A different process on the port, an unreadable PID command,\n or an unrecognizable process record is `refused`, not a green default-port guess.\n- **broker**: the registered mesh URL dialed from this host with its recorded TLS requirement.\n\n`absent` means Cotal has no live local component record (or has a stale record); `not-serving`\nmeans the component record is live but its service/readiness surface did not answer or is not ready.\nThose are intentionally separate exit cases. A failed or unreadable probe is `refused`, never an\nabsent component or a clean zero.\n\n## spawn\n\n```bash\ncotal spawn [<persona>] [--detach] [--name <n>] [--agent <a>] [--model <m>] [--variant <v>] [--prompt <text>] [--cwd <dir>]\ncotal spawn -f <cotal.yaml> [--dry-run]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | resolved mesh | Target space |\n| `--server <url>` | registry entry | Broker URL override |\n| `--creds <path>` | none | Control-caller creds for an off-registry manager (`--detach` only) |\n| `--name <n>` | persona's `name:` | Presence-name override (does not choose the persona) |\n| `--config <persona-or-path>` | none | Persona catalog name or file path; wins over the positional |\n| `--agent <a>` | persona's `agent:`, else `COTAL_DEFAULT_AGENT`, else `claude` | Connector type (`claude`, `opencode`, `jcode`, `hermes`, and so on) |\n| `--role <r>` | persona's `role:` | Role override |\n| `--model <m>` | persona's `model:` | Model override |\n| `--variant <v>` | persona's `variant:` | Model variant override (connector-defined; e.g. OpenCode reasoning tiers) |\n| `--cwd <dir>` | this cwd | Working directory to root the agent at |\n| `--prompt <text>` | none | Initial prompt auto-submitted at start |\n| `--resume <id>` | none | Fork an existing session id into the mesh; only connectors that declare resume support accept it (see [the matrix](connectors.md)) |\n| `--events` / `--no-events` | off | Publish the session's structured event plane to its own event channel |\n| `--share-tools <sel>` | none | Share named operator MCP servers with the agent |\n| `--subscribe <a,b>` | persona's | Channel read-set override |\n| `--allow-subscribe <a,b>` | = subscribe | Read-ACL override |\n| `--allow-publish <a,b>` | deny | Post-ACL override |\n| `--detach`, `-d` | off | Launch via the manager into a detached PTY (reattach with `cotal attach`) |\n| `--on <instance>` | class anycast | With `--detach` only: pin the launch to one manager instance id (the whole id, as `ps` prints it). Refused on a foreground spawn (no manager to pin), with `-f` (a manifest deploy launches through the manager class queue), and when empty |\n| `--file <cotal.yaml>`, `-f` | none | Deploy a manifest onto the running mesh |\n| `--dry-run` | off | With `-f`: print the plan, mutate nothing |\n| `--allow-stale <a,b>` | none | With `-f`: waive named stale agents (apply-only) |\n| `--runtime <name>` | manifest's | With `-f`: override the manifest's runtime |\n\n`--events` turns on the session's **event plane**: a stream of structured events describing what\nthe agent did, rather than the prose it wrote, on a channel of its own. The channel is named after\nthe agent's principal, `events.<owner>.<actor>`, never after its display name, because two live\nagents are allowed to share a display name and would then share a stream. The launch grants publish\nrights on that channel alone, foreground and detached alike, and a connector that does not\npublish an event plane refuses the flag rather than starting a session whose events have nowhere to\ngo.\n\nThe flag and the grant are separate on purpose. Holding publish rights on a channel is not a request\nto publish to it, so writing an event channel into an agent file's `allowPublish` does not turn the\nplane on: only the launch does.\n\nThe persona (`--config` > positional > `COTAL_DEFAULT_PERSONA` > `default`) is loaded from the\ntarget mesh's `.cotal/agents/`; the launch flags override the file. Foreground runs the agent\nattached to your terminal; `--detach` hands the launch to the running manager. Both modes get the\ndurable backstop on a mesh that runs the delivery daemon; `--live-only` skips it for a foreground\nspawn (messages posted while it is disconnected are then not replayed). A foreground exit retires\nthe agent's creds and broker footprint, like a manager despawn. A `--detach` spawn is an\n**action**: the manager accepts it and returns the allocated identity at once, then the launch\nfollows to a terminal outcome rather than blocking (see [the control surface](control-surface.md)).\nSee [Connect Claude Code](connect-claude.md) and [Agent files](agent-files.md); `-f` is a\n[manifest deploy](#manifest-deploys). (`cotal start` was merged into `cotal spawn --detach`.)\n\n## models\n\n```bash\ncotal models [--agent <connector>] [--refresh]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--agent <connector>` | all registered connectors | Connector whose catalog to list |\n| `--refresh` | off | Ask the connector to refresh its provider cache |\n\nAsks the running manager for each connector's model catalog (model ids plus their variants)\nfor connectors that expose one. OpenCode and Codex query harness/provider surfaces; Jcode reads\nproviders that enable `model_catalog = true` in the operator Jcode `config.toml`. Jcode's listed\neffort tiers render as `variants (declared, not provider-verified)`, and launch can still refuse one.\nA connector without a catalog says so. Pick a result with `cotal spawn --model <id> --variant <v>`,\nwhere `<id>` is the model id as the catalog printed it. OpenCode and Codex ids are the full\n`provider/model`; Jcode ids are bare (`opus-5`, not `cliproxy/opus-5`), because the provider is\nselected by the operator's Jcode config and a prefixed id is refused at launch with the bare form\nnamed.\n\n## endpoints\n\n```bash\ncotal endpoints [--space <s>] [--server <url>] [--creds <path>]\n```\n\nLists the mesh presence roster: agents, the manager, and any other protocol endpoint, with each\nendpoint's role, kind, status, and current activity. Unlike `ps`, this is a read-only presence view;\nit is not limited to child processes owned by the manager.\n\n## Endpoint control\n\n```bash\ncotal describe <endpoint> [--space <s>]\ncotal invoke <endpoint> <command> [--args '<json>'] [--space <s>]\ncotal invoke <endpoint> <command> --name <agent> [--admin] [--space <s>]\n```\n\nThe generic v0.4 service surface. `describe` resolves a registered endpoint's command set off the\nwire - the reserved `describe` command answers the registered contract digests, the schemas are\nfetched from the space's content-addressed contract store, recompiled, and verified against those\ndigests - and prints each command with its capability class and targeting shape. `invoke` calls one\ncommand by name: `--args` is a JSON object validated against the fetched input schema *before*\npublish; a targeted command takes `--name <agent>` (resolved to the agent's current principal via\n`ps`) or `--self`. `--admin` uses the admin instrument credential, whose cross-agent reach rides\nthe operator-only `any` authorization mode. Neither command has compile-time knowledge of any\nendpoint's schemas - this is the same trust chain every built-in control command now uses. Needs an\nauth mesh: the manager registers its service on both static and per-user meshes (a signed-in user\nrides their bearer; cross-agent reach needs the `admin` scope). An open mesh has no service\nregistry.\n\n## Managed seats\n\n```bash\ncotal ps [--on <instance>] [--wide | --json] [--space <s>]\ncotal stop --name <n> [--on <instance>] [--space <s>]\ncotal attach --name <n> [--on <instance>] [--no-reconnect] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--name <n>` | none | Managed agent to stop / attach (required) |\n| `--on <instance>` | class anycast (`ps`: class scatter) | Pin to one manager instance id (multi-manager space); takes the whole id as `ps` prints it, not a prefix. An empty value (`--on \"\"`, an unset shell variable) is refused, never treated as absent |\n| `--wide` (`ps`) | off | After each seat's compact row, print extra operational facts the manager records: `cwd`, `pid`, spawner, lifecycle uid, and the owning manager's instance id and host. Model and requested variant stay in the identity row rather than printing twice. A fact the manager did not record (for example a runtime with no real process) prints nothing, never a placeholder |\n| `--json` (`ps`) | off | Machine-readable: one JSON object per seat per line, copied unchanged from the manager row. Instance headers and errors go to stderr, so stdout contains only rows. Mutually exclusive with `--wide` |\n| `--no-reconnect` (`attach`) | off | End the attach when its session ends, instead of re-establishing it. For scripts that want one run and one exit code |\n\nThe human `ps` row is presentation text and is not a stable parsing target. Scripts use `--json`,\nwhich is the machine-readable row contract.\n\nThese are operator clients over the running manager's control plane. The default row includes the\nconnector, model pin, optional requested variant, and runtime as operational descriptors for the\nmanaged row. They do not make a shared display name a unique protocol identity; use `--json` when\nunambiguous owner+actor attribution is required. An omitted variant means no override was requested;\nCotal does not invent an effective provider default it cannot observe. `ps` also prints two state\nfacts per managed agent, because they answer different questions: the process fact from the manager's\nown runtime handle (`running` with its uptime, or `exited` with how long it ran), and the mesh fact\nfrom the roster (`idle` / `working` / `waiting` / `mesh offline`, or `not in roster` when the seat has\nno presence row at all: a seat that has not joined yet, or one that never did). A seat can be\n`running` and `mesh offline` at once: the process is alive and its presence has lapsed. On a user-auth\nmesh `ps` also renders each managed agent's last credential-refresh outcome, fail-closed.\n\n**Mode split (chosen up front, never try-scatter-then-degrade):**\n\n- **Static / open mesh.** Bare `ps` is a **class scatter**: it freezes the live manager class from\n the records registry, merges every registered instance's agents grouped and attributed per\n instance, and a non-answering instance is shown as `registered, no answer within the deadline`\n (never silently omitted). That label is the whole claim: the instance is registered and did not\n answer. It does not say the host is down, because a dead host never deregisters itself and a\n live one can be slow; if it is gone, deregister it.\n `--on <instance>` pins the read to one exact instance id instead. A wrong pin fails loud\n rather than falling through: a well-formed id that no live manager carries is reported as\n `manager instance <id> did not answer` (nothing else is asked), and a credential without that\n instance's rail is reported as refused by the broker, not as an unresponsive manager. A manager\n that answers with a refusal is shown with its own cause; \"no manager reachable\" is said only when\n nothing answered at all. If the scatter's own registry read fails (the freeze or the reconcile),\n `ps` says the manager registry could not be read rather than pronouncing on the managers, which\n may all be up.\n\n**`stop` and `attach` route by seat locality.** A seat can only be stopped or attached by the\nmanager actually running it, and the class queue does not know which one that is. So on a\nstatic/open mesh both verbs first ask every registered instance which one hosts the named seat, then\naddress that instance directly. This happens by default; you do not need `--on`.\n\n`--on <instance>` remains the override, for when you already know where the seat lives or the\nlookup itself is degraded. It is also the **only** route on a **user-auth mesh**: a ledger-scoped\nbearer does not hold the registry-read rows the lookup needs, so there the verbs stay on the class\nqueue unless you pin them yourself.\n\nIf no reachable instance has the seat, the error reports how many managers answered and names\nthose that did not. It does not collapse that state into a bare `no agent <name>`. That distinction matters\nbecause a single manager cannot tell \"hosted elsewhere\" from \"does not exist\": it answers\n`not-found` for both.\n- **User-auth mesh.** `cotal ps` reports what **one** manager knows about your agents (an `ep.one`\n read against the manager's in-memory roster, owner-filtered). It does **not** report other\n manager instances. It cannot tell you that one is down: an unreachable manager is absent\n from the list. Completeness across a multi-manager user-auth space is not claimed.\n A manager that does not answer fails the command outright (exit non-zero), rather than printing\n an empty list that could be read as \"no agents\". Your ledger row needs the `admin` scope to\n reach `ps` at all; `spawn` alone is refused by the broker (the ep tier boundary).\n\n`attach` streams and drives an agent's terminal on the `pty` runtime; detach with the escape key\n(Ctrl-] by default; see [`COTAL_DETACH_KEY`](config.md)). It does so over a one-use, holder-bound\nmesh session ([SPEC](../SPEC.md) \xA713.6): the manager replies with a signed session grant (never a\n`127.0.0.1` URL), the CLI redeems it once over the broker, and the browser console (`cotal console`)\ndrives the same session. `stop` and `attach` need a running manager to talk to. On a static mesh\nthey are cross-agent admin operations. On a user-auth mesh, your own agents (any agent under your\nowner) need only the `spawn` scope; another owner's agent needs `admin` on your ledger row\n([identity & auth](identity-and-auth.md)). Launch detached agents with [`spawn --detach`](#spawn).\n\n**`attach` reconnects when the link dies.** A session lives on a network link, and a laptop that\nsleeps, a VPN that drops or a wifi handover kills it. When that happens `attach` prints\n`[cotal: connection lost, reconnecting]` on stderr and starts asking the manager for a new session:\na fresh grant, a fresh per-session credential, a fresh connection, so every attempt re-runs the same\nauthorization the first attach did. On success it prints `[cotal: reconnected]`, the manager repaints\nthe seat's current screen the way it does for any attach, and you carry on in the same terminal.\nRetries wait 1s, 2s, 5s, 10s, then 30s, for as long as the seat exists. The detach key is read the\nwhole time the loop runs, the waits and the attempts alike, so a reconnect never traps you: press it\nwhile a session is being established and the attach ends there, and a session that lands behind the\npress is handed back to the manager rather than left holding a slot. Everything else you type while\nthere is no session is dropped rather than queued, so keystrokes aimed at a terminal that turned out\nto be frozen, Ctrl-C included, are not delivered to the agent by a reconnect you did not know had\nhappened. That starts before the first session, not at the first reconnect: at a terminal, `attach`\nreads and drops what you type while it is still resolving the mesh, so a key struck at a prompt that\nhas not come up yet does not reach the agent when it does.\n\nA **pipe** carries script input. For example, `printf 'ls\\n' | cotal attach --name web` is\nbuffered until the session opens. Buffering continues across reconnects, so\n`tail -f log | cotal attach --name web` does not lose the part of its feed written while the link was\ndown. Only a terminal gets the reader; `--no-reconnect` keeps the old behaviour on both.\n\nIt stops on its own when reconnecting cannot help, and says why: a manager that refuses the attach\nexits non-zero with the manager's own message, and a reconnect that finds the seat no longer there\n(despawned, or its agent exited while the link was down) exits cleanly with `seat <name> is gone`.\nA refusal that could still pass, such as a manager at its session ceiling, is relayed in the\nmanager's own words while the loop keeps trying, once per refusal rather than once per attempt.\nPressing the detach key, or the agent's process exiting while you are attached, ends the attach as\nit always did. `--no-reconnect` turns all of this off and restores the single-session behaviour,\nwhich is what a script wants.\n\nEach reconnect also hands the abandoned session back to the manager, over the first link that can\ncarry the message, so an attach that flaps does not eat the manager's session slots one outage at a\ntime. If that message never gets a link, the attach says so when it ends.\n\nWhich mesh `attach` resolves also decides **whose trust it redeems with**. Redeeming a session grant\nmeans minting a short-lived, session-scoped credential from the space's seed, and that seed comes\nfrom the root the mesh resolved to, never from a `.cotal` found by walking up from whichever\ndirectory you happen to be standing in. The difference is not hypothetical: `~/.cotal` exists on\nevery install because the mesh registry lives there, so a command run anywhere under your home\ndirectory but outside a project used to mint from your home directory's trust and present it to a\nbroker that trusts a different chain, which surfaced as a bare authorization failure that named\nnothing. A directory that does hold another chain for the same space is now reported on the way\npast, and not obeyed:\n\n```text\n! this directory resolves to /Users/you, whose .cotal/auth holds a DIFFERENT trust chain for space \"team\".\n attach used /Users/you/projects/app, the root this mesh resolved to. The other one is not being used, and is worth a look.\n```\n\nWhen the resolved mesh holds no seed at all, `attach` refuses and names what it resolved, the broker\nand the root, instead of describing a directory it did not use.\n\nTerminal bytes stream over the mesh; the manager's own HTTP/WS face serves the console. That endpoint binds\n**loopback by default**, so nothing is exposed by accident; `cotal up --host <addr>` passes its bind\naddress down, which is what lets you attach to an agent whose manager runs on another machine. A\nbare `cotal supervise` and an embedded manager stay machine-local. Set it directly with\n`supervise --console-host <host>`.\n\nThat address is **recorded on the mesh** and carried forward, because it is a decision rather than\nsomething later commands can work out for themselves (a broker dial address is not a manager bind\naddress). Every later manager launch for the same mesh reuses it, including a same-root `cotal up` repair,\nan adopted preserved or restored listener, and a `spawn -f` manifest deploy. A manager replacement\ndoes not quietly move a reachable attach face back to loopback. Passing `--host` again overrides it,\nso you can widen or narrow exposure whenever you like; a mesh that never asked stays loopback-only\nand records nothing.\n\nBecause that face carries terminal read and write for every managed agent, it is credentialed in two\ntiers. A mesh caller receives a **ticket** bound to the single agent the manager just authorized,\nsingle-use and short-lived, so one authorized attach can never be re-pointed at someone else's\nagent. The **console token** is the operator's own, reaches every agent, and is printed only to the\nmanager's output. The roster, the live feed, and the PTY stream all answer `401` without one; the\nstatic console shell is served openly, since it describes no agent.\n\n## input\n\n```bash\ncotal input --name <n> --text <text> [--no-enter] [--on <instance>] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--name <n>` | | Managed agent to type into (required) |\n| `--text <text>` | | The text to type, taken verbatim (required) |\n| `--no-enter` | off | Type the text and stop there, without pressing Enter |\n| `--on <instance>` | class anycast | Pin to one manager instance id using the same rules as [`attach`](#managed-seats) |\n\nTypes one line into a running agent's terminal, as if you had typed it there, and returns. This is\nthe half of [`attach`](#managed-seats) that a program wants: `attach` is a live stream that holds a\nsession open and expects a terminal on your side, so a script, a cron job or a web UI cannot use it\nto send a single line. `input` is one authorized call.\n\nWhat it is for is **harness commands**. A line beginning with `/` is not chat and not a message: it\nis something the agent's own harness handles, and the only way in is the keyboard.\n\n```bash\ncotal input --name reviewer --text \"/compact\" # ask the harness to compact its context\ncotal input --name reviewer --text \"/model opus\" # switch its model\ncotal input --name reviewer --text \"hold on that PR\" # ordinary typing works too\n```\n\n**Quoting.** `--text` takes a value, so a payload starting with `/` survives as written. A payload\nstarting with a dash needs the `=` form, because the shell-style `--text --foo` is ambiguous and is\nrefused rather than guessed:\n\n```bash\ncotal input --name reviewer --text=--verbose # dash-leading text: use --text=<value>\n```\n\nEnter is pressed by default, since a command typed but never submitted has not been delivered.\n`--no-enter` types the text and leaves it sitting at the prompt, which is how you stage a line and\nsend it later.\n\nNothing comes back but a delivery receipt (`\u2713 sent 9 bytes to reviewer`, counting the trailing\ncarriage return). Whatever the agent does next shows up where its output already goes: the mesh, its\ntranscript, or an `attach`.\n\n**This one is operator-only, and more narrowly than `stop` or `attach`.** Those two are granted to\nanything holding `spawn`, so an agent can stop and attach to seats under its own owner. `input` is\nnot: it is granted only to operator credentials, which on a user-auth mesh means your ledger row\nneeds the `admin` scope, the same scope [`ps`](#managed-seats) already needs there. The reason is\nthat a write into a terminal is control of whatever is running in it, and on a user-auth mesh the\nown-owner rule covers every seat under you, not only the ones you launched: a `spawn`-scoped agent\ncould otherwise type into a sibling it never started. Seat locality is still resolved for you.\n\nOnly the `pty` runtime can be typed into. The external terminal runtimes (`tmux`, `cmux`, `orca`,\n`herdr`) attach to a process they do not own, so they have no input stream for it and the command\nrefuses by name rather than dropping the keystroke.\n\n## personas\n\n```bash\ncotal personas list [-v] [--running]\ncotal personas show <name>\ncotal personas edit <name>\ncotal personas new <name> (--prompt <t> | --from <f>) [--role <r>] [--model <m>]\ncotal personas rm <name> --force\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which mesh's persona catalog |\n| `--role <r>` | none | `new`: the persona's role |\n| `--model <m>` | none | `new`: the persona's model |\n| `--prompt <t>` | none | `new`: the persona's prompt text |\n| `--from <f>` | none | `new`: seed the prompt from a file |\n| `--verbose`, `-v` | off | `list`: include role / model / description |\n| `--running` | off | `list`: mark personas live on the mesh |\n| `--force` | none | `rm`: required, delete without prompting |\n\nPersonas are the local agent files under the resolved mesh root's `.cotal/agents/`, the same catalog\n`cotal spawn` launches from. `--space` and `--server` therefore move every list, read, write, delete\nand completion operation to the selected mesh. An unresolved target refuses rather than falling back\nto the current directory. See [Agent files](agent-files.md) for the file format.\n\n## supervise\n\n```bash\ncotal supervise [--runtime <name>] [--space <s>] [--server <url>] [--spawn <names>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space to supervise |\n| `--server <url>` | hosting mesh, or matching registered mesh | Broker URL. A registered mesh supplies it when omitted; a different explicit value is refused. |\n| `--runtime <name>` | `pty` | Agent runtime (`pty` built in; extension runtimes are explicit-only) |\n| `--console-port <n>` | none | Protocol-console port |\n| `--console-host <host>` | loopback | Bind host for the console + attach endpoint. Loopback keeps it machine-local; `cotal up` passes the address it bound the broker to, which is what lets `cotal attach` reach this manager from another machine |\n| `--roster <file>` | none | Declarative roster to boot at startup |\n| `--launch <spec>` | none | Resolved manifest launch spec (from `up -f` / `spawn -f`) |\n| `--spawn <names>` | none | Comma-separated personas to pre-spawn at startup |\n\nThe manager is the agent supervisor and control plane: it answers `spawn --detach`, `stop`, `ps`,\n`attach`, and the `cotal_*` manager tools. `cotal up --detach` starts one for you; run `supervise`\ndirectly to recover a dead manager or drive a custom runtime. Default runtime is `pty`; install an\noptional provider first (`cotal ext add @cotal-ai/orca`, `@cotal-ai/tmux`, `@cotal-ai/cmux`, or `@cotal-ai/herdr`) and\nselect it explicitly. A missing provider or app fails loudly; there is no fallback. See [Deploy](deploy.md).\n\nOn a normal `SIGINT`/`SIGTERM`, the manager stops every seat and requires the selected runtime to\nprove the seat is gone before it releases the manager lease or service registration. A stop that\ncannot prove exit fails loud and keeps manager authority instead of reporting a clean shutdown while\nan orphan still holds broker rails. After an abrupt manager death, the same logical successor\nterminalizes only its own durable static slots, verify-evicts the predecessor's broker principal,\nrecords that result in the lifecycle's caller-readable audit detail, and only then retires the\nlifecycle and frees the alias. Missing or unverified broker evidence keeps the slot terminalizing.\nDelivery-admin does not terminate the orphan OS process; safe successor process reaping requires\ndurable process start-identity pinning and is tracked separately.\n\nA `meshes add --mode user` entry is a **participant** registration, not hosting authority. A\nparticipant may run `supervise` only when the host advertises the remote manager authority service\nand the signed-in actor has the dedicated `supervise` ledger scope. The CLI obtains the closed,\nloopback-only `manager-service` view; `spawn` and `admin` do not substitute for that scope. The\nhost issues the manager's public-nkey JWT material through its lifecycle-bound prepare \u2192 activate\n\u2192 renew protocol, never by handing the participant a signer or static provisioner credential.\n\nWithout that advertised host service or scope, `supervise` refuses before it starts a manager.\nRun `cotal spawn` without `--detach` to launch a foreground agent, or ask the space host to enable\nthe authority service and grant `supervise` for detached agents. If a running remote manager loses\nrenewal, it reports degraded state and refuses unsafe new starts and restarts; live agents are not\nsilently replaced. Do not run `cotal down` or `cotal up` on a participant machine to repair this\ncondition.\n\n## reconcile-gate\n\n```bash\ncotal reconcile-gate [--space <s>] [--server <url>] [--endpoint <e>] [--instance <id>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space the frozen gate lives in |\n| `--server <url>` | the local mesh | Broker URL |\n| `--endpoint <e>` | `manager` | Endpoint whose gate is frozen |\n| `--instance <id>` | this folder's persisted manager instance | Instance id |\n\n**When you need this.** A manager restart killed after deregistration begins but before the new\nincarnation finishes leaves the endpoint's issuance gate *frozen*, held by a\nprocess that no longer exists. The freeze is what stops two incarnations serving at once, which is\ncorrect. The successor manager now completes that dead registration itself on boot, using the same\nguard this command uses: it acts only when the freeze-holder is affirmatively gone under a complete\nCONNZ sweep (`gone` and `sweepComplete=true`), then abort-reopens the gate at generation+1 with\nprocessEpoch unchanged and continues the normal takeover. Live, unknown, unestablishable, and\nwrong-op-kind still refuse; there is no TTL.\n\nUse this command when the boot path cannot run: the delivery daemon is down, the repair targets a\nnon-manager endpoint, or you want to lift the freeze without starting a manager. It checks that the\nholder really is gone, prints what it found, and then finishes the dead operation the same way as the\ninterrupted restart would have: revoke the old credentials, evict their holders with verification,\nand reopen the gate.\n\nIf verification is interrupted, the command leaves the gate frozen and durably records each holder\nwhose eviction was already verified. A retry still repeats the freeze-holder liveness check, then\nskips only progress bound to the same registration operation, frozen-gate revision, and holder set.\nThe output reports holders completed before this attempt, completed now, and still remaining. A new\nfreeze or changed holder set starts from zero. Cursor cleanup happens only after reopen; a retained\ncursor is harmless because its old gate revision cannot authorize a later freeze.\n\n**It refuses far more often than it acts, on purpose**, and always says which check stopped it:\n\n| Refusal | What it means | What to do |\n|---|---|---|\n| `holder-alive` | The freeze-holder still has a live connection: a manager *is* running | Stop that process first. Reconciling would evict a live manager's credentials |\n| `holder-unknown` | The connection sweep could not prove the holder absent | Not safe to proceed: an unprovable holder is treated as a live one. Re-run once the broker answers completely |\n| `liveness-unestablishable` | The delivery daemon could not be asked at all | Start it (`cotal up` runs it) and re-run. Silence is never read as death |\n| `not-frozen` / `no-gate` | The gate is open, or there is no gate at that coordinate | Nothing to repair: check `--endpoint` / `--instance` |\n| `wrong-op-kind` | Frozen under a takeover or retirement, not a registration | Out of scope for this command; it will not reinterpret another operation's intent |\n| `eviction-unverified` | The holder looked gone but eviction could not be verified | The gate is left frozen, unchanged. Investigate the broker before retrying |\n| `raced` | A newer manager moved the gate mid-repair | Re-run `cotal doctor` and look again |\n\nThere is no `--force`, and no path that discards gate state: the only way this reopens a gate is by\nproving the holder is gone and then completing the operation properly.\n\n## deregister-instance\n\n```bash\ncotal deregister-instance [--space <s>] [--server <url>] [--endpoint <e>] [--instance <id>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space the instance is registered in |\n| `--server <url>` | the local mesh | Broker URL |\n| `--endpoint <e>` | `manager` | Endpoint the instance serves |\n| `--instance <id>` | this folder's persisted manager instance | Instance id, the whole id as `cotal ps` prints it |\n\n**When you need this.** The service registry records *registration*, not liveness, and nothing in\nthe model expires a row. A manager that stops cleanly removes its own registration. One whose host\ndied without writing anything cannot, so its record goes on claiming a live instance forever: every\nclass scatter in that space freezes the dead slot in, and `cotal ps`, `stop` and `attach` each pay\ntheir whole deadline waiting for a machine that is never coming back. A laptop that was reimaged, a\ncontainer that was deleted, a box that will not be back on the network: those registrations have no\nother exit.\n\nThis command is that exit. It asks the instance first, and it removes a record only when the broker\naffirms the instance's own rail is empty: nothing subscribed there. Then it deletes the\nregistration's two records keys, each pinned to the revision it read, and prints what it removed.\n\n**Silence alone never passes.** An unanswered describe is what a dead host, a wedged process and a\nslow one all look like, and a hung process still holds its subscriptions, so the broker sees\ninterest on its rail. That instance is refused and the observation is printed. A dead process holds\nno connection and therefore no subscription, so a real corpse is still removed.\n\n**Every refusal names the failed check:**\n\n| Refusal | What it means | What to do |\n|---|---|---|\n| `instance-answered` | The instance answered a pinned describe. It is alive | Nothing to repair. If it is wedged rather than gone, stop the process first; its own clean stop removes the record |\n| `instance-not-affirmed-gone` | It did not answer, and the broker did not report its rail empty, which is what a held subscription looks like: slow or hung, not affirmed gone | Nothing was removed. Stop the process; its record goes on its own clean stop, or re-run this once it is down |\n| `liveness-unestablishable` | The probe itself failed, so nothing was learned | Fix the probe's path (credential, broker) and re-run. A probe that could not run is never read as death |\n| `not-registered` | No registration at that coordinate | Check `--instance` and `--endpoint`. This takes the whole id, never a prefix |\n| `superseded` | The record moved between the read and the delete | Something is writing to it. Nothing was removed; re-observe before retrying |\n\nThere is no `--force` and no sweep: silence is not death, and a rule that removed rows on silence\nwould eventually remove a live instance that was merely slow. An operator names one instance, the\nbroker's verdict on its rail is what authorizes the removal, and the guard's job is to show them\nthey named a dead one. Removal is not a one way door either. The same instance re-registers over\nthe tombstone on its next start, under the same identity.\n\n## runtimes\n\n```bash\ncotal runtimes\n```\n\nLists every agent runtime the manager can spawn through: the built-in `pty`, the official providers\n(`orca`, `tmux`, `cmux`, `herdr`), and any custom provider installed via `cotal ext add`. Each installed\nprovider is probed so you can see what is actually reachable on this machine before selecting it:\n\n```\npty built in\norca installed \xB7 reachable @cotal-ai/orca\ntmux available \xB7 cotal ext add @cotal-ai/tmux\ncmux available \xB7 cotal ext add @cotal-ai/cmux\nherdr available \xB7 cotal ext add @cotal-ai/herdr\n```\n\n`installed \xB7 reachable` / `unreachable` is the provider's own `available()` probe; `available` means\nit is a known runtime you can add with the shown command. Selecting an unknown or uninstalled runtime\nvia `up`/`spawn --runtime <name>` fails loud and, for a known one, points at the exact `cotal ext add`\npackage. There is no silent fallback to `pty`.\n\n## send\n\n```bash\ncotal send dm <agent> \"<text>\" [--space <s>] [--server <url>] [--creds <path>]\ncotal send msg <channel> \"<text>\"\ncotal send ask <role> \"<text>\"\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which mesh, and (off-registry) which credential |\n\nOne-shot messaging: connect, send a single direct message (`dm`), channel post (`msg`), or role\nask/anycast (`ask`), then exit. For a running conversation, agents use the mesh tools instead\n([MCP tools](mcp-tools.md)).\n\n## channels\n\n```bash\ncotal channels list\ncotal channels set <name> [--replay | --no-replay] [--window <n>] [--desc <s>] [--instructions <s>]\ncotal channels default --replay | --no-replay\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Target mesh |\n| `--replay` / `--no-replay` | none | `set`/`default`: replay history to new joiners, or not |\n| `--window <n>` | none | `set`: replay window size |\n| `--desc <s>` | none | `set`: one-line channel description |\n| `--instructions <s>` | none | `set`: instructions shown to joiners |\n\nInspects and edits the channel registry: replay policy, description, and joiner instructions. ACL\nsemantics (who may read or post) are set at mint / provision time, not here; see\n[Channels and permissions](channels-and-permissions.md). On a user-auth mesh, `list` rides your\nown login as is; `set` and `default` edit the registry over a short-lived\nchannel-writer view, which needs ledger scope `admin` ([Identity & auth](identity-and-auth.md)).\n\n\n## history\n\n```bash\ncotal history clear --force [--dms] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Target mesh |\n| `--dms` | off | Also clear DM history |\n| `--force` | none | Required: clear without prompting |\n\nPurges retained channel history; `--dms` extends it to direct-message history. An alias of\n[`clean history`](#clean). On a user-auth mesh the purge rides a short-lived purger view over\nyour login, which needs ledger scope `admin` ([Identity & auth](identity-and-auth.md)).\n\n## console\n\n```bash\ncotal console [--plain] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Space to watch |\n| `--plain` | off | Line stream instead of the TUI |\n\nA live protocol view for a space: a lazygit-style TUI, or a plain line stream on `--plain`. On a\nuser-auth mesh it rides the read-only admin view over your login, which needs ledger scope\n`admin`. See [Watch a mesh](watch-a-mesh.md).\n\n## web\n\n```bash\ncotal web [--detach] [--host <host>] [--port <n>] [--no-open] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Space to serve |\n| `--host <host>` | `127.0.0.1` | Concrete HTTP bind and browser host; wildcard addresses are refused |\n| `--port <n>` | `7799` | HTTP port |\n| `--detach` | off | Run in the background; stop with `cotal down web` or bare `cotal down` |\n| `--no-open` | off | Don't open the browser |\n\nThe browser observability dashboard: presence, channels, and a live feed. It is **not** part of\n`cotal up`: it ships inside `cotal-ai` as the `@cotal-ai/web` extension, seeded automatically on first\nrun (like the built-in connectors) so it always matches your CLI version. It self-registers `cotal web`\ninto this surface and serves\n`http://cotal.localhost:7799` by default (loopback; `*.localhost` resolves in Chrome/Firefox/Edge; Safari may\nneed `http://127.0.0.1:7799`). On a user-auth mesh the dashboard rides the read-only admin view\nover your login, and a channel purge asks for its own channel-purger view per click; both need\nledger scope `admin`. Detached mode re-execs the current Cotal installation, writes diagnostics to\nthe mesh root's `.cotal/web.log`, and reports success only after the HTTP server answers. It requires\na recorded mesh root, but can be launched from any directory once `cotal up` has recorded the mesh.\nSee [Watch a mesh](watch-a-mesh.md).\n\n## mint\n\n```bash\ncotal mint <name> [--profile <agent|observer|admin>] [--out <path>] [--signer]\ncotal mint <name> --provision [--role <role>] [--space <s>] [--server <url>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--profile <agent\\|observer\\|admin>` | `agent` | Credential profile |\n| `--out <path>` | `.cotal/auth/creds/space.<key>/<name>.creds` | Output path - the default sits under the resolved space's segment (`<key>` is that space's hex encoding, as in [Project files](config.md#project-files)) |\n| `--signer` | off | Emit a stripped account-signing file instead |\n| `--force` | off | With `--signer`: overwrite an existing file |\n| `--allow-subscribe <a,b>` | the agent file's, else subscribe | Read-ACL override, **agent profile only**: `observer` and `admin` carry a fixed read set, and `mint` refuses this flag there rather than narrowing nothing |\n| `--allow-publish <a,b>` | the agent file's, else deny | Post-ACL override, **agent profile only** |\n| `--role <role>` | the agent file's | Agent profile: the anycast task queue the identity pulls (`svc_<role>`) |\n| `--provision` | off | Agent profile: also pre-create the identity's bind-only DM/deliver durables (and its role's task queue) on the live mesh, so the credential can consume |\n| `--space <s>`, `--server <url>` | the resolved mesh | Which root supplies the agent file, static trust and default credential storage; with `--provision`, also which live mesh receives the durables |\n\nMints a NATS creds file for a space in **static** auth mode, scoped to a profile and (optionally)\nexplicit read/post ACLs. `--signer` emits an account-signing file for delegating minting to another\nhost. A per-user-auth space refuses `mint`: agents there join under a logged-in user\n([`login`](#login) + [`actor grant`](#actor)), never via a handed-out creds file. See\n[Identity and auth](identity-and-auth.md).\n\nFor an agent profile, the resolved mesh root supplies the persona ACL, the signing material and the\ndefault credential destination as one authority. If the current folder also holds trust for a\ndifferent space or account, mint refuses before writing and names both roots. It never combines a\npersona from one root with credentials signed or stored under another.\n\nA plain mint is creds only: the identity can publish within its post ACL at once, but on an authed\nmesh its DM inbox and task queue are provisioner-pre-created and bind-only, so a **consuming**\nconnect fails until they exist. `--provision` performs that pre-create in the same command (a\nprovisioner cred is minted from the space's trust material, used, and dropped), so a long-running\nclient you start yourself can receive DMs and role anycasts like a spawned seat. The command prints\nthe identity's principal (its wire id) and lifecycle uid; a consuming client passes that uid as its\n`lifecycleUid`. Agent profile only; an open mesh needs none of this (peers self-create there). The\nThe same resolved authority is used for both the credential and `--provision`, so the broker\nfootprint cannot be created under a different root's trust material.\n\n## Login\n\n```bash\ncotal login --idp <auth base URL> [--client-id <id>]\ncotal logout --idp <auth base URL>\n```\n\nSigns you in to a per-user-auth mesh's IdP (device code flow) and caches the session; run it\nonce per machine. It prints your IdP subject, the id the operator grants against. After a\nlogin, every command on that mesh works under your identity: each connect takes a fresh IdP\nproof, exchanges it locally for a short-lived bearer, and is authorized against the actor\nledger at connect time. `logout` revokes the IdP session and clears the cache. See\n[identity & auth](identity-and-auth.md).\n\n## actor\n\n```bash\n# an upsert of the WHOLE row: a flag left off is the WIDE default below, not \"unchanged\"\ncotal actor grant <actor> --sub <IdP subject> [--scope a,b] [--allow-subscribe a,b] [--allow-publish a,b] [--role <r>] [--label <l>]\ncotal actor revoke <actor> (--sub <IdP subject> | --owner <u_\u2026>)\ncotal actor list\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | the folder's | Space whose ledger to manage |\n| `--sub <subject>` | none | The IdP subject (shown by `cotal login`) the actor belongs to |\n| `--owner <u_\u2026>` | none | The derived owner token (alternative to `--sub`) |\n| `--scope <a,b>` | `spawn,role:default` | Capability scope (`''` = none; `spawn` = may run agents; `role:<r>` = may delegate role r; `admin` = cross-agent control; `supervise` = eligible for the closed remote manager-service view when the host enables it) |\n| `--allow-subscribe <a,b>` | `>` (all channels) | Channel read ACL; the user's envelope, their agents can never read beyond it |\n| `--allow-publish <a,b>` | `>` (all channels) | Channel post ACL; also the envelope for their agents' posting |\n| `--role <r>` | none | Role (scopes the task-queue consumer) |\n| `--label <l>` | none | Display label for `actor list` (never the IdP subject) |\n\nThe actor ledger is the single authorization source of a user-auth space: no row, no access.\nA bare `grant` is the **full** envelope (all channels, may spawn); the flags narrow it. A\nre-grant **replaces the whole row**, not the one field you name, so to add a capability spell\nevery field out: the new scope plus the row's current read set, post set, role and label\n(`cotal actor list` shows what a row holds). A field left off does not stay as it was, it\nreverts to the wide default in the table above, which is how a narrow reader becomes a reader\nof every channel. A re-grant retires the current interactive lifecycle through the running auth\nservice before it rotates the row, so copied bearers cannot cross an authorization update. If that\nretirement cannot be confirmed, the row is left unchanged and the command fails with the recovery\naction. `revoke` uses the same retirement before deleting the row, which lets a later grant create a\nreal successor instead of colliding with a live predecessor. `supervise` is separate from `spawn` and `admin`: it only makes a signed-in\nperson eligible for the host-provided closed remote manager-service view; it does not grant\nmanagement of another owner or a general host profile. `revoke` denies the next exchange and\nthe next connect with no restart, and evicts the principal's live connections. Managed-agent rows\n(written by the spawn path) live in a disjoint row space this command never touches. See\n[identity & auth](identity-and-auth.md).\n\n## doctor\n\n```bash\ncotal doctor auth [--fix]\n```\n\nCredential-health diagnosis and repair for this folder's mesh: renders every managed\ncredential as healthy / near-expiry / expired and ends in `healthy` or the exact next\ncommand; `--fix` applies the repairs it can. The one surface every stale-credential error\npoints at.\n\n## join\n\n```bash\ncotal join --space <s> --name <n> [--role <r>] [--channel <c>]\ncotal join --link <url> | --token <t>\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which mesh, and which credential |\n| `--name <n>` | none | Your presence name |\n| `--role <r>` | none | Your role |\n| `--channel <c>` | none | Channel to join |\n| `--kind <k>` | `agent` | Endpoint kind |\n| `--link <url>` | none | Join link (`cotal://\u2026`) |\n| `--token <t>` | none | Join token |\n| `--lifecycle-uid <uid>` | none | Required with `--creds`: the lifecycle UID minted alongside the credential (`COTAL_LIFECYCLE_UID` works too). A credential's durable grants name exact lifecycle-keyed resources, so `join` refuses to invent one |\n| `--tls` | off | Connect over TLS |\n\nAn interactive presence: join a space under your own name and role, without launching an agent\nharness. A `--link` or `--token` supplies the where and the auth in one value. See\n[Spaces](spaces.md) and [Identity and auth](identity-and-auth.md).\n\n## Manifest deploys\n\nA `cotal.yaml` manifest declares a whole mesh (channels, personas, roles, and ACLs) in one file.\nThree commands consume it, plus a read-only validator:\n\n```bash\ncotal up -f cotal.yaml # boot a fresh mesh from the manifest\ncotal spawn -f cotal.yaml # deploy the manifest additively onto a running mesh\ncotal down -f cotal.yaml # tear that deploy down (or --run <id> for one run)\ncotal topology view -f cotal.yaml # validate + view the access graph, change nothing\n```\n\n`up -f` and `spawn -f` differ in target: `up -f` brings up a new broker and applies the manifest;\n`spawn -f` requires an already-reachable mesh and applies additively (ownership-scoped). On a\nuser-auth mesh, `spawn -f` deploys over your own login (the deployer view, gated on ledger scope\n`spawn`): the manifest's agents land under your owner, a manifest claiming another owner is\nrefused, and seeding new channels additionally needs scope `admin`. Both take\n`--dry-run` to print the plan without mutating anything. `topology` validates the manifest and\nrenders its channel / role / ACL graph. See [Define a team](define-a-team.md) and the\n[manifest reference](manifest.md).\n\n## ext\n\n```bash\ncotal ext # same as `list`\ncotal ext add <npm-package>\ncotal ext remove <name>\ncotal ext list\ncotal ext root # print just the install prefix (scriptable)\ncotal ext seed [--repair|--reset|--force]\n```\n\nOperator-installed extensions: `add` installs an npm package into a cotal-owned prefix and records\nevery registry provider it contributes. Commands appear in help, completion, and dispatch; runtime\nproviders are lazy-loaded by commands such as `supervise`; local process providers participate in\n`status` and selective `down`. `remove` and `list` manage them. The `@cotal-ai/web` dashboard is the\ncanonical command/process example. Installed packages and their location are described in\n[config](config.md).\n\nBare `cotal ext` lists the inventory, headed by the install prefix. That prefix is a cotal-owned npm\nroot kept **separate** from npm's own global tree. These packages never show up in `npm list -g`,\n`cotal ext` (or the Extensions section of `cotal status`) is the canonical inventory. `cotal ext root`\nprints only the path, for scripts. The versions shown are the manifest pin recorded at add time.\n\nRemoving an extension that owns a running local process is refused with the mesh root and its\n`cotal down <component>` command; stop it first so uninstalling the package never strands a process\nwhose lifecycle provider is gone.\n\n### Built-in connectors are seeded extensions\n\nThe first-party agent connectors (`claude`, `opencode`, `codex`, `hermes`, `jcode`, `pi`) are not compiled into\nthe binary. They are seeded on first run through the **same** `ext add` path a third party uses, and\nappear in `cotal ext list` like any other extension. So you can remove one you do not want\n(`cotal ext remove @cotal-ai/connector-hermes`), and a deliberately-removed connector STAYS removed\nacross upgrades. `cotal ext add <your-package>` adds a third-party connector the same way. The web\ndashboard (`@cotal-ai/web`, providing `command:web`) is the seventh built-in seeded on the same path.\n\n`cotal ext seed` is the maintenance entry for that seeding (it runs automatically on the first real\ncommand of each boot, so you rarely call it):\n\n| Flag | Meaning |\n|---|---|\n| (none) | Reconcile: seed any never-seeded built-in, refresh a seeded one whose version the binary bumped, leave a removed one removed. A no-op once current. |\n| `--repair` | Recover after an interrupted seed or a lost authority (rebuilds the interrupted connector; restores the removed-vs-never-seeded record from its durable backup). |\n| `--reset` | Discard the record and re-seed all seven built-ins (the six connectors plus the web dashboard). **Resurrects any you removed.** Rebuilds cleanly over corrupt seed state. |\n| `--force` | Re-seed the built-ins even when the version stamp is current or a downgrade. |\n\nWhen a newer `cotal` advances the operator-global seed store to its generation, it prints one\nmigration line naming the old and new generations, the exact CLI entry that wrote the store, the\ncommit timestamp, and `seed/stamp.json`. That writer and timestamp are kept in the stamp, so a later\nolder CLI refusal can say which executable wrote the generation it will not overwrite and when.\nLegacy generation-only stamps remain readable; their refusal simply has no writer provenance to add.\n\nAn older `cotal` refuses a seed store written by a newer version. When it can verify a sufficient\n`cotal` executable on PATH or at the installer's `~/.local/bin/cotal` location, the refusal names\nthat absolute path so a reduced service PATH does not select the older binary again. Otherwise it\nkeeps the generic newer-version instruction. `--reset` remains the explicit way to rebuild the store\nfor the running older version.\n\nThe default connector for a bare `cotal spawn` (no `--agent`) is the persona's `agent:` pin if it\nhas one, else `claude`; set `COTAL_DEFAULT_AGENT` (e.g. `opencode`) to change the fallback. It is\na default, so a persona that pins its harness still wins over it. An `--agent` naming a removed\nconnector fails loud with the exact\n`cotal ext add` to restore it. Set `COTAL_SKIP_CONNECTOR_SEED=1` to turn off the automatic first-run\nseed/refresh entirely (for a controlled or offline setup that manages connectors by hand); `cotal ext\nseed` still runs on request.\n\n## completion\n\n```bash\ncotal completion <bash|zsh|fish|powershell> # print a stub to eval / source\ncotal completion install [shell] # install it persistently\n```\n\nPrints or installs shell completion. Completion candidates come from each command's declared flags\nand, where useful, live mesh state (spaces, personas, managed agents) resolved offline.\n\n## feedback\n\n```bash\ncotal feedback \"<summary>\" [--type <t>] [--email <e>] [--details <text>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--type <t>` | none | `bug` \\| `idea` \\| `friction` \\| `praise` \\| `other` |\n| `--details <text>` | none | Longer free-form details |\n| `--severity <s>` | none | `low` \\| `medium` \\| `high` |\n| `--area <a>` | none | The part of Cotal this concerns |\n| `--email <e>` | git email | Contact email (required on the keyless public path) |\n| `--name <n>` | none | Your name (optional) |\n| `--url <url>` | keyed / public intake | Intake URL override |\n| `--key <k>` | `COTAL_FEEDBACK_KEY` | Feedback key |\n\nSends feedback to the Cotal developers. With a key (`--key` / `COTAL_FEEDBACK_KEY`) it routes to the\nkeyed beta intake; without one it goes to the public `cotal.ai` intake and requires a contact email\n(`--email` / `COTAL_FEEDBACK_EMAIL`, else your git email). Run a self-hosted intake with\n[`feedback-intake`](#server-daemons).\n\n## run\n\nOperate durable workflow runs (cotal-lang programs) from the terminal.\n\n```bash\ncotal run start --file <program> [--timeout <dur>] [--endpoint <ep>]\ncotal run resume <runId> --file <program>\ncotal run ps\ncotal run journal <runId>\ncotal run answer <runId> <stepKey> --by <who> [--value <json>] [--artifact <ref>]\n```\n\n`start` mints the run id (the record never takes a caller-supplied one), prints it, and drives the\nrun to quiescence. `resume` takes an existing run over and continues it from its step journal.\n`ps` lists the run records on the endpoint and `journal` renders one run's durable records; both\nonly inspect, driving nothing. `answer` resolves an open checkpoint, presenting as the holder that\narmed it, with `--by` naming the answerer inside the resolution. `--timeout` sets the default\ncheckpoint timeout for a drive (default 1h). The guide is [workflows](workflows.md).\n\n## Server daemons\n\nTwo long-lived infra roles ship with the CLI. They are not part of everyday operation; the delivery\ndaemon comes up automatically with `cotal up --detach` in auth mode.\n\n```bash\ncotal deliver --space <s> [--server <url>] [--creds <file>]\ncotal auth-service --space <s> --server <url> [--port <n>] [--exchange-public-port <n>] [--exchange-public-url <https://\u2026>] [--exchange-trusted-proxy]\ncotal feedback-intake --keys <keys.json> [--port <n>] [--creds <file>]\n```\n\n`auth-service` runs a user-auth space's identity plane: the NATS auth callout, the\ncapability-gated local exchange and JWKS, and, when `--exchange-public-port` is set, the closed public\nexchange/discovery face forwarded by an HTTPS reverse proxy. `--exchange-public-url` is the proxy URL\nadvertised to clients; `--exchange-trusted-proxy` opts into last-hop `X-Forwarded-For` attribution.\n`cotal up --user-auth` starts and supervises the service for you, so you run it directly only to\nrecover one by hand.\n\n`deliver` runs the server-side Plane-3 delivery daemon: the durable backstop and membership/ACL\nauthority. It is auth-mode-only and single-instance (`--shard`/`--shards` accept only `N=1`);\n`--dev-mint` mints a scoped cred from the local signer for standalone dev. See the\n[delivery daemon](delivery-daemon.md). `feedback-intake` runs a self-hosted feedback server\n(requires `--keys` and a scoped `--creds`), announcing submissions into a space channel; flags\ninclude `--host`/`--port`, `--store`, `--space`/`--channel`, `--max-bytes`, and `--rate-limit`.\n\n## Plumbing\n\n`cotal __complete <words\u2026>` is the internal entry the shell-completion stubs call to emit candidates\nfor the current command line; you never run it directly. `cotal agent-bearer` is machine-facing\nplumbing on user-auth meshes: spawned agents exec it to print a fresh short-lived bearer from their\nspawn-time secret; you never run it directly either. Its local arm uses `--dir` to discover the\ncapability-gated loopback service. A remotely enrolled, already-granted agent instead receives\n`--exchange-url <https://base>` in its launch argv: that arm sends `{owner, actor, actorToken}` to the\npinned public exchange with no local capability, follows no redirects, and refuses every non-HTTPS\nURL because the actor token is the credential in the request body. (`cotal start` is a removed tombstone: it\nerrors and points you to `cotal spawn --detach`.)\n"
|
|
16391
16391
|
},
|
|
16392
16392
|
{
|
|
16393
16393
|
"slug": "config",
|
|
@@ -16494,6 +16494,13 @@ var DOCS_BUNDLE = {
|
|
|
16494
16494
|
"summary": "One-line definitions of the terms used across these docs and the spec.",
|
|
16495
16495
|
"body": "# Glossary\n\n> **Reference** (informative) \xB7 **For:** everyone\n\nOne-line definitions of the terms used across these docs and the spec. The base terminology is\n[SPEC \xA71](../SPEC.md#1-scope-and-terminology); each entry links to its home.\n\n- **Agent file / persona**, a `.cotal/agents/<name>.md` file: AgentCard-shaped identity and\n channel grants in the frontmatter, with the Markdown body as the agent's persona (appended\n system prompt). [agent-files.md](agent-files.md)\n\n- **Agent node**: an instance whose `kind` is `agent`, as opposed to a plain `endpoint` such as\n an observer or dashboard. [SPEC \xA71](../SPEC.md#1-scope-and-terminology)\n\n- **Anycast**: a delivery mode addressed to a service **role**, delivered to one of its\n consumers (load-balanced). [SPEC \xA74](../SPEC.md#4-delivery-modes)\n\n- **Attention**, an advisory per-instance receive preference in presence: a global mode\n (`open` / `dnd` / `focus`) and per-channel overrides (`quiet` / `muted`). It shapes what wakes\n an agent, not what the broker delivers or authorizes. [SPEC \xA76](../SPEC.md#6-presence-and-discovery)\n\n- **Broker**: the message router for a space; v0 assumes a single trusted broker.\n [SPEC \xA71](../SPEC.md#1-scope-and-terminology)\n\n- **Channel**: a named, dotted, hierarchical multicast topic within a space.\n [SPEC \xA77](../SPEC.md#7-channels)\n\n- **Channel registry**: the per-space store of channel config (`replay`, `replayWindow`,\n `deliveryClass`, `description`, `instructions`), keyed by channel name.\n [SPEC \xA77](../SPEC.md#7-channels)\n\n- **Connector**: an adapter that bridges an agent harness (Claude Code, OpenCode, Hermes, \u2026) to\n the mesh, exposing the `cotal_*` tools. [connect-claude.md](connect-claude.md)\n\n- **Control plane**: the request/reply layer (from v0.4 the endpoint control surface, on the\n `ep` rails) plus the infra roles behind it (the manager and the delivery daemon) that\n provision and supervise a mesh.\n [SPEC \xA713](../SPEC.md#13-endpoint-control-surface-v04), [architecture.md](architecture.md)\n\n- **Delivery class (`live` / `durable`)**, a channel's per-channel delivery guarantee: `live`\n is at-most-once; `durable` adds a per-member backstop (at-least-once within retention). Distinct\n from delivery *mode*. [SPEC \xA74](../SPEC.md#4-delivery-modes), [channels-and-permissions.md](channels-and-permissions.md)\n\n- **Delivery daemon (Plane-3)**, the server-side component providing the durable backstop:\n fan-out writer, trusted reader, and membership registry. [delivery-daemon.md](delivery-daemon.md)\n\n- **Delivery modes**, the three addressing axes: multicast (channel), unicast (instance), and\n anycast (role). One per message. [SPEC \xA74](../SPEC.md#4-delivery-modes)\n\n- **Direct message / unicast**: a message addressed to one named instance's inbox.\n [SPEC \xA74](../SPEC.md#4-delivery-modes)\n\n- **Durable backstop**: the per-subscriber store that retains `durable`-channel posts (and\n authorized `live`-channel `@mention` copies) until the member has seen them.\n [SPEC \xA74](../SPEC.md#4-delivery-modes), [delivery-daemon.md](delivery-daemon.md)\n\n- **Endpoint**: a connected participant, identified by a stable instance id; the general term\n for any instance, agent or not. [SPEC \xA71](../SPEC.md#1-scope-and-terminology)\n\n- **History / replay**: retained channel messages backfilled to a joiner when a channel's\n `replay` is on, bounded to the reader's ACL and optionally to `replayWindow`.\n [SPEC \xA77](../SPEC.md#7-channels)\n\n- **Instance (id)**: a connected participant; its **id** is its principal (below), used\n identically as sender, presence key, and durable name. [SPEC \xA72](../SPEC.md#2-identity)\n\n- **Join link**: a `cotal://` / `cotals://` URL encoding broker host, space, optional credential,\n and optional channels, the onboarding half of the contract.\n [SPEC \xA710](../SPEC.md#10-connection-and-onboarding)\n\n- **Lifecycle UID (`lifecycleUid`)**: an unguessable, never-reused id of one managed lifecycle\n under a principal; it distinguishes a live instance from a same-name successor and keys that\n incarnation's durable state. Advisory in presence, authoritative in the trusted lifecycle\n mapping. [SPEC \xA713.1](../SPEC.md#131-lifecycle-identity), [\xA76](../SPEC.md#6-presence-and-discovery)\n\n- **Manager**, the agent supervisor and provisioner host: spawns and manages agent nodes over a\n pluggable runtime, and pre-creates the durables and membership records agents can't create\n themselves. [run-a-mesh.md](run-a-mesh.md)\n\n- **Manifest**, `cotal.yaml` (`kind: Mesh`): the declarative, channel-centric description of a\n team's channels, agents, and access, launched with one command. [manifest.md](manifest.md)\n\n- **Mention**: a lowercased peer name in a message's `mentions`; a wake hint that, on a `live`\n channel, also routes a durable copy to each mentioned target authorized to read the channel.\n [SPEC \xA74](../SPEC.md#4-delivery-modes), [\xA75](../SPEC.md#5-envelopes)\n\n- **Mesh**, a running Cotal deployment: a broker, a space, and the peers coordinating in it.\n [what-is-cotal.md](what-is-cotal.md)\n\n- **Multicast**: a delivery mode delivered to every subscriber of a channel.\n [SPEC \xA74](../SPEC.md#4-delivery-modes)\n\n- **Observer**: a read-only profile that reads chat, history, presence, and the channel\n registry, but cannot publish and cannot see DMs. [SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization)\n\n- **Principal (`owner.actor`)**: an instance's wire identity, two routing tokens: the\n **owner** (the account the agent acts on behalf of; a derived `u_\u2026` token under per-user\n auth, the literal `local` in open mode) and the **actor** (the agent's handle under that\n owner). The connection's nkey is only the transport credential.\n [SPEC \xA72](../SPEC.md#2-identity), [identity & auth](identity-and-auth.md)\n\n- **Presence**, the per-space directory keyed by instance id: each peer's card, status,\n activity, attention, and heartbeat timestamp. [SPEC \xA76](../SPEC.md#6-presence-and-discovery)\n\n- **Profile (`agent` / `observer` / `admin`)**: a default-deny credential class defining what\n subjects, streams, durables, and KV keys a credential may touch. [SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization),\n [Appendix B](../SPEC.md#appendix-b-profile-acls)\n\n- **Provisioner**: the privileged, signing-capable role that mints scoped credentials and writes\n the durables and membership records agents cannot write themselves.\n [SPEC \xA77](../SPEC.md#7-channels), [Appendix B](../SPEC.md#appendix-b-profile-acls)\n\n- **Retirement**: the terminal teardown of a lifecycle when an agent is despawned, stopped, or\n supervision-escalated (settle in-flight work, evict its credentials, record it retired). The\n freed name is held reserved until it completes, which is what makes reusing an agent's name\n safe. [SPEC \xA713.1](../SPEC.md#131-lifecycle-identity), [identity & auth](identity-and-auth.md)\n\n- **Role / service**: a named anycast target a group of agents share; a message to the role\n reaches one of them. [SPEC \xA71](../SPEC.md#1-scope-and-terminology), [\xA74](../SPEC.md#4-delivery-modes)\n\n- **Runtime (`pty` / `tmux` / `cmux` / `orca` / `herdr`)**, how the manager runs each agent process: the\n built-in pty, or a native terminal surface via an extension. [run-a-mesh.md](run-a-mesh.md)\n\n- **Space**: an isolated coordination context and tenant boundary; one space maps to one NATS\n account. [SPEC \xA71](../SPEC.md#1-scope-and-terminology), [spaces.md](spaces.md)\n\n- **Spawn capability**, the `spawn` control-plane capability in an agent file: grants publish to\n the privileged control subject so the agent may start or despawn peers. [agent-files.md](agent-files.md)\n\n- **Trusted reader**: the privileged component that reads the mixed durable backstop on an\n agent's behalf and re-authorizes each entry (current ACL and membership) before delivering it.\n [delivery-daemon.md](delivery-daemon.md), [SPEC \xA78](../SPEC.md#8-nats--jetstream-binding)\n"
|
|
16496
16496
|
},
|
|
16497
|
+
{
|
|
16498
|
+
"slug": "lang-card",
|
|
16499
|
+
"title": "The cotal-lang card",
|
|
16500
|
+
"kind": "Reference (informative)",
|
|
16501
|
+
"summary": "One page to write a correct workflow program.",
|
|
16502
|
+
"body": '# The cotal-lang card\n\n> **Reference** (informative) \xB7 **For:** people writing a cotal-lang program \xB7 **Normative:** [spec/cotal-lang.md](../spec/cotal-lang.md)\n\nOne page to write a correct workflow program. The normative reference is\n[spec/cotal-lang.md](../spec/cotal-lang.md); this card compresses the parts programs get wrong\nfirst. A program is one module of restricted JavaScript: no imports, no `class`, no `Promise`, no\nhost globals. Every effect is journalled under a step key, so a run can stop on any host and\nresume on another with the recorded steps returning instantly.\n\n## Effects\n\n| Primitive | Call | Returns |\n|---|---|---|\n| `spawn` | `await spawn(persona, { name?, worktree?, join?, role?, permits?, supervise?, onFork? })` | agent handle |\n| `turn` | `await turn(agent, { name, deadline? })` | `{ status: "done" \\| "blocked" \\| "handoff", to?, note?, at }` |\n| `ask` | `await ask(agent, { name, schema, deadline?, attempts? })` | the record the agent published |\n| `checkpoint` | `await checkpoint(name, prompt, { schema?, timeout?, onExpiry?, to? })` | see below |\n| `sleep` | `await sleep("10m", { name? })` | `null` |\n| `wait` | `await wait(event, { name?, timeout? })` | the event value, `null` on timeout |\n| `notify` | `await notify(agents, fact, { name? })` | `null` |\n| `monitor` | `await monitor(agent, { name? })` | `null` |\n\n`parallel`, `race`, `fanOut` and `conclave` are the four concurrency scopes (below). Step names\nare kebab-case; where the reference says a name is required, it must be a string literal. Option\nbags are closed: an unknown key is refused (L3011) with the full signature in the answer.\nDurations are a whole number and one unit: `"30s"`, `"10m"`, `"4h"`, `"2d"`.\n\n## Results you branch on\n\nA `turn` yields the agent\'s status. An `ask` yields the record the agent published. `schema` is\nopaque to the language: it is hashed and handed to the handler unchanged. Its handler-side\ncontract on `ask` is the shorthand, a record mapping each required top-level field of the reply\nto one of `"string"`, `"number"`, `"boolean"`, `"array"`, `"record"`, `"null"`. A handler\nenforcing it refuses a schema it cannot read with L4022 rather than skipping the check, counts\neach non-conforming reply against `attempts`, and reports L4006 when they are exhausted. The\nreference simulator enforces this. A `checkpoint`\'s `schema` stays uninterpreted in this\nrevision. A `checkpoint` is a durable pause raced against a durable timer:\n\n- resolved: `{ status: "resolved", value?, by?, at, artifact? }`\n- expired with `onExpiry: "proceed"`, or after an `"escalate"` hop expires too: `{ status: "expired", at }`\n- expired with the default `onExpiry: "fail"`: throws L4007\n\n```js\nconst gate = await checkpoint("ship-gate", "Ship 1.4.0 to npm?", { timeout: "4h", onExpiry: "proceed" })\nif (gate.status === "resolved") {\n log(gate.value, gate.by)\n} else {\n log("expired, holding")\n}\n```\n\n## The await rule (L2013)\n\nA call that starts an effect must be awaited where it stands, returned, or passed as a branch\nthunk to a scope. Anything else starts work nothing waits for, and the validator refuses it.\n\n```js\n// refused: L2013\nconst timer = sleep("10m")\n```\n\nValid forms: `await sleep("10m")`, `return sleep("10m")` inside a function, or\n`race({ timeout: () => sleep("10m"), reply: () => turnSomeone() })` as branch thunks. The same\nrule covers user functions declared `async`.\n\n## Concurrency\n\n`parallel` and `race` take branches unevaluated, as a record of thunks. The record keys are the\nbranch keys and survive reordering; array branches are keyed by index, which shifts when you\ninsert one (warning L3023). `fanOut(items, fn, { name, key? })` runs `fn(item, index)` per item;\nthe branch key is `key(item)`, else the item\'s string `id`, else the fan-out is refused (L3021).\n\n```js\nasync function review(pr) {\n const seat = await spawn("reviewer", { worktree: pr.id })\n return await turn(seat, { name: "review", deadline: "30m" })\n}\nconst prs = [{ id: "pr-11" }, { id: "pr-12" }]\nconst results = await fanOut(prs, (pr) => review(pr), { name: "review-all", key: (pr) => pr.id })\nlog(results)\n```\n\nA branch may not write to anything born outside it. Return values from branches and read the\nscope\'s result instead.\n\n```js\n// refused: L2032\nlet seen = 0\nawait parallel({\n a: async () => { seen = 1 },\n b: async () => { seen = 2 },\n})\n```\n\n## Values across effects\n\nA value that crosses an effect boundary is frozen on the way back: writing to it is L2031, so\ncopy it into a fresh record first. Effect arguments must have a canonical form: `undefined` or a\nnon-finite number inside one is L3041, a function is L3042. `json.stringify` is the canonical\nform (sorted keys, no spaces), and it refuses what has no canonical form (L4016) rather than\ndropping it.\n\n## Top refusals\n\n| Code | What it refuses | Write instead |\n|---|---|---|\n| L2013 | an effect call nothing awaits | `await` it, `return` it, or pass a thunk branch |\n| L2032 | a branch writing outside itself | return from the branch, read the scope\'s result |\n| L2031 | writing a value that crossed an effect | copy into a fresh record, then write |\n| L2012 | a host global by name | the replacement in the message, e.g. `json.stringify` |\n| L2011 | `Promise` | the four scopes |\n| L1025 | `==`, `!=` | `===`, `!==` |\n| L1001 | `class` | records and functions |\n| L4018 | a record, array or function where a primitive is needed | convert explicitly |\n| L3013 | a computed step name where a literal is required | a string literal |\n| L3011 | an unknown option key | the signature in the refusal |\n\nEvery code has a row in the reference\'s Appendix A, and the message a refusal prints is that\nrow\'s title, so search the reference for it verbatim.\n'
|
|
16503
|
+
},
|
|
16497
16504
|
{
|
|
16498
16505
|
"slug": "manifest",
|
|
16499
16506
|
"title": "Mesh manifest (`cotal.yaml`)",
|
|
@@ -16513,7 +16520,7 @@ var DOCS_BUNDLE = {
|
|
|
16513
16520
|
"title": "Message flow",
|
|
16514
16521
|
"kind": "Concept (informative)",
|
|
16515
16522
|
"summary": "How peers see each other and how messages reach them: the presence directory, the three delivery modes, and the two delivery guarantees.",
|
|
16516
|
-
"body": '# Message flow\n\n> **Concept** (informative) \xB7 **For:** everyone \xB7 **Normative:** [SPEC \xA74](../SPEC.md#4-delivery-modes), [\xA76](../SPEC.md#6-presence-and-discovery), [\xA77](../SPEC.md#7-channels), [\xA78](../SPEC.md#8-nats--jetstream-binding)\n\nHow peers see each other and how messages reach them: the presence directory, the three\ndelivery modes, and the two delivery guarantees. This page explains; the linked spec\nsections define.\n\n## Presence\n\nPresence is a per-space directory keyed by instance id: each peer\'s identity card\n(`AgentCard`: name, role, kind, tags, what it can do) plus its live state:\n\n- `idle`: free\n- `waiting`: blocked on input, approval, or a peer\n- `working`: the seat (or its surviving connector) claims it is busy. That is a\n process-alive claim, not a progress claim. A frozen seat can stay `working` while\n its heartbeat stays fresh. Surfaces that have no outside observation of last\n assistant-message age render `progress unknown` rather than treating heartbeat\n age as work age. A stale observation overlays `stalled Xm` on the still-fresh\n presence. The observation classifier is workstation/operator data, not part of\n the wire protocol; human wording stays in each CLI, connector, or web renderer.\n Compact maps and DM pickers use the presence glyph only and make no progress\n claim. See issue #876.\n- `offline`: gone (gracefully, or its heartbeat lapsed)\n\nA peer refreshes its own entry on a heartbeat; observers also derive `offline` from stale\ntimestamps, so a crashed agent cannot linger as "working". That derivation is gated on the\nobserver actually hearing the bucket: if the whole watch has been silent past the liveness\nwindow, the honest output is that the *view* is stale, not that every peer died in one TTL\n(real rosters do not do that). Offline peers stay in the roster for observability. An\n`activity` string rides along ("what I\'m doing right now"), and a peer\'s **attention**\npreference is mirrored here too (below). Each instance writes *only its own* key; presence\nis where discovery lives (our equivalent of `.well-known`), not a place to describe others.\nDetails: [SPEC \xA76](../SPEC.md#6-presence-and-discovery). The dashboard surfaces a stale view\non the same header mark it uses for a refused poll ([watch a mesh](watch-a-mesh.md)).\n\n## Three delivery modes\n\nEvery delivery message is addressed one of three ways\n([SPEC \xA74](../SPEC.md#4-delivery-modes)):\n\n| Mode | Addressed by | Reaches |\n|---|---|---|\n| **multicast** | `channel` | every subscriber of the channel |\n| **unicast** | `to` (instance id) | one specific peer\'s inbox |\n| **anycast** | `toService` (role) | *any one* holder of the role: "whoever is a reviewer" |\n\n| Multicast | Unicast | Anycast |\n|---|---|---|\n|  |  |  |\n\nChannels are dotted and hierarchical (`team.backend`); publishing is always concrete,\nsubscriptions may wildcard a subtree (`team.>`). Anycast is queued work: a task with no\nworker online *waits*; multiple online instances of a role load-balance; the task is\nremoved once acked.\n\n**Mentions.** A multicast message may carry `mentions: [name\u2026]`, a *priority hint*, not\na routing target. The message still reaches the whole channel, but a mentioned peer is\nwoken immediately while everyone else picks it up when next idle. Names (not instance\nids) ride the wire, so the match survives reconnects.\n\n**Deriving the mode.** A receiver derives how a message was addressed (channel / dm /\nanycast) from the *delivering subject*, never from payload fields: the payload is\nadvisory and forgeable, while the subject is broker-policed\n([SPEC \xA74](../SPEC.md#4-delivery-modes), [identity & auth](identity-and-auth.md)).\n\n**How the block is framed.** Delivered messages arrive as one block: a header, the items,\nand a tail. The tail names the *order of operations* - do what was asked with your own\ntools, verify the result, then reply - and says not to report an action that was not\nperformed, while still naming the reply verbs. This matters because a peer message is\nfrequently a work order and the tail lands where the model decides its next\naction. A tail that lists only reply tools reads as "this is a chat turn, answer it", and\nfor a weak model an answer that sounds finished is cheaper than the work: a live seat told\nto write a file and confirm sent the confirmation seconds later, with no file tool called\nand no file on disk, twice. A footer cannot make a model honest, so this narrows the\nfailure rather than closing it; what it does guarantee is that the connector is not\nsteering toward it.\n\n## Durable transport\n\nPlain pub/sub is at-most-once: a message reaches only whoever is subscribed *at that\ninstant*. Agents are constantly `working` or `offline`; a DM sent mid-turn would simply\nvanish. So delivery rides **JetStream streams**: the broker stores each message and every\nreader keeps its own bookmark, catching up at its own pace with nothing missed and no\ninterruption required. One mechanism covers three needs at once: live delivery, the\ninbound buffer, and late-join history. DMs and anycast are always at-least-once this way\n([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding)).\n\n## Channel delivery\n\nChannel delivery has two wire-observable classes, fixed per channel\n([SPEC \xA74](../SPEC.md#4-delivery-modes), [\xA77](../SPEC.md#7-channels)):\n\n- **`live`**: native broker subscription, at-most-once. You receive what is published\n while you are subscribed; a busy or offline moment is a gap. Join = subscribe, leave =\n unsubscribe: self-serve, bounded by your read ACL, no privileged mediation.\n- **`durable`**. `live` plus a per-member **durable backstop**: the message is also\n retained for each member and delivered on its next connection or turn, pending until\n acked. At-least-once for current members, within the channel\'s retention window. The\n machinery behind the backstop is the [delivery daemon](delivery-daemon.md).\n\nA message delivered both ways is one logical delivery; receivers dedupe by `id`, and\nreceiver deduplication MUST NOT use the empty string as a key: distinct messages that carry\n`id: ""` are not coalesced by the receiver. Duplicate surfacing is disclosed only where the\npath is already at-least-once (live is at-most-once). The publisher obligation to supply a\nunique string id (SPEC \xA75) is unchanged; an absent or non-string id is a malformed envelope. The\nspace default class is set at creation from the deployment profile (local/self-hosted \u21D2\n`durable`); a channel can override it.\n\n**Replay on join.** A channel\'s registry config (`replay`, `replayWindow`) says whether a\nfresh joiner gets recent history backfilled, marked as historical so an agent doesn\'t\nmistake a resolved old thread for live traffic. Historical channel ambient is delivered\npull-only: it never drives automatic turns or wakes the session, and is read on demand\nthrough `cotal_inbox`. A historical @mention or DM stays automatic. Mail addressed to\nyou is never noise. Replay off is **noise control, not\nconfidentiality**: history stays readable within the read ACL\n([channels & permissions](channels-and-permissions.md)).\n\n## Attention\n\nOrthogonal to all of the above, each agent chooses how much traffic *wakes* it: a global\nmode (`open` / `dnd` / `focus`) plus per-channel overrides (`quiet` / `muted`). This is\n**connector UX**: the broker still authorizes and delivers; attention\nonly shapes when the receiving agent\'s session is interrupted. It is mirrored into\npresence as advisory observability ("locally muted #deploys; DM to reach"), never read\nback into delivery. Semantics and tables:\n[Connect Claude](connect-claude.md#attention); the concrete\nknobs: [`cotal_status` / `cotal_channel_mode`](mcp-tools.md).\n\n## Related\n\n- [Spaces & channels](spaces.md): the isolation boundary vs the topic axis.\n- [Delivery daemon](delivery-daemon.md): the durable backstop\'s three pieces.\n- [Identity & auth](identity-and-auth.md): who may publish and read where.\n- [Watch a mesh](watch-a-mesh.md): seeing presence and traffic live.\n'
|
|
16523
|
+
"body": '# Message flow\n\n> **Concept** (informative) \xB7 **For:** everyone \xB7 **Normative:** [SPEC \xA74](../SPEC.md#4-delivery-modes), [\xA76](../SPEC.md#6-presence-and-discovery), [\xA77](../SPEC.md#7-channels), [\xA78](../SPEC.md#8-nats--jetstream-binding)\n\nHow peers see each other and how messages reach them: the presence directory, the three\ndelivery modes, and the two delivery guarantees. This page explains; the linked spec\nsections define.\n\n## Presence\n\nPresence is a per-space directory keyed by instance id: each peer\'s identity card\n(`AgentCard`: name, role, kind, tags, what it can do) plus its live state:\n\n- `idle`: free\n- `waiting`: blocked on input, approval, or a peer\n- `working`: the seat (or its surviving connector) claims it is busy. That is a\n process-alive claim, not a progress claim. A frozen seat can stay `working` while\n its heartbeat stays fresh. Surfaces that have no outside observation of last\n assistant-message age render `progress unknown` rather than treating heartbeat\n age as work age. A stale observation overlays `stalled Xm` on the still-fresh\n presence. The observation classifier is workstation/operator data, not part of\n the wire protocol; human wording stays in each CLI, connector, or web renderer.\n Compact maps and DM pickers use the presence glyph only and make no progress\n claim. See issue #876.\n- `offline`: gone (gracefully, or its heartbeat lapsed)\n\nA peer refreshes its own entry on a heartbeat; observers also derive `offline` from stale\ntimestamps, so a crashed agent cannot linger as "working". That derivation is gated on the\nobserver actually hearing the bucket: if the whole watch has been silent past the liveness\nwindow, the honest output is that the *view* is stale, not that every peer died in one TTL\n(real rosters do not do that). Offline peers stay in the roster for observability. An\n`activity` string rides along ("what I\'m doing right now"), and a peer\'s **attention**\npreference is mirrored here too (below). Each instance writes *only its own* key; presence\nis where discovery lives (our equivalent of `.well-known`), not a place to describe others.\nDetails: [SPEC \xA76](../SPEC.md#6-presence-and-discovery). The dashboard surfaces a stale view\non the same header mark it uses for a refused poll ([watch a mesh](watch-a-mesh.md)).\n\n`CotalEndpoint.presenceView()` reports whether its local roster can support an absence verdict.\n`current` is usable, `unpopulated` means the current watch has not completed its initial snapshot,\nand `stale` means the watch has been silent past its liveness window. Both unsafe states carry\n`fresh: false`, so an older consumer degrades instead of treating a partial reconnect refill as a\ncomplete roster. `waitForPresenceSnapshot()` returns `snapshot` or `timeout`; a timeout is a bounded\ngive-up, not proof that the snapshot completed.\n\n## Three delivery modes\n\nEvery delivery message is addressed one of three ways\n([SPEC \xA74](../SPEC.md#4-delivery-modes)):\n\n| Mode | Addressed by | Reaches |\n|---|---|---|\n| **multicast** | `channel` | every subscriber of the channel |\n| **unicast** | `to` (instance id) | one specific peer\'s inbox |\n| **anycast** | `toService` (role) | *any one* holder of the role: "whoever is a reviewer" |\n\n| Multicast | Unicast | Anycast |\n|---|---|---|\n|  |  |  |\n\nChannels are dotted and hierarchical (`team.backend`); publishing is always concrete,\nsubscriptions may wildcard a subtree (`team.>`). Anycast is queued work: a task with no\nworker online *waits*; multiple online instances of a role load-balance; the task is\nremoved once acked.\n\n**Mentions.** A multicast message may carry `mentions: [name\u2026]`, a *priority hint*, not\na routing target. The message still reaches the whole channel, but a mentioned peer is\nwoken immediately while everyone else picks it up when next idle. Names (not instance\nids) ride the wire, so the match survives reconnects.\n\n**Deriving the mode.** A receiver derives how a message was addressed (channel / dm /\nanycast) from the *delivering subject*, never from payload fields: the payload is\nadvisory and forgeable, while the subject is broker-policed\n([SPEC \xA74](../SPEC.md#4-delivery-modes), [identity & auth](identity-and-auth.md)).\n\n**How the block is framed.** Delivered messages arrive as one block: a header, the items,\nand a tail. The tail names the *order of operations* - do what was asked with your own\ntools, verify the result, then reply - and says not to report an action that was not\nperformed, while still naming the reply verbs. This matters because a peer message is\nfrequently a work order and the tail lands where the model decides its next\naction. A tail that lists only reply tools reads as "this is a chat turn, answer it", and\nfor a weak model an answer that sounds finished is cheaper than the work: a live seat told\nto write a file and confirm sent the confirmation seconds later, with no file tool called\nand no file on disk, twice. A footer cannot make a model honest, so this narrows the\nfailure rather than closing it; what it does guarantee is that the connector is not\nsteering toward it.\n\n## Durable transport\n\nPlain pub/sub is at-most-once: a message reaches only whoever is subscribed *at that\ninstant*. Agents are constantly `working` or `offline`; a DM sent mid-turn would simply\nvanish. So delivery rides **JetStream streams**: the broker stores each message and every\nreader keeps its own bookmark, catching up at its own pace with nothing missed and no\ninterruption required. One mechanism covers three needs at once: live delivery, the\ninbound buffer, and late-join history. DMs and anycast are always at-least-once this way\n([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding)).\n\n## Channel delivery\n\nChannel delivery has two wire-observable classes, fixed per channel\n([SPEC \xA74](../SPEC.md#4-delivery-modes), [\xA77](../SPEC.md#7-channels)):\n\n- **`live`**: native broker subscription, at-most-once. You receive what is published\n while you are subscribed; a busy or offline moment is a gap. Join = subscribe, leave =\n unsubscribe: self-serve, bounded by your read ACL, no privileged mediation.\n- **`durable`**. `live` plus a per-member **durable backstop**: the message is also\n retained for each member and delivered on its next connection or turn, pending until\n acked. At-least-once for current members, within the channel\'s retention window. The\n machinery behind the backstop is the [delivery daemon](delivery-daemon.md).\n\nA message delivered both ways is one logical delivery; receivers dedupe by `id`, and\nreceiver deduplication MUST NOT use the empty string as a key: distinct messages that carry\n`id: ""` are not coalesced by the receiver. Duplicate surfacing is disclosed only where the\npath is already at-least-once (live is at-most-once). The publisher obligation to supply a\nunique string id (SPEC \xA75) is unchanged; an absent or non-string id is a malformed envelope. The\nspace default class is set at creation from the deployment profile (local/self-hosted \u21D2\n`durable`); a channel can override it.\n\n**Replay on join.** A channel\'s registry config (`replay`, `replayWindow`) says whether a\nfresh joiner gets recent history backfilled, marked as historical so an agent doesn\'t\nmistake a resolved old thread for live traffic. Historical channel ambient is delivered\npull-only: it never drives automatic turns or wakes the session, and is read on demand\nthrough `cotal_inbox`. A historical @mention or DM stays automatic. Mail addressed to\nyou is never noise. Replay off is **noise control, not\nconfidentiality**: history stays readable within the read ACL\n([channels & permissions](channels-and-permissions.md)).\n\n## Attention\n\nOrthogonal to all of the above, each agent chooses how much traffic *wakes* it: a global\nmode (`open` / `dnd` / `focus`) plus per-channel overrides (`quiet` / `muted`). This is\n**connector UX**: the broker still authorizes and delivers; attention\nonly shapes when the receiving agent\'s session is interrupted. It is mirrored into\npresence as advisory observability ("locally muted #deploys; DM to reach"), never read\nback into delivery. Semantics and tables:\n[Connect Claude](connect-claude.md#attention); the concrete\nknobs: [`cotal_status` / `cotal_channel_mode`](mcp-tools.md).\n\n## Related\n\n- [Spaces & channels](spaces.md): the isolation boundary vs the topic axis.\n- [Delivery daemon](delivery-daemon.md): the durable backstop\'s three pieces.\n- [Identity & auth](identity-and-auth.md): who may publish and read where.\n- [Watch a mesh](watch-a-mesh.md): seeing presence and traffic live.\n'
|
|
16517
16524
|
},
|
|
16518
16525
|
{
|
|
16519
16526
|
"slug": "release",
|
|
@@ -16547,8 +16554,8 @@ var DOCS_BUNDLE = {
|
|
|
16547
16554
|
"slug": "setup-internals",
|
|
16548
16555
|
"title": "Setup internals (maintainer notes)",
|
|
16549
16556
|
"kind": "Project (non-normative maintainer notes)",
|
|
16550
|
-
"summary": "cotal setup (implementations/cli/src/commands/setup.ts) is configure-only
|
|
16551
|
-
"body": "# Setup internals (maintainer notes)\n\n> **Project** (non-normative maintainer notes) \xB7 **For:** maintainers changing how setup works\n>\n> How `cotal setup` works, and the cross-repo couplings it depends on. If you change one of\n> the things in the **Invariants** table, update the listed siblings in the same change, or\n> setup silently breaks for npx users.\n\n## The flow\n\n`cotal setup`\n([`implementations/cli/src/commands/setup.ts`](../implementations/cli/src/commands/setup.ts))\nis **configure-only and state-independent**: it checks prerequisites, installs the Claude Code\nplugin, and seeds persona files, and it **launches nothing**: no mesh, no web dashboard, no\nmanager, no delivery daemon, no cmux/tmux session, no demo. Starting the stack is `cotal up`; the\ndashboard is `cotal web`. Every file it writes is announced (`\u2192 wrote \u2026` via `provenance.wrote`).\nIt is two-tier, gated on a machine marker.\n\n**First run** (no `~/.cotal/onboarded.json`, or `--full`, or `--yes`) runs `runFirstRun(yes)`:\n\n- splash \u2192 intro \u2192 core **checks** (Node >= 22; **locate** `nats-server`: located, never\n started) \u2192 **connector picker** \u2192 write the demo personas (david/sven/me) and seed the generic\n `default` \u2192 **offer a global install** (`offerGlobalInstall`) \u2192 onboarded marker \u2192 a finale that\n lists the commands to start things (`cotal up --detach`, `cotal web`, `cotal spawn \u2026`,\n `cotal console`, `cotal down`). Nothing is running when it returns.\n- The old `--auth` / `--open` flags are **gone**: they set the mesh MODE at launch time, and setup\n no longer launches; mode is now `cotal up [--open]`'s concern (an unknown-option error names\n them, no silent no-op).\n\n**Later runs** run `runEnsure`: re-seed the `default` persona if it's missing (announced),\nre-offer the **global install** (`offerGlobalInstall`, same `isNpx()` + PATH-scan gate as first\nrun, so a repeat `npx cotal-ai setup` on a machine that still lacks a durable `cotal` finally\ninstalls it), then print the **status card** (`readyCard`). The card is **read-only probes** (`machineStatus`/`meshStatus`/`webUp`/`managerUp` for NATS, the plugin, the mesh, the web\ndashboard, and the manager) and for anything down it prints the exact command to start it\n(`cotal up --detach`, `cotal web`, `cotal supervise`). Displaying state never depends on it; setup\nstill launches nothing.\n\n**`--skills`** is the status-card write: it asks installed connectors with a declared skills setup\nhook to reconcile their own harness, then reconciles `~/.agents/skills`. The base CLI passes only the\nvendor-neutral skills directory, version, and state directory; connector packages own native assets\nand commands. It does not seed personas, install the mesh\nconnector, offer a global install, or write the onboarded stamp. Combined with `--full` or\n`--demo` it is refused.\n\nThe seeded `default` persona has an empty active `subscribe` set and wildcard\n`allowSubscribe`/`allowPublish` ACLs. A fresh agent receives no channel traffic until it joins a\nchannel, but can join, create, read, and post to channels on demand. The guided demo personas keep\ntheir existing `welcome` read and post scope. Repeat setup replaces the prior default template only\nwhen its bytes still match the shipped legacy body with `allowPublish: []`; any user edit makes the\nfile ineligible and leaves it byte-identical.\n\nSteps run in-process via `runSteps`\n([`lib/steps.ts`](../implementations/cli/src/lib/steps.ts)). A step can be `optional` (asked\nY/n), carry a `confirm` consent prompt, or be `live` (it draws its own pane via\n[`lib/live-window.ts`](../implementations/cli/src/lib/live-window.ts)). On failure, an\ninteractive run offers a Claude handoff\n([`lib/assist.ts`](../implementations/cli/src/lib/assist.ts)).\n\nThe **connector picker** (`pickConnectors`) multiselects the **setup connector surface**\n(`setupConnectorSurface`): every connector name the live registry or the installed extension\nmanifest advertises, materialized through the same loader the rest of the CLI uses. No connector\nname is written into `setup.ts`. `setupConnectorCandidates` turns that surface into choices and\nreads each hint off the connector's own declarations: `requires` names the executables a candidate\nstill needs on PATH, `setup` says whether it owns setup actions at all, and `pluginRoot` says\nwhether those actions install plugin assets. A selected candidate runs its connector-owned\n`connector` action through `connectorSetupStep`, which receives the `Connector` itself; a candidate\nthat declares no provider is simply marked ready (OpenCode auto-wires at spawn, injecting its\nplugin via `buildLaunch` and never writing the user's config). The `skills` action runs for every\npresent connector that declares one, selected or not, because Cotal's authored skills are\nindependent of mesh membership. Two experts (david, the engineer; sven, the guide) plus the\noperator's own driving session (`me`) are written by default, and `me` is the persona\n`cotal spawn me` drives.\n\n**`--yes`** forces non-interactive accept-all even on a TTY: optional plus `confirm` steps run\n(so the demo personas are written), the global install takes its default, and a failure aborts\nwith the log path and a non-zero exit. It still launches nothing. The control plane comes up with\n`cotal up --detach`. This is the agent/CI contract; keep it working.\n\n## Invariants\n\n| Thing | Must stay in sync across | Why |\n|---|---|---|\n| Marketplace name **`cotal-mesh`** | `setup.ts` (materialized `marketplace.json`), `CHANNEL_REF` in [`extensions/connector-claude-code/src/extension.ts`](../extensions/connector-claude-code/src/extension.ts), repo [`.claude-plugin/marketplace.json`](../.claude-plugin/marketplace.json) | The wake channel ref `plugin:cotal@cotal-mesh` binds by this name |\n| Plugin assets | the claude connector's own [`src/setup.ts`](../extensions/connector-claude-code/src/setup.ts) copy list (`dist/mcp.cjs`, `dist/hook.cjs`, `.claude-plugin/plugin.json`, `.mcp.json`, `hooks/hooks.json`) and its `package.json` `files` field | The connector materializes its own plugin; missing or renamed assets break the install, and the base CLI has no copy of the list |\n| `Connector.pluginRoot` | [`packages/core/src/connector.ts`](../packages/core/src/connector.ts) (contract) plus set in the claude connector's `extension.ts` | A connector's declaration that it ships installable plugin assets; the picker phrases its hint from it |\n| `BUNDLED_PKG_PREFIX` | [`lib/nats-bin.ts`](../implementations/cli/src/lib/nats-bin.ts) \u2194 the `@eplightning/nats-server-*` `optionalDependencies` in [`implementations/cli/package.json`](../implementations/cli/package.json) | The bundled NATS binary is resolved by `${prefix}-${platform}-${arch}`. (Future: swap the prefix to our own `@cotal-ai/nats-server-*`.) |\n| Onboard marker plus `ONBOARD_VERSION` | `~/.cotal/onboarded.json` in [`lib/onboard.ts`](../implementations/cli/src/lib/onboard.ts); version const in `setup.ts` | Flips first-run vs ensure |\n| Demo-agent format | `DEMO_AGENTS` in `setup.ts` matches the frontmatter shape read by [`packages/core/src/agent-file.ts`](../packages/core/src/agent-file.ts) (same as `examples/01-lateral-coordination/agents/`) | `cotal spawn <name>` loads these |\n| Managed personas | each `DEMO_AGENTS` body carries a `# managed by cotal-setup` frontmatter marker; `writeDemoAgent` refreshes the file when the body changes, backing a marker-less (user-edited) file up to `<name>.md.bak` first | Edit `DEMO_AGENTS` plus re-run setup to update david/sven/me; delete the marker line to take ownership |\n| `DEFAULT_SERVER` | [`packages/core/src/endpoint.ts`](../packages/core/src/endpoint.ts) | The address `cotal up` starts and the status card probes |\n\n## Background processes (`cotal up`)\n\n`cotal up` brings up the whole local stack in one place; since setup became configure-only\n(stage 2b), this is where the mesh and control plane start, so `cotal spawn --detach` /\n`cotal_spawn` find a manager right after `up`. The control plane comes up in cutover order:\nold-manager preflight \u2192 **delivery daemon** (auth mode only) \u2192 **manager**, via\n`ensureControlPlane`\n([`lib/delivery-proc.ts`](../implementations/cli/src/lib/delivery-proc.ts)). The detached\nprocesses, all stopped by `cotal down`:\n\nWith no explicit `--server`, `cotal up` auto-selects a free local port when the default broker\naddress is already held by another root or an unrecorded broker; an explicit `--server` remains\nfail-loud on collision.\n\n- **Mesh:** `startMeshDetached`\n ([`commands/up.ts`](../implementations/cli/src/commands/up.ts)) is the one place that boots a\n background nats-server (foreground `up` and `up --detach` both route through it). Writes\n `.cotal/nats.pid` and tails `.cotal/nats.log`.\n- **Delivery daemon:** `startDeliveryDetached` / `ensureDelivery`\n ([`lib/delivery-proc.ts`](../implementations/cli/src/lib/delivery-proc.ts)) re-execs `cotal\n deliver` detached with a pre-minted scoped `delivery.creds` (auth mode only, the durable\n backstop; open mode has none). Writes `.cotal/delivery.<key>.pid` and `.cotal/delivery.<key>.log`,\n where `<key>` is the space key ([Config](config.md#project-files)), so a root can serve a space per\n daemon.\n- **Manager:** `startManagerDetached` / `ensureManager`\n ([`lib/manager-proc.ts`](../implementations/cli/src/lib/manager-proc.ts)) re-execs `cotal\n supervise` detached (pty runtime); it answers the control plane\n (`cotal_spawn` / `cotal_despawn` / `cotal_persona`). Writes `.cotal/manager.<key>.log`;\n `managerUp(space)` checks that space's pid record for setup's status card. The **manager itself**\n writes `.cotal/manager.<key>.pid`, so a supervisor started by a container entrypoint, by cron, or\n by hand is recorded the same way a detached `cotal up` is. Readers verify the recorded pid is alive and is a\n supervisor before trusting it ([Config](config.md#project-files)).\n\nThe **web dashboard** is *not* part of `cotal up`. It ships inside `cotal-ai` as the `@cotal-ai/web`\nextension and is seeded automatically by the boot reconcile, the same durable, version-locked path as\nthe built-in connectors (`SEEDED_EXTENSIONS`), so it always matches the CLI version and needs no\nseparate install. Start it with `cotal web`; it records\n`.cotal/web.pid`, self-registers that process with `down`, and is addressed as\n`http://cotal.localhost:7799` (binds loopback; `*.localhost` resolves in Chrome/Firefox/Edge,\nSafari may need plain `127.0.0.1`). `webUp()` probes the port for setup's status card.\n\nAll recorded local processes self-register `local-process` descriptors. Bare `cotal down` resolves\nthe full set and stops it in dependency order; `cotal down manager` (or another component name)\nselects only that descriptor. Installed extensions cache their contributed registry keys, so the\nbase CLI does not hardcode optional package pidfiles.\n\nAll re-execs resolve this CLI via `selfArgv()` / `selfCotal()`\n([`lib/self-exec.ts`](../implementations/cli/src/lib/self-exec.ts)) = `[node, ...loaderFlags,\nentry]` (tsx loader in dev, compiled JS in prod), so they never need `cotal` on PATH; the stack\ncomes up identically via `npx`, `npm i -g`, and a dev clone.\n\nFor ergonomics only, an npx run with no global `cotal` offers to `npm i -g cotal-ai`\n(`offerGlobalInstall`, pinned to the running version): gated on `isNpx()` plus a PATH scan\n(`cotalOnPath()`, not `onPath(\"cotal\")`, since `cotal --version` is not a real command). The\ninteractive prompt defaults to yes, the non-interactive path (`--yes` or no TTY) takes the\ndefault, and a failed install is non-fatal (warn plus manual command). The same `self-exec.ts`\nexposes `displayCmd()`, the prefix (`cotal` / `npx cotal-ai` / `pnpm cotal`) used in the\nstatus-card hints so they match how you ran it.\n\n## Built-in connectors are seeded extensions\n\nThe first-party connectors (`claude`, `opencode`, `codex`, `hermes`, `pi`) are **not** static-imported by\nthe binary. The composition root (`bin/cotal.ts`) registers no connector; they self-register only when\nimported, and they are imported only once installed. On the first real command of each boot the CLI\n**seeds** them through the same `cotal ext add` path a third party uses, so they are ordinary\nextensions you can `cotal ext remove`. Code lives in [`implementations/cli/src/seed/`](../implementations/cli/src/seed/);\nthe entry is `reconcileSeededConnectors()`, gated in `runCli` before the manifest overlay so\n`ext seed --repair` survives a corrupt manifest.\n\n**What ships where.** The connectors are `devDependencies` of `cotal-ai` (not runtime deps), and a\n`prepack` step ([`bin/scripts/copy-seeded-connectors.mjs`](../bin/scripts/copy-seeded-connectors.mjs))\n`npm pack`s each into `bin/seeded-connectors/<name>/` (honoring each connector's own `files`), added to\nthe package `files`. `SEEDED_EXTENSIONS` (`@cotal-ai/workspace`) is the shared list: the\nconnectors plus `web`. The prepack asserts that every bundled payload's `name` and `version` match\nthe umbrella (the\n`fixed` changeset group keeps them lockstep), so a version-skewed payload can never be published; `web`\nalso emits `dist/web/vendor/vendor-manifest.json` (name/version/license/sha512) as the auditable\ninventory of its vendored browser libs (marked/DOMPurify ship as opaque `dist` bytes, not runtime deps).\n`seed/paths.ts:shippedSourceDir` resolves the live `extensions/<pkg>` dir in a\nsource checkout and `<cotal-ai>/seeded-connectors/<name>` in a published install. The reconcile copies\nthat payload into the durable store `seed/store/<version>/<name>` and `ext add --install-links` reifies\nthe `file:` dep from THAT stable path (a volatile source would fail to re-reify); `ext add` then\njunction-links each `@cotal-ai/*` peer to the binary's own copy. Before the first lazy import in each\nprocess, materialization rechecks those links by realpath and rebinds stale links under the extension\nlock. This lets the registry-facing imports of a global install, npx, and source worktrees share the\nmachine prefix while each process still gets its host's single `@cotal-ai/core` registry instance;\nlauncher artifacts are self-contained and do not resolve those mutable links later.\n\n**Reconcile policy** (generation = the `cotal-ai` version): a never-seeded built-in is seeded; a\nstill-installed one WE seeded (`source: \"seeded\"`) is refreshed only when the version bumps (semver\ncompare) or under `--force`; an operator-managed official entry (a manual `ext add` at a chosen\nversion, no seeded marker) is left untouched on upgrade; a deliberately-removed one stays removed. The\n`ever-seeded` **authority** (`seed/authority.json`, mirrored to a monotonic `.bak`) is the sole arbiter\nof removed-vs-never-seeded and is unioned with its backup on read, so a truncated authority never\nresurrects a removal. Before writing the generation stamp, setup verifies that every\n(re)installed extension is recorded in the manifest, present on disk with a resolvable entry file,\nand at the generation version. A version-skewed payload fails loud (`ext seed --repair`) rather than being stamped as current. A cotal\n**older** than the store's stamped generation refuses before writing anything, rather than stamping the\nstore back down to its own version while refreshing nothing: run the newer cotal, or `ext seed --reset`\nto rebuild the store for the version you are running. The refusal names a concrete cotal executable\nonly after a bounded `--version` probe proves that executable is at least the store generation;\notherwise it retains the generic instruction. A generation advance records the exact\nrealpath-resolved CLI entry and an ISO timestamp in `seed/stamp.json`, then announces the migration\nafter that stamp commits. An older CLI includes those fields in its refusal when present; legacy\ngeneration-only stamps stay valid and retain the shorter refusal.\n\n**Crash safety.** One shared advisory lock ([`packages/workspace/src/advisory-lock.ts`](../packages/workspace/src/advisory-lock.ts):\natomic hard-link publish, PID + process-start liveness, bounded wait, dead-owner reclaim) guards the\nwhole reconcile and every `cotal ext` mutation; a live reconcile is waited on, not mistaken for a crash.\nA crash **cursor** is journaled before each connector mutation and cleared only at the final commit, so\na SIGKILL mid-run is detected on the next boot (fail loud \u2192 `ext seed --repair` re-installs the\ninterrupted connector before it clears the evidence). Seed children are authenticated (they carry the\nlive lock's nonce + parent PID, not a bare env flag) and record a liveness marker so a post-crash repair\nrefuses to race an orphaned installer. `ext seed --reset` quarantines corrupt manifest/authority state\naside and rebuilds. See [cli.md `ext`](cli.md#ext) for the operator-facing flags.\n"
|
|
16557
|
+
"summary": "cotal setup (implementations/cli/src/commands/setup.ts) is configure-only: it checks prerequisites, installs the Claude Code plugin, and seeds persona files, and it launches nothing: no mesh, no we\u2026",
|
|
16558
|
+
"body": "# Setup internals (maintainer notes)\n\n> **Project** (non-normative maintainer notes) \xB7 **For:** maintainers changing how setup works\n>\n> How `cotal setup` works, and the cross-repo couplings it depends on. If you change one of\n> the things in the **Invariants** table, update the listed siblings in the same change, or\n> setup silently breaks for npx users.\n\n## The flow\n\n`cotal setup`\n([`implementations/cli/src/commands/setup.ts`](../implementations/cli/src/commands/setup.ts))\nis **configure-only**: it checks prerequisites, installs the Claude Code\nplugin, and seeds persona files, and it **launches nothing**: no mesh, no web dashboard, no\nmanager, no delivery daemon, no cmux/tmux session, no demo. Starting the stack is `cotal up`; the\ndashboard is `cotal web`. Every file it writes is announced (`\u2192 wrote \u2026` via `provenance.wrote`).\nIt is two-tier, gated on a machine marker. Persona seeding resolves the selected mesh root first,\nthen uses the same `.cotal/agents` catalog as spawn. With no mesh it names a cwd fallback; an\nambiguous or broken target refuses rather than choosing a root.\n\n**First run** (no `~/.cotal/onboarded.json`, or `--full`, or `--yes`) runs `runFirstRun(yes)`:\n\n- splash \u2192 intro \u2192 core **checks** (Node >= 22; **locate** `nats-server`: located, never\n started) \u2192 **connector picker** \u2192 resolve and announce the persona destination \u2192 seed the generic\n `default` and optional demo personas (david/sven/me) there \u2192 **offer a global install**\n (`offerGlobalInstall`) \u2192 onboarded marker \u2192 a finale that\n lists the commands to start things (`cotal up --detach`, `cotal web`, `cotal spawn \u2026`,\n `cotal console`, `cotal down`). Nothing is running when it returns.\n- The old `--auth` / `--open` flags are **gone**: they set the mesh MODE at launch time, and setup\n no longer launches; mode is now `cotal up [--open]`'s concern (an unknown-option error names\n them, no silent no-op).\n\n**Later runs** run `runEnsure`: resolve and announce the same destination, re-seed the `default`\npersona if it's missing,\nre-offer the **global install** (`offerGlobalInstall`, same `isNpx()` + PATH-scan gate as first\nrun, so a repeat `npx cotal-ai setup` on a machine that still lacks a durable `cotal` finally\ninstalls it), then print the **status card** (`readyCard`). The card is **read-only probes** (`machineStatus`/`meshStatus`/`webUp`/`managerUp` for NATS, the plugin, the mesh, the web\ndashboard, and the manager) and for anything down it prints the exact command to start it\n(`cotal up --detach`, `cotal web`, `cotal supervise`). Displaying state never depends on it; setup\nstill launches nothing.\n\n**`--skills`** is the status-card write: it asks installed connectors with a declared skills setup\nhook to reconcile their own harness, then reconciles `~/.agents/skills`. The base CLI passes only the\nvendor-neutral skills directory, version, and state directory; connector packages own native assets\nand commands. It does not seed personas, install the mesh\nconnector, offer a global install, or write the onboarded stamp. Combined with `--full` or\n`--demo` it is refused.\n\nThe seeded `default` persona has an empty active `subscribe` set and wildcard\n`allowSubscribe`/`allowPublish` ACLs. A fresh agent receives no channel traffic until it joins a\nchannel, but can join, create, read, and post to channels on demand. The guided demo personas keep\ntheir existing `welcome` read and post scope. Repeat setup replaces the prior default template only\nwhen its bytes still match the shipped legacy body with `allowPublish: []`; any user edit makes the\nfile ineligible and leaves it byte-identical.\n\nSteps run in-process via `runSteps`\n([`lib/steps.ts`](../implementations/cli/src/lib/steps.ts)). A step can be `optional` (asked\nY/n), carry a `confirm` consent prompt, or be `live` (it draws its own pane via\n[`lib/live-window.ts`](../implementations/cli/src/lib/live-window.ts)). On failure, an\ninteractive run offers a Claude handoff\n([`lib/assist.ts`](../implementations/cli/src/lib/assist.ts)).\n\nThe **connector picker** (`pickConnectors`) multiselects the **setup connector surface**\n(`setupConnectorSurface`): every connector name the live registry or the installed extension\nmanifest advertises, materialized through the same loader the rest of the CLI uses. No connector\nname is written into `setup.ts`. `setupConnectorCandidates` turns that surface into choices and\nreads each hint off the connector's own declarations: `requires` names the executables a candidate\nstill needs on PATH, `setup` says whether it owns setup actions at all, and `pluginRoot` says\nwhether those actions install plugin assets. A selected candidate runs its connector-owned\n`connector` action through `connectorSetupStep`, which receives the `Connector` itself; a candidate\nthat declares no provider is simply marked ready (OpenCode auto-wires at spawn, injecting its\nplugin via `buildLaunch` and never writing the user's config). The `skills` action runs for every\npresent connector that declares one, selected or not, because Cotal's authored skills are\nindependent of mesh membership. Two experts (david, the engineer; sven, the guide) plus the\noperator's own driving session (`me`) are written by default, and `me` is the persona\n`cotal spawn me` drives.\n\n**`--yes`** forces non-interactive accept-all even on a TTY: optional plus `confirm` steps run\n(so the demo personas are written), the global install takes its default, and a failure aborts\nwith the log path and a non-zero exit. It still launches nothing. The control plane comes up with\n`cotal up --detach`. This is the agent/CI contract; keep it working.\n\n## Invariants\n\n| Thing | Must stay in sync across | Why |\n|---|---|---|\n| Marketplace name **`cotal-mesh`** | `setup.ts` (materialized `marketplace.json`), `CHANNEL_REF` in [`extensions/connector-claude-code/src/extension.ts`](../extensions/connector-claude-code/src/extension.ts), repo [`.claude-plugin/marketplace.json`](../.claude-plugin/marketplace.json) | The wake channel ref `plugin:cotal@cotal-mesh` binds by this name |\n| Plugin assets | the claude connector's own [`src/setup.ts`](../extensions/connector-claude-code/src/setup.ts) copy list (`dist/mcp.cjs`, `dist/hook.cjs`, `.claude-plugin/plugin.json`, `.mcp.json`, `hooks/hooks.json`) and its `package.json` `files` field | The connector materializes its own plugin; missing or renamed assets break the install, and the base CLI has no copy of the list |\n| `Connector.pluginRoot` | [`packages/core/src/connector.ts`](../packages/core/src/connector.ts) (contract) plus set in the claude connector's `extension.ts` | A connector's declaration that it ships installable plugin assets; the picker phrases its hint from it |\n| `BUNDLED_PKG_PREFIX` | [`lib/nats-bin.ts`](../implementations/cli/src/lib/nats-bin.ts) \u2194 the `@eplightning/nats-server-*` `optionalDependencies` in [`implementations/cli/package.json`](../implementations/cli/package.json) | The bundled NATS binary is resolved by `${prefix}-${platform}-${arch}`. (Future: swap the prefix to our own `@cotal-ai/nats-server-*`.) |\n| Onboard marker plus `ONBOARD_VERSION` | `~/.cotal/onboarded.json` in [`lib/onboard.ts`](../implementations/cli/src/lib/onboard.ts); version const in `setup.ts` | Flips first-run vs ensure |\n| Demo-agent format | `DEMO_AGENTS` in `setup.ts` matches the frontmatter shape read by [`packages/core/src/agent-file.ts`](../packages/core/src/agent-file.ts) (same as `examples/01-lateral-coordination/agents/`) | `cotal spawn <name>` loads these |\n| Managed personas | each `DEMO_AGENTS` body carries a `# managed by cotal-setup` frontmatter marker; `writeDemoAgent` refreshes the file when the body changes, backing a marker-less (user-edited) file up to `<name>.md.bak` first | Edit `DEMO_AGENTS` plus re-run setup to update david/sven/me; delete the marker line to take ownership |\n| `DEFAULT_SERVER` | [`packages/core/src/endpoint.ts`](../packages/core/src/endpoint.ts) | The address `cotal up` starts and the status card probes |\n\n## Background processes (`cotal up`)\n\n`cotal up` brings up the whole local stack in one place; since setup became configure-only\n(stage 2b), this is where the mesh and control plane start, so `cotal spawn --detach` /\n`cotal_spawn` find a manager right after `up`. The control plane comes up in cutover order:\nold-manager preflight \u2192 **delivery daemon** (auth mode only) \u2192 **manager**, via\n`ensureControlPlane`\n([`lib/delivery-proc.ts`](../implementations/cli/src/lib/delivery-proc.ts)). The detached\nprocesses, all stopped by `cotal down`:\n\nWith no explicit `--server`, `cotal up` auto-selects a free local port when the default broker\naddress is already held by another root or an unrecorded broker; an explicit `--server` remains\nfail-loud on collision.\n\n- **Mesh:** `startMeshDetached`\n ([`commands/up.ts`](../implementations/cli/src/commands/up.ts)) is the one place that boots a\n background nats-server (foreground `up` and `up --detach` both route through it). Writes\n `.cotal/nats.pid` and tails `.cotal/nats.log`.\n- **Delivery daemon:** `startDeliveryDetached` / `ensureDelivery`\n ([`lib/delivery-proc.ts`](../implementations/cli/src/lib/delivery-proc.ts)) re-execs `cotal\n deliver` detached with a pre-minted scoped `delivery.creds` (auth mode only, the durable\n backstop; open mode has none). Writes `.cotal/delivery.<key>.pid` and `.cotal/delivery.<key>.log`,\n where `<key>` is the space key ([Config](config.md#project-files)), so a root can serve a space per\n daemon.\n- **Manager:** `startManagerDetached` / `ensureManager`\n ([`lib/manager-proc.ts`](../implementations/cli/src/lib/manager-proc.ts)) re-execs `cotal\n supervise` detached (pty runtime); it answers the control plane\n (`cotal_spawn` / `cotal_despawn` / `cotal_persona`). Writes `.cotal/manager.<key>.log`;\n `managerUp(space)` checks that space's pid record for setup's status card. The **manager itself**\n writes `.cotal/manager.<key>.pid`, so a supervisor started by a container entrypoint, by cron, or\n by hand is recorded the same way a detached `cotal up` is. Readers verify the recorded pid is alive and is a\n supervisor before trusting it ([Config](config.md#project-files)).\n\nThe **web dashboard** is *not* part of `cotal up`. It ships inside `cotal-ai` as the `@cotal-ai/web`\nextension and is seeded automatically by the boot reconcile, the same durable, version-locked path as\nthe built-in connectors (`SEEDED_EXTENSIONS`), so it always matches the CLI version and needs no\nseparate install. Start it with `cotal web`; it records\n`.cotal/web.pid`, self-registers that process with `down`, and is addressed as\n`http://cotal.localhost:7799` (binds loopback; `*.localhost` resolves in Chrome/Firefox/Edge,\nSafari may need plain `127.0.0.1`). `webUp()` probes the port for setup's status card.\n\nAll recorded local processes self-register `local-process` descriptors. Bare `cotal down` resolves\nthe full set and stops it in dependency order; `cotal down manager` (or another component name)\nselects only that descriptor. Installed extensions cache their contributed registry keys, so the\nbase CLI does not hardcode optional package pidfiles.\n\nEach recorded pidfile also carries a sibling `<pidfile>.identity` pin (pid plus the process's\nstart, where the OS reports one). The pin proves that the live process is still the recorded one.\nA reused pid and a torn or unreadable pin are refused and preserved. A pre-pin record warns and is\nsignalled so an upgraded CLI can stop a stack launched by the previous version; the next launch\nwrites a pin. A record clears only once death is confirmed.\n\nAll re-execs resolve this CLI via `selfArgv()` / `selfCotal()`\n([`lib/self-exec.ts`](../implementations/cli/src/lib/self-exec.ts)) = `[node, ...loaderFlags,\nentry]` (tsx loader in dev, compiled JS in prod), so they never need `cotal` on PATH; the stack\ncomes up identically via `npx`, `npm i -g`, and a dev clone.\n\nFor ergonomics only, an npx run with no global `cotal` offers to `npm i -g cotal-ai`\n(`offerGlobalInstall`, pinned to the running version): gated on `isNpx()` plus a PATH scan\n(`cotalOnPath()`, not `onPath(\"cotal\")`, since `cotal --version` is not a real command). The\ninteractive prompt defaults to yes, the non-interactive path (`--yes` or no TTY) takes the\ndefault, and a failed install is non-fatal (warn plus manual command). The same `self-exec.ts`\nexposes `displayCmd()`, the prefix (`cotal` / `npx cotal-ai` / `pnpm cotal`) used in the\nstatus-card hints so they match how you ran it.\n\n## Built-in connectors are seeded extensions\n\nThe first-party connectors (`claude`, `opencode`, `codex`, `hermes`, `pi`) are **not** static-imported by\nthe binary. The composition root (`bin/cotal.ts`) registers no connector; they self-register only when\nimported, and they are imported only once installed. On the first real command of each boot the CLI\n**seeds** them through the same `cotal ext add` path a third party uses, so they are ordinary\nextensions you can `cotal ext remove`. Code lives in [`implementations/cli/src/seed/`](../implementations/cli/src/seed/);\nthe entry is `reconcileSeededConnectors()`, gated in `runCli` before the manifest overlay so\n`ext seed --repair` survives a corrupt manifest.\n\n**What ships where.** The connectors are `devDependencies` of `cotal-ai` (not runtime deps), and a\n`prepack` step ([`bin/scripts/copy-seeded-connectors.mjs`](../bin/scripts/copy-seeded-connectors.mjs))\n`npm pack`s each into `bin/seeded-connectors/<name>/` (honoring each connector's own `files`), added to\nthe package `files`. `SEEDED_EXTENSIONS` (`@cotal-ai/workspace`) is the shared list: the\nconnectors plus `web`. The prepack asserts that every bundled payload's `name` and `version` match\nthe umbrella (the\n`fixed` changeset group keeps them lockstep), so a version-skewed payload can never be published; `web`\nalso emits `dist/web/vendor/vendor-manifest.json` (name/version/license/sha512) as the auditable\ninventory of its vendored browser libs (marked/DOMPurify ship as opaque `dist` bytes, not runtime deps).\n`seed/paths.ts:shippedSourceDir` resolves the live `extensions/<pkg>` dir in a\nsource checkout and `<cotal-ai>/seeded-connectors/<name>` in a published install. The reconcile copies\nthat payload into the durable store `seed/store/<version>/<name>` and `ext add --install-links` reifies\nthe `file:` dep from THAT stable path (a volatile source would fail to re-reify); `ext add` then\njunction-links each `@cotal-ai/*` peer to the binary's own copy. Before the first lazy import in each\nprocess, materialization rechecks those links by realpath and rebinds stale links under the extension\nlock. This lets the registry-facing imports of a global install, npx, and source worktrees share the\nmachine prefix while each process still gets its host's single `@cotal-ai/core` registry instance;\nlauncher artifacts are self-contained and do not resolve those mutable links later.\n\n**Reconcile policy** (generation = the `cotal-ai` version): a never-seeded built-in is seeded; a\nstill-installed one WE seeded (`source: \"seeded\"`) is refreshed only when the version bumps (semver\ncompare) or under `--force`; an operator-managed official entry (a manual `ext add` at a chosen\nversion, no seeded marker) is left untouched on upgrade; a deliberately-removed one stays removed. The\n`ever-seeded` **authority** (`seed/authority.json`, mirrored to a monotonic `.bak`) is the sole arbiter\nof removed-vs-never-seeded and is unioned with its backup on read, so a truncated authority never\nresurrects a removal. Before writing the generation stamp, setup verifies that every\n(re)installed extension is recorded in the manifest, present on disk with a resolvable entry file,\nand at the generation version. A version-skewed payload fails loud (`ext seed --repair`) rather than being stamped as current. A cotal\n**older** than the store's stamped generation refuses before writing anything, rather than stamping the\nstore back down to its own version while refreshing nothing: run the newer cotal, or `ext seed --reset`\nto rebuild the store for the version you are running. The refusal names a concrete cotal executable\nonly after a bounded `--version` probe proves that executable is at least the store generation;\notherwise it retains the generic instruction. A generation advance records the exact\nrealpath-resolved CLI entry and an ISO timestamp in `seed/stamp.json`, then announces the migration\nafter that stamp commits. An older CLI includes those fields in its refusal when present; legacy\ngeneration-only stamps stay valid and retain the shorter refusal.\n\n**Crash safety.** One shared advisory lock ([`packages/workspace/src/advisory-lock.ts`](../packages/workspace/src/advisory-lock.ts):\natomic hard-link publish, PID + process-start liveness, bounded wait, dead-owner reclaim) guards the\nwhole reconcile and every `cotal ext` mutation; a live reconcile is waited on, not mistaken for a crash.\nA crash **cursor** is journaled before each connector mutation and cleared only at the final commit, so\na SIGKILL mid-run is detected on the next boot (fail loud \u2192 `ext seed --repair` re-installs the\ninterrupted connector before it clears the evidence). Seed children are authenticated (they carry the\nlive lock's nonce + parent PID, not a bare env flag) and record a liveness marker so a post-crash repair\nrefuses to race an orphaned installer. `ext seed --reset` quarantines corrupt manifest/authority state\naside and rebuilds. See [cli.md `ext`](cli.md#ext) for the operator-facing flags.\n"
|
|
16552
16559
|
},
|
|
16553
16560
|
{
|
|
16554
16561
|
"slug": "spaces",
|
|
@@ -16583,7 +16590,7 @@ var DOCS_BUNDLE = {
|
|
|
16583
16590
|
"title": "Workflow runs",
|
|
16584
16591
|
"kind": "Concept (informative)",
|
|
16585
16592
|
"summary": "A workflow run is a program that coordinates agents over hours or days and survives the process that started it.",
|
|
16586
|
-
"body": '# Workflow runs\n\n> **Concept** (informative) \xB7 **For:** people writing a durable multi-agent workflow, and implementers hosting one \xB7 **Normative:** [SPEC \xA714](../SPEC.md#14-workflow-runs-v05) and the language reference [`spec/cotal-lang.md`](../spec/cotal-lang.md)\n\nA **workflow run** is a program that coordinates agents over hours or days and survives the\nprocess that started it. The program is written in **Cotal Lang**, a small subset of JavaScript in\nwhich every interaction with the world is one of a dozen **effects** (`spawn`, `turn`, `ask`,\n`checkpoint`, `sleep`, `wait`, `notify`, `monitor`, and the four concurrency scopes) and everything\nelse is ordinary, pure JavaScript. Every effect is written into the run\'s **step journal** before\nit is performed and settled after, keyed by where in the program it happened rather than by when,\nso a run that dies is resumed on any host by **re-running the program from the top** with recorded\neffects returning their recorded results. Nothing about the interpreter is ever serialized: the\njournal and the program are the whole state.\n\n## A first program\n\n```js\nconst planner = await spawn("planner")\nconst builder = await spawn("builder", { worktree: "wt-1" })\n\nconst plan = await ask(planner, { name: "plan", schema: { steps: "array" } })\nconst ok = await checkpoint("approve-plan", "Approve the plan?", { timeout: "4h", onExpiry: "proceed" })\nif (ok.status !== "resolved") {\n await notify([planner], { decision: "approve-plan", outcome: "expired" })\n}\n\nconst r = await turn(builder, { name: "build", deadline: "30m" })\nif (r.status === "blocked") {\n await turn(planner, { name: "unblock" })\n}\n\nconst outcome = await race({\n reply: () => wait(replied(builder), { timeout: "20m" }),\n giveUp: () => sleep("1h"),\n}, { name: "await-or-move-on" })\nlog("outcome", outcome.index)\n```\n\nRead it as the flowchart it is. `spawn` brings agents in; `ask` is the narrow case where the\nprogram itself needs a value (`schema` is a record the program hands the handler unchanged; the\nlanguage hashes it and gives it no meaning, and no handler in this repository enforces one yet);\n`checkpoint` is a durable pause a human resolves from anywhere, raced against a durable timer; `turn`\nwakes an agent for one turn and returns how it yielded; `race` runs two branches and keeps the one\nwhose recorded clock is earliest. Agents talk to each other in channels as they always do; the\nprogram never speaks in a channel, and the one thing it can put in front of an agent (`notify`) is a\nbounded decision record, not prose.\n\n## The mental model\n\n- **Pure code is JavaScript.** Loops, records, arrays, closures, template literals, destructuring,\n `try`/`catch`, arithmetic, `switch`, compound assignment, optional chaining, spread and rest: what\n you would write anyway, with the parts that hide effects or make meaning depend on the host removed\n (`class`, `this`, `new`, `for...in`, `==`, labels, regex literals, `Math`/`Date`/`JSON`, promises,\n generators). Every refusal names its code and the edit that fixes it. The builtins are a short list\n (`keys`, `map`, `sort`, `json.stringify`, `now()`, `random()`), and arrays, strings and numbers\n answer their usual methods (`xs.map`, `s.trim()`, `n.toFixed()`) and nothing outside that table.\n Records and arrays you build are yours to change until they cross an effect boundary; a member you\n do not own, a host prototype, or a value another branch built is refused with a code, never a\n surprise.\n- **Every effect is journalled and hashed.** A step is keyed `(scope path, kind, name, occurrence)`\n and its inputs are hashed. Reorder your program, add a step, rename a variable: recorded steps\n still match. Change what a step asks (a checkpoint\'s prompt, a sleep\'s duration, a turn\'s\n deadline) and the resume stops with a **divergence** naming the step, rather than replaying an\n answer to a question the program no longer asks.\n- **Concurrency is visible.** `parallel`, `race`, `fanOut` and `conclave` are the only ways to do\n two things at once, each branch gets its own journal namespace, and the scope writes its own\n entry saying how it settled: which arm won a race is a recorded fact, decided by the arms\'\n recorded clocks and declaration order, never by a scheduler. A branch may not write to anything\n declared outside it; return the value and read it out of the scope\'s result.\n- **Time and randomness are tamed.** `now()` is the branch\'s run clock, the end of the last effect\n it awaited; `random()` is a seeded stream derived per scope. Both replay identically.\n- **Values freeze at the boundary.** What crossed into or out of an effect is what the journal\n recorded, and it cannot change afterwards; build a new value.\n- **The journal is the debugger.** Every entry carries its key, its inputs\' hash, its outcome and\n its timing, and every error is in the program\'s own coordinates. A run can be **simulated** with a\n scripted handler and **dry-run** to a plan before it touches an agent. The simulator is\n discrete-event: timed effects park at their wake times and are delivered in wake order on one\n virtual clock, so concurrent branches accumulate the durations they wrote and a simulated `race`\n is decided by the same rule a live handler produces (least recorded clock, ties by declaration\n order). A `sleep("1m")` arm beats a `sleep("1h")` arm whatever their declaration order.\n\nFull rules, with every code: [`spec/cotal-lang.md`](../spec/cotal-lang.md).\n\n## Continuing a run\n\n**Resume** is re-execution: the driver replays the journal, the program runs from the top, recorded\nsteps return instantly, and the first unrecorded step is performed live. It refuses a journal that\nbelongs to another run, a pin that differs from the recorded ones, and a different language version.\n\n**Migrate** moves a run onto edited source. A dry walk of the new program over the recorded journal\nfinds every recorded step the edit changed (a divergence) and every one it no longer reaches (an\norphan), and the orphan table says what each means: a removed `sleep` is nothing, a removed `turn`\nalready happened, a removed `spawn` is a live agent you must adopt or release, a removed resolved\n`checkpoint` is a human decision you must explicitly discard. The decision is filed as a\n`migration` record with the actor\'s name on it.\n\n**Fork** starts a new run from a named step of an old one, copying the prefix under the parent\'s\npins (seed included, so the copied history\'s pure draws are the same draws). The child is a new run\nunder a new id whose record names the parent and the cut step (`forkedFrom`); the parent is\nuntouched.\n\n## Operating a run\n\n`cotal run` is the operator surface over the driver. Every verb opens one connection to the\nresolved mesh target (the usual `--space` / `--server` / `--creds` flags). `start`, `resume` and\n`answer` drive and exit when the drive settles; `ps` and `journal` inspect and exit at once.\n`start` mints the run id and prints it, and the record never takes a caller-supplied one.\n\n```bash\ncotal run start --file build.cotal.js # drive a new run; the minted id is printed\ncotal run ps # list run records: state, holder, lineage\ncotal run journal run-3f2a90c41b7e0d5a6c884e19b02df4a1 # print the durable step journal\ncotal run resume run-3f2a90c41b7e0d5a6c884e19b02df4a1 --file build.cotal.js # take the run over and continue it\ncotal run answer run-3f2a90c41b7e0d5a6c884e19b02df4a1 "/checkpoint:approve#0" --by dana --value \'"yes"\'\n```\n\n`start` and `resume` need `--file`: the record stores no source, so the caller supplies the same\nprogram (handing an edited one is a migration decision, and the resume stops on the divergence).\nA run whose step was refused (L5016) exits with code 2 and stays held; `resume` on a host that can\nperform the step performs it live and continues from there. `answer` resolves an open checkpoint\nthrough the run driver, presenting as the arming holder, with the answerer\'s name on the record.\nCheckpoint expiry rides the mediated timer writer, which the delivery daemon pumps on a live mesh;\non a bare broker a pause still resolves, it just cannot expire.\n\n## What is on the wire\n\nThe run\'s wire footprint is [SPEC \xA714](../SPEC.md#14-workflow-runs-v05):\n\n| Thing | Where | What it is |\n| --- | --- | --- |\n| the run | `run.<endpoint>.<runId>` record | the resolved **pins** (seed, logical epoch, budgets, language version) on the immutable half; holder, lease and `journalHigh` on the status half |\n| the step journal | `WFJ_<space>` stream, one subject per run | append-only, no age eviction, no Direct Get; every append fenced by the run subject\'s own sequence; takeover is replay-then-activate |\n| a checkpoint answer | `answer.<endpoint>.<token>.<answerId>` | the payload beside the one-use settle fact; the settle names the answer it accepted |\n| a notice | `notice.<endpoint>.<runId>.<addresseeId>.<noticeId>` | one bounded decision told to one agent, rendered ahead of its next turn |\n| a migration | `migration.<endpoint>.<runId>.<migrationId>` | the report and who applied it, keyed by the report\'s own digest |\n\nA run\'s **driver** holds publish on only its own run\'s subject and its own replay durable, never\na space-wide grant.\n\n## What ships today\n\nThe language, its validator, interpreter, simulator and dry run are `@cotal-ai/lang`\n(`packages/lang`), usable in-process with your own effect handler and with no broker: `validate(src)`,\nthen `run(src, { runId, handler })`, and `resume(src, journal, { runId, pins, handler })` to pick a\nrun up from its journal (the package README has the snippet, with `SimHandler` as the handler). That\nis the in-process route, yours to drive with your own handler; a run the driver starts executes on\nthe compiled engine, as the engine paragraph below says. The wire\nsubstrate of \xA714 (the `WFJ_<space>` stream, the four record kinds, the activation barrier, the\nper-run grants) is in `@cotal-ai/core`, and the run driver, journal store, migrate and fork are\n`@cotal-ai/runtime` (`implementations/runtime`). On the mesh handler, `sleep`, `checkpoint`,\n`wait(message(...))`, `wait(idle(...))` and `notify` are durable; `spawn`, `turn`, `ask`,\n`monitor`, `wait(replied(...))`, `wait(down(...))` and `conclave` refuse with **L5016 (effect not\ndurable on this host)** until the durable action machinery they ride lands. That refusal holds the\nrun rather than ending it: the entry is settled `refused` (never `failed`, since nothing was\nattempted), the run unwinds with the uncatchable L5025 and is recorded `released`, and a resume on\na host that can perform the step performs it live. A run started today heals the day those effects\nland. The operator surface over the driver is `cotal run`; the section above has the verbs.\n\n**Two engines, and which one runs your program.** The tree-walker is language version `1` and the\ncompiled engine is version `2`, two languages rather than two speeds of one (`spec/cotal-lang.md`\n\xA78.4 lists what differs). The driver hosts both: **every run a driver starts is stamped `2` and\nexecuted by the compiled engine**. The program runs in its own locked-down worker thread with\nnothing in its global scope, while the effects and the durable journal stay in the driver\'s process,\nbridged over a message port. No socket or credential enters the isolate holding the program,\nand **every version-`1` record keeps replaying on the walker**, which is the walker\'s job. The\ndriver serves a declared set of versions, and a record whose version it does not serve is refused\nby name (**L5023**) with the run left untouched, instead of being replayed by whichever engine\nhappens to be present. Records do not cross between versions in either direction; the repair is to\nresume on the recorded version, or to fork.\n\n**The engine needs node 22 or newer** and refuses below it with **L1000**, which is an\nimplementation limit and not a language error, so you will not find it in the catalog. It is a floor\nrather than a warning because the engine\'s frame plumbing rests on `AsyncLocalStorage`, and 22 is\nthe lowest node it has been measured on. The walker has no such floor.\n'
|
|
16593
|
+
"body": '# Workflow runs\n\n> **Concept** (informative) \xB7 **For:** people writing a durable multi-agent workflow, and implementers hosting one \xB7 **Normative:** [SPEC \xA714](../SPEC.md#14-workflow-runs-v05) and the language reference [`spec/cotal-lang.md`](../spec/cotal-lang.md)\n\nA **workflow run** is a program that coordinates agents over hours or days and survives the\nprocess that started it. The program is written in **Cotal Lang**, a small subset of JavaScript in\nwhich every interaction with the world is one of a dozen **effects** (`spawn`, `turn`, `ask`,\n`checkpoint`, `sleep`, `wait`, `notify`, `monitor`, and the four concurrency scopes) and everything\nelse is ordinary, pure JavaScript. Every effect is written into the run\'s **step journal** before\nit is performed and settled after, keyed by where in the program it happened rather than by when,\nso a run that dies is resumed on any host by **re-running the program from the top** with recorded\neffects returning their recorded results. Nothing about the interpreter is ever serialized: the\njournal and the program are the whole state.\n\n## A first program\n\n```js\nconst planner = await spawn("planner")\nconst builder = await spawn("builder", { worktree: "wt-1" })\n\nconst plan = await ask(planner, { name: "plan", schema: { steps: "array" } })\nconst ok = await checkpoint("approve-plan", "Approve the plan?", { timeout: "4h", onExpiry: "proceed" })\nif (ok.status !== "resolved") {\n await notify([planner], { decision: "approve-plan", outcome: "expired" })\n}\n\nconst r = await turn(builder, { name: "build", deadline: "30m" })\nif (r.status === "blocked") {\n await turn(planner, { name: "unblock" })\n}\n\nconst outcome = await race({\n reply: () => wait(replied(builder), { timeout: "20m" }),\n giveUp: () => sleep("1h"),\n}, { name: "await-or-move-on" })\nlog("outcome", outcome.index)\n```\n\nRead it as the flowchart it is. `spawn` brings agents in; `ask` is the narrow case where the\nprogram itself needs a value (`schema` is a record the program hands the handler unchanged; the\nlanguage hashes it and gives it no meaning, and the reference simulator enforces the shorthand\ncontract on it);\n`checkpoint` is a durable pause a human resolves from anywhere, raced against a durable timer; `turn`\nwakes an agent for one turn and returns how it yielded; `race` runs two branches and keeps the one\nwhose recorded clock is earliest. Agents talk to each other in channels as they always do; the\nprogram never speaks in a channel, and the one thing it can put in front of an agent (`notify`) is a\nbounded decision record, not prose.\n\n## The mental model\n\n- **Pure code is JavaScript.** Loops, records, arrays, closures, template literals, destructuring,\n `try`/`catch`, arithmetic, `switch`, compound assignment, optional chaining, spread and rest: what\n you would write anyway, with the parts that hide effects or make meaning depend on the host removed\n (`class`, `this`, `new`, `for...in`, `==`, labels, regex literals, `Math`/`Date`/`JSON`, promises,\n generators). Every refusal names its code and the edit that fixes it. The builtins are a short list\n (`keys`, `map`, `sort`, `json.stringify`, `now()`, `random()`), and arrays, strings and numbers\n answer their usual methods (`xs.map`, `s.trim()`, `n.toFixed()`) and nothing outside that table.\n Records and arrays you build are yours to change until they cross an effect boundary; a member you\n do not own, a host prototype, or a value another branch built is refused with a code, never a\n surprise.\n- **Every effect is journalled and hashed.** A step is keyed `(scope path, kind, name, occurrence)`\n and its inputs are hashed. Reorder your program, add a step, rename a variable: recorded steps\n still match. Change what a step asks (a checkpoint\'s prompt, a sleep\'s duration, a turn\'s\n deadline) and the resume stops with a **divergence** naming the step, rather than replaying an\n answer to a question the program no longer asks.\n- **Concurrency is visible.** `parallel`, `race`, `fanOut` and `conclave` are the only ways to do\n two things at once, each branch gets its own journal namespace, and the scope writes its own\n entry saying how it settled: which arm won a race is a recorded fact, decided by the arms\'\n recorded clocks and declaration order, never by a scheduler. A branch may not write to anything\n declared outside it; return the value and read it out of the scope\'s result.\n- **Time and randomness are tamed.** `now()` is the branch\'s run clock, the end of the last effect\n it awaited; `random()` is a seeded stream derived per scope. Both replay identically.\n- **Values freeze at the boundary.** What crossed into or out of an effect is what the journal\n recorded, and it cannot change afterwards; build a new value.\n- **The journal is the debugger.** Every entry carries its key, its inputs\' hash, its outcome and\n its timing, and every error is in the program\'s own coordinates. A run can be **simulated** with a\n scripted handler and **dry-run** to a plan before it touches an agent. The simulator is\n discrete-event: timed effects park at their wake times and are delivered in wake order on one\n virtual clock, so concurrent branches accumulate the durations they wrote and a simulated `race`\n is decided by the same rule a live handler produces (least recorded clock, ties by declaration\n order). A `sleep("1m")` arm beats a `sleep("1h")` arm whatever their declaration order.\n\nFull rules, with every code: [`spec/cotal-lang.md`](../spec/cotal-lang.md).\n\n## Continuing a run\n\n**Resume** is re-execution: the driver replays the journal, the program runs from the top, recorded\nsteps return instantly, and the first unrecorded step is performed live. It refuses a journal that\nbelongs to another run, a pin that differs from the recorded ones, and a different language version.\n\n**Migrate** moves a run onto edited source. A dry walk of the new program over the recorded journal\nfinds every recorded step the edit changed (a divergence) and every one it no longer reaches (an\norphan), and the orphan table says what each means: a removed `sleep` is nothing, a removed `turn`\nalready happened, a removed `spawn` is a live agent you must adopt or release, a removed resolved\n`checkpoint` is a human decision you must explicitly discard. The decision is filed as a\n`migration` record with the actor\'s name on it.\n\n**Fork** starts a new run from a named step of an old one, copying the prefix under the parent\'s\npins (seed included, so the copied history\'s pure draws are the same draws). The child is a new run\nunder a new id whose record names the parent and the cut step (`forkedFrom`); the parent is\nuntouched.\n\n## Operating a run\n\n`cotal run` is the operator surface over the driver. Every verb opens one connection to the\nresolved mesh target (the usual `--space` / `--server` / `--creds` flags). `start`, `resume` and\n`answer` drive and exit when the drive settles; `ps` and `journal` inspect and exit at once.\n`start` mints the run id and prints it, and the record never takes a caller-supplied one.\n\n```bash\ncotal run start --file build.cotal.js # drive a new run; the minted id is printed\ncotal run ps # list run records: state, holder, lineage\ncotal run journal run-3f2a90c41b7e0d5a6c884e19b02df4a1 # print the durable step journal\ncotal run resume run-3f2a90c41b7e0d5a6c884e19b02df4a1 --file build.cotal.js # take the run over and continue it\ncotal run answer run-3f2a90c41b7e0d5a6c884e19b02df4a1 "/checkpoint:approve#0" --by dana --value \'"yes"\'\n```\n\n`start` and `resume` need `--file`: the record stores no source, so the caller supplies the same\nprogram (handing an edited one is a migration decision, and the resume stops on the divergence).\nA run whose step was refused (L5016) exits with code 2 and stays held; `resume` on a host that can\nperform the step performs it live and continues from there. `answer` resolves an open checkpoint\nthrough the run driver, presenting as the arming holder, with the answerer\'s name on the record.\nCheckpoint expiry rides the mediated timer writer, which the delivery daemon pumps on a live mesh;\non a bare broker a pause still resolves, it just cannot expire.\n\n## What is on the wire\n\nThe run\'s wire footprint is [SPEC \xA714](../SPEC.md#14-workflow-runs-v05):\n\n| Thing | Where | What it is |\n| --- | --- | --- |\n| the run | `run.<endpoint>.<runId>` record | the resolved **pins** (seed, logical epoch, budgets, language version) on the immutable half; holder, lease and `journalHigh` on the status half |\n| the step journal | `WFJ_<space>` stream, one subject per run | append-only, no age eviction, no Direct Get; every append fenced by the run subject\'s own sequence; takeover is replay-then-activate |\n| a checkpoint answer | `answer.<endpoint>.<token>.<answerId>` | the payload beside the one-use settle fact; the settle names the answer it accepted |\n| a notice | `notice.<endpoint>.<runId>.<addresseeId>.<noticeId>` | one bounded decision told to one agent, rendered ahead of its next turn |\n| a migration | `migration.<endpoint>.<runId>.<migrationId>` | the report and who applied it, keyed by the report\'s own digest |\n\nA run\'s **driver** holds publish on only its own run\'s subject and its own replay durable, never\na space-wide grant.\n\n## What ships today\n\nThe language, its validator, interpreter, simulator and dry run are `@cotal-ai/lang`\n(`packages/lang`), usable in-process with your own effect handler and with no broker: `validate(src)`,\nthen `run(src, { runId, handler })`, and `resume(src, journal, { runId, pins, handler })` to pick a\nrun up from its journal (the package README has the snippet, with `SimHandler` as the handler). That\nis the in-process route, yours to drive with your own handler; a run the driver starts executes on\nthe compiled engine, as the engine paragraph below says. The wire\nsubstrate of \xA714 (the `WFJ_<space>` stream, the four record kinds, the activation barrier, the\nper-run grants) is in `@cotal-ai/core`, and the run driver, journal store, migrate and fork are\n`@cotal-ai/runtime` (`implementations/runtime`). On the mesh handler, `sleep`, `checkpoint`,\n`wait(message(...))`, `wait(idle(...))` and `notify` are durable; `spawn`, `turn`, `ask`,\n`monitor`, `wait(replied(...))`, `wait(down(...))` and `conclave` refuse with **L5016 (effect not\ndurable on this host)** until the durable action machinery they ride lands. That refusal holds the\nrun rather than ending it: the entry is settled `refused` (never `failed`, since nothing was\nattempted), the run unwinds with the uncatchable L5025 and is recorded `released`, and a resume on\na host that can perform the step performs it live. A run started today heals the day those effects\nland. The operator surface over the driver is `cotal run`; the section above has the verbs.\n\n**Two engines, and which one runs your program.** The tree-walker is language version `1` and the\ncompiled engine is version `2`, two languages rather than two speeds of one (`spec/cotal-lang.md`\n\xA78.4 lists what differs). The driver hosts both: **every run a driver starts is stamped `2` and\nexecuted by the compiled engine**. The program runs in its own locked-down worker thread with\nnothing in its global scope, while the effects and the durable journal stay in the driver\'s process,\nbridged over a message port. No socket or credential enters the isolate holding the program,\nand **every version-`1` record keeps replaying on the walker**, which is the walker\'s job. The\ndriver serves a declared set of versions, and a record whose version it does not serve is refused\nby name (**L5023**) with the run left untouched, instead of being replayed by whichever engine\nhappens to be present. Records do not cross between versions in either direction; the repair is to\nresume on the recorded version, or to fork.\n\n**The engine needs node 22 or newer** and refuses below it with **L1000**, which is an\nimplementation limit and not a language error, so you will not find it in the catalog. It is a floor\nrather than a warning because the engine\'s frame plumbing rests on `AsyncLocalStorage`, and 22 is\nthe lowest node it has been measured on. The walker has no such floor.\n'
|
|
16587
16594
|
}
|
|
16588
16595
|
],
|
|
16589
16596
|
"spec": {
|