Skip to content

fix: block unsafe root-like delete_file paths#2859

Merged
wwwillchen merged 2 commits intodyad-sh:mainfrom
wwwillchen:agent-2754-1772236131
Mar 4, 2026
Merged

fix: block unsafe root-like delete_file paths#2859
wwwillchen merged 2 commits intodyad-sh:mainfrom
wwwillchen:agent-2754-1772236131

Conversation

@wwwillchen
Copy link
Copy Markdown
Collaborator

@wwwillchen wwwillchen commented Mar 3, 2026

Summary

  • Reject blank and whitespace-only delete_file paths at schema level.
  • Add runtime guard to refuse project-root-equivalent delete targets (like ., ./, .\\, or normalized foo/..).
  • Add unit tests covering root-path rejection and normal file/directory delete behavior.

Test plan

  • npm test -- src/pro/main/ipc/handlers/local_agent/tools/delete_file.spec.ts
  • npm run fmt && npm run lint:fix && npm run ts

Made with Cursor


Open with Devin

@wwwillchen
Copy link
Copy Markdown
Collaborator Author

@BugBot run

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request focuses on improving the stability and user control within the local agent. It addresses a critical security concern by implementing comprehensive validation and runtime checks for the delete_file tool, preventing accidental or malicious deletion of sensitive project root paths. Concurrently, a significant new feature has been introduced: a step limit for the local agent. This mechanism automatically pauses the agent after 50 tool calls, offering users a crucial point of intervention for complex or potentially looping AI tasks, enhancing overall system reliability and user experience.

Highlights

  • Enhanced delete_file Tool Safety: Implemented robust schema validation to reject blank or whitespace-only paths and added a runtime guard to prevent the deletion of project-root-equivalent paths (e.g., ., ./, foo/..).
  • Introduced Local Agent Step Limit: A new feature that pauses the local agent's execution after 50 tool calls, providing a mechanism to prevent runaway processes and allow user intervention.
  • New UI Component for Step Limit: Developed DyadStepLimit React component and integrated it into the markdown parser to display the pause notification and a "Continue" button.
  • Expanded Test Coverage: Added unit tests for the delete_file tool's new safety checks and new end-to-end tests for the local agent's step limit functionality.
Changelog
  • e2e-tests/fixtures/engine/local-agent/step-limit.ts
    • Added a new fixture that generates 50 read_file tool calls to simulate reaching the local agent's step limit.
  • e2e-tests/local_agent_step_limit.spec.ts
    • Added a new E2E test case to verify that the local agent pauses after hitting the step limit and displays a "Continue" button.
  • e2e-tests/snapshots/local_agent_step_limit.spec.ts_local-agent---step-limit-pause-1.aria.yml
    • Added a new Playwright snapshot for the local-agent - step limit pause E2E test.
  • package-lock.json
    • Updated the project version in package-lock.json from 0.38.0-beta.1 to 0.39.0-beta.1.
  • rules/git-workflow.md
    • Added a new section explaining issues with removing GitHub labels containing special characters (like colons) via gh api due to security hooks.
  • src/components/chat/DyadMarkdownParser.tsx
    • Imported the DyadStepLimit component.
    • Added dyad-step-limit to DYAD_CUSTOM_TAGS.
    • Implemented rendering logic for the dyad-step-limit custom tag, passing steps, limit, and state properties.
  • src/components/chat/DyadStepLimit.tsx
    • Added a new React component DyadStepLimit which displays a "Paused after X tool calls" message, a "Continue" button, and handles the continuation logic.
  • src/pro/main/ipc/handlers/local_agent/local_agent_handler.ts
    • Defined MAX_TOOL_CALL_STEPS constant (set to 50).
    • Added totalStepsExecuted tracking across agent passes.
    • Updated stepCountIs in stopWhen condition to use MAX_TOOL_CALL_STEPS.
    • Implemented logic to append a <dyad-step-limit> custom tag to the response when totalStepsExecuted reaches MAX_TOOL_CALL_STEPS, pausing the agent.
  • src/pro/main/ipc/handlers/local_agent/tools/delete_file.spec.ts
    • Added new unit tests for the deleteFileTool covering:
    • Schema validation for empty and whitespace-only paths.
    • Rejection of project-root-equivalent paths (e.g., ., ./, foo/..).
    • Successful deletion of files and directories, including gitRemove calls.
    • buildXml returning undefined for blank paths.
  • src/pro/main/ipc/handlers/local_agent/tools/delete_file.ts
    • Modified deleteFileSchema to use z.string().refine to ensure the path is not empty or whitespace-only.
    • Added a runtime check in the execute method to normalize the path and throw an error if it resolves to the project root (., ./, .\, or foo/..).
    • Updated buildXml to check for path?.trim() instead of just path.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@wwwillchen wwwillchen added the needs-human:review-issue ai agent flagged an issue that requires human review label Mar 3, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a crucial security fix to prevent the delete_file tool from targeting project root-equivalent paths. The implementation is robust, adding validation at both the schema and runtime levels, and is well-supported by new unit tests. I've suggested a minor simplification to the path validation logic to enhance code clarity.

