fix: resolve kiro-cli via resolve_kiro_cli() in auto-update paths - #7712
fix: resolve kiro-cli via resolve_kiro_cli() in auto-update paths#7712bolichen97 wants to merge 1 commit into
Conversation
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Root-cause fix through the existing shared resolver, with the update/launch-target-divergence invariant pinned by an AST audit — right shape, right layer, proportionate scope. [DESIGN-REVIEWED] 152fb45 |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of First-Principles-Verdict: CONCERNS The fix is cause-level and its deferred siblings are counted — but a standing repo-wide AST enforcement and a thread-offload behavior change ship undeclared inside it. What this change shipsIntent: make auto-update maintain the kiro-cli binary the agent actually launches, instead of silently skipping installs the inherited
Watch
[FIRST-PRINCIPLES-REVIEWED] 152fb45 |
GPT 5.6 Review — 🔴 changes requested (blocking)GPT 5.6 found at least one blocking issue that must be resolved before merging This comment is updated in place on each push. BLOCKING -- src/kiro_crew/slack/gateway.py:9642 -- Fixed-directory resolution enables unsandboxed agent-planted execution |
f8b5237 to
717bdd4
Compare
Legitimate, and legitimate specifically because this PR introduced it. I verified the chain rather than taking it on report:
Applied the suggested form verbatim — One correction to the record for anyone reading the earlier verification notes: Pinned by a test rather than left to review: Gates re-run on the rebased head ( |
The four sites are real — I verified each one — and the disclosure complaint was fair. That half is closed: the description now carries a 1. This PR does not create the incoherence, and does not deepen it. Before it, the updater and the version probe agreed with each other while both disagreed with the binary the agent actually spawns ( 2. The gateway remedy is not the "one-line fold-in" the finding describes. 3. The strongest argument against the fold-in is the finding one lane over. 4. The remaining two are a different surface with different intended semantics. None of the four is a reachable Critical/High: no crash, no security hole, no data loss, no removed guard. The worst case is a suppressed advisory warning and a misleading diagnostics row, both pre-existing and neither made worse here. That is what makes them deferrable to a follow-up rather than in scope for a fix whose stated purpose is the update path. |
Agreed on the diagnosis, and the verdict is PASS so nothing is blocked. I am declining the fold-in for this PR, and the description now names all the sibling sites under The decisive reason is that the suggested change would land the defect the blocking lane just flagged. Secondary cost, for the record: Neither site is a reachable Critical/High — the worst case is a suppressed advisory version warning and a diagnostics row reading "unavailable", both pre-existing and neither made worse by this diff. |
The two auto-update steps gated on shutil.which("kiro-cli") and spawned
the bare name, resolving only through the inherited PATH. A working
install reachable only through a fixed known_kiro_cli_dirs() entry was
therefore silently skipped by `kiro-cli update` (issue #7704).
Resolve the absolute path with resolve_kiro_cli() at both sites
(cli_server._update and GatewayOrchestrator auto-update); skip when it
returns None as before, otherwise spawn the returned absolute path.
Update the existing auto-update tests and add regression coverage for
the fixed-dir-only and no-install cases.
Both spawn sites sit in test_spawn_audit.py's BENIGN_SPAWNS on the
basis that their argv is fixed, and that basis moves when argv[0]
becomes a resolved path. Give each entry its own justification naming
what bounds it instead — one resolver, shared with the ACP session
spawn, so the maintenance step can only ever name the binary the agent
itself runs — and record the residual it does not close: the candidate
order includes user-writable install directories, which the write side
owns rather than a maintenance spawn. A new audit test pins that every
site which execs the user's Kiro CLI resolves through
kiro_cli.resolve_kiro_cli, and that the two update sites spawn its
answer with no added argv element, so no site can reintroduce a
bare-name or PATH-only lookup. cli.md's Update Command list documents
the kiro-cli step and how its path is resolved.
717bdd4 to
152fb45
Compare
The half that is true, stated plainly. Where the chain completes, though, is The remedy does not remove the branch it names. And this very function already refuses that pattern, 486 lines above the flagged line. I weighed the four alternatives and rejected each on this repo's own recorded decisions, not on taste:
What I landed instead, since the finding did surface something this diff genuinely owed. Both sites sit in The residual is escalated, not waived. Fencing the trusted agent runtime from agent writes is the change that would close every current and future site at once, and it is a keystone Gates on the rebased head ( |
I am not overriding this and I am not applying the demanded remedy. Below is what I verified holds, what does not, and the two rulings the residual needs. An earlier disposition of mine argued the marginal capability of this diff is empty; that argument was wrong on Linux and I am withdrawing it. Conceded: on Linux there is a real confinement delta, and it is this diff'sEvery step run against this branch:
So on a Linux host whose invoking PATH does not name Rejected: the demanded remedy, because it narrows the class rather than closing itOn Two consequences for the remedy:
Why neither half of the closure is inside this PRThe confinement half. Routing the maintenance spawn is not additive here. The write half, which is the reviewer's own precondition ("until fixed candidate directories are write-protected"). Three mechanisms exist and each costs something a maintainer should price, not an auto-update patch:
A leaf-only fence on the One more piece of the repo's own record, because it frames what the ruling is about rather than excusing the delta. What I am doingKeeping the code, because the fix it makes is correct and the remedy would undo it while leaving the class open. Escalating the residual as its own keystone change with its reproduction: it is a |
The disclosure complaint is correct and it is the part I owed. The description now carries the thread offload as an explicit bullet in the Fix section — the mechanism ( On removing any of the three, the reasoning differs per item and none of them is optional for this diff: Item 4 is not separable from the fix. The offload exists because of the fix: the new resolver call is what puts a glob of every tool-manager version root on the gateway event loop, so shipping the resolver swap without the offload would introduce a loop stall that Item 8 is owed by the diff rather than riding on it. Both functions were in Item 5 I agree with on the narrow point and am keeping anyway, for the reason the lane itself names. The three behavioural tests do fail a revert to On the siblings, the lane's own verdict this round is that they are declared with per-site reasons and accepted-and-deferred, which matches the record; I am not re-litigating them here. I am taking the closing suggestion: |
Audit note — #7964 fixes the same six lines; it is being closed, and it leaves you one thing to absorb#7964 ("fix(security): pin the auto-update Please pick this up — it is real, and it is the contributor's
Caveats for that follow-up, both flagged by the reviewers: kiro-cli 2.15+ resolves its siblings Worth taking too: #7964's One note on this PR's own stateIt is not mergeable as it stands — its blocking review finding is dispositioned rather than From a repository-wide duplicate/overlap audit of every pull request open against |
The mechanism is real and is main's condition rather than this diff's: The remedy that would close it at the root — fencing the agent's write tools out of the trusted agent runtime, the way Recorded here so the ruling is visible on the PR rather than only in a chat. This PR still needs either an explicit override of this lane or a decision to accept the lane's revert, and the revert reinstates the bug #7704 reports. |
Three independent findings, each closed with the smallest change that removes the mechanism. 1. Model-hidden-tool filter bypass at stub registration (MEDIUM). `Backend` exempts any stub uuid starting with `INTERNAL_STUB_PREFIXES` from the MCP Apps render path AND the model-visibility filter. That is correct for requests the gateway mints itself, but `_handle_stub_conn` rejected only an EMPTY `stub_uuid`, so a registrant that simply NAMED itself with the prefix inherited both exemptions and could be served tools the model is meant not to see -- with no SEL record of the withhold that never happened. Refused at registration, mirroring the existing empty-check reject. The gateway's own internal stubs are attached in-process (`Backend.attach_stub` for `__app_call__`, `probe_tool_surface` for `__tool_surface__`), never through a Register frame, so nothing legitimate is refused. 2. ReDoS in the markdown one-line fold (MEDIUM). `_md_one_line` folded with `\s*\n\s*`, where `\s` matches a newline too, so the runs and the anchor competed for the same characters: on a newline-FREE whitespace run the engine retried every split at every offset. Called per heading and per table cell, with provider-controlled content bounded only by the 8MiB fetch cap -- 200k spaces took ~45s. Replaced with split/strip/join, which reads each character a fixed number of times: the same input now folds in 0.3ms. Output is unchanged, verified by a differential harness over 40k inputs including \r, \v, \f, \x85, \u00a0, \u2028 and \u3000. 3. Markdown code-span breakout (MEDIUM). `_md_inline_code` fenced ADF `code`-marked text but let INTERIOR newlines through verbatim. A code span is inline, so a blank line ended the enclosing paragraph and everything after it was parsed as fresh markdown -- outside the fence, and so past `_md_escape_inline`, `_md_link_target` and the redaction gate. Line breaks are now collapsed to a space before fencing. Collapsed rather than promoted to a fenced block because a block would change the document structure at every call site, while the escape is what has to hold. Scope note: an earlier revision of this branch also carried a fix for the bare-name `kiro-cli` spawn in the gateway's unattended auto-update (`slack/gateway.py`). That fix is dropped here because PR #7712 already owns it under issue #7704 -- same `resolve_kiro_cli()` approach, plus the `asyncio.to_thread` offload, plus the `cli_server.py` sibling call site and the `docs/system-specs/modules/cli.md` update this branch did not carry. Keeping a second copy would only conflict with it. Tested - pytest test_source_providers, test_gatewayd_more_coverage, test_governance_updates, test_spawn_audit, test_gatewayd_diag, test_gatewayd_self_exit, test_mcp_gatewayd_coverage, test_source_providers_comment_guard, test_source_provider_plugin: pass. The two `test_provider_executable_accepts_*` failures are pre-existing on origin/main in this environment (home ownership) and pass in CI. - black (added lines only, py312 target), flake8, isort, mypy --platform linux: clean. - brand / focus-cue / changelog / harness-parity gates run diff-scoped with their BASE_REF exported: pass. docs-lint: pass. Revert-verified (each guard fails when its fix is reverted, and passes again when restored): - #1 the registration test fails on `__app_call__` when the prefix reject is removed. - #2 the linearity guard fails at 45.6s against a 2.0s budget with the old pattern restored. - #3 both code-span tests fail, one on `'\n' not in out`, when the collapse is removed.
|
Drive-by from #8118, which briefly carried a duplicate of this fix before I found #7704 and dropped it — this PR is the better version and owns it. Handing over the one thing mine had that this one does not, then getting out of the way. The gateway spawn inherits its environment; the git spawns beside it do not. In That matters here for a reason narrower than the residual you already adjudicated: Adding Entirely your call whether it belongs in this PR or a follow-up — it is adjacent to your stated scope, not inside it, and I would not want to widen a PR that is already through review on the strength of a drive-by. Flagging it rather than pushing it, since #8118 no longer touches this file. |
Three independent findings, each closed with the smallest change that removes the mechanism. 1. Model-hidden-tool filter bypass at stub registration (MEDIUM). `Backend` exempts any stub uuid starting with `INTERNAL_STUB_PREFIXES` from the MCP Apps render path AND the model-visibility filter. That is correct for requests the gateway mints itself, but `_handle_stub_conn` rejected only an EMPTY `stub_uuid`, so a registrant that simply NAMED itself with the prefix inherited both exemptions and could be served tools the model is meant not to see -- with no SEL record of the withhold that never happened. Refused at registration, mirroring the existing empty-check reject, and the refusal is itself audited: claiming a reserved prefix is an attempt to acquire an exemption, which is the same class of access decision as `_audit_peer_identity_denied` and is recorded the same way. The sibling rejects on this path stay WARNING-only because they are schema failures with no control being evaded. The gateway's own internal stubs are attached in-process (`Backend.attach_stub` for `__app_call__`, `probe_tool_surface` for `__tool_surface__`), never through a Register frame, so nothing legitimate is refused. 2. ReDoS in the markdown one-line fold (MEDIUM). `_md_one_line` folded with `\s*\n\s*`, where `\s` matches a newline too, so the runs and the anchor competed for the same characters: on a newline-FREE whitespace run the engine retried every split at every offset. Called per heading and per table cell, with provider-controlled content bounded only by the 8MiB fetch cap -- 200k spaces took ~45s. Replaced with split/strip/join, which reads each character a fixed number of times: the same input now folds in 0.3ms. Output is unchanged, verified by a differential harness over 40k inputs including \r, \v, \f, \x85, \u00a0, \u2028 and \u3000. 3. Markdown code-span breakout (MEDIUM). `_md_inline_code` fenced ADF `code`-marked text but let INTERIOR newlines through verbatim. A code span is inline, so a blank line ended the enclosing paragraph and everything after it was parsed as fresh markdown -- outside the fence, and so past `_md_escape_inline`, `_md_link_target` and the redaction gate. Line breaks are now collapsed to a space before fencing. Collapsed rather than promoted to a fenced block because a block would change the document structure at every call site, while the escape is what has to hold. Scope note: an earlier revision of this branch also carried a fix for the bare-name `kiro-cli` spawn in the gateway's unattended auto-update (`slack/gateway.py`). That fix is dropped here because PR #7712 already owns it under issue #7704 -- same `resolve_kiro_cli()` approach, plus the `asyncio.to_thread` offload, plus the `cli_server.py` sibling call site and the `docs/system-specs/modules/cli.md` update this branch did not carry. Keeping a second copy would only conflict with it. Tested - pytest test_source_providers, test_gatewayd_more_coverage, test_mcp_gatewayd_coverage, test_governance_updates, test_spawn_audit, test_gatewayd_diag, test_gatewayd_self_exit, test_source_providers_comment_guard, test_source_provider_plugin: pass. The two `test_provider_executable_accepts_*` failures are pre-existing on origin/main in this environment (home ownership) and pass in CI. - black (added lines only, py312 target), flake8, isort, mypy --platform linux: clean. - brand / focus-cue / changelog / harness-parity gates run diff-scoped with their BASE_REF exported: pass. docs-lint: pass. Revert-verified (each guard fails when its fix is reverted, and passes again when restored): - #1 the registration test fails on `__app_call__` when the prefix reject is removed, and the audit test fails `assert [] == ['__app_call__deadbeef']` when the SEL call is removed. - #2 the linearity guard fails at 45.6s against a 2.0s budget with the old pattern restored. - #3 both code-span tests fail, one on `'\n' not in out`, when the collapse is removed.
|
Closing without merging, by the author's decision. The GPT lane's blocking finding on this branch has one remedy it will accept — restore the With both ends closed, this branch has no path to green that leaves the fix in it, so it is withdrawn rather than overridden. #7704 stays open. Nothing here is a rebuttal of the bug: The four sibling bare-name sites documented in the description above ( |
) Three independent findings, each closed with the smallest change that removes the mechanism. 1. Model-hidden-tool filter bypass at stub registration (MEDIUM). `Backend` exempts any stub uuid starting with `INTERNAL_STUB_PREFIXES` from the MCP Apps render path AND the model-visibility filter. That is correct for requests the gateway mints itself, but `_handle_stub_conn` rejected only an EMPTY `stub_uuid`, so a registrant that simply NAMED itself with the prefix inherited both exemptions and could be served tools the model is meant not to see -- with no SEL record of the withhold that never happened. Refused at registration, mirroring the existing empty-check reject, and the refusal is itself audited: claiming a reserved prefix is an attempt to acquire an exemption, which is the same class of access decision as `_audit_peer_identity_denied` and is recorded the same way. The sibling rejects on this path stay WARNING-only because they are schema failures with no control being evaded. The gateway's own internal stubs are attached in-process (`Backend.attach_stub` for `__app_call__`, `probe_tool_surface` for `__tool_surface__`), never through a Register frame, so nothing legitimate is refused. 2. ReDoS in the markdown one-line fold (MEDIUM). `_md_one_line` folded with `\s*\n\s*`, where `\s` matches a newline too, so the runs and the anchor competed for the same characters: on a newline-FREE whitespace run the engine retried every split at every offset. Called per heading and per table cell, with provider-controlled content bounded only by the 8MiB fetch cap -- 200k spaces took ~45s. Replaced with split/strip/join, which reads each character a fixed number of times: the same input now folds in 0.3ms. Output is unchanged, verified by a differential harness over 40k inputs including \r, \v, \f, \x85, \u00a0, \u2028 and \u3000. 3. Markdown code-span breakout (MEDIUM). `_md_inline_code` fenced ADF `code`-marked text but let INTERIOR newlines through verbatim. A code span is inline, so a blank line ended the enclosing paragraph and everything after it was parsed as fresh markdown -- outside the fence, and so past `_md_escape_inline`, `_md_link_target` and the redaction gate. Line breaks are now collapsed to a space before fencing. Collapsed rather than promoted to a fenced block because a block would change the document structure at every call site, while the escape is what has to hold. Scope note: an earlier revision of this branch also carried a fix for the bare-name `kiro-cli` spawn in the gateway's unattended auto-update (`slack/gateway.py`). That fix is dropped here because PR #7712 already owns it under issue #7704 -- same `resolve_kiro_cli()` approach, plus the `asyncio.to_thread` offload, plus the `cli_server.py` sibling call site and the `docs/system-specs/modules/cli.md` update this branch did not carry. Keeping a second copy would only conflict with it. Tested - pytest test_source_providers, test_gatewayd_more_coverage, test_mcp_gatewayd_coverage, test_governance_updates, test_spawn_audit, test_gatewayd_diag, test_gatewayd_self_exit, test_source_providers_comment_guard, test_source_provider_plugin: pass. The two `test_provider_executable_accepts_*` failures are pre-existing on origin/main in this environment (home ownership) and pass in CI. - black (added lines only, py312 target), flake8, isort, mypy --platform linux: clean. - brand / focus-cue / changelog / harness-parity gates run diff-scoped with their BASE_REF exported: pass. docs-lint: pass. Revert-verified (each guard fails when its fix is reverted, and passes again when restored): - #1 the registration test fails on `__app_call__` when the prefix reject is removed, and the audit test fails `assert [] == ['__app_call__deadbeef']` when the SEL call is removed. - #2 the linearity guard fails at 45.6s against a 2.0s budget with the old pattern restored. - #3 both code-span tests fail, one on `'\n' not in out`, when the collapse is removed. Co-authored-by: Joe Guo <zejiangg@amazon.com>
Fixes #7704.
Problem
Two auto-update steps gated on
shutil.which("kiro-cli")and then spawned the bare name, so they resolved only through the process's inheritedPATH:src/kiro_crew/cli_server.py(CLI-side auto-update)src/kiro_crew/slack/gateway.py(gateway auto-update)Because the spawn was
which-guarded, a working install reachable only through a fixedknown_kiro_cli_dirs()entry was silently skipped bykiro-cli update. Same root cause as #7674 (bare-name resolution instead ofresolve_kiro_cli()); deliberately left out of PR #7692's scope (diagnostics-only).Fix
Both sites now resolve via the existing
resolve_kiro_cli()helper (kiro_cli.py): skip when it returnsNone(unchanged best-effort no-op), otherwise spawn the returned absolute path. Same containment posture as before (update remains best-effort).cli_server.py: addedfrom kiro_crew.kiro_cli import resolve_kiro_cli; replaced theshutil.whichgate withresolve_kiro_cli()and spawn[kiro_cli_bin, "update"].slack/gateway.py: same import; replaced the gate and spawn the resolved path.shutilremains used elsewhere in both files.slack/gateway.pyalso moves the resolution off the event loop —kiro_cli_bin = await asyncio.to_thread(resolve_kiro_cli). This is a timing change shipped deliberately with the fix, not a drive-by:resolve_kiro_cli()stats every fixed candidate directory and every inherited-PATHentry and reachesenv.augmented_path(), which globs every mise/asdf/nvm/fnm version root, whereas theshutil.whichit replaces walkedos.environ["PATH"]only. Resolving on the loop would therefore newly stall chat and heartbeat on the first cold call.cli_server.py::_updatestays synchronous — it is a CLI verb with no event loop, so the stat walk blocks nothing but the command the operator invoked.test/test_slack_gateway.py::TestAutoApplyUpdateVenvPath::test_kiro_cli_update_timeout_kills_child_and_stays_nonfatalasserts the resolver does not run on the thread running the coroutine.resolve_kiro_cli()is a strict superset of the oldshutil.whichgate: it honoursKIROCREW_KIRO_BIN, then the fixed known directories, then the inheritedPATH, and returnsNonewhen nothing is executable. Where a host has copies both onPATHand in a fixed directory, update now targets the fixed-directory copy — which is the binary the agent spawn itself resolves (acp/client.py), so the updater and the agent now agree on which binary they are talking about.Scope: the sibling bare-name sites are deliberately not in this PR
Four other call sites still resolve the bare name. None of them is on an auto-update path, none is made worse by this change, and each needs its own reasoning — so they are left for a follow-up rather than folded in here:
slack/gateway.py::_warn_if_kiro_cli_outdated— the boot version probe. Not the one-line swap it looks like: it is a declaredtest_spawn_audit.pysandbox exemption annotated "fixed argv", it carries a documented never-raises contract, and seven existing tests intest/test_slack_gateway_more_coverage.pypatchasyncio.create_subprocess_execand would need the resolver patched too. It was already probing a binary the agent may not be running, both before and after this PR.diagnostics.py::_kiro_cli_version— reportsunavailablefor a fixed-directory install. Akirocrew doctorsurface this PR does not touch.cli_setup.py— its message reads "kiro-cli not found on PATH", soPATHis the intended semantics; changing the probe without rewriting the advice would make the advice wrong.cli_doctor.py— already importsresolve_kiro_cliand uses it elsewhere; the remaining sites need a per-site read.Tests
Updated existing auto-update tests to patch
resolve_kiro_cliand assert on the resolved absolute path. Added regression tests:test_fixed_dir_only_kiro_cli_is_updated_not_skipped— resolvable via a fixed directory while absent fromPATH; the update must run and must spawn the absolute path.test_no_kiro_cli_skips_the_backend_update—Noneresolves to a skip, preserving the best-effort no-op.Also updated
test/test_slack_gateway.pyto patchresolve_kiro_cliinstead ofshutil.which.Verification
Run against this branch rebased on
main:isort --check-only,flake8,mypy --platform linux src/kiro_crew— clean.scripts/check_black_formatting.py,check_subprocess_encoding.py,check_agent_sdk_boundary.py,check_sync_io_in_async.py,check_lockdown_before_publish.py— all pass. (The earlierBackend Lint & Type Check (3.10)red was this gate:test_cli_server_more_coverage.pywas not black-formatted. Fixed.)test_cli_server_more_coverage.py,test_slack_gateway.py,test_slack_gateway_more_coverage.py,test_spawn_audit.py,test_security_posture.py,test_kiro_prerequisite.py— 683 passed.The earlier
Backend Tests (3.10, 4)red wastest_subagent_state_write_serialization.py::TestOnLoopCallerDoesNotWait::test_a_coroutine_does_not_wait_on_a_held_lockfailing a timed threshold by 0.08s (0.58sagainst< 0.5) on a shared runner. That file is not in this diff and the test passes on bothmainand this branch locally;Coverage Gatefailed closed on that shard (backend-test=failure -- failing closed), so it is the same failure reported twice.Pattern harvest
Rule candidate: semgrep
Pattern: a hardcoded
kiro-clipath or a bare"kiro-cli"argv name on a spawnpath, where the repo already owns a resolver (
resolve_kiro_cli()). Every suchcall site silently diverges from the resolver's search order, so an operator whose
binary lives outside the assumed location gets a working agent everywhere except
the one path that hardcoded it. Review prompt: when a binary has a named resolver,
grep for every other spelling of that binary before adding a call site.
The spawn-audit record for these two sites, and the residual
Both changed functions sit in
test/test_spawn_audit.py'sBENIGN_SPAWNS, on themodule docstring's stated basis that a self-update spawn is a "fixed argv against our
own install". That basis moves the moment
argv[0]becomes a resolved path, so eachentry now carries its own justification naming what actually bounds it — one resolver,
shared with the ACP session spawn (
acp/client.py::_resolve_kiro_bin), so themaintenance step can only ever name the binary the agent itself runs, and no
agent-supplied value reaches the argv.
test_kiro_cli_update_spawns_resolve_through_the_shared_resolverpins that invariantacross all three sites, so the update target and the launch target cannot drift apart
and no site can reintroduce a bare name or a
shutil.whichgate. Confirmed to bite bymutating production code on each of its four branches.
docs/system-specs/modules/cli.md§ Update Command documents the previously unlistedkiro-cli step and how its path is resolved.
The residual is stated in both places rather than left implicit: the candidate order
includes user-writable install directories —
~/.local/binis where Kiro CLI's owninstaller puts the binary — and
security.is_sensitive_path/is_sensitive_write_pathboth answer
Falsefor it, so an auto-approved agent write there is not refused. Thatresidual belongs to the resolver and to the write side, not to a maintenance spawn: the
ACP session spawn resolves through the same function, reaches the same candidate ahead
of every
PATHentry, and runs at every session start.Closing it is a keystone decision with no cheap form, which is why it is filed for its
own PR rather than carried here.
_WRITE_PROTECTED_HOME_PATHSalone would not close it:that list is enforced at the file-edit gate only, so an entry there leaves
cp payload ~/.local/bin/kiro-clipermitted. The path-shaped alternative in thesensitive-command regex — the mechanism that actually refuses a write under
.kiro/agents— is verb-independent, so applying its shape to a general-purpose userbin directory would also refuse routine reads and listings there. And kiro-cli is a
multi-call binary that execs sibling executables resolved relative to its own path, so a
leaf-only fence on the
kiro-cliname is insufficient in any case.One property of this diff belongs in the same record rather than only in a review
thread: on Linux the ACP session spawn runs inside the namespace sandbox (the
is_kiro_clidelegation is gated to darwin and win32), while both update sites areunrouted
BENIGN_SPAWNSentries. So on a host where~/.local/binis not on theinvoking
PATH, this change adds one execution of that candidate outside thecredential-hiding sandbox. Routing the maintenance spawn is not the remedy available
here — the audit already records that a self-update spawn cannot run confined because
the installer must write its own install directory, and any routed spawn must
additionally carry a kernel RLIMIT ceiling, which is not something to put on a
downloader on an untested assumption. It is part of the same escalation.