Files
LiJian 95dfa1d389 🐛 fix(device): probe the sandbox helper this app ships, not the one the backend guesses (#18176)
* 🐛 fix(device): probe the helper this app ships, not the one the backend guesses

Reported from an installed canary build: the Local Sandbox row said

  Sandbox Runtime dependencies are unavailable: srt-win.exe not found.
  Looked in: …\resources\app.asar\vendor\srt-win\x64\srt-win.exe

while `resources/sandbox-runtime/vendor/srt-win/x64/srt-win.exe` was sitting
right there. The Set-up button was equally useless, because provisioning was
never the thing that was missing.

`probeSandboxCapability` went through `SandboxManager.checkDependencies()`,
which resolves the helper via the backend's own `getSrtWinPath()` — a path
relative to the backend's package directory. Bundling that package into the
main process makes the computed path land inside `app.asar`, where no file can
exist. The launch path was already immune (it passes `windows.srtWin.path`);
only the probe still asked the wrong question, so a correctly installed app
reported a missing binary and offered a button that could not change the answer.

Windows now runs the checks directly against the resolved path, mirroring the
backend's own sequence — binary present, sandbox user provisioned with a
readable credential, WFP filters installed — and stages the helper first, so
the path it validates is the one the launch will use. `cannot-read` on filter
enumeration stays a non-failure: it is admin-gated, and the behavioural egress
check at `initialize()` is what fails closed.

macOS and Linux keep the backend's own check. Neither resolves anything
relative to the app, so neither was affected.

Verified against the installed build's own layout: the probe that reported
"not found" now returns available.

Two follow-ons:

- ship the backend's package.json beside the binaries, so the staging directory
  stays keyed on the real version instead of falling back to the binary's size —
  the fallback worked, but defeated per-version isolation in exactly the case it
  exists for, an installed app being updated;
- make the `createSrtConfig` assertion platform-aware, since the Windows helper
  override is absent everywhere else and an exact-match expectation would fail
  on one platform or the other.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* 🐛 fix(device): make one function decide the sandbox helper path

Review follow-up on both findings.

**Probe and launch could disagree.** The probe fell back to the unstaged source
when staging failed, while `createSrtConfig` fell back to *no override at all* —
letting the backend resolve its own package-relative path, which is inside
`app.asar` once bundled. A host where staging cannot happen (ProgramData locked
down, disk full) was therefore advertised as available and failed on the first
command: exactly the "claims a guarantee it never applied" outcome this feature
exists to avoid.

`resolveEffectiveSrtWin` is now the single place that resolves and stages, and
the probe, the launch, and setup all call it. They cannot diverge by
construction. It falls back to the unstaged source rather than giving up,
because a per-machine install already sits somewhere the sandbox account can
read — staging is an accommodation for per-user installs, not a precondition.

**The probe had no test.** The previous commit changed `probeSandboxCapability`
but only asserted on `createSrtConfig`, which already used the staging helper —
so putting the probe back on `SandboxManager.checkDependencies()` would have
gone unnoticed, and AGENTS.md asks for a regression test that fails before the
fix. Added one covering the Windows branch directly: it pins that the backend
check is *not* consulted, that the resolved path is what gets validated, that an
unprovisioned account stays distinguishable from a missing binary (only one of
those is what the Set-up button fixes), that `cannot-read` filter enumeration is
not a failure, and that other platforms keep the backend check. Five of the eight
fail against the pre-fix implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* 🐛 fix(tool-runtime): stop runCommand swallowing the reason it failed

Every sandboxed command came back as
`[UNKNOWN_EXEC_ERROR] Tool execution failed`, with the actual reason discarded.

`normalizeResult` put the daemon's `error` inside `result.result.error`, but
`ComputerRuntime.errorOutput` reads the ServiceResult's own `error`, then
`state.stderr`, then `state.error`. A command that fails *before spawning* fills
none of those — there is no process, so no stderr and no exit code — so the
chain ran to its last-resort fallback every time.

That is the exact shape of every Local Sandbox refusal ("requires a working
directory", "unavailable on this device"), which is how a precise, actionable
message turned into a generic one that told the user nothing and gave the model
nothing to correct. The same gap was already fixed for editLocalFile / grep /
glob; runCommand was missed.

Surfaces `raw.error` at the top level like those tools do, and pins both halves:
a pre-spawn failure keeps its reason, and a successful run still reports whether
it was actually sandboxed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 13:01:21 +08:00
..
…
…