Files
deepseek-harness/docs/tool-catalog.md
T
imccyu 1b1ba96d4f fix(tool-cordis): replace the pass-through ctx proxy with a whitelist façade
Review finding (#220): the guarded proxy only special-cased ctx.tools, so
mount code could reach an UNGUARDED context through ctx.root, ctx.extend(), or
a service instance's .ctx, then ctx.root.tools.register({…}) to bypass the
marker check and host-realm normalization — a raw vm-realm result would later
error a real agent turn at the session-log plainness check.

The sandbox ctx is now a whitelist façade, not a pass-through proxy: it exposes
only what a mount needs — tools.register (marker-guarded), on/once, provide, the
timer helpers, and injected services resolved through a guarded get — and denies
every framework-plumbing member (root, parent, fiber, reflect, registry, extend,
isolate, intercept, plugin, set, mixin, …) with a teaching error. Injected
services are wrapped so a method returning a Context is rejected on the way back
(the .ctx escape), closing the one indirect leak. There is no context-valued
member left to reach; cross-mount provide/inject is untouched (the plugin's own
inject and the fiber's pending/active gating are unchanged). ctx.plugin (child
plugins) and ctx.set are denied by design; ctx.effect is deferred (FIXME).

Adds tests/sandbox-context.spec.ts covering the escape class (root/extend/fiber/
plugin/set/… denied, the classic root.tools.register bypass, the .ctx escape,
read-only writes) plus the async-service and symbol/in-operator paths for 100%
coverage. RFC/README/tool-catalog/config-catalog updated; api-catalog.ts
regenerated (also picks up the codeRuntime service that entered on the master
merge and was left stale).
2026-07-09 13:57:03 +08:00

19 KiB

Tool Schema Catalog

Every model-facing tool a shipped plugin contributes to ctx.tools: the name, description, and JSON-Schema parameters the model receives via the system-prompt assembly. It complements the cordis events & services catalogs (the wiring a plugin listens to and calls) and core-data-structures/ (the types those signatures move) — this page is the tools the agent is offered.

This file is GENERATED and verified fresh by pnpm run verify-tool-catalog (part of doc-sync) — do not edit it by hand. Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads ctx.tools.schemas(), because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs packages/*/tool-* and fails if any package is missing from the generator's boot manifest, so a new tool cannot be silently undocumented. See the tool-schema-catalog RFC.

Scope: shipped product tools under packages/*/tool-*, each booted with its DEFAULT config. The registered tool NAME can be a load-time config (e.g. tool-subagent's toolName), so a deployment may surface a package under a different or additional name — a per-package note records those shipped aliases where they exist. The examples/ demo tools (e.g. echo) are excluded, matching the cordis catalog's packages-only scope.

Tool Package Map

This table connects model-visible tool names to the plugin package and service seams behind them. Exact JSON Schemas follow in the package sections below.

Tool package Model-visible names Requires Writes / affects Shipped aliases Deployment note
@deepseek-ai/dsh-tool-bash bash, bash_kill, bash_output ctx.tools, ctx.bash tool/call, tool/result, context/message via agent.inject() for background completion notices - The bash/bash_output/bash_kill tools are model-facing consumers of the bash executor seam.
@deepseek-ai/dsh-tool-cordis cordis_inspect, cordis_mount, cordis_unmount ctx.tools tool/call, tool/result, live plugin-tree mutations (mount/unmount) - Ships in examples/cordis-agent only (a deliberate opt-in — mounted code gets the real ctx, see docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins the model mounts may register ADDITIONAL model-visible tools at runtime; the request-header ToolsDelta logs those tool-set changes.
@deepseek-ai/dsh-tool-fs edit, read, write ctx.tools, ctx.fs, ctx.systemPrompt tool/call, fs/write-intent or fs/edit-intent for mutations, fs/observed after successful file operations, tool/result - The read-before-write/edit policy is added by @deepseek-ai/dsh-fs-policy (an fs/* event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The tool schemas above are identical with or without the policy plugin.
@deepseek-ai/dsh-tool-subagent subagent ctx.tools, ctx.subagents tool/call, tool/result, child session events through the chosen provider subagent, subagent_fork The registered tool name is the load-time toolName config (default subagent); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees subagent_fork (bound to the fork backend) with an identical schema — see examples/coding-agent/cordis.yml and examples/acp-agent/cordis.yml.
@deepseek-ai/dsh-tool-todo todo_write ctx.tools, owning Agent session tool/call, todo/write, tool/result - todo_write is session-owned state; UIs render the latest todo/write event as a checklist or ACP plan.
@deepseek-ai/dsh-tool-web web_fetch, web_search ctx.tools, ctx.web, ctx.systemPrompt tool/call, tool/result - web_search and web_fetch keep provider selection behind ctx.web so model-visible schemas stay stable across backend swaps.

@deepseek-ai/dsh-tool-bash

bash

Execute a bash command (bash -c) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass workdir instead of using cd. Non-zero exits are reported as [exit code: N]. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set run_in_background: true for long-running commands: the call returns a task id immediately; poll it with bash_output and stop it with bash_kill.

{
  "type": "object",
  "properties": {
    "command": {
      "type": "string",
      "description": "The bash command to execute."
    },
    "description": {
      "type": "string",
      "description": "Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."
    },
    "timeoutMs": {
      "type": "number",
      "description": "Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."
    },
    "workdir": {
      "type": "string",
      "description": "Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."
    },
    "run_in_background": {
      "type": "boolean",
      "description": "Run in the background and return a task id immediately. No timeout applies."
    }
  },
  "required": [
    "command",
    "description"
  ]
}

Source: packages/bash/tool-bash/src/index.ts

bash_kill

Ask the executor to kill a running background bash task by task id.

{
  "type": "object",
  "properties": {
    "task_id": {
      "type": "string",
      "description": "Task id returned by the bash tool."
    }
  },
  "required": [
    "task_id"
  ]
}

Source: packages/bash/tool-bash/src/index.ts

bash_output

Read new output from a background bash task started with bash + run_in_background. Returns only output produced since the previous bash_output call, plus the task status. Tasks keep running while you do other work; poll again later for more output.

{
  "type": "object",
  "properties": {
    "task_id": {
      "type": "string",
      "description": "Task id returned by the bash tool."
    }
  },
  "required": [
    "task_id"
  ]
}

Source: packages/bash/tool-bash/src/index.ts

The bash/bash_output/bash_kill tools are model-facing consumers of the bash executor seam.

@deepseek-ai/dsh-tool-cordis

cordis_inspect

Inspect the live cordis runtime that is running THIS agent. Read-only. Sections: services (every provided ctx service and the plugin fiber that owns it), plugins (a flat list of the loaded plugins with their lifecycle states), tools (the model-facing tools currently registered, i.e. what you can call), dynamic (plugins you mounted via cordis_mount: id, name, state, provided services, awaited services), api (method signatures AND argument/return type shapes for every LIVE service — read this before writing plugin code that calls a service), events (every harness event with its dispatch mode and exact signature — pick listener targets here). Omit what to get all six sections.

{
  "type": "object",
  "properties": {
    "what": {
      "type": "string",
      "description": "Limit the report to one section. Omit for all sections.",
      "enum": [
        "services",
        "plugins",
        "tools",
        "dynamic",
        "api",
        "events"
      ]
    }
  }
}

Source: packages/cordis/tool-cordis/src/index.ts

cordis_mount

Mount a NEW cordis plugin into the live runtime that is running THIS agent (self-modification). code runs as the body of an async JavaScript function in an isolated sandbox and MUST return a plugin. Two forms: FUNCTION form return (ctx) => { … } — cannot declare inject, uses whatever services are on the parent context, and accessing a service without inject (e.g. ctx.bash) throws; use it only when you need no injected services. OBJECT form return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } } — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form for any plugin that needs bash, llm, sessions, etc. BEFORE calling a service from your code, read cordis_inspect what:"api" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside apply, use the standard cordis API: ctx.on(event, listener) to observe events (see cordis_inspect what:"events"), or call harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, async execute(args) { … } })) to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'boolean'|'object'|'array', required?: true, description?, enum?, items?, properties? }; a JSON-Schema-style { type: 'object', properties, required: […] } wrapper and type 'integer' are also accepted and normalized. A tool's execute MUST return an ARRAY of content blocks, e.g. return [{ type: 'text', text: someString }] — never a bare string. Mounts can COMPOSE: one plugin may ctx.provide('name', value) a service and another may declare inject: ['name'] to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside apply is cleaned up automatically on unmount. Sandbox globals: console (tagged [cordis:<id>], writes through to the harness terminal), harness.defineTool, harness.registerTool, btoa, atob, TextEncoder, TextDecoder. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: require, setTimeout/setInterval, and fetch throw redirect errors; process and Buffer are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned on unmount) — cordis_inspect what:"api" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no as, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing next callback which MUST be called — returning without next() VETOES the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your ctx is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.

{
  "type": "object",
  "properties": {
    "code": {
      "type": "string",
      "description": "Body of an async JS function; must `return` the plugin to mount."
    }
  },
  "required": [
    "code"
  ]
}

Source: packages/cordis/tool-cordis/src/index.ts

cordis_unmount

Dispose a plugin previously mounted with cordis_mount, by id. All its registrations (event listeners, tools, services) are cleaned up through the cordis effect lifecycle. Returns only after disposal has fully completed (quiescence, not just a request to stop).

{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "description": "The dynamic mount id returned by cordis_mount (e.g. \"dyn-1\")."
    }
  },
  "required": [
    "id"
  ]
}

Source: packages/cordis/tool-cordis/src/index.ts

Ships in examples/cordis-agent only (a deliberate opt-in — mounted code gets the real ctx, see docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins the model mounts may register ADDITIONAL model-visible tools at runtime; the request-header ToolsDelta logs those tool-set changes.

@deepseek-ai/dsh-tool-fs

edit

Edit an existing UTF-8 text file by replacing literal text.

{
  "type": "object",
  "properties": {
    "file_path": {
      "type": "string",
      "description": "Path to edit, resolved by the filesystem backend."
    },
    "old_string": {
      "type": "string",
      "description": "Literal text to replace. Must match exactly."
    },
    "new_string": {
      "type": "string",
      "description": "Literal replacement text. Use an empty string to delete the match."
    },
    "replace_all": {
      "type": "boolean",
      "description": "Replace all matches. Defaults to false; when false, old_string must appear exactly once."
    }
  },
  "required": [
    "file_path",
    "old_string",
    "new_string"
  ]
}

Source: packages/fs/tool-fs/src/index.ts

read

Read a UTF-8 text file and return line-numbered content.

{
  "type": "object",
  "properties": {
    "file_path": {
      "type": "string",
      "description": "Path to read, resolved by the filesystem backend."
    },
    "offset": {
      "type": "number",
      "description": "1-based first line to return. Defaults to 1."
    },
    "limit": {
      "type": "number",
      "description": "Maximum number of lines to return. Defaults to 2000."
    }
  },
  "required": [
    "file_path"
  ]
}

Source: packages/fs/tool-fs/src/index.ts

write

Create or fully replace a UTF-8 text file.

{
  "type": "object",
  "properties": {
    "file_path": {
      "type": "string",
      "description": "Path to write, resolved by the filesystem backend."
    },
    "content": {
      "type": "string",
      "description": "Full UTF-8 text content to write."
    }
  },
  "required": [
    "file_path",
    "content"
  ]
}

Source: packages/fs/tool-fs/src/index.ts

The read-before-write/edit policy is added by @deepseek-ai/dsh-fs-policy (an fs/* event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The tool schemas above are identical with or without the policy plugin.

@deepseek-ai/dsh-tool-subagent

subagent

Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation.

{
  "type": "object",
  "properties": {
    "description": {
      "type": "string",
      "description": "A short (3-5 word) description of the delegated task, for display."
    },
    "prompt": {
      "type": "string",
      "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."
    }
  },
  "required": [
    "description",
    "prompt"
  ]
}

Source: packages/subagent/tool-subagent/src/index.ts

The registered tool name is the load-time toolName config (default subagent); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees subagent_fork (bound to the fork backend) with an identical schema — see examples/coding-agent/cordis.yml and examples/acp-agent/cordis.yml.

@deepseek-ai/dsh-tool-todo

todo_write

Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Keep AT MOST ONE todo in_progress at a time; while work remains, exactly one active task should be in_progress. Mark a todo completed the moment it is done (do not batch completions), and allow no in_progress item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: pending (not started), in_progress (being worked on now), completed (finished).

{
  "type": "object",
  "properties": {
    "todos": {
      "type": "array",
      "description": "The COMPLETE task list, replacing any previous list.",
      "items": {
        "type": "object",
        "properties": {
          "content": {
            "type": "string",
            "description": "What the task is — a short imperative line."
          },
          "status": {
            "type": "string",
            "description": "pending (not started) | in_progress (now) | completed (done).",
            "enum": [
              "pending",
              "in_progress",
              "completed"
            ]
          }
        },
        "required": [
          "content",
          "status"
        ]
      }
    }
  },
  "required": [
    "todos"
  ]
}

Source: packages/todo/tool-todo/src/index.ts

todo_write is session-owned state; UIs render the latest todo/write event as a checklist or ACP plan.

@deepseek-ai/dsh-tool-web

web_fetch

Fetch the content of a specific HTTP(S) URL and return it decoded to text.

{
  "type": "object",
  "properties": {
    "url": {
      "type": "string",
      "description": "The HTTP(S) URL to fetch."
    }
  },
  "required": [
    "url"
  ]
}

Source: packages/web/tool-web/src/index.ts

Search the web for current information. Returns an optional summary answer and a list of source URLs.

{
  "type": "object",
  "properties": {
    "query": {
      "type": "string",
      "description": "The search query."
    }
  },
  "required": [
    "query"
  ]
}

Source: packages/web/tool-web/src/index.ts

web_search and web_fetch keep provider selection behind ctx.web so model-visible schemas stay stable across backend swaps.