@cotal-ai/pi 0.30.2 → 0.31.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/connector.d.ts +1 -0
- package/dist/connector.d.ts.map +1 -1
- package/dist/index.js +57 -38
- package/dist/standalone.js +8 -8
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1321,7 +1321,6 @@ var OS_ENV_ALLOW = [
|
|
|
1321
1321
|
"CommonProgramFiles",
|
|
1322
1322
|
"PUBLIC"
|
|
1323
1323
|
];
|
|
1324
|
-
var SESSION_ENV_PREFIX = "COTAL_";
|
|
1325
1324
|
var OPERATOR_ENV_KEEP = [
|
|
1326
1325
|
"COTAL_HOME",
|
|
1327
1326
|
"COTAL_FEEDBACK_KEY",
|
|
@@ -1344,35 +1343,24 @@ var OPERATOR_ENV_KEEP = [
|
|
|
1344
1343
|
"COTAL_ORCA_BIN"
|
|
1345
1344
|
];
|
|
1346
1345
|
function launchEnv(opts = {}) {
|
|
1347
|
-
if (opts.envAllow !== void 0) {
|
|
1348
|
-
const env2 = {};
|
|
1349
|
-
const sourceKey = /* @__PURE__ */ new Map();
|
|
1350
|
-
for (const k of Object.keys(process.env))
|
|
1351
|
-
sourceKey.set(k.toLowerCase(), k);
|
|
1352
|
-
const copy = (name) => {
|
|
1353
|
-
const src = sourceKey.get(name.toLowerCase());
|
|
1354
|
-
if (src === void 0)
|
|
1355
|
-
return;
|
|
1356
|
-
const v2 = process.env[src];
|
|
1357
|
-
if (v2 !== void 0)
|
|
1358
|
-
env2[src] = v2;
|
|
1359
|
-
};
|
|
1360
|
-
for (const k of OS_ENV_ALLOW)
|
|
1361
|
-
copy(k);
|
|
1362
|
-
for (const k of [...opts.envAllow, ...opts.mcpKeys ?? []])
|
|
1363
|
-
copy(k);
|
|
1364
|
-
return env2;
|
|
1365
|
-
}
|
|
1366
|
-
const keep = new Set(OPERATOR_ENV_KEEP);
|
|
1367
1346
|
const env = {};
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1347
|
+
const sourceKey = /* @__PURE__ */ new Map();
|
|
1348
|
+
for (const k of Object.keys(process.env))
|
|
1349
|
+
sourceKey.set(k.toLowerCase(), k);
|
|
1350
|
+
const copy = (name) => {
|
|
1351
|
+
const src = sourceKey.get(name.toLowerCase());
|
|
1352
|
+
if (src === void 0)
|
|
1353
|
+
return;
|
|
1354
|
+
const v2 = process.env[src];
|
|
1355
|
+
if (v2 !== void 0)
|
|
1356
|
+
env[src] = v2;
|
|
1357
|
+
};
|
|
1358
|
+
for (const k of OS_ENV_ALLOW)
|
|
1359
|
+
copy(k);
|
|
1360
|
+
for (const k of OPERATOR_ENV_KEEP)
|
|
1361
|
+
copy(k);
|
|
1362
|
+
for (const k of [...opts.providerKeys ?? [], ...opts.mcpKeys ?? [], ...opts.envAllow ?? []])
|
|
1363
|
+
copy(k);
|
|
1376
1364
|
return env;
|
|
1377
1365
|
}
|
|
1378
1366
|
function aclEnv(opts) {
|
|
@@ -16080,7 +16068,7 @@ import { isConcreteChannel as isConcreteChannel3, channelInAllow as channelInAll
|
|
|
16080
16068
|
|
|
16081
16069
|
// ../connector-core/dist/docs-bundle.generated.js
|
|
16082
16070
|
var DOCS_BUNDLE = {
|
|
16083
|
-
"version": "0.
|
|
16071
|
+
"version": "0.31.0",
|
|
16084
16072
|
"generatedFrom": "docs/*.md + SPEC.md + spec/cotal-lang.md + spec/cotal.schema.json",
|
|
16085
16073
|
"pages": [
|
|
16086
16074
|
{
|
|
@@ -16102,7 +16090,7 @@ var DOCS_BUNDLE = {
|
|
|
16102
16090
|
"title": "Architecture",
|
|
16103
16091
|
"kind": "Concept (informative)",
|
|
16104
16092
|
"summary": "Cotal is built as a thin waist: the normative wire contract (subjects, message schemas, presence/discovery, delivery semantics, the auth grammar) is the standard (SPEC), and everything else is a pl\u2026",
|
|
16105
|
-
"body": "# Architecture\n\n> **Concept** (informative) \xB7 **For:** anyone who wants to know how Cotal is built, and why \xB7 **Normative:** [SPEC](../SPEC.md)\n\nCotal is built as a thin waist: the normative wire contract (subjects, message schemas,\npresence/discovery, delivery semantics, the auth grammar) is the standard\n([SPEC](../SPEC.md)), and everything else is a pluggable edge over existing building\nblocks. Identity, transport, storage, and discovery compose from proven pieces (NATS,\nJetStream, JWT/nkeys) rather than being reinvented. Adapters stay thin and swappable, and\nnothing adapter-specific leaks into the core.\n\n## Influences: A2A\n\nCotal reuses A2A's vocabulary and shapes so it stays interoperable rather than siloed, and\nimplements them over NATS/JetStream.\n\n**From A2A** come the *data shapes*: `AgentCard` (identity / role / tags / skills),\n`Message` / `Part` (text and data), and correlation ids (`contextId`). We do not adopt\nA2A's HTTP/JSON-RPC transport, `Task` RPCs, or its request/response server model, none of\nwhich fit lateral pub/sub.\n\nThe *addressing model* is Cotal's own: the hierarchical address `space / service / instance`\nand three delivery modes, multicast, unicast, anycast\n([presence & delivery](presence-and-delivery.md)). **Mentions** are a priority hint on a\nmulticast, not a routing target. NATS/JetStream is the data plane, adding the durability and\npresence a bare pub/sub layer leaves to the app.\n\nIdentity is an A2A `AgentCard` whose instance id is shaped to later become a **DID**\n(`did:key`) so authenticity can survive an untrusted relay ([roadmap](roadmap.md)).\n\n## One wire, mapped onto NATS\n\nThe messaging plane rides three subject kinds, with the sender encoded in the subject\nitself, where the server can police it, rather than in a self-asserted payload field\n([SPEC \xA73](../SPEC.md#3-subject-layout)); the endpoint control surface adds its own rails\n([SPEC \xA713](../SPEC.md#13-endpoint-control-surface-v04)):\n\n| Delivery | Subject |\n|---|---|\n| multicast | `cotal.<space>.chat.<owner>.<actor>.<channel\u2026>` |\n| unicast | `cotal.<space>.inst.<toOwner>.<toActor>.<owner>.<actor>` |\n| anycast | `cotal.<space>.svc.<role>.<owner>.<actor>` |\n| endpoint (control) | `cotal.<space>.ep.<one\\|all\\|inst\\|reply>.\u2026` ([\xA713.2](../SPEC.md#132-grammar)) |\n\nThe sender is a **principal**, an `owner.actor` pair: the account the agent acts on behalf\nof, then the agent's own handle under it ([identity & auth](identity-and-auth.md)). Two\ntokens instead of one means the broker can deny cross-owner *and* same-owner cross-actor\nforgery in the subject grammar itself.\n\nBehind the subjects, each space gets three **JetStream streams** (chat / DM / task, for\nstorage, per-reader bookmarks, and history), **KV buckets** for presence and the channel\nregistry, and the endpoint control surface on its own rails and streams\n([SPEC \xA713](../SPEC.md#13-endpoint-control-surface-v04)). Rather than re-implementing delivery\nguarantees, Cotal uses the native NATS mechanisms: streams for at-least-once and late\njoin, queue groups for anycast load-balancing, KV TTL for liveness ([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding);\nthe reasoning: [presence & delivery](presence-and-delivery.md)). Isolation is one NATS\n**account per space** ([spaces & channels](spaces.md)); authorization is per-agent JWT\nACLs ([identity & auth](identity-and-auth.md)). Large artifacts are reserved for a\nper-space Object Store ([roadmap](roadmap.md)).\n\nWhether any of this *requires* NATS is answered in\n[transport vs protocol](transport.md): the contract is transport-agnostic; NATS/JetStream\nis the reference binding.\n\n## Package layout: one-way tiers\n\n```\nexamples \u2500\u2500\u2192 implementations \u2500\u2500\u2192 workspace \u2500\u2500\u2192 core \u2190(peer)\u2500\u2500 extensions\n (interoperate at runtime over NATS, not via imports)\n```\n\n- **`@cotal-ai/core`**, the protocol: subjects, schemas, the NATS client layer, and the\n extension contracts (`Connector`, `Command`, `Runtime`) with the `Registry` they\n self-register into. Depends on nothing else in the repo.\n- **`@cotal-ai/workspace`**, the machine-local operator layer over `~/.cotal`: mesh\n registry, target resolution, auth-path helpers. Not part of the wire standard, so a\n third party can embed core without inheriting workstation plumbing.\n- **`extensions/*`**: pluggable adapters (connectors, runtimes). Each **peer-depends** on\n core (binding to the host's single core instance) and self-registers on import; an\n unknown agent type **throws**, no silent fallback.\n- **`implementations/*`**, opinionated surfaces over core: the CLI, the manager, the\n delivery daemon, the web dashboard. Implementations never import each other; they meet\n at runtime, in a shared space over NATS. A composition root (the `cotal` binary, or an\n example) wires the pieces it wants.\n- **`examples/*`**: use-cases and composition roots, never published\n ([examples](examples.md)). An example only configures and orchestrates; new message\n kinds or subjects go into core, generalized, never into an example.\n\nThe published binary also loads **operator-installed extensions**: `cotal ext add\n<npm-package>` installs into a cotal-owned prefix, imports once so the package\nself-registers, then caches every contributed `kind:name`. Command metadata is cached for\n`--help`/completion; running a command or requesting a provider imports its owner lazily and\nuses the live object. Before that first import, the loader rebinds shared peers to the current\nhost under the extension-prefix lock; version skew or an unbindable peer fails loudly.\nThe repo's `@cotal-ai/web` dashboard and optional tmux/cmux/Orca/Herdr runtimes use this mechanism.\nRuntime resolution stays registry-driven and open-ended: a name with no registered/installed\nprovider fails loud (never a fallback), and a third-party runtime installs under its own package\nname. The CLI does carry a small, non-authoritative map of the first-party runtime names\n(`orca`/`tmux`/`cmux`/`herdr`) to their `@cotal-ai/*` packages, used only to print an exact `cotal ext add`\nhint for a known-but-uninstalled runtime and to list them in `cotal runtimes`; it never resolves or\nregisters a provider.\n\nMachine-local processes use the same registry. The base CLI contributes broker/control-plane\n`local-process` descriptors, while an installed package contributes its own (for example `web`).\nThat keeps `cotal down <component>` and `cotal status` extensible without teaching the base CLI\npackage-specific pidfiles. A provider process claims its declared pidfile with exclusive create;\nextension removal reserves that same path so startup cannot cross uninstall.\n\nBeyond the app-bound connectors, `@cotal-ai/pi` is a **host-native plugin**: a pi extension\nloaded into the user's own pi (CLI or SDK-embedded), placing a Cotal endpoint inside the\nsession's process and driving its run loop off the inbox \u2014 see\n[connect-pi](connect-pi.md).\n\n## Connectors: four surfaces, one runtime\n\nEvery coding-agent integration exposes the same four surfaces:\n\n| Surface | Carries |\n|---|---|\n| Outbound, ambient | lifecycle \u2192 presence and activity, automatically |\n| Outbound, deliberate | the messaging tools (`cotal_send` / `cotal_dm` / `cotal_anycast`) |\n| Inbound, pull | `cotal_inbox` |\n| Inbound, push | wake-and-inject into the live session |\n\nThe shared runtime lives in [`@cotal-ai/connector-core`](../extensions/connector-core):\nthe mesh agent, the [`cotal_*` tool surface](mcp-tools.md) (defined once in its tool\nspecs, so it cannot drift across hosts), and the delivery buffer with its attention\npolicy. Each adapter is a thin client\nover it that binds to its host's native mechanism: an installed plugin + MCP server for\n[Claude Code](connect-claude.md), an in-process plugin for\n[OpenCode](connect-opencode.md) (beta), a Python sidecar for\n[Hermes](connect-hermes.md) (alpha), a host-native extension for\n[pi](connect-pi.md) (alpha). The [connectors matrix](connectors.md) compares them\nfeature-by-feature.\n\nThe endpoint underneath self-heals: when the transport connection dies terminally, a\nsupervisor rebuilds it (rebuilds are serialized and coalesced), and unacked in-flight\nmessages redeliver on the rebound durables, so nothing is lost across the gap. A manual\n`/reconnect` is the human-invoked counterpart.\n\n## Manager: a supervisor, not an orchestrator\n\nThe CLI does not spawn agents itself; a long-lived **manager** owns their lifecycle,\nasked over the mesh. The manager is not a privileged control plane: it is an ordinary\nservice endpoint on the same `ep` rails as any other daemon\n([\xA713](../SPEC.md#13-endpoint-control-surface-v04)), holding only the capability rows its\ncallers grant it. It owns process lifecycle and config binding (start / stop / restart,\nbinding env and policy) and has no say in what work the agents do. Agents coordinate\nlaterally; the manager only births and configures them.\n\n- **Off the message hot path.** Each agent self-connects to the mesh through its own\n connector. The manager owns processes in order to control them, but observes everything\n through presence, so a bring-your-own-terminal agent it never spawned still shows up in\n `ps`.\n- **Pluggable runtimes.** Spawning is abstracted behind a `Runtime` contract (like pm2 or\n docker for agent TUIs): **`pty`** ships built-in (the manager owns a pseudo-terminal;\n watch or type via `cotal attach`); **`tmux`**, **`cmux`**, **`orca`**, and **`herdr`** are\n extensions that put each teammate in its own native terminal surface (explicit opt-ins\n that throw when the extension isn't loaded, never a silent fallback); **byo** is the\n floor (a human's own terminal, tracked via presence); **host** (Agent SDK, true mid-turn\n interrupt) is the documented upgrade path ([roadmap](roadmap.md)).\n- **Served commands.** `spawn` (an action, below), `stop`, `ps`, `status`, `attach`,\n `models`, `definePersona`, and `bind` are endpoint commands\n ([\xA713.5](../SPEC.md#135-verbs)) any authorized node can send, policy-gated\n ([identity & auth](identity-and-auth.md)). A caller learns them off the wire with `cotal\n describe manager`; nothing is compiled in.\n- **Spawn is an action.** Asking for an agent no longer blocks the caller while the process\n comes up. The manager accepts a spawn **goal** ([\xA713.6](../SPEC.md#136-composites)) and\n immediately returns the allocated identity (the agent's name, its `owner`/`actor`/`uid`\n triple, a `goalId`, and the executor coordinate `{lifecycleUid, epoch}`); progress events\n then report the launch until a terminal outcome. Presence within the readiness window is\n `succeeded`, an early exit is `failed`, and the window passing with neither is\n `uncertain`: a bounded, reconcilable outcome a later `ps` settles against the live roster,\n never a silent hang.\n- **Bounded spawn.** A gate caps concurrent and in-flight agents and a minimum-lifetime\n floor bounds spawn/despawn churn, so a capability-holding but compromised peer cannot\n fork-bomb the host. The gate runs at goal acceptance, before any identity is minted or\n process launched, so a refused spawn leaves nothing behind.\n- **Inherited env, minus Cotal's own namespace.** A spawned agent gets the operator's\n environment, because a harness they installed and configured should behave the same way\n under `cotal spawn` as it does in their shell. Cotal resets only its own `COTAL_*`\n namespace, which is identity rather than preference: a connector supplies those per child\n and does so conditionally, so an inherited value would reach an agent that was never\n granted it. Connection material is not in the environment at all; it rides a private file.\n An operator who wants the child confined declares `spawn.env` in the cotal config.\n- **Instance addressing.** One space can hold more than one manager. Each keeps a stable\n logical instance id across restarts and advances its process epoch when it comes back, so\n peers address a specific manager without caring which process currently serves it. `cotal\n spawn <persona> --detach --on <instance>` pins one instance (`ps`, `stop` and `attach` take\n the same flag); an untargeted spawn rides class anycast and the acceptance records which\n instance took it. `ps` and `status` scatter across every registered instance and label a\n non-answering one as registered with no answer within the deadline, never dropping it.\n- **A manager holds a liveness lease, and only proof ends it.** Each instance keeps its own key\n in the space's manager bucket and refreshes it several times over inside the key's TTL. A\n refresh that gets *no answer* is not a lost lease: it proves nothing about the key, and the\n write may even have landed with only the acknowledgement lost. So the manager re-reads the key\n before deciding. It keeps serving when the key is still its own, adopting whatever revision the\n broker actually has, and shuts itself down only on proof: the key is gone, or it now holds a\n different process. Going longer than the TTL with no refresh that *landed* is its own reason\n to stop, and it says so in those words. That window runs from the last write that actually\n restarted the key's TTL: a re-read that finds the key unchanged is a real answer and the\n manager keeps serving on it, but reading a key does not refresh it, so it buys no extra time.\n Either way that stops one instance, never the space; a sibling manager keeps serving.\n- **Attach is a mesh session.** The console and dashboard discover agents over the **mesh**\n (presence, `ps`). `cotal attach` no longer hands back a `127.0.0.1` URL: it redeems a\n one-use, holder-bound session offer, and the terminal bytes stream over the mesh on\n core-NATS session subjects scoped to the two parties, with backpressure surfaced as an\n explicit drop notice rather than silent loss. That is also how attach reaches a manager on\n another machine \u2014 through the broker, not by dialing the manager's own socket. A late\n attach still repaints the full screen from a replayed snapshot of a headless terminal\n mirror (including alternate-screen TUIs). If the manager restarts, its successor refuses\n the old session and the client surfaces \"manager restarted; re-attach\".\n- **The manager's console face is a separate, credentialed surface.** The manager still\n serves the browser console over local HTTP: the static page plus the roster, the live feed,\n and the route that mints the browser's own session. It binds loopback unless the operator\n says otherwise (`cotal supervise --console-host`), and every route that carries mesh data\n or mints a credential requires the manager's console token.\n\nThe result is that an agent can grow and shape its own team: ask for a teammate\n(`cotal_spawn`), mint a persona on the fly (`cotal_persona`), or tear one down\n(`cotal_despawn`). Every newcomer joins as a peer, not as a child of whoever requested\nit. Each managed agent runs under a durable **lifecycle**: a despawn retires it (settling\nand evicting the old incarnation) before its name frees for reuse, and a supervised restart\nrecovers the same lifecycle rather than minting a new one, so durables and credentials key\non the lifecycle, not the reusable name ([SPEC \xA713.1](../SPEC.md#131-lifecycle-identity);\n[identity & auth](identity-and-auth.md)). Destructive space-wide operations (history purge)\nstay operator-only.\n\n\n## Observers\n\nA watch surface is a read-only observer: an endpoint that consumes without registering\npresence (invisible to peers) while watching everyone else's. All three surfaces\n(terminal console, plain stream, web dashboard) derive from that one observer through a\nshared render-agnostic model, so no surface re-implements wire semantics. The guide is\n[watch a mesh](watch-a-mesh.md); the model is [MeshView](mesh-view.md).\n\n## Names, roles, instances\n\nThree identity layers, in increasing permanence\n([SPEC \xA72](../SPEC.md#2-identity), [\xA76](../SPEC.md#6-presence-and-discovery)):\n\n- **`name`** is a cosmetic, reusable human handle. Addressing by name is best-effort\n convenience, with deterministic and fail-loud resolution: a unique live name resolves,\n and a collision among live peers throws with the candidate ids rather than silently\n picking one. The manager auto-numbers its own spawns (`reviewer` \u2192 `reviewer-2`).\n- **`role`** is the addressable service, which makes it the anycast address:\n `svc.reviewer` reaches \"whoever is a reviewer\", so the label carries routing meaning.\n- **The instance id** is the authoritative address: the presence key, the unicast target,\n the credential subject.\n\n**Instance continuity:** the id tracks *context* continuity, not the label. A resumed\nsession (same context window) keeps its id; presence, thread correlation, and in-flight\nDMs stay continuous. A fresh context, even reusing the name, is a **new** instance with a\nnew id: reusing an id across a discontinuous context would tell peers \"same agent, same\nmemory\" when the new session has none. One deliberate exception: OpenCode's `/new` inside\nthe same managed process keeps the mesh identity and advances only the thread correlation\nid: process continuity, not credential reuse.\n\n## Deferred\n\nSessions/moderator, signed envelopes + DID identity, instant offline, artifact delivery,\nauth-callout, and federation are designed for but not built yet; each is tracked, with\nits direction, in the [roadmap](roadmap.md).\n"
|
|
16093
|
+
"body": "# Architecture\n\n> **Concept** (informative) \xB7 **For:** anyone who wants to know how Cotal is built, and why \xB7 **Normative:** [SPEC](../SPEC.md)\n\nCotal is built as a thin waist: the normative wire contract (subjects, message schemas,\npresence/discovery, delivery semantics, the auth grammar) is the standard\n([SPEC](../SPEC.md)), and everything else is a pluggable edge over existing building\nblocks. Identity, transport, storage, and discovery compose from proven pieces (NATS,\nJetStream, JWT/nkeys) rather than being reinvented. Adapters stay thin and swappable, and\nnothing adapter-specific leaks into the core.\n\n## Influences: A2A\n\nCotal reuses A2A's vocabulary and shapes so it stays interoperable rather than siloed, and\nimplements them over NATS/JetStream.\n\n**From A2A** come the *data shapes*: `AgentCard` (identity / role / tags / skills),\n`Message` / `Part` (text and data), and correlation ids (`contextId`). We do not adopt\nA2A's HTTP/JSON-RPC transport, `Task` RPCs, or its request/response server model, none of\nwhich fit lateral pub/sub.\n\nThe *addressing model* is Cotal's own: the hierarchical address `space / service / instance`\nand three delivery modes, multicast, unicast, anycast\n([presence & delivery](presence-and-delivery.md)). **Mentions** are a priority hint on a\nmulticast, not a routing target. NATS/JetStream is the data plane, adding the durability and\npresence a bare pub/sub layer leaves to the app.\n\nIdentity is an A2A `AgentCard` whose instance id is shaped to later become a **DID**\n(`did:key`) so authenticity can survive an untrusted relay ([roadmap](roadmap.md)).\n\n## One wire, mapped onto NATS\n\nThe messaging plane rides three subject kinds, with the sender encoded in the subject\nitself, where the server can police it, rather than in a self-asserted payload field\n([SPEC \xA73](../SPEC.md#3-subject-layout)); the endpoint control surface adds its own rails\n([SPEC \xA713](../SPEC.md#13-endpoint-control-surface-v04)):\n\n| Delivery | Subject |\n|---|---|\n| multicast | `cotal.<space>.chat.<owner>.<actor>.<channel\u2026>` |\n| unicast | `cotal.<space>.inst.<toOwner>.<toActor>.<owner>.<actor>` |\n| anycast | `cotal.<space>.svc.<role>.<owner>.<actor>` |\n| endpoint (control) | `cotal.<space>.ep.<one\\|all\\|inst\\|reply>.\u2026` ([\xA713.2](../SPEC.md#132-grammar)) |\n\nThe sender is a **principal**, an `owner.actor` pair: the account the agent acts on behalf\nof, then the agent's own handle under it ([identity & auth](identity-and-auth.md)). Two\ntokens instead of one means the broker can deny cross-owner *and* same-owner cross-actor\nforgery in the subject grammar itself.\n\nBehind the subjects, each space gets three **JetStream streams** (chat / DM / task, for\nstorage, per-reader bookmarks, and history), **KV buckets** for presence and the channel\nregistry, and the endpoint control surface on its own rails and streams\n([SPEC \xA713](../SPEC.md#13-endpoint-control-surface-v04)). Rather than re-implementing delivery\nguarantees, Cotal uses the native NATS mechanisms: streams for at-least-once and late\njoin, queue groups for anycast load-balancing, KV TTL for liveness ([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding);\nthe reasoning: [presence & delivery](presence-and-delivery.md)). Isolation is one NATS\n**account per space** ([spaces & channels](spaces.md)); authorization is per-agent JWT\nACLs ([identity & auth](identity-and-auth.md)). Large artifacts are reserved for a\nper-space Object Store ([roadmap](roadmap.md)).\n\nWhether any of this *requires* NATS is answered in\n[transport vs protocol](transport.md): the contract is transport-agnostic; NATS/JetStream\nis the reference binding.\n\n## Package layout: one-way tiers\n\n```\nexamples \u2500\u2500\u2192 implementations \u2500\u2500\u2192 workspace \u2500\u2500\u2192 core \u2190(peer)\u2500\u2500 extensions\n (interoperate at runtime over NATS, not via imports)\n```\n\n- **`@cotal-ai/core`**, the protocol: subjects, schemas, the NATS client layer, and the\n extension contracts (`Connector`, `Command`, `Runtime`) with the `Registry` they\n self-register into. Depends on nothing else in the repo.\n- **`@cotal-ai/workspace`**, the machine-local operator layer over `~/.cotal`: mesh\n registry, target resolution, auth-path helpers. Not part of the wire standard, so a\n third party can embed core without inheriting workstation plumbing.\n- **`extensions/*`**: pluggable adapters (connectors, runtimes). Each **peer-depends** on\n core (binding to the host's single core instance) and self-registers on import; an\n unknown agent type **throws**, no silent fallback.\n- **`implementations/*`**, opinionated surfaces over core: the CLI, the manager, the\n delivery daemon, the web dashboard. Implementations never import each other; they meet\n at runtime, in a shared space over NATS. A composition root (the `cotal` binary, or an\n example) wires the pieces it wants.\n- **`examples/*`**: use-cases and composition roots, never published\n ([examples](examples.md)). An example only configures and orchestrates; new message\n kinds or subjects go into core, generalized, never into an example.\n\nThe published binary also loads **operator-installed extensions**: `cotal ext add\n<npm-package>` installs into a cotal-owned prefix, imports once so the package\nself-registers, then caches every contributed `kind:name`. Command metadata is cached for\n`--help`/completion; running a command or requesting a provider imports its owner lazily and\nuses the live object. Before that first import, the loader rebinds shared peers to the current\nhost under the extension-prefix lock; version skew or an unbindable peer fails loudly.\nThe repo's `@cotal-ai/web` dashboard and optional tmux/cmux/Orca/Herdr runtimes use this mechanism.\nRuntime resolution stays registry-driven and open-ended: a name with no registered/installed\nprovider fails loud (never a fallback), and a third-party runtime installs under its own package\nname. The CLI does carry a small, non-authoritative map of the first-party runtime names\n(`orca`/`tmux`/`cmux`/`herdr`) to their `@cotal-ai/*` packages, used only to print an exact `cotal ext add`\nhint for a known-but-uninstalled runtime and to list them in `cotal runtimes`; it never resolves or\nregisters a provider.\n\nMachine-local processes use the same registry. The base CLI contributes broker/control-plane\n`local-process` descriptors, while an installed package contributes its own (for example `web`).\nThat keeps `cotal down <component>` and `cotal status` extensible without teaching the base CLI\npackage-specific pidfiles. A provider process claims its declared pidfile with exclusive create;\nextension removal reserves that same path so startup cannot cross uninstall.\n\nBeyond the app-bound connectors, `@cotal-ai/pi` is a **host-native plugin**: a pi extension\nloaded into the user's own pi (CLI or SDK-embedded), placing a Cotal endpoint inside the\nsession's process and driving its run loop off the inbox \u2014 see\n[connect-pi](connect-pi.md).\n\n## Connectors: four surfaces, one runtime\n\nEvery coding-agent integration exposes the same four surfaces:\n\n| Surface | Carries |\n|---|---|\n| Outbound, ambient | lifecycle \u2192 presence and activity, automatically |\n| Outbound, deliberate | the messaging tools (`cotal_send` / `cotal_dm` / `cotal_anycast`) |\n| Inbound, pull | `cotal_inbox` |\n| Inbound, push | wake-and-inject into the live session |\n\nThe shared runtime lives in [`@cotal-ai/connector-core`](../extensions/connector-core):\nthe mesh agent, the [`cotal_*` tool surface](mcp-tools.md) (defined once in its tool\nspecs, so it cannot drift across hosts), and the delivery buffer with its attention\npolicy. Each adapter is a thin client\nover it that binds to its host's native mechanism: an installed plugin + MCP server for\n[Claude Code](connect-claude.md), an in-process plugin for\n[OpenCode](connect-opencode.md) (beta), a Python sidecar for\n[Hermes](connect-hermes.md) (alpha), a host-native extension for\n[pi](connect-pi.md) (alpha). The [connectors matrix](connectors.md) compares them\nfeature-by-feature.\n\nThe endpoint underneath self-heals: when the transport connection dies terminally, a\nsupervisor rebuilds it (rebuilds are serialized and coalesced), and unacked in-flight\nmessages redeliver on the rebound durables, so nothing is lost across the gap. A manual\n`/reconnect` is the human-invoked counterpart.\n\n## Manager: a supervisor, not an orchestrator\n\nThe CLI does not spawn agents itself; a long-lived **manager** owns their lifecycle,\nasked over the mesh. The manager is not a privileged control plane: it is an ordinary\nservice endpoint on the same `ep` rails as any other daemon\n([\xA713](../SPEC.md#13-endpoint-control-surface-v04)), holding only the capability rows its\ncallers grant it. It owns process lifecycle and config binding (start / stop / restart,\nbinding env and policy) and has no say in what work the agents do. Agents coordinate\nlaterally; the manager only births and configures them.\n\n- **Off the message hot path.** Each agent self-connects to the mesh through its own\n connector. The manager owns processes in order to control them, but observes everything\n through presence, so a bring-your-own-terminal agent it never spawned still shows up in\n `ps`.\n- **Pluggable runtimes.** Spawning is abstracted behind a `Runtime` contract (like pm2 or\n docker for agent TUIs): **`pty`** ships built-in (the manager owns a pseudo-terminal;\n watch or type via `cotal attach`); **`tmux`**, **`cmux`**, **`orca`**, and **`herdr`** are\n extensions that put each teammate in its own native terminal surface (explicit opt-ins\n that throw when the extension isn't loaded, never a silent fallback); **byo** is the\n floor (a human's own terminal, tracked via presence); **host** (Agent SDK, true mid-turn\n interrupt) is the documented upgrade path ([roadmap](roadmap.md)).\n- **Served commands.** `spawn` (an action, below), `stop`, `ps`, `status`, `attach`,\n `models`, `definePersona`, and `bind` are endpoint commands\n ([\xA713.5](../SPEC.md#135-verbs)) any authorized node can send, policy-gated\n ([identity & auth](identity-and-auth.md)). A caller learns them off the wire with `cotal\n describe manager`; nothing is compiled in.\n- **Spawn is an action.** Asking for an agent no longer blocks the caller while the process\n comes up. The manager accepts a spawn **goal** ([\xA713.6](../SPEC.md#136-composites)) and\n immediately returns the allocated identity (the agent's name, its `owner`/`actor`/`uid`\n triple, a `goalId`, and the executor coordinate `{lifecycleUid, epoch}`); progress events\n then report the launch until a terminal outcome. Presence within the readiness window is\n `succeeded`, an early exit is `failed`, and the window passing with neither is\n `uncertain`: a bounded, reconcilable outcome a later `ps` settles against the live roster,\n never a silent hang.\n- **Bounded spawn.** A gate caps concurrent and in-flight agents and a minimum-lifetime\n floor bounds spawn/despawn churn, so a capability-holding but compromised peer cannot\n fork-bomb the host. The gate runs at goal acceptance, before any identity is minted or\n process launched, so a refused spawn leaves nothing behind.\n- **Declared environment boundary.** A spawned agent receives a fixed OS allow-list (PATH/HOME/\n locale, including PATH entries connector binaries live in), the machine-wide `COTAL_*` operator\n knobs, connector-declared provider inputs, explicitly shared MCP references, and names\n deliberately added through `spawn.env`. It never inherits the manager's ambient environment, so\n host-session markers (`CLAUDE_CODE_CHILD_SESSION` and the analogous names other hosts use) and\n unrelated capabilities cannot become properties of every seat. Connection material rides a private\n file instead of the environment.\n- **Instance addressing.** One space can hold more than one manager. Each keeps a stable\n logical instance id across restarts and advances its process epoch when it comes back, so\n peers address a specific manager without caring which process currently serves it. `cotal\n spawn <persona> --detach --on <instance>` pins one instance (`ps`, `stop` and `attach` take\n the same flag); an untargeted spawn rides class anycast and the acceptance records which\n instance took it. `ps` and `status` scatter across every registered instance and label a\n non-answering one as registered with no answer within the deadline, never dropping it.\n- **A manager holds a liveness lease, and only proof ends it.** Each instance keeps its own key\n in the space's manager bucket and refreshes it several times over inside the key's TTL. A\n refresh that gets *no answer* is not a lost lease: it proves nothing about the key, and the\n write may even have landed with only the acknowledgement lost. So the manager re-reads the key\n before deciding. It keeps serving when the key is still its own, adopting whatever revision the\n broker actually has, and shuts itself down only on proof: the key is gone, or it now holds a\n different process. Going longer than the TTL with no refresh that *landed* is its own reason\n to stop, and it says so in those words. That window runs from the last write that actually\n restarted the key's TTL: a re-read that finds the key unchanged is a real answer and the\n manager keeps serving on it, but reading a key does not refresh it, so it buys no extra time.\n Either way that stops one instance, never the space; a sibling manager keeps serving.\n- **Attach is a mesh session.** The console and dashboard discover agents over the **mesh**\n (presence, `ps`). `cotal attach` no longer hands back a `127.0.0.1` URL: it redeems a\n one-use, holder-bound session offer, and the terminal bytes stream over the mesh on\n core-NATS session subjects scoped to the two parties, with backpressure surfaced as an\n explicit drop notice rather than silent loss. That is also how attach reaches a manager on\n another machine \u2014 through the broker, not by dialing the manager's own socket. A late\n attach still repaints the full screen from a replayed snapshot of a headless terminal\n mirror (including alternate-screen TUIs). If the manager restarts, its successor refuses\n the old session and the client surfaces \"manager restarted; re-attach\".\n- **The manager's console face is a separate, credentialed surface.** The manager still\n serves the browser console over local HTTP: the static page plus the roster, the live feed,\n and the route that mints the browser's own session. It binds loopback unless the operator\n says otherwise (`cotal supervise --console-host`), and every route that carries mesh data\n or mints a credential requires the manager's console token.\n\nThe result is that an agent can grow and shape its own team: ask for a teammate\n(`cotal_spawn`), mint a persona on the fly (`cotal_persona`), or tear one down\n(`cotal_despawn`). Every newcomer joins as a peer, not as a child of whoever requested\nit. Each managed agent runs under a durable **lifecycle**: a despawn retires it (settling\nand evicting the old incarnation) before its name frees for reuse, and a supervised restart\nrecovers the same lifecycle rather than minting a new one, so durables and credentials key\non the lifecycle, not the reusable name ([SPEC \xA713.1](../SPEC.md#131-lifecycle-identity);\n[identity & auth](identity-and-auth.md)). Destructive space-wide operations (history purge)\nstay operator-only.\n\n\n## Observers\n\nA watch surface is a read-only observer: an endpoint that consumes without registering\npresence (invisible to peers) while watching everyone else's. All three surfaces\n(terminal console, plain stream, web dashboard) derive from that one observer through a\nshared render-agnostic model, so no surface re-implements wire semantics. The guide is\n[watch a mesh](watch-a-mesh.md); the model is [MeshView](mesh-view.md).\n\n## Names, roles, instances\n\nThree identity layers, in increasing permanence\n([SPEC \xA72](../SPEC.md#2-identity), [\xA76](../SPEC.md#6-presence-and-discovery)):\n\n- **`name`** is a cosmetic, reusable human handle. Addressing by name is best-effort\n convenience, with deterministic and fail-loud resolution: a unique live name resolves,\n and a collision among live peers throws with the candidate ids rather than silently\n picking one. The manager auto-numbers its own spawns (`reviewer` \u2192 `reviewer-2`).\n- **`role`** is the addressable service, which makes it the anycast address:\n `svc.reviewer` reaches \"whoever is a reviewer\", so the label carries routing meaning.\n- **The instance id** is the authoritative address: the presence key, the unicast target,\n the credential subject.\n\n**Instance continuity:** the id tracks *context* continuity, not the label. A resumed\nsession (same context window) keeps its id; presence, thread correlation, and in-flight\nDMs stay continuous. A fresh context, even reusing the name, is a **new** instance with a\nnew id: reusing an id across a discontinuous context would tell peers \"same agent, same\nmemory\" when the new session has none. One deliberate exception: OpenCode's `/new` inside\nthe same managed process keeps the mesh identity and advances only the thread correlation\nid: process continuity, not credential reuse.\n\n## Deferred\n\nSessions/moderator, signed envelopes + DID identity, instant offline, artifact delivery,\nauth-callout, and federation are designed for but not built yet; each is tracked, with\nits direction, in the [roadmap](roadmap.md).\n"
|
|
16106
16094
|
},
|
|
16107
16095
|
{
|
|
16108
16096
|
"slug": "mcp-tools",
|
|
@@ -16158,14 +16146,14 @@ var DOCS_BUNDLE = {
|
|
|
16158
16146
|
"title": "Configuration & environment",
|
|
16159
16147
|
"kind": "Reference: describes the TypeScript reference implementation (the `cotal` CLI and connectors), not the wire contract.",
|
|
16160
16148
|
"summary": "Three things configure a Cotal workstation: the config file (per-connector settings, notably which of your MCP servers get shared with spawned agents), a set of COTAL environment variables, and the\u2026",
|
|
16161
|
-
"body": '# Configuration & environment\n\n> **Reference**: describes the TypeScript reference implementation (the `cotal` CLI and connectors), not the wire contract. \xB7 **For:** operators \xB7 **Wire contract:** [SPEC](../SPEC.md)\n\nThree things configure a Cotal workstation: the **config file** (per-connector settings, notably\nwhich of your MCP servers get shared with spawned agents), a set of **`COTAL_*` environment\nvariables**, and the **on-disk layout** under a project\'s `.cotal/` and your machine\'s `~/.cotal`.\nNone of these are part of the wire contract; they configure the reference implementation only.\n\n## The config file\n\nThe cotal config file carries per-connector launch settings. It is layered from two locations,\nmost-specific-wins:\n\n| Layer | Path | Scope |\n|---|---|---|\n| Base | `$XDG_CONFIG_HOME/cotal/config.json` (else `~/.config/cotal/config.json`; `%APPDATA%\\Cotal\\config.json` on Windows) | Operator-level, every space |\n| Override | `<project-root>/.cotal/config.json` | Space-local |\n\nThey merge per connector and per server name: a server in the space-local file replaces the\nsame-named server in the operator-level file; connectors or servers present in only one side are\nkept. A missing file is empty (valid); malformed JSON or a non-object top level is a loud error.\n\nIt carries two things: which of your personal MCP servers a connector should **share** with the\nagents it spawns, and an optional `spawn.env` allow-list that confines what a spawned agent\'s\nprocess environment contains (see [Environment variables](#environment-variables) below; the default\nis that the agent inherits yours).\n\nThe sharing half: By default a spawned agent gets none: the Claude connector launches with\n`--strict-mcp-config`, dropping every ambient MCP server (they are heavy and useless to a meshed\nteammate). This file is the explicit opt-in.\n\n```json\n{\n "connectors": {\n "claude": {\n "mcpServers": {\n "github": {\n "command": "npx",\n "args": ["-y", "@modelcontextprotocol/server-github"],\n "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }\n }\n }\n }\n }\n}\n```\n\nEach server is written in the de-facto `.mcp.json` shape, so you can copy an entry straight out of\nyour own Claude / VS Code / Cursor config. Secrets ride as **`${VAR}` references** (also\n`${VAR:-default}`), resolved from your environment at launch and forwarded to the child **by name**\n(never as literals) so the file stays safe to keep in `~/.config` or a gitignored `.cotal/`. Only\n`command`, `args`, `env`, `url`, and `headers` are expanded; any other key passes through verbatim.\n\n**`--share-tools` interplay**. The per-spawn selection narrows what this config declares:\n\n| `--share-tools` | Result |\n|---|---|\n| (flag absent) | Every server declared for the connector |\n| `none` or empty | Nothing |\n| `a,b` | Only those named: each **must** be declared, or the spawn fails (no silent drop) |\n\nToday only the `claude` connector consumes shared MCP servers; OpenCode inherits config through its\nown merge layer and Hermes has no MCP. See [Connect Claude Code](connect-claude.md) for the full\nsharing model.\n\n## Environment variables\n\nThese are the operator-facing variables. Most of the connector-session ones (space, name, role, \u2026)\nare set **for you** by `cotal spawn` / the manager when they launch an agent; you set them by hand\nonly when you drive a connector session yourself (e.g. your own `claude` with the plugin) or a custom\nlauncher. Comma-separated lists are trimmed.\n\n| Variable | Consumed by | Meaning | Default |\n|---|---|---|---|\n| `COTAL_SPACE` | connector session | Space to join | `demo` (or the join link\'s) |\n| `COTAL_NAME` | connector session | Presence name / identity | required (or via `COTAL_AGENT_FILE` / `COTAL_LINK`) |\n| `COTAL_ROLE` | connector session | Role | agent file\'s `role:`, else none |\n| `COTAL_SERVERS` | connector session | Broker URL(s). Hand-driven sessions only: a launcher-spawned seat gets this in its launch material instead (see below) | the default local broker (or the link\'s) |\n| `COTAL_CREDS` | connector session | Path to a NATS creds file (auth mode). Hand-driven sessions only, same as above | none (open mode) |\n| `COTAL_LINK` | connector session | `cotal://token@host/space` join link: supplies server, auth, space | none |\n| `COTAL_AGENT_FILE` | connector session | Path to a persona file: supplies name, role, kind, channels | none |\n| `COTAL_SUBSCRIBE` | connector session | Active channel read set | agent file / link, else `general` |\n| `COTAL_ALLOW_SUBSCRIBE` | connector session | Read ACL (channels the agent *may* read) | = `COTAL_SUBSCRIBE` |\n| `COTAL_ALLOW_PUBLISH` | connector session | Post ACL (channels the agent *may* post to) | deny (empty) |\n| `COTAL_MODEL` | connector session | Model label (display metadata) | agent file\'s `model:`, else none |\n| `COTAL_KIND` | connector session | Endpoint kind | `agent` |\n| `COTAL_TLS` | connector session | Connect over TLS (`1`) | off |\n| `COTAL_TOKEN` | connector session | Auth token (token / open modes) | none |\n| `COTAL_CAPABILITIES` | connector session | Control-plane capabilities (e.g. `spawn`) that gate manager tools | agent file\'s `capabilities:` |\n| `COTAL_QUIET` / `COTAL_MUTED` | connector session | Per-channel attention defaults (never-wake / drop-on-receive) | agent file\'s, else none |\n| `COTAL_CHANNEL` | Claude connector | Force channel wake-nudges on (`1`) / off; set to `1` by the Claude launcher | auto-detect |\n| `COTAL_EVENTS` | connector session | Arm this session\'s event plane (`1`); set by the launcher for `--events` spawns | off |\n| `COTAL_EVENTS_DEFAULT` | manager | Default event plane for managed spawns (`1`) | off |\n| `COTAL_DEFAULT_AGENT` | `cotal spawn` | Default connector type for a bare spawn | `claude` |\n| `COTAL_DEFAULT_PERSONA` | `cotal spawn` | Default persona for a bare spawn | `default` |\n| `COTAL_SKIP_CONNECTOR_SEED` | boot gate | Skip the automatic built-in-connector seed/refresh on a command (`1`); `cotal ext seed` still works | off |\n| `COTAL_DETACH_KEY` | `cotal attach` | Detach escape key (`ctrl-<char>` / `^<char>`) | `ctrl-]` |\n| `COTAL_FEEDBACK_KEY` | `feedback`, connector | Beta feedback key \u2192 keyed intake | none (public intake) |\n| `COTAL_FEEDBACK_EMAIL` | `feedback`, connector | Contact email for the keyless public intake | your git email |\n| `COTAL_FEEDBACK_URL` | `feedback`, connector | Intake URL override (self-hosted) | keyed / public intake |\n| `COTAL_SKIP_ASSIST` | `setup` | Disable the interactive Claude handoff on a failed step (`1`; for CI) | off |\n| `COTAL_COMPLETE_DEBUG` | `completion` | Print completion-resolution errors to stderr | off |\n| `COTAL_SERVE_HEADLESS` | OpenCode runtime | Run the OpenCode server without a foreground TUI (`1`) | off |\n| `COTAL_HOME` | workspace | Override the machine-home dir for the **mesh registry only** (`meshes/`, `current-mesh`, onboard marker). Does **not** redirect project-root paths (`findCotalRoot` / `.cotal/broker-policy.json`, NATS store, manager/delivery state, auth). Tests that run `cotal up` must also use a temp project root with its own `.cotal/` as `cwd` | `~/.cotal` |\n\n> `--console-port` is a `cotal supervise` flag, not an environment variable; there is no\n> `COTAL_CONSOLE_PORT`.\n\n### Set by the launcher, not by you\n\nThese are wired into a spawned child\'s environment by the connector / launcher and read back inside\nthe session. They are not operator knobs; listed so you recognize them in a process listing.\n\n| Variable | Purpose |\n|---|---|\n| `COTAL_ID` | Stable agent id chosen by the launcher (static meshes) |\n| `COTAL_LIFECYCLE_UID` | The incarnation\'s lifecycle UID, minted once per spawn; the session binds its lifecycle-keyed DM/delivery/history consumers by it (its credential pins the same names). Required for an authed launch (`COTAL_CREDS` or user-mode); config parsing fails loud without it. Open mode omits it (the endpoint self-mints per session) |\n| `COTAL_OWNER` / `COTAL_ACTOR` / `COTAL_SENTINEL_CREDS` / `COTAL_BEARER_CMD` | User-auth launch identity: the agent\'s principal, its sentinel creds path, and the exec-able bearer command; all four together, mutually exclusive with `COTAL_CREDS`. A launcher-spawned seat carries them in its launch material instead of its environment. A remote enrollment\'s bearer argv uses `agent-bearer --exchange-url <https://base>`; the token never falls back to a local service file |\n| `COTAL_LAUNCH_MATERIAL` | Path to this launch\'s private 0600 material file (see [Launch material](#launch-material) below). Carries the broker URL, the creds path, the auth token, the user-auth identity, and the control token. A PATH, never a secret |\n| `COTAL_CONTROL_SOCKET` | The session\'s local control endpoint path. The MCP server listens on it and the lifecycle hooks connect to it; the token that authenticates the first frame rides the launch material, not the environment |\n| `COTAL_BRIDGE_SOCKET` / `COTAL_TOOLS_FILE` / `COTAL_PARENT_PID` | Hermes sidecar plumbing (bridge socket, generated tool descriptors, launcher pid to watch) |\n| `OPENCODE_CONFIG_CONTENT` | Inline OpenCode config (the injected cotal plugin, highest merge layer) |\n| `OPENCODE_DB` / `OPENCODE_HOME` / `OPENCODE_PORT` / `OPENCODE_SERVER_URL` / `COTAL_OPENCODE_*` | OpenCode server plumbing (home, port, DB, server URL) |\n\nA spawned agent inherits **your environment**, so a harness you already configured resolves its\nmodel and provider the same way it does when you run it yourself. Cotal resets its own `COTAL_*`\nnames before the child starts, keeping the machine-wide ones (`COTAL_HOME`, the `COTAL_FEEDBACK_*`\nset, `COTAL_DEFAULT_AGENT` / `COTAL_DEFAULT_PERSONA`, the `*_BIN` overrides and the timing knobs).\nThat reset is not a preference setting: a connector supplies the per-session names for each child\nand does so conditionally, so an inherited one would never be overwritten and would hand an agent\nanother agent\'s credential path, ACL, or lifecycle uid. Connection material is not in the\nenvironment at all (see [identity & auth](identity-and-auth.md)).\n\nTo confine a spawned agent instead, declare `spawn.env` in the config file:\n\n```json\n{ "spawn": { "env": ["MY_PROVIDER_API_KEY"] } }\n```\n\nThe child then gets a fixed OS allow-list (PATH, HOME, TERM, locale, XDG/Windows config dirs) plus\nexactly the names you list, plus any `${VAR}` a shared MCP server references. An empty array is a\nreal policy, meaning the OS allow-list alone. A space-local `spawn` block replaces the\noperator-level one outright rather than merging, so the narrower file stays narrow.\n\nThree states that look alike are not: no `spawn` block means no allow-list and the agent inherits\nyour environment; `"spawn": { "env": [] }` means the OS allow-list alone; and `"spawn": {}` in a\nspace-local file replaces the operator-level block with nothing, so that space inherits even when\nyour machine-wide file confines. The last one is how a space opts out of machine-wide containment,\nwhich is worth knowing before you write it by accident.\n\nBe honest with yourself about what this buys: `HOME` is forwarded either way, so an agent with a\nshell reads `~/.aws`, `~/.ssh` and `~/.config` regardless. `spawn.env` protects what a file on disk\ncannot hand over anyway, and that is more than a list of secret values. Some variables are **capability\nhandles**: they do not contain a secret, they name a live process that will act on your behalf.\n`SSH_AUTH_SOCK` is the sharp one. Inherit it and the agent can ask your `ssh-agent` to sign, which\nmeans it can reach any host or sign any commit that key authorises, and it keeps that power even\nif the private key file is not on disk at all. Nothing under `~/.ssh` has to exist for it to work,\nso "a shell reads `~/.ssh` regardless" does not cover this case. The same shape covers a\n`gpg-agent` socket and the desktop and cloud credential brokers. So `spawn.env` protects two things:\nsecrets that live **only** in the environment, such as an `aws-vault exec` or `op run` shell or\nCI-injected values, and the capability handles above, which it removes along with everything else\nit does not name. Real containment is still a sandbox or a VM.\n\nModel discovery is the exception, and it is deliberate rather than an oversight. When the `codex` or\n`opencode` connector enumerates a model catalog (`cotal models`, and the manager\'s selector), it runs\nthat harness with your environment minus Cotal\'s own `COTAL_*`, and it does **not** consult\n`spawn.env`. Those probes are short-lived catalog reads rather than agent seats, so an allow-list\nthat confines a seat does not confine them.\n\n### Launch material\n\nA process environment is inherited by every descendant. A seat launched with its credential, its\nbroker URL and its control token in the environment hands all three to the build it runs, the linter,\nthe third-party CLI, the test suite that reads its broker from the environment. Nothing in that chain\nasked for any of it.\n\nSo a launcher-spawned seat does not get them in its environment. The launcher writes them to a single\n**0600 file inside a 0700 private directory** and exports only its path, as `COTAL_LAUNCH_MATERIAL`.\nThe session reads the file once at startup. This is the same shape `cotal agent-bearer` already uses\nfor its spawn-time secret: the material rides a file, never argv (which is visible in a process\nlisting) and never the ambient environment (which is inherited).\n\nThree connectors drop the path once they have read it, so the shells and tools those seats run\ninherit no reference at all: **pi** and **codex**, whose sessions run in the seat process, and\n**OpenCode**, whose seat process is a shim that starts `opencode serve` (the plugin runs in that\nserver, which is also what executes the session\'s tool calls). Those three also **delete the file**\nat the same moment, along with the private directory that held it. Nothing reads it again, so leaving\nit on disk would only extend how long a copy of the material exists. The directory is only removed\nwhen it is provably the one the launcher wrote: the right filename inside, the launcher\'s prefix on\nthe directory, the directory sitting directly in the OS temp root, and a non-recursive removal that\nfails rather than deletes if anything else is in there.\n\nTwo keep it, and for the same reason in both cases: a process that starts LATER has to read it.\n**Claude**\'s readers are short-lived children, the MCP server and one process per lifecycle hook,\nwhich begin after the session is already running. **Hermes**\' launcher starts a gateway child that\nneeds the control token. For those two, a shell the seat runs still inherits a path to the material\nfile, though not the material itself.\n\nWhat this does: the values are out of every descendant\'s environment, so an `env` dump, a CI log, a\nsuite that defaults its broker from the environment, or a tool handed a credential it never asked\nfor, all stop seeing them. What it does not do: hide the material from a process running as the same\nuser that deliberately opens the file. No environment-level control can, and the same is already true\nof `~/.cotal/auth/creds`. What changes is that reaching the material is a deliberate act rather than\nan inheritance nobody chose.\n\nDriving a connector session **by hand** still works the documented way: set `COTAL_CREDS` /\n`COTAL_SERVERS` (and the user-auth quartet) yourself, and no material file is involved. Setting both\na material file and any of them is refused rather than resolved by precedence: one launch carries one\nidentity plane. `COTAL_LINK` counts as one of them, because a join link carries the server, the auth\nand the space in a single string.\n\nThe control endpoint is a pair, and **half a pair is refused**. A launch with a control socket path\nand no resolvable token, or a token and no socket path, does not fall back to running without a\ncontrol plane: it fails with a sentence naming which half is missing. The one exception is the\nlifecycle hook relay, which catches that refusal, writes a single warning to stderr naming no values,\nand then does nothing, because a hook that throws is a hook that blocked the session. Failing open is\ndeliberate; failing open silently is not.\n\n## On-disk layout\n\n### Project: `.cotal/`\n\nA project\'s state lives in `.cotal/` at the mesh root (found by walking up from the cwd, like `.git`).\n**It is gitignored**; it holds secrets and machine-local process state.\n\n| Path | What it is |\n|---|---|\n| `auth/broker.json` | Broker trust material: the operator seed and the system account (secret; the system-account signing seed is stripped before writing). One per broker, shared by every space on it |\n| `auth/account.<key>.json` | One space\'s own NATS data account and signing seed (secret). One file per space, all signed by the broker above; `<key>` is a stable, case-safe hex encoding of the space name (never the raw name, so two case-differing spaces can\'t collide) |\n| `auth/space.<key>/` | One space\'s user-auth state (IdP pin, issuer keys, owner secret, callout account), present only when that space enables per-user auth. Keyed by the same case-safe hex encoding; pre-hex layouts (`auth/<space>/`) are renamed here on first touch |\n| `auth/creds/<name>.creds` | Per-agent minted NATS credentials |\n| `auth/server.conf` | Generated nats-server config for the broker. The core renderer accepts every space on the broker; `cotal up` currently orchestrates one space per root, so it renders that one space\'s account |\n| `broker-policy.json` | Durable broker **launch** policy (TLS-required cert/key path references, or plaintext). Survives `cotal down` so a bare re-`up` cannot silently drop TLS. Under the project root \u2014 **not** under `COTAL_HOME` |\n| `agents/<name>.md` | Persona / agent files ([Agent files](agent-files.md)) |\n| `manifests/<hash>.json` | Manifest-deploy ledger (records of `up -f` / `spawn -f` runs) |\n| `config.json` | Space-local connector config (the override layer above) |\n| `nats.pid` \xB7 `nats.log` | Background nats-server pid + log |\n| `manager.pid` \xB7 `manager.log` | Manager (supervisor) pid + log; `manager.delivery-aware` marks a delivery-aware build. The manager writes the pid itself, whatever started it, and removes it on a clean stop only while it still names that process. A reader treats the record as a running manager only if the pid is alive **and** the process is a supervisor: a recycled pid belonging to something else is reported as a stale record, never signalled |\n| `delivery.pid` \xB7 `delivery.log` \xB7 `delivery.creds` | Delivery daemon pid, log, and scoped cred (auth mode) |\n| `web.pid` \xB7 `web.log` | Web dashboard pid + log |\n| `membership.json` \xB7 `membership-*.creds` | Membership feed state + its scoped creds |\n| `setup.log` | Last `cotal setup` run |\n\n### Machine: `~/.cotal`\n\nCross-project machine state, so a `cotal spawn` from any directory can find a running mesh. Location:\n`~/.cotal` on POSIX, `%LOCALAPPDATA%\\Cotal` on Windows; overridable with `COTAL_HOME`.\n\n`COTAL_HOME` overrides **this tree only** (registry + current pointer + onboard marker). It is not a\nfull workstation sandbox. Broker launch policy, the JetStream store, pidfiles, and auth live under\nthe **project** `.cotal/` found by walking up from the cwd ([Project: `.cotal/`](#project-cotal)\nabove, including `broker-policy.json` on TLS meshes). A probe that sets `COTAL_HOME` alone and runs\n`cotal up --tls-cert \u2026` from a directory whose walked root is the operator home still writes those\nproject paths on the live machine.\n\n| Path | What it is |\n|---|---|\n| `meshes/space.<key>.json` | Registry of running meshes: one file per broker `cotal up` started (server URL, root path, mode, TLS-required client intent when recorded); `<key>` is the same case-safe hex encoding of the space name, and the record\'s own `space` field is authoritative |\n| `current-mesh` | Default space a bare `cotal spawn` joins (set by `cotal use`) |\n| `onboarded.json` | First-run marker (with `ONBOARD_VERSION`) that flips setup between first-run and status-card |\n| the Claude plugin marketplace | The installed `cotal-mesh` plugin assets |\n\n### Config dir: `$XDG_CONFIG_HOME/cotal`\n\nDistinct from `~/.cotal`. Location: `$XDG_CONFIG_HOME/cotal`, else `~/.config/cotal` on POSIX, or\n`%APPDATA%\\Cotal` on Windows.\n\n| Path | What it is |\n|---|---|\n| `config.json` | Operator-level connector config (the base layer above) |\n| `extensions/` | `cotal ext` install prefix: its own npm root (`node_modules`) plus an `extensions.json` provider/command-display cache. Built-in connectors install here too, seeded on first run |\n| `seed/` | Built-in-connector seeding state: the `ever-seeded` authority (+ durable backup), the init witness, the version stamp, the crash cursor, and `store/<version>/<name>` (the stable payloads `ext add --install-links` reifies each seeded connector from) |\n\nBoth `extensions/` and `seed/store/` are operator-global: shared by every space, project directory, and\ncheckout on the machine, and moved only by `$XDG_CONFIG_HOME` (a fresh project dir isolates `.cotal/`,\nnot these). Running `cotal up`, or any command that seeds, from a tree that is not a released install\nre-seeds `seed/store/<version>` with that tree\'s packages under the same version key, so every later\nmesh on the machine materializes those bytes while `cotal ext ls` still reports the published version.\nTo keep the machine-wide store untouched when running from a non-released checkout, point\n`$XDG_CONFIG_HOME` at an isolated dir (on Windows, `%APPDATA%` relocates them). The reconcile names on\nstderr both the store payloads it writes and any old generation it removes, so a machine-wide re-seed\nor cleanup is visible when it happens. Those lines are provenance output: a run whose stderr is closed\nor redirected away keeps the write and loses the line.\n\nFor how `cotal setup` populates the machine state and the plugin, and how the built-in connectors are\nseeded as removable extensions, see [setup internals](setup-internals.md).\n'
|
|
16149
|
+
"body": '# Configuration & environment\n\n> **Reference**: describes the TypeScript reference implementation (the `cotal` CLI and connectors), not the wire contract. \xB7 **For:** operators \xB7 **Wire contract:** [SPEC](../SPEC.md)\n\nThree things configure a Cotal workstation: the **config file** (per-connector settings, notably\nwhich of your MCP servers get shared with spawned agents), a set of **`COTAL_*` environment\nvariables**, and the **on-disk layout** under a project\'s `.cotal/` and your machine\'s `~/.cotal`.\nNone of these are part of the wire contract; they configure the reference implementation only.\n\n## The config file\n\nThe cotal config file carries per-connector launch settings. It is layered from two locations,\nmost-specific-wins:\n\n| Layer | Path | Scope |\n|---|---|---|\n| Base | `$XDG_CONFIG_HOME/cotal/config.json` (else `~/.config/cotal/config.json`; `%APPDATA%\\Cotal\\config.json` on Windows) | Operator-level, every space |\n| Override | `<project-root>/.cotal/config.json` | Space-local |\n\nThey merge per connector and per server name: a server in the space-local file replaces the\nsame-named server in the operator-level file; connectors or servers present in only one side are\nkept. A missing file is empty (valid); malformed JSON or a non-object top level is a loud error.\n\nIt carries two things: which of your personal MCP servers a connector should **share** with the\nagents it spawns, and optional `spawn.env` names that deliberately add environment capability to a\nspawned agent (see [Environment variables](#environment-variables) below).\n\nThe sharing half: By default a spawned agent gets none: the Claude connector launches with\n`--strict-mcp-config`, dropping every ambient MCP server (they are heavy and useless to a meshed\nteammate). This file is the explicit opt-in.\n\n```json\n{\n "connectors": {\n "claude": {\n "mcpServers": {\n "github": {\n "command": "npx",\n "args": ["-y", "@modelcontextprotocol/server-github"],\n "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }\n }\n }\n }\n }\n}\n```\n\nEach server is written in the de-facto `.mcp.json` shape, so you can copy an entry straight out of\nyour own Claude / VS Code / Cursor config. Secrets ride as **`${VAR}` references** (also\n`${VAR:-default}`), resolved from your environment at launch and forwarded to the child **by name**\n(never as literals) so the file stays safe to keep in `~/.config` or a gitignored `.cotal/`. Only\n`command`, `args`, `env`, `url`, and `headers` are expanded; any other key passes through verbatim.\n\n**`--share-tools` interplay**. The per-spawn selection narrows what this config declares:\n\n| `--share-tools` | Result |\n|---|---|\n| (flag absent) | Every server declared for the connector |\n| `none` or empty | Nothing |\n| `a,b` | Only those named: each **must** be declared, or the spawn fails (no silent drop) |\n\nToday only the `claude` connector consumes shared MCP servers; OpenCode inherits config through its\nown merge layer and Hermes has no MCP. See [Connect Claude Code](connect-claude.md) for the full\nsharing model.\n\n## Environment variables\n\nThese are the operator-facing variables. Most of the connector-session ones (space, name, role, \u2026)\nare set **for you** by `cotal spawn` / the manager when they launch an agent; you set them by hand\nonly when you drive a connector session yourself (e.g. your own `claude` with the plugin) or a custom\nlauncher. Comma-separated lists are trimmed.\n\n| Variable | Consumed by | Meaning | Default |\n|---|---|---|---|\n| `COTAL_SPACE` | connector session | Space to join | `demo` (or the join link\'s) |\n| `COTAL_NAME` | connector session | Presence name / identity | required (or via `COTAL_AGENT_FILE` / `COTAL_LINK`) |\n| `COTAL_ROLE` | connector session | Role | agent file\'s `role:`, else none |\n| `COTAL_SERVERS` | connector session | Broker URL(s). Hand-driven sessions only: a launcher-spawned seat gets this in its launch material instead (see below) | the default local broker (or the link\'s) |\n| `COTAL_CREDS` | connector session | Path to a NATS creds file (auth mode). Hand-driven sessions only, same as above | none (open mode) |\n| `COTAL_LINK` | connector session | `cotal://token@host/space` join link: supplies server, auth, space | none |\n| `COTAL_AGENT_FILE` | connector session | Path to a persona file: supplies name, role, kind, channels | none |\n| `COTAL_SUBSCRIBE` | connector session | Active channel read set | agent file / link, else `general` |\n| `COTAL_ALLOW_SUBSCRIBE` | connector session | Read ACL (channels the agent *may* read) | = `COTAL_SUBSCRIBE` |\n| `COTAL_ALLOW_PUBLISH` | connector session | Post ACL (channels the agent *may* post to) | deny (empty) |\n| `COTAL_MODEL` | connector session | Model label (display metadata) | agent file\'s `model:`, else none |\n| `COTAL_KIND` | connector session | Endpoint kind | `agent` |\n| `COTAL_TLS` | connector session | Connect over TLS (`1`) | off |\n| `COTAL_TOKEN` | connector session | Auth token (token / open modes) | none |\n| `COTAL_CAPABILITIES` | connector session | Control-plane capabilities (e.g. `spawn`) that gate manager tools | agent file\'s `capabilities:` |\n| `COTAL_QUIET` / `COTAL_MUTED` | connector session | Per-channel attention defaults (never-wake / drop-on-receive) | agent file\'s, else none |\n| `COTAL_CHANNEL` | Claude connector | Force channel wake-nudges on (`1`) / off; set to `1` by the Claude launcher | auto-detect |\n| `COTAL_EVENTS` | connector session | Arm this session\'s event plane (`1`); set by the launcher for `--events` spawns | off |\n| `COTAL_EVENTS_DEFAULT` | manager | Default event plane for managed spawns (`1`) | off |\n| `COTAL_DEFAULT_AGENT` | `cotal spawn` | Default connector type for a bare spawn | `claude` |\n| `COTAL_DEFAULT_PERSONA` | `cotal spawn` | Default persona for a bare spawn | `default` |\n| `COTAL_SKIP_CONNECTOR_SEED` | boot gate | Skip the automatic built-in-connector seed/refresh on a command (`1`); `cotal ext seed` still works | off |\n| `COTAL_DETACH_KEY` | `cotal attach` | Detach escape key (`ctrl-<char>` / `^<char>`) | `ctrl-]` |\n| `COTAL_FEEDBACK_KEY` | `feedback`, connector | Beta feedback key \u2192 keyed intake | none (public intake) |\n| `COTAL_FEEDBACK_EMAIL` | `feedback`, connector | Contact email for the keyless public intake | your git email |\n| `COTAL_FEEDBACK_URL` | `feedback`, connector | Intake URL override (self-hosted) | keyed / public intake |\n| `COTAL_SKIP_ASSIST` | `setup` | Disable the interactive Claude handoff on a failed step (`1`; for CI) | off |\n| `COTAL_COMPLETE_DEBUG` | `completion` | Print completion-resolution errors to stderr | off |\n| `COTAL_SERVE_HEADLESS` | OpenCode runtime | Run the OpenCode server without a foreground TUI (`1`) | off |\n| `COTAL_HOME` | workspace | Override the machine-home dir for the **mesh registry only** (`meshes/`, `current-mesh`, onboard marker). Does **not** redirect project-root paths (`findCotalRoot` / `.cotal/broker-policy.json`, NATS store, manager/delivery state, auth). Tests that run `cotal up` must also use a temp project root with its own `.cotal/` as `cwd` | `~/.cotal` |\n\n> `--console-port` is a `cotal supervise` flag, not an environment variable; there is no\n> `COTAL_CONSOLE_PORT`.\n\n### Set by the launcher, not by you\n\nThese are wired into a spawned child\'s environment by the connector / launcher and read back inside\nthe session. They are not operator knobs; listed so you recognize them in a process listing.\n\n| Variable | Purpose |\n|---|---|\n| `COTAL_ID` | Stable agent id chosen by the launcher (static meshes) |\n| `COTAL_LIFECYCLE_UID` | The incarnation\'s lifecycle UID, minted once per spawn; the session binds its lifecycle-keyed DM/delivery/history consumers by it (its credential pins the same names). Required for an authed launch (`COTAL_CREDS` or user-mode); config parsing fails loud without it. Open mode omits it (the endpoint self-mints per session) |\n| `COTAL_OWNER` / `COTAL_ACTOR` / `COTAL_SENTINEL_CREDS` / `COTAL_BEARER_CMD` | User-auth launch identity: the agent\'s principal, its sentinel creds path, and the exec-able bearer command; all four together, mutually exclusive with `COTAL_CREDS`. A launcher-spawned seat carries them in its launch material instead of its environment. A remote enrollment\'s bearer argv uses `agent-bearer --exchange-url <https://base>`; the token never falls back to a local service file |\n| `COTAL_LAUNCH_MATERIAL` | Path to this launch\'s private 0600 material file (see [Launch material](#launch-material) below). Carries the broker URL, the creds path, the auth token, the user-auth identity, and the control token. A PATH, never a secret |\n| `COTAL_CONTROL_SOCKET` | The session\'s local control endpoint path. The MCP server listens on it and the lifecycle hooks connect to it; the token that authenticates the first frame rides the launch material, not the environment |\n| `COTAL_BRIDGE_SOCKET` / `COTAL_TOOLS_FILE` / `COTAL_PARENT_PID` | Hermes sidecar plumbing (bridge socket, generated tool descriptors, launcher pid to watch) |\n| `OPENCODE_CONFIG_CONTENT` | Inline OpenCode config (the injected cotal plugin, highest merge layer) |\n| `OPENCODE_DB` / `OPENCODE_HOME` / `OPENCODE_PORT` / `OPENCODE_SERVER_URL` / `COTAL_OPENCODE_*` | OpenCode server plumbing (home, port, DB, server URL) |\n\nA spawned agent receives a fixed OS execution allow-list (PATH, HOME, TERM, locale, and\nXDG/Windows config directories), the machine-wide `COTAL_*` operator knobs (`COTAL_HOME`, the\nfeedback set, the default-agent pair, the `*_BIN` overrides, the timing knobs), the provider inputs\nits connector declares, and `${VAR}` names an explicitly shared MCP server requires. It does not\ninherit the manager\'s ambient environment. This keeps host-session markers such as\n`CLAUDE_CODE_CHILD_SESSION` / `CLAUDECODE` (and the analogous names other hosts use to mark a nested\nsession), unrelated service secrets, and environment-only capabilities out of seats unless\ndeliberately supplied. A seat\'s transcript/resume behaviour is a property of the seat, never of how\nmany layers up someone once ran `cotal up` inside an agent. Connection material is not in the\nenvironment at all (see [identity & auth](identity-and-auth.md)).\n\nPATH is forwarded whole, including entries such as `~/.local/bin` where connector binaries live, so\na seat can still launch after the strip. There is no inherit mode and no opt-in-to-containment flag:\nthe allow-list is the only path.\n\nTo deliberately add an environment name for a spawned agent, declare `spawn.env` in the config file:\n\n```json\n{ "spawn": { "env": ["MY_PROVIDER_API_KEY"] } }\n```\n\nThe listed names are added to the fixed boundary. That is also the opt-in for a host-session marker\na persona has chosen to receive (`CLAUDE_CODE_CHILD_SESSION` and friends). An empty array adds\nnothing. A space-local `spawn` block replaces the operator-level one outright rather than merging,\nso a local list stays exactly local. No `spawn` block, `"spawn": { "env": [] }`, and `"spawn": {}`\nall add no names.\n\nBe honest with yourself about what this buys: `HOME` is forwarded, so an agent with a shell reads\n`~/.aws`, `~/.ssh` and `~/.config` regardless. The boundary protects what a file on disk cannot hand\nover anyway, and that is more than a list of secret values. Some variables are **capability\nhandles**: they do not contain a secret, they name a live process that will act on your behalf.\n`SSH_AUTH_SOCK` is the sharp one. Inherit it and the agent can ask your `ssh-agent` to sign, which\nmeans it can reach any host or sign any commit that key authorises, and it keeps that power even\nif the private key file is not on disk at all. Nothing under `~/.ssh` has to exist for it to work,\nso "a shell reads `~/.ssh` regardless" does not cover this case. The same shape covers a\n`gpg-agent` socket and the desktop and cloud credential brokers. So the default boundary protects:\nsecrets that live **only** in the environment, such as an `aws-vault exec` or `op run` shell or\nCI-injected values, and the capability handles above, which it removes along with everything else\nit does not name. Real containment is still a sandbox or a VM.\n\nModel discovery is the exception, and it is deliberate rather than an oversight. When the `codex` or\n`opencode` connector enumerates a model catalog (`cotal models`, and the manager\'s selector), it runs\nthat harness with your environment minus Cotal\'s own `COTAL_*`, and it does **not** consult\n`spawn.env`. Those probes are short-lived catalog reads rather than agent seats, so an allow-list\nthat confines a seat does not confine them.\n\n### Launch material\n\nA process environment is inherited by every descendant. A seat launched with its credential, its\nbroker URL and its control token in the environment hands all three to the build it runs, the linter,\nthe third-party CLI, the test suite that reads its broker from the environment. Nothing in that chain\nasked for any of it.\n\nSo a launcher-spawned seat does not get them in its environment. The launcher writes them to a single\n**0600 file inside a 0700 private directory** and exports only its path, as `COTAL_LAUNCH_MATERIAL`.\nThe session reads the file once at startup. This is the same shape `cotal agent-bearer` already uses\nfor its spawn-time secret: the material rides a file, never argv (which is visible in a process\nlisting) and never the ambient environment (which is inherited).\n\nThree connectors drop the path once they have read it, so the shells and tools those seats run\ninherit no reference at all: **pi** and **codex**, whose sessions run in the seat process, and\n**OpenCode**, whose seat process is a shim that starts `opencode serve` (the plugin runs in that\nserver, which is also what executes the session\'s tool calls). Those three also **delete the file**\nat the same moment, along with the private directory that held it. Nothing reads it again, so leaving\nit on disk would only extend how long a copy of the material exists. The directory is only removed\nwhen it is provably the one the launcher wrote: the right filename inside, the launcher\'s prefix on\nthe directory, the directory sitting directly in the OS temp root, and a non-recursive removal that\nfails rather than deletes if anything else is in there.\n\nTwo keep it, and for the same reason in both cases: a process that starts LATER has to read it.\n**Claude**\'s readers are short-lived children, the MCP server and one process per lifecycle hook,\nwhich begin after the session is already running. **Hermes**\' launcher starts a gateway child that\nneeds the control token. For those two, a shell the seat runs still inherits a path to the material\nfile, though not the material itself.\n\nWhat this does: the values are out of every descendant\'s environment, so an `env` dump, a CI log, a\nsuite that defaults its broker from the environment, or a tool handed a credential it never asked\nfor, all stop seeing them. What it does not do: hide the material from a process running as the same\nuser that deliberately opens the file. No environment-level control can, and the same is already true\nof `~/.cotal/auth/creds`. What changes is that reaching the material is a deliberate act rather than\nan inheritance nobody chose.\n\nDriving a connector session **by hand** still works the documented way: set `COTAL_CREDS` /\n`COTAL_SERVERS` (and the user-auth quartet) yourself, and no material file is involved. Setting both\na material file and any of them is refused rather than resolved by precedence: one launch carries one\nidentity plane. `COTAL_LINK` counts as one of them, because a join link carries the server, the auth\nand the space in a single string.\n\nThe control endpoint is a pair, and **half a pair is refused**. A launch with a control socket path\nand no resolvable token, or a token and no socket path, does not fall back to running without a\ncontrol plane: it fails with a sentence naming which half is missing. The one exception is the\nlifecycle hook relay, which catches that refusal, writes a single warning to stderr naming no values,\nand then does nothing, because a hook that throws is a hook that blocked the session. Failing open is\ndeliberate; failing open silently is not.\n\n## On-disk layout\n\n### Project: `.cotal/`\n\nA project\'s state lives in `.cotal/` at the mesh root (found by walking up from the cwd, like `.git`).\n**It is gitignored**; it holds secrets and machine-local process state.\n\n| Path | What it is |\n|---|---|\n| `auth/broker.json` | Broker trust material: the operator seed and the system account (secret; the system-account signing seed is stripped before writing). One per broker, shared by every space on it |\n| `auth/account.<key>.json` | One space\'s own NATS data account and signing seed (secret). One file per space, all signed by the broker above; `<key>` is a stable, case-safe hex encoding of the space name (never the raw name, so two case-differing spaces can\'t collide) |\n| `auth/space.<key>/` | One space\'s user-auth state (IdP pin, issuer keys, owner secret, callout account), present only when that space enables per-user auth. Keyed by the same case-safe hex encoding; pre-hex layouts (`auth/<space>/`) are renamed here on first touch |\n| `auth/creds/<name>.creds` | Per-agent minted NATS credentials |\n| `auth/server.conf` | Generated nats-server config for the broker. The core renderer accepts every space on the broker; `cotal up` currently orchestrates one space per root, so it renders that one space\'s account |\n| `broker-policy.json` | Durable broker **launch** policy (TLS-required cert/key path references, or plaintext). Survives `cotal down` so a bare re-`up` cannot silently drop TLS. Under the project root \u2014 **not** under `COTAL_HOME` |\n| `agents/<name>.md` | Persona / agent files ([Agent files](agent-files.md)) |\n| `manifests/<hash>.json` | Manifest-deploy ledger (records of `up -f` / `spawn -f` runs) |\n| `config.json` | Space-local connector config (the override layer above) |\n| `nats.pid` \xB7 `nats.log` | Background nats-server pid + log |\n| `manager.pid` \xB7 `manager.log` | Manager (supervisor) pid + log; `manager.delivery-aware` marks a delivery-aware build. The manager writes the pid itself, whatever started it, and removes it on a clean stop only while it still names that process. A reader treats the record as a running manager only if the pid is alive **and** the process is a supervisor: a recycled pid belonging to something else is reported as a stale record, never signalled |\n| `delivery.pid` \xB7 `delivery.log` \xB7 `delivery.creds` | Delivery daemon pid, log, and scoped cred (auth mode) |\n| `web.pid` \xB7 `web.log` | Web dashboard pid + log |\n| `membership.json` \xB7 `membership-*.creds` | Membership feed state + its scoped creds |\n| `setup.log` | Last `cotal setup` run |\n\n### Machine: `~/.cotal`\n\nCross-project machine state, so a `cotal spawn` from any directory can find a running mesh. Location:\n`~/.cotal` on POSIX, `%LOCALAPPDATA%\\Cotal` on Windows; overridable with `COTAL_HOME`.\n\n`COTAL_HOME` overrides **this tree only** (registry + current pointer + onboard marker). It is not a\nfull workstation sandbox. Broker launch policy, the JetStream store, pidfiles, and auth live under\nthe **project** `.cotal/` found by walking up from the cwd ([Project: `.cotal/`](#project-cotal)\nabove, including `broker-policy.json` on TLS meshes). A probe that sets `COTAL_HOME` alone and runs\n`cotal up --tls-cert \u2026` from a directory whose walked root is the operator home still writes those\nproject paths on the live machine.\n\n| Path | What it is |\n|---|---|\n| `meshes/space.<key>.json` | Registry of running meshes: one file per broker `cotal up` started (server URL, root path, mode, TLS-required client intent when recorded); `<key>` is the same case-safe hex encoding of the space name, and the record\'s own `space` field is authoritative |\n| `current-mesh` | Default space a bare `cotal spawn` joins (set by `cotal use`) |\n| `onboarded.json` | First-run marker (with `ONBOARD_VERSION`) that flips setup between first-run and status-card |\n| the Claude plugin marketplace | The installed `cotal-mesh` plugin assets |\n\n### Config dir: `$XDG_CONFIG_HOME/cotal`\n\nDistinct from `~/.cotal`. Location: `$XDG_CONFIG_HOME/cotal`, else `~/.config/cotal` on POSIX, or\n`%APPDATA%\\Cotal` on Windows.\n\n| Path | What it is |\n|---|---|\n| `config.json` | Operator-level connector config (the base layer above) |\n| `extensions/` | `cotal ext` install prefix: its own npm root (`node_modules`) plus an `extensions.json` provider/command-display cache. Built-in connectors install here too, seeded on first run |\n| `seed/` | Built-in-connector seeding state: the `ever-seeded` authority (+ durable backup), the init witness, the version stamp, the crash cursor, and `store/<version>/<name>` (the stable payloads `ext add --install-links` reifies each seeded connector from) |\n\nBoth `extensions/` and `seed/store/` are operator-global: shared by every space, project directory, and\ncheckout on the machine, and moved only by `$XDG_CONFIG_HOME` (a fresh project dir isolates `.cotal/`,\nnot these). Running `cotal up`, or any command that seeds, from a tree that is not a released install\nre-seeds `seed/store/<version>` with that tree\'s packages under the same version key, so every later\nmesh on the machine materializes those bytes while `cotal ext ls` still reports the published version.\nTo keep the machine-wide store untouched when running from a non-released checkout, point\n`$XDG_CONFIG_HOME` at an isolated dir (on Windows, `%APPDATA%` relocates them). The reconcile names on\nstderr both the store payloads it writes and any old generation it removes, so a machine-wide re-seed\nor cleanup is visible when it happens. Those lines are provenance output: a run whose stderr is closed\nor redirected away keeps the write and loses the line.\n\nFor how `cotal setup` populates the machine state and the plugin, and how the built-in connectors are\nseeded as removable extensions, see [setup internals](setup-internals.md).\n'
|
|
16162
16150
|
},
|
|
16163
16151
|
{
|
|
16164
16152
|
"slug": "connect-claude",
|
|
16165
16153
|
"title": "Connect Claude",
|
|
16166
16154
|
"kind": "Guide (informative)",
|
|
16167
16155
|
"summary": "The Claude Code connector turns a real claude session into a Cotal mesh peer.",
|
|
16168
|
-
"body": "# Connect Claude\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\nThe Claude Code connector turns a real `claude` session into a Cotal mesh peer. A bundled\nplugin inside the session joins NATS, maps lifecycle hooks to presence, and exposes the\nmesh tools. Nothing wraps Claude; it is an ordinary session that happens to be on the\nmesh.\n\nThe shared mesh runtime (agent, `cotal_*` tools, hook relay) lives in\n[`@cotal-ai/connector-core`](../extensions/connector-core); this connector is the thin\nClaude-specific adapter over it. Siblings: [OpenCode](connect-opencode.md) (beta),\n[Hermes](connect-hermes.md) (alpha), [pi](connect-pi.md) (alpha); the\n[Connectors](connectors.md) matrix compares them feature-by-feature.\n\n## Set up\n\n```bash\ncotal setup # one-time: installs the plugin, seeds one agent; launches nothing\ncotal up # brings up the mesh + delivery daemon + a detached manager\n```\n\n`cotal setup` installs the cotal plugin (so the repo's Claude sessions get the `cotal_*`\ntools) and seeds one `default` persona; `cotal up` brings up the local stack so\n`cotal spawn --detach` / `cotal_spawn` work right away. Re-running either is idempotent.\nThe install mechanics and the invariants behind them are in\n[setup internals](setup-internals.md).\n\n`cotal setup` also installs Cotal's authored Agent Skills (`SKILL.md`, the agentskills.io format) for\ncoordinating agent teams (today `team-topology`), from one canonical source, on two channels:\n\n- **Claude Code** gets a second, skills-only plugin, `cotal-skills`, from the same `cotal-mesh`\n marketplace, at **user scope** (machine-wide), and **independent of the mesh connector**: it carries no\n code and no core dependency, installs whenever Claude is on `PATH` (even with the connector removed),\n and uninstalls on its own with `claude plugin uninstall cotal-skills --scope user`. Its plugin version\n is stamped from the running CLI release, so an upgrade + `cotal setup` runs `claude plugin update` and\n the deployed install actually gets the new skill. `cotal setup` installs it on first run and on repeat\n runs, so upgraders are not left behind.\n- **Every other harness** (Codex, Cursor, OpenCode, Gemini CLI, Windsurf/Devin) reads the cross-vendor\n `~/.agents/skills/` directory convention, which has no remote index, so `cotal setup` **reconciles** it:\n it installs/updates each Cotal skill, backs up a copy you have edited to `SKILL.md.bak` before\n replacing it, and removes a Cotal skill that is no longer shipped. Only skills Cotal owns are touched;\n your own or third-party skills there are left alone. `cotal status` reports whether the drop is current,\n stale, missing, or has a retired skill to reconcile. This is the working cross-vendor path.\n\nCotal also generates an [Agent Skills discovery index](https://cotal.ai/.well-known/agent-skills/index.json)\non cotal.ai, but that RFC is still a draft with no harness consuming it yet, so it is a forward bet,\nnot a channel to rely on today.\n\n## Spawn a session\n\n```bash\ncotal spawn # foreground: your default agent, in this terminal\ncotal spawn dave --detach # supervised: the manager runs it in a PTY\n```\n\nA spawn resolves a persona from `.cotal/agents/<name>.md` ([agent files](agent-files.md));\n`--model`, `--variant`, `--cwd`, `--prompt`, ACL overrides, and `--share-tools` apply to\nboth forms ([run a mesh](run-a-mesh.md) has the full resolution rules). The session joins\nwith identity from its environment and auto-registers presence by the time it is\ninteractive.\n\nInside the session, the agent orients with one read-only tool, `cotal_orientation`: its\nidentity, the channels it reads and may post to, its capabilities, the tools available,\nwho's present, and unread counts. The full tool surface is the\n[MCP tool catalog](mcp-tools.md). In auth mode the team-supervision tools\n(`cotal_spawn` / `cotal_persona`) are injected **only** for personas declaring\n`capabilities: [spawn]` (the same grant that opens the privileged control subject), so an\nagent's toolset matches what it can actually invoke. Clearing retained history is\noperator-only ([run a mesh](run-a-mesh.md)), never an agent tool.\n\n## How it binds\n\nClaude Code exposes four integration surfaces, and three of them collapse into a single\ndual-purpose MCP server:\n\n| Surface | Mechanism |\n|---|---|\n| Outbound, ambient | `http` lifecycle hooks \u2192 POST to the connector (presence, activity) |\n| Outbound, deliberate | MCP tools `cotal_send` / `cotal_dm` / `cotal_anycast` (+ `cotal_feedback`) |\n| Inbound, pull | MCP tool `cotal_inbox` (same server) |\n| Inbound, push | Channel nudge + hook drain (below) |\n\nThe manager launches the *real* `claude` (no wrapper):\n\n```\nclaude --strict-mcp-config --mcp-config '{\"mcpServers\":{\"cotal\":{\u2026}}}' \\\n --dangerously-load-development-channels server:cotal\n# env: COTAL_SPACE, COTAL_NAME, COTAL_ROLE, COTAL_SERVERS, COTAL_CHANNEL=1\n```\n\n- **MCP isolation.** A spawned agent runs with **only** the cotal MCP server:\n `--strict-mcp-config` ignores every other MCP source, crucially the operator's personal\n `~/.claude.json` servers (several spawns each booting a heavy helper would starve\n memory). Share your own servers deliberately (see below).\n- **Installed, not `--plugin-dir`.** The plugin is installed once (`claude plugin install\n cotal@cotal-mesh --scope local`) because its hooks bind only to an *installed* plugin.\n In a clone the marketplace is the repo's `.claude-plugin/marketplace.json`; `cotal setup`\n (npx, no clone) materializes the same marketplace under `~/.cotal/claude-plugin/` (each plugin dir is\n rebuilt from scratch and atomically replaced, never merged, so no stale file rides in). The\n `cotal-skills` plugin installs from that same marketplace at user scope (`claude plugin install\n cotal-skills@cotal-mesh --scope user`); its assets ship inside the CLI package, not the connector, and\n its version tracks the CLI release so updates land.\n- **Identity-gated.** Connector code requires `COTAL_NAME` *or* `COTAL_LINK`. A plain\n `claude` with no `COTAL_*` env stays inert and never joins, so your own sessions in a\n repo do not appear as stray peers.\n- **Hands-free.** The dev-channels flag prints a one-time confirm prompt; the PTY runtime\n auto-clears it, so a supervised launch needs no keypress.\n\nInbound mesh messages arrive in context as\n`<channel source=\"cotal\" from=\"bob\" kind=\"dm\" \u2026>\u2026</channel>`: each meta key a tag\nattribute the agent can read for routing.\n\n## How messages reach the session\n\nDurable deliveries land in the connector's inbox from JetStream consumers\n([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding)); live channel traffic can instead arrive\nthrough an at-most-once core subscription. A durable message sent while the agent is busy\nor offline waits on the stream. Two things move a message from inbox to model; one\ndelivers, the other only wakes:\n\n- **Hook drain (delivery).** `SessionStart` / `UserPromptSubmit` hooks read automatic inbox items and\n inject them as `additionalContext`. This is the single authoritative path: deterministic and works\n on any Claude Code build. Quiet ambient is excluded and stays buffered for `cotal_inbox`.\n A message is **acked only once the hook reply carrying it has cleared both legs of its journey**:\n the connector's control socket to the hook process (which gives up after 2s), and the hook\n process's own stdout to Claude Code (which it force-exits 1s after starting to write). The relay\n sends a receipt back down the control socket from that stdout write's callback, and only on a\n clean write (a runtime whose pipe has gone away fails it), and the connector treats that receipt,\n not its own socket write, as delivery. So a large injection killed mid-flush, or one written to a\n broken pipe, leaves the message un-acked and JetStream redelivers it. What this does *not* prove is\n that Claude Code read or applied the reply: a payload small enough to fit the pipe buffer is\n reported written the moment the kernel takes it. That residual is why the path errs toward\n at-least-once rather than treating a confirmed write as a confirmed read. Acking when\n the reply was merely *formatted* meant a lost reply was a lost message: it was already marked\n handled, so its own redelivery was silently acked on arrival.\n This errs toward **at-least-once**: if a reply lands but its confirmation does not, the batch is\n surfaced again and flagged as a possible repeat. A duplicate injection is noise; a buried DM stops\n the peer answering at all.\n- **Channel nudge (wake).** An arriving message fires a `notifications/claude/channel`\n event that wakes an *idle* session into a turn, so the drain runs *now* instead of at\n the next prompt. The nudge never acks anything. A nudge that the host rejects is retried with a\n bounded backoff while anything is still pending. For an idle session it is the only wake source,\n so dropping it means silence until someone types. If a nudge is lost anyway (a race in the host's\n channel startup), JetStream redelivery re-announces the unacked durable item through the same\n attention policy, so a durable message always wakes the session eventually. If the channel cannot\n run at all, delivery still waits for the next hook. Live-only traffic has no durable retry.\n\n**Two priority tiers.** A *directed* message (DM, anycast, or a channel message that\n`@mentions` us) always nudges. *Ambient* channel chatter does not nudge mid-turn; it\naccumulates, and the `Stop` \u2192 idle transition fires one batch nudge so the backlog drains\ntogether.\n\n**Constraints (accepted).** Channels are a Claude Code research preview (\u2265 v2.1.80;\npermission relay \u2265 v2.1.81): Anthropic auth only, admin-enabled on Team/Enterprise, and a\ncustom channel needs the `--dangerously-load-development-channels` launch flag. The hook\ndrain does not depend on any of that; the channel only adds \"wake me when idle.\"\n\nThe same channel also relays **tool-permission requests** onto the mesh, so a peer (a\nhuman at the CLI, a policy node) can approve or deny an agent's pending tool call through\nCotal rather than a per-terminal prompt.\n\n### Attention: how much traffic wakes you\n\nAn agent picks how aggressively peer traffic reaches it with\n`cotal_status({ attention })` (three modes, orthogonal to presence):\n\n| arrival | open (default) | dnd | focus |\n|---|---|---|---|\n| directed (dm / anycast) | wake + inject | wake + inject | wake + inject |\n| channel `@mention` | wake + inject | wake + inject | ack-drop; wake to *pull*; not injected |\n| ambient channel chatter | wake when idle; hold while working | never wakes; injects next turn | ack-drop; recall via `cotal_inbox` |\n\nPer-channel overrides refine this: **quiet** (delivered, never wakes; `@mention` still\nwakes) and **muted** (dropped on receive, mentions included; DMs/anycast unaffected), set\nwith `cotal_channel_mode` or as agent-file defaults (`quiet:` / `muted:`,\n[agent files](agent-files.md)). A per-channel override is the final word for that channel.\nQuiet ambient is pull-only: it never hitchhikes on a human prompt, DM, mention, or other\nconnector-driven turn. `cotal_inbox` explicitly surfaces and clears it. A quiet-channel\n`@mention` remains automatic and injects normally.\n\nA pull is bounded too, and clears only what it hands over. One `cotal_inbox` call carries at most a\nreceivable window (direct messages and role requests first, then channel traffic, replayed history\nlast); whatever does not fit stays buffered, is named in the reply, and comes back on the next call.\nA message too large for one whole response is never consumed at all: it is named with its sender and\nsize and left buffered, because clearing what cannot be delivered is the loss this bound exists to stop.\nThat matters most on the path where it is easiest to lose mail: reconnecting brings a channel-history\nreplay with it, so the largest payload and the least expendable message arrive in the same read.\n\nThe local inbox is bounded. On pathological overflow it evicts pull-only items before automatic\ntraffic. If the bounded live/durable classification guard also fills, the connector fails closed:\notherwise-normal ambient becomes pull-only until restart. Muted hard-drop and normal focus recall\nstill take precedence. Focus also keeps a bounded exclusion list so mode toggles cannot recall\nquiet/muted traffic; if that safety bound fills, recall skips the affected channel and reports it\nas incomplete rather than risk resurfacing excluded content.\nIf the separate hard-drop disposition guard fills, channel traffic is dropped for the rest of the\nsession rather than risk a late copy bypassing an earlier muted/focus decision; DMs and anycast are\nunaffected.\n\nAttention is **advisory UX, not a boundary**: any peer can wake a dnd/focus agent by\nnaming it, and `muted` means \"I opted out of receiving\", not \"the channel is blocked\";\nthe broker still authorizes and delivers. Focus's real effect is shrinking the\nuntrusted-ambient injection surface (only subject-authenticated dm/anycast auto-inject).\nIt resets to **open** on `SessionStart`, so a restarted agent never stays silently deaf.\nYour attention is mirrored into presence so peers can see it.\n\n## Presence mapping\n\nThe connector wires a small subset of Claude Code hooks to presence states; presence is\ncoarse, and \"what it is doing\" rides on activity updates. Presence is **advisory**: a presence\npublish that fails (the endpoint mid-reconnect, say) is swallowed and never prevents the same hook\nfrom delivering messages or flushing held ones.\n\n| Hook | \u2192 state |\n|---|---|\n| `SessionStart` | `idle` (join; surfaces the inbox; captures the live model into `meta.model` when no pin) |\n| `UserPromptSubmit` | `working` (turn starts; surfaces the inbox) |\n| `PreToolUse` | no change; records *what* is about to run, so a permission wait can name it |\n| `Notification` (permission / elicitation) | `waiting` (blocked on a human: activity leads with the pending tool, e.g. `Bash: git push \u2026`) |\n| `Stop` / `StopFailure` | `idle` (turn done / died on an API error; flushes anything held while busy). On the [event plane](#event-plane) the two differ: `StopFailure` closes the run with `RUN_ERROR`. |\n| `SessionEnd` | `offline` (graceful leave) |\n\nHooks are relayed over the connector's **authenticated** local control endpoint (per-user\nsocket + per-launch token, constant-time checked), so a local process that finds the path\nstill can't drive presence or stop the agent. The full Claude Code hook-event list lives\nwith the adapter:\n[`extensions/connector-claude-code`](../extensions/connector-claude-code/README.md).\n\n## Event plane\n\nA session launched with `cotal spawn --events` publishes a **structured** account of what it\ndid: run boundaries per turn, assistant text, reasoning, and each tool call with its arguments,\nits end, and its result. Not prose about the work, the work itself, in a vocabulary a program can\nread. Arming is `COTAL_EVENTS`, which the launcher sets for `--events` spawns; a personal session\nwith the plugin installed publishes nothing.\n\nTool arguments and results go on this channel verbatim, so withholding user-authored text does not\nmake the stream safe to widen: anything a tool reads or prints, including a secret in a command line\nor in the contents of a file, reaches every reader of the channel.\n\nThe channel is **`events.<owner>.<actor>`**, named after the session's principal. What the actor\nhalf is depends on the mesh, and the difference matters when you go looking for it: on a static mesh\nit is a key the manager allocated, never the display name, so two live agents sharing a display name\ndo not share a stream; on a user-auth mesh it is the agent's own name, because that is what the\nledger row is keyed on. Spelled out again with both halves below. The launch grants publish rights\non exactly that one channel. A spawn\nthat asks for a *different* agent's event channel is refused at the door rather than granted, since\nthat channel carries the session's tool inputs and outputs. The same rule runs on restart: a manager\nresume document that names another agent's event channel is refused rather than adopted, because the\nmanaged row is re-armed from that document and the credential is re-minted from the row.\n\nThe rule reads a **concrete** channel, two principal tokens and nothing else. A pattern such as\n`events.<owner>.>` is not an event channel to it and passes untouched, governed by ordinary ACL\nauthority: on a user mesh the delegation envelope, on a static mesh the spawning credential itself.\nThat is deliberate, because the pattern is the form an operator writes on purpose for an observer,\nand it is worth knowing rather than assuming the fence is total.\n\nTo let something else read a plane, grant it out of band. The refusal prints the command for the\nmesh it is running on, spelled out in full, and only that one.\n\nOn a **user-auth** mesh:\n\n```bash\ncotal actor grant <reader> --owner <owner> --scope '' --allow-subscribe 'events.<owner>.<actor>' --allow-publish ''\n```\n\nEvery field, deliberately. `actor grant` is an upsert of the whole row, and an omitted flag is not\n\"leave it alone\": it is the wide default, `>` read, `>` post, and `spawn,role:default` scope. A bare\n`cotal actor grant <reader>` therefore grants a reader of every channel in the space, which is the\nopposite of what a scoped watcher is for.\n\nOn a **static** mesh there is no actor ledger for `actor grant` to write to, and the refusal says\nso; mint the reader instead:\n\n```bash\ncotal mint watcher --profile agent --allow-subscribe 'events.<owner>.<actor>' --provision\n```\n\nThe **agent** profile, not the observer one. `mint` reads `--allow-subscribe` only for that\nprofile, and refuses it anywhere else: `--profile observer --allow-subscribe <channel>` exits\nnon-zero and writes no creds file, because the observer profile carries a fixed read set over the\nwhole chat plane, which is the opposite of what a scoped watcher is for. The agent profile also prints the lifecycle uid the\nreader needs, since an authed consuming endpoint refuses to start without one.\n\nTwo things a reader has to do that are not obvious, both on `CotalEndpoint`. It must pass the event\nchannel in `channels`, or the endpoint joins `general` by default and a scoped credential is refused\nthere. And it reads history with `readHistory(channel)`, the delivery daemon's mediated read, not\n`channelHistory(channel)`: a scoped credential is denied the ad-hoc consumer the direct read\ncreates, by design. `cotal console` and the web console already do both.\n\nThe `<owner>.<actor>` pair is the session's principal, not its display name. On a user-auth mesh\nthe actor half **is** the agent's name, so the channel is `events.<your-owner>.<agent-name>`. On a\nstatic mesh the owner half is the literal `local` and the actor is a key the manager allocated, so\nthe channel is `events.local.<key>`; the spawn reply carries that key as `id`. Note\nthat `cotal console` and the web console keep event channels out of their channel lists on purpose,\nsince a plane is a machine feed rather than a conversation; they draw the frames when you open the\nchannel by name.\n\nThe rule governs the manager's doors, which are the ones a caller other than you can reach. A\nforeground `cotal spawn` on your own machine mints from your own signing material, so it can still\ngrant any channel you name: that is the out-of-band grant, not a way around the rule.\n\n**A failed turn is published as a run error, not as a finished run.** Claude Code decides for itself\nwhether a turn finished or died and fires one of two hooks accordingly, so the connector relays that\ndecision rather than making one of its own: a turn that ended on an API error ends its run with\n`RUN_ERROR` carrying the harness's own error kind (`rate_limit`, `billing_error`, `server_error`,\n`max_output_tokens` and the rest) as the code, and whatever detail it reported as the message. If that\ndetail cannot fit in the one closing frame, the shared close still publishes exactly one `RUN_ERROR`\nthat does fit: it keeps the code and says the original detail was omitted or shortened because of the\nbound, so a reader is never shown a truncated message as complete. A turn that ended normally still\nends with a run-finished event carrying no outcome, which says the turn ended and does not claim it\nsucceeded.\n\nEvents are written to a per-session write-ahead log before they are published, so a hook that fires\nafter a restart resumes at the cursor it left rather than replaying or skipping, and a run that was\nopen when the session stopped is closed rather than left dangling.\n\nOne channel carries **every session of one agent**, because it is named after the principal and not\nafter the session. Alongside the per-session logs the connector keeps one small record per principal,\nholding the last sequence the broker assigned on that channel, so a new session continues the stream\nits predecessor left instead of starting again from nothing. Both live under the events state root\n(`COTAL_WORKSPACE_ROOT`), and neither is something you edit by hand.\n\nA **missing** record is not a fault: the connector rebuilds it from the session logs beside it,\nwhich is how an agent that was already running before this record existed keeps its stream. That\nrebuild stops if any one of those session logs is damaged. Unreadable, not valid JSON, and written\nfor a different principal all count, and so does a session directory or a log that is a link rather\nthan the real file the connector wrote, or a log that has more than one name. A tip taken from the\nrest would be too low, and it would stop publication later with nothing left to point at the cause.\nThe connector names the file instead, and the only way past it is the directory removal described\nbelow, under the same condition. A record that **disagrees with the broker** is a fault, and the\nconnector stops publishing and says why rather than guessing. A record that **moved while a session\nwas writing to it** is refused the same way: it means something else wrote the principal's record,\nand the connector reports which value it held and which the file holds rather than writing over the\nlater one. There is no command to clear it. The state is the principal's directory under the events\nroot, and clearing it by hand means removing that directory whole: the sequence, the cursor and the\nper-session logs only mean anything together, so removing part of it leaves a state the next start\nrefuses. Removing it is only half a remedy, and the half that comes first is the channel. The\ndirectory is where the agent's memory of the tip lives, not the tip itself, so on a channel that\nstill holds frames the next session opens expecting an empty one and stops on the same\ndisagreement, with the logs a tip could have been rebuilt from now gone. Purge the channel first,\nthen remove the directory.\n\nReading it: `cotal console` and the web console draw event frames directly. A frame carries no text\npart by design, so a surface that renders a message as flat text shows a marker instead of prose.\n\n**On a per-user-auth mesh, arming needs the spawner's grant to cover the channel.** The event\nchannel is added to the child's publish set, and delegation only narrows: an agent may hand down\na subset of what it holds and no more. So a peer-initiated `--events` spawn is refused unless the\nspawning identity's own grant already covers the child's event channel. The refusal prints the\nexact `cotal actor grant` command that widens it. An operator launch, whose chain reaches an\nadmin-scoped or roster row, is unaffected.\n\n## Resume an existing session (fork, never hijack)\n\n`--resume <session-id>` pulls an existing Claude session, its context and transcript,\ninto the mesh. It **forks**: Claude mints a *new* session id from that transcript\n(`--resume <id> --fork-session`), so the meshed agent gets its own session and the\noriginal is untouched.\n\n- `cotal spawn --resume <id>` (foreground) is the primary surface: the transcript is on\n *your* machine, and errors are Claude's own stderr, inline.\n- `--detach --resume <id>` works, with two differences: the id resolves against the\n **manager host's** `~/.claude` (you practically need `--cwd`), and the manager waits for\n a real outcome; `\u2713 started` means the agent *joined the mesh*, `\u2717 exited on launch`\n carries Claude's last output, and an uncertain launch (~30 s) is reported without\n tearing the agent down.\n- Resume is an **operator surface only**, deliberately not exposed on MCP `cotal_spawn`\n (a mesh peer naming host-local transcripts would widen `spawn` into transcript\n disclosure). Only the Claude connector supports it today; OpenCode and Hermes fail loud.\n- Needs a `claude` new enough for `--resume \u2026 --fork-session` (verified on 2.1.197).\n\n## Sharing your MCP servers\n\nIsolation is the default, but a meshed teammate sometimes genuinely needs one of your own\ntools (say, web search). The opt-in is the cotal config file\n(`~/.config/cotal/config.json`, or a space-local `.cotal/config.json` layered on top):\neach entry the familiar `.mcp.json` shape, secrets written as `${VAR}` references, never\nliterals ([full format](config.md)).\n\nAt launch the connector forwards *only* the named vars the chosen servers declare and\npasses the merged config as an owner-only temp file; `--strict-mcp-config` stays on, so\nonly cotal + the explicitly shared servers load. Scope per spawn with\n`--share-tools tavily,figma` (or `--share-tools none`).\n\nTwo caveats: sharing a server grants its credential to the agent (the var lives in the\nClaude process's environment, so share only when you're fine with that teammate holding\nthe key), and memory adds up, because a heavy server boots once per spawn, multiplied\nacross a team.\n\n## Feedback\n\n`cotal_feedback` works out of the box: without a key it posts to the public intake at\n`https://cotal.ai/v1/feedback` (needs a contact email: `COTAL_FEEDBACK_EMAIL`, then\n`git config user.email`, else the agent asks). Set `COTAL_FEEDBACK_KEY=fbk_<key>` in a\nbeta tester's environment to route to the keyed intake (`Authorization: Bearer`, identity\nderived from the key); `COTAL_FEEDBACK_URL` overrides either endpoint. The CLI can send\ntoo: `cotal feedback \"<summary>\" [--type bug]`. Each submission carries\n`origin: human | agent`, whether the tester asked, or the agent auto-reported a major\nissue.\n"
|
|
16156
|
+
"body": "# Connect Claude\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\nThe Claude Code connector turns a real `claude` session into a Cotal mesh peer. A bundled\nplugin inside the session joins NATS, maps lifecycle hooks to presence, and exposes the\nmesh tools. Nothing wraps Claude; it is an ordinary session that happens to be on the\nmesh.\n\nThe shared mesh runtime (agent, `cotal_*` tools, hook relay) lives in\n[`@cotal-ai/connector-core`](../extensions/connector-core); this connector is the thin\nClaude-specific adapter over it. Siblings: [OpenCode](connect-opencode.md) (beta),\n[Hermes](connect-hermes.md) (alpha), [pi](connect-pi.md) (alpha); the\n[Connectors](connectors.md) matrix compares them feature-by-feature.\n\n## Set up\n\n```bash\ncotal setup # one-time: installs the plugin, seeds one agent; launches nothing\ncotal up # brings up the mesh + delivery daemon + a detached manager\n```\n\n`cotal setup` installs the cotal plugin (so the repo's Claude sessions get the `cotal_*`\ntools) and seeds one `default` persona; `cotal up` brings up the local stack so\n`cotal spawn --detach` / `cotal_spawn` work right away. Re-running either is idempotent.\nThe install mechanics and the invariants behind them are in\n[setup internals](setup-internals.md).\n\n`cotal setup` also installs Cotal's authored Agent Skills (`SKILL.md`, the agentskills.io format) for\ncoordinating agent teams (today `team-topology`), from one canonical source, on two channels:\n\n- **Claude Code** gets a second, skills-only plugin, `cotal-skills`, from the same `cotal-mesh`\n marketplace, at **user scope** (machine-wide), and **independent of the mesh connector**: it carries no\n code and no core dependency, installs whenever Claude is on `PATH` (even with the connector removed),\n and uninstalls on its own with `claude plugin uninstall cotal-skills --scope user`. Its plugin version\n is stamped from the running CLI release, so an upgrade + `cotal setup` runs `claude plugin update` and\n the deployed install actually gets the new skill. `cotal setup` installs it on first run and on repeat\n runs, so upgraders are not left behind.\n- **Every other harness** (Codex, Cursor, OpenCode, Gemini CLI, Windsurf/Devin) reads the cross-vendor\n `~/.agents/skills/` directory convention, which has no remote index, so `cotal setup` **reconciles** it:\n it installs/updates each Cotal skill, backs up a copy you have edited to `SKILL.md.bak` before\n replacing it, and removes a Cotal skill that is no longer shipped. Only skills Cotal owns are touched;\n your own or third-party skills there are left alone. `cotal status` reports whether the drop is current,\n stale, missing, or has a retired skill to reconcile. This is the working cross-vendor path.\n\nCotal also generates an [Agent Skills discovery index](https://cotal.ai/.well-known/agent-skills/index.json)\non cotal.ai, but that RFC is still a draft with no harness consuming it yet, so it is a forward bet,\nnot a channel to rely on today.\n\n## Spawn a session\n\n```bash\ncotal spawn # foreground: your default agent, in this terminal\ncotal spawn dave --detach # supervised: the manager runs it in a PTY\n```\n\nA spawn resolves a persona from `.cotal/agents/<name>.md` ([agent files](agent-files.md));\n`--model`, `--variant`, `--cwd`, `--prompt`, ACL overrides, and `--share-tools` apply to\nboth forms ([run a mesh](run-a-mesh.md) has the full resolution rules). The session joins\nwith identity from its environment and auto-registers presence by the time it is\ninteractive.\n\nInside the session, the agent orients with one read-only tool, `cotal_orientation`: its\nidentity, the channels it reads and may post to, its capabilities, the tools available,\nwho's present, and unread counts. The full tool surface is the\n[MCP tool catalog](mcp-tools.md). In auth mode the team-supervision tools\n(`cotal_spawn` / `cotal_persona`) are injected **only** for personas declaring\n`capabilities: [spawn]` (the same grant that opens the privileged control subject), so an\nagent's toolset matches what it can actually invoke. Clearing retained history is\noperator-only ([run a mesh](run-a-mesh.md)), never an agent tool.\n\n## How it binds\n\nClaude Code exposes four integration surfaces, and three of them collapse into a single\ndual-purpose MCP server:\n\n| Surface | Mechanism |\n|---|---|\n| Outbound, ambient | `http` lifecycle hooks \u2192 POST to the connector (presence, activity) |\n| Outbound, deliberate | MCP tools `cotal_send` / `cotal_dm` / `cotal_anycast` (+ `cotal_feedback`) |\n| Inbound, pull | MCP tool `cotal_inbox` (same server) |\n| Inbound, push | Channel nudge + hook drain (below) |\n\nThe manager launches the *real* `claude` (no wrapper):\n\n```\nclaude --strict-mcp-config --mcp-config '{\"mcpServers\":{\"cotal\":{\u2026}}}' \\\n --dangerously-load-development-channels server:cotal\n# env: COTAL_SPACE, COTAL_NAME, COTAL_ROLE, COTAL_CHANNEL=1, plus claude's documented auth vars\n```\n\n- **Model auth.** Locally, `claude` still reads macOS Keychain / `~/.claude`. In a container or\n CI there is no Keychain, so the connector forwards the documented credential set \u2014\n `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`), `ANTHROPIC_API_KEY` /\n `ANTHROPIC_AUTH_TOKEN`, and the cloud-provider flags plus their credential vars. Host-session\n markers (`CLAUDE_CODE_CHILD_SESSION`, `CLAUDECODE`) stay out so a nested seat still saves a\n transcript. See [Deploy](deploy.md).\n- **MCP isolation.** A spawned agent runs with **only** the cotal MCP server:\n `--strict-mcp-config` ignores every other MCP source, crucially the operator's personal\n `~/.claude.json` servers (several spawns each booting a heavy helper would starve\n memory). Share your own servers deliberately (see below).\n- **Installed, not `--plugin-dir`.** The plugin is installed once (`claude plugin install\n cotal@cotal-mesh --scope local`) because its hooks bind only to an *installed* plugin.\n In a clone the marketplace is the repo's `.claude-plugin/marketplace.json`; `cotal setup`\n (npx, no clone) materializes the same marketplace under `~/.cotal/claude-plugin/` (each plugin dir is\n rebuilt from scratch and atomically replaced, never merged, so no stale file rides in). The\n `cotal-skills` plugin installs from that same marketplace at user scope (`claude plugin install\n cotal-skills@cotal-mesh --scope user`); its assets ship inside the CLI package, not the connector, and\n its version tracks the CLI release so updates land.\n- **Identity-gated.** Connector code requires `COTAL_NAME` *or* `COTAL_LINK`. A plain\n `claude` with no `COTAL_*` env stays inert and never joins, so your own sessions in a\n repo do not appear as stray peers.\n- **Hands-free.** The dev-channels flag prints a one-time confirm prompt; the PTY runtime\n auto-clears it, so a supervised launch needs no keypress.\n\nInbound mesh messages arrive in context as\n`<channel source=\"cotal\" from=\"bob\" kind=\"dm\" \u2026>\u2026</channel>`: each meta key a tag\nattribute the agent can read for routing.\n\n## How messages reach the session\n\nDurable deliveries land in the connector's inbox from JetStream consumers\n([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding)); live channel traffic can instead arrive\nthrough an at-most-once core subscription. A durable message sent while the agent is busy\nor offline waits on the stream. Two things move a message from inbox to model; one\ndelivers, the other only wakes:\n\n- **Hook drain (delivery).** `SessionStart` / `UserPromptSubmit` hooks read automatic inbox items and\n inject them as `additionalContext`. This is the single authoritative path: deterministic and works\n on any Claude Code build. Quiet ambient is excluded and stays buffered for `cotal_inbox`.\n A message is **acked only once the hook reply carrying it has cleared both legs of its journey**:\n the connector's control socket to the hook process (which gives up after 2s), and the hook\n process's own stdout to Claude Code (which it force-exits 1s after starting to write). The relay\n sends a receipt back down the control socket from that stdout write's callback, and only on a\n clean write (a runtime whose pipe has gone away fails it), and the connector treats that receipt,\n not its own socket write, as delivery. So a large injection killed mid-flush, or one written to a\n broken pipe, leaves the message un-acked and JetStream redelivers it. What this does *not* prove is\n that Claude Code read or applied the reply: a payload small enough to fit the pipe buffer is\n reported written the moment the kernel takes it. That residual is why the path errs toward\n at-least-once rather than treating a confirmed write as a confirmed read. Acking when\n the reply was merely *formatted* meant a lost reply was a lost message: it was already marked\n handled, so its own redelivery was silently acked on arrival.\n This errs toward **at-least-once**: if a reply lands but its confirmation does not, the batch is\n surfaced again and flagged as a possible repeat. A duplicate injection is noise; a buried DM stops\n the peer answering at all.\n- **Channel nudge (wake).** An arriving message fires a `notifications/claude/channel`\n event that wakes an *idle* session into a turn, so the drain runs *now* instead of at\n the next prompt. The nudge never acks anything. A nudge that the host rejects is retried with a\n bounded backoff while anything is still pending. For an idle session it is the only wake source,\n so dropping it means silence until someone types. If a nudge is lost anyway (a race in the host's\n channel startup), JetStream redelivery re-announces the unacked durable item through the same\n attention policy, so a durable message always wakes the session eventually. If the channel cannot\n run at all, delivery still waits for the next hook. Live-only traffic has no durable retry.\n\n**Two priority tiers.** A *directed* message (DM, anycast, or a channel message that\n`@mentions` us) always nudges. *Ambient* channel chatter does not nudge mid-turn; it\naccumulates, and the `Stop` \u2192 idle transition fires one batch nudge so the backlog drains\ntogether.\n\n**Constraints (accepted).** Channels are a Claude Code research preview (\u2265 v2.1.80;\npermission relay \u2265 v2.1.81): Anthropic auth only, admin-enabled on Team/Enterprise, and a\ncustom channel needs the `--dangerously-load-development-channels` launch flag. The hook\ndrain does not depend on any of that; the channel only adds \"wake me when idle.\"\n\nThe same channel also relays **tool-permission requests** onto the mesh, so a peer (a\nhuman at the CLI, a policy node) can approve or deny an agent's pending tool call through\nCotal rather than a per-terminal prompt.\n\n### Attention: how much traffic wakes you\n\nAn agent picks how aggressively peer traffic reaches it with\n`cotal_status({ attention })` (three modes, orthogonal to presence):\n\n| arrival | open (default) | dnd | focus |\n|---|---|---|---|\n| directed (dm / anycast) | wake + inject | wake + inject | wake + inject |\n| channel `@mention` | wake + inject | wake + inject | ack-drop; wake to *pull*; not injected |\n| ambient channel chatter | wake when idle; hold while working | never wakes; injects next turn | ack-drop; recall via `cotal_inbox` |\n\nPer-channel overrides refine this: **quiet** (delivered, never wakes; `@mention` still\nwakes) and **muted** (dropped on receive, mentions included; DMs/anycast unaffected), set\nwith `cotal_channel_mode` or as agent-file defaults (`quiet:` / `muted:`,\n[agent files](agent-files.md)). A per-channel override is the final word for that channel.\nQuiet ambient is pull-only: it never hitchhikes on a human prompt, DM, mention, or other\nconnector-driven turn. `cotal_inbox` explicitly surfaces and clears it. A quiet-channel\n`@mention` remains automatic and injects normally.\n\nA pull is bounded too, and clears only what it hands over. One `cotal_inbox` call carries at most a\nreceivable window (direct messages and role requests first, then channel traffic, replayed history\nlast); whatever does not fit stays buffered, is named in the reply, and comes back on the next call.\nA message too large for one whole response is never consumed at all: it is named with its sender and\nsize and left buffered, because clearing what cannot be delivered is the loss this bound exists to stop.\nThat matters most on the path where it is easiest to lose mail: reconnecting brings a channel-history\nreplay with it, so the largest payload and the least expendable message arrive in the same read.\n\nThe local inbox is bounded. On pathological overflow it evicts pull-only items before automatic\ntraffic. If the bounded live/durable classification guard also fills, the connector fails closed:\notherwise-normal ambient becomes pull-only until restart. Muted hard-drop and normal focus recall\nstill take precedence. Focus also keeps a bounded exclusion list so mode toggles cannot recall\nquiet/muted traffic; if that safety bound fills, recall skips the affected channel and reports it\nas incomplete rather than risk resurfacing excluded content.\nIf the separate hard-drop disposition guard fills, channel traffic is dropped for the rest of the\nsession rather than risk a late copy bypassing an earlier muted/focus decision; DMs and anycast are\nunaffected.\n\nAttention is **advisory UX, not a boundary**: any peer can wake a dnd/focus agent by\nnaming it, and `muted` means \"I opted out of receiving\", not \"the channel is blocked\";\nthe broker still authorizes and delivers. Focus's real effect is shrinking the\nuntrusted-ambient injection surface (only subject-authenticated dm/anycast auto-inject).\nIt resets to **open** on `SessionStart`, so a restarted agent never stays silently deaf.\nYour attention is mirrored into presence so peers can see it.\n\n## Presence mapping\n\nThe connector wires a small subset of Claude Code hooks to presence states; presence is\ncoarse, and \"what it is doing\" rides on activity updates. Presence is **advisory**: a presence\npublish that fails (the endpoint mid-reconnect, say) is swallowed and never prevents the same hook\nfrom delivering messages or flushing held ones.\n\n| Hook | \u2192 state |\n|---|---|\n| `SessionStart` | `idle` (join; surfaces the inbox; captures the live model into `meta.model` when no pin) |\n| `UserPromptSubmit` | `working` (turn starts; surfaces the inbox) |\n| `PreToolUse` | no change; records *what* is about to run, so a permission wait can name it |\n| `Notification` (permission / elicitation) | `waiting` (blocked on a human: activity leads with the pending tool, e.g. `Bash: git push \u2026`) |\n| `Stop` / `StopFailure` | `idle` (turn done / died on an API error; flushes anything held while busy). On the [event plane](#event-plane) the two differ: `StopFailure` closes the run with `RUN_ERROR`. |\n| `SessionEnd` | `offline` (graceful leave) |\n\nHooks are relayed over the connector's **authenticated** local control endpoint (per-user\nsocket + per-launch token, constant-time checked), so a local process that finds the path\nstill can't drive presence or stop the agent. The full Claude Code hook-event list lives\nwith the adapter:\n[`extensions/connector-claude-code`](../extensions/connector-claude-code/README.md).\n\n## Event plane\n\nA session launched with `cotal spawn --events` publishes a **structured** account of what it\ndid: run boundaries per turn, assistant text, reasoning, and each tool call with its arguments,\nits end, and its result. Not prose about the work, the work itself, in a vocabulary a program can\nread. Arming is `COTAL_EVENTS`, which the launcher sets for `--events` spawns; a personal session\nwith the plugin installed publishes nothing.\n\nTool arguments and results go on this channel verbatim, so withholding user-authored text does not\nmake the stream safe to widen: anything a tool reads or prints, including a secret in a command line\nor in the contents of a file, reaches every reader of the channel.\n\nThe channel is **`events.<owner>.<actor>`**, named after the session's principal. What the actor\nhalf is depends on the mesh, and the difference matters when you go looking for it: on a static mesh\nit is a key the manager allocated, never the display name, so two live agents sharing a display name\ndo not share a stream; on a user-auth mesh it is the agent's own name, because that is what the\nledger row is keyed on. Spelled out again with both halves below. The launch grants publish rights\non exactly that one channel. A spawn\nthat asks for a *different* agent's event channel is refused at the door rather than granted, since\nthat channel carries the session's tool inputs and outputs. The same rule runs on restart: a manager\nresume document that names another agent's event channel is refused rather than adopted, because the\nmanaged row is re-armed from that document and the credential is re-minted from the row.\n\nThe rule reads a **concrete** channel, two principal tokens and nothing else. A pattern such as\n`events.<owner>.>` is not an event channel to it and passes untouched, governed by ordinary ACL\nauthority: on a user mesh the delegation envelope, on a static mesh the spawning credential itself.\nThat is deliberate, because the pattern is the form an operator writes on purpose for an observer,\nand it is worth knowing rather than assuming the fence is total.\n\nTo let something else read a plane, grant it out of band. The refusal prints the command for the\nmesh it is running on, spelled out in full, and only that one.\n\nOn a **user-auth** mesh:\n\n```bash\ncotal actor grant <reader> --owner <owner> --scope '' --allow-subscribe 'events.<owner>.<actor>' --allow-publish ''\n```\n\nEvery field, deliberately. `actor grant` is an upsert of the whole row, and an omitted flag is not\n\"leave it alone\": it is the wide default, `>` read, `>` post, and `spawn,role:default` scope. A bare\n`cotal actor grant <reader>` therefore grants a reader of every channel in the space, which is the\nopposite of what a scoped watcher is for.\n\nOn a **static** mesh there is no actor ledger for `actor grant` to write to, and the refusal says\nso; mint the reader instead:\n\n```bash\ncotal mint watcher --profile agent --allow-subscribe 'events.<owner>.<actor>' --provision\n```\n\nThe **agent** profile, not the observer one. `mint` reads `--allow-subscribe` only for that\nprofile, and refuses it anywhere else: `--profile observer --allow-subscribe <channel>` exits\nnon-zero and writes no creds file, because the observer profile carries a fixed read set over the\nwhole chat plane, which is the opposite of what a scoped watcher is for. The agent profile also prints the lifecycle uid the\nreader needs, since an authed consuming endpoint refuses to start without one.\n\nTwo things a reader has to do that are not obvious, both on `CotalEndpoint`. It must pass the event\nchannel in `channels`, or the endpoint joins `general` by default and a scoped credential is refused\nthere. And it reads history with `readHistory(channel)`, the delivery daemon's mediated read, not\n`channelHistory(channel)`: a scoped credential is denied the ad-hoc consumer the direct read\ncreates, by design. `cotal console` and the web console already do both.\n\nThe `<owner>.<actor>` pair is the session's principal, not its display name. On a user-auth mesh\nthe actor half **is** the agent's name, so the channel is `events.<your-owner>.<agent-name>`. On a\nstatic mesh the owner half is the literal `local` and the actor is a key the manager allocated, so\nthe channel is `events.local.<key>`; the spawn reply carries that key as `id`. Note\nthat `cotal console` and the web console keep event channels out of their channel lists on purpose,\nsince a plane is a machine feed rather than a conversation; they draw the frames when you open the\nchannel by name.\n\nThe rule governs the manager's doors, which are the ones a caller other than you can reach. A\nforeground `cotal spawn` on your own machine mints from your own signing material, so it can still\ngrant any channel you name: that is the out-of-band grant, not a way around the rule.\n\n**A failed turn is published as a run error, not as a finished run.** Claude Code decides for itself\nwhether a turn finished or died and fires one of two hooks accordingly, so the connector relays that\ndecision rather than making one of its own: a turn that ended on an API error ends its run with\n`RUN_ERROR` carrying the harness's own error kind (`rate_limit`, `billing_error`, `server_error`,\n`max_output_tokens` and the rest) as the code, and whatever detail it reported as the message. If that\ndetail cannot fit in the one closing frame, the shared close still publishes exactly one `RUN_ERROR`\nthat does fit: it keeps the code and says the original detail was omitted or shortened because of the\nbound, so a reader is never shown a truncated message as complete. A turn that ended normally still\nends with a run-finished event carrying no outcome, which says the turn ended and does not claim it\nsucceeded.\n\nEvents are written to a per-session write-ahead log before they are published, so a hook that fires\nafter a restart resumes at the cursor it left rather than replaying or skipping, and a run that was\nopen when the session stopped is closed rather than left dangling.\n\nOne channel carries **every session of one agent**, because it is named after the principal and not\nafter the session. Alongside the per-session logs the connector keeps one small record per principal,\nholding the last sequence the broker assigned on that channel, so a new session continues the stream\nits predecessor left instead of starting again from nothing. Both live under the events state root\n(`COTAL_WORKSPACE_ROOT`), and neither is something you edit by hand.\n\nA **missing** record is not a fault: the connector rebuilds it from the session logs beside it,\nwhich is how an agent that was already running before this record existed keeps its stream. That\nrebuild stops if any one of those session logs is damaged. Unreadable, not valid JSON, and written\nfor a different principal all count, and so does a session directory or a log that is a link rather\nthan the real file the connector wrote, or a log that has more than one name. A tip taken from the\nrest would be too low, and it would stop publication later with nothing left to point at the cause.\nThe connector names the file instead, and the only way past it is the directory removal described\nbelow, under the same condition. A record that **disagrees with the broker** is a fault, and the\nconnector stops publishing and says why rather than guessing. A record that **moved while a session\nwas writing to it** is refused the same way: it means something else wrote the principal's record,\nand the connector reports which value it held and which the file holds rather than writing over the\nlater one. There is no command to clear it. The state is the principal's directory under the events\nroot, and clearing it by hand means removing that directory whole: the sequence, the cursor and the\nper-session logs only mean anything together, so removing part of it leaves a state the next start\nrefuses. Removing it is only half a remedy, and the half that comes first is the channel. The\ndirectory is where the agent's memory of the tip lives, not the tip itself, so on a channel that\nstill holds frames the next session opens expecting an empty one and stops on the same\ndisagreement, with the logs a tip could have been rebuilt from now gone. Purge the channel first,\nthen remove the directory.\n\nReading it: `cotal console` and the web console draw event frames directly. A frame carries no text\npart by design, so a surface that renders a message as flat text shows a marker instead of prose.\n\n**On a per-user-auth mesh, arming needs the spawner's grant to cover the channel.** The event\nchannel is added to the child's publish set, and delegation only narrows: an agent may hand down\na subset of what it holds and no more. So a peer-initiated `--events` spawn is refused unless the\nspawning identity's own grant already covers the child's event channel. The refusal prints the\nexact `cotal actor grant` command that widens it. An operator launch, whose chain reaches an\nadmin-scoped or roster row, is unaffected.\n\n## Resume an existing session (fork, never hijack)\n\n`--resume <session-id>` pulls an existing Claude session, its context and transcript,\ninto the mesh. It **forks**: Claude mints a *new* session id from that transcript\n(`--resume <id> --fork-session`), so the meshed agent gets its own session and the\noriginal is untouched.\n\n- `cotal spawn --resume <id>` (foreground) is the primary surface: the transcript is on\n *your* machine, and errors are Claude's own stderr, inline.\n- `--detach --resume <id>` works, with two differences: the id resolves against the\n **manager host's** `~/.claude` (you practically need `--cwd`), and the manager waits for\n a real outcome; `\u2713 started` means the agent *joined the mesh*, `\u2717 exited on launch`\n carries Claude's last output, and an uncertain launch (~30 s) is reported without\n tearing the agent down.\n- Resume is an **operator surface only**, deliberately not exposed on MCP `cotal_spawn`\n (a mesh peer naming host-local transcripts would widen `spawn` into transcript\n disclosure). Only the Claude connector supports it today; OpenCode and Hermes fail loud.\n- Needs a `claude` new enough for `--resume \u2026 --fork-session` (verified on 2.1.197).\n\n## Sharing your MCP servers\n\nIsolation is the default, but a meshed teammate sometimes genuinely needs one of your own\ntools (say, web search). The opt-in is the cotal config file\n(`~/.config/cotal/config.json`, or a space-local `.cotal/config.json` layered on top):\neach entry the familiar `.mcp.json` shape, secrets written as `${VAR}` references, never\nliterals ([full format](config.md)).\n\nAt launch the connector forwards *only* the named vars the chosen servers declare and\npasses the merged config as an owner-only temp file; `--strict-mcp-config` stays on, so\nonly cotal + the explicitly shared servers load. Scope per spawn with\n`--share-tools tavily,figma` (or `--share-tools none`).\n\nTwo caveats: sharing a server grants its credential to the agent (the var lives in the\nClaude process's environment, so share only when you're fine with that teammate holding\nthe key), and memory adds up, because a heavy server boots once per spawn, multiplied\nacross a team.\n\n## Feedback\n\n`cotal_feedback` works out of the box: without a key it posts to the public intake at\n`https://cotal.ai/v1/feedback` (needs a contact email: `COTAL_FEEDBACK_EMAIL`, then\n`git config user.email`, else the agent asks). Set `COTAL_FEEDBACK_KEY=fbk_<key>` in a\nbeta tester's environment to route to the keyed intake (`Authorization: Bearer`, identity\nderived from the key); `COTAL_FEEDBACK_URL` overrides either endpoint. The CLI can send\ntoo: `cotal feedback \"<summary>\" [--type bug]`. Each submission carries\n`origin: human | agent`, whether the tester asked, or the agent auto-reported a major\nissue.\n"
|
|
16169
16157
|
},
|
|
16170
16158
|
{
|
|
16171
16159
|
"slug": "connect-codex",
|
|
@@ -16186,7 +16174,7 @@ var DOCS_BUNDLE = {
|
|
|
16186
16174
|
"title": "Connect Jcode (beta)",
|
|
16187
16175
|
"kind": "Guide (informative)",
|
|
16188
16176
|
"summary": "Jcode joins a Cotal mesh as a lateral peer.",
|
|
16189
|
-
"body": "# Connect Jcode (beta)\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\n[Jcode](https://github.com/1jehuang/jcode) joins a Cotal mesh as a lateral peer. The connector\ncreates one private Jcode Harness API instance per seat, one Jcode session inside it, and exposes\nthe normal `cotal_*` tool surface through Jcode's documented stdio MCP configuration.\n\n**Beta** means the supported path is deliberately narrow: a fresh private session, prompt\ninjection, presence, managed start/stop, and an attached TUI work. Features that do not preserve\nthat private session's mesh surface fail loud: `--resume`, exact-session continuation,\n`--variant`, `--share-tools`, `--events`, and connector `--opt` values are not supported.\n\n## Install\n\nThe connector is seeded with the Cotal CLI. It currently supports **macOS and Linux only**:\nJcode's released Harness API bridge is a Unix-socket surface. Install Jcode 0.78.1 or later from\nits GitHub release and make the binary available as `jcode` on `PATH`:\n\n```bash\njcode version --json\ncotal spawn --agent jcode\n```\n\nIf an older Cotal installation is missing the connector, run `cotal ext seed --repair` (or\n`cotal ext add @cotal-ai/connector-jcode`). This connector intentionally uses the released\nbinary's `api-bridge` command; it does not require a Rust checkout.\n\n## Spawn it\n\n```bash\ncotal spawn --agent jcode\ncotal spawn reviewer --agent jcode -d\ncotal spawn --agent jcode --model gpt-5.6-sol --prompt \"Review the current change.\"\nCOTAL_DEFAULT_AGENT=jcode cotal spawn\n```\n\nA detached seat is managed normally: `cotal ps`, `cotal attach`, and `cotal stop` control the\nsame process the connector starts. In a terminal, Jcode opens on the managed session. With piped\noutput it stays headless; set `COTAL_JCODE_TUI=1` or `COTAL_JCODE_TUI=0` in the environment of the\nprocess building the launch to override that choice. For a detached spawn, that is the manager's\nenvironment.\n\n## How it binds\n\nJcode's stable integration surface is the **Harness API**: protocol-v1 NDJSON over a Unix socket.\nThe connector launches a **private instance** with `@1jehuang/jcode-sdk`'s `launchInstance()` and\nattaches only to that instance's own socket:\n\n- `launchInstance()` starts a private `JCODE_HOME`, runtime directory, daemon, and `api-bridge`;\n the connector holds the process handle first-hand and closes that instance with the Cotal seat.\n This gives each managed Cotal peer one owned session and prevents it from seeing or changing\n the operator's live Jcode sessions.\n- Attaching to an **operator-run** `jcode api-bridge` shares the operator's live session\n inventory. That is appropriate for a dashboard or editor integration, but not a managed Cotal\n seat: stop, prompt injection, and session selection could act on the operator's work. The\n connector never attaches to an operator bridge.\n- A managed seat **never updates its own binary**. Jcode's background updater restarts the\n process tree when it lands a release; that restart drops the seat's TUI, which is the only\n connection the Jcode server counts as a client, and nothing re-attaches, so the server's idle\n reaper takes the seat down five minutes later in the middle of a turn. The seat's version is\n whatever is on `PATH` when you spawn it, and it stays that version for the seat's life. Update\n deliberately, between seats, not under a running agent.\n\nOn a graceful stop **and** on a startup failure, the connector proves the private daemon tree is\nactually gone rather than trusting the SDK's registry-keyed stop (which is a silent no-op when the\n`servers.json` socket path does not match verbatim): it reads the PIDs the private home itself\nrecords, sends a bounded SIGTERM, escalates survivors to an exact-PID SIGKILL, and reports a\nfailed stop instead of a clean one if any recorded process survives. It never signals by name, so\nteardown can only ever reach the seat's own tree.\n\nThe private Jcode home lives under `<manager-workspace>/.cotal/jcode/`. It is unique per\nspace/name and is owner-only. Jcode's own credential inheritance is used for the private instance,\nso provider logins work without copying its transcript/config tree into the seat. The spawned\nJcode process does not inherit `COTAL_*` values or the Cotal launch-material pointer.\n\nIf a provider failure closes the private Harness API connection during a mesh-driven turn, the\nconnector leaves that turn's inbox batch unacknowledged and makes one private replacement\nconnection to the same session. The seat reports `waiting` while it reconnects, then redrives that\nunacknowledged batch only after the session attaches. A failed replacement, or a second disconnect,\nends the seat rather than silently retrying bridges without bound.\n\nJcode currently supports **stdio** MCP servers. The connector writes only its own `cotal` entry to\nthe private `JCODE_HOME/mcp.json`; it starts a stdio MCP bridge for that entry and relays its calls\nto the host's one `MeshAgent`. The Jcode/MCP child receives a per-launch relay capability, but not\nthe Cotal broker credential or its launch-material pointer. Jcode also overlays project\n`.jcode/mcp.json`, `.mcp.json`, and `.claude/mcp.json`; a managed launch **refuses** a workspace\ncontaining any of those files, because one could replace the `cotal` bridge or add tools that were\nnot explicitly shared. Operator MCP configuration is isolated in the private home and project MCP\nconfiguration is not supported yet.\n\nBefore the seat joins the mesh, the host runs a mandatory Jcode turn that calls\n`cotal_orientation`. Jcode loads MCP tools asynchronously; its first turn can use the pre-MCP tool\nsnapshot immediately before Jcode rebuilds that snapshot. The host repeats the identical proof once\nin that case. A second absence fails the launch, so a bridge that never comes up remains a loud\nfailure rather than an agent that is present but mute. A managed Jcode seat has a **three-minute\nbounded readiness window**: first boot can download model material, start the MCP bridge, and wait\nthrough the provider-backed readiness turns. If that window expires, the launch is `uncertain`, not\na failed or cleanup verdict; use `cotal attach <name>` or `cotal ps` to inspect it and do not stop\nit solely because the window elapsed. The host then waits for the mesh connection and presence bind\nto complete before it adds a no-reply notice that the bootstrap orientation predates the join and\nthat a new orientation is live context. During a broker outage, it stays waiting and sends no\nconnected notice.\n\nFor a foreground launch, the TUI opens as soon as the session is ready, before the readiness turn,\nso it streams boot activity instead of leaving the terminal blank. Presence still begins only after\nthe readiness proof passes. An inbound peer message then wakes a Harness API turn. The host marks\npresence working while the turn runs, acknowledges exactly the delivered inbox ids only after the\nSDK turn succeeds, and leaves a failed turn unacknowledged for mesh redelivery. Jcode's stable\nHarness API has no measured mid-turn steer surface here, so traffic arriving during a turn waits for\nthe next turn rather than being silently treated as an interrupt. `cotal_inbox` pulls only buffered\nquiet ambient from that host-owned queue; its shared optional `peek` argument is supported, so\n`peek: true` shows those messages without clearing them.\n\n## Models and limits\n\n`--model` is passed to Jcode's session-level Harness API model selector. Jcode validates the model\nagainst the active provider, then the connector reads runtime identity back and refuses startup if\nit is not the requested model; a seat is never allowed to join under a model label it did not\nreceive. The connector does not currently offer a Cotal model catalog because the Harness API's\n`listModels()` is session-scoped and provider-specific. If the mandatory readiness turn receives a\nprovider `invalid_request` refusal for a model id or reasoning-effort value, the launch diagnostic\nnames the provider error code (or `invalid_request` when none is supplied) and rejected value. Other provider response text remains scrubbed, so\nan external observer/UI can correct connector-visible input without exposing private harness output.\n\nThe following fail loud before a new session is provisioned where the manager can preflight them,\nor at connector launch as a backstop:\n\n- **Resume / continuation:** a Cotal seat owns a new private Jcode instance. Reusing a session from\n an operator or another seat would violate that ownership boundary.\n- **Variants:** Jcode reasoning effort is an API operation but has no Cotal variant mapping yet.\n- **Tool sharing:** Jcode resolves its MCP configuration from several global and project sources.\n The connector owns a private configuration containing only `cotal`, rather than claim a chosen\n subset can be safely merged.\n- **Events:** Jcode's Harness API does not provide the durable structured rollout surface required\n by Cotal's event plane.\n- **Launch options:** the connector does not map arbitrary flags/config into the Harness API.\n- **Containers:** the current deploy image does not bundle Jcode, so there is no containerized Jcode connector today.\n\n## Security limits\n\nThe private home protects against accidental sharing and stale session selection; it is not an\nOS-user isolation boundary. A hostile process running as the same user can still read that user's\nfiles or inspect another same-user process. Use OS/container isolation where peers must be mutually\nhostile.\n\nThe model can receive remote peer messages and Jcode is an autonomous coding harness. Treat its\nprovider credentials, filesystem access, and network capability as the privileges of the OS user\nrunning the seat. Cotal's spawn capability governs who may create a seat; it is not a sandbox for\nwhat a model can be persuaded to do after creation.\n"
|
|
16177
|
+
"body": "# Connect Jcode (beta)\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\n[Jcode](https://github.com/1jehuang/jcode) joins a Cotal mesh as a lateral peer. The connector\ncreates one private Jcode Harness API instance per seat, one Jcode session inside it, and exposes\nthe normal `cotal_*` tool surface through Jcode's documented stdio MCP configuration.\n\n**Beta** means the supported path is deliberately narrow: a fresh private session, prompt\ninjection, presence, managed start/stop, and an attached TUI work. Features that do not preserve\nthat private session's mesh surface fail loud: `--resume`, exact-session continuation,\n`--variant`, `--share-tools`, `--events`, and connector `--opt` values are not supported.\n\n## Install\n\nThe connector is seeded with the Cotal CLI. It currently supports **macOS and Linux only**:\nJcode's released Harness API bridge is a Unix-socket surface. Install Jcode 0.78.1 or later from\nits GitHub release and make the binary available as `jcode` on `PATH`:\n\n```bash\njcode version --json\ncotal spawn --agent jcode\n```\n\nIf an older Cotal installation is missing the connector, run `cotal ext seed --repair` (or\n`cotal ext add @cotal-ai/connector-jcode`). This connector intentionally uses the released\nbinary's `api-bridge` command; it does not require a Rust checkout.\n\n## Spawn it\n\n```bash\ncotal spawn --agent jcode\ncotal spawn reviewer --agent jcode -d\ncotal spawn --agent jcode --model gpt-5.6-sol --prompt \"Review the current change.\"\nCOTAL_DEFAULT_AGENT=jcode cotal spawn\n```\n\nA detached seat is managed normally: `cotal ps`, `cotal attach`, and `cotal stop` control the\nsame process the connector starts. In a terminal, Jcode opens on the managed session. With piped\noutput it stays headless; set `COTAL_JCODE_TUI=1` or `COTAL_JCODE_TUI=0` in the environment of the\nprocess building the launch to override that choice. For a detached spawn, that is the manager's\nenvironment.\n\n## How it binds\n\nJcode's stable integration surface is the **Harness API**: protocol-v1 NDJSON over a Unix socket.\nThe connector launches a **private instance** with `@1jehuang/jcode-sdk`'s `launchInstance()` and\nattaches only to that instance's own socket:\n\n- `launchInstance()` starts a private `JCODE_HOME`, runtime directory, daemon, and `api-bridge`;\n the connector holds the process handle first-hand and closes that instance with the Cotal seat.\n This gives each managed Cotal peer one owned session and prevents it from seeing or changing\n the operator's live Jcode sessions.\n- Attaching to an **operator-run** `jcode api-bridge` shares the operator's live session\n inventory. That is appropriate for a dashboard or editor integration, but not a managed Cotal\n seat: stop, prompt injection, and session selection could act on the operator's work. The\n connector never attaches to an operator bridge.\n- A managed seat **never updates its own binary**. Jcode's background updater restarts the\n process tree when it lands a release; that restart drops the seat's TUI, which is the only\n connection the Jcode server counts as a client, and nothing re-attaches, so the server's idle\n reaper takes the seat down five minutes later in the middle of a turn. The seat's version is\n whatever is on `PATH` when you spawn it, and it stays that version for the seat's life. Update\n deliberately, between seats, not under a running agent.\n\nOn a graceful stop **and** on a startup failure, the connector proves the private daemon tree is\nactually gone rather than trusting the SDK's registry-keyed stop (which is a silent no-op when the\n`servers.json` socket path does not match verbatim): it reads the PIDs the private home itself\nrecords, sends a bounded SIGTERM, escalates survivors to an exact-PID SIGKILL, and reports a\nfailed stop instead of a clean one if any recorded process survives. It never signals by name, so\nteardown can only ever reach the seat's own tree.\n\nThe private Jcode home lives under `<manager-workspace>/.cotal/jcode/`. It is unique per\nspace/name and is owner-only. Jcode's own credential inheritance is used for the private instance,\nso provider logins work without copying its transcript/config tree into the seat. The spawned\nJcode process does not inherit `COTAL_*` values or the Cotal launch-material pointer.\n\nIf a provider failure closes the private Harness API connection during a mesh-driven turn, the\nconnector leaves that turn's inbox batch unacknowledged and makes one private replacement\nconnection to the same session. The seat reports `waiting` while it reconnects, then redrives that\nunacknowledged batch only after the session attaches. A failed replacement, or a second disconnect,\nends the seat rather than silently retrying bridges without bound.\n\nJcode currently supports **stdio** MCP servers. The connector writes only its own `cotal` entry to\nthe private `JCODE_HOME/mcp.json`; it starts a stdio MCP bridge for that entry and relays its calls\nto the host's one `MeshAgent`. The Jcode/MCP child receives a per-launch relay capability, but not\nthe Cotal broker credential or its launch-material pointer. Jcode also overlays project\n`.jcode/mcp.json`, `.mcp.json`, and `.claude/mcp.json`; a managed launch **refuses** a workspace\ncontaining any of those files, because one could replace the `cotal` bridge or add tools that were\nnot explicitly shared. Operator MCP configuration is isolated in the private home and project MCP\nconfiguration is not supported yet.\n\nBefore the seat joins the mesh, the host runs a mandatory Jcode turn that calls\n`cotal_orientation`. Jcode loads MCP tools asynchronously; its first turn can use the pre-MCP tool\nsnapshot immediately before Jcode rebuilds that snapshot. The host repeats the identical proof once\nin that case. A second absence fails the launch, so a bridge that never comes up remains a loud\nfailure rather than an agent that is present but mute. A managed Jcode seat has a **three-minute\nbounded readiness window**: first boot can download model material, start the MCP bridge, and wait\nthrough the provider-backed readiness turns. If that window expires, the launch is `uncertain`, not\na failed or cleanup verdict; use `cotal attach <name>` or `cotal ps` to inspect it and do not stop\nit solely because the window elapsed. The host then waits for the mesh connection and presence bind\nto complete before it adds a no-reply notice that the bootstrap orientation predates the join and\nthat a new orientation is live context. During a broker outage, it stays waiting and sends no\nconnected notice.\n\nFor a foreground launch, the TUI opens as soon as the session is ready, before the readiness turn,\nso it streams boot activity instead of leaving the terminal blank. Presence still begins only after\nthe readiness proof passes. An inbound peer message then wakes a Harness API turn. The host marks\npresence working while the turn runs, acknowledges exactly the delivered inbox ids only after the\nSDK turn succeeds, and leaves a failed turn unacknowledged for mesh redelivery. Jcode's stable\nHarness API has no measured mid-turn steer surface here, so traffic arriving during a turn waits for\nthe next turn rather than being silently treated as an interrupt. `cotal_inbox` pulls only buffered\nquiet ambient from that host-owned queue; its shared optional `peek` argument is supported, so\n`peek: true` shows those messages without clearing them.\n\n## Models and limits\n\n`--model` is passed to Jcode's session-level Harness API model selector. Jcode validates the model\nagainst the active provider, then the connector reads runtime identity back and refuses startup if\nit is not the requested model; a seat is never allowed to join under a model label it did not\nreceive. The connector does not currently offer a Cotal model catalog because the Harness API's\n`listModels()` is session-scoped and provider-specific.\n\n`--variant` is the session's **reasoning effort**, applied after the model and before the seat's\nfirst turn \u2014 so a seat never serves a turn at an effort nobody chose. A persona's `variant:` is the\ndefault and `--variant` overrides it, the same way `model:` and `--model` work:\n\n```bash\ncotal spawn --agent jcode --model gpt-5.6-sol --variant high\n```\n\nWhich tiers exist depends on the provider **and** model. The connector does not carry a copy of\nthose ladders: it passes the requested tier to Jcode, which validates it against the active model's\nladder. A rejected tier, or a model with no reasoning-effort surface, ends the launch rather than\nquietly starting the seat at another effort. The external observer/UI receives only the requested\ntier, effective model, fixed `invalid_request` provider code, and an accepted-tier ladder when it\ncan be safely parsed; arbitrary provider rejection text stays private. Omit `--variant` to keep\nJcode's configured default.\n\nIf the mandatory readiness turn receives a provider `invalid_request` refusal for a model id or\nreasoning-effort value, the launch diagnostic names only the provider error code and rejected\nvalue. Other provider response text remains scrubbed, so an external observer/UI can correct\nconnector-visible input without exposing private harness output.\n\nThe following fail loud before a new session is provisioned where the manager can preflight them,\nor at connector launch as a backstop:\n\n- **Resume /continuation:** a Cotal seat owns a new private Jcode instance. Reusing a session from\n an operator or another seat would violate that ownership boundary.\n- **Tool sharing:** Jcode resolves its MCP configuration from several global and project sources.\n The connector owns a private configuration containing only `cotal`, rather than claim a chosen\n subset can be safely merged.\n- **Events:** Jcode's Harness API does not provide the durable structured rollout surface required\n by Cotal's event plane.\n- **Launch options:** the connector does not map arbitrary flags/config into the Harness API.\n- **Containers:** the current deploy image does not bundle Jcode, so there is no containerized Jcode connector today.\n\n## Security limits\n\nThe private home protects against accidental sharing and stale session selection; it is not an\nOS-user isolation boundary. A hostile process running as the same user can still read that user's\nfiles or inspect another same-user process. Use OS/container isolation where peers must be mutually\nhostile.\n\nThe model can receive remote peer messages and Jcode is an autonomous coding harness. Treat its\nprovider credentials, filesystem access, and network capability as the privileges of the OS user\nrunning the seat. Cotal's spawn capability governs who may create a seat; it is not a sandbox for\nwhat a model can be persuaded to do after creation.\n"
|
|
16190
16178
|
},
|
|
16191
16179
|
{
|
|
16192
16180
|
"slug": "connect-opencode",
|
|
@@ -16207,7 +16195,7 @@ var DOCS_BUNDLE = {
|
|
|
16207
16195
|
"title": "Connectors",
|
|
16208
16196
|
"kind": "Guide (informative)",
|
|
16209
16197
|
"summary": "Every connector puts a real agent session on the mesh with the same cotal tools, presence, and delivery model (MCP tools).",
|
|
16210
|
-
"body": "# Connectors\n\n> **Guide** (informative) \xB7 **For:** operators picking a harness \xB7 **Prereqs:** none\n\nEvery connector puts a real agent session on the mesh with the same `cotal_*` tools, presence,\nand delivery model ([MCP tools](mcp-tools.md)). They differ in how they bind to their harness\nand which spawn features are wired. Anything unwired **fails loud**: a flag a connector does\nnot support throws; nothing silently degrades.\n\n| | [Claude Code](connect-claude.md) | [OpenCode](connect-opencode.md) | [Codex](connect-codex.md) | [Hermes](connect-hermes.md) | [Jcode](connect-jcode.md) | [pi](connect-pi.md) |\n|---|---|---|---|---|---|---|\n| Maturity | stable | beta | beta | alpha | beta | alpha |\n| Binds via | installed plugin + MCP server | in-process plugin (native runtime) | host-mode peer driving `codex app-server` | native Python plugin, socket-bridged | host-mode peer driving Jcode Harness API | native pi extension, in-process |\n| Install | `cotal setup` | none, just `opencode` on PATH | seeded with the CLI; needs an authenticated `codex` on PATH | BYO `uv` + `hermes-agent` 0.16; Unix only | seeded with the CLI; needs `jcode` 0.78.1+ on PATH | pi 0.79.10 (one copied file for interactive/SDK) |\n| Watch the real TUI | \u2713 | \u2713 | \u2713 (attached to the mesh-driven thread) | \u2717 (headless gateway) | \u2713 (attached to the managed Jcode session) | \u2713 |\n| Inbound delivery | hook drain at turn start + idle-wake nudge | injected as a turn | wakes a turn; directed messages steer the live turn | fresh agent per message | injected as a Harness API turn | steered into the live turn |\n| Mid-turn steering | \u2717 | \u2717 | \u2713 (directed messages) | \u2014 | \u2717 | \u2713 |\n| Session resume (`--resume`) | \u2713 (forks) | \u2717 ([#154](https://github.com/Cotal-AI/Cotal/issues/154)) | \u2717 (a resumed thread has no MCP tools upstream) | \u2717 | \u2717 (private Harness API instance) | \u2717 |\n| Tool-sharing (`--share-tools`) | \u2713 (scoped opt-in) | \u2717 (inherits your servers wholesale) | \u2717 (isolated per-agent `CODEX_HOME`) | \u2717 | \u2717 (private MCP configuration) | \u2717 |\n| Models | `--model` | `--model` + catalog (`cotal models`) + `--variant` | `--model` + catalog (`cotal models`) + `--variant` (reasoning effort) | any provider, via env | `--model` | `--model` |\n| Event plane (`--events`) | \u2713 | \u2713 | \u2713 | \u2717 | \u2717 | \u2717 |\n| Containers ([deploy](deploy.md)) | \u2713 | \u2713 | \u2717 | \u2717 | \u2717 | \u2717 |\n\n**Native vs. bridged.** OpenCode and pi expose real plugin runtimes, so the connector runs\ninside the host process; pi most directly: peer messages steer the live turn instead of\nwaiting for it to end. Claude Code has no in-process plugin runtime; the connector composes\nthree sanctioned surfaces (an MCP server for tools, lifecycle hooks for presence and delivery\nat turn boundaries, and a research-preview channel that only wakes an idle session). Codex has\nno plugin runtime either and its MCP client cannot wake an idle session, so the connector runs\na host-mode peer over Codex's own app-server protocol (the one the Codex TUI runs on): real\nwake, mid-turn steer, and the `cotal_*` tools served from the host over a loopback MCP endpoint\n\u2014 which is also what keeps them working on a turn typed into the attached Codex TUI. Hermes runs a\nnative plugin inside its Python gateway, bridged to the connector over a local socket; the\ngateway model starts a fresh agent per inbound message, so there is no live turn to steer. Jcode's\nstable Harness API is a Unix-socket NDJSON bridge: the connector starts one private instance,\ncreates one session, and calls its documented stdio MCP configuration from a private `JCODE_HOME`.\n\nEach guide covers spawn forms, model selection, and the exact limits: [Claude\nCode](connect-claude.md) \xB7 [OpenCode](connect-opencode.md) \xB7 [Codex](connect-codex.md) \xB7\n[Hermes](connect-hermes.md) \xB7 [Jcode](connect-jcode.md) \xB7 [pi](connect-pi.md).\n"
|
|
16198
|
+
"body": "# Connectors\n\n> **Guide** (informative) \xB7 **For:** operators picking a harness \xB7 **Prereqs:** none\n\nEvery connector puts a real agent session on the mesh with the same `cotal_*` tools, presence,\nand delivery model ([MCP tools](mcp-tools.md)). They differ in how they bind to their harness\nand which spawn features are wired. Anything unwired **fails loud**: a flag a connector does\nnot support throws; nothing silently degrades.\n\n| | [Claude Code](connect-claude.md) | [OpenCode](connect-opencode.md) | [Codex](connect-codex.md) | [Hermes](connect-hermes.md) | [Jcode](connect-jcode.md) | [pi](connect-pi.md) |\n|---|---|---|---|---|---|---|\n| Maturity | stable | beta | beta | alpha | beta | alpha |\n| Binds via | installed plugin + MCP server | in-process plugin (native runtime) | host-mode peer driving `codex app-server` | native Python plugin, socket-bridged | host-mode peer driving Jcode Harness API | native pi extension, in-process |\n| Install | `cotal setup` | none, just `opencode` on PATH | seeded with the CLI; needs an authenticated `codex` on PATH | BYO `uv` + `hermes-agent` 0.16; Unix only | seeded with the CLI; needs `jcode` 0.78.1+ on PATH | pi 0.79.10 (one copied file for interactive/SDK) |\n| Watch the real TUI | \u2713 | \u2713 | \u2713 (attached to the mesh-driven thread) | \u2717 (headless gateway) | \u2713 (attached to the managed Jcode session) | \u2713 |\n| Inbound delivery | hook drain at turn start + idle-wake nudge | injected as a turn | wakes a turn; directed messages steer the live turn | fresh agent per message | injected as a Harness API turn | steered into the live turn |\n| Mid-turn steering | \u2717 | \u2717 | \u2713 (directed messages) | \u2014 | \u2717 | \u2713 |\n| Session resume (`--resume`) | \u2713 (forks) | \u2717 ([#154](https://github.com/Cotal-AI/Cotal/issues/154)) | \u2717 (a resumed thread has no MCP tools upstream) | \u2717 | \u2717 (private Harness API instance) | \u2717 |\n| Tool-sharing (`--share-tools`) | \u2713 (scoped opt-in) | \u2717 (inherits your servers wholesale) | \u2717 (isolated per-agent `CODEX_HOME`) | \u2717 | \u2717 (private MCP configuration) | \u2717 |\n| Models | `--model` | `--model` + catalog (`cotal models`) + `--variant` | `--model` + catalog (`cotal models`) + `--variant` (reasoning effort) | any provider, via env | `--model` + `--variant` (reasoning effort) | `--model` |\n| Event plane (`--events`) | \u2713 | \u2713 | \u2713 | \u2717 | \u2717 | \u2717 |\n| Containers ([deploy](deploy.md)) | \u2713 | \u2713 | \u2717 | \u2717 | \u2717 | \u2717 |\n\n**Native vs. bridged.** OpenCode and pi expose real plugin runtimes, so the connector runs\ninside the host process; pi most directly: peer messages steer the live turn instead of\nwaiting for it to end. Claude Code has no in-process plugin runtime; the connector composes\nthree sanctioned surfaces (an MCP server for tools, lifecycle hooks for presence and delivery\nat turn boundaries, and a research-preview channel that only wakes an idle session). Codex has\nno plugin runtime either and its MCP client cannot wake an idle session, so the connector runs\na host-mode peer over Codex's own app-server protocol (the one the Codex TUI runs on): real\nwake, mid-turn steer, and the `cotal_*` tools served from the host over a loopback MCP endpoint\n\u2014 which is also what keeps them working on a turn typed into the attached Codex TUI. Hermes runs a\nnative plugin inside its Python gateway, bridged to the connector over a local socket; the\ngateway model starts a fresh agent per inbound message, so there is no live turn to steer. Jcode's\nstable Harness API is a Unix-socket NDJSON bridge: the connector starts one private instance,\ncreates one session, and calls its documented stdio MCP configuration from a private `JCODE_HOME`.\n\nEach guide covers spawn forms, model selection, and the exact limits: [Claude\nCode](connect-claude.md) \xB7 [OpenCode](connect-opencode.md) \xB7 [Codex](connect-codex.md) \xB7\n[Hermes](connect-hermes.md) \xB7 [Jcode](connect-jcode.md) \xB7 [pi](connect-pi.md).\n"
|
|
16211
16199
|
},
|
|
16212
16200
|
{
|
|
16213
16201
|
"slug": "control-surface",
|
|
@@ -16235,7 +16223,7 @@ var DOCS_BUNDLE = {
|
|
|
16235
16223
|
"title": "Deploy: agent teams against an external broker",
|
|
16236
16224
|
"kind": "Guide (informative)",
|
|
16237
16225
|
"summary": "The deploy/ tree runs a team of agents in an isolated container that dials out to an existing Cotal broker.",
|
|
16238
|
-
"body": "# Deploy: agent teams against an external broker\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\nThe `deploy/` tree runs a team of agents in an isolated container that dials **out** to an\nexisting Cotal broker. The container gets no host file access; only NATS traffic crosses the wall.\nOne image, configured entirely by env and mounts; add or reshape a team by editing the roster and\nagent files, never the image.\n\n`deploy/README.md` is the full walkthrough (a local quickstart plus production notes). This page\nis the map: what the tree provides, what you need, and how creds flow.\n\n## What the deploy tree provides\n\n| file | what it is |\n|---|---|\n| `deploy/docker/Dockerfile` | Builds one image (`cotal-runner`) bundling the `cotal`, `claude`, and `opencode` CLIs, installing the mesh plugin (`cotal setup`), and pre-completing Claude's first-run onboarding for unattended use. |\n| `deploy/docker/entrypoint.sh` | Waits for the broker to be reachable, then runs `cotal <cmd> --server $COTAL_SERVERS`. |\n| `deploy/docker/compose.yaml` | Two example services: `team-a` (a manager + roster) and `solo` (one agent). |\n| `deploy/docker/roster.example.yaml` | A roster template to copy. |\n\n**What it does *not* provide:** the broker (external: you point at it), and, per the README,\nhost-side per-agent cred provisioning and stronger sandbox isolation are called out as *later*\nhardening; they are **not built yet**. What ships today is the phase-1 container boundary\ndescribed under [Isolation](#isolation).\n\nThe image supports **Claude Code and OpenCode** agents only; it does not bundle `uv`/`hermes-agent`,\nso [Hermes](connect-hermes.md) cannot run in a container today.\n\n## Two shapes\n\nThe container's command picks the shape:\n\n| command | shape |\n|---|---|\n| `supervise --space <s> --roster /workspace/roster.yaml` | a manager that boots every agent in the roster (all in one container, pty runtime) |\n| `spawn <name>` | one foreground agent, loading `.cotal/agents/<name>.md` |\n\nMix connector types freely within a roster (`agent: claude` / `agent: opencode` per entry). See\n[Define a team](define-a-team.md) for the roster and persona files.\n\n## Prerequisites\n\n- **Docker.**\n- **An external broker**, reachable from the container. `cotal up` binds loopback by default; a\n broker containers dial out to needs `cotal up --host 0.0.0.0` (and auth, the default). Point\n `COTAL_SERVERS` at it: `nats://host.docker.internal:4222` for a broker on your machine, or\n `tls://broker.host:4222` for a hosted one. The deploy tree never runs the broker. The broker\n must be nats-server 2.12 or newer (the v0.4 control surface floor); agents fail loud at connect\n against an older one.\n- **The account signer:** on the host beside your broker, `cotal mint --signer` writes\n `signer.json`: account signing material with no operator key.\n- **A model credential per connector type** (see below).\n\n## Steps\n\nBuild once, from the repo root:\n\n```bash\ndocker build -f deploy/docker/Dockerfile -t cotal-runner .\n```\n\nThen run a team. With compose, paths are relative to `docker/`, so put `signer.json`,\n`team-a/roster.yaml`, and `team-a/agents/*.md` there:\n\n```bash\ncp deploy/docker/roster.example.yaml deploy/docker/team-a/roster.yaml # then edit; add agents + signer.json\nCOTAL_SERVERS=tls://broker.host:4222 \\\nCLAUDE_CODE_OAUTH_TOKEN=<token> OPENCODE_API_KEY=<key> \\\n docker compose -f deploy/docker/compose.yaml up team-a\n```\n\nThe README's quickstart shows the equivalent single `docker run` (with the mounts spelled out) and\na local-broker variant. Watch the team join with `cotal console --plain --space <s>`.\n\n## How creds and auth flow\n\nTwo independent credentials, both set from **outside** the container:\n\n**Broker auth (the NATS mesh).** Mount the stripped `signer.json` read-only at\n`/workspace/.cotal/auth/auth.json`. Inside the container, each agent's own scoped creds are minted\nfrom it into a tmpfs (`/workspace/.cotal/auth/creds`, RAM only). The operator root-of-trust never\nenters a container, so a leaked signer cannot escalate beyond its one NATS account. Inside that\naccount, though, it is full compromise: it can mint `admin` (DM read) and destructive profiles, not\njust ordinary users. The account boundary contains cross-tenant escalation, not damage within the\ntenant. See [Identity and auth](identity-and-auth.md).\n\n**Model auth (the LLM provider).** Set each connector's credential as an env var; the supervisor\nforwards the named vars and each CLI
|
|
16226
|
+
"body": "# Deploy: agent teams against an external broker\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\nThe `deploy/` tree runs a team of agents in an isolated container that dials **out** to an\nexisting Cotal broker. The container gets no host file access; only NATS traffic crosses the wall.\nOne image, configured entirely by env and mounts; add or reshape a team by editing the roster and\nagent files, never the image.\n\n`deploy/README.md` is the full walkthrough (a local quickstart plus production notes). This page\nis the map: what the tree provides, what you need, and how creds flow.\n\n## What the deploy tree provides\n\n| file | what it is |\n|---|---|\n| `deploy/docker/Dockerfile` | Builds one image (`cotal-runner`) bundling the `cotal`, `claude`, and `opencode` CLIs, installing the mesh plugin (`cotal setup`), and pre-completing Claude's first-run onboarding for unattended use. |\n| `deploy/docker/entrypoint.sh` | Waits for the broker to be reachable, then runs `cotal <cmd> --server $COTAL_SERVERS`. |\n| `deploy/docker/compose.yaml` | Two example services: `team-a` (a manager + roster) and `solo` (one agent). |\n| `deploy/docker/roster.example.yaml` | A roster template to copy. |\n\n**What it does *not* provide:** the broker (external: you point at it), and, per the README,\nhost-side per-agent cred provisioning and stronger sandbox isolation are called out as *later*\nhardening; they are **not built yet**. What ships today is the phase-1 container boundary\ndescribed under [Isolation](#isolation).\n\nThe image supports **Claude Code and OpenCode** agents only; it does not bundle `uv`/`hermes-agent`,\nso [Hermes](connect-hermes.md) cannot run in a container today.\n\n## Two shapes\n\nThe container's command picks the shape:\n\n| command | shape |\n|---|---|\n| `supervise --space <s> --roster /workspace/roster.yaml` | a manager that boots every agent in the roster (all in one container, pty runtime) |\n| `spawn <name>` | one foreground agent, loading `.cotal/agents/<name>.md` |\n\nMix connector types freely within a roster (`agent: claude` / `agent: opencode` per entry). See\n[Define a team](define-a-team.md) for the roster and persona files.\n\n## Prerequisites\n\n- **Docker.**\n- **An external broker**, reachable from the container. `cotal up` binds loopback by default; a\n broker containers dial out to needs `cotal up --host 0.0.0.0` (and auth, the default). Point\n `COTAL_SERVERS` at it: `nats://host.docker.internal:4222` for a broker on your machine, or\n `tls://broker.host:4222` for a hosted one. The deploy tree never runs the broker. The broker\n must be nats-server 2.12 or newer (the v0.4 control surface floor); agents fail loud at connect\n against an older one.\n- **The account signer:** on the host beside your broker, `cotal mint --signer` writes\n `signer.json`: account signing material with no operator key.\n- **A model credential per connector type** (see below).\n\n## Steps\n\nBuild once, from the repo root:\n\n```bash\ndocker build -f deploy/docker/Dockerfile -t cotal-runner .\n```\n\nThen run a team. With compose, paths are relative to `docker/`, so put `signer.json`,\n`team-a/roster.yaml`, and `team-a/agents/*.md` there:\n\n```bash\ncp deploy/docker/roster.example.yaml deploy/docker/team-a/roster.yaml # then edit; add agents + signer.json\nCOTAL_SERVERS=tls://broker.host:4222 \\\nCLAUDE_CODE_OAUTH_TOKEN=<token> OPENCODE_API_KEY=<key> \\\n docker compose -f deploy/docker/compose.yaml up team-a\n```\n\nThe README's quickstart shows the equivalent single `docker run` (with the mounts spelled out) and\na local-broker variant. Watch the team join with `cotal console --plain --space <s>`.\n\n## How creds and auth flow\n\nTwo independent credentials, both set from **outside** the container:\n\n**Broker auth (the NATS mesh).** Mount the stripped `signer.json` read-only at\n`/workspace/.cotal/auth/auth.json`. Inside the container, each agent's own scoped creds are minted\nfrom it into a tmpfs (`/workspace/.cotal/auth/creds`, RAM only). The operator root-of-trust never\nenters a container, so a leaked signer cannot escalate beyond its one NATS account. Inside that\naccount, though, it is full compromise: it can mint `admin` (DM read) and destructive profiles, not\njust ordinary users. The account boundary contains cross-tenant escalation, not damage within the\ntenant. See [Identity and auth](identity-and-auth.md).\n\n**Model auth (the LLM provider).** Set each connector's credential as an env var; the supervisor\nforwards the named vars the connector declares (not the manager's whole environment) and each CLI\nreads only the ones it understands. A Claude seat therefore receives `CLAUDE_CODE_OAUTH_TOKEN`\nbecause the Claude connector lists it, not because every `CLAUDE_CODE_*` name is inherited:\n\n| connector | env | notes |\n|---|---|---|\n| `claude` | `CLAUDE_CODE_OAUTH_TOKEN` | from `claude setup-token` on your host; runs on your Claude Pro/Max subscription, same as local |\n| `opencode` | the env var of the provider behind each agent's `model:` | per provider: `OPENCODE_API_KEY` for OpenCode's hosted models, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc. |\n\nEvery var set on a team container reaches every agent in it; **the container is the team's trust\nboundary**, so secrets are not isolated *between* agents in the same container. For hard per-agent\nisolation, run one agent per container (the `solo` service: same image, `spawn <name>`).\n\n## Container layout\n\n`/workspace` is the working directory:\n\n| path | mode | holds |\n|---|---|---|\n| `/workspace/.cotal/auth/auth.json` | ro mount | the stripped signer |\n| `/workspace/.cotal/agents/*.md` | ro mount | personas |\n| `/workspace/roster.yaml` | ro mount | the roster (supervise mode) |\n| `/workspace/.cotal/auth/creds/` | tmpfs (`mode=01777`) | minted per-agent creds, RAM only |\n\n## Isolation\n\nPhase 1 is a non-root user (uid 10001), `cap_drop: ALL`, no host mounts beyond the read-only ones\nabove, and an ephemeral writable fs. Egress is the broker plus each agent's model API. Stronger\nisolation (a fully read-only rootfs, or gVisor / Kata via `--runtime`) is a later swap with no app\nchange.\n\n## See also\n\n- [Define a team](define-a-team.md): roster and persona files\n- [Identity and auth](identity-and-auth.md): the signer, minting, and account scoping\n- [Connect Claude Code](connect-claude.md) \xB7 [Connect OpenCode](connect-opencode.md)\n"
|
|
16239
16227
|
},
|
|
16240
16228
|
{
|
|
16241
16229
|
"slug": "embedding",
|
|
@@ -16305,7 +16293,7 @@ var DOCS_BUNDLE = {
|
|
|
16305
16293
|
"title": "Security model",
|
|
16306
16294
|
"kind": "Concept (informative threat model)",
|
|
16307
16295
|
"summary": "Cotal v0 provides containment and sender authenticity for peers sharing one trusted NATS broker.",
|
|
16308
|
-
"body": "# Security model\n\n> **Concept** (informative threat model) \xB7 **For:** operators and security reviewers \xB7 **Normative:** [SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization). This page is the threat model SPEC \xA79 references; where the two disagree, the spec wins.\n\nCotal v0 provides containment and sender authenticity for peers sharing one trusted NATS\nbroker. It is not an end-to-end encrypted or untrusted-relay protocol. The enforcement\nmechanics (profiles, ACLs, consumer confinement) are defined in\n[SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization) and\n[Appendix B](../SPEC.md#appendix-b-profile-acls), explained informally in\n[identity & auth](identity-and-auth.md); this page covers **who the adversaries are and\nwhat is (not) defended**.\n\n## Trust boundary\n\n- One Cotal space maps to one NATS account.\n- The broker, operator, account signing key holder, and any `admin` credential are trusted.\n- On a per-user-auth mesh, ledger scope `admin` is the same trust grade as an `admin`\n credential: it unlocks the elevated views (the whole-space read tap, history and channel\n purges, channel-registry writes, cross-owner control), so grant it as operator authority,\n not as a convenience ([identity & auth](identity-and-auth.md)).\n- Agents are not trusted to self-report sender identity, channel permissions, or DM access.\n\n## Adversaries\n\nEach adversary, what it can attempt, and what stops it (or why it is out of scope).\n\n- **Compromised or malicious peer agent** (authenticated, in-space): the primary adversary.\n It cannot forge another agent's `from.id` (the subject sender, an `owner.actor` principal,\n is pinned to its connection by NATS permissions; not another owner, and not a sibling actor\n under its own owner), cannot publish to channels outside its declared allow-list, and cannot read\n another agent's DMs or another role's work queue ([SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization)).\n It still can send well-formed hostile content to channels it is allowed on\n (see *Prompt-facing data*) and flood within its limits (see *availability* under *What v0\n does not protect*). These are **broker-enforced** guarantees and assume the peer has no host\n filesystem or process access to the account signer: the default single-host manager and container\n compositions do not isolate the signer from a same-uid agent, which could then mint `admin` and\n read any DM. Isolating it is a hosted-composition concern (see [Embedding Cotal](embedding.md) and\n [Deploy](deploy.md)).\n- **Buggy or lazy receiver:** sender authenticity depends on the receiver enforcing the\n `from.id`-equals-subject-sender check; a client that skips it accepts spoofed senders. The\n check is therefore normative: receivers MUST reject on mismatch\n ([SPEC \xA75](../SPEC.md#5-envelopes), [\xA712](../SPEC.md#12-conformance)).\n- **On-path network attacker** (between an agent and the broker): defeated only when the join\n link uses `cotals://` (TLS **required** \u2014 client refuses if the broker is not TLS). Plain\n `cotal://` does **not** require TLS: a NATS client may still auto-upgrade against an honest\n TLS broker, but a forged plaintext `INFO` can strip the upgrade and harvest credentials. Use\n plain `cotal://` only on trusted networks and in dev.\n- **Content author targeting a reading model:** any writer of channel `description` /\n `instructions`, presence `activity`, message bodies, or free-form metadata can attempt\n prompt injection against an agent that reads it. See *Prompt-facing data*.\n- **Untrusted broker, relay, operator, or admin:** out of scope by definition. The broker and\n any `admin` credential can read, drop, replay, or alter all plaintext traffic. v0 makes no\n claim against a hostile broker; signed envelopes and untrusted-relay bindings are reserved\n for a later version ([roadmap](roadmap.md)).\n\n## What v0 protects\n\nThe guarantees, at a glance, each enforced by the broker per\n[SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization):\n\n- **Sender authenticity**: the sender id is encoded in the subject and enforced by NATS\n permissions; receivers reject payloads whose `from.id` mismatches.\n- **Space containment**: account boundaries isolate one space's subjects, streams, and KV\n buckets from another.\n- **Channel publish scope**: posting only as self, only to declared `allowPublish`\n channels (default-deny).\n- **Channel read scope**, reads bounded to the `allowSubscribe` ACL: live joins are\n broker-refused outside it, and history reads ride server-pinned single-channel consumers.\n - **Known metadata leak (not content):** agents hold `STREAM.INFO` on the chat stream, so\n a `subjects_filter` query can enumerate retained chat *subjects* (channel names, sender\n ids, per-subject counts) including channels outside `allowSubscribe`. This is metadata,\n never message content, and channel *names* are already public via the registry. Hiding\n even the existence/volume of other channels requires the per-channel-stream model and is\n deferred strict-containment work ([roadmap](roadmap.md)).\n- **DM / task peer confidentiality**: per-identity inbox prefixes plus\n provisioner-created bind-only consumers, so an agent cannot read someone else's inbox or\n steal another role's work; durable-channel backstop reads are re-authorized by a trusted\n reader ([delivery daemon](delivery-daemon.md)).\n- **Transport secrecy (optional)**: `cotals://` enforces TLS for the hop to the broker.\n It protects that hop, not the broker itself.\n\n## What v0 does not protect\n\n- **Untrusted broker or relay:** the broker can read, drop, replay, or alter plaintext\n traffic. Signed envelopes are reserved for a later version.\n- **End-to-end secrecy:** DMs are plaintext to the broker and to `admin`. Cotal v0\n deliberately does not add end-to-end encryption, trading secrecy for a single trusted broker.\n- **Non-repudiation:** sender authenticity is broker-enforced, not portable proof. (A2A signs\n every message for this; here it is reserved as signed envelopes.)\n- **Availability:** an authenticated peer can flood any channel or inbox it may write to. v0\n relies on coarse NATS account limits (connections, subscriptions, payload and storage caps)\n and adds no per-agent application-level rate limiting.\n- **Replay by a peer:** a peer may re-send its own prior messages; v0 defines no protocol-level\n nonce or idempotency key. It cannot replay as another agent (subject binding still holds).\n- **Static agent credential revocation:** on a static-auth mesh, a *manager-spawned* agent cred\n is now bounded (24h TTL, renewed by the manager for live agents only) and lifecycle-registered:\n despawn drives the full \xA713.1 retirement \u2014 its ledger rows are revoked and the manager's\n control surface refuses the retired incarnation's credential outright. What remains: within\n the TTL window a *copied* cred keeps its inline data-plane grants (static has no auth callout,\n so nothing re-checks at reconnect), and an out-of-band `cotal mint` cred is still long-lived\n until key rotation. A per-user-auth mesh closes both: short-lived bearers, ledger revocation\n that bites at the next connect, and live-connection eviction\n ([identity & auth](identity-and-auth.md)). A copied signing *seed* still stays valid until\n rotation on either kind of mesh.\n- **The operator's own environment, in a spawned agent:** a managed spawn hands the child the\n operator's environment, on the reasoning that a harness they installed should behave the way it\n does in their shell, and that the alternative was Cotal maintaining a list of inference vendors.\n So an agent can read whatever sits in the shell the mesh was started from. This is a smaller change\n than it sounds: `HOME` and the config dirs were always forwarded, so an agent with a shell could\n already read `~/.aws`, `~/.ssh` and `~/.cotal` off disk, and the model key is in its process by\n necessity. It matters for secrets that exist **only** in the environment, such as an\n `aws-vault exec` or `op run` shell. `spawn.env` in the [config file](config.md) restores an\n allow-list for operators who need it; real containment is a workspace sandbox or a VM. What is\n **not** optional is the reset of Cotal's own `COTAL_*` namespace, which stops one agent's\n credential path, ACL or lifecycle uid from reaching another.\n- **Manager compromise:** the operator side is split into narrow, single-purpose profiles (there\n is **no allow-all cred**); the long-lived **supervisor** serves control and touches\n presence/its lease but cannot read a DM, create a consumer, or delete a stream; the destructive\n verbs (`STREAM.DELETE`/`PURGE`, cross-agent stop, per-agent provisioning) ride ephemeral\n per-command creds (teardown / control-caller-admin / deployer / provisioner). What stays hot on\n a static-auth mesh is the account **signing key** on the mint/manager box (a compromise there\n can still mint fresh creds); on a per-user-auth mesh it is held by the auth service (the callout\n stage) and by any running manager, which self-mints its supervisor cred and renewals from it\n ([identity & auth](identity-and-auth.md)).\n- **A static mesh's spawn credential is the ACL tier:** a caller that may spawn may also name the\n child's channel ACL, and on a static-auth mesh nothing attenuates that against the caller's own\n grant, because there is no ledger to attenuate against. This is the same class as the entry above\n and is not specific to any channel: the read set a spawn-capable static caller may hand its child\n covers ordinary channels, and `events.*` alongside them. A per-user-auth mesh does attenuate it:\n every delegation must sit inside the spawner's own grant, checked by NATS-pattern containment\n along the whole chain, at the grant write and again at every bearer exchange\n ([identity & auth](identity-and-auth.md)). Grant `spawn` on a static mesh as ACL authority, not\n as a narrow \"add a teammate\" permission.\n- **`spawn` is host-launch authority:** launch options are a raw passthrough (no allow/deny\n list), so a persona holding `capabilities: [spawn]` can drive the connector's full launch\n surface on the manager host (Claude `--mcp-config`, `--add-dir`, permission flags; OpenCode\n agent-config keys). The boundary is *who* may spawn (the authenticated caller, gated by the\n capability), not *which* flags they pass. Grant `spawn` as host-launch authority, not a narrow\n \"add a teammate\" permission ([run a mesh](run-a-mesh.md#spawning-agents)).\n\n## Prompt-facing data\n\nChannel `description` and `instructions`, presence `activity`, message bodies, and free-form\nmetadata may reach models. Writers that can set channel registry text are privileged, and\nregistry text is length-bounded, but clients MUST still render all of it as attributed,\nadvisory data, never as trusted system instruction. This is the indirect-prompt-injection\nsurface common to agent protocols (MCP tool descriptions, A2A agent cards): Cotal's position is\nthat the reading client, not the wire, is the trust boundary for model-facing text.\n\n## Reporting\n\nReport a suspected vulnerability privately to the maintainers rather than in a public issue.\n"
|
|
16296
|
+
"body": "# Security model\n\n> **Concept** (informative threat model) \xB7 **For:** operators and security reviewers \xB7 **Normative:** [SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization). This page is the threat model SPEC \xA79 references; where the two disagree, the spec wins.\n\nCotal v0 provides containment and sender authenticity for peers sharing one trusted NATS\nbroker. It is not an end-to-end encrypted or untrusted-relay protocol. The enforcement\nmechanics (profiles, ACLs, consumer confinement) are defined in\n[SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization) and\n[Appendix B](../SPEC.md#appendix-b-profile-acls), explained informally in\n[identity & auth](identity-and-auth.md); this page covers **who the adversaries are and\nwhat is (not) defended**.\n\n## Trust boundary\n\n- One Cotal space maps to one NATS account.\n- The broker, operator, account signing key holder, and any `admin` credential are trusted.\n- On a per-user-auth mesh, ledger scope `admin` is the same trust grade as an `admin`\n credential: it unlocks the elevated views (the whole-space read tap, history and channel\n purges, channel-registry writes, cross-owner control), so grant it as operator authority,\n not as a convenience ([identity & auth](identity-and-auth.md)).\n- Agents are not trusted to self-report sender identity, channel permissions, or DM access.\n\n## Adversaries\n\nEach adversary, what it can attempt, and what stops it (or why it is out of scope).\n\n- **Compromised or malicious peer agent** (authenticated, in-space): the primary adversary.\n It cannot forge another agent's `from.id` (the subject sender, an `owner.actor` principal,\n is pinned to its connection by NATS permissions; not another owner, and not a sibling actor\n under its own owner), cannot publish to channels outside its declared allow-list, and cannot read\n another agent's DMs or another role's work queue ([SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization)).\n It still can send well-formed hostile content to channels it is allowed on\n (see *Prompt-facing data*) and flood within its limits (see *availability* under *What v0\n does not protect*). These are **broker-enforced** guarantees and assume the peer has no host\n filesystem or process access to the account signer: the default single-host manager and container\n compositions do not isolate the signer from a same-uid agent, which could then mint `admin` and\n read any DM. Isolating it is a hosted-composition concern (see [Embedding Cotal](embedding.md) and\n [Deploy](deploy.md)).\n- **Buggy or lazy receiver:** sender authenticity depends on the receiver enforcing the\n `from.id`-equals-subject-sender check; a client that skips it accepts spoofed senders. The\n check is therefore normative: receivers MUST reject on mismatch\n ([SPEC \xA75](../SPEC.md#5-envelopes), [\xA712](../SPEC.md#12-conformance)).\n- **On-path network attacker** (between an agent and the broker): defeated only when the join\n link uses `cotals://` (TLS **required** \u2014 client refuses if the broker is not TLS). Plain\n `cotal://` does **not** require TLS: a NATS client may still auto-upgrade against an honest\n TLS broker, but a forged plaintext `INFO` can strip the upgrade and harvest credentials. Use\n plain `cotal://` only on trusted networks and in dev.\n- **Content author targeting a reading model:** any writer of channel `description` /\n `instructions`, presence `activity`, message bodies, or free-form metadata can attempt\n prompt injection against an agent that reads it. See *Prompt-facing data*.\n- **Untrusted broker, relay, operator, or admin:** out of scope by definition. The broker and\n any `admin` credential can read, drop, replay, or alter all plaintext traffic. v0 makes no\n claim against a hostile broker; signed envelopes and untrusted-relay bindings are reserved\n for a later version ([roadmap](roadmap.md)).\n\n## What v0 protects\n\nThe guarantees, at a glance, each enforced by the broker per\n[SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization):\n\n- **Sender authenticity**: the sender id is encoded in the subject and enforced by NATS\n permissions; receivers reject payloads whose `from.id` mismatches.\n- **Space containment**: account boundaries isolate one space's subjects, streams, and KV\n buckets from another.\n- **Channel publish scope**: posting only as self, only to declared `allowPublish`\n channels (default-deny).\n- **Channel read scope**, reads bounded to the `allowSubscribe` ACL: live joins are\n broker-refused outside it, and history reads ride server-pinned single-channel consumers.\n - **Known metadata leak (not content):** agents hold `STREAM.INFO` on the chat stream, so\n a `subjects_filter` query can enumerate retained chat *subjects* (channel names, sender\n ids, per-subject counts) including channels outside `allowSubscribe`. This is metadata,\n never message content, and channel *names* are already public via the registry. Hiding\n even the existence/volume of other channels requires the per-channel-stream model and is\n deferred strict-containment work ([roadmap](roadmap.md)).\n- **DM / task peer confidentiality**: per-identity inbox prefixes plus\n provisioner-created bind-only consumers, so an agent cannot read someone else's inbox or\n steal another role's work; durable-channel backstop reads are re-authorized by a trusted\n reader ([delivery daemon](delivery-daemon.md)).\n- **Transport secrecy (optional)**: `cotals://` enforces TLS for the hop to the broker.\n It protects that hop, not the broker itself.\n\n## What v0 does not protect\n\n- **Untrusted broker or relay:** the broker can read, drop, replay, or alter plaintext\n traffic. Signed envelopes are reserved for a later version.\n- **End-to-end secrecy:** DMs are plaintext to the broker and to `admin`. Cotal v0\n deliberately does not add end-to-end encryption, trading secrecy for a single trusted broker.\n- **Non-repudiation:** sender authenticity is broker-enforced, not portable proof. (A2A signs\n every message for this; here it is reserved as signed envelopes.)\n- **Availability:** an authenticated peer can flood any channel or inbox it may write to. v0\n relies on coarse NATS account limits (connections, subscriptions, payload and storage caps)\n and adds no per-agent application-level rate limiting.\n- **Replay by a peer:** a peer may re-send its own prior messages; v0 defines no protocol-level\n nonce or idempotency key. It cannot replay as another agent (subject binding still holds).\n- **Static agent credential revocation:** on a static-auth mesh, a *manager-spawned* agent cred\n is now bounded (24h TTL, renewed by the manager for live agents only) and lifecycle-registered:\n despawn drives the full \xA713.1 retirement \u2014 its ledger rows are revoked and the manager's\n control surface refuses the retired incarnation's credential outright. What remains: within\n the TTL window a *copied* cred keeps its inline data-plane grants (static has no auth callout,\n so nothing re-checks at reconnect), and an out-of-band `cotal mint` cred is still long-lived\n until key rotation. A per-user-auth mesh closes both: short-lived bearers, ledger revocation\n that bites at the next connect, and live-connection eviction\n ([identity & auth](identity-and-auth.md)). A copied signing *seed* still stays valid until\n rotation on either kind of mesh.\n- **Operator environment capability in a spawned agent:** a managed spawn receives a fixed OS\n execution allow-list (PATH included, so connector binaries under `~/.local/bin` still resolve),\n the machine-wide `COTAL_*` operator knobs, connector-declared provider inputs, shared-MCP\n references, and only names explicitly added through `spawn.env` in the [config file](config.md).\n It does not receive ambient host-session markers (`CLAUDE_CODE_CHILD_SESSION`, `CLAUDECODE`,\n `CLAUDE_CODE_ENTRYPOINT`, and the analogous names other hosts use to mark a nested session),\n temporary credentials, source-control tokens, or unrelated service secrets unless a persona or\n operator names them. Connector-declared auth vars still cross: a Claude seat receives\n `CLAUDE_CODE_OAUTH_TOKEN` (and the rest of that connector's documented credential set) so a\n container with no Keychain can authenticate, which is the forwarding `docs/deploy.md` promises.\n This boundary does not confine files accessible through HOME or other supplied filesystem roots.\n Use a sandbox or VM when filesystem containment is required.\n- **Manager compromise:** the operator side is split into narrow, single-purpose profiles (there\n is **no allow-all cred**); the long-lived **supervisor** serves control and touches\n presence/its lease but cannot read a DM, create a consumer, or delete a stream; the destructive\n verbs (`STREAM.DELETE`/`PURGE`, cross-agent stop, per-agent provisioning) ride ephemeral\n per-command creds (teardown / control-caller-admin / deployer / provisioner). What stays hot on\n a static-auth mesh is the account **signing key** on the mint/manager box (a compromise there\n can still mint fresh creds); on a per-user-auth mesh it is held by the auth service (the callout\n stage) and by any running manager, which self-mints its supervisor cred and renewals from it\n ([identity & auth](identity-and-auth.md)).\n- **A static mesh's spawn credential is the ACL tier:** a caller that may spawn may also name the\n child's channel ACL, and on a static-auth mesh nothing attenuates that against the caller's own\n grant, because there is no ledger to attenuate against. This is the same class as the entry above\n and is not specific to any channel: the read set a spawn-capable static caller may hand its child\n covers ordinary channels, and `events.*` alongside them. A per-user-auth mesh does attenuate it:\n every delegation must sit inside the spawner's own grant, checked by NATS-pattern containment\n along the whole chain, at the grant write and again at every bearer exchange\n ([identity & auth](identity-and-auth.md)). Grant `spawn` on a static mesh as ACL authority, not\n as a narrow \"add a teammate\" permission.\n- **`spawn` is host-launch authority:** launch options are a raw passthrough (no allow/deny\n list), so a persona holding `capabilities: [spawn]` can drive the connector's full launch\n surface on the manager host (Claude `--mcp-config`, `--add-dir`, permission flags; OpenCode\n agent-config keys). The boundary is *who* may spawn (the authenticated caller, gated by the\n capability), not *which* flags they pass. Grant `spawn` as host-launch authority, not a narrow\n \"add a teammate\" permission ([run a mesh](run-a-mesh.md#spawning-agents)).\n\n## Prompt-facing data\n\nChannel `description` and `instructions`, presence `activity`, message bodies, and free-form\nmetadata may reach models. Writers that can set channel registry text are privileged, and\nregistry text is length-bounded, but clients MUST still render all of it as attributed,\nadvisory data, never as trusted system instruction. This is the indirect-prompt-injection\nsurface common to agent protocols (MCP tool descriptions, A2A agent cards): Cotal's position is\nthat the reading client, not the wire, is the trust boundary for model-facing text.\n\n## Reporting\n\nReport a suspected vulnerability privately to the maintainers rather than in a public issue.\n"
|
|
16309
16297
|
},
|
|
16310
16298
|
{
|
|
16311
16299
|
"slug": "setup-internals",
|
|
@@ -17487,6 +17475,37 @@ function startControlServer(agent, endpoint, handle, opts = {}) {
|
|
|
17487
17475
|
var STANDALONE = fileURLToPath(
|
|
17488
17476
|
import.meta.url.includes("/dist/") ? new URL("./standalone.js", import.meta.url) : new URL("../dist/standalone.js", import.meta.url)
|
|
17489
17477
|
);
|
|
17478
|
+
var PI_PROVIDER_KEYS = [
|
|
17479
|
+
"ANTHROPIC_OAUTH_TOKEN",
|
|
17480
|
+
"ANTHROPIC_API_KEY",
|
|
17481
|
+
"OPENAI_API_KEY",
|
|
17482
|
+
"OPENROUTER_API_KEY",
|
|
17483
|
+
"OPENCODE_API_KEY",
|
|
17484
|
+
"GEMINI_API_KEY",
|
|
17485
|
+
"GOOGLE_CLOUD_API_KEY",
|
|
17486
|
+
"GROQ_API_KEY",
|
|
17487
|
+
"CEREBRAS_API_KEY",
|
|
17488
|
+
"DEEPSEEK_API_KEY",
|
|
17489
|
+
"MISTRAL_API_KEY",
|
|
17490
|
+
"XAI_API_KEY",
|
|
17491
|
+
"ZAI_API_KEY",
|
|
17492
|
+
"ZAI_CODING_CN_API_KEY",
|
|
17493
|
+
"MINIMAX_API_KEY",
|
|
17494
|
+
"MINIMAX_CN_API_KEY",
|
|
17495
|
+
"MOONSHOT_API_KEY",
|
|
17496
|
+
"FIREWORKS_API_KEY",
|
|
17497
|
+
"TOGETHER_API_KEY",
|
|
17498
|
+
"NVIDIA_API_KEY",
|
|
17499
|
+
"KIMI_API_KEY",
|
|
17500
|
+
"HF_TOKEN",
|
|
17501
|
+
"COPILOT_GITHUB_TOKEN",
|
|
17502
|
+
"AI_GATEWAY_API_KEY",
|
|
17503
|
+
"CLOUDFLARE_API_KEY",
|
|
17504
|
+
"XIAOMI_API_KEY",
|
|
17505
|
+
"XIAOMI_TOKEN_PLAN_CN_API_KEY",
|
|
17506
|
+
"XIAOMI_TOKEN_PLAN_AMS_API_KEY",
|
|
17507
|
+
"XIAOMI_TOKEN_PLAN_SGP_API_KEY"
|
|
17508
|
+
];
|
|
17490
17509
|
var piConnector = {
|
|
17491
17510
|
kind: "connector",
|
|
17492
17511
|
name: "pi",
|
|
@@ -17514,7 +17533,7 @@ var piConnector = {
|
|
|
17514
17533
|
if (stateRoot) mkSecretDir(stateRoot);
|
|
17515
17534
|
if (sessionStatePath2) rmSync(sessionStatePath2, { force: true });
|
|
17516
17535
|
const env = {
|
|
17517
|
-
...launchEnv({ envAllow: opts.envAllow }),
|
|
17536
|
+
...launchEnv({ providerKeys: PI_PROVIDER_KEYS, envAllow: opts.envAllow }),
|
|
17518
17537
|
...aclEnv(opts),
|
|
17519
17538
|
// Creds, broker URL and the control token ride a 0600 file; only its path is exported, and the
|
|
17520
17539
|
// extension drops even that once it has read it, so a shell this seat runs inherits neither.
|