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