@sema-agent/core 7.7.0 → 7.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/agents/agent-transcript-tool.js +2 -2
  3. package/dist/agents/list-agents-tool.js +2 -3
  4. package/dist/agents/observer.js +2 -2
  5. package/dist/agents/send-message-tool.js +2 -1
  6. package/dist/agents/subagent-steps.js +5 -4
  7. package/dist/agents/subagent.js +2 -3
  8. package/dist/core/ask-question.js +2 -2
  9. package/dist/core/checkpoint-store.d.ts +38 -4
  10. package/dist/core/context-edit.d.ts +0 -22
  11. package/dist/core/context-edit.js +2 -11
  12. package/dist/core/engine-notice.d.ts +6 -0
  13. package/dist/core/fs-write-gate-policy.d.ts +7 -1
  14. package/dist/core/fs-write-gate-policy.js +7 -7
  15. package/dist/core/gate-fold.js +2 -2
  16. package/dist/core/gate-lanes.js +4 -3
  17. package/dist/core/governance-codes.d.ts +1 -1
  18. package/dist/core/governance-codes.js +6 -0
  19. package/dist/core/hooks.d.ts +8 -2
  20. package/dist/core/hooks.js +1 -1
  21. package/dist/core/lsp.js +2 -2
  22. package/dist/core/mcp-server-spec.d.ts +17 -0
  23. package/dist/core/mcp.js +3 -1
  24. package/dist/core/memory-engine/tools.js +4 -12
  25. package/dist/core/permission-rules.d.ts +32 -2
  26. package/dist/core/permission-rules.js +43 -37
  27. package/dist/core/present-plan-tool.js +3 -4
  28. package/dist/core/runner/active-skill-scope.js +7 -4
  29. package/dist/core/runner/compaction-call-options.d.ts +1 -1
  30. package/dist/core/runner/contracts.d.ts +27 -3
  31. package/dist/core/runner/prepare-ask-lane.d.ts +1 -1
  32. package/dist/core/runner/prepare-caps-and-workflow.d.ts +5 -4
  33. package/dist/core/runner/prepare-caps-and-workflow.js +16 -11
  34. package/dist/core/runner/prepare-defer-classify.d.ts +5 -26
  35. package/dist/core/runner/prepare-defer-classify.js +10 -10
  36. package/dist/core/runner/prepare-delegation-surface.d.ts +5 -4
  37. package/dist/core/runner/prepare-delegation-surface.js +15 -17
  38. package/dist/core/runner/prepare-gate-stations.d.ts +4 -1
  39. package/dist/core/runner/prepare-gate-stations.js +3 -1
  40. package/dist/core/runner/prepare-hands-readface.d.ts +3 -2
  41. package/dist/core/runner/prepare-hands-readface.js +3 -2
  42. package/dist/core/runner/prepare-inherited-gate.d.ts +1 -1
  43. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  44. package/dist/core/runner/prepare-lsp.d.ts +4 -3
  45. package/dist/core/runner/prepare-lsp.js +3 -2
  46. package/dist/core/runner/prepare-memory-engine-session.d.ts +4 -4
  47. package/dist/core/runner/prepare-memory-engine-session.js +4 -2
  48. package/dist/core/runner/prepare-offload-wrappers.js +7 -5
  49. package/dist/core/runner/prepare-park-ask.d.ts +4 -0
  50. package/dist/core/runner/prepare-park-ask.js +4 -1
  51. package/dist/core/runner/prepare-project-context.d.ts +5 -23
  52. package/dist/core/runner/prepare-project-context.js +7 -5
  53. package/dist/core/runner/prepare-prompt-assembly.d.ts +1 -1
  54. package/dist/core/runner/prepare-protocol-tools.d.ts +5 -1
  55. package/dist/core/runner/prepare-protocol-tools.js +38 -21
  56. package/dist/core/runner/prepare-question-face.d.ts +4 -3
  57. package/dist/core/runner/prepare-question-face.js +5 -3
  58. package/dist/core/runner/prepare-run-refs.d.ts +16 -0
  59. package/dist/core/runner/prepare-run-refs.js +6 -0
  60. package/dist/core/runner/prepare-safety-scan.js +42 -2
  61. package/dist/core/runner/prepare-task.js +37 -19
  62. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -2
  63. package/dist/core/runner/prepare-tool-disclosure-mount.js +3 -1
  64. package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -0
  65. package/dist/core/runner/prepare-wiring-manifest.js +3 -8
  66. package/dist/core/runner/run-harness-handlers.d.ts +46 -0
  67. package/dist/core/runner/run-harness-handlers.js +345 -0
  68. package/dist/core/runner/run-turn-boundary.d.ts +69 -0
  69. package/dist/core/runner/run-turn-boundary.js +694 -0
  70. package/dist/core/runner/runtask.js +47 -1043
  71. package/dist/core/runner/session-rule-policy.js +9 -4
  72. package/dist/core/runner/synthetic-tools.js +5 -8
  73. package/dist/core/runner/tool-disclosure.d.ts +4 -0
  74. package/dist/core/runner/tool-disclosure.js +9 -5
  75. package/dist/core/runner/tool-end-body.js +3 -2
  76. package/dist/core/runner/tool-face-overlay.d.ts +40 -0
  77. package/dist/core/runner/tool-face-overlay.js +125 -0
  78. package/dist/core/runner/tool-output-projection.d.ts +10 -0
  79. package/dist/core/runner/tool-output-projection.js +33 -10
  80. package/dist/core/sensitive-path-policy.d.ts +3 -0
  81. package/dist/core/sensitive-path-policy.js +6 -5
  82. package/dist/core/shared-memory/tools.js +3 -6
  83. package/dist/core/store-contracts/checkpoint-store-contract.js +23 -0
  84. package/dist/core/task-event.d.ts +10 -0
  85. package/dist/core/task-registry.js +4 -5
  86. package/dist/core/task-spec.d.ts +8 -0
  87. package/dist/core/tool-catalog-entries.d.ts +48 -0
  88. package/dist/core/tool-catalog-entries.js +174 -0
  89. package/dist/core/tool-conformance.d.ts +22 -0
  90. package/dist/core/tool-conformance.js +37 -0
  91. package/dist/core/tool-face.d.ts +171 -0
  92. package/dist/core/tool-face.js +25 -0
  93. package/dist/core/tool-policy.d.ts +24 -0
  94. package/dist/core/tool-policy.js +4 -2
  95. package/dist/core/tool-registry.d.ts +114 -0
  96. package/dist/core/tool-registry.js +114 -0
  97. package/dist/core/tool-result-budget.d.ts +0 -3
  98. package/dist/core/tool-result-budget.js +4 -3
  99. package/dist/core/tool-result-store.d.ts +0 -18
  100. package/dist/core/tool-result-store.js +2 -18
  101. package/dist/core/tool-roster.d.ts +450 -0
  102. package/dist/core/tool-roster.js +497 -0
  103. package/dist/core/tool-spec.d.ts +54 -3
  104. package/dist/core/tools.js +7 -0
  105. package/dist/core/wiring-manifest.d.ts +13 -0
  106. package/dist/core/wiring-manifest.js +2 -1
  107. package/dist/core/write-protect.d.ts +2 -1
  108. package/dist/core/write-protect.js +5 -4
  109. package/dist/engine/compaction/utils.d.ts +6 -0
  110. package/dist/engine/compaction/utils.js +19 -19
  111. package/dist/engine/loop/types.d.ts +6 -0
  112. package/dist/index.d.ts +15 -4
  113. package/dist/index.js +15 -4
  114. package/dist/orchestration/goal.js +2 -1
  115. package/dist/orchestration/run-spec.js +3 -5
  116. package/dist/orchestration/run-workflow-tool.js +2 -2
  117. package/dist/prompt-assembly/event-registry.js +3 -3
  118. package/dist/prompt-assembly/tool-catalog.d.ts +2 -2
  119. package/dist/tools/fs/fs-bash.d.ts +1 -1
  120. package/dist/tools/fs/fs-bash.js +7 -11
  121. package/dist/tools/fs/fs-read.js +2 -2
  122. package/dist/tools/fs/fs-search-tools.d.ts +0 -7
  123. package/dist/tools/fs/fs-search-tools.js +3 -16
  124. package/dist/tools/fs/fs-write.js +4 -6
  125. package/dist/tools/fs/repo-map.js +2 -2
  126. package/dist/tools/fs/safety.d.ts +2 -1
  127. package/dist/tools/fs/safety.js +6 -5
  128. package/dist/tools/monitor.js +2 -2
  129. package/dist/tools/scheduler-tools.js +5 -8
  130. package/dist/tools/task-list.js +5 -8
  131. package/dist/tools/todo.js +2 -2
  132. package/dist/tools/web.js +3 -6
  133. package/dist/tools/worktree.js +3 -4
  134. package/package.json +4 -1
  135. package/test/export-surface.snapshot.json +147 -7
