# Agent files

An agent file stores an agent's identity in YAML frontmatter and its persona in the Markdown body.
The catalog path is `.cotal/agents/<name>.md`:

```markdown
---
name: dave              # → COTAL_NAME / card.name
role: builder           # → COTAL_ROLE / card.role (presence + anycast address)
description: …          # → card.description
tags: [edit, test]      # → card.tags ("what it can do")
subscribe: [general, team.backend]     # channels it reads at boot (omit = none)
allowSubscribe: [general, team.>]      # read ACL (omit = same as subscribe)
allowPublish: [general, team.backend]  # post ACL (omit = none, default-deny)
model: opus             # optional model override
variant: high           # optional connector-defined model variant
capabilities: [spawn, run] # may manage teammates and start workflow runs
---
You are a builder on a shared mesh of peer agents…
```

The file accepts LF or CRLF line endings and an optional UTF-8 byte order mark.
A UTF-16 file is refused by name with the instruction to save it as UTF-8.

The frontmatter supplies the presence [AgentCard](/docs/spec/#6-presence-and-discovery).
The body supplies the persona text that the connector applies to the session's system prompt at launch.
Channel authorization follows [SPEC §9](/docs/spec/#9-nats--jetstream-security-and-authorization) and [Appendix B](/docs/spec/#appendix-b-profile-acls).

Connectors that use an external prompt file write a private temporary copy and pass its path.
The body does not appear in the agent process's arguments.
The copy is private to the OS user. Other processes running as that user can read it while it exists, just as they can read the agent file.

The launcher removes the copy after it proves that the agent process has exited.
On POSIX systems, a watcher beside the agent also removes it after exit if the launcher was killed, including for the built-in `pty` runtime and direct foreground launches.
A refusal before the command could start a process removes the copy at once.
An uncertain failure before the child started, a killed watcher after its launcher died, or a killed launcher on Windows can leave the copy for the OS temporary-file reaper.

## Fields

The parser is [`agent-file.ts`](https://github.com/Cotal-AI/Cotal/blob/main/packages/core/src/agent-file.ts).
It requires a YAML mapping between `---` fences and a valid `name`.

| Field | Type | Meaning |
| --- | --- | --- |
| `name` | string, required | Display name in `card.name`. At most 128 characters. Longer names are refused before credentials can overflow the broker's CONNECT line. The catalog filename selects the persona; `name` selects its displayed identity. |
| `role` | string | Presence label and anycast service address. A single `[A-Za-z0-9_-]` token; other spellings are refused when the agent joins. See [SPEC §3](/docs/spec/#3-subject-layout). |
| `kind` | `agent` \| `endpoint` | Participation class. Defaults to `agent`. |
| `description` | string | One-line summary in `card.description`. |
| `tags` | string[] | Capability tags in `card.tags`. |
| `subscribe` | string[] | Channels read at boot. Runtime join and leave operations can change this active set. Every entry must be within `allowSubscribe`. A [family](/api-reference/channels-and-permissions/#families) such as `team.>` gets stored copies of posts on its durable channels from startup. Omitted or empty means no channels, including `general`; DM, anycast, and presence remain available. |
| `allowSubscribe` | string[] | Channels the agent may read. Wildcard subtrees such as `team.>` are allowed. Omitted or empty defaults to `subscribe`. |
| `allowPublish` | string[] | Channels the agent may post to. Omitted or empty grants no posting permission. Wildcard subtrees are allowed. |
| `quiet` | string[] | Attention default. Ambient messages stay buffered until `cotal_inbox`; mentions remain automatic. Entries must be concrete channels within the read ACL. |
| `muted` | string[] | Attention default. Messages are dropped on receive, including mentions. Entries must be concrete channels within the read ACL. A channel cannot be both `quiet` and `muted`. |
| `model` | string | Model passed to the application. Claude accepts aliases such as `opus` or a full ID; OpenCode uses `provider/model`. A role's `modelPolicy` entry requires an allowed model and refuses a missing model. Whitespace-only values are refused when the file loads. See [Model policy](/api-reference/config/#model-policy). |
| `variant` | string | Connector-defined model variant, listed by `cotal models` where supported. A role's `modelPolicy` variants restrict the value when present. Whitespace-only values are refused when the file loads. |
| `agent` | string | Connector pinned by the persona, such as `claude` or `jcode`. An explicit `--harness` overrides it; the legacy `--agent` spelling is also accepted. The pin overrides caller and manager defaults. An unregistered connector is refused. See [Persona lookup](#persona-lookup). |
| `launchOptions` | map | Connector-specific options passed through to the application. Claude uses flags; OpenCode uses agent configuration; Codex uses `-c` overrides. Hermes, Jcode, and pi refuse options. `--opt key=value` overrides the same key in the file. A role governed by `modelPolicy` launches without options. See [Launch agents](/docs/launch-agents/#spawning-agents). |
| `capabilities` | string[] | Control permissions. `spawn` grants host-launch authority, named stop, and persona definition. `run` grants workflow start, resume, answer, status, and list, plus the spawn permissions needed by programs; it also exposes `cotal_run`. Missing capabilities grant none. `role:<r>` lets the agent hand role `r` to a helper it spawns. On a static mesh it also binds role `r`'s task queue, which is how the spawn bound sees that the agent holds the role; on a user-auth mesh the ledger checks it. `admin` is never a persona capability. See [Identity and auth](/docs/identity-and-auth/). |
| `owner` | string | Policy set by `definePersona` to a signed-in creator or one of the manager's own seats. On a static space, another creator, including the CLI, leaves the file operator-owned. Only that owner or an admin can redefine the file over the wire. An operator-written file without an owner requires admin authority for wire redefinition. This field is not peer-supplied content. |
| Any other scalar key | string | Stored in `meta` for connector-specific hints. Unmodeled maps and lists are not stored. A saved definition cannot use `meta` to overwrite modeled fields or the renamed `channels` and `publish` fields. The live session overlays connector-owned `connector`, `model`, `variant`, and `host` metadata, so the file cannot misstate its connector or machine. |

The parser refuses the old `channels` and `publish` keys.
[Channels and permissions](/api-reference/channels-and-permissions/) describes the three channel verbs.
Attention settings are defaults for each instance and reset on restart; runtime changes do not rewrite the file.
See [Attention](/docs/connect-claude/#attention).

The broker enforces control capabilities.
Because `launchOptions` passes native application options through, `spawn` grants authority to launch applications with those options on the manager host.
See the [Security model](/docs/security/).

## Persona lookup

A catalog name resolves to `<target-root>/.cotal/agents/<name>.md`.
The selected mesh determines the target root through the registry, including `cotal use`, `--space`, and `--server`.
Local persona commands, setup, status, and agent-profile minting use that catalog.
A managed spawn reads the selected manager's catalog on its execution host.
The file path is a local convention; NATS presence provides mesh discovery, and the file's card supplies the presence record.

The launcher sets `COTAL_AGENT_FILE=<abs path>` for the joined session to read its card.
`COTAL_LINK` supplies the mesh connection.
Individual `COTAL_*` variables can override file values as described in [Configuration](/api-reference/config/).

Without an explicit persona, selection checks `COTAL_DEFAULT_PERSONA`, the space's saved `persona`, the user's saved `persona`, and then `default`.
The application checks an explicit `--harness` or legacy `--agent`, the persona's `agent`, the caller's `COTAL_DEFAULT_AGENT`, the space's saved `harness`, and the user's saved `harness`.
When the caller supplies no harness preference, a managed launch uses the manager's default before the product default, Claude.
An explicit flag overrides the file, and the file overrides environment defaults in both managed and foreground launches.
See [Preferences](/api-reference/config/#preferences).

| Launch mode | Persona source and terminal behavior |
| --- | --- |
| Managed with attachment, `cotal spawn dave --on <instance>` | Reads the manager's `.cotal/agents/dave.md`, waits for readiness, and attaches this terminal when the runtime supports it. |
| Managed without attachment, `cotal spawn dave --detach` | Uses the same manager-side resolution and returns after readiness. |
| Direct, `cotal spawn dave` or `cotal spawn dave --foreground` | Reads the selected local catalog and runs the native application in this terminal. |
| Explicit file, `cotal spawn --file <persona-file>` | Reads a persona file on the execution host. It does not upload a local file or deploy a Mesh manifest. The public CLI reserves `--config` for persisted internal callers. |

The launch modes accept model, variant, working-directory, kickoff-prompt, ACL, and shared-tool overrides.
Mode-specific constraints and attachment requirements are in [Launch agents](/docs/launch-agents/) and the [spawn reference](/api-reference/cli-agents/#spawn).

`.cotal/` is gitignored because it contains local state.
Shared persona files can live outside that directory, as in [`examples/01-lateral-coordination/agents/`](https://github.com/Cotal-AI/Cotal/blob/main/examples/01-lateral-coordination/agents/).

## Persona purpose

The persona body describes the agent's responsibilities and how it coordinates with peers.
Useful details include the work it owns, where it reports results, and when it asks another role for help.
A role title such as "expert" does not specify those behaviors.
Links to repository docs or other sources give the agent a place to check facts instead of treating the prompt as their authority.

## Defining one at runtime

For a one-off helper, pass its persona on the spawn ([inline personas](#inline-personas)).
Define a persona when a broad one is worth reusing.
`cotal_persona(name, prompt, model?, role?, agent?, subscribe?, allowSubscribe?, allowPublish?, capabilities?, announce?)` sends content to the manager, which writes an agent file.
`cotal_spawn(name, role?, agent?, model?, variant?, join?)` then launches it.
Both tools also accept an `instance` pin for one full manager instance.
The [tool catalog](/api-reference/mcp-tools/) defines their full argument sets.
A persona that names no channel field at all is scoped to the spawner's own read and post grants, never wider.
Such a persona still starts on no channel unless the spawn passes `join`, and each channel in `join` must be one the spawner may read.

A complete agent file in `prompt` is merged into one frontmatter block.
For a new definition, its grants, role, and agent survive unless an explicit tool argument overrides them.
On redefinition, existing fields remain unless an explicit tool argument changes them; the prompt replaces the body.
Explicit `model`, `role` and `agent` arguments must be nonblank strings. Empty or whitespace-only values are refused with `bad-request` naming the field before any file is written. Omit a field to retain its inherited value.
Malformed leading frontmatter is refused with `prompt-frontmatter`.
A definition the manager could not load back is refused and nothing is written, for example one whose `subscribe` names a channel or family that no `allowSubscribe` entry contains.
`capabilities` is policy: only an explicit `capabilities` argument sets it, and the manager's `define-persona` command checks it against the definer before it writes.
A definer can put into a persona only capabilities it holds itself, and a later spawn of the persona is checked again against its spawner.
A redefine that omits `capabilities` keeps the saved list, and that list is checked the same way, so a definer that no longer holds a saved capability passes the ones it holds, or `[]` to clear them.
An open mesh checks nothing, and a manager whose user authority is hosted refuses any capability.
`owner` has no tool argument, and prompt frontmatter cannot grant capabilities or claim ownership.
A new definition records a signed-in creator or one of the manager's own seats as owner. Other static-space definitions remain operator-owned.

A persona without `capabilities` has no `spawn` permission.
A launch with effective role `manager` and no `spawn` capability is refused before allocation.
An operator can add `capabilities: [spawn]` to the file, a definer that holds `spawn` can define the persona with it, or the persona can launch under another role.

Definition sends no announcement unless `announce` names a channel.
With `announce: <channel>`, the announcement goes only to that channel.
A team channel limits the notice to the peers working there; the notice is not a request or authorization for other peers to spawn the persona.
`cotal_personas` lists and shows the catalog over the wire with spawn capability and ownership checks.
The local `cotal persona` command and legacy `cotal personas list` read the workspace catalog.
Spawning a missing name fails.

The operator's `cotal persona` command lists, shows, creates, edits, and removes files in the selected space's catalog without a broker.
Complete file input preserves all fields and source bytes after validation.
Local create and edit operations require the frontmatter `name` to match the catalog name.
Editor drafts validate before promotion and refuse concurrent local CLI changes.
See the [persona reference](/api-reference/cli-agents/#persona).

On a user-auth mesh, owned persona content can launch only for its creator principal or a caller with a current admin grant.
A hosted run uses its admitted caller for that check.
Foreign content returns not-found before connector loading or enrollment, including when selected by an explicit file path.
Unowned operator files remain launchable.
[Manifest launches](/api-reference/manifest/) have separate owner authorization.

## Inline personas

A spawn can carry its helper's persona itself.
An agent passes it as the `persona` argument of `cotal_spawn`, which forwards it as a `persona` record on the manager's `spawn` command.
The record holds `prompt` (the persona body), `role`, `model`, `variant`, `description`, `subscribe`, `allowSubscribe`, `allowPublish`, `capabilities` and `save`.
The spawn's own `prompt` is still the first turn the helper receives.
`name` is the helper's name, and a live name gets a number as a persona identity does.
A spawn with `persona` takes no `config` or `identity`.
The connector checks an inline `model` as it checks a `model` argument: if the manager did not record that pin, the spawn reports an error and says the seat may already be live.

The manager reads the record as it reads a file.
The same channel checks apply, the same spawn flags override it, and the same bound limits it to what the spawner holds ([security](/docs/security/)).
A record that names no channel field takes the spawner's own channels, and one that names `[]` gets none.
The body is plain text: a body that starts with `---` is still body.

The helper runs from a private snapshot, `.cotal/run/inline/<lifecycle-uid>.md`, written once the grant is made.
It holds the body and the descriptive fields, never a grant.
A crash restart refuses a snapshot that changed after the launch, and the manager deletes the snapshot when the helper retires.
If that delete fails, a static mesh keeps the helper's name held and retries it on the next retirement attempt.
An open or user-auth mesh logs the failure with the path and leaves the file for you to delete.
No spawn can name a snapshot as its persona file.

`save: true` also writes `.cotal/agents/<name>.md` under the requested name, for a persona worth reusing.
The copy records the grant the spawn was checked against: every channel field, `[]` included, and the capabilities, without the helper's own event channel.
Its owner is the spawner, as with `cotal_persona`, and a copy a workflow run saves is owned by the run's starter.
The write never replaces a file: a name the catalog already holds is refused before anything starts, so spawn that persona by name or save under another name.
Each later spawn of the copy is bounded against its own spawner.
