1use serde_json::{Value, json};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct DefaultGrant {
13 pub root: bool,
14 pub workflows: bool,
15 pub subagents: bool,
16 pub user: bool,
18 pub agent: bool,
20}
21
22const ALL: DefaultGrant = DefaultGrant {
23 root: true,
24 workflows: true,
25 subagents: true,
26 user: false,
27 agent: false,
28};
29const ROOT_WF: DefaultGrant = DefaultGrant {
30 root: true,
31 workflows: true,
32 subagents: false,
33 user: false,
34 agent: false,
35};
36const ROOT_ONLY: DefaultGrant = DefaultGrant {
37 root: true,
38 workflows: false,
39 subagents: false,
40 user: false,
41 agent: false,
42};
43
44#[derive(Debug, Clone)]
46pub struct Contract {
47 pub name: &'static str,
48 pub description: &'static str,
49 pub input: Value,
50 pub output: Value,
51 pub builtin: bool,
52 pub grant: DefaultGrant,
53 pub family: &'static str,
55}
56
57fn obj(props: Value, required: &[&str]) -> Value {
58 json!({"type": "object", "properties": props, "required": required, "additionalProperties": false})
59}
60fn open_obj(props: Value, required: &[&str]) -> Value {
61 json!({"type": "object", "properties": props, "required": required})
62}
63fn any() -> Value {
64 json!({})
65}
66fn s(desc: &str) -> Value {
67 json!({"type": "string", "description": desc})
68}
69fn arr(items: Value) -> Value {
70 json!({"type": "array", "items": items})
71}
72
73pub fn contracts() -> Vec<Contract> {
75 let mut v = Vec::new();
76 let mut c = |name: &'static str,
77 family: &'static str,
78 description: &'static str,
79 input: Value,
80 output: Value,
81 builtin: bool,
82 grant: DefaultGrant| {
83 v.push(Contract {
84 name,
85 description,
86 input,
87 output,
88 builtin,
89 grant,
90 family,
91 });
92 };
93
94 c(
96 "instruction.read",
97 "instruction",
98 "Read the agent's current instruction (the brief it operates under).",
99 obj(json!({}), &[]),
100 open_obj(
101 json!({"text": {"type": "string"}, "source": {"enum": ["static", "resource"]}, "uri": {"type": "string"}, "version": {"type": "string"}}),
102 &["text", "source"],
103 ),
104 true,
105 ALL,
106 );
107 c(
108 "instruction.subscribe",
109 "instruction",
110 "(Re)subscribe to the instruction resource, or switch to another URI; an update re-reads it and wakes the agent.",
111 obj(
112 json!({"uri": s("The resource URI (omit to re-subscribe to the current one)")}),
113 &[],
114 ),
115 open_obj(
116 json!({"subscribed": {"type": "boolean"}, "uri": {"type": "string"}}),
117 &["subscribed"],
118 ),
119 true,
120 ROOT_ONLY,
121 );
122
123 c(
125 "subagent.run",
126 "subagent",
127 "Spawn a subagent with its own instruction. mode: sync (wait for the result), async (get a handle), detached (fire and forget), warm (stays alive; send it messages).",
128 obj(
129 json!({
130 "instruction": s("The subagent's brief"),
131 "mode": {"enum": ["sync", "async", "detached", "warm"], "default": "sync"},
132 "workflow": s("Run this workflow instead of a free agent loop"),
133 "tools": arr(json!({"type": "string"})),
134 "servers": arr(json!({"type": "string"})),
135 "limits": open_obj(json!({"steps": {"type": "integer"}, "tokens": {"type": "integer"}, "deadline": {"type": "string"}}), &[]),
136 "context": arr(open_obj(json!({"role": {"type": "string"}, "content": {"type": "string"}}), &["role", "content"])),
137 "output_contract": s("What the result must look like"),
138 "output_schema": {"type": "object"},
139 "skills": arr(json!({"type": "string"}))
140 }),
141 &["instruction"],
142 ),
143 open_obj(
144 json!({"handle": {"type": "string"}, "status": {"type": "string"}, "result": any()}),
145 &["handle", "status"],
146 ),
147 true,
148 ROOT_WF,
149 );
150 c(
151 "subagent.send",
152 "subagent",
153 "Send a message into a warm subagent (steer it).",
154 obj(
155 json!({"handle": s("The subagent handle"), "message": s("The message")}),
156 &["handle", "message"],
157 ),
158 open_obj(
159 json!({"ok": {"type": "boolean"}, "handle": {"type": "string"}}),
160 &["ok"],
161 ),
162 true,
163 ROOT_WF,
164 );
165 c(
166 "subagent.kill",
167 "subagent",
168 "Cancel and stop a subagent.",
169 obj(
170 json!({"handle": s("The subagent handle"), "reason": s("Why")}),
171 &["handle"],
172 ),
173 open_obj(
174 json!({"ok": {"type": "boolean"}, "handle": {"type": "string"}}),
175 &["ok"],
176 ),
177 true,
178 ROOT_WF,
179 );
180 c(
181 "subagent.status",
182 "subagent",
183 "The status (and result, when finished) of a subagent.",
184 obj(json!({"handle": s("The subagent handle")}), &["handle"]),
185 open_obj(
186 json!({"handle": {"type": "string"}, "status": {"type": "string"}, "mode": {"type": "string"}, "result": any(), "error": {"type": "string"}}),
187 &["handle", "status"],
188 ),
189 true,
190 ROOT_WF,
191 );
192 c(
193 "subagent.await",
194 "subagent",
195 "Wait for an async subagent to finish (bounded by timeout) and return its result.",
196 obj(
197 json!({"handle": s("The subagent handle"), "timeout": s("Duration, e.g. 30s")}),
198 &["handle"],
199 ),
200 open_obj(
201 json!({"handle": {"type": "string"}, "status": {"type": "string"}, "result": any(), "error": {"type": "string"}}),
202 &["handle", "status"],
203 ),
204 true,
205 ROOT_WF,
206 );
207 c(
208 "subagent.list",
209 "subagent",
210 "List the subagents of this instance.",
211 obj(json!({}), &[]),
212 open_obj(
213 json!({"subagents": arr(json!({"type": "object"}))}),
214 &["subagents"],
215 ),
216 true,
217 ROOT_WF,
218 );
219
220 c(
222 "code.run",
223 "code",
224 "Run code in a sandbox (only available when mapped to a sandbox MCP server).",
225 obj(
226 json!({"language": s("e.g. python, bash"), "code": s("The program"), "files": {"type": "object"}, "timeout": s("Duration")}),
227 &["language", "code"],
228 ),
229 open_obj(
230 json!({"stdout": {"type": "string"}, "stderr": {"type": "string"}, "exit_code": {"type": "integer"}, "files": {"type": "object"}}),
231 &[],
232 ),
233 false,
234 ROOT_WF,
235 );
236
237 c(
239 "memory.get",
240 "memory",
241 "Read a value from the agent's durable memory.",
242 obj(json!({"key": s("The key")}), &["key"]),
243 open_obj(
244 json!({"found": {"type": "boolean"}, "key": {"type": "string"}, "value": any(), "meta": {"type": "object"}}),
245 &["found"],
246 ),
247 true,
248 ALL,
249 );
250 c(
251 "memory.set",
252 "memory",
253 "Write a JSON value to the agent's durable memory (optional TTL).",
254 obj(
255 json!({"key": s("The key"), "value": any(), "ttl": s("Duration after which the value expires")}),
256 &["key", "value"],
257 ),
258 open_obj(
259 json!({"ok": {"type": "boolean"}, "key": {"type": "string"}, "meta": {"type": "object"}}),
260 &["ok"],
261 ),
262 true,
263 ALL,
264 );
265 c(
266 "memory.list",
267 "memory",
268 "List memory keys (optionally by prefix).",
269 obj(
270 json!({"prefix": s("Key prefix"), "limit": {"type": "integer", "minimum": 1}}),
271 &[],
272 ),
273 open_obj(
274 json!({"keys": arr(json!({"type": "object"})), "truncated": {"type": "boolean"}}),
275 &["keys"],
276 ),
277 true,
278 ALL,
279 );
280 c(
281 "memory.delete",
282 "memory",
283 "Delete a memory key.",
284 obj(json!({"key": s("The key")}), &["key"]),
285 open_obj(
286 json!({"ok": {"type": "boolean"}, "key": {"type": "string"}}),
287 &["ok"],
288 ),
289 true,
290 ALL,
291 );
292
293 c(
295 "artifact.create",
296 "artifact",
297 "Create an artifact (a named piece of content delivered with the task).",
298 obj(
299 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"}}),
300 &["name"],
301 ),
302 open_obj(
303 json!({"id": {"type": "string"}, "name": {"type": "string"}, "size": {"type": "integer"}, "sha256": {"type": "string"}}),
304 &["id"],
305 ),
306 true,
307 ALL,
308 );
309 c(
310 "artifact.get",
311 "artifact",
312 "Read an artifact by id.",
313 obj(json!({"id": s("Artifact id")}), &["id"]),
314 open_obj(
315 json!({"id": {"type": "string"}, "name": {"type": "string"}, "mime": {"type": "string"}, "content": any(), "size": {"type": "integer"}, "sha256": {"type": "string"}}),
316 &["id"],
317 ),
318 true,
319 ALL,
320 );
321 c(
322 "artifact.delete",
323 "artifact",
324 "Delete an artifact.",
325 obj(json!({"id": s("Artifact id")}), &["id"]),
326 open_obj(json!({"ok": {"type": "boolean"}}), &["ok"]),
327 true,
328 ALL,
329 );
330 c(
331 "artifact.list",
332 "artifact",
333 "List artifacts.",
334 obj(
335 json!({"prefix": s("Name prefix"), "limit": {"type": "integer"}}),
336 &[],
337 ),
338 open_obj(
339 json!({"artifacts": arr(json!({"type": "object"}))}),
340 &["artifacts"],
341 ),
342 true,
343 ALL,
344 );
345
346 c(
348 "workflow.run",
349 "workflow",
350 "Start a run of a named workflow (with inputs).",
351 obj(
352 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")}),
353 &["name"],
354 ),
355 open_obj(
356 json!({"run": {"type": "string"}, "status": {"type": "string"}, "output": any(), "task": {"type": "string"}}),
357 &["run", "status"],
358 ),
359 true,
360 ROOT_WF,
361 );
362 c(
363 "workflow.create",
364 "workflow",
365 "Define a new workflow (dialect 3) at runtime.",
366 obj(
367 json!({"definition": {"type": "object"}, "arm": {"type": "boolean"}}),
368 &["definition"],
369 ),
370 open_obj(
371 json!({"name": {"type": "string"}, "hash": {"type": "string"}, "armed": {"type": "boolean"}}),
372 &["name"],
373 ),
374 true,
375 ROOT_ONLY,
376 );
377 c(
378 "workflow.update",
379 "workflow",
380 "Replace a workflow definition (live runs keep their pinned hash).",
381 obj(
382 json!({"name": s("Workflow name"), "definition": {"type": "object"}}),
383 &["name", "definition"],
384 ),
385 open_obj(
386 json!({"name": {"type": "string"}, "hash": {"type": "string"}}),
387 &["name"],
388 ),
389 true,
390 ROOT_ONLY,
391 );
392 c(
393 "workflow.delete",
394 "workflow",
395 "Delete a workflow definition (disarms it; live runs finish).",
396 obj(json!({"name": s("Workflow name")}), &["name"]),
397 open_obj(json!({"ok": {"type": "boolean"}}), &["ok"]),
398 true,
399 ROOT_ONLY,
400 );
401 c(
402 "workflow.list",
403 "workflow",
404 "List workflows and their runs.",
405 obj(json!({}), &[]),
406 open_obj(
407 json!({"workflows": arr(json!({"type": "object"}))}),
408 &["workflows"],
409 ),
410 true,
411 ALL,
412 );
413 c(
414 "workflow.status",
415 "workflow",
416 "The status of a run (or of every run of a workflow).",
417 obj(json!({"run": s("Run id"), "name": s("Workflow name")}), &[]),
418 open_obj(json!({"runs": arr(json!({"type": "object"}))}), &["runs"]),
419 true,
420 ALL,
421 );
422 c(
423 "workflow.cancel",
424 "workflow",
425 "Cancel a run.",
426 obj(json!({"run": s("Run id"), "reason": s("Why")}), &["run"]),
427 open_obj(
428 json!({"ok": {"type": "boolean"}, "status": {"type": "string"}}),
429 &["ok"],
430 ),
431 true,
432 ROOT_WF,
433 );
434 c(
435 "workflow.pause",
436 "workflow",
437 "Pause a run (or disarm a workflow's start nodes).",
438 obj(json!({"run": s("Run id"), "name": s("Workflow name")}), &[]),
439 open_obj(json!({"ok": {"type": "boolean"}}), &["ok"]),
440 true,
441 ROOT_ONLY,
442 );
443 c(
444 "workflow.resume",
445 "workflow",
446 "Resume a paused run (or re-arm a workflow).",
447 obj(json!({"run": s("Run id"), "name": s("Workflow name")}), &[]),
448 open_obj(json!({"ok": {"type": "boolean"}}), &["ok"]),
449 true,
450 ROOT_ONLY,
451 );
452 c(
453 "workflow.signal",
454 "workflow",
455 "Send a named signal (with a payload) into a run, or start a workflow whose start node listens for it.",
456 obj(
457 json!({"name": s("Signal name"), "payload": any(), "run": s("Target run id (optional)")}),
458 &["name"],
459 ),
460 open_obj(json!({"delivered": {"type": "integer"}}), &["delivered"]),
461 true,
462 ALL,
463 );
464 c(
465 "workflow.wait",
466 "workflow",
467 "Wait for a run to finish and return its output.",
468 obj(
469 json!({"run": s("Run id"), "timeout": s("Duration")}),
470 &["run"],
471 ),
472 open_obj(
473 json!({"run": {"type": "string"}, "status": {"type": "string"}, "output": any()}),
474 &["run", "status"],
475 ),
476 true,
477 ROOT_WF,
478 );
479
480 c(
482 "plan.create",
483 "plan",
484 "Create (or replace) this conversation's working plan: a goal and an ordered list of items.",
485 obj(
486 json!({"goal": s("The goal"), "items": arr(json!({"oneOf": [{"type": "string"}, open_obj(json!({"title": {"type": "string"}, "detail": {"type": "string"}}), &["title"])]}))}),
487 &["goal", "items"],
488 ),
489 open_obj(
490 json!({"goal": {"type": "string"}, "items": arr(json!({"type": "object"}))}),
491 &["goal", "items"],
492 ),
493 true,
494 ALL,
495 );
496 c(
497 "plan.get",
498 "plan",
499 "Read this conversation's plan.",
500 obj(json!({}), &[]),
501 open_obj(json!({"plan": any(), "progress": {"type": "string"}}), &[]),
502 true,
503 ALL,
504 );
505 c(
506 "plan.update",
507 "plan",
508 "Advance the plan: set an item's status/note, bind it to a run/subagent, insert an item, or reorder.",
509 obj(
510 json!({
511 "item": {"description": "Item id (number) or exact title", "oneOf": [{"type": "integer"}, {"type": "string"}]},
512 "status": {"enum": ["pending", "in_progress", "done", "blocked", "skipped"]},
513 "note": s("A short note"), "title": s("New title"), "detail": s("New detail"),
514 "bind": open_obj(json!({"run": {"type": "string"}, "subagent": {"type": "string"}, "task": {"type": "string"}}), &[]),
515 "insert": open_obj(json!({"title": {"type": "string"}, "detail": {"type": "string"}, "after": {"type": "integer"}}), &["title"]),
516 "reorder": arr(json!({"type": "integer"}))
517 }),
518 &[],
519 ),
520 open_obj(
521 json!({"goal": {"type": "string"}, "items": arr(json!({"type": "object"})), "progress": {"type": "string"}}),
522 &["goal", "items"],
523 ),
524 true,
525 ALL,
526 );
527 c(
528 "plan.clear",
529 "plan",
530 "Clear the plan (the goal is met or abandoned).",
531 obj(json!({}), &[]),
532 open_obj(json!({"ok": {"type": "boolean"}}), &["ok"]),
533 true,
534 ALL,
535 );
536
537 c(
539 "ask_human",
540 "human",
541 "Ask the human (the conversation's principal) a question and wait for the answer.",
542 obj(
543 json!({"question": s("The question"), "schema": {"type": "object", "description": "Expected answer shape"}, "to": s("Principal or conversation to ask (default: the current one)"), "timeout": s("Duration")}),
544 &["question"],
545 ),
546 open_obj(
547 json!({"reply": any(), "timed_out": {"type": "boolean"}}),
548 &[],
549 ),
550 true,
551 ALL,
552 );
553 c(
554 "sleep",
555 "time",
556 "Wait for a duration (durable: survives restarts).",
557 obj(
558 json!({"duration": s("Duration, e.g. 30s, 5m")}),
559 &["duration"],
560 ),
561 open_obj(json!({"slept_ms": {"type": "integer"}}), &["slept_ms"]),
562 true,
563 ALL,
564 );
565 c(
566 "await",
567 "time",
568 "Wait until a condition holds (CEL over memory/resources/steps/signals) or a timeout elapses.",
569 obj(
570 json!({"condition": s("CEL expression"), "on": arr(json!({"type": "string"})), "timeout": s("Duration")}),
571 &["condition"],
572 ),
573 open_obj(
574 json!({"satisfied": {"type": "boolean"}, "value": any()}),
575 &["satisfied"],
576 ),
577 true,
578 ALL,
579 );
580 c(
581 "context.compact",
582 "context",
583 "Compact this context: summarize older messages, keep the recent ones verbatim.",
584 obj(
585 json!({"target_tokens": {"type": "integer"}, "keep_last": {"type": "integer"}}),
586 &[],
587 ),
588 open_obj(
589 json!({"version": {"type": "integer"}, "est_tokens": {"type": "integer"}, "folded": {"type": "integer"}}),
590 &["version"],
591 ),
592 true,
593 ALL,
594 );
595 c(
596 "think",
597 "intelligence",
598 "One structured reasoning call (no tools): give a prompt and optionally an output schema; get the object back.",
599 obj(
600 json!({"prompt": s("What to think about"), "output_schema": {"type": "object"}, "reads": arr(json!({"type": "string"})), "skills": arr(json!({"type": "string"}))}),
601 &["prompt"],
602 ),
603 any(),
604 true,
605 ALL,
606 );
607 c(
608 "finish",
609 "lifecycle",
610 "Finish the current unit of work with a status and an optional output.",
611 obj(
612 json!({"status": {"enum": ["completed", "failed", "refused", "cancelled"]}, "output": any(), "reason": s("Why"), "exit": {"type": "boolean", "description": "Root only: exit the daemon"}}),
613 &["status"],
614 ),
615 open_obj(json!({"ok": {"type": "boolean"}}), &["ok"]),
616 true,
617 DefaultGrant {
618 root: true,
619 workflows: false,
620 subagents: true,
621 user: false,
622 agent: false,
623 },
624 );
625 c(
626 "status",
627 "status",
628 "The instance status: runs, subagents, conversations, budget, store.",
629 obj(json!({}), &[]),
630 json!({"type": "object"}),
631 true,
632 DefaultGrant {
633 root: true,
634 workflows: true,
635 subagents: true,
636 user: true,
637 agent: true,
638 },
639 );
640
641 c(
643 "knowledge.search",
644 "knowledge",
645 "Search the knowledge base (RAG over documents).",
646 obj(
647 json!({"query": s("The query"), "top_k": {"type": "integer", "minimum": 1}, "filters": {"type": "object"}}),
648 &["query"],
649 ),
650 open_obj(
651 json!({"hits": arr(open_obj(json!({"id": {"type": "string"}, "uri": {"type": "string"}, "title": {"type": "string"}, "score": {"type": "number"}, "snippet": {"type": "string"}, "metadata": {"type": "object"}}), &[]))}),
652 &["hits"],
653 ),
654 false,
655 ALL,
656 );
657 c(
658 "knowledge.get",
659 "knowledge",
660 "Fetch a knowledge document by id or URI.",
661 obj(
662 json!({"id": s("Document id"), "uri": s("Document URI")}),
663 &[],
664 ),
665 open_obj(
666 json!({"content": {"type": "string"}, "mime": {"type": "string"}, "metadata": {"type": "object"}}),
667 &["content"],
668 ),
669 false,
670 ALL,
671 );
672 c(
673 "knowledge.list",
674 "knowledge",
675 "List knowledge documents.",
676 obj(json!({"prefix": s("Prefix")}), &[]),
677 open_obj(json!({"docs": arr(json!({"type": "object"}))}), &["docs"]),
678 false,
679 ALL,
680 );
681 c(
682 "search.query",
683 "search",
684 "Web/docs/code search through the search server.",
685 obj(
686 json!({"query": s("The query"), "kind": {"enum": ["web", "docs", "code"]}, "limit": {"type": "integer", "minimum": 1}, "freshness": s("e.g. day, week")}),
687 &["query"],
688 ),
689 open_obj(
690 json!({"results": arr(open_obj(json!({"title": {"type": "string"}, "url": {"type": "string"}, "snippet": {"type": "string"}, "source": {"type": "string"}, "published": {"type": "string"}}), &[]))}),
691 &["results"],
692 ),
693 false,
694 ALL,
695 );
696 c(
697 "search.fetch",
698 "search",
699 "Fetch a page's content through the search server.",
700 obj(
701 json!({"url": s("The URL"), "max_bytes": {"type": "integer"}}),
702 &["url"],
703 ),
704 open_obj(
705 json!({"content": {"type": "string"}, "mime": {"type": "string"}, "final_url": {"type": "string"}}),
706 &["content"],
707 ),
708 false,
709 ALL,
710 );
711
712 c(
714 "skills.list",
715 "skills",
716 "List the available skills (name, description, when to use).",
717 obj(json!({}), &[]),
718 open_obj(
719 json!({"skills": arr(json!({"type": "object"}))}),
720 &["skills"],
721 ),
722 true,
723 ALL,
724 );
725 c(
726 "skills.load",
727 "skills",
728 "Load a skill's full instructions into this context.",
729 obj(
730 json!({"name": s("Skill name"), "version": s("Version/hash (optional)"), "arguments": {"type": "object"}}),
731 &["name"],
732 ),
733 open_obj(
734 json!({"loaded": {"type": "boolean"}, "name": {"type": "string"}, "hash": {"type": "string"}, "body": {"type": "string"}}),
735 &["loaded"],
736 ),
737 true,
738 ALL,
739 );
740 c(
741 "skills.unload",
742 "skills",
743 "Drop a loaded skill from this context.",
744 obj(json!({"name": s("Skill name")}), &["name"]),
745 open_obj(json!({"ok": {"type": "boolean"}}), &["ok"]),
746 true,
747 ALL,
748 );
749
750 c(
756 "exec",
757 "exec",
758 "Run a local command (argv — NO shell interpretation) and return {stdout, stderr, exit_code, timed_out}. GUARDED and default-OFF (RFC 0028): 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.",
759 obj(
760 json!({
761 "cmd": s("The command to run (argv[0]) — must be in security.exec.allow"),
762 "args": arr(s("Arguments (argv[1..]); passed directly, never through a shell")),
763 "cwd": s("Working directory, relative to and confined within security.exec.workdir"),
764 "stdin": s("Optional standard input for the command"),
765 "timeout": s("Max wall-clock (e.g. `10s`); clamped to the configured maximum")
766 }),
767 &["cmd"],
768 ),
769 open_obj(
770 json!({
771 "stdout": {"type": "string"}, "stderr": {"type": "string"},
772 "exit_code": {"type": "integer"}, "timed_out": {"type": "boolean"}
773 }),
774 &["stdout", "stderr", "exit_code"],
775 ),
776 false,
777 ALL,
778 );
779 v
780}
781
782pub fn names() -> Vec<&'static str> {
784 contracts().into_iter().map(|c| c.name).collect()
785}
786
787#[cfg(test)]
788mod tests {
789 use super::*;
790
791 #[test]
792 fn contracts_are_unique_well_formed_and_cover_the_rfc_table() {
793 let all = contracts();
794 let mut seen = std::collections::BTreeSet::new();
795 for c in &all {
796 assert!(seen.insert(c.name), "duplicate contract {}", c.name);
797 crate::jsonschema::check_schema(&c.input)
798 .unwrap_or_else(|e| panic!("{}: bad input schema: {e:?}", c.name));
799 crate::jsonschema::check_schema(&c.output)
800 .unwrap_or_else(|e| panic!("{}: bad output schema: {e:?}", c.name));
801 assert!(
802 c.name
803 .chars()
804 .all(|ch| ch.is_ascii_alphanumeric() || ch == '.' || ch == '_'),
805 "{}",
806 c.name
807 );
808 }
809 for must in [
810 "instruction.read",
811 "instruction.subscribe",
812 "subagent.run",
813 "subagent.send",
814 "subagent.kill",
815 "subagent.status",
816 "subagent.await",
817 "subagent.list",
818 "code.run",
819 "memory.get",
820 "memory.set",
821 "memory.list",
822 "memory.delete",
823 "artifact.create",
824 "artifact.get",
825 "artifact.delete",
826 "artifact.list",
827 "workflow.run",
828 "workflow.create",
829 "workflow.update",
830 "workflow.delete",
831 "workflow.list",
832 "workflow.status",
833 "workflow.cancel",
834 "workflow.pause",
835 "workflow.resume",
836 "workflow.signal",
837 "workflow.wait",
838 "plan.create",
839 "plan.get",
840 "plan.update",
841 "plan.clear",
842 "ask_human",
843 "sleep",
844 "await",
845 "context.compact",
846 "think",
847 "finish",
848 "status",
849 "knowledge.search",
850 "knowledge.get",
851 "knowledge.list",
852 "search.query",
853 "search.fetch",
854 "skills.list",
855 "skills.load",
856 "skills.unload",
857 ] {
858 assert!(seen.contains(must), "missing contract {must}");
859 }
860 for c in &all {
864 let mapping_only = c.name == "code.run"
865 || c.name == "exec"
866 || c.name.starts_with("knowledge.")
867 || c.name.starts_with("search.");
868 assert_eq!(!c.builtin, mapping_only, "{}", c.name);
869 }
870 assert!(
872 !all.iter()
873 .find(|c| c.name == "finish")
874 .unwrap()
875 .grant
876 .workflows
877 );
878 assert!(all.iter().find(|c| c.name == "status").unwrap().grant.user);
879 }
880}