# Manifest reference

Every kortix.yaml v2 key, with defaults, limits, and validation rules.

Canonical page: https://kortix.com/docs/project/manifest

The manifest is the file the platform treats as authoritative for a project. This page lists every `kortix.yaml` (v2) key, its type, its default, and how the platform validates it. For the plain-language version, see [Your project](/docs/project).

## Two configuration surfaces

A Kortix project has two configuration surfaces with strict, non-overlapping ownership.

- **Kortix config** — `kortix.yaml` at the repo root, plus `.kortix/Dockerfile`. The platform reads this: the trigger sweep, the sandbox builder, session token minting, and the dashboard.
- **OpenCode config** — everything under `.kortix/opencode/` (`opencode.jsonc`, `agents/`, `skills/`, `commands/`, `tools/`, `plugins/`). The OpenCode runtime reads this, in the sandbox and locally.

The join between the two halves is the agent name. A manifest key `agents.<name>` must match the filename `.kortix/opencode/agents/<name>.md`. Every behavioral field lives only in that `.md` file's frontmatter and body, never in `kortix.yaml`:

- system prompt
- `model`, `mode`
- `temperature`, `top_p`
- `steps`, `permission`

The manifest's `agents:` block sets governance only: which agents may launch, and what each one may touch. The validator enforces this. It rejects a v2 agent block that contains any behavioral field, with an error that points at the agent's own `.md` file.

## Location and versions

Any repo with a valid manifest at its root is a Kortix project. `kortix.yaml` (v2, YAML only) is the current format for new projects, created by the web "Create project" flow and by `kortix init`. `kortix.toml` (v1) still works for existing projects, but the platform accepts no new v2 features on it. See [Legacy TOML](/docs/project/legacy-toml) for the v1 schema and the v1-to-v2 migration steps.

`kortix_version: 2` requires YAML — a `.toml` file that declares `kortix_version: 2` fails validation. A manifest that declares a version higher than `2` is rejected outright, so the platform never silently misreads a future field. Unknown top-level keys are ignored, so you can park your own metadata in the file.

`validateManifest()` is the single gate behind `kortix ship`, the change request merge check, and `kortix validate`. The same rules apply everywhere, so anything that merges into `main` is structurally sound. The schema is public and generated from the same package:

- [`kortix.v2.schema.json`](/schema/kortix.v2.schema.json)
- [`kortix.v1.schema.json`](/schema/kortix.v1.schema.json)
- [`kortix.schema.json`](/schema/kortix.schema.json) (dispatches on `kortix_version`)

Point a `kortix.yaml` at the v2 schema for editor validation, or fetch it from the CLI:

```yaml
# yaml-language-server: $schema=https://kortix.com/schema/kortix.v2.schema.json
kortix_version: 2
```

```bash
kortix schema --version 2
```

## Full example

```yaml
kortix_version: 2
default_agent: kortix

project:
  name: my-project
  description: What this project is.

env:
  required: [DATABASE_URL]
  optional: [STRIPE_API_KEY, WEBHOOK_SLACK_SECRET]

sandbox:
  templates:
    - slug: ml
      name: ML Development
      dockerfile: .kortix/Dockerfile.ml
      cpu: 4
      memory: 16

opencode:
  config_dir: .kortix/opencode

agents:
  kortix:
    connectors: all
    secrets: all
    kortix_cli: all
    skills: all

  release-bot:
    connectors: [github]
    kortix_cli: [project.cr.open]
    secrets: [GITHUB_AGENT_TOKEN]

triggers:
  - slug: daily-digest
    type: cron
    agent: kortix
    cron: "0 0 9 * * 1-5"
    timezone: America/Los_Angeles
    prompt: |
      Summarize yesterday's commits. Open a CR against main.
```

## Top-level keys

