fix(themes): enable theme pack routes on Windows - #5943
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Removes a gate whose premise it verifies is dead on main, and hardens every hazard the removal newly reaches — typed, layered, and follow-ups filed. [DESIGN-REVIEWED] f86d1df |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All evidence checked. Writing the review now. First-Principles-Verdict: CONCERNS Every item is a derived consequence of one cause-level deletion, but the new ancestor-link screen guards 1 of 4 sites sharing its own stated root cause. What this change shipsIntent: let Windows users install and use theme packs, whose routes 501'd on a premise now false — a FIX.
Watch
[FIRST-PRINCIPLES-REVIEWED] f86d1df |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsLooking at CANDIDATE 1, I need to verify three things independently: (a) a concrete input that occurs in practice, (b) the call path, (c) an observable wrong outcome. The DELETE→ (a) — the "planted junction" input is a "could", not a practiced condition. The only API path that creates an installed theme directory is (c) — the wrong outcome requires assuming stdlib code I cannot open, and is version-dependent. Whether The candidate's own confidence is "medium," and it dies on both the input (a "could") and the mechanism (unopened, version-dependent stdlib). Dropped. Scanning the remainder of the diff (ancestor-link screen, sandbox-unavailable 503 mapping, executor offloading of the asset/overlay/topbar resolvers, No findings. [OPUS-REVIEWED] f86d1df Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
8d841b7 to
9db1a53
Compare
|
Disposition for the GPT BLOCKING finding on
Gates after the fix: 545 passed (up one, the new test), isort / flake8 / mypy clean. |
|
Dispositions for the First Principles BLOCK on
Also confirming the two "rides along, declared" items the review flagged rather than blocked, since both are called out in the PR body: the new probe test file, and the |
9db1a53 to
c8631e0
Compare
|
Disposition for the GPT BLOCKING finding on
Gates after the fix: 615 passed / 1 skipped, isort / flake8 / mypy clean. |
c8631e0 to
1543104
Compare
|
Disposition for the GPT BLOCKING finding on
Span ledger — hit 3, and why I am flagging rather than just patchingSame file, three rounds, each closing exactly the finding it was given and receiving one new sibling:
Rounds 1–2 shared one root cause, and round 2 answered them together with an invariant plus a branch table rather than a point fix. Round 3 is a genuinely different invariant (event-loop offload, not input validation), which is why it was not caught by that restructure. The honest read of the pattern: the 501 gate was hiding a class of Windows-specific hazards, not one bug, and each round surfaces the next member. That is worth a maintainer's eye — the code is better after each round, but "one more round will be the last" has now been wrong three times. I am flagging it here rather than silently continuing so the next reviewer can see the count. Also in
|
|
Dispositions for the First Principles CONCERNS on
Also confirming the items the review classified as rides-along, since all are now declared in the body: the probe test file, the |
|
Correction to my note above, now that the round of checks on Being explicit about what that does and does not mean, since my comment above asked for a maintainer call and a green rollup could be misread as the call having been answered:
Nothing else is outstanding: mergeable, single commit, |
…5963) The api_theme_detail handler still made seven inline exists()/is_dir() call expressions across six condition sites on its two target paths. Cheap on local disk, but on a UNC data home each stat is SMB-backed and can block for as long as the network takes -- and a blocked event loop stalls every other request the gateway serves. These sites became reachable on Windows when #5943 removed the theme-pack 501 gate and offloaded the asset routes' resolver; this pays down the debt it tracked for the detail route. All the stats now ride one discovery_executor() hop as a single synchronous helper -- every method branch consumes the pair as one logical check, so one hop replaces six -- and the DELETE-dir branch keeps its off-loop re-check under the per-slug install lock. The DELETE-file unlink gains missing_ok=True: the stat now rides an earlier hop, so a concurrent delete winning the race is answered as the idempotent ok it is rather than an escaping FileNotFoundError. A mutation-verified test spies on Path.exists/Path.is_dir for the handler's target paths across GET/PUT/DELETE and fails if any such stat runs on the loop thread. Closes #5963
…5963) (#6190) The api_theme_detail handler still made seven inline exists()/is_dir() call expressions across six condition sites on its two target paths. Cheap on local disk, but on a UNC data home each stat is SMB-backed and can block for as long as the network takes -- and a blocked event loop stalls every other request the gateway serves. These sites became reachable on Windows when #5943 removed the theme-pack 501 gate and offloaded the asset routes' resolver; this pays down the debt it tracked for the detail route. All the stats now ride one discovery_executor() hop as a single synchronous helper -- every method branch consumes the pair as one logical check, so one hop replaces six -- and the DELETE-dir branch keeps its off-loop re-check under the per-slug install lock. The DELETE-file unlink gains missing_ok=True: the stat now rides an earlier hop, so a concurrent delete winning the race is answered as the idempotent ok it is rather than an escaping FileNotFoundError. A mutation-verified test spies on Path.exists/Path.is_dir for the handler's target paths across GET/PUT/DELETE and fails if any such stat runs on the loop thread. Closes #5963
PR #5943 added first_linked_ancestor and wired it in the themes handler: on Windows, an ancestor symlink/junction whose target is a UNC share launders the lexical UNC screen, because the first filesystem call (even an lstat, which resolves every ancestor) becomes the outbound SMB probe. Replicate that Windows-gated, probe-precedes-nothing pattern at the four remaining is_unc_shape call sites: - acp/prompt_blocks.py: refuse the image candidate after the lexical suffix screen and before is_file(), skipping it like the UNC case. - hooks.py validate_file_path: refuse before realpath (the exact chain-resolving call the seam describes). The EXPANDED form is also screened lexically first, since a roaming-profile `~` can surface a UNC shape the raw text did not have and the ancestor walk itself lstat-s each component. - messaging/outbound_files.py _inspect: refuse before EVERY resolving call, including is_sensitive_path (whose candidate forms are built with realpath/resolve), with the same reply as the leaf symlink case; local_destination additionally screens the expanded form (still lexical, its documented contract). - memory.py _read_root_guard: walk the workspace's ancestors before the leaf reparse checks, with an audit line; the old "deliberately not rejected" comment is rewritten to keep only the POSIX half. No user-facing message names the offending ancestor. Windows-marked tests per site mirror the themes tests: ordering pinned by wiring the downstream probes to explode, mutation-checked by asserting the refusal disappears when the walk reports no link, and POSIX behavior pinned unchanged.
PR #5943 added first_linked_ancestor and wired it in the themes handler: on Windows, an ancestor symlink/junction whose target is a UNC share launders the lexical UNC screen, because the first filesystem call (even an lstat, which resolves every ancestor) becomes the outbound SMB probe. Replicate that Windows-gated, probe-precedes-nothing pattern at the four remaining is_unc_shape call sites, plus the second consumer of local_destination's lexical screen that review surfaced: - acp/prompt_blocks.py: refuse the image candidate after the lexical suffix screen and before is_file(), skipping it like the UNC case. - hooks.py validate_file_path: refuse before realpath (the exact chain-resolving call the seam describes). The EXPANDED form is also screened lexically first -- a roaming-profile `~` can surface a UNC shape the raw text did not have, and the ancestor walk itself lstat-s each component -- and a relative input is anchored with the lexical abspath so the walk covers the components realpath would resolve. - messaging/outbound_files.py _inspect: refuse before EVERY resolving call, including is_sensitive_path (whose candidate forms are built with realpath/resolve), with the same reply as the leaf symlink case; local_destination additionally screens the expanded form (still lexical, its documented contract). - image_artifacts.py _local_file: the registration-side consumer of the same lexical screen gains the same guard before its is_file() probe. - memory.py _read_root_guard: walk the workspace's ancestors before the leaf reparse checks, with an audit line; the old "deliberately not rejected" comment is rewritten to keep only the POSIX half, and the gate's INVARIANT docstring is qualified to match. No user-facing message names the offending ancestor. Windows-marked tests per site mirror the themes tests: ordering pinned by wiring the downstream probes (including is_sensitive_path) to explode, mutation-checked by asserting the refusal disappears when the walk reports no link, and POSIX behavior pinned unchanged.
PR #5943 added first_linked_ancestor and wired it in the themes handler: on Windows, an ancestor symlink/junction whose target is a UNC share launders the lexical UNC screen, because the first filesystem call (even an lstat, which resolves every ancestor) becomes the outbound SMB probe. Replicate that Windows-gated, probe-precedes-nothing pattern at the four remaining is_unc_shape call sites, plus the second consumer of local_destination's lexical screen that review surfaced: - acp/prompt_blocks.py: refuse the image candidate after the lexical suffix screen and before is_file(), skipping it like the UNC case. - hooks.py validate_file_path: refuse before realpath (the exact chain-resolving call the seam describes). The EXPANDED form is also screened lexically first -- a roaming-profile `~` can surface a UNC shape the raw text did not have, and the ancestor walk itself lstat-s each component -- and a relative input is anchored with the lexical abspath so the walk covers the components realpath would resolve. - messaging/outbound_files.py _inspect: refuse before EVERY resolving call, including is_sensitive_path (whose candidate forms are built with realpath/resolve), with the same reply as the leaf symlink case; local_destination additionally screens the expanded form (still lexical, its documented contract). - image_artifacts.py _local_file: the registration-side consumer of the same lexical screen gains the same guard before its is_file() probe. - memory.py _read_root_guard: walk the workspace's ancestors before the leaf reparse checks, with an audit line; the old "deliberately not rejected" comment is rewritten to keep only the POSIX half, and the gate's INVARIANT docstring is qualified to match. No user-facing message names the offending ancestor. At every guarded site the LEAF also gets the junction-aware is_link_or_junction check the walk deliberately excludes (its docstring requires the pairing): the first resolving call FOLLOWS a final-component link, so a leaf symlink/junction targeting a UNC share is the same probe. Windows-marked tests per site mirror the themes tests: ordering pinned by wiring the downstream probes (including is_sensitive_path) to explode, mutation-checked by asserting the refusal disappears when the walk reports no link, and POSIX behavior pinned unchanged.
PR #5943 added first_linked_ancestor and wired it in the themes handler: on Windows, an ancestor symlink/junction whose target is a UNC share launders the lexical UNC screen, because the first filesystem call (even an lstat, which resolves every ancestor) becomes the outbound SMB probe. Replicate that Windows-gated, probe-precedes-nothing pattern at the four remaining is_unc_shape call sites, plus the second consumer of local_destination's lexical screen that review surfaced: - acp/prompt_blocks.py: refuse the image candidate after the lexical suffix screen and before is_file(), skipping it like the UNC case. - hooks.py validate_file_path: refuse before realpath (the exact chain-resolving call the seam describes). The EXPANDED form is also screened lexically first -- a roaming-profile `~` can surface a UNC shape the raw text did not have, and the ancestor walk itself lstat-s each component. Anchoring (lexical abspath, re-screened) and the walk are scoped INSIDE the Windows branch so POSIX keeps its byte-identical resolve-through-every-symlink semantics, pinned by a new test; the messaging spec paragraph documenting this function is updated in the same commit. - messaging/outbound_files.py _inspect: refuse before EVERY resolving call, including is_sensitive_path (whose candidate forms are built with realpath/resolve), with the same reply as the leaf symlink case; local_destination additionally screens the expanded form (still lexical, its documented contract). - image_artifacts.py _local_file: the registration-side consumer of the same lexical screen gains the same guard before its is_file() probe. - memory.py _read_root_guard: walk the workspace's ancestors before the leaf reparse checks, with an audit line; the old "deliberately not rejected" comment is rewritten to keep only the POSIX half, and the gate's INVARIANT docstring is qualified to match. No user-facing message names the offending ancestor. At every guarded site the LEAF also gets the junction-aware is_link_or_junction check the walk deliberately excludes (its docstring requires the pairing): the first resolving call FOLLOWS a final-component link, so a leaf symlink/junction targeting a UNC share is the same probe. Windows-marked tests per site mirror the themes tests: ordering pinned by wiring the downstream probes (including is_sensitive_path) to explode, mutation-checked by asserting the refusal disappears when the walk reports no link, and POSIX behavior pinned unchanged.
PR #5943 added first_linked_ancestor and wired it in the themes handler: on Windows, an ancestor symlink/junction whose target is a UNC share launders the lexical UNC screen, because the first filesystem call (even an lstat, which resolves every ancestor) becomes the outbound SMB probe. Replicate that Windows-gated, probe-precedes-nothing pattern at the four remaining is_unc_shape call sites, plus the second consumer of local_destination's lexical screen that review surfaced: - acp/prompt_blocks.py: refuse the image candidate after the lexical suffix screen and before is_file(), skipping it like the UNC case. - hooks.py validate_file_path: refuse before realpath (the exact chain-resolving call the seam describes). The EXPANDED form is also screened lexically first -- a roaming-profile `~` can surface a UNC shape the raw text did not have, and the ancestor walk itself lstat-s each component. Anchoring (lexical abspath, re-screened) and the walk are scoped INSIDE the Windows branch so POSIX keeps its byte-identical resolve-through-every-symlink semantics, pinned by a new test; the messaging spec paragraph documenting this function is updated in the same commit. - messaging/outbound_files.py _inspect: refuse before EVERY resolving call, including is_sensitive_path (whose candidate forms are built with realpath/resolve), with the same reply as the leaf symlink case; local_destination additionally screens the expanded form (still lexical, its documented contract). - image_artifacts.py _local_file: the registration-side consumer of the same lexical screen gains the same guard before its is_file() probe. - memory.py _read_root_guard: walk the workspace's ancestors before the leaf reparse checks, with an audit line; the old "deliberately not rejected" comment is rewritten to keep only the POSIX half, and the gate's INVARIANT docstring is qualified to match. No user-facing message names the offending ancestor. At every guarded site the LEAF also gets the junction-aware is_link_or_junction check the walk deliberately excludes (its docstring requires the pairing): the first resolving call FOLLOWS a final-component link, so a leaf symlink/junction targeting a UNC share is the same probe. Windows-marked tests per site mirror the themes tests: ordering pinned by wiring the downstream probes (including is_sensitive_path) to explode, mutation-checked by asserting the refusal disappears when the walk reports no link, and POSIX behavior pinned unchanged.
PR #5943 added first_linked_ancestor and wired it in the themes handler: on Windows, an ancestor symlink/junction whose target is a UNC share launders the lexical UNC screen, because the first filesystem call (even an lstat, which resolves every ancestor) becomes the outbound SMB probe. Replicate that Windows-gated, probe-precedes-nothing pattern at the four remaining is_unc_shape call sites, plus the second consumer of local_destination's lexical screen that review surfaced: - acp/prompt_blocks.py: refuse the image candidate after the lexical suffix screen and before is_file(), skipping it like the UNC case. - hooks.py validate_file_path: refuse before realpath (the exact chain-resolving call the seam describes). The EXPANDED form is also screened lexically first -- a roaming-profile `~` can surface a UNC shape the raw text did not have, and the ancestor walk itself lstat-s each component. Anchoring (lexical abspath, re-screened) and the walk are scoped INSIDE the Windows branch so POSIX keeps its byte-identical resolve-through-every-symlink semantics, pinned by a new test; the messaging spec paragraph documenting this function is updated in the same commit. - messaging/outbound_files.py _inspect: refuse before EVERY resolving call, including is_sensitive_path (whose candidate forms are built with realpath/resolve), with the same reply as the leaf symlink case; local_destination additionally screens the expanded form (still lexical, its documented contract). - image_artifacts.py _local_file: the registration-side consumer of the same lexical screen gains the same guard before its is_file() probe. - memory.py _read_root_guard: walk the workspace's ancestors before the leaf reparse checks, with an audit line; the old "deliberately not rejected" comment is rewritten to keep only the POSIX half, and the gate's INVARIANT docstring is qualified to match. No user-facing message names the offending ancestor. At every guarded site the LEAF also gets the junction-aware is_link_or_junction check the walk deliberately excludes (its docstring requires the pairing): the first resolving call FOLLOWS a final-component link, so a leaf symlink/junction targeting a UNC share is the same probe. Windows-marked tests per site mirror the themes tests: ordering pinned by wiring the downstream probes (including is_sensitive_path) to explode, mutation-checked by asserting the refusal disappears when the walk reports no link, and POSIX behavior pinned unchanged.
PR #5943 added first_linked_ancestor and wired it in the themes handler: on Windows, an ancestor symlink/junction whose target is a UNC share launders the lexical UNC screen, because the first filesystem call (even an lstat, which resolves every ancestor) becomes the outbound SMB probe. Replicate that Windows-gated, probe-precedes-nothing pattern at the four remaining is_unc_shape call sites, plus the second consumer of local_destination's lexical screen that review surfaced: - acp/prompt_blocks.py: refuse the image candidate after the lexical suffix screen and before is_file(), skipping it like the UNC case. - hooks.py validate_file_path: refuse before realpath (the exact chain-resolving call the seam describes). The EXPANDED form is also screened lexically first -- a roaming-profile `~` can surface a UNC shape the raw text did not have, and the ancestor walk itself lstat-s each component. Anchoring (lexical abspath, re-screened) and the walk are scoped INSIDE the Windows branch so POSIX keeps its byte-identical resolve-through-every-symlink semantics, pinned by a new test; the messaging spec paragraph documenting this function is updated in the same commit. - messaging/outbound_files.py _inspect: refuse before EVERY resolving call, including is_sensitive_path (whose candidate forms are built with realpath/resolve), with the same reply as the leaf symlink case; local_destination additionally screens the expanded form (still lexical, its documented contract). - image_artifacts.py _local_file: the registration-side consumer of the same lexical screen gains the same guard before its is_file() probe. - memory.py _read_root_guard: walk the workspace's ancestors before the leaf reparse checks, with an audit line; the old "deliberately not rejected" comment is rewritten to keep only the POSIX half, and the gate's INVARIANT docstring is qualified to match. No user-facing message names the offending ancestor. At every guarded site the LEAF also gets the junction-aware is_link_or_junction check the walk deliberately excludes (its docstring requires the pairing): the first resolving call FOLLOWS a final-component link, so a leaf symlink/junction targeting a UNC share is the same probe. Windows-marked tests per site mirror the themes tests: ordering pinned by wiring the downstream probes (including is_sensitive_path) to explode, mutation-checked by asserting the refusal disappears when the walk reports no link, and POSIX behavior pinned unchanged.
PR #5943 added first_linked_ancestor and wired it in the themes handler: on Windows, an ancestor symlink/junction whose target is a UNC share launders the lexical UNC screen, because the first filesystem call (even an lstat, which resolves every ancestor) becomes the outbound SMB probe. Replicate that Windows-gated, probe-precedes-nothing pattern at the four remaining is_unc_shape call sites, plus the second consumer of local_destination's lexical screen that review surfaced: - acp/prompt_blocks.py: refuse the image candidate after the lexical suffix screen and before is_file(), skipping it like the UNC case. - hooks.py validate_file_path: refuse before realpath (the exact chain-resolving call the seam describes). The EXPANDED form is also screened lexically first -- a roaming-profile `~` can surface a UNC shape the raw text did not have, and the ancestor walk itself lstat-s each component. Anchoring (lexical abspath, re-screened) and the walk are scoped INSIDE the Windows branch so POSIX keeps its byte-identical resolve-through-every-symlink semantics, pinned by a new test; the messaging spec paragraph documenting this function is updated in the same commit. - messaging/outbound_files.py _inspect: refuse before EVERY resolving call, including is_sensitive_path (whose candidate forms are built with realpath/resolve), with the same reply as the leaf symlink case; local_destination additionally screens the expanded form (still lexical, its documented contract). - image_artifacts.py _local_file: the registration-side consumer of the same lexical screen gains the same guard before its is_file() probe. - memory.py _read_root_guard: walk the workspace's ancestors before the leaf reparse checks, with an audit line; the old "deliberately not rejected" comment is rewritten to keep only the POSIX half, and the gate's INVARIANT docstring is qualified to match. No user-facing message names the offending ancestor. At every guarded site the LEAF also gets the junction-aware is_link_or_junction check the walk deliberately excludes (its docstring requires the pairing): the first resolving call FOLLOWS a final-component link, so a leaf symlink/junction targeting a UNC share is the same probe. Windows-marked tests per site mirror the themes tests: ordering pinned by wiring the downstream probes (including is_sensitive_path) to explode, mutation-checked by asserting the refusal disappears when the walk reports no link, and POSIX behavior pinned unchanged.
PR #5943 added first_linked_ancestor and wired it in the themes handler: on Windows, an ancestor symlink/junction whose target is a UNC share launders the lexical UNC screen, because the first filesystem call (even an lstat, which resolves every ancestor) becomes the outbound SMB probe. Replicate that Windows-gated, probe-precedes-nothing pattern at the four remaining is_unc_shape call sites, plus the second consumer of local_destination's lexical screen that review surfaced: - acp/prompt_blocks.py: refuse the image candidate after the lexical suffix screen and before is_file(), skipping it like the UNC case. - hooks.py validate_file_path: refuse before realpath (the exact chain-resolving call the seam describes). The EXPANDED form is also screened lexically first -- a roaming-profile `~` can surface a UNC shape the raw text did not have, and the ancestor walk itself lstat-s each component. Anchoring (lexical abspath, re-screened) and the walk are scoped INSIDE the Windows branch so POSIX keeps its byte-identical resolve-through-every-symlink semantics, pinned by a new test; the messaging spec paragraph documenting this function is updated in the same commit. - messaging/outbound_files.py _inspect: refuse before EVERY resolving call, including is_sensitive_path (whose candidate forms are built with realpath/resolve), with the same reply as the leaf symlink case; local_destination additionally screens the expanded form (still lexical, its documented contract). - image_artifacts.py _local_file: the registration-side consumer of the same lexical screen gains the same guard before its is_file() probe. - memory.py _read_root_guard: walk the workspace's ancestors before the leaf reparse checks, with an audit line; the old "deliberately not rejected" comment is rewritten to keep only the POSIX half, and the gate's INVARIANT docstring is qualified to match. No user-facing message names the offending ancestor. At every guarded site the LEAF also gets the junction-aware is_link_or_junction check the walk deliberately excludes (its docstring requires the pairing): the first resolving call FOLLOWS a final-component link, so a leaf symlink/junction targeting a UNC share is the same probe. Windows-marked tests per site mirror the themes tests: ordering pinned by wiring the downstream probes (including is_sensitive_path) to explode, mutation-checked by asserting the refusal disappears when the walk reports no link, and POSIX behavior pinned unchanged.
PR #5943 added first_linked_ancestor and wired it in the themes handler: on Windows, an ancestor symlink/junction whose target is a UNC share launders the lexical UNC screen, because the first filesystem call (even an lstat, which resolves every ancestor) becomes the outbound SMB probe. Replicate that Windows-gated, probe-precedes-nothing pattern at the four remaining is_unc_shape call sites, plus the second consumer of local_destination's lexical screen that review surfaced: - acp/prompt_blocks.py: refuse the image candidate after the lexical suffix screen and before is_file(), skipping it like the UNC case. - hooks.py validate_file_path: refuse before realpath (the exact chain-resolving call the seam describes). The EXPANDED form is also screened lexically first -- a roaming-profile `~` can surface a UNC shape the raw text did not have, and the ancestor walk itself lstat-s each component. Anchoring (lexical abspath, re-screened) and the walk are scoped INSIDE the Windows branch so POSIX keeps its byte-identical resolve-through-every-symlink semantics, pinned by a new test; the messaging spec paragraph documenting this function is updated in the same commit. - messaging/outbound_files.py _inspect: refuse before EVERY resolving call, including is_sensitive_path (whose candidate forms are built with realpath/resolve), with the same reply as the leaf symlink case; local_destination additionally screens the expanded form (still lexical, its documented contract). - image_artifacts.py _local_file: the registration-side consumer of the same lexical screen gains the same guard before its is_file() probe. - memory.py _read_root_guard: walk the workspace's ancestors before the leaf reparse checks, with an audit line; the old "deliberately not rejected" comment is rewritten to keep only the POSIX half, and the gate's INVARIANT docstring is qualified to match. No user-facing message names the offending ancestor. At every guarded site the LEAF also gets the junction-aware is_link_or_junction check the walk deliberately excludes (its docstring requires the pairing): the first resolving call FOLLOWS a final-component link, so a leaf symlink/junction targeting a UNC share is the same probe. Windows-marked tests per site mirror the themes tests: ordering pinned by wiring the downstream probes (including is_sensitive_path) to explode, mutation-checked by asserting the refusal disappears when the walk reports no link, and POSIX behavior pinned unchanged.
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
PR #5943 added first_linked_ancestor and wired it in the themes handler: on Windows, an ancestor symlink/junction whose target is a UNC share launders the lexical UNC screen, because the first filesystem call (even an lstat, which resolves every ancestor) becomes the outbound SMB probe. Replicate that Windows-gated, probe-precedes-nothing pattern at the four remaining is_unc_shape call sites, plus the second consumer of local_destination's lexical screen that review surfaced: - acp/prompt_blocks.py: refuse the image candidate after the lexical suffix screen and before is_file(), skipping it like the UNC case. - hooks.py validate_file_path: refuse before realpath (the exact chain-resolving call the seam describes). The EXPANDED form is also screened lexically first -- a roaming-profile `~` can surface a UNC shape the raw text did not have, and the ancestor walk itself lstat-s each component. Anchoring (lexical abspath, re-screened) and the walk are scoped INSIDE the Windows branch so POSIX keeps its byte-identical resolve-through-every-symlink semantics, pinned by a new test; the messaging spec paragraph documenting this function is updated in the same commit. - messaging/outbound_files.py _inspect: refuse before EVERY resolving call, including is_sensitive_path (whose candidate forms are built with realpath/resolve), with the same reply as the leaf symlink case; local_destination additionally screens the expanded form (still lexical, its documented contract). - image_artifacts.py _local_file: the registration-side consumer of the same lexical screen gains the same guard before its is_file() probe. - memory.py _read_root_guard: walk the workspace's ancestors before the leaf reparse checks, with an audit line; the old "deliberately not rejected" comment is rewritten to keep only the POSIX half, and the gate's INVARIANT docstring is qualified to match. No user-facing message names the offending ancestor. At every guarded site the LEAF also gets the junction-aware is_link_or_junction check the walk deliberately excludes (its docstring requires the pairing): the first resolving call FOLLOWS a final-component link, so a leaf symlink/junction targeting a UNC share is the same probe. Windows-marked tests per site mirror the themes tests: ordering pinned by wiring the downstream probes (including is_sensitive_path) to explode, mutation-checked by asserting the refusal disappears when the walk reports no link, and POSIX behavior pinned unchanged.
PR #5943 added first_linked_ancestor and wired it in the themes handler: on Windows, an ancestor symlink/junction whose target is a UNC share launders the lexical UNC screen, because the first filesystem call (even an lstat, which resolves every ancestor) becomes the outbound SMB probe. Replicate that Windows-gated, probe-precedes-nothing pattern at the four remaining is_unc_shape call sites, plus the second consumer of local_destination's lexical screen that review surfaced: - acp/prompt_blocks.py: refuse the image candidate after the lexical suffix screen and before is_file(), skipping it like the UNC case. - hooks.py validate_file_path: refuse before realpath (the exact chain-resolving call the seam describes). The EXPANDED form is also screened lexically first -- a roaming-profile `~` can surface a UNC shape the raw text did not have, and the ancestor walk itself lstat-s each component. Anchoring (lexical abspath, re-screened) and the walk are scoped INSIDE the Windows branch so POSIX keeps its byte-identical resolve-through-every-symlink semantics, pinned by a new test; the messaging spec paragraph documenting this function is updated in the same commit. - messaging/outbound_files.py _inspect: refuse before EVERY resolving call, including is_sensitive_path (whose candidate forms are built with realpath/resolve), with the same reply as the leaf symlink case; local_destination additionally screens the expanded form (still lexical, its documented contract). - image_artifacts.py _local_file: the registration-side consumer of the same lexical screen gains the same guard before its is_file() probe. - memory.py _read_root_guard: walk the workspace's ancestors before the leaf reparse checks, with an audit line; the old "deliberately not rejected" comment is rewritten to keep only the POSIX half, and the gate's INVARIANT docstring is qualified to match. No user-facing message names the offending ancestor. At every guarded site the LEAF also gets the junction-aware is_link_or_junction check the walk deliberately excludes (its docstring requires the pairing): the first resolving call FOLLOWS a final-component link, so a leaf symlink/junction targeting a UNC share is the same probe. Windows-marked tests per site mirror the themes tests: ordering pinned by wiring the downstream probes (including is_sensitive_path) to explode, mutation-checked by asserting the refusal disappears when the walk reports no link, and POSIX behavior pinned unchanged.
PR #5943 added first_linked_ancestor and wired it in the themes handler: on Windows, an ancestor symlink/junction whose target is a UNC share launders the lexical UNC screen, because the first filesystem call (even an lstat, which resolves every ancestor) becomes the outbound SMB probe. Replicate that Windows-gated, probe-precedes-nothing pattern at the four remaining is_unc_shape call sites, plus the second consumer of local_destination's lexical screen that review surfaced: - acp/prompt_blocks.py: refuse the image candidate after the lexical suffix screen and before is_file(), skipping it like the UNC case. - hooks.py validate_file_path: refuse before realpath (the exact chain-resolving call the seam describes). The EXPANDED form is also screened lexically first -- a roaming-profile `~` can surface a UNC shape the raw text did not have, and the ancestor walk itself lstat-s each component. Anchoring (lexical abspath, re-screened) and the walk are scoped INSIDE the Windows branch so POSIX keeps its byte-identical resolve-through-every-symlink semantics, pinned by a new test; the messaging spec paragraph documenting this function is updated in the same commit. - messaging/outbound_files.py _inspect: refuse before EVERY resolving call, including is_sensitive_path (whose candidate forms are built with realpath/resolve), with the same reply as the leaf symlink case; local_destination additionally screens the expanded form (still lexical, its documented contract). - image_artifacts.py _local_file: the registration-side consumer of the same lexical screen gains the same guard before its is_file() probe. - memory.py _read_root_guard: walk the workspace's ancestors before the leaf reparse checks, with an audit line; the old "deliberately not rejected" comment is rewritten to keep only the POSIX half, and the gate's INVARIANT docstring is qualified to match. No user-facing message names the offending ancestor. At every guarded site the LEAF also gets the junction-aware is_link_or_junction check the walk deliberately excludes (its docstring requires the pairing): the first resolving call FOLLOWS a final-component link, so a leaf symlink/junction targeting a UNC share is the same probe. Windows-marked tests per site mirror the themes tests: ordering pinned by wiring the downstream probes (including is_sensitive_path) to explode, mutation-checked by asserting the refusal disappears when the walk reports no link, and POSIX behavior pinned unchanged.
PR #5943 added first_linked_ancestor and wired it in the themes handler: on Windows, an ancestor symlink/junction whose target is a UNC share launders the lexical UNC screen, because the first filesystem call (even an lstat, which resolves every ancestor) becomes the outbound SMB probe. Replicate that Windows-gated, probe-precedes-nothing pattern at the four remaining is_unc_shape call sites, plus the second consumer of local_destination's lexical screen that review surfaced: - acp/prompt_blocks.py: refuse the image candidate after the lexical suffix screen and before is_file(), skipping it like the UNC case. - hooks.py validate_file_path: refuse before realpath (the exact chain-resolving call the seam describes). The EXPANDED form is also screened lexically first -- a roaming-profile `~` can surface a UNC shape the raw text did not have, and the ancestor walk itself lstat-s each component. Anchoring (lexical abspath, re-screened) and the walk are scoped INSIDE the Windows branch so POSIX keeps its byte-identical resolve-through-every-symlink semantics, pinned by a new test; the messaging spec paragraph documenting this function is updated in the same commit. - messaging/outbound_files.py _inspect: refuse before EVERY resolving call, including is_sensitive_path (whose candidate forms are built with realpath/resolve), with the same reply as the leaf symlink case; local_destination additionally screens the expanded form (still lexical, its documented contract). - image_artifacts.py _local_file: the registration-side consumer of the same lexical screen gains the same guard before its is_file() probe. - memory.py _read_root_guard: walk the workspace's ancestors before the leaf reparse checks, with an audit line; the old "deliberately not rejected" comment is rewritten to keep only the POSIX half, and the gate's INVARIANT docstring is qualified to match. No user-facing message names the offending ancestor. At every guarded site the LEAF also gets the junction-aware is_link_or_junction check the walk deliberately excludes (its docstring requires the pairing): the first resolving call FOLLOWS a final-component link, so a leaf symlink/junction targeting a UNC share is the same probe. Windows-marked tests per site mirror the themes tests: ordering pinned by wiring the downstream probes (including is_sensitive_path) to explode, mutation-checked by asserting the refusal disappears when the walk reports no link, and POSIX behavior pinned unchanged. Co-authored-by: Zezhen Xu <zezhexu@dev-dsk-zezhexu-2b-15d11a49.us-west-2.amazon.com>
Problem / Motivation
Theme-pack install, detail, assets, overlays, topbars and removal all returned HTTP 501 "theme packs are not yet supported on Windows". The gate existed because every one of those routes reads through the
O_NOFOLLOW+ fd-real-path containment chokepoint (safe_read_file_bytes_nolink), andhooks._fd_real_pathhad no Windows implementation — it returnedNonethere, so the chokepoint fail-closed on every read. Rather than fail opaquely, the routes were gated honestly.That premise no longer holds.
_fd_real_pathgained a complete Windows branch onmainindependently of this work (hooks.py:1826:ctypes.WinDLL("kernel32")+msvcrt.get_osfhandle→GetFinalPathNameByHandleW, with\\?\UNC\/\\?\prefix normalization and fail-closedreturn None). The gate has therefore been blocking a capability that works, and the comment above it asserted something aboutmainthat had become false.Why it matters
Windows is a first-class platform, and theme packs are one of the few user-facing surfaces still refused outright there. Every Windows user hits a 501 on install and on every pack-backed asset route. The stale comment compounded it: anyone reading
themes.pyto decide whether the gate was still needed was told the Windows implementation did not exist.What changed (motivation → approach → change)
Symptom: 501 on all pack routes on Windows. Root cause: a gate whose stated justification is no longer true. Change: remove
_THEMES_WIN_UNSUPPORTED/_win_unsupported_response, and exercise the real containment path on Windows.The gate was hiding a CLASS of Windows-specific hazards, not one bug. An earlier revision of this description said "one failure", which undersold the surface a reviewer has to check — corrected here. Six consequence items ship alongside the deletion, and all six are reachable only because the gate is gone:
githubsource clones through the sandbox chokepoint, which fail-closes wherever no OS sandbox backend exists — Windows, and every GitHub runner._clone_githubcatches the typedSandboxUnavailableErrorand answers 503 withtheme_install_sandbox_unavailable. 503 because this is a host capability gap: the request was well-formed and no retry or different body carries the remedy. No unsandboxed retry — the URL is user-influenced andgit cloneexecutes remote content, which is why the spawn is chokepointed at all. Local sources spawn nothing and stay installable, so the capability this change enables is not re-gated behind a new status.\\attacker\sharemakes Windows open an SMB connection and authenticate, handing the gateway's credentials to a host the caller chose._resolve_local_sourcenow screens the raw and expanded text lexically viahooks.is_unc_shape+unc_probe_allowed— the same chokepointacp/prompt_blocks.py:171applies to attachment paths — beforeis_dir(). Not a blanket ban:unc_probe_allowedkeeps the one legitimate case working, a roaming profile whose home is itself a UNC share.Path.is_symlink()returns False for a Windows junction, so a junction root passed the old check and was resolved through. Now usesplatform_compat.is_link_or_junction.os.path.islinkis False for a junction andos.walkreports one as an ordinary directory, so a pack carrying a junction back to its own root recursed until a path-lengthOSErrorescaped as a 500._resolve_theme_assetdoesresolve()/is_file(), which are SMB-backed on a UNC data home; the three routes already offloaded their read todiscovery_executor()but called the resolve inline. Known incomplete — see the note below.is_link_or_junctiontests only the final component. A path sitting beneath an ancestor link whose TARGET is\\attacker\sharewas caught by neither, and resolving that chain is what opens the attacker's SMB connection. Newplatform_compat.first_linked_ancestor, called from_resolve_local_source. Two things about it are load-bearing and I got both wrong on the first attempt — see the review disposition comments:is_link_or_junctionis anlstat, and while anlstatdoes not follow the final component it still resolves every ancestor — so a leaf-first order makes the leaf probe itself traverse the junction and open the connection the screen exists to prevent. Pinned bytest_the_walk_runs_BEFORE_the_leaf_probe, which wires the leaf predicate to raise so an order regression explodes rather than passing.IS_WINDOWS-gated. Ungated it refuses legitimate POSIX paths: on macOS/tmpand/varare symlinks to/private/*, so any install from a temp dir would 400 on a platform this PR does not otherwise touch. The gate is not merely damage control — on POSIX the probe is not the harm, and the real guard isis_sensitive_pathapplied to the resolved path further down, so the walk buys nothing there. Pinned bytest_a_posix_path_beneath_a_linked_ancestor_is_still_accepted.lstatruns only after every ancestor above it is known clean and the probe never traverses one. The refusal reuses the leaf case's exact message: which ancestor is linked is filesystem layout, and the caller supplied a path to guess at it.The five screens in
_resolve_local_sourcenow run in strictly increasing order of filesystem exposure — lexical UNC (touches nothing) → ancestor walk (root-firstlstats) → leaf link check (onelstat) →is_dir()(resolves the chain) →resolve()+is_sensitive_path. That ordering is the invariant to preserve when editing this function; the round-5 bug was a screen in the right place doing the right thing one line too late.The symlink capability probe keeps the fail-soft contract it already has on
main: any failure to create a symlink means "this host cannot", and the tests it guards skip. The probe runs at conftest import time, so a propagating error is not a loud signal on one test — it is a collection error that takes down the entire session, including every test that never touches a symlink.Known incomplete, both now tracked
Both reviews asked that these be committed follow-ups rather than standing maybes. Filed:
target.exists()/dir_target.is_dir()calls remain on the event loop inapi_theme_detail(themes.py:635–715). They are SMB-backed on a UNC data home and the 501 gate covered them too, so they are newly reachable by this PR's own argument. Item 5 exists because it was a GPT blocking finding here; the siblings are the same fix and belong together, which is why they are one issue rather than a widening of this PR into a sixth round.is_unc_shapesites. A lexical screen followed by something that resolves the chain. Raised by First Principles; I checked all four production sites and the claim does not generalise evenly, so the issue records them individually:acp/prompt_blocks.py:171(is_file()) andhooks.py:1673(realpath— not named in the review) are clear matches;memory.py:592already checks the workspace leaf and reasons about traversal, so it is half-defended;messaging/outbound_files.py:373is followed by purely lexical checks and needs tracing before it is called a match. Three unrelated subsystems, so not in this PR.Tests
test/test_symlink_capability_probe.py(new) pins the fail-soft probe contract, including the Windows privilege shape — which carries an unrelated errno alongside winerror 1314 and so has to survive on theOSErrortype alone.test_a_junction_subdirectory_is_refused_like_a_symlink— proven red-before with the import intact and only the call reverted (DID NOT RAISE ValueError); stashing the whole file instead fails on a missing monkeypatch target and looks like proof without being it.test_a_local_path_beneath_a_linked_ancestor_is_refused— the item 6 vector. Proven red-before by disabling only the guard's call line while still evaluatingfirst_linked_ancestor(p), so a dropped import cannot masquerade as the proof; with the guard off the function returns the path resolved through the link, which is the vector itself rather than a generic assertion miss.test_it_reports_the_OUTERMOST_link_when_several_are_nested— pins the root-first order directly, since that ordering is the safety property and a correct-looking return value can still have been reached by an unsafe probe.test_an_unlinked_local_path_is_still_acceptedandtest_the_error_does_not_disclose_which_ancestor_was_linked— the two ways this fix could go wrong: refusing ordinary nesting, and leaking filesystem layout in the refusal.test_a_unc_source_is_refused_without_touching_the_filesystem— wires bothPath.is_dirandis_link_or_junctionto raise, so the assertion is that no filesystem call happens, not merely that the path is refused.test_a_roaming_profile_unc_source_gets_past_the_shape_screen— the exact inverse, guarding against over-blocking the legitimate roaming-profile case.test_a_junction_root_is_refused— the root-junction branch.test_dashboard_themes_coverage.py/test_theme_install.py— 501 assertions replaced with coverage of the real Windows path and the 503 translation; Windows-skip scaffolding dropped.error-code-baseline.jsonregenerated:missing_code1359 → 1358 (the removed 501 carried no machine-readable code). The regeneration also corrects_compliant1167 → 1189; only the three totals lines changed and the per-file map is byte-identical, so that is pre-existing staleness, not an effect of this diff.Manual verification
N/A from this host — every behaviour this change enables is win32-only and this is a Linux machine, which is also why #3293 could not self-verify. The diff therefore leans on the capability probe and typed refusals rather than platform-conditional skips. The 503 translation and the UNC/junction screens are reachable on Linux CI (no sandbox backend on runners; both predicates are lexical or stat-based), so those are covered by the suite rather than by inspection.
Screenshots / video
Why no screenshot: backend handler + tests + docs only. No dashboard component, layout, or string renders differently; the user-visible change is an HTTP status on Windows, which has no pixel form on this platform.
Declined here, with reason
The Design review suggests replacing the
err == _THEME_GIT_SANDBOX_UNAVAILABLEstring-identity check (compared in both_do_installandapi_themes_install) with a typed status returned from_clone_github, so rewording the user-facing text cannot silently degrade 503+codeback to a bare 400. That is a fair fragility and it is advisory, not blocking. Not taken here: the degradation it guards against is already pinned by the 503 translation tests, so the refactor buys robustness against a future edit rather than fixing present behaviour — and this PR is five review rounds deep on a security seam. Worth doing on its own.Related Issues
Closes #311
Supersedes #3293
Follow-ups: #5962, #5963