fix(skills): refuse writes to kiro-user/ and kiro-workspace/ keys - #8259
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of All claims verified: the read path resolves these prefixes via session/machine-scoped roots ( Design-Verdict: PASS Refuse-on-write is the proportionate contract enforcement for already-documented-read-only territories; the alternatives were weighed and the diff matches the description exactly. Suggestions
[DESIGN-REVIEWED] 6ef3788 |
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: |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All claims verified. I have everything needed for the review: the defect is real ( First-Principles-Verdict: PASS Closes a reported one-key-two-files defect (#8244) at the contract the docs already promised; every item is the fix or mandated by AGENTS.md. What this change shipsIntent: stop the skills endpoint from editing/deleting a different file than the reader was shown for
Verification: WatchThe third territory with the identical read/write divergence, [FIRST-PRINCIPLES-REVIEWED] 6ef3788 |
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: |
2d7348e to
845168a
Compare
kiro-user/<rel> and kiro-workspace/<rel> skill keys resolve per-machine and per-session on the read path, but the dashboard write handlers joined the same key onto the core skills root. One key therefore named two different files, so an edit or a delete reported success against a file the reader was never shown. Both territories are already documented read-only, so the write path refuses them instead of writing the core-root copy: PUT and DELETE answer 405 with Allow: GET and code readonly_skill_prefix, and a create whose sanitized name lands in either answers 400 with code reserved_skill_prefix. Reads and non-prefixed writes are untouched. The two module specs that document this endpoint family record the refusal alongside the existing kiro-workspace/ read scoping.
845168a to
6ef3788
Compare
CI status on
|
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
CI now fully green on
|
| condition | elapsed |
|---|---|
| no coverage | ~2.0s (3 runs) |
under coverage tracing, idle box |
2.5-3.2s (3 runs) |
| CI shard 3 observed | 6.41s |
6.41s is ~4x below the quadratic signature, so the regex is linear as intended. The relevant detail is that shard 3 runs pytest -n auto --cov=kiro_crew --cov=sage_lib, so coverage tracing plus full core saturation eats most of the headroom the 6.0s ceiling assumes — the budget is ~2x clear under CI conditions, not the ~4x its docstring implies. Re-running the failed jobs on the identical snapshot passed both.
The full file passes locally (57 passed), as do this PR's own 10 new tests.
One hypothesis checked and disproved, recorded so nobody re-derives it: since no .test_durations is committed, pytest-split falls back to an even split by test count, so this PR's 10 added tests do shift shard indices. But collecting --splits 4 --group 3 both with and without test/test_skill_write_territory_guard.py places the linearity test in group 3 either way — this PR did not move it into the failing shard. The under-budgeted ceiling is pre-existing and owned by main, not by this diff; it is left untouched here rather than widened from an unrelated PR.
Remaining gate is human. mergeable_state is blocked solely on the required approving review — require_last_push_approval is set and the last pusher is bolichen97, so this needs a maintainer who is not the last pusher. All four blocking review lanes (GPT 5.6, Opus 4.8, Design, First Principles) report PASS with zero findings on this revision, there are no unresolved threads, and no finding is awaiting a disposition.
[operator: bolichen97]
chenmingwei23
left a comment
There was a problem hiding this comment.
Approving: PR Readiness green (the repo's only required check), no failing lanes, MERGEABLE.
Closes #8244.
Problem
kiro-workspace/<rel>skill keys resolve against the requesting session's project directory on the READ path (via_resolve_skill_root-><project>/.kiro/skills), andkiro-user/<rel>resolves per-machine (~/.kiro/skills). But the write handlers joined the key straight onto a core skills root. The same key therefore named a different file on write than the reader was shown, so an edit or delete could hit a file the reader never saw. This is the same one-key-two-files shape closed forpackage/, left open for the two open-standard skill-key territories.Contract decision
The issue asked for a design decision among three options (session-scope the key, refuse on the write path, or document the ambiguity). This PR chooses refuse-on-write:
kiro-user/andkiro-workspace/are already documented read-only inapi_skills.skills.create/update/delete_skill(a much larger change and behavior expansion).Changes
src/kiro_crew/dashboard/handlers/prompts.py: newREADONLY_SKILL_KEY_PREFIXES = ("kiro-user/", "kiro-workspace/").api_skill_detail: PUT/DELETE on these prefixes -> 405 withAllow: GETand codereadonly_skill_prefix, placed before any session-key work soskills.*is never reached.api_skills_create: create into these prefixes -> 400 with codereserved_skill_prefix, checked against the sanitised name soKiro-Workspace/Foois also caught.test/test_skill_write_territory_guard.py(new): 10 async direct-handler tests.docs/system-specs/modules/memory-skills-hooks.mdanddocs/system-specs/modules/learn-cron-dashboard.md: both spec sites that documentGET/PUT/DELETE /api/skills/{name}now record the refusal and its two error codes, so the spec no longer implies PUT/DELETE succeed for akiro-workspace/key.Independence from PR #7105
The
package/write guard (PACKAGE_KEY_PREFIX) referenced in the issue is not on main — it arrives with open PR #7105, which explicitly nameskiro-user/andkiro-workspace/as the untracked follow-up. This change was designed fresh against main and touches only these two prefixes with a separateREADONLY_SKILL_KEY_PREFIXEStuple, introducing noPACKAGE_KEY_PREFIXreference. The two changes can land in either order without a symbol or logic collision.Testing
python -m pytest test/test_skill_write_territory_guard.py test/test_dashboard_pinned_write_migration.py test/test_skill_browser.py -n0 -q=> 114 passed.skills.*), while the 4 scoping/GET regression tests stay green; restoring the guard returns all 10 to green.prompts.py.code).Non-blocking notes
content-required checks, so a reserved-prefix create with a malformed body reportscontent_requiredfirst (cosmetic; the write is still refused).kiro-workspace/with no leaf sanitises tokiro-workspaceand resolves to a harmless non-territory file (not a shadowing hazard, since the reader only treatskiro-workspace/<leaf>as a territory).Pattern harvest
Rule candidate: AUTOSDE
recurring-defect-patternsPattern: one key space where the READ verb resolves the name through a
root-selecting indirection (per-session, per-machine, or per-provider) while the
WRITE verbs hand the same raw name to a writer that joins it onto one fixed
root. The two verbs then name different files for a single key, so an edit or
delete lands somewhere the reader was never shown and reports success. Flag a
changed line that adds or extends a mutating handler over a key space whose read
path is root-selecting, unless the write path resolves the root the same way or
refuses the prefix outright.
Why it generalises rather than being a one-off: this key space has three sibling
territories resolved by the same root-selecting reader (
kiro-user/,kiro-workspace/,package/) and one fixed-root writer behind all of them, soeach prefix is an independent instance of the same defect — which is exactly how
this one survived the first pass. The shape is not skill-specific: any endpoint
family that resolves reads through a session-scoped root and writes through a
global one has it.