Skip to main content

agentd/config/v2/
schema.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2//! The **JSON Schema (Draft 2020-12) of the settings document** — hand-written
3//! (no `schemars`, the moat) and held faithful to [`super::Settings`] by the
4//! tests in `super::tests`, which walk both shapes object by object. It is the
5//! single source for the path bindings (env `AGENTD_<PATH>` names, `--<path>`
6//! flags, `--help`), for `--config-schema=2`, and for agentctl's admission
7//! validation, so a section added here reaches all of them at once.
8//!
9//! Conventions: every object is `additionalProperties: false` (mirrors
10//! `deny_unknown_fields`); durations are strings (`10m`, `500ms`, bare seconds
11//! also accepted); secrets are strings that MUST be `{{secret:…}}` /
12//! `{{secret-file:…}}` references when they come from a file, so a config
13//! document is always safe to commit.
14
15use serde_json::{Map, Value, json};
16
17/// The schema's `x-agentd-contract-version` — the config contract this
18/// document describes, matched against the capabilities manifest.
19pub const SCHEMA_CONTRACT_VERSION: &str = "1.0";
20
21/// The document version this schema describes (`config_version`).
22pub const CONFIG_VERSION: &str = "1";
23
24pub fn schema() -> Value {
25    let duration = json!({ "type": ["string", "integer"], "description": "a duration: `10m`, `90s`, `500ms`, or bare seconds" });
26    let secret = json!({ "type": "string", "description": "a secret — from a file it MUST be a `{{secret:NAME}}` / `{{secret-file:PATH}}` reference; env/flag values may be inline" });
27    let string_map = json!({ "type": "object", "additionalProperties": { "type": "string" } });
28    let tool_select = json!({
29        "oneOf": [
30            { "enum": ["all", "none"] },
31            { "type": "array", "items": { "type": "string" } }
32        ],
33        "description": "`all` | `none` | a list of names"
34    });
35    let budget = json!({
36        "type": "object",
37        "additionalProperties": false,
38        "properties": {
39            "windows": { "type": "array", "items": { "$ref": "#/$defs/BudgetWindow" } },
40            "lifetime_tokens": { "type": "integer", "minimum": 0, "description": "hard ceiling; 0 = unbounded" },
41            "scope": { "type": "array", "items": { "enum": ["instance", "run", "conversation", "principal"] } },
42            "on_exhausted": { "enum": ["wait", "slow", "degrade", "refuse", "fail"] },
43            "slow": { "type": "object", "additionalProperties": false, "properties": { "factor": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 } } },
44            "degrade": { "type": "object", "additionalProperties": false, "properties": { "model": { "type": "string" } } },
45            "reserve": { "type": "object", "additionalProperties": false, "properties": {
46                "estimate": { "enum": ["context", "fixed", "none"] },
47                "fixed": { "type": "integer", "minimum": 0 } } }
48        }
49    });
50    let mut properties = Map::new();
51    properties.insert(
52        "config_version".to_string(),
53        json!({ "type": "string", "const": CONFIG_VERSION, "description": "the document version" }),
54    );
55    top_level_properties(
56        &mut properties,
57        &duration,
58        &secret,
59        &string_map,
60        &tool_select,
61        &budget,
62    );
63    let mut defs = Map::new();
64    defs_properties(&mut defs, &secret, &string_map, &budget, &duration);
65    json!({
66        "$schema": "https://json-schema.org/draft/2020-12/schema",
67        // The URL this document is SERVED at, so an editor that fetches the
68        // `$id` gets this schema and not a 404. Versioned by the config
69        // document version, so pinning `config_version: "1"` and pinning the
70        // schema are the same decision.
71        "$id": format!("https://agentd.dev/schema/config-{CONFIG_VERSION}.json"),
72        "x-agentd-contract-version": SCHEMA_CONTRACT_VERSION,
73        "title": format!("agentd configuration (config_version {CONFIG_VERSION})"),
74        "description": "agentd configuration document (YAML or JSON; several files merge in order; every path is also AGENTD_<PATH> and --<path>)",
75        "type": "object",
76        "additionalProperties": false,
77        "properties": Value::Object(properties),
78        "$defs": Value::Object(defs)
79    })
80}
81
82#[allow(clippy::too_many_arguments)]
83fn top_level_properties(
84    m: &mut Map<String, Value>,
85    duration: &Value,
86    secret: &Value,
87    string_map: &Value,
88    tool_select: &Value,
89    budget: &Value,
90) {
91    m.insert("agent".to_string(), json!({
92                "type": "object", "additionalProperties": false,
93                "properties": {
94                    "name": { "type": "string", "description": "instance identity (falls back to the downward-API instance, then the hostname)" },
95                    "instruction": { "type": "string", "description": "static text, or a single-token URI a configured MCP server serves (read + subscribed)" },
96                    "prompt": { "type": "string", "description": "a one-shot task (--prompt): with no workflows configured the generated run executes this, while `instruction` stays the standing policy (the run's system prompt)" },
97                    "preflight": { "enum": ["never", "auto", "always"] },
98                    "wake_on": { "type": "array", "items": { "enum": ["a2a_message", "human_reply", "subagent_result", "workflow_finished", "workflow_failed", "instruction_updated", "budget_resumed"] } },
99                    "on_workflow_finished": { "enum": ["ignore", "note", "think"] },
100                    "tools": { "type": "object", "additionalProperties": false, "properties": {
101                        "internal": tool_select, "mcp": tool_select, "code": tool_select } },
102                    "max_parallel_turns": { "type": "integer", "minimum": 1 },
103                    "conversation_budget": budget,
104                    "ask_human_fallback": { "enum": ["wait", "pause", "idle", "fail", "finish", "stop", "auto"], "description": "what ask_human does with no human channel (and, for auto, on an unanswered gate timeout): wait (park until timeout), fail (default), or auto (an LLM judge answers on the operator's behalf, marked as auto)" },
105                "approval": { "enum": ["ask", "auto", "accept"], "description": "whether a gate asks a person (ask), lets an LLM judge decide (auto), or takes the ask's recommendation (accept); runtime-settable via config.set" }
106                }
107            }));
108    m.insert("intelligence".to_string(), json!({
109                "type": "object", "additionalProperties": false,
110                "properties": {
111                    "endpoints": { "oneOf": [ { "type": "array", "items": { "type": "string" } }, { "type": "string" } ], "description": "ordered endpoint list (failover); one comma-separated string is accepted" },
112                    "model": { "type": "string", "description": "the default model — a declared `models:` tier name, or a literal model string" },
113                    "models": { "type": "object", "description": "named model tiers: cost/quality tiering inside one workflow without forking a process. A tier points AT a `services:` entry and may only narrow — it inherits that service's trifecta tags and can never declare its own floor.", "additionalProperties": {
114                        "type": "object", "additionalProperties": false, "required": ["model"], "properties": {
115                            "model": { "type": "string", "description": "the wire model name sent to the provider" },
116                            "service": { "type": "string", "description": "a `services:` entry of kind: intelligence supplying endpoint, auth and tags" },
117                            "window": { "type": "integer", "minimum": 1, "description": "this model's context window, so compaction stops guessing from the model NAME" },
118                            "fallback": { "type": "string", "description": "the tier to degrade to — a ladder that walks down instead of failing" },
119                            "pricing": { "type": "object", "additionalProperties": false, "properties": { "input_per_1k": { "type": "number" }, "output_per_1k": { "type": "number" } } } } } },
120                    "default": { "type": "string", "description": "the tier used when nothing names one" },
121                    "preflight_model": { "type": "string", "description": "the tier preflight runs on — a recurring fixed cost that does not need the answering model" },
122                    "dialect": { "enum": ["openai", "anthropic", "bedrock"], "description": "wire dialect; bedrock = native Amazon Bedrock Converse (pair with auth.kind=aws)" },
123                    "token": secret,
124                    "token_file": { "type": "string" },
125                    "headers": string_map,
126                    "auth": { "$ref": "#/$defs/Auth" },
127                    "swap_policy": { "enum": ["finish-on-old", "restart-turn"] },
128                    "structured_output": { "enum": ["auto", "json_schema", "tool", "prompt"] },
129                    "budget": budget,
130                    "pricing": { "type": "object", "additionalProperties": { "$ref": "#/$defs/Pricing" } },
131                    "timeout": duration
132                }
133            }));
134    m.insert(
135        "mcp".to_string(),
136        json!({
137            "type": "object", "additionalProperties": false,
138            "properties": {
139                "servers": { "type": "array", "items": { "$ref": "#/$defs/McpServer" } },
140                "default_timeout": duration
141            }
142        }),
143    );
144    m.insert("tools".to_string(), json!({
145                "type": "object", "additionalProperties": false,
146                "properties": {
147                    "disabled": { "type": "array", "items": { "type": "string" } },
148                    "overrides": { "type": "object", "additionalProperties": { "$ref": "#/$defs/ToolOverride" } }
149                }
150            }));
151    m.insert("store".to_string(), json!({
152                "type": "object", "additionalProperties": false,
153                "properties": {
154                    "kind": { "enum": ["mcp", "http", "file", "memory", "none"] },
155                    "prefix": { "type": "string" },
156                    "mcp": { "$ref": "#/$defs/StoreMcp" },
157                    "http": { "$ref": "#/$defs/StoreHttp" },
158                    "file": { "$ref": "#/$defs/StoreFile" },
159                    "checkpoint": { "type": "object", "additionalProperties": false, "properties": { "debounce_ms": { "type": "integer", "minimum": 0 } } },
160                    "retention": { "type": "object", "additionalProperties": false, "properties": {
161                        "runs": { "type": "object", "additionalProperties": false, "properties": {
162                            "keep_last": { "type": "integer", "minimum": 0, "description": "keep at most this many terminal runs" },
163                            "ttl": duration } } } },
164                    "durability": { "type": "object", "additionalProperties": false, "properties": {
165                        "a2a": { "enum": ["strict", "eventual"] }, "steps": { "enum": ["strict", "eventual"] },
166                        "work": { "enum": ["durable", "ephemeral"], "description": "default durability CLASS for runs + subagent records: ephemeral = nothing persists unless a workflow/spawn says durable: true (the fast path); default durable" } } },
167                    "on_error": { "enum": ["halt", "degrade"] },
168                    "audit": { "type": "boolean" },
169                    "timeout": duration
170                }
171            }));
172    m.insert(
173        "memory".to_string(),
174        json!({ "type": "object", "additionalProperties": false, "properties": {
175                "max_value_bytes": { "type": "integer", "minimum": 1 },
176                "list_default_limit": { "type": "integer", "minimum": 1 } } }),
177    );
178    m.insert("context".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
179                "compact_at": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 },
180                "keep_last": { "type": "integer", "minimum": 0 },
181                "model_window": { "type": "integer", "minimum": 1, "description": "the model's context window in tokens (overrides the value inferred from intelligence.model)" },
182                "plan": { "type": "object", "additionalProperties": false, "properties": { "max_items": { "type": "integer", "minimum": 1 } } },
183                "template": { "type": "string", "description": "the system-prompt template; unset = the built-in default, printed by `agentd --context-template`" },
184                "templates": { "type": "object", "additionalProperties": { "type": "string" }, "description": "named alternates a node selects with context: {template: <name>}" },
185                "summarize": { "type": "object", "additionalProperties": false, "description": "compaction's model-facing half", "properties": {
186                    "prompt": { "type": "string", "description": "override the summarizer guidance; the JSON schema it must satisfy is fixed" },
187                    "model": { "type": "string", "description": "summarize on a cheaper model than the instance's" } } } } }));
188    m.insert("knowledge".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
189                "server": { "type": "string" },
190                "auto_context": { "type": "object", "additionalProperties": false, "properties": {
191                    "on": { "enum": ["turn", "never"] }, "top_k": { "type": "integer", "minimum": 1 }, "max_bytes": { "type": "integer", "minimum": 1 } } } } }));
192    m.insert("search".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": { "server": { "type": "string" } } }));
193    m.insert(
194        "skills".to_string(),
195        json!({ "type": "object", "additionalProperties": false, "properties": {
196                "sources": { "type": "array", "items": { "$ref": "#/$defs/SkillSource" } },
197                "dir": { "type": "string", "description": "a local folder of skill files (frontmatter + body, or <name>/SKILL.md); `skills/` beside the config is adopted when this is unset" },
198                "reference_prefix": { "type": "string" },
199                "max_loaded": { "type": "integer", "minimum": 1 },
200                "max_bytes": { "type": "integer", "minimum": 1 } } }),
201    );
202    m.insert(
203        "streams".to_string(),
204        json!({ "type": "object", "additionalProperties": {
205        "type": "object", "additionalProperties": false, "properties": {
206            "retention": { "type": "object", "additionalProperties": false, "properties": {
207                "max_events": { "type": "integer", "minimum": 1 },
208                "max_age": { "type": "string" } } } } } }),
209    );
210    m.insert(
211        "services".to_string(),
212        json!({ "type": "object", "additionalProperties": { "$ref": "#/$defs/Service" },
213                "description": "the service catalog: the named external services this deployment may use; mcp.servers entries reference entries via `service:` and may only narrow them" }),
214    );
215    m.insert("vars".to_string(), json!({ "type": "object", "additionalProperties": true,
216                "description": "operator-defined constants; reference anywhere (and in workflows) as {{config.NAME}} — dotted paths reach nested values, unresolved references refuse startup" }));
217    m.insert("workflows".to_string(), json!({ "type": "array", "items": { "$ref": "#/$defs/WorkflowRef" }, "description": "inline workflow definitions or {name, file|uri} references" }));
218    m.insert("limits".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
219                "max_runs": { "type": "integer", "minimum": 1 },
220                "run": { "type": "object", "additionalProperties": false, "properties": {
221                    "steps": { "type": "integer", "minimum": 1 }, "tokens": { "type": "integer", "minimum": 1 }, "deadline": duration } },
222                "subagents": { "type": "object", "additionalProperties": false, "properties": {
223                    "depth": { "type": "integer", "minimum": 0 }, "breadth": { "type": "integer", "minimum": 1 },
224                    "total": { "type": "integer", "minimum": 1 }, "rate": { "type": "string", "description": "`<burst>/<per>s`, e.g. `8/2s`" },
225                    "instances": { "type": "object", "additionalProperties": false, "description": "instance-tier children: defaults 2 live / 8 lifetime / 4/1h", "properties": {
226                        "breadth": { "type": "integer", "minimum": 1 }, "total": { "type": "integer", "minimum": 1 }, "rate": { "type": "string" } } } } },
227                "inline_max_bytes": { "type": "integer", "minimum": 1 },
228                "step_timeout": duration,
229                "max_message_depth": { "type": "integer", "minimum": 1, "description": "how many chained `message` deliveries may run before one is refused (default 8) — the loop guard on message → turn → run → message" },
230                "workflow": { "type": "object", "additionalProperties": false, "properties": {
231                    "fan_out": { "type": "integer", "minimum": 1, "description": "max concurrent lanes a foreach/batch body may use; a definition asking for more is refused at load" } } } } }));
232    m.insert("lifecycle".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
233                "run_until": { "enum": ["auto", "idle", "drained"] },
234                "idle_grace": duration,
235                "drain_timeout": duration,
236                "run_id": { "type": "string" },
237                "exit_code_map": { "type": "object", "additionalProperties": { "type": "integer", "minimum": 0, "maximum": 255 }, "description": "remap the policy exit codes (3/7 only): {\"3\": N, \"7\": N}" },
238                "watch_config": { "type": "boolean" },
239                "until_signal": { "type": "string", "description": "delivery of this signal begins graceful shutdown — the retirement trigger a parent composes into an instance-tier child" } } }));
240    m.insert("subagents".to_string(), json!({ "type": "object", "additionalProperties": false,
241                "description": "subagent templates + spawn policy: operator-declared definitions the model may instantiate, filling declared params only",
242                "properties": {
243                "allow_freeform": { "type": "boolean", "description": "false = templates are the ONLY spawn path (freeform flat-tier instruction spawns are refused); default true" },
244                "defaults": { "type": "object", "additionalProperties": false, "description": "applied to every spawn unless overridden at the template or call site", "properties": {
245                    "model": { "type": "string" }, "priority": { "enum": ["low", "normal", "high"] },
246                    "mode": { "enum": ["sync", "async", "detached", "warm"] },
247                    "limits": { "type": "object", "additionalProperties": true },
248                    "durable": { "type": "boolean", "description": "default durability class for spawns (false = memory-only records)" } } },
249                "templates": { "type": "object", "additionalProperties": { "$ref": "#/$defs/SubagentTemplate" } } } }));
250    m.insert("a2a".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
251                "listen": { "type": "string", "description": "https://host:port (loopback http:// for dev)" },
252                "tls": { "type": "object", "additionalProperties": false, "properties": {
253                    "cert": { "type": "string" }, "key": { "type": "string" }, "client_ca": { "type": "string" } } },
254                "bearer": secret,
255                "principals": { "type": "array", "items": { "$ref": "#/$defs/Principal" } },
256                "peers": { "type": "array", "items": { "$ref": "#/$defs/A2aPeer" } },
257                "conversation_ttl": duration,
258                "push": { "type": "object", "additionalProperties": false,
259                    "description": "push notifications: a caller registers a webhook and agentd POSTs its task's updates there. Default-OFF — the URL comes from a peer, so making the request at all is the operator's decision.",
260                    "properties": {
261                    "enabled": { "type": "boolean", "description": "accept CreateTaskPushNotificationConfig and deliver on transitions" },
262                    "allow_private": { "type": "boolean", "description": "permit webhook targets on private / loopback addresses (a separate and larger decision — a peer could otherwise reach agentd's own surfaces or a cloud metadata endpoint)" } } } } }));
263    m.insert("interface".to_string(), json!({ "type": "object", "additionalProperties": false,
264                "description": "The display-client (TUI/web-UI) surface, served on the A2A listener. Default-OFF.",
265                "properties": {
266                "enabled": { "type": "boolean", "description": "serve the interface methods (SubscribeToEvents, interface.info, …)" },
267                "debug": { "type": "boolean", "description": "expose extra debug information (transcripts, run step detail, the log ring, audit feed events); runtime-togglable via the config.set op" },
268                "origins": { "type": "array", "items": { "type": "string" }, "description": "extra allowed browser origins (scheme://host[:port]) for a hosted web UI; loopback origins never need listing" },
269                "display": { "type": "object", "additionalProperties": false,
270                    "description": "what clients render in their chrome — ordered item lists for the top (header) and bottom (status bar) edges; unknown items are skipped",
271                    "properties": {
272                    "top": { "type": "array", "items": { "type": "string" } },
273                    "bottom": { "type": "array", "items": { "type": "string" } } } },
274                "pairing": { "type": "object", "additionalProperties": false,
275                    "description": "pairing-code login: a rotating 6-digit code (shown to operators) a client exchanges for a session token — the low-friction alternative to copying a bearer",
276                    "properties": {
277                    "enabled": { "type": "boolean" },
278                    "role": { "enum": ["operator", "user", "agent", "anonymous"], "description": "the role a paired session gets (operator or user; default operator)" },
279                    "ttl": { "type": ["string", "integer"], "description": "session-token lifetime (default 12h)" } } } } }));
280    m.insert("webhooks".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
281                "listen": { "type": "string", "description": "https://host:port (loopback http:// for dev) — the inbound webhook surface" },
282                "tls": { "type": "object", "additionalProperties": false, "properties": {
283                    "cert": { "type": "string" }, "key": { "type": "string" }, "client_ca": { "type": "string" } } },
284                "default_auth": { "$ref": "#/$defs/WebhookAuth" } } }));
285    m.insert("goal".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
286                "statement": { "type": "string", "description": "the goal in natural language (the LLM judge reads it)" },
287                "check": { "type": "object", "additionalProperties": false, "properties": {
288                    "every": duration, "condition": { "type": "string", "description": "a cheap CEL predicate over durable state, evaluated first" }, "via": { "enum": ["both", "condition", "agent"] } } },
289                "stuck_after": { "type": "integer", "minimum": 1 },
290                "on_achieved": { "$ref": "#/$defs/GoalAction" },
291                "on_stuck": { "$ref": "#/$defs/GoalAction" } } }));
292    m.insert("observability".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
293                "log_level": { "enum": ["trace", "debug", "info", "warn", "error"] },
294                "log_content": { "type": "boolean" },
295                "otel": { "type": "object", "additionalProperties": false, "properties": {
296                    "endpoint": { "type": "string" }, "traces": { "type": "boolean" }, "metrics": { "type": "boolean" }, "logs": { "type": "boolean" } } },
297                "metrics_addr": { "type": "string" },
298                "health_file": { "type": "string" },
299                "report_file": { "type": "string" },
300                "events_ring": { "type": "integer", "minimum": 1 },
301                "audit": { "type": "object", "additionalProperties": false, "properties": {
302                    "sink": { "type": "array", "items": { "enum": ["log", "store", "stream"] } },
303                    "stream": { "type": "string", "description": "the declared stream `sink: [stream]` appends to — the supported path off the box, and the only sink a workflow can consume" } } },
304                "runtime_events": { "type": "object", "additionalProperties": false, "properties": {
305                    "stream": { "type": "string", "description": "declared stream the selected events land on" },
306                    "include": { "type": "array", "items": { "type": "string" }, "description": "event families taken in full (the segment before the first dot); an unknown family is a startup error" },
307                    "sampled": { "type": "array", "items": { "type": "string" }, "description": "event families taken at 1-in-16 — for high-rate families that arrive in storms" },
308                    "queue": { "type": "integer", "minimum": 1, "description": "how many events may queue between ticks before the tap drops and counts (default 512)" } } },
309                "traceparent": { "type": "string" } } }));
310    m.insert("identity".to_string(), json!({ "type": "object", "additionalProperties": false,
311                "description": "who work is done ON BEHALF OF — including work nobody typed",
312                "properties": {
313                "autonomous_as": { "type": "string", "description": "the actor a schedule/webhook/stream firing is attributed to (default `system`); without it the attribution chain is dropped at its first hop" },
314                "labels": { "type": "object", "additionalProperties": { "type": "string" }, "description": "labels stamped on autonomous work" } } }));
315    m.insert("security".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
316                "allow_trifecta": { "type": "boolean" },
317                "policies": { "type": "array", "description": "ordered verdicts on a tool call; first match wins, no match is allow", "items": {
318                    "type": "object", "additionalProperties": false, "properties": {
319                        "match": { "type": "object", "additionalProperties": false, "properties": {
320                            "tool": { "type": "string", "description": "tool-name glob; absent matches every tool" },
321                            "tags": { "type": "array", "items": { "enum": ["untrusted_input", "sensitive", "egress"] }, "description": "every listed trifecta tag must be present on the tool" },
322                            "caller": { "type": "array", "items": { "enum": ["root", "workflow", "subagent"] } },
323                            "principal": { "type": "string", "description": "principal-id glob, for calls carrying one" },
324                            "args": { "type": "string", "description": "CEL over `args`, `tool` and `caller` — the only place an ARGUMENT can be judged, since grants are name patterns" } } },
325                        "action": { "enum": ["allow", "deny", "ask", "shadow"], "description": "shadow refuses and says the call was held; it never fabricates a result" },
326                        "question": { "type": "string", "description": "the question put to a person for `ask`; {{tool}}, {{caller}} and {{args}} are substituted" },
327                        "on_timeout": { "enum": ["allow", "deny", "ask", "shadow"], "description": "what an unanswered `ask` becomes (default deny)" },
328                        "timeout": { "type": "string" } } } },
329                "workflows": { "type": "object", "additionalProperties": false, "properties": {
330                    "immutable": { "type": "boolean", "description": "refuse workflow.create/update/delete at runtime — definitions become read-only for the model, subagents and operators alike; loading from config/file/url/dir is unaffected" } } },
331                "tls_ca": { "type": "string" },
332                "aauth": { "$ref": "#/$defs/AAuth" },
333                "cgroup": { "type": "object", "additionalProperties": false, "properties": {
334                    "spec": { "type": "string" }, "memory_max": { "type": "string" }, "pids_max": { "type": "string" } } },
335                "exec": { "type": "object", "additionalProperties": false,
336                    "description": "The guarded local command runner (default-OFF; needs --features exec).", "properties": {
337                    "enabled": { "type": "boolean" },
338                    "allow": { "type": "array", "items": { "type": "string" }, "description": "allow-listed command names (argv[0])" },
339                    "workdir": { "type": "string" }, "timeout": duration,
340                    "max_output": { "type": "integer" },
341                    "env": { "type": "array", "items": { "type": "string" }, "description": "env var names passed through" } } },
342                "egress": { "enum": ["open", "closed"], "description": "closed = an outbound MCP dial whose URL matches no services: catalog entry is refused; default open" } } }));
343}
344
345fn defs_properties(
346    m: &mut Map<String, Value>,
347    secret: &Value,
348    string_map: &Value,
349    budget: &Value,
350    duration: &Value,
351) {
352    m.insert("BudgetWindow".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["per"], "properties": {
353                "per": { "enum": ["second", "minute", "hour", "day", "week"] },
354                "tokens": { "type": "integer", "minimum": 1 },
355                "requests": { "type": "integer", "minimum": 1 },
356                "reset": { "type": "string", "pattern": "^[0-9]{2}:[0-9]{2}Z$", "description": "calendar-window reset time (UTC), e.g. 00:00Z" } } }));
357    m.insert("Pricing".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
358                "input_per_1k": { "type": "number", "minimum": 0 }, "output_per_1k": { "type": "number", "minimum": 0 }, "currency": { "type": "string" } } }));
359    m.insert("McpServer".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["name"],
360                "oneOf": [ { "required": ["endpoint"] }, { "required": ["service"] } ],
361                "properties": {
362                "name": { "type": "string", "pattern": "^[a-zA-Z0-9_-]+$" },
363                "endpoint": { "type": "string" },
364                "service": { "type": "string", "description": "reference a services: catalog entry — inherit its connection settings (restating endpoint/auth/headers is refused) and narrow its tool ceiling" },
365                "ns": { "type": "string", "pattern": "^[a-zA-Z0-9_-]+$", "description": "tool namespace prefix (`ns.tool`)" },
366                "headers": string_map,
367                "tags": { "type": "object", "additionalProperties": { "type": "array", "items": { "enum": ["untrusted_input", "sensitive", "egress"] } } },
368                "allow": { "type": "array", "items": { "type": "string" }, "description": "admit only advertised tools matching these globs" },
369                "exclude": { "type": "array", "items": { "type": "string" }, "description": "never admit advertised tools matching these globs (beats allow)" },
370                "aauth": { "type": "boolean" },
371                "oauth": { "type": "object", "additionalProperties": false, "required": ["token_url", "client_id", "client_secret"], "properties": {
372                    "token_url": { "type": "string" }, "client_id": { "type": "string" }, "client_secret": secret, "scope": { "type": "string" } } },
373                "auth": { "$ref": "#/$defs/Auth" },
374                "timeout": duration } }));
375    m.insert("Auth".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["kind"],
376                "description": "A unified credential provider.", "properties": {
377                "kind": { "enum": ["static", "oauth2", "aws", "spiffe"] },
378                "issuer": { "type": "string" }, "token_url": { "type": "string" },
379                "device_authorization_url": { "type": "string" }, "authorization_url": { "type": "string" },
380                "client_id": { "type": "string" }, "client_secret": secret,
381                "grant": { "enum": ["device", "authorization_code", "client_credentials"] },
382                "scopes": { "type": "array", "items": { "type": "string" } }, "audience": { "type": "string" },
383                "token": secret, "header": { "type": "string" }, "value": secret,
384                "region": { "type": "string" }, "service": { "type": "string" },
385                "source": { "enum": ["env", "static", "imds", "irsa", "sso"] },
386                "sso_start_url": { "type": "string" }, "account_id": { "type": "string" }, "role_name": { "type": "string" },
387                "svid": { "enum": ["jwt", "x509"] }, "jwt_svid_file": { "type": "string" },
388                "svid_file": { "type": "string" }, "key_file": { "type": "string" } } }));
389    m.insert("ToolOverride".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["server", "tool"], "properties": {
390                "server": { "type": "string" }, "tool": { "type": "string" },
391                "args": { "type": "string", "description": "a JSON template or `CEL: …` producing the MCP tool arguments from `args`/`ctx`" },
392                "result": { "type": "string", "description": "a JSON pointer / template / `CEL: …` mapping the CallToolResult to the internal output schema" } } }));
393    m.insert("WebhookAuth".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
394                "hmac": { "type": "object", "additionalProperties": false, "properties": {
395                    "secret": secret, "header": { "type": "string", "description": "the header carrying the signature (default X-Signature)" }, "algo": { "enum": ["sha256"] }, "prefix": { "type": "string", "description": "a prefix stripped before the constant-time compare, e.g. sha256=" } } },
396                "bearer": secret,
397                "header": { "type": "object", "additionalProperties": false, "properties": { "name": { "type": "string" }, "equals": secret } },
398                "none": { "type": "boolean", "description": "loopback-only, no auth (dev) — explicit opt-in" } } }));
399    m.insert("GoalAction".to_string(), json!({ "oneOf": [
400                { "enum": ["finish", "idle", "replan", "escalate"] },
401                { "type": "object", "additionalProperties": false, "required": ["workflow"], "properties": { "workflow": { "type": "string" } } } ] }));
402    m.insert("StoreOp".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["tool"], "properties": {
403                "tool": { "type": "string" }, "args": { "type": "string" }, "ok": { "type": "string" }, "conflict": { "type": "string" },
404                "value": { "type": "string" }, "keys": { "type": "string" } } }));
405    m.insert("StoreMcp".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["server"], "properties": {
406                "server": { "type": "string" },
407                "put": { "$ref": "#/$defs/StoreOp" }, "get": { "$ref": "#/$defs/StoreOp" },
408                "list": { "$ref": "#/$defs/StoreOp" }, "delete": { "$ref": "#/$defs/StoreOp" } } }));
409    m.insert("HttpOp".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["url"], "properties": {
410                "method": { "enum": ["GET", "PUT", "POST", "DELETE"] }, "url": { "type": "string" }, "body": { "type": "string" },
411                "value": { "type": "string" }, "keys": { "type": "string" }, "conflict_status": { "type": "integer", "minimum": 100, "maximum": 599 } } }));
412    m.insert("StoreHttp".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["base_url"], "properties": {
413                "base_url": { "type": "string" }, "headers": string_map,
414                "get": { "$ref": "#/$defs/HttpOp" }, "put": { "$ref": "#/$defs/HttpOp" },
415                "list": { "$ref": "#/$defs/HttpOp" }, "delete": { "$ref": "#/$defs/HttpOp" } } }));
416    // The file store deliberately exposes one knob — where the state lives —
417    // and even that is optional: an omitted `path` resolves through
418    // $AGENTD_STATE_DIR / $XDG_STATE_HOME, so durability needs no config.
419    m.insert("StoreFile".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
420                "min_free": { "type": "string", "description": "shed new work below this much free disk (256MB, 1.5GiB, bytes; 0 disables); warn at twice it" },
421                "path": { "type": "string", "description": "the state root; default $AGENTD_STATE_DIR, else $XDG_STATE_HOME/agentd/state, else $HOME/.local/state/agentd/state, else the OS temp dir" } } }));
422    m.insert("SkillSource".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["server"], "properties": {
423                "server": { "type": "string" }, "discover": { "enum": ["prompts", "resources", "auto"] }, "filter": { "type": "string" } } }));
424    // A `workflows[]` entry is either a REFERENCE (`file`/`uri`/`url`/`dir`) or
425    // an inline definition — and most people write them inline, which is where
426    // an editor's completion earns its keep. So the workflow document's own
427    // properties are folded in beside the reference fields, from the same
428    // `KINDS`-derived schema the validator uses. Without this an inline
429    // workflow was `additionalProperties: true`: no completion for `steps`, no
430    // node kinds, and a typo caught only at startup.
431    //
432    // Merged rather than expressed as a `oneOf` on purpose. A `oneOf` would
433    // let the schema also catch "you gave both `file` and `steps`" — which the
434    // loader already refuses with a better message ("one entry, one source") —
435    // at the cost of ambiguous completion in every editor, since neither
436    // branch matches a half-written entry. Completion is the job here.
437    let workflow_doc = crate::engine::model::workflow_schema();
438    let mut wf_ref = json!({ "type": "object", "required": ["name"], "properties": {
439                "name": { "type": "string" }, "armed": { "type": "boolean" },
440                "file": { "type": "string", "description": "a path on disk" },
441                "uri": { "type": "string", "description": "an MCP resource (mcp://<server>/<uri>, or one a connected server serves)" },
442                "url": { "type": "string", "description": "fetched over HTTP(S) at startup; fail-closed if unreachable" },
443                "headers": { "type": "object", "additionalProperties": { "type": "string" }, "description": "headers for `url` — credential values must be {{secret:…}} references" },
444                "timeout": duration,
445                "allow_private": { "type": "boolean", "description": "permit `url` to resolve to a private/loopback address" },
446                "dir": { "type": "string", "description": "load every matching file in a directory" },
447                "glob": { "type": "string", "description": "comma-separated globs relative to `dir` (default `*.yaml,*.yml,*.json`); `**` recurses" } },
448                "additionalProperties": false,
449                "description": "a {name, file|uri|url} reference, a {dir, glob} directory, or an inline workflow definition" });
450    if let (Some(dst), Some(src)) = (
451        wf_ref["properties"].as_object_mut(),
452        workflow_doc.get("properties").and_then(Value::as_object),
453    ) {
454        for (k, v) in src {
455            // The reference fields win where the names collide (`name`,
456            // `armed`, `description`): those are the config layer's own.
457            dst.entry(k.clone()).or_insert_with(|| v.clone());
458        }
459    }
460    m.insert("WorkflowRef".to_string(), wf_ref);
461    // The workflow document's own `$defs` (`step`, `kinds`) come along, so the
462    // `#/$defs/step` references inside the folded properties still resolve.
463    if let Some(src) = workflow_doc.get("$defs").and_then(Value::as_object) {
464        for (k, v) in src {
465            m.entry(k.clone()).or_insert_with(|| v.clone());
466        }
467    }
468    m.insert("Principal".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["match", "role"], "properties": {
469                "match": { "type": "object", "additionalProperties": false, "properties": {
470                    "san": { "type": "string" }, "sub": { "type": "string" }, "bearer_ref": { "type": "string" }, "aauth_agent": { "type": "string" }, "any": { "type": "boolean" } } },
471                "role": { "enum": ["operator", "user", "agent", "anonymous"] },
472                "grants": { "type": "array", "items": { "type": "string" } },
473                "quotas": { "type": "object", "additionalProperties": false, "properties": {
474                    "rate": { "type": "string", "description": "`<burst>/<per>s` arrival quota; operators are exempt" }, "budget": budget } },
475                "labels": { "type": "object", "additionalProperties": { "type": "string" }, "description": "operator-declared attributes carried into the run, the MCP `_meta` and the audit line" } } }));
476    m.insert("SubagentTemplate".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["instruction"],
477                "description": "an operator-declared subagent definition: `instruction` is a full instruction document — no config-defining directives = the flat worker; machinery (:::workflow/:::mcp/:::stream/:::config/:::tools) = an instance-tier child",
478                "properties": {
479                "instruction": { "type": "string", "description": "the definition; {{params.X}} holes fold in at spawn as data, never re-parsed for directives" },
480                "params": { "type": "object", "additionalProperties": { "$ref": "#/$defs/ParamSpec" }, "description": "the ONLY holes the model may fill, schema-validated at spawn" },
481                "servers": { "type": "array", "items": { "type": "string" }, "description": "flat tier: narrowing server grants from the parent's set" },
482                "tools": { "type": "array", "items": { "type": "string" }, "description": "flat tier: narrowing tool grants" },
483                "limits": { "type": "object", "additionalProperties": true, "description": "flat tier: the per-spawn limits object; instance tier: OS caps only (memory, cpu)" },
484                "mode": { "enum": ["sync", "async", "detached", "warm"], "description": "instance tier supports detached only (phase A)" },
485                "model": { "type": "string" }, "priority": { "enum": ["low", "normal", "high"] },
486                "skills": { "type": "array", "items": { "type": "string" } },
487                "context": { "type": "array", "items": { "type": "object" } },
488                "output_contract": { "type": "string" }, "output_schema": { "type": "object", "additionalProperties": true },
489                "budget": budget,
490                "ttl": { "type": ["string", "integer"], "description": "instance tier: retire after this long (graceful drain)" },
491                "until": { "type": "string", "description": "instance tier: a signal name (templated over params) whose delivery in the child retires it" },
492                "singleton": { "type": "boolean", "description": "one live child; its A2A peer alias is the template name" },
493                "durable": { "type": "boolean", "description": "false = memory-only record (an instance child runs on a memory store; no restore-respawn); absent = the store.durability.work default" },
494                "result": { "type": "object", "additionalProperties": false, "required": ["workflow"], "properties": { "workflow": { "type": "string" } },
495                            "description": "instance mode: sync — resolve the spawn when the child's named workflow first completes, returning its output (needs a parent A2A listener)" },
496                "mirror_streams": { "type": "array", "items": { "type": "string" },
497                            "description": "child streams mirrored into the parent's same-named streams (declared on both sides; needs a parent A2A listener)" } } }));
498    m.insert(
499        "ParamSpec".to_string(),
500        json!({ "type": "object", "additionalProperties": false, "properties": {
501                "type": { "enum": ["string", "number", "integer", "boolean"] },
502                "required": { "type": "boolean" },
503                "default": {},
504                "enum": { "type": "array" },
505                "description": { "type": "string" } } }),
506    );
507    m.insert("Service".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["endpoint"],
508                "description": "a service-catalog entry: connection settings, one shared credential, authoritative trifecta tags (a floor for any matching endpoint), and a tool-surface ceiling consumers can only narrow",
509                "properties": {
510                "kind": { "enum": ["mcp"], "description": "phase A: mcp only (intelligence/peer/http reserved)" },
511                "endpoint": { "type": "string", "description": "the connection URL and the dial-time match base (scheme + authority + path prefix)" },
512                "headers": string_map,
513                "tags": { "type": "object", "additionalProperties": { "type": "array", "items": { "enum": ["untrusted_input", "sensitive", "egress"] } },
514                          "description": "authoritative — unioned into any consumer whose endpoint matches, referencing or inline, open or closed" },
515                "allow": { "type": "array", "items": { "type": "string" }, "description": "the CEILING: the widest advertised-tool surface any consumer may get" },
516                "exclude": { "type": "array", "items": { "type": "string" }, "description": "never admitted, unioned into every consumer (beats allow)" },
517                "auth": { "$ref": "#/$defs/Auth" },
518                "rate": { "type": "string", "description": "per-instance pacing toward the service (`<burst>/<per>`, e.g. `60/1m`)" },
519                "timeout": duration } }));
520    m.insert("A2aPeer".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["name", "endpoint"], "properties": {
521                "name": { "type": "string", "pattern": "^[a-zA-Z0-9_-]+$" }, "endpoint": { "type": "string" },
522                "headers": string_map, "client_cert": { "type": "string" }, "client_key": { "type": "string" },
523                "auth": { "$ref": "#/$defs/Auth" } } }));
524    m.insert("AAuth".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["provider"], "properties": {
525                "provider": { "type": "string" }, "key_file": { "type": "string" }, "enroll_token": secret,
526                "enroll_assertion_file": { "type": "string" }, "person_server": { "type": "string" } } }));
527}