| Key | Required | Notes |
|---|---|---|
| `kortix_version` | yes | must be `2` |
| `default_agent` | yes | must name a declared, enabled agent |
| `runtime` | no | only legal value is `"opencode"` (the default) |
| `project.name` / `project.description` | no | display metadata; the platform does not read `project.name` for the project's display name |
| `env.required` / `env.optional` | no | env var names; `required` is advisory only, never enforced at session start |
| `sandbox` / `sandbox.templates` / `sandbox.default` | no | sandbox image(s) and hardware |
| `opencode.config_dir` | no | default `.kortix/opencode` |
| `triggers` | no | list of cron and webhook triggers |
| `connectors` | no | list of integration definitions |
| `policy.default_mode` | no | `allow_all` (default) or `risk`; project-wide connector approval mode |
| `agents` | yes | name-to-block governance map; must not be empty |

## `project:`

Optional, human-facing metadata: `name` and `description`. The platform does not read this table for anything — a project's display name comes from its own database record. Treat it as documentation for people reading the repo.

## `env:`

Declares the env var names your sessions need. Values live in the dashboard's Environment variables page, never inline in the manifest. The platform decrypts and injects them as plain env vars at session start.

```yaml
env:
  required: [DATABASE_URL]
  optional: [STRIPE_API_KEY]
```

| Field | Type | Notes |
|---|---|---|
| `required` | `string[]` | Advisory. The dashboard prompts the user for these, but session start does not block on a missing value. |
| `optional` | `string[]` | Available to sessions if set. Absence is fine. |

Env var names match `^[A-Z_][A-Z0-9_]*$`. The Secrets Manager caps a name at 64 characters — a longer name parses here but can never get a value. Names starting with `KORTIX_` can never get a value either. Keep names at 64 characters or fewer, and avoid the `KORTIX_` prefix. Full contract: [Secrets](/docs/project/secrets).

## `sandbox:` and `sandbox.templates`

A list of named, bootable sandbox images. Optional — with no entries, every session boots the platform's default image. Each template needs exactly one of `dockerfile` (repo-relative) or `image` (a public Docker reference, tag- or digest-pinned).

```yaml
sandbox:
  templates:
    - slug: ml
      name: ML Development
      dockerfile: .kortix/Dockerfile.ml
      cpu: 4
      memory: 16
      disk: 50
```

| Field | Type | Default | Notes |
|---|---|---|---|
| `slug` | string | — | Required. Unique per project. `default` is reserved. |
| `name` | string | slug | Display label in the dashboard picker. |
| `dockerfile` | string | — | Repo-relative path. Mutually exclusive with `image`. |
| `image` | string | — | Public Docker image, tag- or digest-pinned. Mutually exclusive with `dockerfile`. |
| `entrypoint` | string | runtime layer's own | Overrides the container entrypoint. |
| `cpu` | int | provider default | vCPU cores. Bound: 1–32. |
| `memory` | int | provider default | RAM in GiB. Bound: 1–128. |
| `disk` | int | provider default | Disk in GiB. Bound: 1–500. |

Each value must be a positive integer. A value below the minimum fails validation with an error. A value above the maximum passes with a warning, then clamps at runtime. GPUs are not supported — declaring `gpu` produces a warning, not an error. See [Runtime](/docs/work/runtime) for what the runtime layer injects on top of your image.

### `sandbox.default`

Set `default` on `sandbox` to make one template the project-wide default. Every session, trigger, and channel then boots it without naming a slug.

```yaml
sandbox:
  default: dev
  templates:
    - slug: dev
      dockerfile: .kortix/Dockerfile
```

`default` must name a template declared in this manifest, or the reserved value `"default"` for the platform image.

A spec change (`cpu`, `memory`, `disk`, or the image itself) rebuilds the project's snapshot. The new size applies on the next session, not a running one.

## `opencode:`

Where the OpenCode config directory lives. Optional, with a default.

```yaml
opencode:
  config_dir: .kortix/opencode
```

