Skip to main content

agentd/registry/
internal.rs

1// SPDX-License-Identifier: Apache-2.0
2//! The **internal tool contracts** (RFC 0028 §3): name, description, input and
3//! output JSON Schemas, whether a built-in implementation exists (mapping-only
4//! contracts are `code.run`, `knowledge.*`, `search.*`), and the default
5//! grants. Contracts are what callers see; an override (RFC 0028 §4) swaps the
6//! implementation, never the contract.
7
8use serde_json::{Value, json};
9
10/// Who may call a tool by default (RFC 0028 §3 "Grants default").
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct DefaultGrant {
13    pub root: bool,
14    pub workflows: bool,
15    pub subagents: bool,
16    /// Granted to A2A `user` principals by default (RFC 0029 §2).
17    pub user: bool,
18    /// Granted to A2A `agent` principals by default.
19    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/// One contract.
45#[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    /// The tool's family (`memory`, `plan`, …) for `agent.tools.internal` lists.
54    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
73/// Every internal contract (deterministic order).
74pub 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    // ---- instruction ----
95    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    // ---- subagents ----
124    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    // ---- code (mapping-only) ----
221    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    // ---- memory ----
238    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    // ---- artifacts ----
294    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    // ---- workflows ----
347    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    // ---- plan ----
481    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    // ---- misc ----
538    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    // ---- knowledge / search (profiles, mapping-only) ----
642    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    // ---- skills ----
713    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    // ---- exec (guarded local command runner; DEFAULT-OFF, RFC 0028 §exec) ----
751    // A mapping-only contract by default: agentd runs no local code (RFC 0012)
752    // unless an operator both builds `--features exec` AND sets `security.exec`.
753    // Otherwise `exec` is delegated off-box via `tools.overrides`. It carries the
754    // `sensitive` + `egress` trifecta tags (attached in `Registry::build`).
755    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
782/// The contract names, in table order.
783pub 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        // Mapping-only contracts have no built-in. (`exec` is mapping-only in the
861        // catalogue; a local runner is turned on in `Registry::build` under the
862        // `exec` feature + `security.exec`.)
863        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        // finish is not granted to workflows (they use the finish step).
871        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}