@@ -1,8 +1,9 @@
1
1
  import { canonicalizeTarget, writeTargetPath } from "../../tools/fs/safety.js";
2
2
  import { isWinFormPath } from "../../tools/fs/safety.js";
3
3
  import { createCoarseCommandNamePolicy, namespacedCoveringEntries, namespacedCoveringHit } from "../tool-policy.js";
4
- export const PATH_WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit"]);
5
- export const PATH_CONFINABLE_WRITE_TOOLS = new Set([...PATH_WRITE_TOOLS, "NotebookEdit"]);
4
+ import { declaredPathTargetOf, isProtectedWrite, pathConfinableWriteToolNames, skillScopeWriteToolNames } from "../tool-registry.js";
5
+ export const PATH_WRITE_TOOLS = skillScopeWriteToolNames();
6
+ export const PATH_CONFINABLE_WRITE_TOOLS = pathConfinableWriteToolNames();
6
7
  export function isWithin(root, p) {
7
8
  if (!root)
8
9
  return false;
@@ -49,14 +50,18 @@ export function createSessionRulePolicy(rules, opts) {
49
50
  return d;
50
51
  }
51
52
  if (allowDirs) {
52
- if (!PATH_CONFINABLE_WRITE_TOOLS.has(toolName)) {
53
+ if (!isProtectedWrite(req)) {
53
54
  const eff = toolEffects?.get(toolName) ?? "write";
54
55
  if (eff !== "read") {
55
56
  return deny(`write-capable tool "${req.toolName}" denied: session rule confines writes to allowDirs but this tool cannot be path-confined`);
56
57
  }
57
58
  }
58
59
  else {
59
- const path = writeTargetPath(toolName, req.args);
60
+ const declared = declaredPathTargetOf(req);
61
+ if (declared === undefined) {
62
+ return deny(`write tool "${req.toolName}" denied: session rule confines writes to allowDirs but the tool declares no path target to confine`);
63
+ }
64
+ const path = writeTargetPath(req, declared);
60
65
  if (typeof path !== "string" || path.length === 0) {
61
66
  return deny(`write tool "${req.toolName}" denied: session rule confines writes to allowDirs but the call has no resolvable path`);
62
67
  }
@@ -1,6 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import { defineTool } from "../tools.js";
3
3
  import { sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
4
+ import { toolFace } from "../tool-catalog-entries.js";
4
5
  export const OUTPUT_TOOL_NAME = "StructuredOutput";
5
6
  export const REPORT_BLOCKED_TOOL_NAME = "ReportBlocked";
6
7
  export const REPORT_FINDINGS_TOOL_NAME = "ReportFindings";
@@ -15,10 +16,9 @@ export function resolveOutputRetries(outputRetries) {
15
16
  export function createOutputTool(ref, schema, strictSchema) {
16
17
  return defineTool({
17
18
  name: OUTPUT_TOOL_NAME,
18
- contract: { contractId: "core.structured_output@1", implementationRevision: "1" },
19
+ ...toolFace("structured-output"),
19
20
  description: "Call this to submit your FINAL result as a JSON object matching the required schema. The arguments " +
20
21
  "ARE the result. Calling this ends the task. Do this once you have the answer (use other tools first if needed).",
21
- effect: "read",
22
22
  parameters: strictSchema ?? schema,
23
23
  execute: (args) => {
24
24
  ref.value = args;
@@ -30,9 +30,8 @@ export function createOutputTool(ref, schema, strictSchema) {
30
30
  export function createReportBlockedTool(ref) {
31
31
  return defineTool({
32
32
  name: REPORT_BLOCKED_TOOL_NAME,
33
- contract: { contractId: "core.report_blocked@1", implementationRevision: "1" },
33
+ ...toolFace("report-blocked"),
34
34
  description: "Call this ONLY when you cannot complete the task — missing information, missing permission/credentials, or an ambiguous request you cannot resolve. Provide a concise reason that an external system or operator can act on. Calling this ends the task.",
35
- effect: "read",
36
35
  parameters: Type.Object({ reason: Type.String() }),
37
36
  execute: (args) => {
38
37
  ref.reason = String(args.reason ?? "").trim() || "blocked";
@@ -59,14 +58,13 @@ const REPORT_FINDING_SCHEMA = Type.Object({
59
58
  export function createReportFindingsTool() {
60
59
  return defineTool({
61
60
  name: REPORT_FINDINGS_TOOL_NAME,
62
- contract: { contractId: "core.report_findings@1", implementationRevision: "1" },
61
+ ...toolFace("report-findings"),
63
62
  description: "Report code-review findings as a typed list so the host UI can render them. Use this only when the " +
64
63
  "active code-review instructions tell you to report findings with this tool; otherwise follow whatever " +
65
64
  "output format those instructions specify. When reporting a review's results, call it once with the " +
66
65
  "verified findings ranked most-severe first (empty array if nothing survived verification) and do not " +
67
66
  "also print the findings as text. When re-reporting after applying fixes (only if the apply " +
68
67
  "instructions ask for it), set `outcome` on each finding to what actually happened.",
69
- effect: "read",
70
68
  parameters: Type.Object({
71
69
  level: Type.Optional(Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high"), Type.Literal("xhigh"), Type.Literal("max")], { description: "Effort level the review ran at" })),
72
70
  findings: Type.Array(REPORT_FINDING_SCHEMA, {
@@ -177,7 +175,7 @@ export function createSkillTool(skills, scope) {
177
175
  const names = skills.map((s) => s.name).join(", ");
178
176
  return defineTool({
179
177
  name: SKILL_TOOL_NAME,
180
- contract: { contractId: "core.skill@1", implementationRevision: "1" },
178
+ ...toolFace("skill"),
181
179
  description: `Invoke a skill.\n\n` +
182
180
  `A skill is a packaged set of instructions the user or project has set up for a particular kind of task (deploy steps, a review checklist, a repo-specific workflow). ` +
183
181
  `Available skills appear in a system-reminder listing with one-line descriptions. ` +
@@ -192,7 +190,6 @@ export function createSkillTool(skills, scope) {
192
190
  skill: Type.String({ description: 'The skill name. E.g., "commit", "review-pr", or "pdf"' }),
193
191
  args: Type.Optional(Type.String({ description: "Optional arguments for the skill" })),
194
192
  }),
195
- effect: "read",
196
193
  execute: (rawArgs) => {
197
194
  const a = (rawArgs ?? {});
198
195
  const name = String(a.skill ?? "");
@@ -39,6 +39,9 @@ export interface DeferredToolInfo {
39
39
  name: string;
40
40
  hint: string;
41
41
  description: string;
42
+ /** The real tool's aliases — a placeholder stands for the tool under EVERY name it answers to, so dispatch by
43
+ * alias resolves to the same slot before and after activation (and the roster's name→row resolution agrees). */
44
+ aliases?: readonly string[];
42
45
  }
43
46
  /** First line of a description, trimmed to a single short hint for a placeholder / announcement. */
44
47
  export declare function deferHint(description: string, max?: number): string;
@@ -90,6 +93,7 @@ export declare function classifyDeferred(opts: {
90
93
  export declare function buildDeferredRegistry(deferred: ReadonlySet<string>, tools: ReadonlyArray<{
91
94
  name: string;
92
95
  description: string;
96
+ aliases?: readonly string[];
93
97
  }>): Map<string, DeferredToolInfo>;
94
98
  /**
95
99
  * RB-403 — the direct-call lane a placeholder may carry. The upstream defer gate is SHAPE
@@ -3,6 +3,7 @@ import { Value } from "typebox/value";
3
3
  import { defineTool, errorResult } from "../tools.js";
4
4
  import { formatZodValidationError, truncateError } from "../tool-errors.js";
5
5
  import { DEFAULT_CHARS_PER_TOKEN } from "../../internal/harness.js";
6
+ import { toolFace } from "../tool-catalog-entries.js";
6
7
  export const TOOL_SEARCH_NAME = "ToolSearch";
7
8
  const DEFER_AUTO_FRACTION = 0.1;
8
9
  const CHARS_PER_TOKEN = DEFAULT_CHARS_PER_TOKEN;
@@ -62,7 +63,7 @@ export function buildDeferredRegistry(deferred, tools) {
62
63
  const reg = new Map();
63
64
  for (const t of tools) {
64
65
  if (deferred.has(t.name))
65
- reg.set(t.name, { name: t.name, hint: deferHint(t.description), description: t.description });
66
+ reg.set(t.name, { name: t.name, hint: deferHint(t.description), description: t.description, ...(t.aliases !== undefined && t.aliases.length > 0 ? { aliases: [...t.aliases] } : {}) });
66
67
  }
67
68
  return reg;
68
69
  }
@@ -127,6 +128,7 @@ export function createPlaceholderTool(info, direct) {
127
128
  if (direct !== undefined) {
128
129
  return {
129
130
  name: info.name,
131
+ ...(info.aliases !== undefined ? { aliases: [...info.aliases] } : {}),
130
132
  label: info.name,
131
133
  description: `${info.hint} — deferred: its parameters are not listed here. Call ` +
132
134
  `${TOOL_SEARCH_NAME}({"query":"select:${sn}"}) to load them; a call that already matches this ` +
@@ -156,6 +158,7 @@ export function createPlaceholderTool(info, direct) {
156
158
  }
157
159
  return defineTool({
158
160
  name: info.name,
161
+ ...(info.aliases !== undefined ? { aliases: [...info.aliases] } : {}),
159
162
  description: `${info.hint} — deferred: call ${TOOL_SEARCH_NAME}({"query":"select:${sn}"}) to load its parameters before use.`,
160
163
  parameters: EMPTY_PARAMS,
161
164
  effect: "read",
@@ -263,8 +266,10 @@ export function extractDiscoveredToolNames(messages, registry) {
263
266
  const resolved = resolveToolSearch(part.arguments, registry);
264
267
  pendingSearch.set(part.id, prior === undefined ? resolved : [...prior, ...resolved]);
265
268
  }
266
- else if (registry.has(part.name)) {
267
- pendingDirect.set(part.id, part.name);
269
+ else {
270
+ const canonical = registry.has(part.name) ? part.name : [...registry.values()].find((i) => i.aliases?.includes(part.name))?.name;
271
+ if (canonical !== undefined)
272
+ pendingDirect.set(part.id, canonical);
268
273
  }
269
274
  }
270
275
  }
@@ -299,7 +304,7 @@ export function createToolSearchTool(opts) {
299
304
  });
300
305
  return defineTool({
301
306
  name: TOOL_SEARCH_NAME,
302
- contract: { contractId: "core.tool_search@1", implementationRevision: "1" },
307
+ ...toolFace("tool-search"),
303
308
  description: "Discover and activate deferred tools. " +
304
309
  activationPosture +
305
310
  "Query forms: " +
@@ -317,7 +322,6 @@ export function createToolSearchTool(opts) {
317
322
  })),
318
323
  max_results: Type.Optional(Type.Number({ description: "Maximum number of results to return (default: 5)" })),
319
324
  }),
320
- effect: "read",
321
325
  execute: async (raw) => {
322
326
  const staleSelect = (raw ?? {});
323
327
  if (staleSelect.select !== undefined) {
@@ -2,6 +2,7 @@ import {} from "../../internal/harness.js";
2
2
  import { MCP_DELIVERY_VERDICTS } from "../mcp-failure.js";
3
3
  import { TOOL_SEARCH_NAME } from "./tool-disclosure.js";
4
4
  import { structuredFrom, toolOutputFrom } from "./tool-output-projection.js";
5
+ import { TASK_CREATE_TOOL_NAME, TASK_UPDATE_TOOL_NAME, TODO_WRITE_TOOL_NAME } from "../tool-catalog-entries.js";
5
6
  const BATCH_RESPONSE_MAX_CHARS = 500;
6
7
  export function batchResponseDigest(result) {
7
8
  const content = result !== null && typeof result === "object" ? result.content : result;
@@ -54,9 +55,9 @@ export function reconciledToolEndBody(orphan) {
54
55
  return toolEndBodyFrom({ content: orphan.text, details: { code: orphan.errorKind } }, true);
55
56
  }
56
57
  export function writeFamilyOfCanonical(name) {
57
- if (name === "TaskCreate" || name === "TaskUpdate")
58
+ if (name === TASK_CREATE_TOOL_NAME || name === TASK_UPDATE_TOOL_NAME)
58
59
  return "task";
59
- if (name === "TodoWrite")
60
+ if (name === TODO_WRITE_TOOL_NAME)
60
61
  return "todo";
61
62
  if (name === TOOL_SEARCH_NAME)
62
63
  return "tool_search";
@@ -0,0 +1,40 @@
1
+ /**
2
+ * design/388 §1.1 / §2.7 — the MCP `toolFaces` OVERLAY: a deployment's per-tool face declaration for a
3
+ * protocol tool that has no `ToolSpec` of its own (`McpServerSpec.toolFaces`, keyed by the server's RAW
4
+ * tool name like `allowTools` / `toolAxes`). Validated against the MATERIALIZED schema at every listing
5
+ * (the first mount and every RefreshMcpTools): `pathTarget.param` must be a top-level property; an
6
+ * `approvalCard` must find the keys the card needs; a `ruleFace` may only name scalar keys; `family`
7
+ * must be a member of the closed set. A failing overlay does NOT mount — the tool mounts BARE (no
8
+ * face) and the leg announces `config.tool_face_invalid` once per tool; a `toolFaces` key naming a
9
+ * tool the listing does not carry announces `config.tool_face_undeclared` once per name. Never a
10
+ * silent partial application: an overlay is one declaration, applied whole or not at all.
11
+ *
12
+ * Prepare-path machinery (layer 1): pure over the tool objects and the server specs; the notice sink
13
+ * is a parameter.
14
+ */
15
+ import type { AgentTool } from "../../internal/harness-types.js";
16
+ import type { McpServerSpec } from "../mcp-server-spec.js";
17
+ import { type ToolApprovalCard } from "../tool-face.js";
18
+ import { type EngineNotice } from "../engine-notice.js";
19
+ /** The keys each specialised approval card needs on the call (design/388 §2.5 boundary ①: the SHELL owns the
20
+ * card's full schema; this is the pre-filter the overlay is validated against). */
21
+ export declare const APPROVAL_CARD_REQUIRED_KEYS: Readonly<Record<ToolApprovalCard, readonly string[]>>;
22
+ export type McpToolFace = NonNullable<McpServerSpec["toolFaces"]>[string];
23
+ /** Why an overlay is refused — a closed reason list, each a distinct declaration mistake. */
24
+ export declare function toolFaceProblem(face: McpToolFace, schema: AgentTool["parameters"]): string | undefined;
25
+ export interface ApplyToolFacesInput {
26
+ /** The materialized tools of ONE server (namespaced names). */
27
+ tools: readonly AgentTool[];
28
+ server: Pick<McpServerSpec, "name" | "toolFaces">;
29
+ /** The leg's notice sink and its once-per-leg ledger (keys: `invalid:<name>` / `undeclared:<server>/<raw>`). */
30
+ onNotice: ((notice: EngineNotice) => void) | undefined;
31
+ announced: Set<string>;
32
+ sessionId: string;
33
+ runId: string;
34
+ }
35
+ /** Apply the server's overlay to its tools: a validated face is stamped on a COPY of the tool object (the same
36
+ * untyped carry `defineTool` uses, so the roster reads it off the instance). A face that does NOT fit the materialized
37
+ * schema is a bad declaration: the tool is NOT mounted on this leg (announced once, `config.tool_face_invalid`) —
38
+ * mounting it bare would silently drop the write protection the operator declared for it (an MCP name has no catalog
39
+ * floor), and a schema refresh that invalidates only a display hint must fail loud, never open. */
40
+ export declare function applyMcpToolFaces(input: ApplyToolFacesInput): AgentTool[];
@@ -0,0 +1,125 @@
1
+ import { MCP_NAMESPACE } from "../protocol-table.js";
2
+ import { mintNamespacePrefix } from "../protocol-naming.js";
3
+ import { TOOL_APPROVAL_CARDS, TOOL_FAMILIES, RENDER_HINT_ACTIVITY_MAX_CHARS, RENDER_HINT_MAX_CHARS, RENDER_HINT_MAX_LIST, TOOL_KEY_MAX_CHARS } from "../tool-face.js";
4
+ import { inputKeysOf } from "../tool-roster.js";
5
+ import { deliverEngineNotice } from "../engine-notice.js";
6
+ export const APPROVAL_CARD_REQUIRED_KEYS = Object.freeze({
7
+ "file-edit": ["file_path"],
8
+ "file-write": ["file_path"],
9
+ "notebook-edit": ["notebook_path"],
10
+ shell: ["command"],
11
+ fetch: ["url"],
12
+ plan: [],
13
+ question: [],
14
+ generic: [],
15
+ });
16
+ export function toolFaceProblem(face, schema) {
17
+ const { keys, scalar } = inputKeysOf(schema);
18
+ const top = new Set(keys.map((k) => k.name));
19
+ if (face.family !== undefined && !TOOL_FAMILIES.includes(face.family))
20
+ return `family ${JSON.stringify(face.family)} is outside the closed set`;
21
+ const overlong = (label, values) => {
22
+ const k = values.find((v) => v.length > TOOL_KEY_MAX_CHARS);
23
+ return k === undefined ? undefined : `${label} ${JSON.stringify(k.slice(0, 40) + "…")} is ${k.length} characters; a roster key is at most ${TOOL_KEY_MAX_CHARS}`;
24
+ };
25
+ const vocabulary = new Set(scalar);
26
+ if (face.pathTarget !== undefined) {
27
+ if (typeof face.pathTarget.param !== "string" || !top.has(face.pathTarget.param))
28
+ return `pathTarget.param ${JSON.stringify(face.pathTarget.param)} is not a top-level property of the tool's schema`;
29
+ if (!["read", "create", "edit"].includes(face.pathTarget.access))
30
+ return `pathTarget.access ${JSON.stringify(face.pathTarget.access)} is outside the closed set`;
31
+ const long = overlong("pathTarget.param", [face.pathTarget.param]) ?? overlong("pathTarget.aliases", face.pathTarget.aliases ?? []);
32
+ if (long !== undefined)
33
+ return long;
34
+ vocabulary.add(face.pathTarget.param);
35
+ for (const a of face.pathTarget.aliases ?? [])
36
+ vocabulary.add(a);
37
+ }
38
+ const hints = face.renderHints;
39
+ if (hints !== undefined) {
40
+ const card = hints.approvalCard;
41
+ if (card !== undefined) {
42
+ if (!TOOL_APPROVAL_CARDS.includes(card))
43
+ return `renderHints.approvalCard ${JSON.stringify(card)} is outside the closed set`;
44
+ const missing = APPROVAL_CARD_REQUIRED_KEYS[card].filter((k) => !top.has(k));
45
+ if (missing.length > 0)
46
+ return `renderHints.approvalCard ${JSON.stringify(card)} needs ${missing.join(", ")} on the tool's schema`;
47
+ }
48
+ if (hints.userFacingName !== undefined && hints.userFacingName.length > RENDER_HINT_MAX_CHARS)
49
+ return `renderHints.userFacingName is ${hints.userFacingName.length} characters; the bound is ${RENDER_HINT_MAX_CHARS}`;
50
+ if (hints.activity !== undefined && hints.activity.length > RENDER_HINT_ACTIVITY_MAX_CHARS)
51
+ return `renderHints.activity is ${hints.activity.length} characters; the bound is ${RENDER_HINT_ACTIVITY_MAX_CHARS}`;
52
+ if ((hints.summaryParams?.length ?? 0) > RENDER_HINT_MAX_LIST)
53
+ return `renderHints.summaryParams lists ${hints.summaryParams.length} entries; the bound is ${RENDER_HINT_MAX_LIST}`;
54
+ if ((hints.resultCards?.length ?? 0) > RENDER_HINT_MAX_LIST)
55
+ return `renderHints.resultCards lists ${hints.resultCards.length} entries; the bound is ${RENDER_HINT_MAX_LIST}`;
56
+ const long = overlong("renderHints.summaryParams", hints.summaryParams ?? []) ?? overlong("renderHints.resultCards", hints.resultCards ?? []);
57
+ if (long !== undefined)
58
+ return long;
59
+ const stray = (hints.summaryParams ?? []).find((p) => !top.has(p));
60
+ if (stray !== undefined)
61
+ return `renderHints.summaryParams names ${JSON.stringify(stray)}, which is not a top-level property`;
62
+ }
63
+ for (const p of face.ruleFace?.params ?? [])
64
+ if (!vocabulary.has(p))
65
+ return `ruleFace.params names ${JSON.stringify(p)}, which is neither a scalar top-level property nor the declared path target`;
66
+ for (const p of face.ruleFace?.primaryParams ?? [])
67
+ if (!vocabulary.has(p))
68
+ return `ruleFace.primaryParams names ${JSON.stringify(p)}, which is neither a top-level property nor the declared path target`;
69
+ const long = overlong("ruleFace.params", face.ruleFace?.params ?? []) ?? overlong("ruleFace.primaryParams", face.ruleFace?.primaryParams ?? []);
70
+ if (long !== undefined)
71
+ return long;
72
+ return undefined;
73
+ }
74
+ export function applyMcpToolFaces(input) {
75
+ const faces = input.server.toolFaces;
76
+ if (faces === undefined)
77
+ return [...input.tools];
78
+ const prefix = mintNamespacePrefix(MCP_NAMESPACE, input.server.name);
79
+ const accepted = new Set(input.tools.filter((t) => t.name.startsWith(prefix)).map((t) => t.mcpRemoteName).filter((r) => typeof r === "string"));
80
+ for (const raw of Object.keys(faces)) {
81
+ if (accepted.has(raw))
82
+ continue;
83
+ const key = `undeclared:${input.server.name}/${raw}`;
84
+ if (input.announced.has(key))
85
+ continue;
86
+ input.announced.add(key);
87
+ deliverEngineNotice(input.onNotice, {
88
+ code: "config.tool_face_undeclared",
89
+ message: `MCP server "${input.server.name}": toolFaces declares a face for "${raw}", which the server's tool listing does not carry — the declaration is inert`,
90
+ detail: { server: input.server.name, tool: raw, sessionId: input.sessionId, runId: input.runId },
91
+ });
92
+ }
93
+ return input.tools.flatMap((t) => {
94
+ const stamped = stampOne(t);
95
+ return stamped === undefined ? [] : [stamped];
96
+ });
97
+ function stampOne(t) {
98
+ const raw = t.mcpRemoteName;
99
+ const face = raw !== undefined && t.name.startsWith(prefix) ? faces?.[raw] : undefined;
100
+ if (raw === undefined || face === undefined)
101
+ return t;
102
+ const hit = { raw, face };
103
+ const problem = toolFaceProblem(hit.face, t.parameters);
104
+ if (problem !== undefined) {
105
+ const key = `invalid:${t.name}`;
106
+ if (!input.announced.has(key)) {
107
+ input.announced.add(key);
108
+ deliverEngineNotice(input.onNotice, {
109
+ code: "config.tool_face_invalid",
110
+ message: `MCP server "${input.server.name}": the toolFaces overlay for "${hit.raw}" does not fit the tool's materialized schema (${problem}) — the tool is NOT mounted until the overlay fits (mounting it bare would drop the protection the overlay declares)`,
111
+ detail: { server: input.server.name, tool: hit.raw, problem, sessionId: input.sessionId, runId: input.runId },
112
+ });
113
+ }
114
+ return undefined;
115
+ }
116
+ const f = hit.face;
117
+ return {
118
+ ...t,
119
+ ...(f.family !== undefined ? { family: f.family } : {}),
120
+ ...(f.pathTarget !== undefined ? { pathTarget: f.pathTarget } : {}),
121
+ ...(f.renderHints !== undefined ? { renderHints: f.renderHints } : {}),
122
+ ...(f.ruleFace !== undefined ? { ruleFace: f.ruleFace } : {}),
123
+ };
124
+ }
125
+ }
@@ -9,4 +9,14 @@ export declare const toolOutputFrom: (result: unknown) => {
9
9
  truncated: boolean;
10
10
  totalChars?: number;
11
11
  } | undefined;
12
+ /**
13
+ * L4's run-time half (design/388 §2.4): a per-leg projector — the engine card set plus the leg's declared
14
+ * caller cards — that DROPS an undeclared `details.type` exactly as the module-level gate does and announces
15
+ * it ONCE per leg (`config.tool_card_undeclared`, audience operator). The module-level {@link structuredFrom}
16
+ * is this projector over the engine set alone, with no announce seat.
17
+ */
18
+ export declare function createStructuredProjector(opts: {
19
+ declaredCards?: Iterable<string>;
20
+ onUndeclared?: (type: string) => void;
21
+ }): (result: unknown) => unknown;
12
22
  export declare const structuredFrom: (result: unknown) => unknown;
@@ -1,3 +1,4 @@
1
+ import { engineCardTypes } from "../tool-registry.js";
1
2
  const MAX_TOOL_END_OUTPUT_CHARS = 16384;
2
3
  const safeJsonLength = (v) => {
3
4
  try {
@@ -56,16 +57,38 @@ export const toolOutputFrom = (result) => {
56
57
  }
57
58
  return { output: raw, truncated: true, totalChars };
58
59
  };
59
- const CC_DETAIL_TYPES = new Set([
60
- "ask-question",
61
- "edit", "create", "update", "bash", "notebook-edit", "notebook", "file_unchanged", "worktree", "text", "grep", "glob", "mcp",
62
- "agent", "task", "task-list", "task-output", "workflow-run",
63
- "web-fetch", "web-search", "todo", "cron-create", "cron-delete", "cron-list", "image",
64
- "task-stop", "tool-search", "repo-map", "fork", "enter-plan-mode", "exit-plan-mode",
65
- "monitor-start", "path_not_in_root", "read_path_denied", "readonly_out_of_root", "bash_invalid_timeout",
66
- "report-findings", "schedule-wakeup", "send-message", "agent-transcript", "a2a", "document",
67
- "list-agents",
68
- ]);
60
+ const CC_DETAIL_TYPES = engineCardTypes();
61
+ export function createStructuredProjector(opts) {
62
+ const allowed = new Set([...CC_DETAIL_TYPES, ...(opts.declaredCards ?? [])]);
63
+ const announced = new Set();
64
+ return (result) => {
65
+ const details = result !== null && typeof result === "object" ? result.details : undefined;
66
+ if (details === null || typeof details !== "object")
67
+ return undefined;
68
+ const t = details.type;
69
+ if (typeof t !== "string")
70
+ return undefined;
71
+ if (!allowed.has(t)) {
72
+ if (opts.onUndeclared !== undefined && !announced.has(t)) {
73
+ announced.add(t);
74
+ opts.onUndeclared(t);
75
+ }
76
+ return undefined;
77
+ }
78
+ return boundedCard(details);
79
+ };
80
+ }
81
+ function boundedCard(details) {
82
+ try {
83
+ const serialized = JSON.stringify(details) ?? "";
84
+ if (serialized.length > MAX_TOOL_END_OUTPUT_CHARS * 4)
85
+ return undefined;
86
+ }
87
+ catch {
88
+ return undefined;
89
+ }
90
+ return details;
91
+ }
69
92
  export const structuredFrom = (result) => {
70
93
  const details = result !== null && typeof result === "object" ? result.details : undefined;
71
94
  if (details === null || typeof details !== "object")
@@ -1,5 +1,8 @@
1
1
  import type { ExecutionEnv } from "../internal/harness-types.js";
2
2
  import type { ToolPolicy } from "./tool-policy.js";
3
+ /** fs hand tools that take a `path` and write — the reliably-guardable surface. NotebookEdit joined
4
+ * in [886]①a (mirror of the fs-write-gate's covered set): its target is `notebook_path`, and a
5
+ * guard that ignored notebook writes to the same paths by default was the same hole one tool away. */
3
6
  /**
4
7
  * RECOMMENDED default pattern set for {@link createSensitivePathPolicy} (additive export).
5
8
  * NO upstream verbatim anchor exists (cc-anchors/2.1.209 carries no sensitive/dangerous file-pattern
@@ -2,7 +2,7 @@ import { homedir } from "node:os";
2
2
  import { isAbsolute, join, resolve } from "node:path";
3
3
  import { canonicalizeTarget, expandHomeTilde, isAbsolutePathForm, isWinFormPath, writeTargetPath } from "../tools/fs/safety.js";
4
4
  import { compileSegmentPattern, matchSegmentPatterns } from "../tools/fs/read-deny.js";
5
- const DEFAULT_GUARDED_TOOLS = ["Write", "Edit", "MultiEdit", "NotebookEdit"];
5
+ import { isProtectedWrite } from "./tool-registry.js";
6
6
  export const RECOMMENDED_SENSITIVE_PATTERNS = [
7
7
  ".env",
8
8
  ".env.*",
@@ -90,13 +90,14 @@ export function createSensitivePathPolicy(opts) {
90
90
  }
91
91
  return keys;
92
92
  };
93
- const guarded = new Set(opts.tools ?? DEFAULT_GUARDED_TOOLS);
93
+ const guardedByName = opts.tools !== undefined ? new Set(opts.tools) : undefined;
94
94
  return {
95
95
  async check(req, signal) {
96
- const canonical = req.toolName;
97
- if (compiled.length === 0 || !guarded.has(canonical))
96
+ if (compiled.length === 0)
98
97
  return { action: "allow" };
99
- const path = writeTargetPath(canonical, req.args);
98
+ if (guardedByName !== undefined ? !guardedByName.has(req.toolName) : !isProtectedWrite(req))
99
+ return { action: "allow" };
100
+ const path = writeTargetPath(req);
100
101
  if (typeof path !== "string" || path.length === 0)
101
102
  return { action: "allow" };
102
103
  const canon = await canonicalizeTarget(opts.env, path, signal, req.cwd ?? opts.rootPath);
@@ -2,6 +2,7 @@ import { Type } from "typebox";
2
2
  import { errorResult } from "../tools.js";
3
3
  import { CONNECTING_MESSAGE, GENERIC_FAILURE_MESSAGE, NO_STORES_MESSAGE, SHARED_STORE_FRAME, UNAVAILABLE_MESSAGE, datePrefixOf, echoUntrusted, foldReaderError, listPrefixArgument, normalizeEntries, normalizeMemoryPath, normalizeReadValue, normalizeSnapshot, pageDocuments, relativePathRefusal, sanitizeSharedContent, validateDocumentPath, validatePathPrefix, } from "./normalize.js";
4
4
  import { SHARED_MEMORY_LIST_PAGE_SIZE, SHARED_MEMORY_LIST_TOOL_NAME, SHARED_MEMORY_READ_CAP_BYTES, SHARED_MEMORY_READ_TOOL_NAME, } from "./types.js";
5
+ import { toolFace } from "../tool-catalog-entries.js";
5
6
  const LIST_HINT = "List shared memory stores and their documents — check here before telling the user you do not have something.";
6
7
  const LIST_DESCRIPTION = [
7
8
  LIST_HINT,
@@ -125,11 +126,9 @@ export function createSharedMemoryTools(opts) {
125
126
  };
126
127
  const listTool = {
127
128
  name: SHARED_MEMORY_LIST_TOOL_NAME,
129
+ ...toolFace("memory-list"),
128
130
  description: LIST_DESCRIPTION,
129
- effect: "read",
130
131
  defer: true,
131
- offload: false,
132
- contract: { contractId: "core.memory_list@1", implementationRevision: "1" },
133
132
  parameters: Type.Object({
134
133
  store: Type.Optional(Type.String({
135
134
  description: "Id of the connected memory store to list. Omit to list the memory stores connected to this session (id, description, and writable or read-only).",
@@ -258,11 +257,9 @@ export function createSharedMemoryTools(opts) {
258
257
  };
259
258
  const readTool = {
260
259
  name: SHARED_MEMORY_READ_TOOL_NAME,
260
+ ...toolFace("memory-read"),
261
261
  description: READ_DESCRIPTION,
262
- effect: "read",
263
262
  defer: true,
264
- offload: false,
265
- contract: { contractId: "core.memory_read@1", implementationRevision: "1" },
266
263
  parameters: Type.Object({
267
264
  store: Type.String({
268
265
  description: `Id of the connected memory store to read from (call ${SHARED_MEMORY_LIST_TOOL_NAME} with no arguments to see the connected stores).`,
@@ -301,6 +301,29 @@ export async function checkpointStoreContract(make, runAssertion) {
301
301
  const stored = (await store.get(bitless.token)).pendingAction;
302
302
  assert.equal("hasBidiControls" in stored, false, "projection backfill must not write back to the row");
303
303
  });
304
+ run("design/388 B18 × B6 identity + face group: a row's seven optional members (capabilityId/contractId/shapeDigest/implementationRevision/rosterDigest + family/pathTarget) survive the round-trip verbatim; an absent group stays absent", async () => {
305
+ const store = make();
306
+ const carried = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "id-carried" });
307
+ const group = {
308
+ capabilityId: "caller::Write@acme.write@2",
309
+ contractId: "acme.write@2",
310
+ shapeDigest: "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
311
+ implementationRevision: "r7",
312
+ rosterDigest: "sha256:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
313
+ family: "file-write",
314
+ pathTarget: { param: "destination", aliases: ["path"], access: "create", skillScopeEligible: true },
315
+ };
316
+ Object.assign(carried.pendingAction, group);
317
+ const bare = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "id-bare" });
318
+ for (const cp of [carried, bare])
319
+ await store.put(cp.token, cp);
320
+ const back = (await store.get(carried.token)).pendingAction;
321
+ for (const [k, v] of Object.entries(group))
322
+ assert.deepEqual(back[k], v, `the row's ${k} must survive the round-trip verbatim (a whitelisting serializer that drops it makes every resume-edit recheck judge by the catalog's slot for the NAME)`);
323
+ const bareBack = (await store.get(bare.token)).pendingAction;
324
+ for (const k of Object.keys(group))
325
+ assert.equal(k in bareBack, false, `an absent ${k} must stay OMITTED (a pre-388 row reads name-only, never a partial face)`);
326
+ });
304
327
  run("#457 previewWithheld three-form matrix: a row's spelling survives + projects; absent stays absent; an out-of-contract spelling reads as absent", async () => {
305
328
  const store = make();
306
329
  const oversize = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "pw-oversize" });
@@ -885,6 +885,16 @@ export type TaskEvent = ({
885
885
  */
886
886
  type: "wiring_manifest";
887
887
  manifest: import("./wiring-manifest.js").WiringManifest;
888
+ } & TaskEventIdentity) | ({
889
+ /**
890
+ * design/388 §2.5 — a RUN-TIME change to this leg's mounted tool set (today: the RefreshMcpTools
891
+ * seam re-inserting a server's tools in place). Carries the WHOLE post-change roster (positions
892
+ * come from it; a consumer replaces its copy) plus a summary usable only when the consumer holds
893
+ * `fromDigest` — otherwise it adopts the snapshot and records a skew (never rejects). The prepared
894
+ * roster itself rides `wiring_manifest.manifest.tools`; this frame never precedes that one.
895
+ */
896
+ type: "tool_roster_delta";
897
+ delta: import("./tool-roster.js").ToolRosterDelta;
888
898
  } & TaskEventIdentity) | {
889
899
  type: "done";
890
900
  result: TaskResult;
@@ -9,8 +9,9 @@ import { registerMonitorLane, pollMonitorLane, stopMonitorLane } from "./task-re
9
9
  import { registerWorkflowLane, pollWorkflowLane, stopWorkflowLane } from "./task-registry-workflow.js";
10
10
  import { mintCompletionId, canAccessWorkflowRun, formatWorkflowRun, clipTaskOutput, assertOwnership, sleepPollStep, statusFromBackground, rollSpoolText, accountDroppedBytes, renderSpoolBody, spoolDropNote, droppedGapNote, alreadyTerminalStopNote, terminalTaskSummary, TASK_OUTPUT_MAX_CHARS, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccess, DURABLE_AGENT_HANDLE_RE, } from "./task-registry-shared.js";
11
11
  export { normalizeAgentName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR } from "./task-registry-shared.js";
12
- import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_CONTRACT, TASK_STOP_CONTRACT, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, REGISTRY_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "./task-tool-shape.js";
12
+ import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, REGISTRY_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "./task-tool-shape.js";
13
13
  import { durableAgentArmedLane, durableAgentRowProbeLane, beginDurableClaimLane, endDurableClaimLane, reapDurableAgentsLane, noteBackgroundAgentActivityLane, reapStaleSessionBackgroundAgentsLane, releaseDurableTranscriptAnchorLane, bindBackgroundAgentSessionLane, registerBackgroundAgentLane, recordBackgroundAgentOrgAdmissionLane, parkBackgroundAgentLane, reconcileParkedAgentsLane, claimParkedAgentLane, rollbackParkedClaimLane, consumeParkedFlipLane, finalizeParkedResumeLane, settleBackgroundAgentLane, abortBackgroundAgentsForOwnerLane, serveDurableAgentRowLane, resolveBackgroundAgentByNameLane, backgroundAgentCycleSeqLane, activeDelegationHandlesLane, markRetainedContinuationLane, reviveBackgroundAgentLane, settleRevivedAgentLane, unmarkRetainedContinuationLane, attachAgentNotifyLane, deliverToRunningAgentLane, runningBackgroundAgentLabelsLane, runningAgentFooterLane, notFoundRunningAgentsTail, pollBackgroundAgentLane, stopBackgroundAgentLane, } from "./task-registry-agent.js";
14
+ import { toolFace } from "./tool-catalog-entries.js";
14
15
  export { canAccessWorkflowRun, clipTaskOutput, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE };
15
16
  const BLOCK_DEFAULT_TIMEOUT_MS = 30_000;
16
17
  const BLOCK_MAX_TIMEOUT_MS = 600_000;
@@ -1234,10 +1235,9 @@ export function createTaskOutputTool(opts) {
1234
1235
  const caps = { ...REGISTRY_TASK_TOOL_CAPS, notification: opts.notificationWired !== false };
1235
1236
  return defineTool({
1236
1237
  name: TASK_OUTPUT_TOOL_NAME,
1237
- contract: TASK_OUTPUT_CONTRACT,
1238
+ ...toolFace("task-output"),
1238
1239
  description: composeTaskOutputDescription(caps),
1239
1240
  parameters: composeTaskOutputParams(caps),
1240
- effect: "read",
1241
1241
  execute: async (rawArgs, ctx) => {
1242
1242
  const args = rawArgs;
1243
1243
  const id = resolveTaskIdArg(args.task_id);
@@ -1283,10 +1283,9 @@ export function createTaskOutputTool(opts) {
1283
1283
  export function createTaskStopTool(opts) {
1284
1284
  return defineTool({
1285
1285
  name: TASK_STOP_TOOL_NAME,
1286
- contract: TASK_STOP_CONTRACT,
1286
+ ...toolFace("task-stop"),
1287
1287
  description: composeTaskStopDescription(REGISTRY_TASK_TOOL_CAPS),
1288
1288
  parameters: TASK_STOP_PARAMS,
1289
- effect: "write",
1290
1289
  execute: async (rawArgs, ctx) => {
1291
1290
  const args = rawArgs;
1292
1291
  const id = resolveTaskIdArg(args.task_id, args.shell_id);
@@ -387,6 +387,14 @@ export interface TaskSpec {
387
387
  * owns that risk, same trust level as assembling `spec.tools` itself.
388
388
  */
389
389
  excludeTools?: string[];
390
+ /**
391
+ * design/388 §2.2 ⑥ — unmount the WHOLE tool face: the leg mounts nothing (no hands, no
392
+ * scaffolds, no MCP or skill tools; the effect roster reads `entries: []` and the model request
393
+ * carries `tools: []`). The utility-task shape a shell used to express with a hand-kept exclusion
394
+ * list. Not spelled `excludeTools: ["*"]` on purpose — `"*"` is a legal caller tool name (only `__`
395
+ * is reserved). `excludeTools` beside it is redundant, not an error.
396
+ */
397
+ excludeAllTools?: true;
390
398
  /**
391
399
  * Per-request DEFERRED disclosure for ALREADY-MOUNTED tools (built-ins
392
400
  * included — `ToolSpec.defer` only covers caller specs): wire names listed here ship as