# Agent manifest

Current agent.yaml settings, with types, defaults, validation rules, and effects.

`agent.yaml` is the deployable contract for one agent version. This page describes the current authoring fields: what each setting accepts, what it defaults to, and what rejects a manifest at registration.

For a guided introduction to the fields you touch first, read
[Manifest essentials](/guides/build/manifest-essentials).

Two rules apply everywhere:

- **Unknown top-level keys are rejected.** A typo fails the manifest rather than being ignored. The
  one exception is a key prefixed `x-`, which is accepted and carried through untouched.
- **A manifest is validated against a version, not an agent.** Settings are captured at
  registration and are immutable for that version; you change them by publishing a new version.

## A complete manifest

Most agents need about this much. Every field links to the section that defines it.

```yaml title="agent.yaml"
name: support-copilot                 # stable agent identity
version: 0.2.0                        # labels one immutable build
framework: claude-agents-sdk          # harness adapter
entrypoint: agent:create_agent        # python_module:function
description: Triage and draft responses to support requests
kind: interactive                     # interactive | background
identity_mode: service                # pinned by the first version

model: claude-sonnet
permitted_models: [claude-sonnet, claude-haiku]

persistent: true                      # required for hosted versions
session:
  idle_timeout: 30m
  max_lifetime: 24h
resources:
  cpu: 100m
  memory: 512Mi

network:
  egress:
    - api.zendesk.com                 # bare hostname, within the tenant ceiling

policy:
  approval_required_tools: [Bash]     # a person decides before these run
  enable_ask_user: true
  scope_ceiling: [llm:claude-sonnet:invoke]
  allowed_audiences: [https://api.tanomis.com]

integrations:
  support:
    connector: global/support-desk  # use an existing configured connector
    required: true
    tools:
      allow: [search_tickets, draft_reply]
      approval_required: [draft_reply]
```

## Field index

Grouped by the section that defines each field. "Hosted: yes" means the hosted control plane
requires it; lenient non-hosted profiles may default it.

### Identity and packaging

