Spec: Project DSL
- Status: Draft
- Last amended: 2026-06-03 (agent execution limits —
max_stepsandmax_output_tokensonAgentSpec) - Constrained by: ADR-0006, ADR-0007, ADR-0008, ADR-0009, ADR-0011, ADR-0015, ADR-0022, ADR-0023, ADR-0024
- Implements:
packages/dsl/(planned)
Purpose
This spec defines the project DSL: the YAML file an operator authors to declare a kaged project. It is the source of truth for the agent tree (which agents exist and their parent-child dispatch relationships), what each agent can touch (cage and tools), which prompts they use, and which model backs each agent.
This document is normative for:
- The schema (JSON Schema published at
kaged.dev/schema/v1.json). - The parser's behavior — strict mode, error messages, cross-reference validation.
- The CLI surfaces (
kaged dsl validate,kaged dsl migrate). - What the daemon loads at session-start time.
It is not normative for:
- The wire shape of API requests that operate on projects after they're loaded (that's
http-api.md). - The runtime semantics of how the primary actually dispatches to subagents (that's
daemon.md). - The cage enforcement mechanism — only the cage contract. The sandbox spec (
sandbox.md) implements the contract this spec declares.
Constraints (from ADRs)
| Constraint | Source |
|---|---|
| Format is YAML 1.2 | ADR-0006 |
Non-executable; never eval'd, never imported as code |
ADR-0006 |
| Validated against published JSON Schema; mirrored as Zod internally | ADR-0006 |
| Strict mode: unknown fields are errors | ADR-0006 |
Single file at .kaged/project.yaml (no imports, no includes in v1) |
ADR-0006 |
Schema versioned via top-level version: integer |
ADR-0006 |
| Cage block is the operator-readable contract; enforcement is bwrap-shaped | ADR-0009 |
| Network allowlist is declarative; supports glob hostnames | ADR-0009 |
File layout
<project root>/
├── .kaged/
│ └── project.yaml # this file
└── prompts/
├── primary.md
├── scraper.md
└── deployer.md # referenced from project.yaml
- Exactly one
.kaged/project.yamlper project root. - Filename:
project.yaml(not.yml). - Encoding: UTF-8, LF line endings, no BOM.
- Path references (e.g.,
system_prompt: project:/prompts/primary.md) use a URI prefix (project:/orconfig:/) per ADR-0015. Naked paths are rejected at parse time.
The shape, top-level
# yaml-language-server: $schema=https://kaged.dev/schema/v1.json
version: 1 # required, integer
project: <slug> # required, string
description: <string> # optional
primary: # required, AgentSpec (the root agent)
model: <model-id>
system_prompt: <path>
cage: disabled # required; only `disabled` accepted for root agent (interim)
tools: # optional, per-agent tool overrides
"file.read": { enabled: true }
parameters: <object> # optional, model-specific
description: <string> # optional
subagents: # optional, recursive named-object map
<name>: # AgentSpec form (no `path:` field)
model: <model-id>
system_prompt: <path>
cage: <cage-block> | disabled
tools: <tools-map> # optional, per-agent tool overrides
parameters: <object> # optional
description: <string> # optional
subagents: # optional, recursive (depth limit 16)
<name>: ...
<name>: # Project-reference form (presence of `path:` is the discriminator)
path: project:/sub/path # required; project:/ only in v1
name: <string> # optional; tool-name override presented to the LLM
description: <string> # optional; description override presented to the LLM
overrides: <object> # optional; partial ProjectDsl deep-merged on top of nested project
plugins: # optional, named-object map (project-level registry)
<plugin-id>:
package: <string> # required; plugin package identifier
source: <string> # optional; install source with magic prefix
enabled: <boolean> # optional; project-wide default (default: false)
config: <object> # optional; plugin-specific project defaults
tasks: # optional, named-object map
<name>:
command: <string>
description: <string> # optional
group: <string> # optional
cwd: <path> # optional
long_running: false # optional
confirm: false # optional
env: <object> # optional
The full JSON Schema is in Appendix A. Field-by-field semantics are below.
Fields
version (integer, required)
The schema major version. Currently 1.
- Required. Must be the first non-comment key.
- Integer, not string.
version: "1"is invalid. - Major bumps only. Minor amendments to the schema do not bump this; they're additive.
- Forward-compat: A kaged version that supports schema v1 will error on a file declaring
version: 2. It will not "try its best." - Migration:
kaged dsl migrate <file>rewrites a v1 file to the current major if a migration path exists; otherwise errors with a diff.
project (string, required)
The project's slug. Identifies the project across logs, the audit trail, the URL of the web UI, and the storage namespace.
- Required.
- Pattern:
^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$— lowercase letters, digits, and hyphens; 2-64 chars; cannot start or end with a hyphen. - Unique per kaged installation. If two project directories declare the same slug, the daemon errors at session-start time and refuses to load either.
description (string, optional)
Human-readable description of the project. Used in the web UI as the project subtitle. Plain text; no markdown rendering.
- Optional. Default: empty string.
- Max length: 280 characters (one tweet's worth; if you need more, write a README).
primary (AgentSpec, required)
The root agent for the project. Exactly one. This is an AgentSpec — the same recursive shape used at every position in the agent tree per ADR-0022. See § AgentSpec below for the full shape.
The root agent has two special properties that non-root agents do not:
- Role-based default tools. The root agent gets all
kaged.*tools enabled by default:kaged.checkpoint,kaged.issue.*,kaged.ask, andkaged.form. All other namespaces (file,edit,search,code,debug) start disabled and must be explicitly opted in via operator config ([default_tools]) or the project'sprimary.toolsblock. Every other agent in the tree starts with an empty tool set; the operator opts in per agent. - Cage interim restriction. Until the supervisor supports caging the primary process, the only legal value for the root agent's
cageisdisabled. The parser emits a parse-time error for any other value. Non-root agents accept the full cage block.
AgentSpec
AgentSpec is the single recursive shape for every agent in the project tree. It is used identically for ProjectDsl.primary (the root agent) and for every entry under any subagents map at any depth. There is no PrimaryAgent vs Subagent distinction — the only differentiation is positional (root vs non-root).
Per ADR-0022: the tree structure is the call graph. A parent agent can call its direct children; sibling and cross-tree calls do not exist.
# AgentSpec — used at every agent position
model: <model-alias> # required
system_prompt: <path> # required
cage: <cage-block> | disabled # required
description: <string> # optional
parameters: <object> # optional
include_tool_results_in_context: <boolean> # optional, default true
tools: # optional, per-agent tool overrides
"<tool-name>":
enabled: true
description: <string>
parameters: <object>
"<glob>": null # nullify = reset to defaults
plugins: # optional, per-agent plugin overrides
<name>: # must reference a slot in the project-level registry
enabled: <boolean> # optional; override registry default
hooks: [ <hook-name>, ... ] # optional, lifecycle hooks to subscribe to
config: <object> # optional, override registry config
compaction: # optional, per-agent compaction (ADR-0024)
strategy: drop | summarize | delegate | checkpoint # optional, default 'drop'
upper_threshold: <0.0..1.0> # optional, default 0.85
lower_threshold: <0.0..1.0> # optional, default 0.60
always_keep: [ <predicate>... ] # optional
summarize: <object> # optional, defaults provided (prompt, window, preserve_recent)
delegate: <object> # optional, strategy-specific
subagents: # optional, recursive named-object map
<name>: <AgentSpec> # same shape, recursively
<name>: <ProjectReference> # or a project reference (presence of `path:`)
<name>: null # ADR-0015 nullification
AgentSpec fields
model (string, required)
A model alias — a free-form name that the operator's local config resolves to a concrete provider:model identifier (per ADR-0011).
- Examples:
smart-generalist,low-cost-coder,local-only,my-codereviewer. - Pattern:
^[a-z][a-z0-9-]{0,62}[a-z0-9]$— lowercase letters, digits, hyphens; 2-64 chars. - Must NOT contain a colon. A value containing
:(e.g.claude:sonnet-4.6) is rejected at parse time with a clear error pointing the operator to ADR-0011. The DSL is portable; concrete provider:model strings live only inlocal-config.md. - Reserved names (not usable as aliases):
primary,subagent,operator,system,default. - Resolution timing: parse-time validation checks the shape of the alias name only. Whether the operator's local config has a binding is checked at project-load time (see Validation timing) — an unresolved alias keeps the project in
pendingstate until the operator binds it. - Provider reachability (does the resolved provider's
base_urlrespond? does the API key work?) is checked at session-start time — same as before. A bound alias whose provider is unreachable produces a 502provider_unreachableperhttp-api.md.
Recommended starter aliases (kaged ships with these as suggestions, not enforcement):
| Alias | Intended role |
|---|---|
smart-generalist |
Default primary; capable general-purpose model |
smart-careful |
Higher-quality (slower, more expensive) model for hard work |
low-cost-fast |
Cheap fast model for simple tasks |
low-cost-coder |
Cheap fast model with reasonable coding ability |
local-only |
Whatever local model the operator runs |
tiny |
Minimal local model for trivial tasks |
Projects are encouraged to use these where they fit and document custom aliases in their README. See local-config.md for the binding mechanism.
system_prompt (string, required)
A path to a markdown file containing the agent's system prompt. Uses a URI prefix (project:/ or config:/) per ADR-0015.
- Required.
- Path rules:
- Must use a URI prefix. Naked paths are rejected at parse time.
- Must NOT escape the project root via
..segments.
- Validation at parse time: the parser checks the path's shape (prefix, no
..escape). File existence is checked at project-load time (so a project being authored can validate without prompt files yet existing); a missing prompt at session-start fails the session, not the parse. - Format: Markdown, optionally with YAML frontmatter. Frontmatter is opaque to the DSL parser; the daemon's prompt loader handles it. See prompts spec (not yet written).
cage (object or string, required)
The cage policy. See cage block below.
Two forms are accepted:
- Object form — a full cage block declaring
fs,net,state, optionalseccompandlimits. This is the default and expected shape. - String form — the literal value
disabled.
# object form (the default)
cage:
fs: []
net:
allow: []
state: ephemeral
# string form — opt out of sandboxing entirely
cage: disabled
Rules:
- Required field on every agent. You must write
cage:even when you want it disabled. There is no implicit cage — silence is never the policy. There are no project-level cage defaults; each agent declares its own cage independently. - Root agent interim restriction. Until the supervisor supports caging the primary process, the only legal value for the root agent's
cageisdisabled. The parser emits a parse-time error for any other value on the root agent. Non-root agents accept both forms. - No "wide-open" shortcut for the object form. To grant broad access via a real cage, the operator must write it out (
fs: [{mode: rw, path: /}]etc.), which is intentionally ugly. Operators who want full host access should usecage: disabledinstead — it's honest about what's happening. cage: disabledsemantics (per ADR-0009 amendment):- The supervisor spawns the agent as the daemon's UID with no
bwrapwrapper, no namespacing, no seccomp filter, no cgroup limits beyond the daemon's own. - The agent has full host filesystem read-write access (whatever the daemon user has).
- The agent can reach any network destination the daemon user can reach.
- The agent runs in the daemon's process tree (it can be signaled by other processes the daemon user owns).
- The honest framing: an uncaged agent IS your daemon's hands. Same UID, same access, same blast radius.
- The supervisor spawns the agent as the daemon's UID with no
- No cage inheritance. Each agent declares its own cage; there is no inheritance between parent and child cages because cages are per-process and a child runs in its own sandbox context. The removed
cage_defaultsis not replaced by any implicit defaulting mechanism. - Parse-time warning. The DSL validator emits a warning (not an error) for every
cage: disabledentry on a non-root agent. The warning text names the agent and links to ADR-0009. (The root agent'scage: disabledis the only legal value in the interim — no warning.) - Runtime visibility. The web UI shows uncaged agents with a magenta
[UNCAGED]badge instead of the standard[CAGED]badge. Every spawn of an uncaged agent emits anagent.spawn.uncagedaudit event. - Daemon-wide override: when the daemon runs with
--no-sandbox(ADR-0009), every agent's cage is treated asdisabledregardless of what its DSL declares. The DSL is still parsed and validated (so the file remains portable to a sandboxed daemon) but the cage block is not enforced.
description (string, optional)
Human-readable description of the agent. Used in the UI and as the tool description when this agent is presented to its parent as a synthetic agent-<key> tool.
- Optional. Default: empty string.
- Max length: 280 characters.
parameters (object, optional)
Model-specific parameters (temperature, max_tokens, top_p, etc.). Opaque to the DSL parser. The daemon passes them through to the model provider.
- Optional. Default: provider defaults.
- Schema: any object. The daemon validates against the specific provider's accepted parameters at session-start time.
max_steps (integer, optional)
Maximum number of tool-call rounds the agent loop is allowed before stopping. Passed to the Mastra Agent.stream() execution options.
- Optional. Default: provider/model defaults (Mastra v1.36.0 default is 5; kaged recommends 20 for complex tasks).
- Schema: integer,
1–100. Values outside this range are rejected at parse time. - Per-agent. Each agent in the tree can set its own limit. Subagents inherit from parent unless explicitly overridden.
max_output_tokens (integer, optional)
Maximum number of output tokens the model may generate in a single completion. This is the output token budget, not the context window. Passed to the provider as max_tokens (or provider-specific equivalent).
- Optional. Default: provider defaults (Fireworks default for
kimi-k2p6-turbois ~1024; kaged recommends 4096+ for tool-heavy agents). - Schema: integer,
1–65536. Values outside this range are rejected at parse time. - Per-agent. Each agent can set its own budget. A thinking model (e.g., kimi) may consume thinking tokens from this budget before emitting visible text.
- Note: This is a request to the provider. The provider may stop earlier (finish reason
stop) or hit this ceiling (finish reasonlength). The UI surfaces the actual stop reason so the operator can adjust.
tools (object, optional)
Per-agent tool configuration overrides. Keys are tool names (dot-delimited, e.g. file.read, code.lsp) or glob patterns (debug, *). Values are either a ToolOverride object or null (ADR-0015 nullification).
This field replaces the former project-level tools: block. Each agent declares its own tool surface.
tools:
"file.read":
enabled: true
description: "Read file contents (project-scoped)"
"debug":
enabled: false # disable this tool for this agent
"search.grep":
parameters:
max_results: 500 # merge into default parameters
"code.lsp": null # nullify — reset any inherited overrides
- Optional. If absent, tool defaults apply based on the agent's position (see Tool resolution below).
- Keys are dot-delimited tool names matching the tool registry's naming convention (
<namespace>.<tool>). Seeagent-tooling.mdfor the full built-in catalog. - Values are either:
ToolOverrideobject — with any subset of:enabled(boolean),description(string),parameters(object).null— ADR-0015 nullification. Resets any overrides, restoring the tool to its built-in defaults.
kaged.issue.*andkaged.workflow.*tools carry aprincipal_scope: "root-only"tag. The schema rejects these tools on non-root agents. Attempting to enablekaged.issue.createon a subagent is a parse error.
Tool resolution
Tool resolution applies to every agent in the tree, not just the root. The layered order is the same at every level (later wins):
- Available tools — the set of tools registered in the runtime's
ToolRegistry(all 19 built-in tools across 9 namespaces:file,edit,search,code,debug,shell,compute,project,kaged). Only tools actually registered can appear in the resolved set. - Default-enabled base state — the root agent starts with
kaged.*tools enabled (DEFAULT_ENABLED_TOOLS); all non-root agents start with nothing enabled. This is the key difference: subagents must explicitly opt in to every tool via their owntools:block or operator config. - Operator-level overrides — from
local.toml[default_tools](seelocal-config.md). Applied system-wide to all agents. Use{ enabled: true }to opt in a disabled-by-default tool, ornull/{ enabled: false }to disable a default-enabled tool. - Agent-level overrides — the agent's own
tools:block (this field). Per-agent policy; overrides operator defaults. - Cage filter at dispatch — even if a tool is enabled,
dispatch()still checks cage permissions before execution. A caged agent callingfile.writeon a path outside itscage.fsmounts getscapability_denied.
When availableTools is provided to compileProjectDsl(), the compiler materializes a tools: { <name>: { enabled: true/false } } block on every agent in the tree — root, inline subagents, and project-reference primaries. This materialized view is the "total render" visible in the synthesized endpoint.
Resolution is performed by resolveRootTools() in @kaged/dsl (exported from packages/dsl/src/defaults.ts). The function accepts the available tool names, an ordered list of override layers, and a defaultEnabled set (root uses DEFAULT_ENABLED_TOOLS, subagents use an empty set). It returns the filtered list of enabled tool names.
The compileProjectDsl() function accepts optional availableTools and operatorToolOverrides in its options. When provided, the compiled result includes resolvedRootTools: string[] — the effective root-agent tool list. When omitted, resolvedRootTools is null. The synthesized endpoint passes DEFAULT_ROOT_TOOLS as availableTools and the operator's default_tools as operatorToolOverrides, so resolved_tools is always populated in its response.
Future: plugins will inject additional tools into the available set before compilation. The availableTools parameter accommodates this — the daemon pre-merges plugin-provided tools into the registry before passing names to the compiler.
This separation is intentional: tools: is the operator's per-agent policy; cage: is the operator's security boundary.
include_tool_results_in_context (boolean, optional, default true)
Controls whether prior tool calls and their results are included in the LLM conversation context when reconstructing message history for subsequent turns.
- Default:
true. Tool calls (with arguments) and tool results (with output) from prior turns are included in the message history sent to the LLM, giving the model full visibility into what tools were called and what they returned. - When
false: Only the text content of prior assistant messages is included. Tool calls and results are stripped from the context. This reduces token usage at the cost of the model losing awareness of prior tool interactions. - Per-agent. Each agent in the tree can set this independently. A parent agent might include tool results while a cheap subagent omits them to save tokens.
- Future: This field is the natural hook for context compaction strategies — a future processor could selectively summarize or truncate tool results based on age, size, or relevance.
plugins (named-object map, optional)
The project-level plugin registry. Every plugin used in the project MUST be declared here first. This is the gateway — it makes the plugin available to agents, system services, and the UI. Per-agent plugins: blocks only override or disable; they can never introduce a plugin the registry doesn't know about.
plugins:
memory:
package: "@kaged/memory-markdown"
source: "npm:@kaged/memory-markdown"
enabled: true
config:
isolation: project
max_entries: 1000
audit:
package: "@example/audit-log"
source: "github:example/audit-log"
enabled: false
config:
log_level: info
Keys are operator-chosen plugin slot names (slug format
^[a-z][a-z0-9_-]*$). The slot name is independent of the package name — a project may have multiple plugins of the same package under different slots, though this is rare. Per ADR-0015, setting a key tonullinproject.local.yamlremoves the plugin (operator opt-out).package(string, required) — the plugin package identifier (e.g.@kaged/memory-markdown). Resolved at project-load time against the operator's local plugin store; missing plugins trigger the install prompt perplugin-host.md § Install flow.source(string, optional) — where to fetch the plugin if it's not installed. Uses magic prefix conventions:npm:<package>— install from npm registrygithub:<owner>/<repo>— clone from GitHubproject:/<path>— relative to the project rootconfig:/<path>— relative to the operator's config directorygit:<full-url>— clone from any Git URL (SSH or HTTPS)
If
sourceis omitted and the plugin isn't installed, the daemon reports it as missing but cannot offer to install it.enabled(boolean, optional, defaultfalse) — the project-wide default. Whentrue, the plugin is active for agents that don't override. Whenfalse, agents must explicitly setenabled: trueto use it.config(object, optional) — the plugin's project-side configuration. Opaque to the DSL parser; validated by the plugin'sconfig_schemafrom its manifest at project-load time. Project-committed; never contains secrets (per ADR-0023 project/system config split).- Operator-local secrets for the same plugin live in
local.toml [plugins."<package>"]perlocal-config.md § Plugin system config.
- Operator-local secrets for the same plugin live in
Per-agent plugin overrides
Agents reference plugins by their registry slot name. The per-agent block overrides enabled, hooks, and config:
primary:
plugins:
memory:
enabled: true
hooks: [ on_session_start, on_session_idle ]
config:
store: "config:/memory"
audit:
enabled: false # opted out
subagents:
researcher:
plugins:
memory:
enabled: true
hooks:
- pre_compact
- post_compact
config:
isolation: agent # override: this agent gets its own memory
Per-agent slot names must reference a key in the project-level registry (cross-reference validation error if not).
enabled(boolean, optional) — overrides the registry default for this agent.hooks(list of strings, optional), which are lifecycle hooks this plugin subscribes to on this agent. Allowed values:on_session_start,on_session_idle,pre_compact,post_compact. Seeplugin-host.md § Lifecycle hooks.on_session_startandon_session_idleonly fire on the primary (per ADR-0023 — sessions are primary-owned). Declaring these on a subagent is permitted in the DSL but produces a warning at load time and the hook never fires.pre_compactandpost_compactfire per-agent.
config(object, optional) — deep-merged on top of the registry config. Allows per-agent config overrides without repeating the entire config block.Tool registration. Tools declared in the plugin's manifest are auto-enabled on every agent that enables the plugin. The agent's
tools:block can disable specific plugin tools as an escape hatch (perplugin-host.md § Plugin tool naming):
primary:
plugins:
memory:
enabled: true
tools:
"memory-markdown.recall": { enabled: false } # last-resort disable
- Max entries: 16 plugins per agent.
compaction (object, optional)
Per-agent context compaction configuration. Per ADR-0024, each agent has its own context window and its own compaction events. Subagents inherit defaults from their parent if unset; the parser materializes the resolved per-agent compaction at compile time so the synthesized DSL endpoint shows the effective value.
primary:
compaction:
strategy: summarize
upper_threshold: 0.85
lower_threshold: 0.60
summarize:
model: cheap-summarizer
window_messages: 20
preserve_recent: 10
prompt: project:/prompts/compactor.md
always_keep: []
Optional. If absent on an agent, the agent inherits its parent's compaction config. If the entire tree has no
compaction:declared, kaged falls back to:strategy: drop upper_threshold: 0.85 lower_threshold: 0.60 always_keep: [] summarize: prompt: config:/prompts/compaction-summary.md window_messages: 20 preserve_recent: 10 max_summary_tokens: 1500The default includes a
summarizeblock so thedropstrategy can attempt summarization before falling back to pure drop (see § Default compaction summary prompt).strategy(enum, default'drop') — one of'drop','summarize','delegate','checkpoint'. Seeagent.md § Compactionfor full semantics of each.upper_threshold(number, 0.0..1.0, default0.85) — fraction of the model's context window at which compaction triggers. Crossing this fires the compaction pipeline.lower_threshold(number, 0.0..1.0, default0.60) — fraction the strategy compacts down to. Hysteresis: prevents oscillation when the message list is near the upper bound. Must be <upper_threshold(validation error if not).always_keep(list of strings, optional) — operator-configured always-keep predicates. Each entry is a predicate identifier the harness recognizes (e.g.first_user_message,messages_with_tag:critical). The harness's defaults — system prompt and first operator message — are always included regardless. Seeagent.md § Always-keep set.
Strategy-specific fields
summarize (object, required when strategy: summarize):
summarize:
model: <model-alias> # optional — alias resolved per local-config; when omitted, uses the agent's own model
window_messages: 20 # optional, default 20 — how many messages to compress per event
preserve_recent: 10 # optional, default 10 — always keep N most recent intact
prompt: config:/prompts/compaction-summary.md # optional, default config:/prompts/compaction-summary.md — summarizer prompt
max_summary_tokens: 1500 # optional, default 1500 — budget for the summary itself
modelis optional. When omitted, the summarizer uses the agent's own model alias. Operators who want a cheaper model for summarization can set this explicitly.promptis optional with a default ofconfig:/prompts/compaction-summary.md. This file is auto-created bykaged project initif it does not exist (see § Default compaction summary prompt). Operators can override it with aproject:/path for project-specific prompts.
The summarizer model is invoked via the standard provider router (same path as the primary). Cost is tracked separately in the session's stats (per agent.md § Cost surfacing).
delegate (object, required when strategy: delegate):
delegate:
plugin: <plugin-slot-name> # required — the name key from `plugins:` declaration above
fallback_strategy: drop # optional, default 'drop' — what to do if the plugin fails
The named plugin must be declared in this agent's plugins: block AND must claim role: compactor in its manifest. Parse-time validation enforces both.
checkpoint (object, optional when strategy: checkpoint):
checkpoint:
fallback_strategy: drop # optional, default 'drop' — what to apply if operator approves without editing
auto_resume_timeout_sec: null # optional, default null (wait indefinitely)
When the strategy is checkpoint, the session pauses; the operator inspects the proposed compaction in the Compactor UI (per ui/compactor.md) and approves, edits, or rejects.
Inheritance
Subagents inherit their parent's compaction: block. Override is field-level: any field specified on the subagent's compaction: overrides the same field; non-specified fields inherit:
primary:
compaction:
strategy: summarize
summarize: { model: cheap-summarizer, ... }
subagents:
researcher:
compaction:
# inherits primary's summarize.{model, ...} and thresholds
upper_threshold: 0.75 # researcher compacts earlier (cheap model with smaller window)
Default compaction summary prompt
Every kaged project ships a default compaction summary prompt at config:/prompts/compaction-summary.md. This is the prompt used when the summarize strategy (or the drop-with-summarize-fallback) invokes a model to compress older messages into a structured handoff.
Auto-creation. kaged project init scaffolds this file into .kaged/prompts/compaction-summary.md alongside the existing default.md system prompt. If the file is missing when a project is loaded (e.g. operator deleted it, or an older project predates this feature), the daemon writes the default content at project-load time. The operator is free to edit the file after creation — it is operator-owned config, not a managed artifact.
Default content. The default prompt produces a structured context checkpoint handoff with these sections:
- Goal — user goals; list multiple if session covers different tasks.
- Constraints & Preferences — constraints or requirements mentioned.
- Progress — Done (completed tasks/changes), In Progress (current work), Blocked (issues preventing progress).
- Key Decisions — decisions with brief rationale.
- Next Steps — ordered list of next actions.
- Critical Context — important data, pending questions, references.
- Additional Notes — anything else important not covered above.
The prompt instructs the summarizer to preserve exact file paths, function names, error messages, and relevant tool outputs. If the conversation ends with an unanswered question or pending request, the prompt requires that exact question to be preserved in the summary.
Override. Operators can point compaction.summarize.prompt to any project:/ or config:/ path. The default is used only when no explicit prompt is set (or when it is set to the default config:/prompts/compaction-summary.md).
Dogfood. The kaged-on-kaged config (.kaged/prompts/compaction-summary.md in this repo) uses the same default prompt.
subagents (named-object map, optional)
A map of child agents this agent may dispatch, keyed by name. Order is not significant. Child agents are spawned on demand by their parent, not at session-start.
- Optional. If absent or empty, the agent has no children — it is a leaf node.
- Max entries: 64 per agent (per level).
- Keys are agent names — uniqueness is structural (duplicate YAML keys are a parse error).
- Override semantics: in
project.local.yaml, setting a key tonullremoves that agent from the merged result (ADR-0015 nullification). - Recursive nesting.
subagentsmay itself contain agents with their ownsubagents. Depth is bounded at 16 levels (same as the existing project-reference depth limit per ADR-0015). The tree is the call graph: a parent agent can call its direct children; sibling and cross-tree calls do not exist.
Agent key (string, required)
The agent's name within its parent's scope. Used in the UI, in the audit log, and as the synthetic tool name (agent-<key>) the parent's LLM sees.
- Pattern:
^[a-z][a-z0-9_]{0,30}[a-z0-9]$— lowercase letters, digits, and underscores; 2-32 chars; starts with a letter; cannot end with underscore. - Reserved names:
primary,operator,system. Using these is a parse error.
Each value under subagents is either an AgentSpec (same recursive shape) or a ProjectReference (see Project-reference subagents).
Default cage profile
When an agent's cage object form does not specify optional fields (seccomp, limits), kaged applies:
seccomp: default
limits:
memory_mb: 256
cpu_shares: 1024
pids: 64
walltime_sec: 600
The required cage fields (fs, net, state) have no implicit defaults — each agent must declare them explicitly. Maximally restrictive starts with fs: [], net: { allow: [] }, state: ephemeral — the cage starts with nothing; every grant is explicit.
Project-reference subagents
A subagent value may be a project reference instead of a full subagent declaration. The parent project points at a nested kaged project, and the nested project's primary is exposed to the parent as a callable subagent.
This is the surface implementation of ADR-0015 §7 ("Compiled Contextualization") and §6 ("Cross-Project Injection"). It is how a parent project composes other projects as units of work, without coupling either project to the other's internals — each project remains a self-contained silo per ADR-0015 §1.
subagents:
# AgentSpec form (inline agent)
scraper:
model: low-cost-fast
system_prompt: project:/prompts/scraper.md
cage:
fs: []
net: { allow: [example.com] }
state: ephemeral
tools:
"search.grep": { enabled: true }
# Project-reference form
builder:
path: project:/sub/frontend-builder
name: ui_builder # optional: tool name presented to the LLM
description: Builds the frontend # optional: description presented to the LLM
overrides: # optional: partial ProjectDsl deep-merged on top
primary:
model: smart-careful
Discriminator
The parser distinguishes the two forms by the presence of path:.
path:absent → value is anAgentSpec. Must satisfy the full AgentSpec schema (model,system_prompt,cage).path:present → value is aProjectReference. Must satisfy the project-reference schema below.model,system_prompt,cage, andtoolsare forbidden at this layer — they belong to the nested project's own DSL.
Mixing fields from both shapes (e.g. path: and cage: on the same entry) is a parse error.
subagents.<name>.path (string, required for project references)
URI-prefixed path to the nested project's root directory (the directory containing .kaged/project.yaml).
- Required for the project-reference form. Its presence is what selects this shape.
- Accepted prefixes in v1:
project:/only. The path must resolve to a directory inside the parent project's root, per ADR-0015 §1 ("project root is the ceiling").config:/is rejected (the nested project is project content, not configuration metadata). - Other prefixes (
git:/,https:/,file:/) are explicitly deferred. They depend on ADR-0015 §4 ("security ceiling") which is currently unimplemented perfederated-config.md. A later amendment lifts this restriction. - The resolved path must contain
.kaged/project.yaml. Existence is checked at project-load time, not parse time. Parse time validates only the URI shape. - No
..escape, per the standard URI rules infederated-config.md. - A nested project cannot reference the parent or any ancestor. Cycle detection at compile time (see Compilation and cycles below).
subagents.<name>.name (string, optional)
Override the tool name the subagent is presented as to the LLM. Since every subagent surfaces to the model as a tool, this is purely a presentation hint.
- Optional. Default: the nested project's
project:slug. - Pattern: the same subagent-name pattern (
^[a-z][a-z0-9_]{0,30}[a-z0-9]$). Reserved names (primary,operator,system) are rejected. - Does NOT change the map key. The map key (
builderin the example above) remains the canonical local identifier — it is what audit logs record, what the operator reviews.name:only affects the string the LLM sees. - Rationale: the operator wants short, model-friendly tool names; the operator also wants project-meaningful map keys. These goals don't always agree. Splitting them keeps both honest.
subagents.<name>.description (string, optional)
Override the tool description presented to the LLM.
- Optional. Default: the nested project's top-level
description:field (or empty if absent). - Max length: 280 characters, matching the project-level
descriptionconstraint. - Plain text. No markdown rendering.
subagents.<name>.overrides (object, optional)
A partial ProjectDsl deep-merged on top of the nested project's own resolved config, per ADR-0015 §2 merge semantics.
- Optional. Default: no overrides; the nested project loads as-is.
- Merge order (low → high precedence):
- Nested project's
.kaged/project.yaml - Nested project's
.kaged/project.local.yaml(if present on the operator's machine) - Parent's
subagents.<name>.overridesblock
- Nested project's
- Deep merge with nullification. Same algorithm as
federated-config.md§ Configuration merging. Setting a key tonullremoves it; objects deep-merge; arrays replace. versionandprojectare forbidden in overrides. Project identity cannot be re-stamped by the parent (same rule asproject.local.yaml).- Schema validation runs on the merged result, not on the partial override. The override alone need not be a valid
ProjectDsl; the merged output must. - No cage widening shortcut. If the parent wants to tighten a nested agent's cage, it does so explicitly via
overrides.primary.cageoroverrides.primary.subagents.<nested-name>.cage. Each agent in the nested project declares its own cage; there is no inheritance to manipulate. See Cage policy for nested projects below. - Path references inside overrides resolve against the nested project's root, not the parent's.
system_prompt: project:/prompts/x.mdin an override points at<nested-root>/prompts/x.md. Otherwise the override would break the nested project's portability.
Cage policy for nested projects
Nested-project agents keep their own cages, declared in the nested project's own DSL. Each agent declares its own cage independently; there is no inheritance between parent and child cages. The project-reference itself has no cage: field.
This preserves ADR-0011 portability (the nested project is self-contained and runnable in isolation) and is consistent with ADR-0015 §4's "security ceiling" direction (a parent should not be able to widen a child's policy; if the ceiling were enforced it would be expressed via overrides, not via implicit defaulting).
If the parent wants to tighten a nested agent's cage for this composition, it does so via overrides:
subagents:
builder:
path: project:/sub/frontend-builder
overrides:
primary:
subagents:
# The nested project declares `compiler` with broad net access;
# the parent restricts it for this composition.
compiler:
cage:
net:
allow: [] # no network for this composition
A future amendment, once ADR-0015 §4 lands, will formalize the security ceiling so the parent's tightening is enforced even if a malicious overrides block tries to widen instead.
Cross-references and call graph
Project-reference agents participate in the parent agent's call graph by map key. The tree structure is the call graph per ADR-0022: a parent agent can call its direct children; sibling and cross-tree calls do not exist.
- The project-reference has no
can_be_called_byfield — call capability is implicit from the tree position. The parent agent that declares the project-reference as a child can call it. - The project-reference's nested project's primary becomes this entry's
AgentSpec, with the nested project's subagents becoming this entry's subagents, recursively. After flattening, the shape is uniform top-to-bottom.
Compilation and cycles
At project-load time, the daemon performs a compilation pass per ADR-0015 §7: it walks the project-reference tree, resolves each nested project, and produces a single in-memory manifest the runtime uses to dispatch.
Algorithm
The compiler is a recursive walk over the parent's subagents map. For each entry that satisfies isProjectReference, the compiler:
- Resolves the path.
path: project:/sub/builderis resolved against the current project's root (not the topmost ancestor) per ADR-0015 §1 silo boundary. Result: an absolute filesystem path to the nested project's root directory. - Reads the nested project. Looks for
<resolved-root>/.kaged/project.yaml. If missing, compilation fails with anested_project_missingdiagnostic naming the offending map key and the resolved path. - Loads the nested overlay. If
<resolved-root>/.kaged/project.local.yamlexists, it is layered on top via the standardloadProjectDsloverlay merge. - Applies the parent's
overridesblock. The reference'soverrides(if any) is deep-merged on top of the nested project's already-resolved DSL using ADR-0015 §2 semantics (deep merge,nullnullifies,versionandprojectrejected). Schema validation runs on the merged result. - Recurses. The compiler walks the nested project's own
subagentsmap. Project-refs there are resolved against the nested project's root.
The compiled output is a uniform AgentSpec subtree: the nested project's primary becomes this entry's AgentSpec, with the nested project's subagents becoming this entry's subagents, recursively. The original ProjectReference metadata (path, name, description, overrides) is retained as a _source annotation alongside the compiled AgentSpec so downstream consumers can trace each entry back to its declaration. After compilation, the tree is AgentSpec top-to-bottom — there is no ProjectReference vs AgentSpec distinction in the compiled manifest.
Cycle detection
- Arbitrary depth supported. Project A may reference B, which may reference C, and so on.
- Cycle detection at compile time. The compiler tracks visited absolute root paths along the current walk. If a path is re-entered, compilation fails with a
compile_cyclediagnostic naming the chain (e.g.A (/foo) → B (/foo/sub) → A (/foo)). The parent project entersinvalidstate. - Depth limit. Compilation aborts with a
compile_depth_exceededdiagnostic when the walk exceeds 16 levels. Configurable for tooling that needs deeper introspection; the daemon ships with the default. The limit guards against pathological non-cyclic chains and bounds load time.
Cage policy is not rebased
Cage paths inside a nested project remain project-root-relative to the nested project's own root, exactly as authored. cage.fs[].path: data in nested project B resolves to <B-root>/data at runtime, not <parent-root>/.../data. The compiled tree carries the nested DSL verbatim; the runtime mount resolver applies the nested root.
This preserves the portability promise: a nested project that runs standalone uses the same paths it does when composed into a parent.
Failure semantics
When any step above fails, compilation surfaces a DslError carrying DslDiagnostic[] with one entry per failure. The diagnostic includes:
- The offending map key path (e.g.
primary.subagents.builderfor a top-level ref,primary.subagents.builder.subagents.innerfor a deeper failure). - The resolved filesystem path involved.
- The underlying cause (missing file, parse error, schema-validation failure of the merged result, cycle path, depth limit, override violation).
The GET /api/v1/projects/:id/dsl/synthesized endpoint surfaces these as a 422 response per http-api.md. Project status at the daemon's registry transitions to invalid when the compilation step itself fails (as opposed to the file being missing on first load, which is pending).
Other compile-time properties
- State is siloed per ADR-0015 §7. When the nested project runs as a subagent of the parent, the nested project's conversation logs and ephemeral state are not inherited; the nested project operates under the parent's session state. The nested project's own session state (when it is run standalone) is unaffected.
- A nested project does not see its parent. The nested project's DSL has no field referring to the parent and no way to introspect it. This is the portability promise of ADR-0011.
What this is not
- Not a hot-reload mechanism. Changing the nested project's DSL while the parent's session is running does not propagate until the parent session restarts (or a hot-reload story is specified in
daemon.md). - Not a cross-machine reference. Nested projects in v1 live inside the parent project's root tree. Remote references (
git:/,https:/) are deferred. - Not parent injection. ADR-0015 §6 describes a complementary mechanism where a parent injects virtual subagent definitions into a child. That is not what this section specs — that mechanism is still deferred per
federated-config.md.
Cage block
The cage block declares filesystem, network, state, seccomp, and resource policy. It is the operator-readable contract enforced by the sandbox supervisor (sandbox.md).
cage:
fs: # required, list (may be empty)
- mode: ro|rw
path: /abs/path
net: # required
allow: [<hostname-glob>, ...]
state: ephemeral|scratch # required
seccomp: default|relaxed # optional, default: default
limits: # optional
memory_mb: 256
cpu_shares: 1024
pids: 64
walltime_sec: 600
cage.fs (list, required)
Filesystem mount declarations. Each entry is a {mode, path} object.
- Required field, but may be empty (
fs: []= no host filesystem visible; cage sees only its own tmpfs). - Entries are evaluated in order. Later entries override earlier ones if paths conflict.
- Modes:
ro— read-only bind mount into the cage.rw— read-write bind mount. The subagent can modify the file at the resolved path.
pathis always relative to the project root (per ADR-0011):- Absolute paths (
/etc/foo,/srv/anything) are rejected at parse time. Projects are portable; absolute host paths are operator-machine-specific. To grant access outside the project root, see "What if a subagent needs outside-root access?" below. - Path must NOT escape the project root via
..segments. - Leading
./is optional../dataanddataresolve identically. - The resolved path is
<project-root>/<path>. Daemon checks existence at session-start (not parse-time, because paths may be created by earlier subagent runs or by operator setup).
- Absolute paths (
- No
~expansion. Paths are project-relative;~would be a host concept the project cannot meaningfully assume. - No environment variable interpolation in
path. Security boundary (see ADR-0006).
What if a subagent needs outside-root access?
Three options, in order of preference:
- Restructure the project so the data the subagent needs is inside the project root (move it in, symlink it in at the local-machine level, copy it). Keeps the project portable.
- Local symlink. The operator symlinks an external path into the project root on their machine (
ln -s /etc/kubeconfig ./.local-only/kubeconfig). The DSL only knows about the project-relative path. Other operators receiving the project create their own equivalent symlink (or don't, if they don't have that local resource — the subagent then fails on missing path, which is honest). cage: disabledon that subagent (per ADR-0009 amendment). The subagent runs as the daemon UID with full host access. Visible in the DSL; operator-reviewable.
There is no fourth option that makes a non-portable subagent secretly portable. If a subagent needs /etc/kubeconfig, it's not portable, and that's a visible operator decision in the project file.
cage.net (object, required)
Network policy.
- Required.
allow(list of strings, required) — hostname or hostname:port allowlist. Empty list (allow: []) means no network access at all.
Hostname syntax:
| Pattern | Matches |
|---|---|
example.com |
exactly example.com, any port |
example.com:443 |
exactly example.com on port 443 only |
*.example.com |
any single subdomain of example.com (e.g. api.example.com) — does not match example.com itself or a.b.example.com |
**.example.com |
any depth of subdomain of example.com |
10.0.0.0/8 |
CIDR — explicit IP range (use sparingly; prefer hostnames) |
localhost:6443 |
the cage's own loopback on port 6443 (note: localhost in a netns is the cage, not the host) |
- Resolution and filtering happen in the network gatekeeper at the cage layer (
sandbox.md). The DSL only declares the allowlist; the gatekeeper enforces it. - DNS: allowlisted hostnames are resolved by a kaged-managed resolver. Non-allowlisted DNS queries fail with NXDOMAIN.
- Port restriction: if no
:portis given, all TCP ports to that hostname are allowed. If a port is given, only that port.
cage.state (enum, required)
Lifecycle of the cage's own filesystem (tmpfs, scratch space).
ephemeral— the cage's writable scratch (/tmp,/work, anything not mounted from host) vanishes when the subagent exits. The default for unit-of-work subagents.scratch— the writable scratch persists between invocations within the same session, but is wiped when the session ends.- No
persistentvalue in v1. True cross-session persistence requires the operator to declare anrwmount, which is explicit and auditable.
cage.seccomp (enum, optional)
Seccomp policy.
default(default) — the kaged-shipped conservative profile is applied. Blocksptrace,kexec_load,init_module,keyctl, hostmount, and similar host-impacting syscalls.relaxed— only catastrophic syscalls (reboot,kexec_load) are blocked. Use only when a subagent fails underdefaultand the operator has audited why.
There is no per-syscall override in v1. If the default profile bites a legitimate workload, the path forward is either (a) document the issue and we update the default, or (b) the operator opts into relaxed and accepts the wider surface.
cage.limits (object, optional)
Resource limits. Enforced via cgroups (see ADR-0009 and sandbox.md).
memory_mb(integer) — memory cap in megabytes. Default: 256.cpu_shares(integer) — relative CPU weight. Default: 1024.pids(integer) — max process count inside the cage. Default: 64.walltime_sec(integer) — wall-clock timeout. After this, the supervisor SIGTERMs, then SIGKILLs after a 5s grace. Default: 600 (ten minutes).- A subagent that exceeds a limit is killed; an audit event records which limit was hit.
plugins (project-level registry, reinstated)
The top-level plugins: block was temporarily removed by ADR-0023 in favor of per-agent-only declarations. It has been reinstated as the project-level plugin registry. The rationale:
- Not all plugins are agent-related — some are UI plugins, task runners, or project-scoped services.
- A project-level registry provides a single place to declare
source(where to install from) and project-wide defaults. - Per-agent blocks now only override or disable; they cannot introduce new plugins.
Migration from per-agent-only (ADR-0023 era):
Per-agent-only:
primary:
plugins:
memory:
package: "@kaged/memory-markdown"
hooks: [on_session_start]
config: { store: "config:/memory" }
Registry + per-agent override:
plugins:
memory:
package: "@kaged/memory-markdown"
source: "npm:@kaged/memory-markdown"
enabled: true
config: { store: "config:/memory" }
primary:
plugins:
memory:
enabled: true
hooks: [on_session_start]
Install-time behavior: when the daemon loads the project and encounters a plugin in the registry, it checks the operator's local plugin store. Missing plugins with a source field trigger the install prompt. Missing plugins without source are reported but cannot be auto-installed.
| Local-store state | Registry says | Daemon behavior |
|---|---|---|
| Not installed | source: present |
Prompts operator: "Install plugin X from source Y?" Operator approves → install. Declines → plugin stays pending. |
| Not installed | source: absent |
Reports as missing; no install prompt. |
| Installed | matches | Activate; no prompt. |
tasks (named-object map, optional)
Operator-declared runnable commands for the project — build scripts, test suites, dev servers, deploy commands. Tasks appear as one-click buttons in the project UI. The full task-runner subsystem is specified in task-runner.md; this section covers the DSL surface only.
tasks:
test:
command: bun test
description: Run the test suite
group: ci
dev:
command: bun run dev
description: Start the dev server
group: dev
long_running: true
deploy-staging:
command: ./scripts/deploy.sh staging
description: Deploy to staging
group: deploy
confirm: true
db-migrate:
command: bun run db:migrate
description: Run database migrations
cwd: packages/api
- Keys are task names (slugs). Pattern:
^[a-z][a-z0-9_-]{0,30}[a-z0-9]$. Reserved names (adhoc,all,new) are parse errors. Setting a key tonullinproject.local.yamlremoves the task (ADR-0015 nullification). - Max entries: 64 per project.
- Optional. Absence means no named tasks; the operator can still run ad-hoc tasks via the UI.
tasks.<name>.command (string, required)
The shell command to execute. Passed verbatim to the system shell — no DSL-level interpolation.
tasks.<name>.description (string, optional)
Human-readable description shown in the UI. Max 280 characters.
tasks.<name>.group (string, optional)
Organizational group for UI display. Tasks in the same group are visually clustered. Pattern: ^[a-z][a-z0-9_-]{0,30}[a-z0-9]$. Does not affect execution.
tasks.<name>.cwd (string, optional)
Working directory override, project-relative. Default: project root. Same path rules as cage.fs[].path — no absolute paths, no .. escape. Existence checked at task-launch time, not parse time.
tasks.<name>.long_running (boolean, optional)
Hint that this task is a long-lived process (dev server, watcher). Default: false. Affects UI only (stop button vs. "waiting for exit").
tasks.<name>.confirm (boolean, optional)
Prompt the operator for confirmation before launching. Default: false. Use for destructive or expensive operations.
tasks.<name>.env (object, optional)
Extra environment variables. Merged on top of the daemon's environment (additive, not replacing). Values are strings; no interpolation.
Validation timing
The DSL is parsed and validated at four moments. Each is intentional; conflating them would hide errors or fail healthy projects.
| When | What's checked | Failure mode |
|---|---|---|
kaged dsl validate <file> (CLI, on demand) |
Schema, alias name shape, path shape (relative, no ..), agent tree depth limit (≤16), principal_scope tag enforcement (kaged.issue.*/kaged.workflow.* rejected on non-root agents), root agent cage must be disabled |
Exit code non-zero with line/col |
Daemon project-load (POST /api/v1/projects/load) |
All of the above, plus: alias resolution against operator's local config, plugin presence in local store, prompt-file existence, project-reference compilation (cycles, depth, nested DSL validity) | Project enters pending or invalid per local-config.md; not a hard daemon failure |
| Daemon startup for known projects (re-evaluation) | Same as project-load | Project's state is updated in the registry; daemon stays up |
| Session-start | All of the above, plus: model provider reachable, plugin processes can spawn, cage path existence inside project root | Session refuses to start with specific error; project state unchanged |
The first is purely file-level (an author validating their DSL).
The second pulls in the operator's local config (does this operator have the aliases bound, are the plugins installed). A pending project is not a broken project — it's a project that needs operator setup. The UI surfaces what's missing.
The third runs at daemon startup so the project list in kaged status and the UI reflects reality even after operator local config changes.
The fourth runs at the moment a session starts and pulls in runtime state (LLM provider reachable, plugin process healthy, host paths exist).
Error messages
Errors are human-first. Every error includes:
- The file and line number of the offending construct (YAML parsers track this; we preserve it).
- The kind of error (schema violation, cross-reference miss, cycle, etc.).
- The expected shape, where applicable.
- A pointer to the relevant doc (
see docs/specs/project-dsl.md#field).
Examples:
.kaged/project.yaml:14:9
primary.subagents.scraper.cage.fs[0].path: required path "/data" does not exist on this host
Sandbox spawn requires all mount sources to exist.
see docs/specs/project-dsl.md#cagefs
.kaged/project.yaml:22:5
primary.subagents.scraper.tools."kaged.issue.create": tool "kaged.issue.create" carries
principal_scope "root-only" and cannot be enabled on non-root agents.
see docs/specs/project-dsl.md#tool-resolution
.kaged/project.yaml: top-level field "subagnets" is unknown (did you mean "subagents"?)
Unknown fields are errors in strict mode (see ADR-0006).
No error is "an error occurred." Every error is actionable.
Worked examples
See ../dsl/examples/:
| File | What it shows |
|---|---|
minimal.yaml |
The smallest valid DSL (primary only, no subagents). |
single-subagent.yaml |
One subagent with a tight cage and per-agent tools. |
multi-subagent.yaml |
Three subagents in a recursive tree (parent dispatches children). |
network-allowlist.yaml |
Several net.allow patterns: globs, ports, CIDRs. |
with-plugins.yaml |
Project declaring plugins with source and version. |
nested-agents.yaml |
Recursive AgentSpec tree demonstrating per-agent cage and tools at multiple depths. |
insecure.yaml |
A subagent with cage: disabled and the operator-readable warning header. |
portable.yaml |
Best-practice portable project: aliases, project-relative paths, plugin declared with source. |
Each example is a real .kaged/project.yaml file that kaged dsl validate accepts.
CLI surface
This spec defines what the following commands accept and what they output. The CLI implementation lives in packages/dsl/cli/.
kaged dsl validate <path>
Validates a DSL file. Exits 0 on success, non-zero with errors on failure.
<path>can be a file (./project.yaml) or a directory (the tool looks for.kaged/project.yamlinside it).--strict(default): unknown fields are errors. Match the daemon's behavior.--lenient: unknown fields produce warnings. Useful for forward-compat sniffing. Never the daemon's mode.--json: emit errors as JSON for editor integration.
kaged dsl migrate <path> --to <version>
Migrates a DSL file to a different schema major version. Writes the migrated file to stdout (or to <path> with --in-place).
- Errors with a diff if no migration path exists.
kaged dsl schema [--version N]
Prints the JSON Schema for the requested version. Defaults to the current version. Useful for piping into editor configs.
Failure modes
| Failure | Where caught | Behavior |
|---|---|---|
| YAML syntax error | parser | line/col in error, no other validation runs |
| Unknown top-level field | parser, strict mode | error with did-you-mean suggestion |
| Missing required field | schema | error naming the field and its expected type |
| Type mismatch | schema | error with expected vs got |
| Agent tree depth exceeds 16 levels | parser | error: "agent tree depth exceeds the 16-level limit; flatten the tree or split into project references" |
Root agent cage is not disabled |
parser | error: "root agent cage must be disabled (interim restriction); see ADR-0022" |
kaged.issue.* or kaged.workflow.* tool on non-root agent |
parser | error: "tool carries principal_scope 'root-only' and cannot be enabled on non-root agents" |
Path in system_prompt or cage.fs.path is absolute |
parser | error: "paths in the DSL are project-root-relative; see ADR-0011" |
Path uses .. to escape project root |
parser | error: "paths must not escape the project root" |
model value contains : (provider-style) |
parser | error: "model fields are aliases; concrete provider:model bindings live in local config" |
model alias name fails pattern |
parser | error with the pattern rule |
Path-existence failure (system_prompt or cage.fs.path) |
project-load (prompts) or session-start (cage paths) | project enters pending (prompts) or session refuses to start (cage) |
| Alias unresolved in operator's local config | project-load | project enters pending; UI prompts operator to bind |
Plugin in plugins not installed in local store |
project-load | project enters pending; install prompt offered |
| Plugin version mismatch | project-load | install/upgrade prompt with diff |
Unsupported version |
parser | error with current daemon's supported version range |
| Duplicate subagent key | schema | error pointing at both definitions |
cage: disabled on any non-root agent |
parser | warning, not error; names agent + links to ADR-0009 |
cage: value that is neither object nor the literal disabled |
schema | error with the two accepted forms |
Subagent value mixes path: with cage: / model: / system_prompt: / tools: |
schema | error: "this entry has path: (project reference) and also fields belonging to an AgentSpec declaration; pick one form" |
Project-reference path: uses unsupported scheme (git:/, https:/, file:/, config:/) |
parser | error: "v1 accepts project:/ only for nested project references; see project-dsl.md#subagentsnamepath" |
Project-reference path: is naked (no prefix) |
parser | error: naked path; see ADR-0015 |
Project-reference path: uses .. escape |
parser | error: same .. escape rule as other URI paths |
Project-reference path: resolves outside the parent project root |
project-load | project enters invalid; error names the offending reference and the resolved path |
Project-reference target directory has no .kaged/project.yaml |
project-load | project enters pending; error names the missing file |
Project-reference cycle (A → B → A or transitive) |
project-load (compilation pass) | project enters invalid; error names the cycle path |
| Project-reference depth exceeds 16 levels | project-load (compilation pass) | project enters invalid; error: compile_depth_exceeded with the chain |
Project-reference overrides contains version: or project: |
parser | error: project identity cannot be overridden by parent; see project-dsl.md#subagentsnameoverrides |
Project-reference overrides produces an invalid nested DSL when merged |
project-load | project enters invalid; error references the merged-result validation failure |
Project-reference name: collides with a sibling subagent's map key |
cross-ref pass | error: "tool-name override conflicts with another subagent's identity; pick a unique value" |
Open questions
These are tracked here rather than blocking the spec:
- Prompt frontmatter schema. The DSL's
system_promptfield points at a markdown file. Whether/how that file carries metadata (model overrides, prompt versioning, dependencies) is a separate spec (prompts.md— TBD). - Plugin allowlisting per subagent. v1 grants plugins project-wide. A per-agent
plugins_allowed: [...]field onAgentSpecis plausible if needed; not in v1. - Primary cage scheduling. The root agent's
cageis restricted todisabled(interim). When the supervisor gains the ability to cage the primary process, this restriction will be lifted and the root agent will accept a full cage block. Tracked separately from the depth-16 limit andAgentSpecshape, which are stable. - Hot reload. Can the daemon reload a project after its DSL changes mid-session? Probably yes for non-breaking changes (prompt edits, parameter tweaks) and no for breaking changes (agent name changes, cage tightening, project-reference graph changes). Detailed semantics in
daemon.mdwhen written. - Remote project references.
path: git:/.../path: https:/...for project references depend on ADR-0015 §4 (security ceiling) which is currently deferred perfederated-config.md. Added in a later amendment. - Cross-project composition beyond in-tree. In-tree composition via project-reference subagents (see Project-reference subagents) covers the local case. Cross-daemon mesh (
git:/,https:/references, ADR-0015 §6 parent injection) is deferred to v2.
Testing notes
Per ADR-0003, the first code PR for the DSL package lands failing tests. The test corpus this spec implies:
- Schema conformance tests: every field-level rule in this doc has at least one test asserting "this file accepts" and one asserting "this file rejects with this error."
- AgentSpec recursive tests: the recursive
AgentSpecshape is tested at depths 1, 2, 8, and 16 (the limit). Depth 17 is a parse error. Per-agenttools,cage,model,system_prompt, andsubagentsfields are tested at each level.principal_scopeenforcement (kaged.issue.*/kaged.workflow.*rejected on non-root) is tested at root vs depth-1 vs depth-2. - Error message tests: every error message named in Error messages is exercised by a test that asserts the exact line/col and the doc link.
- Round-trip tests: for migration, every supported v1 → vN migration has a fixture pair (input + expected output).
- Worked-example tests: every file in
../dsl/examples/is validated as part of CI. If an example breaks the schema, CI fails. - Project-reference tests: the union under
subagents.<name>must accept both shapes and reject mixed entries. Cycle detection over project references (including transitiveA → B → C → A) is exercised end-to-end via fixture projects. Override-merge tests verify ADR-0015 semantics on the nested-project layer (deep merge, nullification,version/projectrejection, schema validation on the merged result). Compiled output is verified to be a uniformAgentSpecsubtree with_sourceannotations.
Amendments
2026-05-21 — cage: disabled opt-out
Added the string-form value "disabled" to subagents.<name>.cage to support the per-subagent sandbox opt-out introduced in ADR-0009 amendment. Field semantics, JSON Schema, failure-modes table, and cage_defaults interaction all updated.
Also added the insecure.yaml example demonstrating the opt-out with operator-readable warnings inline.
2026-05-21 — Portability: model aliases + project-relative paths + plugin install-on-load
Per ADR-0011, the DSL is now strictly portable. Three substantive changes:
- Model fields are aliases, not provider:model identifiers.
primary.modelandsubagents.<name>.modelare alias names (no:) resolved through the operator's local config. Concrete provider:model bindings live only inlocal-config.md. NewModelAliasschema type added to Appendix A. - All path fields are project-root-relative.
primary.system_prompt,subagents.<name>.system_prompt, andcage.fs[].pathreject absolute paths and..escapes. NewProjectRelativePathschema type added. The "what if a subagent needs outside-root access?" section documents the three options (restructure, symlink,cage: disabled). - Plugins gain
sourceandversionfields. Project plugins can declare where to fetch from if not installed locally; kaged prompts the operator at project-load time per ADR-0008 amendment.
Validation timing gained a fourth moment ("project-load") to handle alias resolution and plugin presence checks against operator local config. Failure modes table extended. Examples in ../dsl/examples/ updated; new portable.yaml added.
2026-05-24 — tools: per-tool override map
Added the optional top-level tools: field — a named-object map (per ADR-0015) keyed by dot-delimited tool names (e.g. file.read, code.lsp). Values are ToolOverride objects (enabled, description, parameters — all optional) or null (ADR-0015 nullification to reset inherited overrides).
This field aligns with the ToolConfigMap / ToolOverride types implemented in @kaged/agent-tooling (packages/agent-tooling/src/tool-config.ts). The daemon's resolveToolConfig() deep-merges config layers (built-in defaults → project.yaml → project.local.yaml → parent injection) with nullification support.
Changes:
- Top-level shape gains
tools:with a YAML example. - New
### toolsfield section documentingenabled,description,parameterssub-fields, resolution order, and interaction with cage policies. - JSON Schema gains
toolsproperty (object withadditionalPropertiesreferencingToolOverride) andToolOverride$def. - Constrained by gains ADR-0015.
2026-05-25 — Array fields → named-object maps (subagents, tasks, plugins, interconnect)
Converted four top-level fields from object[] arrays to Record<string, object> named-object maps. The key serves as the item's identifier; the name field is removed from each sub-schema value. This enables ADR-0015 nullification: setting a key to null in project.local.yaml removes that entry from the merged config.
Changes:
- Top-level shape updated:
subagents,tasks,plugins,interconnectshown as named-object maps with key descriptions. subagentssection rewritten: keyed by subagent name;namefield removed from value;propertyNamespattern and reserved-name exclusion moved to the map key.interconnectsection rewritten: keyed by operator-chosen label; nonamefield in value.pluginssection rewritten: keyed by plugin name (matchingkaged-plugin.yamlname:);namefield removed from value.- New
tasksnarrative section added to Fields area: keyed by task name;namefield removed from value; all sub-fields documented. - JSON Schema (Appendix A): all four top-level properties changed from
array/itemstoobject/additionalPropertieswithoneOf [ref, null];propertyNamespatterns andmaxPropertieslimits added;SubagentandPluginRef$defshavenameremoved fromrequiredandproperties. - Stale array references (
subagents[],plugins[], etc.) updated throughout error messages, failure modes, open questions, amendments, and cross-ref note. - All example files in
docs/dsl/examples/converted from array syntax to named-object map syntax. - Dogfood config (
.kaged/project.yaml) updated.
No migration support — pre-alpha, new format enforced directly.
2026-05-26 — Project-reference subagents (nested projects exposed as subagents)
Added the project-reference value shape under subagents.<name>. A subagent value may now be either:
- A full
Subagentdeclaration (existing behavior), or - A
ProjectReferencepointing at a nested kaged project viapath: project:/.... The nested project's primary is exposed to the parent as a callable subagent.
This is the DSL surface for ADR-0015 §7 ("Compiled Contextualization"), which was previously marked deferred in federated-config.md. The two forms are distinguished by presence of path: — there is no explicit type: discriminator. Mixing fields from both forms is a parse error.
Changes:
- Top-level shape updated to show both subagent value forms side-by-side under
subagents. - New
### Project-reference subagentssection added betweensubagents.<name>.parametersand the cage block. Documents:- The
pathfield (URI-prefixed,project:/only in v1). - The
namefield (optional tool-name override presented to the LLM; map key remains canonical). - The
descriptionfield (optional tool-description override). - The
overridesfield (partialProjectDsldeep-merged on top of the nested project; ADR-0015 merge semantics;versionandprojectforbidden). - Cage policy for nested projects (nested cages stay nested;
cage_defaultsdoes not cross the silo boundary; parent tightens viaoverrides). - Cross-reference and call-graph semantics (project-refs participate by map key; nested primary is the entry point).
- Compilation pass at project-load time with arbitrary depth and cycle detection.
- What this is not (not hot reload, not cross-machine, not the §6 parent-injection mechanism).
- The
- JSON Schema (Appendix A) gains a
ProjectReference$def;subagents.additionalProperties.oneOfextended to include it. - Failure modes table extended with eleven new entries covering shape-mixing, scheme restrictions, cycle detection, override identity collisions, and load-time existence checks.
- No code changes in this PR. Per ADR-0003, the schema mirror in
@kaged/dsl(packages/dsl/src/schema.ts), failing tests, and parser support land in a follow-up PR. The spec is the contract; the code follows.
Cross-spec: federated-config.md un-defers ADR-0015 §7 in the same amendment, cross-linking to this section as the implementation surface.
Open questions deferred to a later amendment:
- Remote project references (
git:/,https:/) — gated on ADR-0015 §4 security ceiling enforcement. - Parent injection of virtual subagents into a child (ADR-0015 §6) — distinct mechanism, still deferred.
- Hot reload semantics for nested-project DSL changes mid-session (belongs to
daemon.md).
2026-05-26 — Recursive AgentSpec, per-agent tools/cage (ADR-0022)
Per ADR-0022, the DSL agent model is unified around a single recursive AgentSpec type. The former PrimaryAgent / Subagent distinction is removed — differentiation is now purely positional (root vs non-root).
Structural changes:
AgentSpecreplacesPrimaryAgent+Subagent. One recursive type used at every agent position.cageis required on all agents (was absent onPrimaryAgent).can_be_called_byis removed — the tree structure is the call graph.tools:moved from project-level to per-agent onAgentSpec. Each agent declares its own tool surface. The former top-leveltools:section is removed.cage_defaultsremoved. No inheritance between cages; each agent declares its own cage independently.interconnectremoved. Sibling and cross-tree calls do not exist; a parent calls its direct children only.- Root agent interim restriction. Root agent's
cagemust bedisabled. Non-root agents accept the full cage block. principal_scopeenforcement.kaged.issue.*andkaged.workflow.*tools carryprincipal_scope: "root-only". The schema rejects them on non-root agents.subagentsmoved insideAgentSpec. Recursive nesting with depth limit 16. The top-levelsubagentsproperty is removed; subagents live underprimary.subagents.
Schema changes:
- JSON Schema (Appendix A):
PrimaryAgentandSubagent$defsreplaced by singleAgentSpec$def. Top-levelrequireddropssubagents.cage_defaults,interconnect,toolsremoved from top-levelproperties.Interconnect$defremoved.AgentSpeccarriestoolsandsubagentsas optional properties with recursive$ref. - Failure modes table: removed
can_be_called_byandinterconnectrows; added depth-limit, root-cage, andprincipal_scoperows. - Open questions: removed
cage_profiles,interconnectevent taxonomy,cross-project interconnect; added primary cage scheduling. - Validation timing: updated CLI check to include depth limit and
principal_scopeenforcement. - Testing notes: updated to cover recursive depth,
principal_scope, and compiledAgentSpecsubtree. - Worked examples table: updated to reflect new example names (
defaults.yaml→nested-agents.yaml, descriptions updated).
No migration support — pre-alpha, new format enforced directly.
2026-05-27 — Recursive tool materialization
Tool materialization now applies to every agent in the tree, not just the root. When availableTools is provided to compileProjectDsl(), every AgentSpec — root, inline subagents, and project-reference primaries — gets a materialized tools: { <name>: { enabled: true/false } } block.
Changes:
compileAgent()now materializes tools at every level. Materialization moved from the post-walk phase incompileProjectDsl()into the recursivecompileAgent()walker. Each agent gets its own materialized tools based on its position (root vs non-root) and its owntools:block.- Non-root agents start with nothing enabled. The root agent uses
DEFAULT_ENABLED_TOOLSas its base state; all subagents use an empty set. Subagents must explicitly opt in to every tool via their owntools:block. resolveRootTools()accepts optionaldefaultEnabledparameter. Third parameter defaults toDEFAULT_ENABLED_TOOLSfor backward compatibility. The compiler passes an empty set for non-root agents.- Operator overrides apply to all agents. The
operatorToolOverrideslayer is threaded through the entire agent tree, so operator-level tool policy is consistent across root and subagents. - Project-reference primaries are treated as root. When crossing a project-reference boundary, the nested project's primary agent gets root-level defaults (kaged.* enabled), matching the semantics of a standalone project.
2026-05-27 — Default-disabled non-kaged tools
Only kaged.* tools (8 total: checkpoint, issue.*, ask, form) are enabled by default on the root agent. All other namespaces (file, search, code, debug — 10 tools) start disabled and require explicit opt-in via operator config ([default_tools]) or project DSL (primary.tools).
Changes:
DEFAULT_ENABLED_TOOLSconstant added to@kaged/dsl. AReadonlySet<string>of the 8kaged.*tools that are on by default. Exported frompackages/dsl/src/defaults.ts.resolveRootTools()base state changed. Wasenabled = truefor all tools; nowenabled = DEFAULT_ENABLED_TOOLS.has(name). Non-kaged tools must be explicitly opted in via an override layer with{ enabled: true }.DEFAULT_ROOT_TOOLSsemantics clarified. The constant is the canonical list of known tools (31 total), not the enabled set. Comments updated to reflect this.- Spec language updated. Role-based default tools section and tool resolution section now document the opt-in model for non-kaged namespaces.
2026-05-27 — Compile-time tool resolution with operator overrides
Tool resolution for the root agent is now performed at compile time inside compileProjectDsl() rather than at runtime in the harness or daemon.
Changes:
DEFAULT_ROOT_TOOLSconstant added to@kaged/dsl. Canonical list of 18 built-in tools across 6 namespaces (file,search,code,debug,shell,kaged). Exported frompackages/dsl/src/defaults.ts.resolveRootTools()function added to@kaged/dsl. Accepts available tool names and an ordered list of override layers (operator-level fromlocal.toml[default_tools], then project-level from DSLprimary.tools). Returns the filtered list of enabled tool names. Later layers override earlier ones;nullor{ enabled: false }disables a tool;{ enabled: true }re-enables it.CompileProjectDslOptionsextended. New optional fields:availableTools(registered tool names from the runtime) andoperatorToolOverrides(from local configdefault_tools).CompileProjectDslResultextended. New field:resolvedRootTools: string[] | null— the effective tool list whenavailableToolswas provided, ornullwhen omitted.- Tool resolution section updated. Documents the four-layer resolution order (available → operator → project → cage) and the compile-time resolution flow.
- Daemon
primary-runner.tsupdated. Now usesresolveRootTools()with operatordefault_toolsas the first layer and DSLprimary.toolsas the second, replacing the formerfilterEnabledTools()call. ToolOverrideLayertype exported from@kaged/dsl. Reusable type for override maps:Record<string, { enabled?: boolean } | null>.
2026-06-03 — Agent execution limits: max_steps and max_output_tokens
Per agent-harness runtime work: Mastra's default maxSteps is 5 and the default maxOutputTokens is provider-dependent. Both are too low for complex tool-heavy workflows. These fields give the operator explicit control over the agent loop budget.
max_stepsadded toAgentSpec. Optional integer1–100. Controls the maximum number of tool-call rounds the agent loop is allowed before stopping. Passed to MastraAgent.stream()execution options.max_output_tokensadded toAgentSpec. Optional integer1–65536. Controls the maximum output tokens per completion. Passed to the provider asmax_tokens(or provider-specific equivalent).parameterssection updated. Theparametersfield (opaque model-specific parameters) is now documented with a note that it is separate frommax_stepsandmax_output_tokens— the latter are kaged-managed execution limits, whileparametersis the escape hatch for provider-specific knobs.AgentSpecshape snippet and JSON Schema (Appendix A) updated. Both fields added to theAgentSpecschema underproperties.- Testing notes updated. Added tests for:
max_stepsinheritance (subagent inherits parent value unless overridden),max_output_tokensoverride, out-of-range rejection (max_steps: 0andmax_output_tokens: 70000are parse errors), and provider passthrough (values reach the harness runtime and Mastra). - Constrained-by list updated. No new ADRs — this is an execution-limit surface, not an architectural decision. The fields are constrained by the existing ADR-0012 (Mastra substrate) and ADR-0014 (provider routing).
2026-05-27 — ADR-0023 & ADR-0024: per-agent plugins and compaction blocks
Project-level
plugins:block removed. The top-levelplugins:key formerly accepted at project root is no longer valid. The parser emits a clear error pointing to ADR-0023 and showing the equivalent per-agent rewrite. This is a breaking schema change atversion: 1; consistent with the pre-alpha "no migration" pattern.AgentSpec.pluginsadded. A new optional named-object map on everyAgentSpecnode carrying per-agent plugin declarations. Each entry:package(required on root declaration, inherited underisolation: project),isolation: 'agent' | 'project'(default'agent', root only),hooks: [hook-name](subscribed lifecycle hooks),config(opaque plugin-defined config). Max 16 plugins per agent. Per ADR-0023's no-inheritance posture, subagents do not inherit a parent's plugins underisolation: agent. Underisolation: project, subagents declare parallelplugins.<name>blocks with override fields.AgentSpec.compactionadded. A new optional object on everyAgentSpecnode carrying per-agent context-compaction config. Fields:strategy('drop' | 'summarize' | 'delegate' | 'checkpoint', default'drop'),upper_threshold(default 0.85),lower_threshold(default 0.60),always_keep, and strategy-specific subfields (summarize,delegate,checkpoint). Subagents inherit field-by-field from parent'scompaction:; overrides at the subagent level only override specified fields.Parse-time validations added.
- Project-level
plugins:rejection. Clear error pointing to ADR-0023 migration. AgentSpec.plugins.<name>.packagerequired on root declarations; forbidden on subagent declarations underisolation: project(sincepackageis inherited).isolationis root-only. Settingisolationon a subagent's plugin declaration is a parse error.compaction.lower_threshold<compaction.upper_thresholdenforced.compaction.delegate.pluginmust reference a declared plugin in the same agent'splugins:block AND that plugin must claimrole: compactorin its manifest (validated at project-load time, not parse time, since the manifest is needed).hooks: [on_session_start | on_session_idle]on a subagent emits a parse-time warning (the hook is primary-only; declaring it on a subagent is permitted but it will never fire).
- Project-level
AgentSpec shape snippet updated to show the two new fields between
tools:andsubagents:.Top-level shape example updated to show plugin declarations on the primary (the common case).
Appendix A JSON Schema to be updated (this entry tracks the spec amendment; the schema update is a follow-up patch).
Constrained-by list extended with ADR-0023 and ADR-0024.
2026-06-06 — Default compaction summary prompt, summarize.model optional, auto-creation on init
Per ADR-0024 2026-06-06 amendment (compaction hardening):
summarize.modelis now optional. When omitted, the summarizer uses the agent's own model alias. Previously it was required. This makes the default compaction config usable without any explicit configuration — operators get summarize-at-threshold for free.summarize.promptdefault defined. The default prompt path isconfig:/prompts/compaction-summary.md. Previouslypromptwas required whenstrategy: summarize. Now it has a default, so thesummarizeblock can be entirely implicit in the fallback config.Default fallback config extended. The no-config fallback now includes a
summarizeblock with default values (prompt: config:/prompts/compaction-summary.md,window_messages: 20,preserve_recent: 10,max_summary_tokens: 1500). This enables thedropstrategy's summarize-at-threshold upgrade (ADR-0024 amendment item 5) without any operator configuration.Default compaction summary prompt section added. New
§ Default compaction summary promptsection documenting the auto-createdconfig:/prompts/compaction-summary.mdfile: its content (structured handoff format), auto-creation behavior (kaged project initand project-load fallback), and override mechanism.AgentSpec shape snippet updated.
summarizecomment changed from "strategy-specific" to "optional, defaults provided" to reflect the new optionality.
2026-05-31 — Plugin registry model: top-level plugins reinstated as project registry
Project-level
plugins:reinstated as the plugin registry. Every plugin used in the project MUST be declared here first. This is the gateway — it makes plugins available to agents and other project services. Per-agentplugins:blocks only override or disable; they cannot introduce new plugins.PluginRefSchemaupdated. Fields:package(string, required),source(string, optional, magic-prefix validated:npm:,github:,project:/,config:/,git:),enabled(boolean, optional, defaultfalse),config(object, optional).versionremoved.AgentPluginDeclSchemasimplified. Fields:enabled(boolean, optional),hooks(list, optional),config(object, optional).packageandisolationremoved —packagelives in the registry only;isolationis now a plugin-config key, not a DSL schema field.Cross-reference validation updated. Per-agent slot names must reference a key in the project-level
plugins:registry. Unknown slots produce across_referenceerror. The oldisolation: projectinheritance validation removed entirely.isolationmoves to plugin config. Plugins that support isolation (e.g. memory-markdown) acceptisolation: agent | projectas a config key. The DSL schema no longer has a first-classisolationfield. This is more flexible — each plugin defines its own scoping semantics.sourcefield semantics defined. Optional field in the registry entry. Tells the daemon where to fetch the plugin if it's not installed. Five prefix patterns supported. Missing plugins withoutsourceare reported but cannot be auto-installed.Appendix A JSON Schema
PluginRefupdated to reflect new fields.
Appendix A — JSON Schema (v1)
This is the normative schema. Published at https://kaged.dev/schema/v1.json and shipped inside the kaged binary.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://kaged.dev/schema/v1.json",
"title": "kaged project DSL v1",
"type": "object",
"additionalProperties": false,
"required": ["version", "project", "primary"],
"properties": {
"version": { "const": 1 },
"project": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$"
},
"description": {
"type": "string",
"maxLength": 280
},
"primary": { "$ref": "#/$defs/AgentSpec" },
"plugins": {
"type": "object",
"additionalProperties": {
"oneOf": [
{ "$ref": "#/$defs/PluginRef" },
{ "type": "null" }
]
},
"propertyNames": {
"minLength": 1
},
"description": "Named-object map of plugins keyed by plugin name. Null values remove the plugin (ADR-0015 nullification)."
},
"tasks": {
"type": "object",
"maxProperties": 64,
"additionalProperties": {
"oneOf": [
{ "$ref": "#/$defs/Task" },
{ "type": "null" }
]
},
"propertyNames": {
"pattern": "^[a-z][a-z0-9_-]{0,30}[a-z0-9]$",
"not": { "enum": ["adhoc", "all", "new"] },
"description": "Task name. See tasks section."
},
"description": "Named-object map of tasks keyed by task name. Null values remove the task (ADR-0015 nullification)."
}
},
"$defs": {
"AgentSpec": {
"type": "object",
"additionalProperties": false,
"required": ["model", "system_prompt", "cage"],
"properties": {
"model": { "$ref": "#/$defs/ModelAlias" },
"system_prompt": { "$ref": "#/$defs/ProjectRelativePath" },
"cage": {
"oneOf": [
{ "$ref": "#/$defs/Cage" },
{ "const": "disabled" }
]
},
"description": {
"type": "string",
"maxLength": 280
},
"parameters": { "type": "object" },
"tools": {
"type": "object",
"additionalProperties": {
"oneOf": [
{ "$ref": "#/$defs/ToolOverride" },
{ "type": "null" }
]
},
"propertyNames": {
"pattern": "^[a-z][a-z0-9]*([.][a-z][a-z0-9]*|[.][*])?$",
"description": "Dot-delimited tool name (e.g. 'file.read') or namespace glob ('debug'). See agent-tooling.md."
},
"description": "Per-agent tool configuration overrides. Keys are tool names or globs; values are ToolOverride objects or null (ADR-0015 nullification)."
},
"subagents": {
"type": "object",
"maxProperties": 64,
"additionalProperties": {
"oneOf": [
{ "$ref": "#/$defs/AgentSpec" },
{ "$ref": "#/$defs/ProjectReference" },
{ "type": "null" }
]
},
"propertyNames": {
"pattern": "^[a-z][a-z0-9_]{0,30}[a-z0-9]$",
"not": { "enum": ["primary", "operator", "system"] },
"description": "Agent name. See subagents section."
},
"description": "Named-object map of child agents keyed by name. Values may be an AgentSpec (recursive), a ProjectReference (nested project — presence of `path` is the discriminator), or null (ADR-0015 nullification). Depth limit: 16 levels."
}
},
"description": "The single recursive agent shape used at every position in the tree. See § AgentSpec."
},
"ProjectReference": {
"type": "object",
"additionalProperties": false,
"required": ["path"],
"properties": {
"path": {
"type": "string",
"pattern": "^project:/(?!/).+$",
"description": "URI-prefixed path to a nested project root. v1 accepts only the project:/ scheme; the path portion must not contain '..' escapes. See the project-reference subagents section."
},
"name": {
"type": "string",
"pattern": "^[a-z][a-z0-9_]{0,30}[a-z0-9]$",
"not": { "enum": ["primary", "operator", "system"] },
"description": "Optional tool-name override presented to the LLM. Defaults to the nested project's slug. Does not change the map key."
},
"description": {
"type": "string",
"maxLength": 280,
"description": "Optional tool-description override presented to the LLM. Defaults to the nested project's description."
},
"overrides": {
"type": "object",
"description": "Partial ProjectDsl deep-merged on top of the nested project's resolved config. 'version' and 'project' are forbidden; otherwise any field accepted by ProjectDsl may appear. Merge follows ADR-0015 §2 (deep merge, null nullifies, arrays replace). Schema validation runs on the merged result, not on this partial."
}
}
},
"ModelAlias": {
"type": "string",
"pattern": "^[a-z][a-z0-9-]{0,62}[a-z0-9]$",
"not": { "enum": ["primary", "subagent", "operator", "system", "default"] },
"description": "Model alias name. Cannot contain ':'. See ADR-0011 and local-config.md."
},
"ProjectRelativePath": {
"type": "string",
"pattern": "^(?!/)(?!.*\\.\\.(\/|$)).+$",
"description": "Path relative to project root. No leading slash; no '..' segments."
},
"Cage": {
"type": "object",
"additionalProperties": false,
"required": ["fs", "net", "state"],
"properties": {
"fs": {
"type": "array",
"items": { "$ref": "#/$defs/Mount" }
},
"net": { "$ref": "#/$defs/Net" },
"state": { "enum": ["ephemeral", "scratch"] },
"seccomp": { "enum": ["default", "relaxed"], "default": "default" },
"limits": { "$ref": "#/$defs/Limits" }
}
},
"Mount": {
"type": "object",
"additionalProperties": false,
"required": ["mode", "path"],
"properties": {
"mode": { "enum": ["ro", "rw"] },
"path": { "$ref": "#/$defs/ProjectRelativePath" }
}
},
"Net": {
"type": "object",
"additionalProperties": false,
"required": ["allow"],
"properties": {
"allow": {
"type": "array",
"items": { "type": "string" }
}
}
},
"Limits": {
"type": "object",
"additionalProperties": false,
"properties": {
"memory_mb": { "type": "integer", "minimum": 16 },
"cpu_shares": { "type": "integer", "minimum": 1 },
"pids": { "type": "integer", "minimum": 1 },
"walltime_sec": { "type": "integer", "minimum": 1 }
}
},
"PluginRef": {
"type": "object",
"additionalProperties": false,
"required": ["package"],
"properties": {
"package": { "type": "string", "minLength": 1 },
"source": { "type": "string", "minLength": 1, "pattern": "^(npm:|github:|project:/|config:/|git:)" },
"enabled": { "type": "boolean" },
"config": { "type": "object" }
}
},
"ToolOverride": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean", "default": true },
"description": { "type": "string" },
"parameters": { "type": "object" }
},
"description": "Per-tool configuration override. All fields optional. See agent-tooling.md for the built-in tool catalog."
}
}
}
Structural validations beyond JSON Schema's expressiveness (agent tree depth limit, root agent cage: disabled interim restriction, principal_scope enforcement for kaged.issue.*/kaged.workflow.* on non-root agents, project-reference cycle detection) are performed by the kaged parser as a post-validation pass.
References
- ADR-0006 — the format decision
- ADR-0009 — the cage mechanism the cage block compiles to
- ADR-0008 — the plugin model
plugins:references - ADR-0015 — federated config merge semantics for overrides
- ADR-0022 — recursive AgentSpec, per-agent tools/cage
- ADR-0003 — the process this spec follows
agent-tooling.md— tool registry, built-in catalog, andToolConfigMap/ToolOverridetypes thetools:field maps to../03-glossary.md— terminology used here../dsl/grammar.md— operator-facing reference (not yet written)../dsl/examples/— worked examples (stubbed)- JSON Schema: https://json-schema.org/