Skip to content

Release v0.51.558 — escape-target symlinks display-only (#4581)#4610

Merged
nesquena-hermes merged 4 commits into
masterfrom
stage-4581
Jun 21, 2026
Merged

Release v0.51.558 — escape-target symlinks display-only (#4581)#4610
nesquena-hermes merged 4 commits into
masterfrom
stage-4581

Conversation

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Release v0.51.558 — Release TQ (workspace tree shows escape-target symlinks, display-only)

Ships #4581 (claw-io) — dedup-winner over #4573 (same feature, stronger tests). Security-surface feature on the symlink-containment boundary (#3398/#3450/#3630).

Added

  • Workspace tree now shows symlinks whose target resolves outside the workspace as display-only rows (previously silently dropped). Non-navigable; the read/list gate still blocks traversal. The row does NOT disclose the resolved outside path or target metadata.

Fixed

  • A prompt dialog could open without its Cancel button after viewing an outside-symlink info dialog (shared-button display now restored).

Gate

  • Full suite: 9944 passed, 0 failed (+ 51 symlink containment/cycle/shape tests, + dialog regression test)
  • Codex + Opus: containment fully intact (gate functions untouched, traversal still blocked, cycle detection preserved, XSS escaped)
  • Info-leak hardening applied during review: outside rows emit no target, no target size, no target-derived dir state; external-link dialog uses a generic message (dropped {target} across all 13 locales).
  • Note: outside rows intentionally keep a constant is_dir:false as a render-hint (not target-derived, discloses nothing) for consistent frontend non-expandable rendering.

Closes the #4573 duplicate. Thanks @claw-io (and @rodboev for the parallel #4573).

…as display-only workspace rows

claw-io: symlinks whose target resolves outside the workspace root were silently dropped; now emitted
with target_outside_workspace=True (display-only — safe_resolve_ws/open_anchored_fd still block
navigation). Dedup-winner over #4573 (stronger tests). Co-authored-by: claw-io <claw-io@users.noreply.github.com>
…path/metadata

Codex+Opus gate (info-leak): the display-only escape-target rows still returned the resolved
outside path (target), target-derived is_dir, and target stat size; ui.js showed the outside path
in the open-dialog. Fix: emit ONLY name/path/type/target_outside_workspace/mtime for outside rows
(no target, no size, is_dir=False); dialog uses a generic 'points outside workspace' message (dropped
{target} from external_link_open_confirm across all 13 locales). Containment was already intact (both
gates confirmed); this closes the path-disclosure. Tests updated to assert the no-leak property.
…hideCancel confirm

Codex gate (SILENT): the new outside-symlink info dialog uses showConfirmDialog({hideCancel:true})
which sets the shared #appDialogCancel to display:none; showPromptDialog only reset textContent, so a
later rename/new-file prompt opened with no Cancel button. Fix: showPromptDialog now restores
cancelBtn.style.display='' first. + regression test.
@nesquena-hermes nesquena-hermes merged commit 1b194a3 into master Jun 21, 2026
11 checks passed
@nesquena-hermes nesquena-hermes deleted the stage-4581 branch June 21, 2026 09:28
@greptile-apps

greptile-apps Bot commented Jun 21, 2026

Copy link
Copy Markdown

Greptile Summary

This release PR ships the escape-target symlink display feature: workspace file-tree listings now emit symlinks whose resolved target falls outside the workspace as display-only rows (previously silently dropped), and fixes a dialog regression where the shared Cancel button could be hidden after viewing an external-symlink info dialog.

  • api/workspace.py: Instead of return-ing on an outside-workspace symlink, the listing now emits the entry with target_outside_workspace=True and deliberately omits target, real is_dir, and size to avoid disclosing target metadata; _is_blocked_system_path continues to fully suppress /etc, /usr, etc. rows.
  • static/ui.js: Adds isExternalLink classification; outside rows get a distinct icon, no drag handle, no rename/delete affordances, and a single-/double-click info dialog; showPromptDialog now unconditionally restores cancelBtn.style.display so a prior hideCancel call can't leave the button invisible.
  • Tests: 51 new symlink containment/shape/cycle tests plus a dialog regression test are included; all existing gate tests are preserved.

Confidence Score: 4/5

The containment gates are untouched and all traversal-blocking tests pass; the only gap is a misleading 'Double-click to rename' tooltip on display-only external-symlink rows.

The workspace security boundary (safe_resolve_ws / open_anchored_fd) is demonstrably unchanged, hardening is thorough (no target path, no size, no real is_dir emitted for outside rows), and the dialog cancel-button regression is cleanly fixed with a test. The one rough edge is the name-span tooltip branch: external symlinks fall through to the !isDirLike arm and inherit 'Double-click to rename' even though double-clicking opens an info dialog instead.

static/ui.js — the tooltip assignment at the nameEl block (lines 14402-14405) needs an isExternalLink guard.

Important Files Changed

Filename Overview
api/workspace.py External symlinks are now emitted with target_outside_workspace=True instead of being silently dropped; containment gates are unchanged, _is_blocked_system_path still fully filters system paths, and the entry intentionally omits target/real is_dir/size to prevent info-leak.
static/ui.js Adds isExternalLink classification, display-only row rendering, info-dialog on click, and the hideCancel/restore-cancel-button dialog fix; the name-span tooltip branch doesn't guard isExternalLink, leaving 'Double-click to rename' shown on rows that open an info dialog on double-click.
static/i18n.js Adds external_link_open_confirm to all 13 locale blocks; strings are English-only (consistent with other recently-added strings in the file).
static/icons.js Adds the external-link SVG path to LI_PATHS; the path data is standard feather-style and referenced correctly by the new renderer branch.
tests/test_workspace_symlink_containment.py Updates containment assertions to reflect display-only emission; read/traversal blocking assertions are preserved and still pass.
tests/test_symlink_cycle_detection.py Renames 'filtered' tests to 'display-only' and adds checks for target_outside_workspace=True and absence of target field; cycle-detection tests are unchanged.
tests/test_workspace_context_menu_and_rename.py Adds a regression test verifying showPromptDialog always resets cancelBtn.style.display after a prior hideCancel call.
tests/test_workspace_symlink_tree_shape.py Adds isExternalLink declaration, external-link icon, and drag-disable tests; TestSymlinkTooltip doesn't add a case for the external-link tooltip path.
tests/test_1707_workspace_filename_click.py Threads isExternalLink into the JS-runner harness alongside the existing isLk/isDirLike locals; change is mechanical and correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[list_dir encounters symlink entry] --> B{reachable & no cycle?}
    B -- No --> Z[return — drop entry]
    B -- Yes --> C[resolve link_target]
    C --> D{_is_blocked_system_path?}
    D -- Yes --> Z
    D -- No --> E{link_target inside workspace?}
    E -- Yes --> F[emit full entry\nname, path, type, target,\nis_dir, size, mtime_ns]
    E -- No --> G[emit display-only entry\nname, path, type,\nis_dir=False, mtime_ns\nNO target / size]
    F --> H[Frontend: normal symlink row\nlink icon, rename, delete]
    G --> I[Frontend: external-link icon\ndisplay-only, click opens info dialog]
    G --> J[safe_resolve_ws still blocks\nnavigation through link]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[list_dir encounters symlink entry] --> B{reachable & no cycle?}
    B -- No --> Z[return — drop entry]
    B -- Yes --> C[resolve link_target]
    C --> D{_is_blocked_system_path?}
    D -- Yes --> Z
    D -- No --> E{link_target inside workspace?}
    E -- Yes --> F[emit full entry\nname, path, type, target,\nis_dir, size, mtime_ns]
    E -- No --> G[emit display-only entry\nname, path, type,\nis_dir=False, mtime_ns\nNO target / size]
    F --> H[Frontend: normal symlink row\nlink icon, rename, delete]
    G --> I[Frontend: external-link icon\ndisplay-only, click opens info dialog]
    G --> J[safe_resolve_ws still blocks\nnavigation through link]
Loading

Comments Outside Diff (1)

  1. static/ui.js, line 14402-14405 (link)

    P2 The tooltip on external-symlink name spans will read "Double-click to rename" even though double-clicking an external symlink opens the info dialog (not a rename prompt). The condition falls into else if(!isDirLike) because isExternalLink is not excluded — !isDirLike is true for external links (isDirLike is forced false), but item.target is undefined (intentionally omitted), so the symlink branch is skipped too. The result is a misleading affordance hint on every display-only row.

Reviews (1): Last reviewed commit: "docs(release): stamp v0.51.558 — escape-..." | Re-trigger Greptile

@cutter-sh

cutter-sh Bot commented Jun 21, 2026

Copy link
Copy Markdown

🎬 Cutter preview — PR #4610

/
/ — New WebUI release v0.51.558 available with update prompt.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant