fix(sandbox): delegate Windows Kiro spawns internally (#5620) - #5653
Conversation
Co-authored-by: Bolin_Chen <17506219+bobbyfine@users.noreply.github.com> (cherry picked from commit c7a6828)
UX Review (Fable 5) — ✅ PASSUX-level review of UX-Verdict: PASS Windows users get first chat with zero config; the only user-facing surfaces are docs and log copy, and both read clearly. Suggestions
[UX-REVIEWED] 20fab83 |
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: |
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS A faithful, well-audited port: the nine-file product diff replays merged #5620 byte-identically, and every hand-resolved deviation is disclosed, justified, and test-verified. Suggestions
[DESIGN-REVIEWED] 20fab83 |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of Both data files and the surrounding repo are read; the mechanical counts I ran are cited inline. Final review: First-Principles-Verdict: PASS A hand-resolved port of a merged, defect-driven fix, with every conflict decision named, every deviation a counted dependency, and every kept boundary intact. What this change shipsIntent: let a fresh Windows install chat without first granting
List capped at 10; the proactor-loop test hygiene, the Watch
[FIRST-PRINCIPLES-REVIEWED] 20fab83 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsI've now traced both candidates and the substantive production changes (the Candidate 1 (mode="off" Windows spawn skips audited delegation): The Candidate 2 ( Step 2: I checked whether the env-scrub widening introduces a regression. On standard/strict tiers the namespace/seatbelt launchers already stripped No findings. [OPUS-REVIEWED] 20fab83 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
Brings #5620 (source squash
c7a68282f) torelease/0.4.0so the insider line stops requiringsandbox_allow_unsandboxed_execbefore a Windows user's first chat.This is NOT a byte-identical cherry-pick, and it is not meant to read as one. A plain
git cherry-pick -xproduced 14 conflicts, 5 of them files that do not exist on this branch at all. Every conflict was resolved by hand and by judgment; every place the result differs from the source commit is listed below. Reviewers should read this as a hand-resolved port, not a replay.The product fix itself IS a faithful replay. The nine
src/files carrying the behaviour change —sandbox.py,acp/client.py,acp/runtime.py,pod/runtime.py,dashboard/handlers/{sessions,agents}.py,apps/backend.py,weixin/attachments.py,testing/fixtures.py— have diffs byte-identical to the source commit (every difference is a@@hunk offset). All the hand work is in the test / docs / CI periphery.What it does
Windows has no Kiro Crew OS sandbox backend, so
wrap_argvpreviously fail-closed and refused agent subprocesses unless the operator setagent.sandbox_allow_unsandboxed_exec. A spawn positively classified as the first-party Kiro backend now delegates isolation to kiro-cli's own internal sandbox, with the parent applying the full env scrub (scrub_agent_subprocess_env) because Windows has no POSIXenv -ulauncher. It stays audit-or-deny: an unwritable SEL event returnsNoneand the caller falls through to normal policy, which still fail-closes on Windows. Scripts, hooks and third-party ACP backends keep the old opt-in requirement.Why the conflicts, and how each was decided
Three CI/test mechanisms #5620 touches are wholly absent from this branch — not just the file, but the gate script, the
ci.ymlwiring and the tests. Their edits are bookkeeping for gates that cannot run here, so they were dropped:.github/subprocess-encoding-baseline.txtscripts/run_scoped_tests.py"python3"/"python"literals →sys.executablescripts/check_lockdown_before_publish.pytest/test_issue_radar_crew_fabric.pytest/test_loop_lock.pyThe code-side encoding pins that baseline records are kept — the ratchet ledger is meaningless without its ratchet, but the pins are real behaviour.
The other conflicts are the 3-way merge presenting whole foreign test bodies as "incoming", because #5620's edits land inside content this branch lacks. Each was dropped after verifying the production symbols it exercises are absent here, so importing it would have shipped guaranteed-failing tests:
test/test_agent.py— fix(agent): make a stale spec model pin diagnosable and resettable #4911'sagent_spec_pathtraversal/symlink/ambiguity suite (~390 lines).agent_spec_path,_conflicting_spec_forandreset_agent_modelare all absent from this branch'ssrc/kiro_crew/agent.py. fix(sandbox): delegate Windows Kiro spawns internally #5620's own 10@requires_symlinksdecorators, itsconftestimport and its Windowsentry["args"][-len(args):]assertion all landed and are kept.test/test_cli_doctor.py—TestEffectiveModelSection+TestWhatsAppSection;_doctor_effective_model,_doctor_whatsappandproject_agent_filesare all absent from this branch'scli_doctor.py. fix(sandbox): delegate Windows Kiro spawns internally #5620's only other change to this file was therequires_symlinksimport, whose sole consumer was inside the dropped block, so keeping it would have been an unused import (flake8 F401). Net: no change to this file.test/test_script_hooks.py—TestLastErroris absent here. fix(sandbox): delegate Windows Kiro spawns internally #5620's other edit in this file (test_timeout→_script_command, removing the dependency on asleepbinary) landed and is kept;_script_commandalready exists on the branch.test/test_mcp_gateway_rewriter.py—test_overlay_lockdown_precedes_contentis absent here._spec_with_env, which it shares, already exists and is untouched.website/electron/test/auto-update.test.js— the whole externally-managed-marker suite and the install/check-phase suite are absent (readExternallyManaged,onInstallFailed,check-in-flight: zero hits on this branch, in the module as well as the test). fix(sandbox): delegate Windows Kiro spawns internally #5620's substantive edit here (a try/catch around a symlink assertion for Windows hosts withoutSeCreateSymbolicLinkPrivilege) sits inside the dropped block, and its other edit — adding a trailing newline at EOF — is already satisfied on this branch. Net: no change to this file.One conflict was a genuine semantic fork rather than absent content:
test/test_mcp_gateway_rewriter.pyasserts which path the rewriter locks down. Main assertsp.suffix == ".tmp"; this branch asserts.json, and.jsonis the correct answer here: atrewriter.py:1486-1494this branch doesatomic_write(target, ...)thenrestrict_to_owner(target), locking the FINAL name. The temp-first ordering that makes.tmptrue is not on this branch, so taking main's line would have madeassert overlay_specsfail. HEAD's assertion is kept, reflowed to one line because the source commit's own baseline hunk prunes this file from.github/black-baseline.txt(it graduates, so it must be fully black-clean).Two docs paragraphs needed trimming rather than wholesale acceptance, because the incoming text asserts things that are false on this branch:
docs/system-specs/modules/harness-parity.md— the H7 row is taken verbatim. It documents exactly the behaviour being shipped, both tests it cites (test_harness_parity.py::test_is_kiro_cli_is_positive,test_sandbox_argv.py::TestKiroInternalSandboxExclusion) exist here, and the KAS backend it references exists inacp/{runtime,client,types}.py.docs/system-specs/modules/security.md— the parent-scrub description is taken, three claims are not._PYTHON_ENV_PREFIXEShere isPYTHONPATH+PYTHONHOME, soPYTHONPYCACHEPREFIXis dropped from the enumeration;pycache_gc.prune_pycachedoes not exist on this branch, so that sentence is dropped; andgatewayd.pyhere pops a hand-listedPYTHONPATH/PYTHONHOMEpair (gatewayd.py:1305-1306) rather than the shared list, so HEAD's weaker and true sentence is kept over main's stronger and false one.docs/guides/windows-install.md— the updated "Core gateway / chat / dashboard" row is taken. The adjacent "Project skills" row in the same hunk is unchanged context on main (no+), not part of fix(sandbox): delegate Windows Kiro spawns internally #5620, and does not exist here, so it is not introduced.Deviations that ADD content not in the source commit
Four, each a dependency the source commit did not need because main already had it. Without these the port is broken, and two of them were caught by the gates rather than by reading:
import atexitinconftest.py— the incoming_drain_windows_proactor_finalizerscallsatexit.register. Main's conftest importedatexitbefore fix(sandbox): delegate Windows Kiro spawns internally #5620; this branch does not, so the function wouldNameError.conftest.py— only the definition was inside the conflict; the call did not land. It is placed before thehasattr(session.config, "workerinput")early return, matching main, because that return fires on exactly the xdist workers whose exit code the drain protects — placing it after would make it a no-op for the case it exists to fix. This is load-bearing for this commit: fix(sandbox): delegate Windows Kiro spawns internally #5620 adds anasyncio.runtotest_session_usage.py, which is the pattern that leaves ProactorEventLoop finalizers behind on Windows. Main's call-site comment claims the fix "ensure[s] a FRESH open event loop is set as current"; the implementation does no such thing (it installs an unraisablehook and forces GC), so that clause is not copied.src/kiro_crew/subprocess_utf8.py—flake8F821 andmypyboth flaggedUTF8_TEXTundefined atcli_server.py:1677. The module does not exist here, andapps/backend.py:45had also landed an import of it, which would have been an ImportError on app-backend startup. Brought over from main; the docstring's claim that the mapping is "enforced byscripts/check_subprocess_encoding.pyin CI" is corrected, since that gate is not on this branch.UTF8_TEXTimport incli_server.py— the cherry-pick brought the use without the import, because main's import predates fix(sandbox): delegate Windows Kiro spawns internally #5620.Deviation: two tests re-pinned to this branch's contract
PYTHONPYCACHEPREFIXis main's third_PYTHON_ENV_PREFIXESkey and is not on this branch, so two of #5620's tests asserted a scrub that does not happen here:test/test_session_usage.py::TestFetchWhoami::test_spawn_uses_full_agent_environment_scrubtest/test_acp_runtime.py::test_runtime_spawn_scrubs_sensitive_env_on_default_autoBoth now use
PYTHONPATHas the_PYTHON_ENV_PREFIXESrepresentative. That keeps each test's discriminating power intact — it covers one key per scrub source (_SENSITIVE_ENV_PREFIXES,_PYTHON_ENV_PREFIXES,_AGENT_DENIED_ENV_KEYS), which is precisely whatscrub_agent_subprocess_envunifies — andPYTHONPATHis a real discriminator because it is deliberately EXCLUDED from_SENSITIVE_ENV_PREFIXES, so only the extra-prefixes path removes it. Mutation-verified: withsessions.py:616reverted to a plainscrub_env()the assertion fails on thePYTHONPATHline, and passes again once restored. Adding the third key was rejected deliberately — on main it arrives paired with thepycache_gcTTL/size bounding this branch lacks, so it is a behaviour decision for a maintainer, not something to smuggle into a port. See the finding below.Tests
test_connections_mint.py::test_the_handlers_package_does_not_import_the_mint_engineand::test_row_tokens_do_not_repeat_across_a_gateway_restart, both reproduced identically on a cleanorigin/release/0.4.0worktree with this commit absent — pre-existing on the branch.test_sandbox_argv.py+test_harness_parity.py+test_spawn_audit.py→ 182 passed, and after the final reflowtest_mcp_gateway_rewriter.py+test_acp_runtime.py+test_session_usage.pyalongside them → 536 passed.tsc -bclean; the 4 affected test files → 108 passed.check_black_formatting.py,isort --check-only,flake8,mypy src/kiro_crew/(1014 files),check_brand_name.py,check_harness_parity.py.Finding worth a follow-up (not fixed here)
This branch's Electron app exports
PYTHONPYCACHEPREFIXinto the gateway environment atwebsite/electron/main.js:992, and_PYTHON_ENV_PREFIXESdoes not include it. So on the packaged desktop app, an agent child onrelease/0.4.0inherits it and mirrors its whole stdlib/site-packages bytecode under the crew home, with nopycache_gcbound to prune it. That is a real pre-existing gap on this branch — exactly what main's third key closes — and it is the reason the two tests above had to be re-pinned rather than satisfied. Worth deciding on separately for the insider line.