Tool type therefore lives in apps/monad/src/capabilities/tools/types.ts, not in the SDK
— tools do not go through @monad/sdk-atom. (The one SDK type a tool touches is
ProviderToolHint, for provider-native tool bindings such as Anthropic computer-use.)
Layout
assertPathWithinRoots, assertUrlAllowed, isBlockedIp,
ToolSecurityError) and the per-OS launchers live in @monad/sandbox
(packages/sandbox/src/security.ts, packages/sandbox/src/launchers/), so the standalone
msr CLI enforces the same policy the daemon does.
Infra shared by tools stays at the tools/ root; only real tool implementations live under
registry/. A tool that needs cohesive internal modules gets a same-named folder (email/,
mcp/) instead of a flat file.
The uniform module contract (registry/contract.ts)
EVERY tool module exposes the SAME entry — export const register: ToolModule<Deps> — a factory
that takes its dependency bag and returns the ready Tool[]. The shape is uniform; the deps
type is parameterized so each module declares exactly what it needs rather than sharing one
god-bag.
A module may keep an internal
createXxxTool builder (several are imported directly by tests);
register is the one canonical entry that assembly goes through.
Assembly
- Static + service → manifests in
registry/index.ts(staticModules,serviceModules).builtinToolsisbuildTools(staticModules, {}). The barrel uses namespace imports (notexport *) precisely because every module exports a symbol namedregister. - Agent-runtime → composed in
agent/execution.tswith live agent deps (model router, inbound-approval gate, context engine, the livegetToolsregistry view, hook runner, …). The order is preserved so the prompt-cache prefix stays stable across turns.
[]: email when no backend is configured,
agent_delegate when there are no delegatable Studio agents at boot. Reflexive tools
(delegate, tool_search, tool_call) read a getTools thunk so they see the live registry —
including hot-installed atom-pack/MCP tools — without rebinding.
Authoring rules
- Declare
scopesandhighRiskhonestly. Gate dangerous, irreversible, or trust-boundary-crossing ops behindneedsApproval(async; runs in the oversight gate). Absent gate + high-risk ⇒ denied (fail-closed). - Resource guards live in
run()bodies, not the schema:assertPathWithinRoots,assertUrlAllowed,isBlockedIp, path-traversal and egress checks. Never skip them on a path you believe is “trusted” — the argument is attacker-controllable. - Sandbox / credential wrapping is the daemon’s job, not the tool’s. Don’t reach for
process/fs primitives that bypass the injected
ToolContextconstraints (sandboxRoots,backends). - Tools are snapshotted by the agent at startup; a hot-installed tool registers into the live
registry but only reaches the model through the live
getToolsview — don’t assume a tool object captured at boot picks up mid-session changes. - Imports: the
Tooltype comes from@/capabilities/tools/types.ts; the only SDK import a tool needs isProviderToolHint. Otherwise@monad/loggeris allowed. Never import@monad/{monad,environment,client}.