1use serde_json::{Map, Value, json};
14
15pub const SCHEMA_CONTRACT_VERSION: &str = "2.0";
17
18pub const CONFIG_VERSION: &str = "2";
20
21pub fn schema() -> Value {
22 let duration = json!({ "type": ["string", "integer"], "description": "a duration: `10m`, `90s`, `500ms`, or bare seconds" });
23 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" });
24 let string_map = json!({ "type": "object", "additionalProperties": { "type": "string" } });
25 let tool_select = json!({
26 "oneOf": [
27 { "enum": ["all", "none"] },
28 { "type": "array", "items": { "type": "string" } }
29 ],
30 "description": "`all` | `none` | a list of names"
31 });
32 let budget = json!({
33 "type": "object",
34 "additionalProperties": false,
35 "properties": {
36 "windows": { "type": "array", "items": { "$ref": "#/$defs/BudgetWindow" } },
37 "lifetime_tokens": { "type": "integer", "minimum": 0, "description": "hard ceiling; 0 = unbounded" },
38 "scope": { "type": "array", "items": { "enum": ["instance", "run", "conversation", "principal"] } },
39 "on_exhausted": { "enum": ["wait", "slow", "degrade", "refuse", "fail"] },
40 "slow": { "type": "object", "additionalProperties": false, "properties": { "factor": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 } } },
41 "degrade": { "type": "object", "additionalProperties": false, "properties": { "model": { "type": "string" } } },
42 "reserve": { "type": "object", "additionalProperties": false, "properties": {
43 "estimate": { "enum": ["context", "fixed", "none"] },
44 "fixed": { "type": "integer", "minimum": 0 } } }
45 }
46 });
47 let mut properties = Map::new();
48 properties.insert(
49 "config_version".to_string(),
50 json!({ "type": "string", "const": CONFIG_VERSION, "description": "the document version" }),
51 );
52 top_level_properties(
53 &mut properties,
54 &duration,
55 &secret,
56 &string_map,
57 &tool_select,
58 &budget,
59 );
60 let mut defs = Map::new();
61 defs_properties(&mut defs, &secret, &string_map, &budget, &duration);
62 json!({
63 "$schema": "https://json-schema.org/draft/2020-12/schema",
64 "$id": format!("https://agentd.dev/schema/config/{SCHEMA_CONTRACT_VERSION}"),
65 "x-agentd-contract-version": SCHEMA_CONTRACT_VERSION,
66 "title": "agentd settings (v2)",
67 "description": "agentd 2.0 configuration document (YAML or JSON; several files merge in order; every path is also AGENTD_<PATH> and --<path>)",
68 "type": "object",
69 "additionalProperties": false,
70 "properties": Value::Object(properties),
71 "$defs": Value::Object(defs)
72 })
73}
74
75#[allow(clippy::too_many_arguments)]
76fn top_level_properties(
77 m: &mut Map<String, Value>,
78 duration: &Value,
79 secret: &Value,
80 string_map: &Value,
81 tool_select: &Value,
82 budget: &Value,
83) {
84 m.insert("agent".to_string(), json!({
85 "type": "object", "additionalProperties": false,
86 "properties": {
87 "name": { "type": "string", "description": "instance identity (falls back to the downward-API instance, then the hostname)" },
88 "instruction": { "type": "string", "description": "static text, or a single-token URI a configured MCP server serves (read + subscribed)" },
89 "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)" },
90 "preflight": { "enum": ["never", "auto", "always"] },
91 "wake_on": { "type": "array", "items": { "enum": ["a2a_message", "human_reply", "subagent_result", "workflow_finished", "workflow_failed", "instruction_updated", "budget_resumed"] } },
92 "on_workflow_finished": { "enum": ["ignore", "note", "think"] },
93 "tools": { "type": "object", "additionalProperties": false, "properties": {
94 "internal": tool_select, "mcp": tool_select, "code": tool_select } },
95 "max_parallel_turns": { "type": "integer", "minimum": 1 },
96 "conversation_budget": budget,
97 "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)" }
98 }
99 }));
100 m.insert("intelligence".to_string(), json!({
101 "type": "object", "additionalProperties": false,
102 "properties": {
103 "endpoints": { "oneOf": [ { "type": "array", "items": { "type": "string" } }, { "type": "string" } ], "description": "ordered endpoint list (failover); one comma-separated string is accepted" },
104 "model": { "type": "string" },
105 "dialect": { "enum": ["openai", "anthropic", "bedrock"], "description": "wire dialect; bedrock = native Amazon Bedrock Converse (pair with auth.kind=aws)" },
106 "token": secret,
107 "token_file": { "type": "string" },
108 "headers": string_map,
109 "auth": { "$ref": "#/$defs/Auth" },
110 "swap_policy": { "enum": ["finish-on-old", "restart-turn"] },
111 "structured_output": { "enum": ["auto", "json_schema", "tool", "prompt"] },
112 "budget": budget,
113 "pricing": { "type": "object", "additionalProperties": { "$ref": "#/$defs/Pricing" } },
114 "timeout": duration
115 }
116 }));
117 m.insert(
118 "mcp".to_string(),
119 json!({
120 "type": "object", "additionalProperties": false,
121 "properties": {
122 "servers": { "type": "array", "items": { "$ref": "#/$defs/McpServer" } },
123 "default_timeout": duration
124 }
125 }),
126 );
127 m.insert("tools".to_string(), json!({
128 "type": "object", "additionalProperties": false,
129 "properties": {
130 "disabled": { "type": "array", "items": { "type": "string" } },
131 "overrides": { "type": "object", "additionalProperties": { "$ref": "#/$defs/ToolOverride" } }
132 }
133 }));
134 m.insert("store".to_string(), json!({
135 "type": "object", "additionalProperties": false,
136 "properties": {
137 "kind": { "enum": ["mcp", "http", "file", "memory", "none"] },
138 "prefix": { "type": "string" },
139 "mcp": { "$ref": "#/$defs/StoreMcp" },
140 "http": { "$ref": "#/$defs/StoreHttp" },
141 "file": { "$ref": "#/$defs/StoreFile" },
142 "checkpoint": { "type": "object", "additionalProperties": false, "properties": { "debounce_ms": { "type": "integer", "minimum": 0 } } },
143 "durability": { "type": "object", "additionalProperties": false, "properties": {
144 "a2a": { "enum": ["strict", "eventual"] }, "steps": { "enum": ["strict", "eventual"] } } },
145 "on_error": { "enum": ["halt", "degrade"] },
146 "audit": { "type": "boolean" },
147 "timeout": duration
148 }
149 }));
150 m.insert(
151 "memory".to_string(),
152 json!({ "type": "object", "additionalProperties": false, "properties": {
153 "max_value_bytes": { "type": "integer", "minimum": 1 },
154 "list_default_limit": { "type": "integer", "minimum": 1 } } }),
155 );
156 m.insert("context".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
157 "compact_at": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 },
158 "keep_last": { "type": "integer", "minimum": 0 },
159 "model_window": { "type": "integer", "minimum": 1, "description": "the model's context window in tokens (overrides the value inferred from intelligence.model)" },
160 "plan": { "type": "object", "additionalProperties": false, "properties": { "max_items": { "type": "integer", "minimum": 1 } } } } }));
161 m.insert("knowledge".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
162 "server": { "type": "string" },
163 "auto_context": { "type": "object", "additionalProperties": false, "properties": {
164 "on": { "enum": ["turn", "never"] }, "top_k": { "type": "integer", "minimum": 1 }, "max_bytes": { "type": "integer", "minimum": 1 } } } } }));
165 m.insert("search".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": { "server": { "type": "string" } } }));
166 m.insert(
167 "skills".to_string(),
168 json!({ "type": "object", "additionalProperties": false, "properties": {
169 "sources": { "type": "array", "items": { "$ref": "#/$defs/SkillSource" } },
170 "reference_prefix": { "type": "string" },
171 "max_loaded": { "type": "integer", "minimum": 1 },
172 "max_bytes": { "type": "integer", "minimum": 1 } } }),
173 );
174 m.insert("workflows".to_string(), json!({ "type": "array", "items": { "$ref": "#/$defs/WorkflowRef" }, "description": "inline dialect-3 definitions or {name, file|uri} references" }));
175 m.insert("limits".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
176 "max_runs": { "type": "integer", "minimum": 1 },
177 "run": { "type": "object", "additionalProperties": false, "properties": {
178 "steps": { "type": "integer", "minimum": 1 }, "tokens": { "type": "integer", "minimum": 1 }, "deadline": duration } },
179 "subagents": { "type": "object", "additionalProperties": false, "properties": {
180 "depth": { "type": "integer", "minimum": 0 }, "breadth": { "type": "integer", "minimum": 1 },
181 "total": { "type": "integer", "minimum": 1 }, "rate": { "type": "string", "description": "`<burst>/<per>s`, e.g. `8/2s`" } } },
182 "inline_max_bytes": { "type": "integer", "minimum": 1 },
183 "step_timeout": duration } }));
184 m.insert("lifecycle".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
185 "run_until": { "enum": ["auto", "idle", "drained"] },
186 "idle_grace": duration,
187 "drain_timeout": duration,
188 "run_id": { "type": "string" },
189 "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}" },
190 "watch_config": { "type": "boolean" } } }));
191 m.insert("a2a".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
192 "listen": { "type": "string", "description": "https://host:port (loopback http:// for dev)" },
193 "tls": { "type": "object", "additionalProperties": false, "properties": {
194 "cert": { "type": "string" }, "key": { "type": "string" }, "client_ca": { "type": "string" } } },
195 "bearer": secret,
196 "principals": { "type": "array", "items": { "$ref": "#/$defs/Principal" } },
197 "peers": { "type": "array", "items": { "$ref": "#/$defs/A2aPeer" } },
198 "conversation_ttl": duration,
199 "push": { "type": "object", "additionalProperties": false,
200 "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.",
201 "properties": {
202 "enabled": { "type": "boolean", "description": "accept CreateTaskPushNotificationConfig and deliver on transitions" },
203 "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)" } } } } }));
204 m.insert("interface".to_string(), json!({ "type": "object", "additionalProperties": false,
205 "description": "The display-client (TUI/web-UI) surface (RFC 0032), served on the A2A listener. Default-OFF.",
206 "properties": {
207 "enabled": { "type": "boolean", "description": "serve the interface methods (SubscribeToEvents, interface.info, …)" },
208 "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" },
209 "origins": { "type": "array", "items": { "type": "string" }, "description": "extra allowed browser origins (scheme://host[:port]) for a hosted web UI; loopback origins never need listing" },
210 "display": { "type": "object", "additionalProperties": false,
211 "description": "what clients render in their chrome — ordered item lists for the top (header) and bottom (status bar) edges; unknown items are skipped",
212 "properties": {
213 "top": { "type": "array", "items": { "type": "string" } },
214 "bottom": { "type": "array", "items": { "type": "string" } } } },
215 "pairing": { "type": "object", "additionalProperties": false,
216 "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",
217 "properties": {
218 "enabled": { "type": "boolean" },
219 "role": { "enum": ["operator", "user", "agent", "anonymous"], "description": "the role a paired session gets (operator or user; default operator)" },
220 "ttl": { "type": ["string", "integer"], "description": "session-token lifetime (default 12h)" } } } } }));
221 m.insert("webhooks".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
222 "listen": { "type": "string", "description": "https://host:port (loopback http:// for dev) — the inbound webhook surface" },
223 "tls": { "type": "object", "additionalProperties": false, "properties": {
224 "cert": { "type": "string" }, "key": { "type": "string" }, "client_ca": { "type": "string" } } },
225 "default_auth": { "$ref": "#/$defs/WebhookAuth" } } }));
226 m.insert("goal".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
227 "statement": { "type": "string", "description": "the goal in natural language (the LLM judge reads it)" },
228 "check": { "type": "object", "additionalProperties": false, "properties": {
229 "every": duration, "condition": { "type": "string", "description": "a cheap CEL predicate over durable state, evaluated first" }, "via": { "enum": ["both", "condition", "agent"] } } },
230 "stuck_after": { "type": "integer", "minimum": 1 },
231 "on_achieved": { "$ref": "#/$defs/GoalAction" },
232 "on_stuck": { "$ref": "#/$defs/GoalAction" } } }));
233 m.insert("observability".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
234 "log_level": { "enum": ["trace", "debug", "info", "warn", "error"] },
235 "log_content": { "type": "boolean" },
236 "otel": { "type": "object", "additionalProperties": false, "properties": {
237 "endpoint": { "type": "string" }, "traces": { "type": "boolean" }, "metrics": { "type": "boolean" }, "logs": { "type": "boolean" } } },
238 "metrics_addr": { "type": "string" },
239 "health_file": { "type": "string" },
240 "report_file": { "type": "string" },
241 "events_ring": { "type": "integer", "minimum": 1 },
242 "audit": { "type": "object", "additionalProperties": false, "properties": {
243 "sink": { "type": "array", "items": { "enum": ["log", "store"] } } } },
244 "traceparent": { "type": "string" } } }));
245 m.insert("security".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
246 "allow_trifecta": { "type": "boolean" },
247 "tls_ca": { "type": "string" },
248 "aauth": { "$ref": "#/$defs/AAuth" },
249 "cgroup": { "type": "object", "additionalProperties": false, "properties": {
250 "spec": { "type": "string" }, "memory_max": { "type": "string" }, "pids_max": { "type": "string" } } },
251 "exec": { "type": "object", "additionalProperties": false,
252 "description": "The guarded local command runner (default-OFF; needs --features exec).", "properties": {
253 "enabled": { "type": "boolean" },
254 "allow": { "type": "array", "items": { "type": "string" }, "description": "allow-listed command names (argv[0])" },
255 "workdir": { "type": "string" }, "timeout": duration,
256 "max_output": { "type": "integer" },
257 "env": { "type": "array", "items": { "type": "string" }, "description": "env var names passed through" } } } } }));
258}
259
260fn defs_properties(
261 m: &mut Map<String, Value>,
262 secret: &Value,
263 string_map: &Value,
264 budget: &Value,
265 duration: &Value,
266) {
267 m.insert("BudgetWindow".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["per"], "properties": {
268 "per": { "enum": ["second", "minute", "hour", "day", "week"] },
269 "tokens": { "type": "integer", "minimum": 1 },
270 "requests": { "type": "integer", "minimum": 1 },
271 "reset": { "type": "string", "pattern": "^[0-9]{2}:[0-9]{2}Z$", "description": "calendar-window reset time (UTC), e.g. 00:00Z" } } }));
272 m.insert("Pricing".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
273 "input_per_1k": { "type": "number", "minimum": 0 }, "output_per_1k": { "type": "number", "minimum": 0 }, "currency": { "type": "string" } } }));
274 m.insert("McpServer".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["name", "endpoint"], "properties": {
275 "name": { "type": "string", "pattern": "^[a-zA-Z0-9_-]+$" },
276 "endpoint": { "type": "string" },
277 "ns": { "type": "string", "pattern": "^[a-zA-Z0-9_-]+$", "description": "tool namespace prefix (`ns.tool`)" },
278 "headers": string_map,
279 "tags": { "type": "object", "additionalProperties": { "type": "array", "items": { "enum": ["untrusted_input", "sensitive", "egress"] } } },
280 "aauth": { "type": "boolean" },
281 "oauth": { "type": "object", "additionalProperties": false, "required": ["token_url", "client_id", "client_secret"], "properties": {
282 "token_url": { "type": "string" }, "client_id": { "type": "string" }, "client_secret": secret, "scope": { "type": "string" } } },
283 "auth": { "$ref": "#/$defs/Auth" },
284 "timeout": duration } }));
285 m.insert("Auth".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["kind"],
286 "description": "A unified credential provider (RFC 0031).", "properties": {
287 "kind": { "enum": ["static", "oauth2", "aws", "spiffe"] },
288 "issuer": { "type": "string" }, "token_url": { "type": "string" },
289 "device_authorization_url": { "type": "string" }, "authorization_url": { "type": "string" },
290 "client_id": { "type": "string" }, "client_secret": secret,
291 "grant": { "enum": ["device", "authorization_code", "client_credentials"] },
292 "scopes": { "type": "array", "items": { "type": "string" } }, "audience": { "type": "string" },
293 "token": secret, "header": { "type": "string" }, "value": secret,
294 "region": { "type": "string" }, "service": { "type": "string" },
295 "source": { "enum": ["env", "static", "imds", "irsa", "sso"] },
296 "sso_start_url": { "type": "string" }, "account_id": { "type": "string" }, "role_name": { "type": "string" },
297 "svid": { "enum": ["jwt", "x509"] }, "jwt_svid_file": { "type": "string" },
298 "svid_file": { "type": "string" }, "key_file": { "type": "string" } } }));
299 m.insert("ToolOverride".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["server", "tool"], "properties": {
300 "server": { "type": "string" }, "tool": { "type": "string" },
301 "args": { "type": "string", "description": "a JSON template or `CEL: …` producing the MCP tool arguments from `args`/`ctx`" },
302 "result": { "type": "string", "description": "a JSON pointer / template / `CEL: …` mapping the CallToolResult to the internal output schema" } } }));
303 m.insert("WebhookAuth".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
304 "hmac": { "type": "object", "additionalProperties": false, "properties": {
305 "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=" } } },
306 "bearer": secret,
307 "header": { "type": "object", "additionalProperties": false, "properties": { "name": { "type": "string" }, "equals": secret } },
308 "none": { "type": "boolean", "description": "loopback-only, no auth (dev) — explicit opt-in" } } }));
309 m.insert("GoalAction".to_string(), json!({ "oneOf": [
310 { "enum": ["finish", "idle", "replan", "escalate"] },
311 { "type": "object", "additionalProperties": false, "required": ["workflow"], "properties": { "workflow": { "type": "string" } } } ] }));
312 m.insert("StoreOp".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["tool"], "properties": {
313 "tool": { "type": "string" }, "args": { "type": "string" }, "ok": { "type": "string" }, "conflict": { "type": "string" },
314 "value": { "type": "string" }, "keys": { "type": "string" } } }));
315 m.insert("StoreMcp".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["server"], "properties": {
316 "server": { "type": "string" },
317 "put": { "$ref": "#/$defs/StoreOp" }, "get": { "$ref": "#/$defs/StoreOp" },
318 "list": { "$ref": "#/$defs/StoreOp" }, "delete": { "$ref": "#/$defs/StoreOp" } } }));
319 m.insert("HttpOp".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["url"], "properties": {
320 "method": { "enum": ["GET", "PUT", "POST", "DELETE"] }, "url": { "type": "string" }, "body": { "type": "string" },
321 "value": { "type": "string" }, "keys": { "type": "string" }, "conflict_status": { "type": "integer", "minimum": 100, "maximum": 599 } } }));
322 m.insert("StoreHttp".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["base_url"], "properties": {
323 "base_url": { "type": "string" }, "headers": string_map,
324 "get": { "$ref": "#/$defs/HttpOp" }, "put": { "$ref": "#/$defs/HttpOp" },
325 "list": { "$ref": "#/$defs/HttpOp" }, "delete": { "$ref": "#/$defs/HttpOp" } } }));
326 m.insert("StoreFile".to_string(), json!({ "type": "object", "additionalProperties": false, "properties": {
329 "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" } } }));
330 m.insert("SkillSource".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["server"], "properties": {
331 "server": { "type": "string" }, "discover": { "enum": ["prompts", "resources", "auto"] }, "filter": { "type": "string" } } }));
332 m.insert("WorkflowRef".to_string(), json!({ "type": "object", "required": ["name"], "properties": {
333 "name": { "type": "string" }, "armed": { "type": "boolean" }, "file": { "type": "string" }, "uri": { "type": "string" } },
334 "additionalProperties": true,
335 "description": "a {name, file} / {name, uri} reference, or an inline dialect-3 definition (RFC 0027)" }));
336 m.insert("Principal".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["match", "role"], "properties": {
337 "match": { "type": "object", "additionalProperties": false, "properties": {
338 "san": { "type": "string" }, "sub": { "type": "string" }, "bearer_ref": { "type": "string" }, "aauth_agent": { "type": "string" }, "any": { "type": "boolean" } } },
339 "role": { "enum": ["operator", "user", "agent", "anonymous"] },
340 "grants": { "type": "array", "items": { "type": "string" } },
341 "quotas": { "type": "object", "additionalProperties": false, "properties": {
342 "rate": { "type": "string" }, "budget": budget } } } }));
343 m.insert("A2aPeer".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["name", "endpoint"], "properties": {
344 "name": { "type": "string", "pattern": "^[a-zA-Z0-9_-]+$" }, "endpoint": { "type": "string" },
345 "headers": string_map, "client_cert": { "type": "string" }, "client_key": { "type": "string" },
346 "auth": { "$ref": "#/$defs/Auth" } } }));
347 m.insert("AAuth".to_string(), json!({ "type": "object", "additionalProperties": false, "required": ["provider"], "properties": {
348 "provider": { "type": "string" }, "key_file": { "type": "string" }, "enroll_token": secret,
349 "enroll_assertion_file": { "type": "string" }, "person_server": { "type": "string" } } }));
350}