Aari 97e2268d52
fix(sandbox): stop glob/grep/ls from surfacing disabled skills' files (#4096)
* fix(sandbox): stop glob/grep/ls from surfacing disabled skills' files

The disabled-skill gate checks the path a tool is given, but ls, glob and
grep all descend from it and return other paths, so a root above a disabled
skill still serves its files. glob and grep never called the gate at all;
ls called it only on its own argument and still leaked from a category root.

Add the entry gate to glob/grep, and filter what all three return through
the existing fail-closed _is_disabled_skill_path. The verdict is memoized per
skill because ExtensionsConfig.from_file() is uncached, so a per-match check
would turn a 100-match grep into 100 config reads.

* fix(sandbox): normalize trailing slashes in the disabled-skill path check

Review follow-ups on the disabled-skill gate:

- _extract_skill_name_from_skills_path returned "" instead of None for a
  category directory carrying a trailing slash. LocalSandbox.list_dir appends
  "/" to directories, so `ls /mnt/skills` yields "/mnt/skills/public/", giving
  parts ["public", ""]. The empty name skipped the `skill_name is None`
  short-circuit and fell through to a config read, landing on the right outcome
  only because unknown skills default to enabled. Drop empty segments so a
  trailing-slash category root takes the existing category-root branch.

- ls_tool resolved the runtime user id twice per call; hoist it into a local,
  matching glob_tool/grep_tool.

- Cover the CUSTOM path: custom/legacy skills resolve their enabled state
  through the per-user _skill_states.json, a different store from the public
  skills' extensions_config.json, and no automated test exercised it.
2026-07-12 11:32:41 +08:00
..