| Field | Type | Default | Notes |
|---|---|---|---|
| `config_dir` | string | `.kortix/opencode` | Repo-relative directory. |

That directory holds agents, skills, commands, tools, plugins, and `opencode.jsonc`. `opencode.jsonc` stays the OpenCode-native registry for plugins, MCP servers, providers, and permissions — do not duplicate those settings in the manifest. See [Agents](/docs/project/agents).

## `triggers:`

A list of cron and webhook definitions. Each entry fires a session that runs `prompt` as its first message.

```yaml
triggers:
  - slug: daily-digest
    type: cron
    cron: "0 0 9 * * 1-5"
    prompt: Summarize yesterday's commits.
```

| Field | Required | Default | Notes |
|---|---|---|---|
| `slug` | yes | — | `[a-z0-9][a-z0-9_-]{0,127}`, unique among triggers. |
| `type` | yes | — | `cron` or `webhook`. |
| `prompt` | yes | — | Non-empty. Supports templating. |
| `name` | no | slug | Human label. |
| `agent` | no | `default` | Must name a key in `agents:`, or fall back to `default_agent`. |
| `enabled` | no | `true` | `false` skips the entry. |
| `model` | no | resolves at fire time | Wire form `provider/model`. Pins the fired session to that model. See [Models](/docs/project/models). |
| `session_mode` | no | `fresh` | `fresh`, `reuse`, `pinned`, or `keyed`. |
| `session_id` | required for `pinned` | — | Exact session to re-prompt. |

Cron triggers need exactly one of `cron` (a 6-field expression) or `run_at` (a one-off ISO-8601 timestamp), plus `timezone` as an IANA name. Webhook triggers need `secret_env`, the name of a secret holding the HMAC signing key. Full field reference, payload templating, endpoints, and session strategy: see [Triggers](/docs/connect/triggers).

## `connectors:`

A list of external tools an agent can call. The definition lives in git; credentials live in the platform, never in the manifest. See [Connectors](/docs/connect/connectors) for the conceptual model.

```yaml
connectors:
  - slug: gmail-work
    provider: pipedream
    app: gmail
    policies:
      - match: "*"
        action: require_approval
```

| Field | Required | Notes |
|---|---|---|
| `slug` | yes | `[a-z0-9][a-z0-9_-]{0,127}`, unique among connectors. |
| `provider` | yes | `pipedream`, `mcp`, `openapi`, `postman`, `graphql`, `http`, or `channel`. |
| `name` | no | Display name. Defaults to slug. |
| `enabled` | no | Defaults to `true`. |
| `credential` | no | Only `shared` is supported. The retired `per_user` mode is a hard error. |
| `sensitive` | no | Defaults to `false`. `true` forces approval on every call to this connector, regardless of `policy.default_mode`. |

Provider-specific fields:

| Provider | Required field | Notes |
|---|---|---|
| `pipedream` | `app` | `account` optional, defaults to slug. |
| `mcp` | `url` | `transport`: `http` (default) or `sse`. |
| `openapi` | `spec` | A URL or repo-relative path. |
| `postman` | `spec` | A Collection JSON URL or path, a `.postman/api` manifest, or a Postman workspace URL. |
| `graphql` | `endpoint` | `spec` optional (SDL). |
| `http` | `base_url` | `spec` optional. |
| `channel` | `platform` | `slack`, `teams`, `email`, or `meet`. |
| `computer` | — | Synth-only. You cannot declare it by hand — it appears when a machine connects. See [Computers](/docs/connect/computers). |

`channel` connectors rarely need a hand-written entry — connecting a channel from the dashboard creates one for you. See [Slack & channels](/docs/connect/slack). The slugs `kortix_slack`, `kortix_teams`, `kortix_email`, `kortix_meet`, and `computer` are platform-owned; using one with a different provider is a validation error.

`connectors.auth`: optional, for providers other than `pipedream`. `type` is `bearer`, `basic`, `custom`, `oauth1`, or `none` (default). `oauth1` is restricted to `openapi`, `postman`, and `http` providers. `in` is `header` (default) or `query`. `name` is required when `type` is `custom`.

`connectors.policies`: a list of `{match, action}` pairs. `match` is a glob over tool names. `action` is `always_run`, `require_approval`, or `block`.

`policy.default_mode`: a top-level key, separate from `connectors:`, that sets the project-wide connector approval mode. It takes `allow_all` (default; every unmatched tool runs) or `risk` (require approval for write and destructive unmatched calls). Set it with `kortix connectors policy set --default <risk|allow_all>` — see [CLI](/docs/cli).

## Channels

v2 removes `channels:` from the schema. Channel-to-agent routing (Slack, Microsoft Teams, email, and meeting bots today) is live operational state, not declarative config. You set it from the dashboard's Channels page or from chat commands, the same boundary that keeps credentials out of git. Connecting a channel still creates a `connectors` entry with `provider: channel` for the agent to call. See [Slack & channels](/docs/connect/slack). The v1 `[[channels]]` table is covered in [Legacy TOML](/docs/project/legacy-toml).

## `agents:`

A name-to-block map, keyed by agent name. This map is governance only — it grants what an agent may touch, never what it says or does.

```yaml
default_agent: kortix

agents:
  kortix:
    connectors: all
    secrets: all
    kortix_cli: all
    skills: all

  release-bot:
    connectors: [github]
    kortix_cli: [project.cr.open]
    secrets: [GITHUB_AGENT_TOKEN]
```

| Field | Default | Notes |
|---|---|---|
| *(map key)* | — | `[a-z0-9][a-z0-9_-]{0,127}`, unique per project. Matches the `.md` filename it governs. |
| `enabled` | `true` | `false` treats the agent as undeclared — the platform will not launch it. |
| `connectors` | `none` | Which connector slugs this agent may call. `all` means every connector. |
| `secrets` | `none` | Which project secrets this agent receives as sandbox env vars. Renamed from v1's `env`. |
| `kortix_cli` | `none` | Which Kortix CLI and API actions this agent may perform. `all` means everything the launching user can do — an agent can never exceed its launcher. |
| `skills` | `none` | Which skills this agent may load. |
| `workspace` | — | Reserved for future git-boundary controls. Not yet enforced. |

v2 is deny-by-default: an omitted `connectors`, `secrets`, `kortix_cli`, or `skills` on a declared agent resolves to `none`. Grant every permission an agent needs explicitly, as the starter's `kortix` agent does above. A project migrating from v1 must re-grant everything by hand — v1 defaults `env` (secrets) to `all` when omitted, the opposite of v2.

`kortix_cli` grants come from a fixed set of project-scoped actions (`project.read`, `project.cr.open`, `project.cr.merge`, `project.trigger.*`, `project.secret.*`, `project.connector.*`, and more). Account-scoped actions — `member.*`, `billing.*`, `project.create` — can never be granted to an agent. Run `kortix validate --scopes` from inside a session to print the live, grantable list.

## When config changes take effect

- A manifest or `.kortix/opencode/` edit applies only after a change request merges to `main`. Sessions and the trigger sweep read the default branch, not session branches.
- A trigger's cron or webhook change is picked up by the scheduler within seconds of the merge.
- A sandbox image or hardware change rebuilds the snapshot. The new spec applies on the next session, not the current one.
- A secret value change in the dashboard resolves at sandbox-create time. It applies on the next session, not a running one.

## Round-trip rules

Dashboard edits are a read-modify-write on the same file. To keep diffs clean across UI and in-session edits:

1. Keep `kortix_version` as the first key.
2. Inside a trigger entry, order fields `slug`, `name`, `type`, `agent`, `enabled`, then type-specific fields, then `prompt` last.
3. If you add a webhook trigger before its secret is set, list the secret name in `env.optional`. Leave the trigger `enabled: false` until the value is in.
