1use serde_json::{Map, Value, json};
16
17pub const SCHEMA_CONTRACT_VERSION: &str = "1.0";
20
21pub 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 "$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 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 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 dst.entry(k.clone()).or_insert_with(|| v.clone());
458 }
459 }
460 m.insert("WorkflowRef".to_string(), wf_ref);
461 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}