Additionally, a new step-limit feature has been added to pause the agent after a configurable number of tool calls, which is a great enhancement for controlling agent execution. The implementation looks solid, though I've identified an unused limit property being passed through components and have recommended its removal to keep the code clean. Overall, these are valuable improvements to the agent's safety and control.

Comment on lines +49 to +51
normalizedPath === "." ||
normalizedPath === "./" ||
normalizedPath === ""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The conditions normalizedPath === "./" and normalizedPath === "" are redundant because path.posix.normalize() converts an empty string or './' into '.'. Simplifying this security-related check to just normalizedPath === "." will make the code cleaner and easier to maintain without altering its behavior.

        normalizedPath === "."

}

export function DyadStepLimit({ node, children }: DyadStepLimitProps) {
const { steps = "50", limit: _limit = "50", state } = node.properties;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The limit property is destructured from node.properties and aliased to _limit, but it's never used within the component. This variable should be removed to improve code clarity. The limit prop is passed from DyadMarkdownParser.tsx and should be removed there as well.

Suggested change
const { steps = "50", limit: _limit = "50", state } = node.properties;
const { steps = "50", state } = node.properties;

node={{
properties: {
steps: attributes.steps,
limit: attributes.limit,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The limit prop is passed to the DyadStepLimit component but is not used there. It should be removed to avoid passing an unused prop and to keep the component interface clean.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 474de522ef

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

// Check if we hit the step limit and append a notice to the response
if (totalStepsExecuted >= MAX_TOOL_CALL_STEPS) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate step-limit notice on actual stop condition

This appends <dyad-step-limit> whenever totalStepsExecuted >= MAX_TOOL_CALL_STEPS, but totalStepsExecuted is cumulative across outer passes (including todo follow-up passes), so the notice can be added even when no pass was actually cut off by stepCountIs(...) and the agent already finished normally. In a multi-pass run (e.g., ~30 steps in pass 1 and ~20 in pass 2), users will still see “Paused after 50 tool calls” and an unnecessary Continue CTA, which misrepresents completion state and can trigger redundant follow-up turns.

Useful? React with 👍 / 👎.

if (
normalizedPath === "." ||
normalizedPath === "./" ||
normalizedPath === ""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead conditions after path.posix.normalize call

Low Severity

The conditions normalizedPath === "./" and normalizedPath === "" are unreachable dead code. path.posix.normalize never returns "./" (it strips trailing slashes, returning "." instead) and never returns "" (it converts empty strings to "."). Both inputs of concern already collapse to ".", which is correctly caught by the preceding normalizedPath === "." check. The dead branches create a false impression that these cases require separate handling.

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 10 files

Confidence score: 4/5

  • This PR looks safe to merge with minimal risk: the reported issue is moderate-low severity (4/10) and appears limited to pause-state messaging behavior rather than a hard failure.
  • In src/pro/main/ipc/handlers/local_agent/local_agent_handler.ts, using total steps across all passes for the limit check can show a "paused" message even when no individual pass actually hit MAX_TOOL_CALL_STEPS, which may confuse users and trigger incorrect status reporting.
  • Pay close attention to src/pro/main/ipc/handlers/local_agent/local_agent_handler.ts - ensure the limit condition is based on a per-pass hit (steps.length >= MAX_TOOL_CALL_STEPS) instead of cross-pass totals.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/pro/main/ipc/handlers/local_agent/local_agent_handler.ts">

<violation number="1" location="src/pro/main/ipc/handlers/local_agent/local_agent_handler.ts:1147">
P2: The step-limit check uses the total steps across all passes, which can trigger the "paused" message even when no pass hit the per-pass limit. Track a per-pass limit hit (e.g., `steps.length >= MAX_TOOL_CALL_STEPS`) and only emit the notice when the limit was actually reached.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

}

// Check if we hit the step limit and append a notice to the response
if (totalStepsExecuted >= MAX_TOOL_CALL_STEPS) {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The step-limit check uses the total steps across all passes, which can trigger the "paused" message even when no pass hit the per-pass limit. Track a per-pass limit hit (e.g., steps.length >= MAX_TOOL_CALL_STEPS) and only emit the notice when the limit was actually reached.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/pro/main/ipc/handlers/local_agent/local_agent_handler.ts, line 1147:

<comment>The step-limit check uses the total steps across all passes, which can trigger the "paused" message even when no pass hit the per-pass limit. Track a per-pass limit hit (e.g., `steps.length >= MAX_TOOL_CALL_STEPS`) and only emit the notice when the limit was actually reached.</comment>

<file context>
@@ -1133,6 +1143,24 @@ export async function handleLocalAgentStream(
     }
 
+    // Check if we hit the step limit and append a notice to the response
+    if (totalStepsExecuted >= MAX_TOOL_CALL_STEPS) {
+      logger.info(
+        `Chat ${req.chatId} hit step limit of ${MAX_TOOL_CALL_STEPS} steps`,
</file context>
Fix with Cubic

Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-agent review: 3 issues found (3 MEDIUM)

normalizedPath === "." ||
normalizedPath === "./" ||
normalizedPath === ""
) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM | security

Parent-traversal path .. bypasses the root-deletion guard

path.posix.normalize('..') returns '..', not '.', so a bare .. path passes all three conditions ('.', './', '') and falls through to safeJoin. While safeJoin will ultimately throw for .. (preventing actual deletion), the guard's purpose is to provide a clear "Refusing to delete project root" error. The current code gives a different, less descriptive error from safeJoin instead.

Also: path.posix.normalize never returns './' or '' — it normalizes both to '.'. These two branches are unreachable dead code.

💡 Suggestion: Simplify and strengthen the guard:

Suggested change
) {
if (
normalizedPath === "." ||
normalizedPath === ".." ||
normalizedPath.startsWith("../")
) {

fullResponse += stepLimitMessage;
await updateResponseInDb(placeholderMessageId, fullResponse);
sendResponseChunk(
event,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM | logic

Step limit detection uses cross-pass accumulation that can false-positive

totalStepsExecuted accumulates steps.length from every pass through the outer while loop (main pass + todo follow-up passes). stepCountIs(MAX_TOOL_CALL_STEPS) resets per-pass, so individual passes are capped at 50. But if pass 1 uses 30 steps (stopped for another reason, e.g. addIntegrationTool) and pass 2 uses 25 steps, totalStepsExecuted = 55 >= 50 triggers the notification even though neither pass was stopped by the step limit.

💡 Suggestion: Track whether any pass was actually stopped by the step limit rather than summing:

const hitStepLimit = steps.length >= MAX_TOOL_CALL_STEPS;

Then use if (hitStepLimit) for the notification.

onClick={handleContinue}
className="ml-auto hover:cursor-pointer"
>
{isLoading ? (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM | interaction

Continue button not disabled while a stream is already active

The button is only disabled by the local isLoading state. If the chat is already streaming (e.g., the user triggered another prompt), the button remains enabled. useStreamChat() likely silently no-ops via pendingStreamChatIds, but the user gets no feedback.

Other similar action cards (e.g., DyadAddIntegration) solve this by consuming isStreaming from useStreamChat().

💡 Suggestion: Destructure isStreaming from useStreamChat() and set disabled={isLoading || isStreaming}.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 3, 2026

🔍 Dyadbot Code Review Summary

Verdict: 🤔 NOT SURE - Potential issues

Reviewed by 3 independent agents: Correctness Expert, Code Health Expert, UX Wizard.

Issues Summary

Severity File Issue
🟡 MEDIUM src/pro/main/ipc/handlers/local_agent/tools/delete_file.ts:48 Parent-traversal path .. bypasses root-deletion guard
🟡 MEDIUM src/pro/main/ipc/handlers/local_agent/local_agent_handler.ts:1148 Step limit cross-pass accumulation can false-positive
🟡 MEDIUM src/components/chat/DyadStepLimit.tsx:57 Continue button not disabled while stream is active
🟢 Low Priority Notes (2 items)
  • Unused limit prop - src/components/chat/DyadStepLimit.tsx:26limit is destructured as _limit but never rendered. Either remove it or display it meaningfully (e.g., "Paused after {steps} of {limit} tool calls").
  • Continue prompt lacks context for AI - src/components/chat/DyadStepLimit.tsx:36 — The literal "Continue" prompt gives the AI no context that it was paused due to a step limit. Consider a more specific prompt like "Continue from where you left off. You were paused after reaching the tool call step limit.".
🚫 Dropped False Positives (4 items)
  • Card streaming state visual inconsistency — Dropped: The content is static text appended at end of stream; progressive rendering is not relevant here.
  • Accessibility: button lacks aria-label — Dropped: The button already has visible text content "Continue" which serves as the accessible name per WCAG. Consistent with other action cards.
  • Children typed as ReactNode but narrowed to string — Dropped: ReactNode is the standard type for children in React. The runtime check is defensive but appropriate.
  • Magic number 50 duplicated — Dropped: The value is already passed through the limit tag attribute from MAX_TOOL_CALL_STEPS. The "50" defaults in the component are just fallbacks.

Generated by Dyadbot multi-agent code review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 3, 2026

Greptile Summary

This PR hardens the delete_file agent tool against two classes of unsafe input: blank/whitespace-only paths (rejected at the Zod schema level) and project-root-equivalent paths such as ., ./, .\\, or traversals like foo/.. (rejected at runtime by normalizing the input with path.posix.normalize and comparing against "."). The fix is necessary because safeJoin — the existing path-safety utility — allows a joined path that resolves exactly to the project root (it only rejects paths that escape above the base, not paths that resolve to the base), meaning an AI-generated delete_file call with path foo/.. could previously wipe the entire project via rmdirSync(..., { recursive: true }).

Key points:

  • The core security fix is correct — all root-equivalent inputs ultimately normalize to "." and are blocked.
  • The guard contains two unreachable conditions (normalizedPath === "./" and normalizedPath === ""): path.posix.normalize never returns either of those values, so both checks are dead code. The single === "." condition is sufficient.
  • The new test suite covers schema validation and root-path rejection well, but the directory-deletion test is missing an assertion that gitRemove is invoked after rmdirSync.

Confidence Score: 4/5

  • Safe to merge — the security goal is achieved correctly despite the dead conditions in the guard.
  • The actual protection works as intended because every root-equivalent path normalizes to "." and is caught by the first condition. The two extra conditions are dead code (style/logic issue) but do not create a security gap. Test coverage is good but has a minor gap for gitRemove in the directory case.
  • Focus on delete_file.ts lines 48–56 where the dead conditions reside.

Important Files Changed

Filename Overview
src/pro/main/ipc/handlers/local_agent/tools/delete_file.ts Adds schema-level blank-path rejection and a runtime guard that normalizes the input path and refuses to proceed if it resolves to the project root. The security intent is correct, but the guard contains two dead conditions (=== "./" and === "") that can never be true after path.posix.normalize.
src/pro/main/ipc/handlers/local_agent/tools/delete_file.spec.ts New test file covering schema validation, root-path safety checks, and basic delete/rmdir behavior. The root-path test matrix is thorough; the directory-deletion test is missing an assertion that gitRemove is called after the directory is removed.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[delete_file called with path] --> B{Schema: path not blank?}
    B -- No --> C[Zod error: Path cannot be empty]
    B -- Yes --> D[Normalize with path.posix.normalize]
    D --> E{normalizedPath equals dot?}
    E -- Yes --> F[throw: Refusing to delete project root]
    E -- No --> G[safeJoin appPath + args.path]
    G --> H{Path escapes base dir?}
    H -- Yes --> I[throw: Unsafe path]
    H -- No --> J{fs.existsSync?}
    J -- No --> K[warn: file does not exist]
    J -- Yes --> L{isDirectory?}
    L -- Yes --> M[rmdirSync recursive]
    L -- No --> N[unlinkSync]
    M --> O[gitRemove]
    N --> O
    O --> P{supabaseProjectId and isServerFunction?}
    P -- Yes --> Q[deleteSupabaseFunction]
    P -- No --> R[return: Successfully deleted]
    Q --> R
    K --> R
Loading

Last reviewed commit: 5dcdb32

Prevent delete_file from accepting blank or project-root-equivalent paths so accidental root deletion cannot occur, and add regression coverage for dangerous inputs.

Also document a GitHub label-removal edge case involving colon characters in gh api DELETE paths.

Made-with: Cursor
@wwwillchen
Copy link
Copy Markdown
Collaborator Author

@BugBot run

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 4, 2026

🔍 Dyadbot Code Review Summary

Verdict: ✅ YES - Ready to merge

Reviewed by 3 independent agents: Correctness Expert, Code Health Expert, UX Wizard.

Issues Summary

Severity File Issue
🟡 MEDIUM src/pro/main/ipc/handlers/local_agent/tools/delete_file.ts:44-56 Guard and safeJoin use different normalization strategies
🟢 Low Priority Notes (1 item)
  • Missing "why" comment on guard - delete_file.ts:44 - A brief comment explaining what the guard catches that safeJoin does not would help future readers understand the layered defense model.
🚫 Dropped False Positives (5 items)
  • Silent failure when file doesn't exist - Dropped: Existing behavior not introduced by this PR
  • Internal error message phrasing - Dropped: This is an agent tool; the error message is functional and appropriate for the AI agent consumer
  • Consent preview shows raw path - Dropped: Minor concern; consent dialog paths are typically normal paths, not traversal attempts
  • fs mock spreading in tests - Dropped: Style nitpick, does not affect test correctness
  • Rules file placement - Dropped: The rules/ directory is for agent-specific rules, this is the correct location

Note: 3 issues already flagged by other reviewers were confirmed valid and deduplicated:

  • .. bypasses the root-deletion guard (github-actions[bot])
  • Dead conditions normalizedPath === "./" and === "" after path.posix.normalize (gemini-code-assist[bot], cursor[bot])

Generated by Dyadbot multi-agent code review

Comment on lines 44 to +56
execute: async (args, ctx: AgentContext) => {
const normalizedPath = path.posix.normalize(
args.path.replace(/\\/g, "/"),
);
if (
normalizedPath === "." ||
normalizedPath === "./" ||
normalizedPath === ""
) {
throw new Error(
`Refusing to delete project root for path: "${args.path}"`,
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM | correctness

Guard and safeJoin use different normalization strategies

The guard converts backslashes to / and uses path.posix.normalize, but safeJoin (called on line 58 with the original args.path) uses OS-native path.resolve. These two layers reason about paths using different normalization semantics, which makes the guard fragile and hard to audit.

A simpler, more robust approach would resolve the path once against ctx.appPath and compare:

Suggested change
execute: async (args, ctx: AgentContext) => {
const normalizedPath = path.posix.normalize(
args.path.replace(/\\/g, "/"),
);
if (
normalizedPath === "." ||
normalizedPath === "./" ||
normalizedPath === ""
) {
throw new Error(
`Refusing to delete project root for path: "${args.path}"`,
);
}
const resolvedPath = path.resolve(ctx.appPath, args.path);
if (resolvedPath === path.resolve(ctx.appPath)) {
throw new Error(
`Refusing to delete project root for path: "${args.path}"`,
);
}

This uses the same native resolution that safeJoin relies on, making the guard's semantics consistent with the actual deletion path.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 4, 2026

🎭 Playwright Test Results

❌ Some tests failed

OS Passed Failed Flaky Skipped
🍎 macOS 237 2 7 6

Summary: 237 passed, 2 failed, 7 flaky, 6 skipped

Failed Tests

🍎 macOS

  • select_component.spec.ts > select component next.js
    • Error: expect(locator).toBeVisible() failed
  • template-create-nextjs.spec.ts > create next.js app
    • Error: expect(locator).toBeVisible() failed

📋 Re-run Failing Tests (macOS)

Copy and paste to re-run all failing spec files locally:

npm run e2e \
  e2e-tests/select_component.spec.ts \
  e2e-tests/template-create-nextjs.spec.ts

⚠️ Flaky Tests

🍎 macOS

  • annotator.spec.ts > annotator - capture and submit screenshot (passed after 1 retry)
  • logs_server.spec.ts > system messages UI shows server logs with correct type (passed after 1 retry)
  • mention_files.spec.ts > mention file (passed after 1 retry)
  • refresh.spec.ts > refresh app (passed after 1 retry)
  • refresh.spec.ts > spa navigation inside iframe does not change iframe src attribute (passed after 1 retry)
  • setup_flow.spec.ts > Setup Flow > setup banner shows correct state when node.js is installed (passed after 1 retry)
  • undo.spec.ts > undo with native git (passed after 1 retry)

📊 View full report

Removed section about removing labels with special characters due to security restrictions on DELETE calls.
@wwwillchen
Copy link
Copy Markdown
Collaborator Author

@BugBot run

@wwwillchen wwwillchen merged commit 7b3f085 into dyad-sh:main Mar 4, 2026
7 of 8 checks passed
Comment on lines +48 to +56
if (
normalizedPath === "." ||
normalizedPath === "./" ||
normalizedPath === ""
) {
throw new Error(
`Refusing to delete project root for path: "${args.path}"`,
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead conditions after path.posix.normalize

path.posix.normalize never returns "./" or "" — it always collapses those to ".". This means the second and third conditions in the guard are unreachable dead code:

  • normalizedPath === "./"normalize("./") returns ".", not "./".
  • normalizedPath === ""normalize("") also returns ".", not "".

The first condition (=== ".") is the only one that actually fires, and it already covers both of those inputs. The tests pass because they're all caught by the "." branch before reaching the dead branches.

Suggested change
if (
normalizedPath === "." ||
normalizedPath === "./" ||
normalizedPath === ""
) {
throw new Error(
`Refusing to delete project root for path: "${args.path}"`,
);
}
if (normalizedPath === ".") {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/pro/main/ipc/handlers/local_agent/tools/delete_file.ts
Line: 48-56

Comment:
**Dead conditions after `path.posix.normalize`**

`path.posix.normalize` never returns `"./"` or `""` — it always collapses those to `"."`. This means the second and third conditions in the guard are unreachable dead code:

- `normalizedPath === "./"``normalize("./")` returns `"."`, not `"./"`.
- `normalizedPath === ""``normalize("")` also returns `"."`, not `""`.

The first condition (`=== "."`) is the only one that actually fires, and it already covers both of those inputs. The tests pass because they're all caught by the `"."` branch before reaching the dead branches.

```suggestion
      if (normalizedPath === ".") {
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 4, 2026

🔍 Dyadbot Code Review Summary

Verdict: ✅ YES - Ready to merge

Reviewed by 3 independent agents: Correctness Expert, Code Health Expert, UX Wizard.

Issues Summary

Severity File Issue
🟡 MEDIUM delete_file.spec.ts:85 Test suite missing .. (bare parent traversal) case
🟢 Low Priority Notes (2 items)
  • Missing WHY comment on root-path guard - delete_file.ts:44-56 - A brief comment explaining that path.posix.normalize reduces all root-equivalent forms to "." would help future maintainers understand the guard's logic.
  • Custom refine vs idiomatic Zod - delete_file.ts:20-27 - z.string().trim().min(1, "Path cannot be empty") is shorter and more consistent with Zod conventions than the .refine() approach.
🚫 Dropped False Positives (5 items)
  • Root-guard doesn't catch '..' traversal - Dropped: Already flagged by existing inline comment (github-actions[bot])
  • Guard and safeJoin use different normalization - Dropped: Already flagged by existing inline comment (github-actions[bot])
  • Dead conditions './' and '' after normalize - Dropped: Already flagged by existing inline comments (gemini-code-assist, cursor)
  • 'Successfully deleted' returned for non-existent files - Dropped: Pre-existing behavior, not introduced by this PR
  • Error message is developer-facing - Dropped: This is an LLM agent tool, not a user-facing UI; developer-level messaging is appropriate here

Generated by Dyadbot multi-agent code review

Comment on lines +85 to +91

expect(fs.existsSync).not.toHaveBeenCalled();
expect(fs.unlinkSync).not.toHaveBeenCalled();
expect(fs.rmdirSync).not.toHaveBeenCalled();
expect(gitRemove).not.toHaveBeenCalled();
},
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM | test-coverage

Test suite missing .. (bare parent traversal) case

The it.each array covers ".", "./", ".\\", "foo/..", and "foo\\.." but omits a bare "..". Since path.posix.normalize("..") returns ".." (not "."), this input actually bypasses the root-path guard in the implementation — a gap already flagged by another reviewer on the implementation side.

Adding ".." to this test list would make the gap visible in CI: the test would fail, documenting that the guard needs to handle parent-traversal paths too.

💡 Suggestion: Add ".." to the it.each array so the test serves as an executable specification of the guard's contract.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-human:review-issue ai agent flagged an issue that requires human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant