rules/local-agent-tools.md
Agent tool definitions live in src/pro/main/ipc/handlers/local_agent/tools/. Each tool has a ToolDefinition with optional flags.
modifiesState: true must be set on any tool that writes to disk or modifies external state (files, database, etc.). This flag controls whether the tool is available in read-only (ask) mode and plan-only mode — see buildAgentToolSet in tool_definitions.ts.write_file capability inside execute_sandbox_script), either mark the parent tool modifiesState: true or make modifiesState a context predicate that returns true whenever the writable host function is exposed. Otherwise read-only / plan-only filtering can still expose writes through the wrapper tool. Prompt descriptions, tool filtering, and runtime capability injection should all derive from the same turn-scoped flag so ask/plan mode can keep the read-only surface without advertising or exposing writes.handleLocalAgentStream handler that writes to the workspace (e.g., ensureDyadGitignored, injecting synthetic todo reminders) should be guarded with if (!readOnly && !planModeOnly) checks. Injecting instructions that reference state-changing tools into non-writable runs will confuse the model since those tools are filtered out.fs.promises (not sync fs methods) in any code running on the Electron main process (e.g., todo_persistence.ts) to avoid blocking the event loop.ctx.onXmlComplete(...) with escaped <dyad-output> content to surface warnings/errors inline. warningMessages creates toast warnings, and throwing turns the whole stream into a ChatErrorBox.ctx.onXmlComplete only updates the message content column and the UI; it does NOT make output visible to future agent turns. parseAiMessagesJson reads from aiMessagesJson whenever it's present and ignores content entirely. For post-loop output that the agent should see next turn (deploy results, step-limit notices), also push a trailing assistant message into accumulatedAiMessages BEFORE the aiMessagesJson write, e.g.: accumulatedAiMessages.push({ role: "assistant", content: [{ type: "text", text: xml }] }).requireMcpToolConsent resolves to a structured result, not a bare boolean. If npm run ts reports Argument of type 'boolean' is not assignable to parameter of type 'McpConsentResult', update mocks to return { approved: true/false }.execute_sql consent metadata or safety checks, audit both Agent mode (shouldAutoApproveAgentTool / executeSqlTool.getConsentMetadata) and Build mode auto-apply (chat_stream_handlers.ts with autoApproveChanges). A SQL safety rule only on the Agent tool path can still be bypassed by Build mode global auto-approve.DO/CALL), and executing wrappers such as EXPLAIN ANALYZE should require consent unless the wrapped statement can be proven safe.PREPARE can hide the statement body and top-level EXECUTE runs a previously prepared statement, so both should require consent unless the classifier can prove the executed statement is safe.handleLocalAgentStream retry behavior, do not only match transport errors like "terminated". Providers can emit structured stream errors such as { type: "error", error: { type: "server_error", ... } }, and those transient 5xx / rate-limit failures need explicit retry classification too.set_chat_summary is added to stopWhen, audit downstream pass gates that inspect the final step's toolCalls. A final metadata tool call should not suppress safety follow-up passes such as incomplete todo reminders.e2e-tests/snapshots/ baselines for old tool-description text after regenerating request snapshots. Some request baselines are extensionless files such as local_agent_explore_code.spec.ts_disabled, not just .txt snapshots.buildAgentToolSet filters out.testing/fake-llm-server, keep Anthropic local-agent fixture routing in sync with the OpenAI chat-completions route for synthetic continuation messages (incomplete todo(s), persisted unfinished todos, and stream retry prompts). If Anthropic routing misses those markers, multi-pass fixtures fall back to the canned file1.txt response mid-flow.SANDBOX_HOST_CALL_NAMES in src/ipc/utils/sandbox/capabilities.ts. MCP tool collection seeds collision detection from that list so MCP capabilities do not silently shadow built-ins when capability maps are merged.execute_sandbox_script is exempted from the wrapper-level app-blueprint gate (CAPABILITY_GATED_BLUEPRINT_TOOLS in tool_definitions.ts) because gating the whole tool would also block read-only scripts and MCP host calls; instead buildWriteFileCapability calls assertAppBlueprintApproved per write, reading ctx.enableAppBlueprint.assertSandboxWritePathAllowed (realpath containment), not just the lexical assertAllowedGuestPath. Reads already resolve symlinks via assertResolvedPathAllowed; a write path that skips realpath resolution can follow a symlinked directory or file out of the app.buildAgentToolSet and sandbox capability bridges live in tools/tool_invocation.ts — a cycle-free module (tool_definitions.ts imports every tool, so tools cannot import back from it). Use those helpers instead of copying the wrapper's blocks.ctx.sandboxWriteFileHostEnabled via shouldIncludeTool(writeFileTool, ...), and per-call re-checks use getToolConsent(writeFileTool) (which honors the tool's defaultConsent fallback). Do not read settings.agentToolConsents directly — a raw read silently diverges if the default consent changes. buildExecuteSandboxScriptDescription requires an explicit includeWriteFile for the same reason: only the caller knows the turn context..dyad/media attachment files, also prune attachments-manifest.json entries under the attachments-manifest:${appPath} lock. Read-time filtering hides broken entries but still leaves stale logical names that force unnecessary suffixes like notes-2.txt on future uploads..dyad/media files that may already exist (for example repeated @media: mentions), reuse an existing manifest entry for the same storedFileName before allocating a new logical name. Otherwise repeated references create noisy attachments:* aliases like image-2.png, image-3.png.AgentContext (in tools/types.ts), grep src/pro/main/ipc/handlers/local_agent/tools/*.spec.ts and update every mock context literal. The TS error appears as e.g. Property 'nitroEnabled' is missing in type ... but required in type 'AgentContext' and surfaces only via npm run ts — npm run lint does not catch it.