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). With `before_step`, \
438         set a BREAKPOINT instead: the run keeps going and pauses just before \
439         that step starts, so it can be inspected in the state it is in rather \
440         than one effect later. Durable — it survives a restart.",
441        obj(
442            json!({"run": s("Run id"), "name": s("Workflow name"),
443                   "before_step": s("Pause just before this step starts (a breakpoint)")}),
444            &[],
445        ),
446        open_obj(
447            json!({"ok": {"type": "boolean"}, "break_before": {"type": "string"}}),
448            &[],
449        ),
450        true,
451        ROOT_ONLY,
452    );
453    c(
454        "workflow.resume",
455        "workflow",
456        "Resume a paused run (or re-arm a workflow).",
457        obj(json!({"run": s("Run id"), "name": s("Workflow name")}), &[]),
458        open_obj(json!({"ok": {"type": "boolean"}}), &["ok"]),
459        true,
460        ROOT_ONLY,
461    );
462    c(
463        "workflow.signal",
464        "workflow",
465        "Send a named signal (with a payload) into a run, or start a workflow whose start node listens for it.",
466        obj(
467            json!({"name": s("Signal name"), "payload": any(), "run": s("Target run id (optional)")}),
468            &["name"],
469        ),
470        open_obj(json!({"delivered": {"type": "integer"}}), &["delivered"]),
471        true,
472        ALL,
473    );
474    c(
475        "workflow.wait",
476        "workflow",
477        "Wait for a run to finish and return its output.",
478        obj(
479            json!({"run": s("Run id"), "timeout": s("Duration")}),
480            &["run"],
481        ),
482        open_obj(
483            json!({"run": {"type": "string"}, "status": {"type": "string"}, "output": any()}),
484            &["run", "status"],
485        ),
486        true,
487        ROOT_WF,
488    );
489
490    // ---- plan ----
491    c(
492        "plan.create",
493        "plan",
494        "Create (or replace) this conversation's working plan: a goal and an ordered list of items.",
495        obj(
496            json!({"goal": s("The goal"), "items": arr(json!({"oneOf": [{"type": "string"}, open_obj(json!({"title": {"type": "string"}, "detail": {"type": "string"}}), &["title"])]}))}),
497            &["goal", "items"],
498        ),
499        open_obj(
500            json!({"goal": {"type": "string"}, "items": arr(json!({"type": "object"}))}),
501            &["goal", "items"],
502        ),
503        true,
504        ALL,
505    );
506    c(
507        "plan.get",
508        "plan",
509        "Read this conversation's plan.",
510        obj(json!({}), &[]),
511        open_obj(json!({"plan": any(), "progress": {"type": "string"}}), &[]),
512        true,
513        ALL,
514    );
515    c(
516        "plan.update",
517        "plan",
518        "Advance the plan: set an item's status/note, bind it to a run/subagent, insert an item, or reorder.",
519        obj(
520            json!({
521                "item": {"description": "Item id (number) or exact title", "oneOf": [{"type": "integer"}, {"type": "string"}]},
522                "status": {"enum": ["pending", "in_progress", "done", "blocked", "skipped"]},
523                "note": s("A short note"), "title": s("New title"), "detail": s("New detail"),
524                "bind": open_obj(json!({"run": {"type": "string"}, "subagent": {"type": "string"}, "task": {"type": "string"}}), &[]),
525                "insert": open_obj(json!({"title": {"type": "string"}, "detail": {"type": "string"}, "after": {"type": "integer"}}), &["title"]),
526                "reorder": arr(json!({"type": "integer"}))
527            }),
528            &[],
529        ),
530        open_obj(
531            json!({"goal": {"type": "string"}, "items": arr(json!({"type": "object"})), "progress": {"type": "string"}}),
532            &["goal", "items"],
533        ),
534        true,
535        ALL,
536    );
537    c(
538        "plan.clear",
539        "plan",
540        "Clear the plan (the goal is met or abandoned).",
541        obj(json!({}), &[]),
542        open_obj(json!({"ok": {"type": "boolean"}}), &["ok"]),
543        true,
544        ALL,
545    );
546
547    // ---- misc ----
548    c(
549        "ask_human",
550        "human",
551        "Ask the human (the conversation's principal) a question and wait for the answer.",
552        obj(
553            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")}),
554            &["question"],
555        ),
556        open_obj(
557            json!({"reply": any(), "timed_out": {"type": "boolean"}}),
558            &[],
559        ),
560        true,
561        ALL,
562    );
563    c(
564        "sleep",
565        "time",
566        "Wait for a duration (durable: survives restarts).",
567        obj(
568            json!({"duration": s("Duration, e.g. 30s, 5m")}),
569            &["duration"],
570        ),
571        open_obj(json!({"slept_ms": {"type": "integer"}}), &["slept_ms"]),
572        true,
573        ALL,
574    );
575    c(
576        "await",
577        "time",
578        "Wait until a condition holds (CEL over memory/resources/steps/signals) or a timeout elapses.",
579        obj(
580            json!({"condition": s("CEL expression"), "on": arr(json!({"type": "string"})), "timeout": s("Duration")}),
581            &["condition"],
582        ),
583        open_obj(
584            json!({"satisfied": {"type": "boolean"}, "value": any()}),
585            &["satisfied"],
586        ),
587        true,
588        ALL,
589    );
590    c(
591        "context.compact",
592        "context",
593        "Compact this context: summarize older messages, keep the recent ones verbatim.",
594        obj(
595            json!({"target_tokens": {"type": "integer"}, "keep_last": {"type": "integer"}}),
596            &[],
597        ),
598        open_obj(
599            json!({"version": {"type": "integer"}, "est_tokens": {"type": "integer"}, "folded": {"type": "integer"}}),
600            &["version"],
601        ),
602        true,
603        ALL,
604    );
605    c(
606        "think",
607        "intelligence",
608        "One structured reasoning call (no tools): give a prompt and optionally an output schema; get the object back.",
609        obj(
610            json!({"prompt": s("What to think about"), "output_schema": {"type": "object"}, "reads": arr(json!({"type": "string"})), "skills": arr(json!({"type": "string"}))}),
611            &["prompt"],
612        ),
613        any(),
614        true,
615        ALL,
616    );
617    c(
618        "finish",
619        "lifecycle",
620        "Finish the current unit of work with a status and an optional output.",
621        obj(
622            json!({"status": {"enum": ["completed", "failed", "refused", "cancelled"]}, "output": any(), "reason": s("Why"), "exit": {"type": "boolean", "description": "Root only: exit the daemon"}}),
623            &["status"],
624        ),
625        open_obj(json!({"ok": {"type": "boolean"}}), &["ok"]),
626        true,
627        DefaultGrant {
628            root: true,
629            workflows: false,
630            subagents: true,
631            user: false,
632            agent: false,
633        },
634    );
635    c(
636        "status",
637        "status",
638        "The instance status: runs, subagents, conversations, budget, store.",
639        obj(json!({}), &[]),
640        json!({"type": "object"}),
641        true,
642        DefaultGrant {
643            root: true,
644            workflows: true,
645            subagents: true,
646            user: true,
647            agent: true,
648        },
649    );
650
651    // ---- knowledge / search (profiles, mapping-only) ----
652    c(
653        "knowledge.search",
654        "knowledge",
655        "Search the knowledge base (RAG over documents).",
656        obj(
657            json!({"query": s("The query"), "top_k": {"type": "integer", "minimum": 1}, "filters": {"type": "object"}}),
658            &["query"],
659        ),
660        open_obj(
661            json!({"hits": arr(open_obj(json!({"id": {"type": "string"}, "uri": {"type": "string"}, "title": {"type": "string"}, "score": {"type": "number"}, "snippet": {"type": "string"}, "metadata": {"type": "object"}}), &[]))}),
662            &["hits"],
663        ),
664        false,
665        ALL,
666    );
667    c(
668        "knowledge.get",
669        "knowledge",
670        "Fetch a knowledge document by id or URI.",
671        obj(
672            json!({"id": s("Document id"), "uri": s("Document URI")}),
673            &[],
674        ),
675        open_obj(
676            json!({"content": {"type": "string"}, "mime": {"type": "string"}, "metadata": {"type": "object"}}),
677            &["content"],
678        ),
679        false,
680        ALL,
681    );
682    c(
683        "knowledge.list",
684        "knowledge",
685        "List knowledge documents.",
686        obj(json!({"prefix": s("Prefix")}), &[]),
687        open_obj(json!({"docs": arr(json!({"type": "object"}))}), &["docs"]),
688        false,
689        ALL,
690    );
691    c(
692        "search.query",
693        "search",
694        "Web/docs/code search through the search server.",
695        obj(
696            json!({"query": s("The query"), "kind": {"enum": ["web", "docs", "code"]}, "limit": {"type": "integer", "minimum": 1}, "freshness": s("e.g. day, week")}),
697            &["query"],
698        ),
699        open_obj(
700            json!({"results": arr(open_obj(json!({"title": {"type": "string"}, "url": {"type": "string"}, "snippet": {"type": "string"}, "source": {"type": "string"}, "published": {"type": "string"}}), &[]))}),
701            &["results"],
702        ),
703        false,
704        ALL,
705    );
706    c(
707        "search.fetch",
708        "search",
709        "Fetch a page's content through the search server.",
710        obj(
711            json!({"url": s("The URL"), "max_bytes": {"type": "integer"}}),
712            &["url"],
713        ),
714        open_obj(
715            json!({"content": {"type": "string"}, "mime": {"type": "string"}, "final_url": {"type": "string"}}),
716            &["content"],
717        ),
718        false,
719        ALL,
720    );
721
722    // ---- skills ----
723    c(
724        "skills.list",
725        "skills",
726        "List the available skills (name, description, when to use).",
727        obj(json!({}), &[]),
728        open_obj(
729            json!({"skills": arr(json!({"type": "object"}))}),
730            &["skills"],
731        ),
732        true,
733        ALL,
734    );
735    c(
736        "skills.load",
737        "skills",
738        "Load a skill's full instructions into this context.",
739        obj(
740            json!({"name": s("Skill name"), "version": s("Version/hash (optional)"), "arguments": {"type": "object"}}),
741            &["name"],
742        ),
743        open_obj(
744            json!({"loaded": {"type": "boolean"}, "name": {"type": "string"}, "hash": {"type": "string"}, "body": {"type": "string"}}),
745            &["loaded"],
746        ),
747        true,
748        ALL,
749    );
750    c(
751        "skills.unload",
752        "skills",
753        "Drop a loaded skill from this context.",
754        obj(json!({"name": s("Skill name")}), &["name"]),
755        open_obj(json!({"ok": {"type": "boolean"}}), &["ok"]),
756        true,
757        ALL,
758    );
759
760    // ---- exec (guarded local command runner; DEFAULT-OFF, RFC 0028 §exec) ----
761    // A mapping-only contract by default: agentd runs no local code (RFC 0012)
762    // unless an operator both builds `--features exec` AND sets `security.exec`.
763    // Otherwise `exec` is delegated off-box via `tools.overrides`. It carries the
764    // `sensitive` + `egress` trifecta tags (attached in `Registry::build`).
765    c(
766        "exec",
767        "exec",
768        "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.",
769        obj(
770            json!({
771                "cmd": s("The command to run (argv[0]) — must be in security.exec.allow"),
772                "args": arr(s("Arguments (argv[1..]); passed directly, never through a shell")),
773                "cwd": s("Working directory, relative to and confined within security.exec.workdir"),
774                "stdin": s("Optional standard input for the command"),
775                "timeout": s("Max wall-clock (e.g. `10s`); clamped to the configured maximum")
776            }),
777            &["cmd"],
778        ),
779        open_obj(
780            json!({
781                "stdout": {"type": "string"}, "stderr": {"type": "string"},
782                "exit_code": {"type": "integer"}, "timed_out": {"type": "boolean"}
783            }),
784            &["stdout", "stderr", "exit_code"],
785        ),
786        false,
787        ALL,
788    );
789    v
790}
791
792/// The contract names, in table order.
793pub fn names() -> Vec<&'static str> {
794    contracts().into_iter().map(|c| c.name).collect()
795}
796
797#[cfg(test)]
798mod tests {
799    use super::*;
800
801    #[test]
802    fn contracts_are_unique_well_formed_and_cover_the_rfc_table() {
803        let all = contracts();
804        let mut seen = std::collections::BTreeSet::new();
805        for c in &all {
806            assert!(seen.insert(c.name), "duplicate contract {}", c.name);
807            crate::jsonschema::check_schema(&c.input)
808                .unwrap_or_else(|e| panic!("{}: bad input schema: {e:?}", c.name));
809            crate::jsonschema::check_schema(&c.output)
810                .unwrap_or_else(|e| panic!("{}: bad output schema: {e:?}", c.name));
811            assert!(
812                c.name
813                    .chars()
814                    .all(|ch| ch.is_ascii_alphanumeric() || ch == '.' || ch == '_'),
815                "{}",
816                c.name
817            );
818        }
819        for must in [
820            "instruction.read",
821            "instruction.subscribe",
822            "subagent.run",
823            "subagent.send",
824            "subagent.kill",
825            "subagent.status",
826            "subagent.await",
827            "subagent.list",
828            "code.run",
829            "memory.get",
830            "memory.set",
831            "memory.list",
832            "memory.delete",
833            "artifact.create",
834            "artifact.get",
835            "artifact.delete",
836            "artifact.list",
837            "workflow.run",
838            "workflow.create",
839            "workflow.update",
840            "workflow.delete",
841            "workflow.list",
842            "workflow.status",
843            "workflow.cancel",
844            "workflow.pause",
845            "workflow.resume",
846            "workflow.signal",
847            "workflow.wait",
848            "plan.create",
849            "plan.get",
850            "plan.update",
851            "plan.clear",
852            "ask_human",
853            "sleep",
854            "await",
855            "context.compact",
856            "think",
857            "finish",
858            "status",
859            "knowledge.search",
860            "knowledge.get",
861            "knowledge.list",
862            "search.query",
863            "search.fetch",
864            "skills.list",
865            "skills.load",
866            "skills.unload",
867        ] {
868            assert!(seen.contains(must), "missing contract {must}");
869        }
870        // Mapping-only contracts have no built-in. (`exec` is mapping-only in the
871        // catalogue; a local runner is turned on in `Registry::build` under the
872        // `exec` feature + `security.exec`.)
873        for c in &all {
874            let mapping_only = c.name == "code.run"
875                || c.name == "exec"
876                || c.name.starts_with("knowledge.")
877                || c.name.starts_with("search.");
878            assert_eq!(!c.builtin, mapping_only, "{}", c.name);
879        }
880        // finish is not granted to workflows (they use the finish step).
881        assert!(
882            !all.iter()
883                .find(|c| c.name == "finish")
884                .unwrap()
885                .grant
886                .workflows
887        );
888        assert!(all.iter().find(|c| c.name == "status").unwrap().grant.user);
889    }
890}