| Field | Type | Required | Default |
|---|---|---|---|
| [`name`](#identity-and-packaging) | string | yes | — |
| [`version`](#identity-and-packaging) | string | in practice | — |
| [`framework`](#identity-and-packaging) | enum | yes | — |
| [`entrypoint`](#identity-and-packaging) | string | yes | — |
| [`description`](#identity-and-packaging) | string | no | empty |
| [`kind`](#behavioral-classification) | enum | yes | — |

### Models and tools

| Field | Type | Required | Default |
|---|---|---|---|
| [`model`](#models) | string | no | resolved by the platform |
| [`permitted_models`](#models) | list of string | no | empty |
| [`permitted_tools`](#models) | list of string | no | empty |

### Attribution and identity

| Field | Type | Required | Default |
|---|---|---|---|
| [`identity_mode`](#attribution-and-identity) | enum | hosted: yes | `service` only in lenient non-hosted profiles |
| [`inherits_user_permissions`](#attribution-and-identity) | boolean | no | `false` |
| [`identity.client_id`](#attribution-and-identity) | string | no | assigned at registration |

### State, sessions, and resources

| Field | Type | Required | Default |
|---|---|---|---|
| [`persistent`](#state-warm-starts-and-sessions) | boolean | hosted: yes | `false` only in lenient non-hosted profiles |
| [`cr_enabled`](#state-warm-starts-and-sessions) | boolean | deprecated compatibility field | ignored for runtime selection |
| [`settlement_mode`](#state-warm-starts-and-sessions) | enum | no | `standard` |
| [`session.idle_timeout`](#state-warm-starts-and-sessions) | duration | yes | — |
| [`session.max_lifetime`](#state-warm-starts-and-sessions) | duration | yes | — |
| [`resources.cpu`](#resources) | quantity | yes | — |
| [`resources.memory`](#resources) | quantity | yes | — |
| [`resources.cpu_limit`](#resources) | quantity | no | platform burst default |
| [`resources.memory_limit`](#resources) | quantity | no | platform burst default |

### Network, policy, tools, and delegation

| Field | Type | Required | Default |
|---|---|---|---|
| [`network.egress`](#network) | list of string | no | empty |
| [`network.egress_mode`](#network) | `allowlist` or `public` | no | `allowlist` |
| [`policy.*`](#policy) | block | no | see the section |
| [`integrations`](#governed-mcp-connectors) | map of requirement | no | empty |
| [`access.github`](#github-repositories) | block | no | none |
| [`delegation.*`](#agent-to-agent-delegation) | block | no | empty |

### Metadata, caps, and harness

| Field | Type | Required | Default |
|---|---|---|---|
| [`data_classification`](#declared-capability-metadata) | list of string | no | empty |
| [`cloud_access`](#declared-capability-metadata) | list of string | no | empty |
| [`cost_cap_usd`](#declared-capability-metadata) | number | no | `0` |
| [`token_cap`](#declared-capability-metadata) | integer | no | `0` |
| [`lifecycle_policy`](#declared-capability-metadata) | block | no | empty |
| [`harness.<framework>`](#harness-settings) | block | no | harness defaults |
| [`autonomy`](#declared-capability-metadata) | any | no | accepted, not yet applied |

## Identity and packaging

```yaml
name: support-copilot
version: 0.1.0
framework: claude-agents-sdk
entrypoint: agent:create_agent
description: Triage and draft responses to support requests.
```

- **`name`** — the stable agent identity, and the name every other command refers to. Must start
  with a letter or digit and contain only letters, digits, underscore, dot, or dash, up to 128
  characters. A deploy that names a different agent than the manifest does is rejected.
- **`version`** — the label for one immutable build. Validation does not require it, but deploy
  does: supply it here or pass `--version` on the command line. A published version is immutable,
  so publish a new label rather than rebuilding an existing one.
- **`framework`** — the harness adapter. One of `claude-agents-sdk` or `deepagents`; confirm the
  set your control plane offers with `tanomis agent harnesses`.
- **`entrypoint`** — `module:function`, resolved inside your packaged project. Exactly one colon,
  with both halves non-empty.
- **`description`** — free text shown in listings. Optional, and worth writing: it is what a person
  scanning the agent catalog reads.

## Behavioral classification

```yaml
kind: interactive
```

**`kind`** is required and is one of:

| Value | Meaning |
|---|---|
| `interactive` | Human in the loop, turn by turn. Latency-sensitive resource profile. |
| `background` | Runs without a person waiting on each turn. |

**`autonomy`** is accepted and stored verbatim but is not yet applied to behavior. Set it only if
you want the declaration on record.

## Models

```yaml
model: "your-workspace-model-id"
permitted_models:
  - "your-workspace-model-id"
```

- **`model`** — the default model resolved for a run. `default` follows the organization's
  default model, resolved at each session start; any other value pins that model.
- **`permitted_models`** — the runtime allowlist. A run that requests a model outside this list is
  refused. Leaving it empty does not widen access; it means the manifest declares no allowlist and
  the model decision falls to platform and tenant policy.

Use model IDs available to the selected tenant. Whenever you change `model`, update the matching
`llm:<model-family>:invoke` entry in `policy.scope_ceiling`, or the declared ceiling stops
describing the model the agent actually calls.

## Attribution and identity

```yaml
identity_mode: service
inherits_user_permissions: false
```

- **`identity_mode`** — the agent's intended authority posture. One of `service` (the agent's own
  machine identity), `delegated` (intended user subject plus agent actor), or `hybrid`. Hosted
  deployments require an explicit value and reject omission; a lenient
  non-hosted profile may resolve a missing value to `service`. **The first non-rejected version
  pins it** across every later version of the agent; a rejected initial attempt does not establish
  the pin, so agree the mode with your organization administrator before the first deploy.
- **`inherits_user_permissions`** — the gated impersonation opt-in. Illegal with
  `identity_mode: service`, because there is no user to inherit from; use `delegated` or `hybrid`.
  Turning it on seeds an impersonation-approval gate rather than granting anything on its own.
- **`identity.client_id`** — the agent's application identity. On a hosted control plane this is
  assigned at registration and an author-supplied value is **rejected**, so omit the `identity`
  block entirely.

> **Availability:** Service-mode bind-time identity is enabled per tenant; delegated and scoped authority are held contracts. See [What is available where](/reference/availability).

For the actor model, hybrid-path boundary, denial sequence, and safe attribution guidance, read
[Agent identity and access](/concepts/identity-and-access).

## State, warm starts, and sessions

```yaml
persistent: true
session:
  idle_timeout: 30m
  max_lifetime: 24h
```

- **`persistent`** — provisions durable tenant-scoped storage and gives each session its own
  directory. It is required for new hosted versions.
- **`cr_enabled`** — deprecated. Older manifests may retain it during the internal rolling
  deployment, but true, false, and omission do not select a runtime in current runtime-api. The
  deploy pipeline and CLI still use `true` temporarily to request a golden build; that legacy
  consumer is removed by the golden-policy follow-up. RAM snapshot policy is platform-owned. Until
  universal-worker and golden-policy cutover, remote admission requires `true` for settlement or
  author-declared egress; that is a rollout barrier, not a tenant-facing compatibility period.
- **`settlement_mode`** — one of `standard`, `work`, or `settlement`, in that order of strength; a
  missing value resolves to `standard`. `standard` is the existing behavior. `work` means the agent
  produces durable work records. `settlement` requires the session to establish the managed
  worker/proxy boundary or fail activation.
- **`session.idle_timeout`** / **`session.max_lifetime`** — both required. Compact Go-style
  durations (`45s`, `30m`, `24h`), positive, and resolving to whole seconds. `1500ms` is rejected.

> **Availability:** A snapshot is an optimization, not the execution-boundary selector. Durable work and settlement are enabled per tenant; a settlement-mode session fails activation where the boundary is unavailable. See [What is available where](/reference/availability).

## Resources

```yaml
resources:
  cpu: 100m
  memory: 512Mi
  cpu_limit: 2
  memory_limit: 2Gi
```

`cpu` and `memory` are the **requests** — the reserved baseline that drives scheduling and density.
They are required. `cpu` accepts a Kubernetes CPU quantity (`500m`, `2`); `memory` accepts a binary
quantity (`512Mi`, `2Gi`) — decimal suffixes like `500M` are rejected.

`cpu_limit` and `memory_limit` are the optional **burst ceiling**. When unset the platform applies a
generous default so a run can spike above its request. Set them to raise or pin the ceiling for one
agent, subject to tenant policy.

Leave the requests at their scaffolded values until a measurement gives you a reason to change them.

## Network

```yaml
network:
  egress:
    - github.com
    - api.example.com
```

**`network.egress_mode`** defaults to `allowlist`. Set it to `public` to permit arbitrary public
hostnames through the managed proxy. In public mode, `egress` must be absent or empty. An unset
tenant ceiling permits this mode; any configured hostname ceiling rejects it. Private and reserved
addresses remain blocked. Public mode requires capable node/helper versions and the external route;
see [Restrict network egress](/guides/govern/restrict-network-egress#allow-arbitrary-public-websites).

**`network.egress`** is the list of external hostnames this agent may reach. It is part of the
version's contract: captured at registration, immutable for that version, and enforced at run time.

Availability is per tenant. Ask your administrator whether author-declared egress is enabled for
the selected tenant before relying on it; where it is not, the list is still validated and recorded
but no external route exists.

### What an entry may be

Each entry is a bare hostname, written in canonical form — lowercase, no surrounding whitespace,
no trailing dot. Anything else is refused at deploy rather than quietly rewritten, so the value you
wrote is the value that is enforced.

Refused, each with the reason named:

| Not allowed | Why |
|---|---|
| `*.github.com` | Wildcards are not permitted in a declared ceiling. An entry already covers its subdomains. |
| `https://github.com`, `github.com:443`, `github.com/path` | The match is on host alone; a scheme, port, or path would be silently ignored. |
| `140.82.121.4` | An address carries no DNS identity, and the allowlist matches the host an agent asks for. |
| the loopback name, `svc.cluster.local`, `printer.local`, `vault.internal` | Names for the sandbox itself or the in-cluster world. Platform destinations are added for you and are not declarable. |
| `GitHub.com`, `github.com.` | Not canonical. Write `github.com`. |
| `github` | A hostname needs at least two labels. |

**An entry covers its subdomains.** `github.com` admits `api.github.com` and `a.b.github.com`. It
does not admit `notgithub.com` or `github.com.example`. Declare the narrowest name that covers what
you need.

### The tenant ceiling

Your administrator can set a per-tenant ceiling that your declaration is intersected with. It
only ever narrows: it cannot add a destination you did not declare, and it never affects the
platform's own routes.

An unconfigured tenant admits any public hostname, so an unset ceiling behaves as though only
your manifest applied. Where a ceiling excludes an entry, the deploy fails naming both the entry and
the ceiling that excluded it, before any image is built — so you find out in seconds rather than
after a build.

A ceiling of `github.com` admits a declaration of `api.github.com`, because that is narrower than
what the administrator permitted.

### What is enforced, and where

- **At deploy.** The grammar and the tenant ceiling, before a build is created.
- **At rollout.** The ceiling again, when the version is enabled for a tenant — the point at which a
  globally published agent first has a tenant to be measured against.
- **At run time.** Every connection is mediated. A destination outside the list is refused. A
  declared hostname that resolves to a private, loopback, link-local, or in-cluster address is
  refused too, and the connection is made to the address that was checked, so a name whose record
  changes cannot redirect it.

The platform's own destinations — the managed model gateway and the governed connector routes — are
always available and are never declared here. Your list can only add to them.

### Requirements

Egress enforcement runs in the managed worker's session-scoped proxy for both snapshot restore and
clean bootstrap. If that boundary cannot be established, activation is refused instead of running
unproxied. During the internal mixed-runtime rollout only, remote admission may require the legacy
`cr_enabled: true` spelling so an older runtime replica cannot accept a contract it cannot enforce.

## Policy

```yaml
policy:
  approval_required_tools:
    - Bash
  enable_ask_user: false
  enable_claude_md: false
  enable_context_files: false
  scope_ceiling:
    - "llm:your-model-family:invoke"
  allowed_audiences:
    - "https://your-workspace-api.example"
  approval_thresholds:
    - condition: "scope:payments.*"
      approver: workspace-admin
```

### Human-in-the-loop

- **`approval_required_tools`** — every listed tool pauses the run and emits a permission request
  that the console renders as an allow/deny card. Names are the harness's own tool names.
- **`enable_ask_user`** — exposes the harness question flow, letting the agent ask a person a
  question mid-run and wait for the answer.
- **`enable_claude_md`** — `claude-agents-sdk` only. Lets the agent read a project `CLAUDE.md` from
  its workspace as a context file. Ignored on other harnesses.
- **`enable_context_files`** — accepted for forward compatibility with a broader context-file
  surface. Today the only such surface is gated by `enable_claude_md`.

`approval_required_tools` and `enable_ask_user` produce the durable runtime interactions described
in [Handle approval-required actions](/guides/run/approval-required-actions). That application flow is
separate from `approval_thresholds` below, whose scoped-token completion path remains held.

### Authorization contract

- **`scope_ceiling`** — the maximum set of concrete capabilities this agent's tokens may ever
  carry. The broker intersects requested scopes with it; it never widens them. Each entry must
  match exactly one of three grammars, and **wildcards are rejected**:

  | Form | Example |
  |---|---|
  | `<resource>.<action>` | `calendar.read` |
  | `mcp:<server>:<tool>:<verb>` | `mcp:github:create_issue:write` |
  | `llm:<model>:<capability>` | `llm:your-model-family:invoke` |

  On a hosted control plane the ceiling is required; where it is optional, an absent ceiling means
  allow-all with a warning.

  **What the ceiling governs.** It bounds what this agent may do *acting as itself*, in two places:

  - **Agent-to-agent delegation.** On a delegated call the caller's ceiling is intersected with the
    callee's, and only scopes inside both are granted. An agent with no ceiling cannot receive a
    delegated grant at all.
  - **Token exchange (held contract).** If a future supported flow exchanges a token against one
    of the agent's `allowed_audiences`, a requested scope outside the ceiling is refused. No
    supported public runtime workflow currently exposes this scoped exchange.

  **What it does not govern.** Three settings look like "what may this agent do", and they are
  separate mechanisms — sizing the ceiling for these will not work:

  | Concern | Where it is actually enforced |
  |---|---|
  | Which model a run may use | `permitted_models` |
  | Which MCP tools an agent may call | `integrations.<key>.tools.allow` intersected with the current workspace grant |
  | What the agent may do as itself | **`scope_ceiling`** |

  In particular, an `mcp:<server>:<tool>:<verb>` entry is a well-formed ceiling scope, but adding
  one does **not** grant an MCP tool call and omitting one does not block it — see
  [Governed MCP connectors](#governed-mcp-connectors). Entries are captured at registration and are
  immutable for that version, so declare the ceiling you need and widen it in a later version
  rather than padding it defensively.

- **`allowed_audiences`** — the `https://` resource indicators the agent's minted tokens may
  target. **Required once `scope_ceiling` is non-empty**, and each entry must be an `https://` URI.
  Use the audience issued for your tenant/environment; never copy one from another environment.

- **`approval_thresholds`** — maps a sensitivity condition to an approver. In the held delegated
  exchange contract, matching a sensitive scope classifies the request as approval-required; no
  supported durable approval ID, decision endpoint, or resume path currently completes that
  exchange. `condition` is required and is a sensitivity *pattern*, so unlike a grant it may use
  wildcards. `approver` is optional and defaults to the invoking user. See [Agent identity and
  access](/concepts/identity-and-access#what-the-service-assertion-is-and-is-not)
  before designing around these fields.

Both `scope_ceiling` and `allowed_audiences` are re-checked for presence on each new version, so a
new version can widen them — which is exactly the decision an agent's author should be making
deliberately.

## Declared capability metadata

```yaml
permitted_tools: []
data_classification:
  - internal
cloud_access:
  - object-storage
cost_cap_usd: 25
token_cap: 2000000
lifecycle_policy:
  required_gates:
    - regression-suite
    - prompt-injection-suite
  gate_categories:
    prompt-injection-suite: adversarial
  rollback: manual
```

- **`permitted_tools`** — the tool allowlist carried into the policy decision point. An empty list
  declares no manifest-level restriction; enumerate tools to narrow what the agent may run.
- **`data_classification`** and **`cloud_access`** — the data sensitivity and cloud-resource classes
  the agent declares. Recorded on the version and surfaced through the catalog for review.
- **`cost_cap_usd`** and **`token_cap`** — declared budgets for the version. Must not be negative.
- **`lifecycle_policy.required_gates`** — the evaluation gates that must pass before the
  gate-guarded promotion. **`lifecycle_policy.rollback`** is `manual` or `auto`; empty resolves to
  `manual`.
- **`lifecycle_policy.gate_categories`** maps a required gate to the kind of eval suite behind it:
  `quality`, `robustness`, or `adversarial`. A gate left out is `quality`. Each key must name a
  gate in `required_gates`, and any other value is refused at registration. The version's gate
  listing (`GET /v1/agents/{name}/versions/{version}/gates`) returns the category on every gate so
  robustness and adversarial suites can be shown apart from quality evals.

`data_classification`, `cloud_access`, `cost_cap_usd`, and `token_cap` are recorded as version
metadata and exposed for governance review. Treat them as the agent's declaration of intent, and
keep the enforcement in `permitted_models`, `permitted_tools`, and `policy.scope_ceiling`.

## Governed MCP connectors

An agent references a configured connector directly. No catalog association, contract publication,
or connection-selection step is needed.

```yaml title="agent.yaml"
integrations:
  finance:
    connector: global/alphavantage-workspace
    required: true
    tools:
      allow: [SYMBOL_SEARCH, COMPANY_OVERVIEW, EARNINGS]
```

Find the connector and its approved tools:

```bash
tanomis mcp connector list
tanomis mcp connector tools CONNECTOR_ID
```

Use `global/NAME` for a platform connector, `tenant/NAME` for a connector owned by the
agent's workspace, or an exact connector ID. An unqualified name works when unambiguous.
A global agent can reference global connectors; publish a tenant-scoped agent when it needs a
tenant-owned connector. The integration map key is just the agent's local name.

The connector supplies the endpoint, credentials and shared/personal account mode. Personal
connectors still require each caller to connect their own account. Credentials never belong in
the manifest. An optional `identity: workspace` or `identity: user` can assert the expected mode;
it must match the connector.

`tools.allow` is the maximum set the agent may use. Optional `tools.approval_required` lists tools
that require confirmation for each call and must be a subset of `tools.allow`. The connector's
current permitted tools and account access can narrow this set, never expand it.

At registration, the platform records the connector ID and approved tool schemas automatically.
These platform-owned details appear separately from author configuration in the Manifest tab.
Deleting and recreating a connector with the same name does not redirect an existing version.

## GitHub repositories

An agent names the GitHub repositories it may use, and what it may do in each, under
`access.github`. Each run gets a GitHub token for exactly those repositories and capabilities;
`git` and `gh` use it automatically.

```yaml title="agent.yaml"
access:
  github:
    repos:
      anyfind/agent-platform-website-tanomis:
        can: [read, push, pull_request]
      anyfind/docs:
        can: [read]
```

| Capability | Lets the agent | GitHub App permission |
|---|---|---|
| `read` | clone, fetch, read files and metadata (always included) | contents: read |
| `push` | push branches | contents: write |
| `pull_request` | open and update pull requests | pull requests: write |
| `issues` | create and comment on issues | issues: write |

There is no separate merge capability: `push` and `pull_request` together can merge a pull
request unless branch protection forbids it.

A global agent cannot know a customer's repositories, so it declares a named slot instead. Each
workspace chooses the repository for the slot when it enables the agent (**Agent → Integrations**,
or `tanomis agent access NAME --bind target=owner/name`):

```yaml title="agent.yaml"
access:
  github:
    choose:
      target:
        can: [read, push, pull_request]
```

What decides access:

- **The tenant's GitHub ceiling.** A workspace admin sets which repositories agents may be given, and
  how far, in **Settings → Organization → GitHub** (or `tanomis github ceiling set`). A declaration outside
  the ceiling is refused at deploy or enablement with the gap named. Lowering the ceiling later
  makes affected runs fail with the same reason.
- **The GitHub App installation.** A repository the workspace's connected installation cannot
  see is refused at deploy.
- **Every run.** The intersection is checked again when a run starts and on every token request,
  and tokens are revoked when the run ends.

Rules:

- Repository names are GitHub `owner/name`; `owner/*` is a ceiling pattern, not a declaration.
- Slot names use lowercase letters, digits, `-` and `_`.
- `github.com` and `api.github.com` are added to `network.egress` automatically. That opens
  those hosts for every path: the run's token limits what the agent can do on GitHub with
  platform credentials, not which GitHub repositories it can send data to.
- `asks_first` is not supported yet and is rejected rather than ignored.
- The retired top-level `repository:` block is rejected with a rewrite hint, including when an
  older version that carries it is cloned or fanned out.

See [GitHub repositories](/guides/connect-tools/github-repositories) for connecting GitHub and
setting the ceiling.

## Changes and availability

- Changing the connector or allowed tools requires a new agent version.
- Rotating credentials on the same connector does not require a new version.
- Changed tool schemas require review and a new agent version.
- **Agent → Integrations** shows the declared connector, readiness and expandable allowed tools.
  Operators can disable or enable access for the agent; they cannot substitute another connector.
- A required connector that is unavailable blocks execution. An unavailable optional connector is omitted.
- Disabled access remains disabled when publishing another version or migrating an old declaration.

Existing catalog-based agent versions remain compatible. New agents should use the direct
connector declaration above. No attachment creation or approval workflow is involved.

## Agent-to-agent delegation

```yaml
# caller
delegation:
  calls:
    - summarizer
    - agent: reconciler
      skills: [reconcile]

# callee
delegation:
  accepts:
    - tenant
  skills:                      # optional
    - name: summarize
      description: Summarize a support thread and return the key facts and the customer's ask.
```

The block has two independent halves. An agent may call, be called, both, or neither, and
declaring neither is the closed default. **The manifest is the grant:** a caller names the agents
it may hand work to, a callee names who may call it, and the intersection of the two, evaluated
against the versions currently enabled for the tenant, is the authority. Nothing is created out of
band and nothing has to be redone when either side deploys a new version.

- **`calls`** — the agents this agent may delegate to. Naming a target puts the delegation tools
  on this agent's bind, and it is the caller-side ceiling: an agent cannot reach a callee it did
  not name. An entry is a bare agent name, or a mapping with `agent` and a `skills` list that
  narrows the target to those skills. No wildcards.
- **`accepts`** — who may delegate to this agent. Empty means nobody.

  | Value | Meaning |
  |---|---|
  | `tenant` | Any agent in the same tenant. The widest class there is. |
  | `self` | Other versions of this same agent only. |
  | `<agent-name>` | That one named agent. |

- **`skills`** — optional. A callee that declares none exposes one implicit skill, `default`,
  whose input is a `prompt` plus any further fields the caller passes, described by the agent's
  own `description`. Declare skills only when the agent offers several distinct typed operations.
  `name` is lowercase alphanumeric with underscores or dashes and unique within the list; renaming
  one breaks every caller that lists it. `description` is required and functional, not decoration:
  a calling model reads it to choose a skill. `input_schema` is optional; when present it is
  enforced on every call. **Schema hashes are derived by the CLI, never written by hand.**

`skills` without `accepts` is rejected: the skills would be unreachable. `accepts` without
`skills` is fine and is the common case.

## Harness settings

Keep only the block matching `framework`. Unknown keys inside a harness block are rejected at
warmup with the offending key named.

### `claude-agents-sdk`

```yaml
harness:
  claude-agents-sdk: {}
```

The Claude Agent SDK harness takes no manifest options today; the empty block is the marker. Its
human-in-the-loop behavior comes from `policy.approval_required_tools`, `policy.enable_ask_user`,
and `policy.enable_claude_md`.

### `deepagents`

```yaml
harness:
  deepagents:
    is_async: true
    stream_modes: [messages, updates, custom]
    checkpointer: workspace-sqlite
    backend: workspace-composite
    recursion_limit: 50
```

| Key | Values | Default |
|---|---|---|
| `is_async` | boolean | `true` |
| `stream_modes` | any of `messages`, `updates`, `custom`, `values` | `[messages, updates, custom]` |
| `checkpointer` | `workspace-sqlite`, `in-memory` | `workspace-sqlite` |
| `checkpointer_options.delta_channels` | boolean | `true` |
| `store` | `workspace-sqlite`, `none` | `none` |
| `backend` | `workspace-composite`, `author-provided` | `workspace-composite` |
| `allow_agent_model_override` | boolean | `false` |
| `allow_author_checkpointer` | boolean | `false` |
| `allow_author_store` | boolean | `false` |
| `recursion_limit` | positive integer | `200` |
| `recursion_limit_ceiling` | positive integer | `1000` |
| `enable_repl_middleware` | boolean | `false` |
| `subagent_event_attribution` | `nested`, `flat` | `nested` |

`recursion_limit` must be less than or equal to `recursion_limit_ceiling`. The runtime clamps the
effective limit to the ceiling, so the ceiling is the real bound on a single run's step count.

Leave `backend`, `checkpointer`, and the three `allow_author_*` switches at their defaults unless
you are deliberately taking ownership of workspace state; the defaults are what keep the workspace
and lifecycle under the runtime's control.

## Cross-field rules

These are the checks that reject a manifest which is individually well-formed:

- New hosted versions require `persistent: true`.
- During the internal mixed-runtime rollout only, remote admission may require legacy
  `cr_enabled: true` for settlement or author-declared egress. The manifest validator itself does
  not make that field authoritative.
- `inherits_user_permissions: true` is illegal with `identity_mode: service`.
- A non-empty `policy.scope_ceiling` requires a non-empty `policy.allowed_audiences`.
- `integrations.<key>.tools.approval_required` must be a subset of that integration's `tools.allow`.
- `delegation.skills` requires a non-empty `delegation.accepts`.
- `harness.deepagents.recursion_limit` must not exceed `recursion_limit_ceiling`.

## Validate before you build

```bash
tanomis agent deploy . --tenant northwind-sandbox --dry-run
```

`--dry-run` runs the full preflight, including the authorization contract, and packages locally
without creating a build or uploading anything. Use it: on the remote build path, registration is
the **last** step, so a manifest error found there costs you an image that has already been built
and pushed.
