1use serde_json::{Value, json};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct DefaultGrant {
17 pub root: bool,
18 pub workflows: bool,
19 pub subagents: bool,
20 pub user: bool,
22 pub agent: bool,
24}
25
26const ALL: DefaultGrant = DefaultGrant {
27 root: true,
28 workflows: true,
29 subagents: true,
30 user: false,
31 agent: false,
32};
33const ROOT_WF: DefaultGrant = DefaultGrant {
34 root: true,
35 workflows: true,
36 subagents: false,
37 user: false,
38 agent: false,
39};
40const ROOT_ONLY: DefaultGrant = DefaultGrant {
41 root: true,
42 workflows: false,
43 subagents: false,
44 user: false,
45 agent: false,
46};
47
48#[derive(Debug, Clone)]
50pub struct Contract {
51 pub name: &'static str,
52 pub description: &'static str,
53 pub input: Value,
54 pub output: Value,
55 pub builtin: bool,
56 pub grant: DefaultGrant,
57 pub family: &'static str,
59}
60
61fn obj(props: Value, required: &[&str]) -> Value {
62 json!({"type": "object", "properties": props, "required": required, "additionalProperties": false})
63}
64fn open_obj(props: Value, required: &[&str]) -> Value {
65 json!({"type": "object", "properties": props, "required": required})
66}
67fn any() -> Value {
68 json!({})
69}
70fn s(desc: &str) -> Value {
71 json!({"type": "string", "description": desc})
72}
73fn arr(items: Value) -> Value {
74 json!({"type": "array", "items": items})
75}
76
77pub fn contracts() -> Vec<Contract> {
79 let mut v = Vec::new();
80 let mut c = |name: &'static str,
81 family: &'static str,
82 description: &'static str,
83 input: Value,
84 output: Value,
85 builtin: bool,
86 grant: DefaultGrant| {
87 v.push(Contract {
88 name,
89 description,
90 input,
91 output,
92 builtin,
93 grant,
94 family,
95 });
96 };
97
98 c(
100 "instruction.read",
101 "instruction",
102 "Read the agent's current instruction (the brief it operates under).",
103 obj(json!({}), &[]),
104 open_obj(
105 json!({"text": {"type": "string"}, "source": {"enum": ["static", "resource"]}, "uri": {"type": "string"}, "version": {"type": "string"}}),
106 &["text", "source"],
107 ),
108 true,
109 ALL,
110 );
111 c(
112 "instruction.subscribe",
113 "instruction",
114 "(Re)subscribe to the instruction resource, or switch to another URI; an update re-reads it and wakes the agent.",
115 obj(
116 json!({"uri": s("The resource URI (omit to re-subscribe to the current one)")}),
117 &[],
118 ),
119 open_obj(
120 json!({"subscribed": {"type": "boolean"}, "uri": {"type": "string"}}),
121 &["subscribed"],
122 ),
123 true,
124 ROOT_ONLY,
125 );
126
127 c(
129 "subagent.run",
130 "subagent",
131 "Spawn a subagent: freeform `instruction`, or `template` naming a declared subagents.templates entry (fill its declared `params` only — an instance-tier template brings its own workflows and runs as a peer daemon). mode: sync (wait for the result), async (get a handle), detached (fire and forget), warm (stays alive; send it messages).",
132 obj(
133 json!({
134 "instruction": s("The subagent's brief (freeform; mutually exclusive with template)"),
135 "template": s("A subagents.templates entry to instantiate"),
136 "params": {"type": "object", "description": "Values for the template's declared params (schema-validated)"},
137 "mode": {"enum": ["sync", "async", "detached", "warm"], "default": "sync"},
138 "tools": arr(json!({"type": "string"})),
139 "servers": arr(json!({"type": "string"})),
140 "limits": open_obj(json!({"steps": {"type": "integer"}, "tokens": {"type": "integer"}, "deadline": {"type": "string"}, "memory": s("OS memory cap for the child process, e.g. \"512MB\" (RLIMIT_AS)"), "cpu": s("OS CPU-time cap, e.g. \"5m\" (RLIMIT_CPU)")}), &[]),
141 "priority": {"enum": ["low", "normal", "high"], "default": "normal", "description": "Contention priority: low sheds first under pressure and runs nicer; high schedules first (and asks the OS for more, best-effort)."},
142 "context": arr(open_obj(json!({"role": {"type": "string"}, "content": {"type": "string"}}), &["role", "content"])),
143 "output_contract": s("What the result must look like"),
144 "output_schema": {"type": "object"},
145 "skills": arr(json!({"type": "string"})),
146 "durable": {"type": "boolean", "description": "false = a memory-only record: never persisted, never restore-respawned (the fast path for throwaway workers); absent = the store.durability.work default"}
147 }),
148 &[],
149 ),
150 open_obj(
151 json!({"handle": {"type": "string"}, "status": {"type": "string"}, "result": any()}),
152 &["handle", "status"],
153 ),
154 true,
155 ROOT_WF,
156 );
157 c(
158 "subagent.retire",
159 "subagent",
160 "Begin graceful retirement of an instance-tier child: it drains its own runs and exits cleanly; escalation to SIGKILL only after the drain window.",
161 obj(
162 json!({"handle": s("The instance child's handle")}),
163 &["handle"],
164 ),
165 open_obj(
166 json!({"ok": {"type": "boolean"}, "handle": {"type": "string"}, "status": {"type": "string"}}),
167 &["ok"],
168 ),
169 true,
170 ROOT_WF,
171 );
172 c(
173 "subagent.send",
174 "subagent",
175 "Send a message into a warm subagent (steer it).",
176 obj(
177 json!({"handle": s("The subagent handle"), "message": s("The message")}),
178 &["handle", "message"],
179 ),
180 open_obj(
181 json!({"ok": {"type": "boolean"}, "handle": {"type": "string"}}),
182 &["ok"],
183 ),
184 true,
185 ROOT_WF,
186 );
187 c(
188 "subagent.kill",
189 "subagent",
190 "Cancel and stop a subagent.",
191 obj(
192 json!({"handle": s("The subagent handle"), "reason": s("Why")}),
193 &["handle"],
194 ),
195 open_obj(
196 json!({"ok": {"type": "boolean"}, "handle": {"type": "string"}}),
197 &["ok"],
198 ),
199 true,
200 ROOT_WF,
201 );
202 c(
203 "subagent.status",
204 "subagent",
205 "The status (and result, when finished) of a subagent.",
206 obj(json!({"handle": s("The subagent handle")}), &["handle"]),
207 open_obj(
208 json!({"handle": {"type": "string"}, "status": {"type": "string"}, "mode": {"type": "string"}, "result": any(), "error": {"type": "string"}}),
209 &["handle", "status"],
210 ),
211 true,
212 ROOT_WF,
213 );
214 c(
215 "subagent.await",
216 "subagent",
217 "Wait for an async subagent to finish (bounded by timeout) and return its result.",
218 obj(
219 json!({"handle": s("The subagent handle"), "timeout": s("Duration, e.g. 30s")}),
220 &["handle"],
221 ),
222 open_obj(
223 json!({"handle": {"type": "string"}, "status": {"type": "string"}, "result": any(), "error": {"type": "string"}}),
224 &["handle", "status"],
225 ),
226 true,
227 ROOT_WF,
228 );
229 c(
230 "subagent.list",
231 "subagent",
232 "List the subagents of this instance.",
233 obj(json!({}), &[]),
234 open_obj(
235 json!({"subagents": arr(json!({"type": "object"}))}),
236 &["subagents"],
237 ),
238 true,
239 ROOT_WF,
240 );
241
242 c(
244 "code.run",
245 "code",
246 "Run code in a sandbox (only available when mapped to a sandbox MCP server).",
247 obj(
248 json!({"language": s("e.g. python, bash"), "code": s("The program"), "files": {"type": "object"}, "timeout": s("Duration")}),
249 &["language", "code"],
250 ),
251 open_obj(
252 json!({"stdout": {"type": "string"}, "stderr": {"type": "string"}, "exit_code": {"type": "integer"}, "files": {"type": "object"}}),
253 &[],
254 ),
255 false,
256 ROOT_WF,
257 );
258
259 c(
261 "memory.get",
262 "memory",
263 "Read a value from the agent's durable memory.",
264 obj(json!({"key": s("The key")}), &["key"]),
265 open_obj(
266 json!({"found": {"type": "boolean"}, "key": {"type": "string"}, "value": any(), "meta": {"type": "object"}}),
267 &["found"],
268 ),
269 true,
270 ALL,
271 );
272 c(
273 "memory.set",
274 "memory",
275 "Write a JSON value to the agent's durable memory (optional TTL).",
276 obj(
277 json!({"key": s("The key"), "value": any(), "ttl": s("Duration after which the value expires")}),
278 &["key", "value"],
279 ),
280 open_obj(
281 json!({"ok": {"type": "boolean"}, "key": {"type": "string"}, "meta": {"type": "object"}}),
282 &["ok"],
283 ),
284 true,
285 ALL,
286 );
287 c(
288 "memory.list",
289 "memory",
290 "List memory keys (optionally by prefix).",
291 obj(
292 json!({"prefix": s("Key prefix"), "limit": {"type": "integer", "minimum": 1}}),
293 &[],
294 ),
295 open_obj(
296 json!({"keys": arr(json!({"type": "object"})), "truncated": {"type": "boolean"}}),
297 &["keys"],
298 ),
299 true,
300 ALL,
301 );
302 c(
303 "memory.push",
304 "memory",
305 "Append a value to the ARRAY at a memory key (created if absent) — the durable queue primitive.",
306 obj(
307 json!({"key": s("The key"), "value": any()}),
308 &["key", "value"],
309 ),
310 open_obj(
311 json!({"ok": {"type": "boolean"}, "key": {"type": "string"}, "length": {"type": "integer"}}),
312 &["ok"],
313 ),
314 true,
315 ALL,
316 );
317 c(
318 "memory.shift",
319 "memory",
320 "Remove and return the FIRST element of the array at a memory key ({found: false} on empty).",
321 obj(json!({"key": s("The key")}), &["key"]),
322 open_obj(
323 json!({"found": {"type": "boolean"}, "value": any(), "remaining": {"type": "integer"}}),
324 &["found"],
325 ),
326 true,
327 ALL,
328 );
329 c(
330 "memory.pop",
331 "memory",
332 "Remove and return the LAST element of the array at a memory key ({found: false} on empty).",
333 obj(json!({"key": s("The key")}), &["key"]),
334 open_obj(
335 json!({"found": {"type": "boolean"}, "value": any(), "remaining": {"type": "integer"}}),
336 &["found"],
337 ),
338 true,
339 ALL,
340 );
341 c(
342 "memory.delete",
343 "memory",
344 "Delete a memory key.",
345 obj(json!({"key": s("The key")}), &["key"]),
346 open_obj(
347 json!({"ok": {"type": "boolean"}, "key": {"type": "string"}}),
348 &["ok"],
349 ),
350 true,
351 ALL,
352 );
353
354 c(
356 "artifact.create",
357 "artifact",
358 "Create an artifact (a named piece of content delivered with the task).",
359 obj(
360 json!({"name": s("File-like name"), "mime": s("MIME type, default text/plain"), "content": any(), "from_step": s("Take the content from a workflow step output"), "sensitive": {"type": "boolean"}}),
361 &["name"],
362 ),
363 open_obj(
364 json!({"id": {"type": "string"}, "name": {"type": "string"}, "size": {"type": "integer"}, "sha256": {"type": "string"}}),
365 &["id"],
366 ),
367 true,
368 ALL,
369 );
370 c(
371 "artifact.get",
372 "artifact",
373 "Read an artifact by id.",
374 obj(json!({"id": s("Artifact id")}), &["id"]),
375 open_obj(
376 json!({"id": {"type": "string"}, "name": {"type": "string"}, "mime": {"type": "string"}, "content": any(), "size": {"type": "integer"}, "sha256": {"type": "string"}}),
377 &["id"],
378 ),
379 true,
380 ALL,
381 );
382 c(
383 "artifact.delete",
384 "artifact",
385 "Delete an artifact.",
386 obj(json!({"id": s("Artifact id")}), &["id"]),
387 open_obj(json!({"ok": {"type": "boolean"}}), &["ok"]),
388 true,
389 ALL,
390 );
391 c(
392 "artifact.list",
393 "artifact",
394 "List artifacts.",
395 obj(
396 json!({"prefix": s("Name prefix"), "limit": {"type": "integer"}}),
397 &[],
398 ),
399 open_obj(
400 json!({"artifacts": arr(json!({"type": "object"}))}),
401 &["artifacts"],
402 ),
403 true,
404 ALL,
405 );
406
407 c(
414 "message.send",
415 "message",
416 "Deliver a message into one of this agent's own conversations, starting a turn there. `to` is a context id, \"root\", or \"new\". Returns once the delivery is durable — the turn runs on its own schedule. To wait for the answer, use the `message` workflow node with `wait: reply`.",
417 obj(
418 json!({"to": s("Context id, \"root\", or \"new\" (default: root)"), "text": s("The message")}),
419 &["text"],
420 ),
421 open_obj(
422 json!({"delivered": {"type": "boolean"}, "conversation": {"type": "string"}, "depth": {"type": "integer"}}),
423 &["delivered", "conversation"],
424 ),
425 true,
426 ALL,
427 );
428
429 c(
431 "workflow.run",
432 "workflow",
433 "Start a run of a named workflow (with inputs).",
434 obj(
435 json!({"name": s("Workflow name"), "inputs": {"type": "object"}, "start": s("Which start node to fire (default: manual/once)"), "wait": {"type": "boolean", "description": "Wait for the run to finish and return its output"}, "timeout": s("Duration when waiting")}),
436 &["name"],
437 ),
438 open_obj(
439 json!({"run": {"type": "string"}, "status": {"type": "string"}, "output": any(), "task": {"type": "string"}}),
440 &["run", "status"],
441 ),
442 true,
443 ROOT_WF,
444 );
445 c(
446 "workflow.create",
447 "workflow",
448 "Define a new workflow at runtime.",
449 obj(
450 json!({"definition": {"type": "object"}, "arm": {"type": "boolean"}}),
451 &["definition"],
452 ),
453 open_obj(
454 json!({"name": {"type": "string"}, "hash": {"type": "string"}, "armed": {"type": "boolean"}}),
455 &["name"],
456 ),
457 true,
458 ROOT_ONLY,
459 );
460 c(
461 "workflow.update",
462 "workflow",
463 "Replace a workflow definition (live runs keep their pinned hash).",
464 obj(
465 json!({"name": s("Workflow name"), "definition": {"type": "object"}}),
466 &["name", "definition"],
467 ),
468 open_obj(
469 json!({"name": {"type": "string"}, "hash": {"type": "string"}}),
470 &["name"],
471 ),
472 true,
473 ROOT_ONLY,
474 );
475 c(
476 "workflow.delete",
477 "workflow",
478 "Delete a workflow definition (disarms it; live runs finish).",
479 obj(json!({"name": s("Workflow name")}), &["name"]),
480 open_obj(json!({"ok": {"type": "boolean"}}), &["ok"]),
481 true,
482 ROOT_ONLY,
483 );
484 c(
485 "workflow.list",
486 "workflow",
487 "List workflows and their runs.",
488 obj(json!({}), &[]),
489 open_obj(
490 json!({"workflows": arr(json!({"type": "object"}))}),
491 &["workflows"],
492 ),
493 true,
494 ALL,
495 );
496 c(
497 "workflow.status",
498 "workflow",
499 "The status of a run (or of every run of a workflow).",
500 obj(json!({"run": s("Run id"), "name": s("Workflow name")}), &[]),
501 open_obj(json!({"runs": arr(json!({"type": "object"}))}), &["runs"]),
502 true,
503 ALL,
504 );
505 c(
506 "workflow.cancel",
507 "workflow",
508 "Cancel a run.",
509 obj(json!({"run": s("Run id"), "reason": s("Why")}), &["run"]),
510 open_obj(
511 json!({"ok": {"type": "boolean"}, "status": {"type": "string"}}),
512 &["ok"],
513 ),
514 true,
515 ROOT_WF,
516 );
517 c(
518 "workflow.pause",
519 "workflow",
520 "Pause a run (or disarm a workflow's start nodes). With `before_step`, \
521 set a BREAKPOINT instead: the run keeps going and pauses just before \
522 that step starts, so it can be inspected in the state it is in rather \
523 than one effect later. Durable — it survives a restart.",
524 obj(
525 json!({"run": s("Run id"), "name": s("Workflow name"),
526 "before_step": s("Pause just before this step starts (a breakpoint)")}),
527 &[],
528 ),
529 open_obj(
530 json!({"ok": {"type": "boolean"}, "break_before": {"type": "string"}}),
531 &[],
532 ),
533 true,
534 ROOT_ONLY,
535 );
536 c(
537 "workflow.resume",
538 "workflow",
539 "Resume a paused run (or re-arm a workflow).",
540 obj(json!({"run": s("Run id"), "name": s("Workflow name")}), &[]),
541 open_obj(json!({"ok": {"type": "boolean"}}), &["ok"]),
542 true,
543 ROOT_ONLY,
544 );
545 c(
546 "workflow.signal",
547 "workflow",
548 "Send a named signal (with a payload) into a run, or start a workflow whose start node listens for it.",
549 obj(
550 json!({"name": s("Signal name"), "payload": any(), "run": s("Target run id (optional)")}),
551 &["name"],
552 ),
553 open_obj(json!({"delivered": {"type": "integer"}}), &["delivered"]),
554 true,
555 ALL,
556 );
557 c(
558 "workflow.wait",
559 "workflow",
560 "Wait for a run to finish and return its output.",
561 obj(
562 json!({"run": s("Run id"), "timeout": s("Duration")}),
563 &["run"],
564 ),
565 open_obj(
566 json!({"run": {"type": "string"}, "status": {"type": "string"}, "output": any()}),
567 &["run", "status"],
568 ),
569 true,
570 ROOT_WF,
571 );
572
573 c(
575 "plan.create",
576 "plan",
577 "Create (or replace) this conversation's working plan: a goal and an ordered list of items.",
578 obj(
579 json!({"goal": s("The goal"), "items": arr(json!({"oneOf": [{"type": "string"}, open_obj(json!({"title": {"type": "string"}, "detail": {"type": "string"}}), &["title"])]}))}),
580 &["goal", "items"],
581 ),
582 open_obj(
583 json!({"goal": {"type": "string"}, "items": arr(json!({"type": "object"}))}),
584 &["goal", "items"],
585 ),
586 true,
587 ALL,
588 );
589 c(
590 "plan.get",
591 "plan",
592 "Read this conversation's plan.",
593 obj(json!({}), &[]),
594 open_obj(json!({"plan": any(), "progress": {"type": "string"}}), &[]),
595 true,
596 ALL,
597 );
598 c(
599 "plan.update",
600 "plan",
601 "Advance the plan: set an item's status/note, bind it to a run/subagent, insert an item, or reorder.",
602 obj(
603 json!({
604 "item": {"description": "Item id (number) or exact title", "oneOf": [{"type": "integer"}, {"type": "string"}]},
605 "status": {"enum": ["pending", "in_progress", "done", "blocked", "skipped"]},
606 "note": s("A short note"), "title": s("New title"), "detail": s("New detail"),
607 "bind": open_obj(json!({"run": {"type": "string"}, "subagent": {"type": "string"}, "task": {"type": "string"}}), &[]),
608 "insert": open_obj(json!({"title": {"type": "string"}, "detail": {"type": "string"}, "after": {"type": "integer"}}), &["title"]),
609 "reorder": arr(json!({"type": "integer"}))
610 }),
611 &[],
612 ),
613 open_obj(
614 json!({"goal": {"type": "string"}, "items": arr(json!({"type": "object"})), "progress": {"type": "string"}}),
615 &["goal", "items"],
616 ),
617 true,
618 ALL,
619 );
620 c(
621 "plan.clear",
622 "plan",
623 "Clear the plan (the goal is met or abandoned).",
624 obj(json!({}), &[]),
625 open_obj(json!({"ok": {"type": "boolean"}}), &["ok"]),
626 true,
627 ALL,
628 );
629
630 c(
643 "ask_human",
644 "human",
645 "Ask a person a question and wait for the answer. The answer is CHECKED against `schema`, so a reply that does not match is rejected and the person is asked again with the reason. By default the question reaches whoever is watching this agent's tasks; `to` names who must answer, and a reply from anyone else is refused.",
646 obj(
647 json!({
648 "question": s("The question"),
649 "to": {"description": "Who must answer: a principal-id glob (\"*@finance.example\"), or {id, role, labels} — all conditions must hold. Anyone else is refused and the gate stays open. Use it when the DECISION belongs to a particular person; omit it when any watcher will do."},
650 "schema": {"type": "object", "description": "The answer's shape, as a JSON Schema. Enforced on the reply, not merely advertised: build it for the decision you actually need. A single-property schema also lets a person answer in plain language (\"yes\" for {approved: boolean})."},
651 "recommend": {"description": "The answer you would choose if nobody replies. Used only when the operator set `agent.approval: accept`; otherwise a person still decides."},
652 "timeout": s("Duration"),
653 }),
654 &["question"],
655 ),
656 open_obj(
657 json!({"reply": any(), "timed_out": {"type": "boolean"}}),
658 &[],
659 ),
660 true,
661 ALL,
662 );
663 c(
664 "sleep",
665 "time",
666 "Wait for a duration (durable: survives restarts).",
667 obj(
668 json!({"duration": s("Duration, e.g. 30s, 5m")}),
669 &["duration"],
670 ),
671 open_obj(json!({"slept_ms": {"type": "integer"}}), &["slept_ms"]),
672 true,
673 ALL,
674 );
675 c(
676 "await",
677 "time",
678 "Wait until a condition holds (CEL over memory/resources/steps/signals) or a timeout elapses.",
679 obj(
680 json!({"condition": s("CEL expression"), "on": arr(json!({"type": "string"})), "timeout": s("Duration")}),
681 &["condition"],
682 ),
683 open_obj(
684 json!({"satisfied": {"type": "boolean"}, "value": any()}),
685 &["satisfied"],
686 ),
687 true,
688 ALL,
689 );
690 c(
691 "context.compact",
692 "context",
693 "Compact this context: summarize older messages, keep the recent ones verbatim.",
694 obj(
695 json!({"target_tokens": {"type": "integer"}, "keep_last": {"type": "integer"}}),
696 &[],
697 ),
698 open_obj(
699 json!({"version": {"type": "integer"}, "est_tokens": {"type": "integer"}, "folded": {"type": "integer"}}),
700 &["version"],
701 ),
702 true,
703 ALL,
704 );
705 c(
706 "think",
707 "intelligence",
708 "One structured reasoning call (no tools): give a prompt and optionally an output schema; get the object back.",
709 obj(
710 json!({"prompt": s("What to think about"), "output_schema": {"type": "object"}, "reads": arr(json!({"type": "string"})), "skills": arr(json!({"type": "string"}))}),
711 &["prompt"],
712 ),
713 any(),
714 true,
715 ALL,
716 );
717 c(
718 "finish",
719 "lifecycle",
720 "Finish the current unit of work with a status and an optional output.",
721 obj(
722 json!({"status": {"enum": ["completed", "failed", "refused", "cancelled"]}, "output": any(), "reason": s("Why"), "exit": {"type": "boolean", "description": "Root only: exit the daemon"}}),
723 &["status"],
724 ),
725 open_obj(json!({"ok": {"type": "boolean"}}), &["ok"]),
726 true,
727 DefaultGrant {
728 root: true,
729 workflows: false,
730 subagents: true,
731 user: false,
732 agent: false,
733 },
734 );
735 c(
736 "status",
737 "status",
738 "The instance status: runs, subagents, conversations, budget, store.",
739 obj(json!({}), &[]),
740 json!({"type": "object"}),
741 true,
742 DefaultGrant {
743 root: true,
744 workflows: true,
745 subagents: true,
746 user: true,
747 agent: true,
748 },
749 );
750
751 c(
753 "knowledge.search",
754 "knowledge",
755 "Search the knowledge base (RAG over documents).",
756 obj(
757 json!({"query": s("The query"), "top_k": {"type": "integer", "minimum": 1}, "filters": {"type": "object"}}),
758 &["query"],
759 ),
760 open_obj(
761 json!({"hits": arr(open_obj(json!({"id": {"type": "string"}, "uri": {"type": "string"}, "title": {"type": "string"}, "score": {"type": "number"}, "snippet": {"type": "string"}, "metadata": {"type": "object"}}), &[]))}),
762 &["hits"],
763 ),
764 false,
765 ALL,
766 );
767 c(
768 "knowledge.get",
769 "knowledge",
770 "Fetch a knowledge document by id or URI.",
771 obj(
772 json!({"id": s("Document id"), "uri": s("Document URI")}),
773 &[],
774 ),
775 open_obj(
776 json!({"content": {"type": "string"}, "mime": {"type": "string"}, "metadata": {"type": "object"}}),
777 &["content"],
778 ),
779 false,
780 ALL,
781 );
782 c(
783 "knowledge.list",
784 "knowledge",
785 "List knowledge documents.",
786 obj(json!({"prefix": s("Prefix")}), &[]),
787 open_obj(json!({"docs": arr(json!({"type": "object"}))}), &["docs"]),
788 false,
789 ALL,
790 );
791 c(
792 "search.query",
793 "search",
794 "Web/docs/code search through the search server.",
795 obj(
796 json!({"query": s("The query"), "kind": {"enum": ["web", "docs", "code"]}, "limit": {"type": "integer", "minimum": 1}, "freshness": s("e.g. day, week")}),
797 &["query"],
798 ),
799 open_obj(
800 json!({"results": arr(open_obj(json!({"title": {"type": "string"}, "url": {"type": "string"}, "snippet": {"type": "string"}, "source": {"type": "string"}, "published": {"type": "string"}}), &[]))}),
801 &["results"],
802 ),
803 false,
804 ALL,
805 );
806 c(
807 "search.fetch",
808 "search",
809 "Fetch a page's content through the search server.",
810 obj(
811 json!({"url": s("The URL"), "max_bytes": {"type": "integer"}}),
812 &["url"],
813 ),
814 open_obj(
815 json!({"content": {"type": "string"}, "mime": {"type": "string"}, "final_url": {"type": "string"}}),
816 &["content"],
817 ),
818 false,
819 ALL,
820 );
821
822 c(
824 "skills.list",
825 "skills",
826 "List the available skills (name, description, when to use).",
827 obj(json!({}), &[]),
828 open_obj(
829 json!({"skills": arr(json!({"type": "object"}))}),
830 &["skills"],
831 ),
832 true,
833 ALL,
834 );
835 c(
836 "skills.load",
837 "skills",
838 "Load a skill's full instructions into this context.",
839 obj(
840 json!({"name": s("Skill name"), "version": s("Version/hash (optional)"), "arguments": {"type": "object"}}),
841 &["name"],
842 ),
843 open_obj(
844 json!({"loaded": {"type": "boolean"}, "name": {"type": "string"}, "hash": {"type": "string"}, "body": {"type": "string"}}),
845 &["loaded"],
846 ),
847 true,
848 ALL,
849 );
850 c(
851 "skills.unload",
852 "skills",
853 "Drop a loaded skill from this context.",
854 obj(json!({"name": s("Skill name")}), &["name"]),
855 open_obj(json!({"ok": {"type": "boolean"}}), &["ok"]),
856 true,
857 ALL,
858 );
859
860 c(
870 "exec",
871 "exec",
872 "Run a local command (argv — NO shell interpretation) and return {stdout, stderr, exit_code, timed_out}. GUARDED and default-OFF: runs only allow-listed commands, confined to a working directory, with a timeout, an output cap, and a minimal environment. Enable a local runner via `security.exec` in a build with `--features exec`, or map it onto an MCP server with `tools.overrides` to delegate execution off-box.",
873 obj(
874 json!({
875 "cmd": s("The command to run (argv[0]) — must be in security.exec.allow"),
876 "args": arr(s("Arguments (argv[1..]); passed directly, never through a shell")),
877 "cwd": s("Working directory, relative to and confined within security.exec.workdir"),
878 "stdin": s("Optional standard input for the command"),
879 "timeout": s("Max wall-clock (e.g. `10s`); clamped to the configured maximum")
880 }),
881 &["cmd"],
882 ),
883 open_obj(
884 json!({
885 "stdout": {"type": "string"}, "stderr": {"type": "string"},
886 "exit_code": {"type": "integer"}, "timed_out": {"type": "boolean"}
887 }),
888 &["stdout", "stderr", "exit_code"],
889 ),
890 false,
891 ALL,
892 );
893 v
894}
895
896pub fn names() -> Vec<&'static str> {
898 contracts().into_iter().map(|c| c.name).collect()
899}
900
901#[cfg(test)]
902mod tests {
903 use super::*;
904
905 #[test]
918 fn ask_human_advertises_exactly_what_it_reads() {
919 let c = contracts()
920 .into_iter()
921 .find(|c| c.name == "ask_human")
922 .expect("ask_human exists");
923 let props = c.input["properties"].as_object().expect("properties");
924 for f in ["question", "schema", "to", "recommend", "timeout"] {
925 assert!(props.contains_key(f), "ask_human must advertise {f:?}");
926 }
927 assert_eq!(c.input["additionalProperties"], serde_json::json!(false));
930 for args in [
931 json!({"question": "ship it?", "recommend": {"approved": true}}),
932 json!({"question": "ship it?", "to": "*@finance.example"}),
933 json!({"question": "ship it?", "to": {"role": "user", "labels": {"team": "finance"}}}),
934 ] {
935 assert!(
936 crate::jsonschema::validate(&c.input, &args).is_ok(),
937 "must validate: {args}"
938 );
939 }
940 assert!(
941 crate::jsonschema::validate(&c.input, &json!({"question": "x", "nonsense": 1}))
942 .is_err(),
943 "an unknown field is still refused"
944 );
945 }
946
947 #[test]
948 fn contracts_are_unique_well_formed_and_cover_the_catalogue() {
949 let all = contracts();
950 let mut seen = std::collections::BTreeSet::new();
951 for c in &all {
952 assert!(seen.insert(c.name), "duplicate contract {}", c.name);
953 crate::jsonschema::check_schema(&c.input)
954 .unwrap_or_else(|e| panic!("{}: bad input schema: {e:?}", c.name));
955 crate::jsonschema::check_schema(&c.output)
956 .unwrap_or_else(|e| panic!("{}: bad output schema: {e:?}", c.name));
957 assert!(
958 c.name
959 .chars()
960 .all(|ch| ch.is_ascii_alphanumeric() || ch == '.' || ch == '_'),
961 "{}",
962 c.name
963 );
964 }
965 for must in [
966 "instruction.read",
967 "instruction.subscribe",
968 "subagent.run",
969 "subagent.send",
970 "subagent.kill",
971 "subagent.status",
972 "subagent.await",
973 "subagent.list",
974 "subagent.retire",
975 "code.run",
976 "memory.get",
977 "memory.set",
978 "memory.list",
979 "memory.push",
980 "memory.shift",
981 "memory.pop",
982 "memory.delete",
983 "artifact.create",
984 "artifact.get",
985 "artifact.delete",
986 "artifact.list",
987 "workflow.run",
988 "workflow.create",
989 "workflow.update",
990 "workflow.delete",
991 "workflow.list",
992 "workflow.status",
993 "workflow.cancel",
994 "workflow.pause",
995 "workflow.resume",
996 "workflow.signal",
997 "workflow.wait",
998 "plan.create",
999 "plan.get",
1000 "plan.update",
1001 "plan.clear",
1002 "ask_human",
1003 "sleep",
1004 "await",
1005 "context.compact",
1006 "think",
1007 "finish",
1008 "status",
1009 "knowledge.search",
1010 "knowledge.get",
1011 "knowledge.list",
1012 "search.query",
1013 "search.fetch",
1014 "skills.list",
1015 "skills.load",
1016 "skills.unload",
1017 ] {
1018 assert!(seen.contains(must), "missing contract {must}");
1019 }
1020 for c in &all {
1024 let mapping_only = c.name == "code.run"
1025 || c.name == "exec"
1026 || c.name.starts_with("knowledge.")
1027 || c.name.starts_with("search.");
1028 assert_eq!(!c.builtin, mapping_only, "{}", c.name);
1029 }
1030 assert!(
1032 !all.iter()
1033 .find(|c| c.name == "finish")
1034 .unwrap()
1035 .grant
1036 .workflows
1037 );
1038 assert!(all.iter().find(|c| c.name == "status").unwrap().grant.user);
1039 }
1040}