fix(code-review-sage): run the review worker under the resolved interpreter - #4979
Conversation
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 Root cause fixed at the right seam — prompts name a real interpreter, the venv-preference alternative is correctly rejected on injection grounds, and the follow-on Windows perms gap is closed rather than shipped. [DESIGN-REVIEWED] bef513c |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of Reading the contract, the intent file, and the patch, then verifying claims against the repository (consumer counts, existing mechanisms, sibling sites) before emitting the structured review. I've checked First-Principles-Verdict: CONCERNS The description says the interpreter comes from What this change shipsIntent: make Code Review Sage work on Windows by naming a real interpreter instead of
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] bef513c |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsThe candidate list surfaced only one weak candidate. Let me verify it against the bar. CANDIDATE 1 claims I re-derived the load-bearing invariants of this diff independently:
Nothing survives at 80+. No findings. [OPUS-REVIEWED] bef513c Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
24660cd to
dab8161
Compare
|
Dispositions for the GPT 5.6 findings on
Legitimate and mine: this PR turned a non-blocking Not reverted, because reverting restores the silent-no-op lockdown this PR exists to close. Instead the function is split: Pinned by Completeness, since this narrowed one branch of a chain: every other site the PR routed through the same lockdown (
Legitimate: a backslash is a legal POSIX filename character and an escape in an unquoted shell word, and the allowlist admitted it bare. This is the second blocking finding in the Both branches, stated explicitly since a fallback chain was narrowed: |
dab8161 to
7e21c1a
Compare
|
Dispositions for Design Review's
Correct on both counts, and I could not pin the shell: the worker's shell tool selects PowerShell or cmd by detection, so the diff was betting on one of two and a space-containing profile is ordinary rather than exceptional. Taken with the two GPT findings in this same span across earlier rounds — Adopted your suggestion directly ("drop dual-shell rendering and have the prompt state the raw absolute path").
Legitimate and unchanged by this round. Making the suite Windows-native is real work with a decided shape, and it is not this PR: the harness asserts What this round does buy against invisible regression: the shell assumption you flagged no longer exists to regress, and the two guards added for it are pure functions that run on every platform CI covers. Filing the harness work as a follow-up issue naming those three concrete blockers.
|
7e21c1a to
f9cd882
Compare
|
Dispositions for First Principles'
Both halves correct, and the second half is the more useful one: the guard I added to prevent exactly this could never have seen it. A line-anchored pattern misses any command inside a blockquote, a list item, or indented prose — so the leftover could have come back at any time with the test still green. The line now reads
Legitimate and verified. It is out of this PR's scope: a different subsystem, reached by a different surface, and pulling it in would widen a Sage fix into the dashboard handlers. Worth noting the disposition is not merely "later" — the sibling is arguably worse now, because after this round Sage does no shell-specific rendering at all, so that call site is the remaining precedent rather than one of two. The issue says so, and carries the reasoning that made this PR abandon per-shell rendering.
Agreed on all counts, including that framing. All nine sites are listed in the issue with the caveat that each needs its own reachability check rather than a blanket substitution. The issue also carries the trap this PR paid a review round to learn: Design Review's separate harness concern is filed as #4988 with the measured Windows baseline, so the deferred work from both lanes is now written down rather than implied. |
|
Disposition for Opus 4.8's
Legitimate, and the trace is exactly right including the two details that made it actionable: that Fixed by splitting rather than by the suggested per-call-site Pinned by Your local-mirror counterpart on the follow-up head independently traced every other site this PR routed through the same lockdown — |
f9cd882 to
86fbf4e
Compare
|
Disposition for the GPT 5.6 finding on
The mechanism holds, and I verified the premise rather than taking it:
What landed instead: a single seam, Two guards, both mutation-checked by removing the lockdown from the seam and confirming each fails: Left deliberately out of scope: the containing directories are still not owner-only, which is the deeper fix (and would also stop the file names disclosing which PRs were reviewed). That belongs with the other lockdown siblings in #4987, not in a change whose subject is the interpreter. Separately, the four red backend shards were not a product defect. |
86fbf4e to
bbf4f04
Compare
|
Disposition for the Opus 4.8 observation on
The mechanism is exactly right, including the part that makes it real: the exception escapes during the tuple assignment, so the caller's Your falsification is also right, and is why this was advisory rather than blocking: on POSIX (the only platform the suite runs, since the conftest still skips Windows) The failure path now closes the descriptor and unlinks the temp, best-effort ( Worth recording that this is the second finding this lane produced whose value was in the falsification, not the verdict: naming why it does not block (empty artifact, untested platform) is what let me fix it without inflating it into a blocker or widening the diff. |
bbf4f04 to
65dc60a
Compare
|
Disposition for the GPT 5.6 BLOCKING finding on
Legitimate, and I verified the mechanism rather than taking it on the wording. Both sites built a fully predictable path ( Both writes now go through the Worth noting this fix was available two rounds ago and I did not take it: when the lockdown-ordering finding landed, I routed the seven Two guards, because the natural one cannot run where it matters:
One thing I got wrong mid-round and am flagging rather than quietly dropping. Checking whether the fix reintroduced the |
65dc60a to
2ff1c16
Compare
|
Disposition for the GPT 5.6 BLOCKING finding on
Legitimate, and the premise is the part I verified rather than assumed:
Nothing is given up by declining the venv preference here, and I checked rather than assuming. That preference exists so an app's own dependencies are importable. This app ships no Two guards, and the second is the one that matters for this span:
Span tally, recorded so a later round can see it: This one needed a maintainer ruling rather than another patch, and got one: the choice between |
…preter The review worker was handed `python3 sage_lib/...` commands. `python3` is not an interpreter on Windows: neither the python.org installer nor `python -m venv` creates a `python3.exe`, and the name resolves instead to a Microsoft Store app-execution alias that runs no Python. The failure is silent -- the command produces no result record, so the review starts and ends with no verdict. The app refused to run there rather than ship that, which was the last reason Code Review Sage did not work on Windows while Issue Radar did. The five worker-prompt commands now name the interpreter resolved through the shared app policy (the app's own venv if it has one, else the gateway's `sys.executable`) -- an absolute path that is correct on every platform. A path needing no quoting is emitted bare, so the prompt stays shell-neutral; only a path with a space becomes shell-specific, and then in the form the shell that will run it needs. A path that needs shell quoting is quoted LITERALLY rather than with double quotes: PowerShell expands `` and honours a backtick escape inside a double-quoted string, and both are legal in a Windows path, so a directory like `C:\tools\$python` would have been rewritten into a path that does not exist -- reintroducing the very silent no-result failure this change removes. The two shipped skills carry commands too, and they are registered by symlink/junction rather than copied, so there is no rendered file to substitute a placeholder into. They therefore stop naming an interpreter at all: commands read `<python> ...` and each skill opens with a note pointing at the one the task prompt names. Removing the refusal makes a second Windows gap live, so it is closed here. Every record, report and cache this app stages was locked to its owner with `os.chmod(0o600)`, which expresses that only on POSIX -- on Windows it toggles the read-only attribute, leaves the inherited DACL intact, and *succeeds*, so the files would stay readable by every other local account with nothing raised to notice. All nine call sites -- four record writers, the learning and report atomic writes, the repo cache, and the run registry and settings writers in the backend routes -- now go through one seam in `store.py` that delegates to the runtime's owner-only lockdown; the POSIX path is the same chmod, including the fail-loud OSError the callers' temp-file cleanup relies on. The test suite stays gated on Windows, and its conftest now says why: the harness is POSIX-only (`0600` as st_mode bits, forward-slash suffixes, shell-script `gh` stubs), which is separate work from making the app run. Tests: the existing prompt guard asserted the prompts contain a `python3 <path>` reference, so it failed on this change rather than passing vacuously; it is now interpreter-agnostic. Four new classes cover the prompts naming no bare interpreter, every script command carrying the resolved one, both shells' quoting forms, the app-root fallback, and the shipped skills -- a `python3` left in a skill reaches the worker exactly as a prompt string would, and the prompt guard cannot see it. Run on Windows with the collection gate lifted, the suite is 746 passed / 21 failed against a 735 / 21 baseline on the same commit: the same 21 pre-existing symlink-plant tests, which need privileges Windows does not grant. Fixes #4630
2ff1c16 to
bef513c
Compare
|
Dispositions for Design Review's
Correct on all three counts, and this is the concern I most needed raised: a green rollup would have merged a record documenting the design the diff rejects. Every claim you named was true of an earlier revision and false of the shipped one. The body now states that
It already does. if not rec["deep_reviewed"]:
rec["skipped_reason"] = "no_review_recorded" # turn completed but wrote no review
progress(change_id, "failed", {"error": "review produced no result record"})
return recSo a worker that ignores the quoting instruction lands in a
Both are now their own named bullets under "What changed", each with the mechanism that forced it: the predictable temp name was plantable in a worker-writable directory and |
|
Dispositions for First Principles'
Agreed, and the framing is the useful part: the diff was right and the text was wrong, so the fix belongs in the text. The body now says
Right on both the causes and the remedy. Each is now its own bullet naming the mechanism that forced it. They are not separable from the fix: item 5 is what makes
Filed as #4987 before this round, with the sites enumerated and the trap that makes the sweep non-trivial recorded in the body:
Verified against the helper rather than taken on the claim, because adopting it would have regressed two properties earlier rounds fixed if the ordering differed. It does not: with It also closes a latent defect I had introduced and not noticed: my writers replaced through a bare The constraint you identified is respected: Recording one span fact for the next round: |
First Principles (Fable 5) — CONCERNS on
|
Problem / Motivation
Code Review Sage does not run on Windows. Its review worker is handed
python3 sage_lib/...commands, andpython3is not an interpreter there:neither the python.org installer nor
python -m venvcreates apython3.exe,and the name resolves instead to a Microsoft Store app-execution alias that runs
no Python. The command produces no result record, so a review starts and ends
with no verdict — a silent failure. Rather than ship that, the app refused on
Windows, which was the last reason Sage did not work there while Issue Radar
(after #4613) did.
Why it matters
Windows users get no code review from the app at all. The refusal is the good
outcome of the two available today: without it, the review appears to run and
silently yields nothing, which is worse than a clear message. Removing the
refusal is the acceptance criterion for #4630, and it is only safe once the
worker names an interpreter that exists on the host.
What changed (motivation → approach → change)
Symptom: the worker's Python commands never run on Windows. Root cause: the
prompts and the shipped skills name a platform-specific alias instead of an
interpreter. So the change stops naming an alias.
The five prompt sites in
sage_lib/review_driver.pynow name the gateway'sown
sys.executable— an absolute path to a real interpreter on every platform.This is deliberately not the shared
resolve_app_python(app_root)policy,which prefers
<app_root>/.venv.store.app_root()resolves underKIROCREW_HOME, the same writable tree the review worker writes into, and thatworker is prompt-injectable: a planted
.venv/Scripts/python.exewould beexecuted by the next review, which is arbitrary code execution as the gateway
plus forged result records — persistence into a later run rather than a
capability the worker already had. Nothing is given up by declining the venv
preference here: that preference exists so an app's own dependencies are
importable, this app ships no
requirements.txt, and the only non-stdlib importanywhere in
sage_lib/iskiro_crewitself, which is importable undersys.executableby construction. An app venv would in fact be the weaker choice,since it need not have
kiro_crewinstalled at all.The path is handed over raw, with no shell quoting. Quoting requires knowing
the worker's shell, which is not pinned: a Windows session may get PowerShell or
cmd, and a form valid in one is a syntax error in the other (a quoted path needs
PowerShell's call operator, which cmd rejects). Since a Windows profile with a
space in it is ordinary rather than exceptional, guessing wrong would restore the
same silent no-result failure for a large share of Windows users. The prompt
therefore tells the worker to quote as its own shell requires.
The two shipped skills carry commands too, and app skills are registered by
symlink/junction into
~/.kiro/crew/skillsrather than copied(
apps/bridges.py), so there is no rendered file to substitute a placeholderinto at load time. They therefore stop naming an interpreter at all: commands
read
<python> …, and each skill opens with a note pointing at the one the taskprompt names.
learn-from-sage/SKILL.mdis included because the review promptroutes inline miss-analysis through it, so its commands reach the worker exactly
as the review skill's do.
The refusal in
sage_lib/discovery.pyis removed, which is what closes theissue.
Removing it makes a second Windows gap live, so this closes it too. Every
record, report and cache the app stages was locked to its owner with
os.chmod(0o600), which expresses that only on POSIX: on Windows it toggles theread-only attribute, leaves the inherited DACL intact, and succeeds — so the
files would stay readable by every other local account with nothing raised to
notice. The
sage_libcall sites now route through one seam,store.open_locked_temp, which creates the temp withmkstempand appliesplatform_compat.restrict_to_ownerbefore returning the descriptor; thePOSIX path is the same chmod, including the fail-loud
OSErrorthe callers' tempcleanup relies on.
Two further changes in
backend/routes.pyfollow from that one and are calledout because they are behaviour changes, not refactors.
registry and the settings writer) previously used
<name>.tmpand a bareos.replace. A predictable name in a worker-writable directory lets aprompt-injected worker pre-plant a symlink there, and
Path.touch()followsone — so the lockdown and the payload would both land on an arbitrary
user-owned file. Both now call
kiro_crew.atomic_write.atomic_write(..., restrict_to_owner=True), which picks an unpredictablemkstempname, locks thetemp down before the payload, refuses a planted parent link, and replaces
through
replace_with_retry— closing a transient-sharing-violation gap onWindows that the bare
os.replacehad (the class tracked in deploy pending confirmations can fail on a transient Windows replace sharing violation #4701 / md-notebook: a contended rename loses the note save on Windows #4898).restrict_to_ownerspawnsicaclsonWindows, so the write can no longer run on the gateway's event loop.
_save_runsis now a coroutine that serializes the registry on the loop — preserving the
snapshot-under-the-caller's-lock property — and offloads only the file write via
asyncio.to_thread; all eight call sitesawaitit.The test suite stays gated on Windows and its conftest now says why: the
harness is POSIX-only (
0600asst_modebits, forward-slash suffixes,shell-script
ghstubs), which is separate work from making the app run. Thedocstring previously gave two reasons; one is now false, so only the true one
remains.
Tests
python3 <path>reference, so it failed on this change rather thanpassing vacuously. It is now interpreter-agnostic, still checking every
referenced script path exists.
TestWorkerPromptInterpreter— no prompt names a bare interpreter; everyscript command carries the resolved one; the prompt states the interpreter for
the skills'
<python>placeholder; the resolved value is absolute; andtest_the_interpreter_is_never_taken_from_a_worker_writable_pathpatchesstore.app_rootto raise, so any reintroduction of the app-root lookup failsthe suite.
TestInterpreterIsHandedOverRaw— a path containing a space, a$, or abackslash run arrives verbatim. Each of those was a separate defect in an
earlier revision of this change, so the guard pins all three at once.
TestShippedSkillsNameNoBareInterpreter— reads the shippedSKILL.mdfilesand fails on a bare-interpreter command. A
python3left in a skill reachesthe worker exactly as a prompt string would, and the prompt guard cannot see
it.
TestRestrictToOwner— the lockdown delegates to the runtime helper (not a rawchmod, which succeeds while doing nothing on Windows), a lockdown failure
propagates so the callers' temp-file cleanup still runs, owner-only permissions
are applied on POSIX, and a failed lockdown leaves behind neither the
descriptor nor the temp file.
test_backend_routes.py— the owner-only lockdownnever runs on the event loop (the test records the executing thread id and
compares after the call, rather than asserting inside a swallowed
except);a planted
runs.json.tmpsymlink is not followed; and the predictable<name>.tmppath is never written, which is the symlink-free twin so theinvariant is also covered on Windows, where creating a symlink needs a
privilege CI does not grant.
Manual verification
Verified on a Windows 11 host that
python_command()returns an absoluteinterpreter and that all four commands in the built prompt carry it.
Sage's own suite was run on Windows with the collection gate lifted, against a
baseline captured at the same commit: 750 passed / 21 failed on this branch
vs 735 / 21 on the base. The 21 are the same pre-existing symlink-plant
tests, which need a privilege this account does not hold (
WinError 1314) — sothis change adds passing tests and no failures. That comparison is also what
substantiates the conftest's new claim that the harness, not the app, is what is
POSIX-bound.
Also green locally:
isort,flake8, the black gate (run with the pinnedblack==26.3.1, since a locally-installed 25.x reports a different verdict),mypy --platform linuxover all 1,038 source files (3 pre-existingwecom/client.pyerrors in an untouched file come from a local aiohttp versiondiffering from CI's pin), the brand-name gate, and the subprocess-spawn audit.
The full backend suite was not completed on this host; CI is the backstop for it.
Related Issues
Fixes #4630