# Manifest reference The project manifest — every table, field, default, and validation rule, for both kortix.yaml (v2) and kortix.toml (v1, legacy). Canonical page: https://kortix.com/docs/reference/manifest The one file the platform treats as authoritative for a project. For the plain-language version, see [Projects](/docs/concepts/projects). The manifest lives at the repo root — `kortix.yaml` (v2, the current default for new projects), its `kortix.yml` short-extension sibling, or `kortix.toml` (v1, legacy, still fully supported for existing projects). Any repo with a valid manifest at the root is a Kortix project. When more than one candidate file is present, the platform resolves them in priority order — `.yaml` first, then `.yml`, then `.toml` — and reads the first one it finds. The parser is permissive — it never throws on a bad entry: bad triggers go into an `errors` list alongside the good ones, and unknown top-level keys are ignored (park your own metadata freely). The retired `apps:` section, if present, is rejected with an explicit "Delete this section." error. > **The canonical schema** > Either version, the always-current, machine-checkable spec is the public JSON Schema — generated straight from `@kortix/manifest-schema`, the same package behind `kortix validate` and the CR-merge gate, so it can't drift from what the platform actually enforces: > > - [`kortix.v2.schema.json`](/schema/kortix.v2.schema.json) — `kortix_version: 2` only (current default) > - [`kortix.v1.schema.json`](/schema/kortix.v1.schema.json) — `kortix_version: 1` only (legacy) > - [`kortix.schema.json`](/schema/kortix.schema.json) — both, dispatched by `kortix_version` > > Point a `kortix.yaml`'s first line at it for editor validation + autocomplete: > > ```yaml > # yaml-language-server: $schema=https://kortix.com/schema/kortix.v2.schema.json > kortix_version: 2 > ``` > > Or fetch it from the CLI: `kortix schema --version 2` (add `--url` for just the URL). ## Full example (v2, `kortix.yaml`) ```yaml # yaml-language-server: $schema=https://kortix.com/schema/kortix.v2.schema.json 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 # repo-relative; or `image: python:3.12-slim` cpu: 4 memory: 16 opencode: config_dir: .kortix/opencode agents: kortix: # governance ONLY — behavior lives in .kortix/opencode/agents/kortix.md connectors: all secrets: all kortix_cli: all skills: all release-bot: # a scoped specialist connectors: [github] kortix_cli: [project.cr.open] # may open a CR, but not merge it secrets: [GITHUB_AGENT_TOKEN] # only this secret, not every project secret triggers: - slug: daily-digest name: Daily digest type: cron agent: kortix enabled: true cron: "0 0 9 * * 1-5" # 09:00 Mon–Fri timezone: America/Los_Angeles prompt: | Summarize yesterday's commits across the repo. Save the result to notes/digest-{{ fired_at }}.md and open a CR against main. - slug: slack-hook name: Slack handler type: webhook agent: kortix enabled: true secret_env: WEBHOOK_SLACK_SECRET # add value via Secrets Manager prompt: | Slack event from {{ headers.user_agent }}. User said: {{ body.text }} ``` `agents:` is a governance-only map — see [`agents:`](#agents-v2) below. There's no `[[channels]]` in v2 (see [Channels](#channels-removed-in-v2)). ## Legacy example (v1, `kortix.toml`) ```toml # Pinned schema version. Lets the platform evolve safely. kortix_version = 1 [project] name = "my-project" description = "What this project is." [env] required = ["DATABASE_URL"] optional = ["STRIPE_API_KEY", "WEBHOOK_SLACK_SECRET"] [[sandbox.templates]] slug = "ml" # unique per project name = "ML Development" dockerfile = ".kortix/Dockerfile.ml" # repo-relative; or `image = "python:3.12-slim"` cpu = 4 memory = 16 [opencode] config_dir = ".kortix/opencode" [[triggers]] slug = "daily-digest" name = "Daily digest" type = "cron" agent = "kortix" enabled = true cron = "0 0 9 * * 1-5" # 09:00 Mon–Fri timezone = "America/Los_Angeles" prompt = """ Summarize yesterday's commits across the repo. Save the result to notes/digest-{{ cron.fired_at }}.md and open a CR against main. """ [[triggers]] slug = "slack-hook" name = "Slack handler" type = "webhook" agent = "kortix" enabled = true secret_env = "WEBHOOK_SLACK_SECRET" # add value via Secrets Manager prompt = """ Slack event from {{ headers.user_agent }}. User said: {{ body.text }} """ ``` ## Schema version ```yaml kortix_version: 2 ``` `kortix_version` is the required schema version. A manifest declaring a version higher than the platform knows about is rejected outright — the platform won't silently misread future fields. `kortix_version: 2` requires YAML (`.yaml`); a `.toml` file declaring `kortix_version: 2` is a validation error pointing at the migration flow. When the platform writes the manifest back after a dashboard edit, it ensures `kortix_version` is the first key. ### `runtime:` (v2 only) Optional top-level string, v2 only. Today the only legal value is `opencode` — the default when omitted — reserved so a future runtime (e.g. a hypothetical `runtime: claude`) is a one-line manifest change rather than a schema break. It has no other effect yet. ```yaml runtime: opencode ``` ## What's parsed where | Surface | v2 (`kortix.yaml`) | v1 (`kortix.toml`, legacy) | | ----------------- | ---------------------------------------------------------- | ------------------------------------------------------- | | Trigger sweep | `triggers:` | `[[triggers]]` | | Sandbox builder | `sandbox.templates` | `[[sandbox.templates]]` | | Sandbox runtime | `opencode:` (where to launch opencode with its config) | `[opencode]` | | Session bootstrap | `env:` (advisory — surfaced to dashboard, not enforced) | `[env]` | | Connector sync | `connectors:` | `[[connectors]]` | | Agent governance | `agents:` (governance-only name→block map — server-side launch roster + connector/`kortix_cli`/`secrets`/`skills` grants) | `[[agents]]` (array of tables, same grant fields plus dead `model`/`file`) | | Dashboard UI | All of the above + the raw manifest | Same | | *(removed)* | `channels:` does not exist in the v2 schema — see [below](#channels-removed-in-v2) | `[[channels]]` parses and round-trips but no runtime path reads it | ## `project:` / `[project]` Optional, human-facing metadata (`name`, `description`). The platform does **not** currently read this table — a project's display name comes from its own record, not the manifest — so treat it as a convention for people reading the repo. ## `env:` / `[env]` Declares the env var *names* your sessions need — same key in both versions. Values live in the dashboard's Environment variables page (still `project_secrets`/`secrets` under the hood), never inline; the platform decrypts and injects them as plain env vars at session start. (The per-agent *grant* field is what's renamed in v2 — `env` → `secrets` inside each `agents:` block — see [`agents:`](#agents-v2) below.) ```yaml env: required: [DATABASE_URL] optional: [STRIPE_API_KEY] ``` | Field | Type | Notes | | ---------- | ---------- | ---------------------------------------------------------------------- | | `required` | `string[]` | Advisory list — surfaced in the dashboard. Not enforced at session start today. | | `optional` | `string[]` | Available to sessions if set; absence is fine. | > **On enforcement** > The dashboard uses `required` to nag the user about secrets to set, but the session bootstrap does not currently block on missing values. Treat `required` as a contract with the user, not the platform. Manifest name validation is permissive — items match `^[A-Z_][A-Z0-9_]*$` (no length cap). The Secrets Manager API caps names at 64 chars (`^[A-Z_][A-Z0-9_]{0,63}$`), so a longer name is accepted in the manifest but can never get a value. Keep names ≤ 64 chars. The `KORTIX_*` prefix is reserved at the Secrets Manager surface, not at parse time: listing `KORTIX_FOO` in `[env]` parses fine but can never have a value. Don't. Full contract: [Secrets](/docs/reference/secrets). ## `sandbox.templates` / `[[sandbox.templates]]` A list of named, bootable sandbox images; the Kortix runtime layer (opencode CLI, agent daemon, entrypoint) is layered on top of it automatically. Optional — with no entries, every session boots the always-available platform default image. Declare as many templates as you like; a session picks one by `slug`. This is also where you size the sandbox hardware. The schema is provider-neutral: an image comes either from a **Dockerfile in your repo** or a **public Docker image reference**, and Kortix maps the `cpu` / `memory` / `disk` resource spec onto Daytona, Platinum, or E2B. ```yaml sandbox: templates: # From a repo Dockerfile - slug: ml # unique per project; not "default" (reserved) name: ML Development # optional display label dockerfile: .kortix/Dockerfile.ml # repo-relative cpu: 4 # vCPU cores memory: 16 # GiB disk: 50 # GiB # From a public image - slug: python name: Python 3.12 image: python:3.12-slim # must be tag- or digest-pinned cpu: 2 memory: 4 ``` > **v1 (`kortix.toml`, legacy)** > ```toml > [[sandbox.templates]] > slug = "ml" > name = "ML Development" > dockerfile = ".kortix/Dockerfile.ml" > cpu = 4 > memory = 16 > disk = 50 > > [[sandbox.templates]] > slug = "python" > name = "Python 3.12" > image = "python:3.12-slim" > cpu = 2 > memory = 4 > ``` | Field | Type | Default | Notes | | ------------ | ------ | ---------------- | --------------------------------------------------------------------- | | `slug` | string | — | **Required.** Unique per project. `default` is reserved. | | `name` | string | slug | Display label shown in the dashboard picker. | | `dockerfile` | string | — | Repo-relative path. **Mutually exclusive** with `image`. | | `image` | string | — | Public Docker image, tag- or digest-pinned (no bare `latest`). **Mutually exclusive** with `dockerfile`. | | `entrypoint` | string | — | Overrides the container entrypoint the snapshot boots with. Optional — when omitted, the Kortix runtime layer's own entrypoint is used. | | `cpu` | int | provider default | vCPU cores. | | `memory` | int | provider default | RAM in GiB. | | `disk` | int | provider default | Disk in GiB. | Exactly one of `image` or `dockerfile` is required per entry. A Dockerfile path must be repo-relative — absolute paths and `..` traversal are rejected. GPUs are not supported in this version. See [Sandbox image](/docs/reference/sandbox-image) for what the runtime layer injects and what your Dockerfile can do. ### `sandbox.default` — the project-wide default template By default every session boots the platform default image; a session opts into a template by passing its `slug`. Set `default` on `sandbox` to make one of your templates the project-wide default instead — then **every** session (the dashboard's "new session", triggers, channels) boots it without specifying a slug. ```yaml sandbox: default: dev # or "default" for the platform image (the implicit default) templates: - slug: dev dockerfile: .kortix/Dockerfile ``` `default` must name a template defined in this manifest (or the reserved `"default"`). It's the only key allowed alongside `templates` — image/build keys directly on `sandbox` are the removed legacy singular shape and are rejected. ### Hardware spec `cpu` / `memory` / `disk` size the sandbox. Each is independent and optional — leave one out and the runtime provider's default applies. Values are coerced to whole numbers; a value below 1 falls back to the default, and a value above the platform ceiling (cpu 32, memory 128 GiB, disk 500 GiB) is clamped down rather than rejected. > **A spec change rebuilds the snapshot** > The spec is baked into the project's snapshot at build time — sandboxes inherit their resources from the snapshot, so there's no per-session override. Changing any spec field is part of the snapshot's content hash, so it triggers one rebuild; the new size takes effect on the **next** session, the same way a Dockerfile edit does. Projects that don't declare a spec are unaffected — their snapshot hash is unchanged. ## `opencode:` / `[opencode]` Where OpenCode's config dir lives. Optional, with a default. The agent daemon launches opencode with `OPENCODE_CONFIG_DIR` pointed here. ```yaml opencode: config_dir: .kortix/opencode ``` | Field | Type | Default | Notes | | ------------ | ------ | ------------------ | ------------------------------------------------------ | | `config_dir` | string | `.kortix/opencode` | Repo-relative dir. Same silent-fallback behavior as `sandbox.templates` paths. | That directory holds agents, skills, slash commands, tools, plugins, and `opencode.jsonc` — the layout opencode reads by convention. OpenCode owns the runtime semantics; Kortix may inspect metadata to build server-side agent/model UI surfaces. See [Kortix vs OpenCode config](/docs/reference/config-boundary) and [opencode.ai/docs](https://opencode.ai/docs/). `opencode.jsonc` remains the OpenCode-native registry for plugins, MCP servers, providers, model/provider settings, permissions, and default runtime behavior. Do not duplicate those details in the manifest. Use `agents:` (v2) / `[[agents]]` (v1) for the Kortix-side decision of which agents are launchable and what server-side grants they receive — in v2, EVERY behavioral field (model, mode, tools, permission, the prompt itself) lives in the agent's own `.md` frontmatter instead, never in the manifest. Legacy v1 projects with no `[[agents]]` keep OpenCode-native discovery for backward compatibility. Projects on `agents:` (v2) or `[[agents]]` (v1) opt into declarative, server-side agent discovery: product UI should use the API's registered agents, while unregistered OpenCode files may still exist for local experiments or runtime internals. Model pickers should similarly come from the server / LLM-gateway catalog rather than a sandbox-local OpenCode provider list. ## `triggers:` / `[[triggers]]` Each entry fires a session that runs `prompt` as its initial message — a fresh session by default, the trigger's prior session when `session_mode: reuse`, or a specific session first when `session_mode: pinned` + `session_id` is set (see below). Sorted alphabetically by slug in the parsed output, so UI ordering is stable. Full reference: [Triggers](/docs/reference/triggers). ```yaml triggers: - slug: daily-digest type: cron cron: "0 0 9 * * 1-5" prompt: Summarize yesterday's commits. ``` ### Common fields | Field | Required | Type | Default | Notes | | -------------- | -------- | ------ | ----------- | -------------------------------------------------------------- | | `slug` | yes | string | — | `[a-z0-9][a-z0-9_-]{0,127}`, unique among triggers. | | `type` | yes | string | — | `"cron"` or `"webhook"`. | | `prompt` | yes | string | — | Mustache-style template. | | `name` | no | string | `slug` | Human label. | | `agent` | no | string | `"default"` | Agent name. Legacy projects resolve this through OpenCode discovery; declarative projects should use a registered `[[agents]]` name. | | `enabled` | no | bool | `true` | When `false`, the scheduler / receiver skip the entry. | | `model` | no | string | — | Wire form `provider/model` (e.g. `anthropic/claude-sonnet-5`). Pins the fired session to that model, taking precedence over the agent/account/platform default chain. Unlike the dead v1 `[[agents]].model`, this one is live. | | `session_mode` | no | string | `"fresh"` | `"fresh"`, `"reuse"`, or `"pinned"` (camelCase `sessionMode` also accepted). `"reuse"` continues the trigger's existing session on each fire; `"pinned"` first tries the exact `session_id`. | | `session_id` | for `pinned` | string | — | Exact project session to re-prompt when `session_mode` is `"pinned"` (camelCase `sessionId` also accepted). | Cron-only: `cron` (6-field croner `second minute hour day month weekday`) or a one-off `run_at` (camelCase `runAt`, an ISO-8601 datetime, e.g. `2026-06-01T09:00:00Z`) — exactly one of the two is required; `run_at` fires the trigger once at that instant instead of on a recurring schedule. `timezone` (IANA name, default `"UTC"`) applies to `cron`. Webhook-only: `secret_env` (name of a `project_secrets` entry holding the HMAC secret). The parser accepts only canonical trigger field names. Slug uniqueness is per-section — a trigger and an app may share a slug; two triggers may not. ## Channels — removed in v2 [#channels-removed-in-v2] **v2 removes `channels:` from the schema outright.** Channel↔agent routing (Slack and Microsoft Teams today; Discord/etc. later) is live operational state — the dashboard's Channels page or chat slash commands — not declarative config, the same boundary rule that keeps credentials out of git. The channel *integration* itself still shows up as a `connectors` entry with `provider: channel` once you connect one; only the routing table moved out of the manifest. ### `[[channels]]` (v1, legacy) Array of tables. Each entry documents a chat platform (Slack or Microsoft Teams today) connected to the project. **Validated but not wired to any runtime behavior** — declaring one, or editing `enabled`/`agent`/`events`/`prompt_prefix`, changes nothing about how the platform actually routes messages. The live channel→agent binding is the `chat_channel_bindings` database row, set from the chat app's commands or the dashboard's Channels page, not from this file. ```toml [[channels]] platform = "slack" enabled = true # optional, default true agent = "kortix" # optional — parsed, not consumed events = ["mention", "dm"] # optional, array of strings — parsed, not consumed prompt_prefix = """ You're working on {{ project.name }}. {{ message.text }} """ ``` | Field | Required | Type | Default | Notes | | --------------- | -------- | ---------- | ------- | ---------------------------------------------------------------------- | | `platform` | yes | string | — | e.g. `"slack"`. One entry per platform per project — a duplicate `platform` is a validation error. | | `enabled` | no | bool | `true` | Validated as a boolean. Not read by any runtime path. | | `agent` | no | string | — | Parsed, not validated, not consumed. | | `events` | no | `string[]` | — | Parsed as an array of strings. Not consumed. | | `prompt_prefix` | no | string | — | Parsed. Not consumed. | > **Declared but inert** > Connecting Slack or Microsoft Teams from the dashboard writes the install into the project's live channel state and creates the `chat_channel_bindings` row — that row, not this manifest block, decides which agent answers in which channel. A hand-written `[[channels]]` entry documents intent for readers of the repo but has no effect on running behavior. Manage channel routing from the chat app's commands or the dashboard's Channels page. ## `connectors:` / `[[connectors]]` Each entry connects an external tool the agent can call. The definition lives in git; credentials live in the platform, never here. Full model: [Connections](/docs/concepts/connections). ```yaml connectors: - slug: gmail-work provider: pipedream # pipedream | mcp | openapi | graphql | http | channel app: gmail # pipedream app slug credential: shared # shared is the only mode — see below policies: - match: "*" action: require_approval # always_run | require_approval | block ``` | Field | Required | Type | Notes | | ------------ | -------- | ------ | ------------------------------------------------------------------ | | `slug` | yes | string | `[a-z0-9][a-z0-9_-]{0,127}`, unique among connectors; the tool namespace. | | `provider` | yes | string | `pipedream` \| `mcp` \| `openapi` \| `graphql` \| `http` \| `channel`. | | `name` | no | string | Display name. Defaults to slug. | | `enabled` | no | bool | Defaults to `true`. | | `credential` | no | string | `shared` is the only mode and the default. `per_user` (per-member BYO credential) was removed 2026-07-05 — a legacy manifest that still says `credential: per_user` is tolerated and always resolves to `shared`. | Provider-specific fields: - **pipedream** — `app` (required), `account` (optional, defaults to slug). - **mcp** — `url` (required), `transport` = `http` (default) or `sse`. - **openapi** — `spec` (required: URL or repo-relative path). - **graphql** — `endpoint` (required), `spec` (optional SDL). - **http** — `base_url` (required), `spec` (optional). - **channel** — `platform` (required) = `slack` \| `teams` \| `email` \| `meet`. Chat platforms. You rarely declare these by hand — connecting one ([Channels](/docs/concepts/channels)) **auto-materializes** the connector (credential = the install token, resolved server-side), so the agent can call it through the Executor and you manage it in Connectors. - **computer** — connected machines over the Agent Computer Tunnel. **Synth-only: you cannot declare it by hand.** Connecting a machine ([Computers](/docs/concepts/computers)) auto-materializes a single `computer` connector that fronts all your machines (it has no credential — the live tunnel is the credential; per-machine access is granted in Computers). Reserved slugs: `kortix_slack`, `kortix_teams`, `kortix_email`, `kortix_meet` (each locked to `provider: channel`) and `computer` (locked to `provider: computer`) are platform-owned — declaring one of these slugs with any other provider is a validation error, the same rule that keeps `computer` synth-only above. ### `connectors.auth` Optional. `type` = `bearer | basic | custom | none` (default `none`); `in` = `header` (default) or `query`; `name` (required when `type = "custom"`); `prefix` (optional). The credential value is stored in the platform connector credential store, not in the manifest. Auth is not allowed with `provider: pipedream`. ### `connectors.policies` / `[[connectors.policies]]` Optional per-tool gates. `match` (glob over tool names, required) + `action` = `always_run | require_approval | block` (required). ## `agents:` (v2) [#agents-v2] A name→block **map**, keyed by agent name — **governance only**. There is no `model`/`mode`/`description`/`permission`/`prompt` here at all: every behavioral field lives in that agent's own OpenCode `.md` file under `.kortix/opencode/agents/.md` (frontmatter + body, a stock OpenCode agent file — no Kortix-specific split). The map key is the join to that `.md`'s filename. `default_agent` (top-level, required in v2) must name a declared, enabled agent here. ```yaml default_agent: kortix agents: kortix: # the default general-purpose agent connectors: all # every connector profile secrets: all # every project secret kortix_cli: all # full Kortix CLI/API powers (∩ the launching user's role) skills: all release-bot: # a scoped specialist connectors: [github] kortix_cli: [project.cr.open] # may open a CR, but not merge it secrets: [GITHUB_AGENT_TOKEN] # only this secret, not every project secret ``` | Field | Required | Type | Default (v2) | Notes | | ------------ | -------- | ------------------------------ | --------------- | ---------------------------------------------------------------------- | | *(map key)* | yes | string | — | `[a-z0-9][a-z0-9_-]{0,127}`, unique per project. Matches the OpenCode `.md` filename it governs. | | `enabled` | no | bool | `true` | `false` treats the name as undeclared — default-deny. | | `connectors` | no | `string[] \| "all" \| "none"` | **`none`** | Which connector slugs this agent may call. `"all"` = every connector profile not scoped away from it. | | `secrets` | no | `string[] \| "all" \| "none"` | **`none`** | Project secret names this agent receives as sandbox env vars and may read via the secrets API. Renamed from v1's `env`. | | `kortix_cli` | no | `string[] \| "all" \| "none"` | **`none`** | Which Kortix CLI/API actions (open/merge CRs, manage triggers, spawn sessions, manage connectors, …) this agent may perform. `"all"` = everything the *launching user* can do. Effective grant is always `userRole ∩ agentGrant` — an agent can never exceed its launcher. Run `kortix validate --scopes` for the full grantable-action list. | | `skills` | no | `string[] \| "all" \| "none"` | **`none`** | Which skills this agent may load. Folds onto the compiled OpenCode `permission.skill`. | | `workspace` | no | — | — | Reserved for the git-boundary work (workspace/git powers) — not yet enforced. | v2 is **deny-by-default**: an omitted `connectors`/`secrets`/`kortix_cli`/`skills` on a declared agent resolves to `none`, the opposite of v1's per-field defaults. Give an agent every grant explicitly (as the starter's `kortix` agent does above) if it should keep full access. ## `[[agents]]` (v1, legacy) Array of tables. Optional — with no `[[agents]]` section, every session is capped only by the launching human's role (the project hasn't adopted per-agent governance). Adding **any** `[[agents]]` entry opts the whole project into declarative, server-side agent governance: any agent name not listed here is then default-denied — it still runs its OpenCode `.md` behavior, but with no connectors, no `kortix_cli` powers, and no project secrets. An agent's *behavior* (prompt, mode, tools, permission tree) still lives entirely in its OpenCode `.md` file under `.kortix/opencode/agents/`. `[[agents]]` is a governance overlay on top of that: which agent names the platform will launch, and what each one may touch. ```toml [[agents]] name = "kortix" # your default general-purpose agent kortix_cli = "all" # full Kortix CLI/API powers (∩ the launching user's role) connectors = "all" # every connector profile [[agents]] name = "release-bot" # a scoped specialist connectors = ["github"] kortix_cli = ["project.cr.open"] # may open a CR, but not merge it env = ["GITHUB_AGENT_TOKEN"] # only this secret, instead of every project secret ``` | Field | Required | Type | Default | Notes | | ------------ | -------- | ------------------------------ | --------------- | ---------------------------------------------------------------------- | | `name` | yes | string | — | `[a-z0-9][a-z0-9_-]{0,127}`, unique per project. Matches the OpenCode `.md` filename it governs. | | `enabled` | no | bool | `true` | `false` treats the name as undeclared — default-deny once the project has adopted `[[agents]]`. | | `connectors` | no | `string[] \| "all" \| "none"` | none (`[]`) | Which `[[connectors]]` slugs this agent may call. `"all"` = every connector profile not scoped away from it. | | `kortix_cli` | no | `string[] \| "all" \| "none"` | none (`[]`) | Which Kortix CLI/API actions this agent may perform. `"all"` = everything the *launching user* can do. Effective grant is always `userRole ∩ agentGrant`. Run `kortix validate --scopes` for the full grantable-action list. | | `env` | no | `string[] \| "all" \| "none"` | **`"all"`** | Project secret names this agent receives as sandbox env vars and may read via the secrets API. Unlike `connectors`/`kortix_cli`, omitting `env` defaults to **all** project secrets in v1 — the opposite of v2's deny-by-default. Renamed `secrets` in v2. | | `model` | no | string | — | Wire form `provider/model` (e.g. `anthropic/claude-sonnet-5`). **Dead — parsed and round-tripped, but never applied at runtime**; the effective model comes from session/trigger model preferences. Removed outright in v2 (lives in the agent's `.md` instead). | | `file` | no | string | conventional `.md` by name | Override path to the agent's OpenCode behavior file. **Dead — parsed but not currently used** during materialization. Removed outright in v2. | > **The `default` sentinel (v1 only)** > In v1, a trigger, channel, or session that names no agent (or names the literal `"default"`) does **not** resolve to a declared `[[agents]]` entry — no agent is ever named `default`. It resolves to OpenCode's own configured default agent (conventionally `kortix`) and is treated as non-binding: full access, capped only by the launching user's role, exactly as if the project had no `[[agents]]` at all. **v2 removes this ambiguity**: `default_agent` is a required top-level key that must name a declared, enabled agent — there's no unbound sentinel. > **Two v1 fields don't do anything** > `model` and `file` round-trip through a v1 manifest (dashboard edits preserve them) but neither is consulted by the runtime. Don't rely on them to change what a session actually does. Both are removed outright in v2 (that behavior lives in the agent's own `.md` file). ## Round-trip rules Dashboard manifest edits are a read-modify-write on the same file. To keep the diff clean across UI and in-session edits: - Keep `kortix_version` as the first key. - Inside a trigger entry (`triggers:` in v2, `[[triggers]]` in v1), write fields in this order: `slug`, `name`, `type`, `agent`, `enabled`, then type-specific fields, then `prompt` last. - If you add a webhook trigger before its secret is set, declare the secret name in `env.optional` so it shows up in the dashboard's Environment variables page, and leave the trigger `enabled: false` until the value is in.