fix: gate agent-spec mutating endpoints on owner auth (#4944) - #5011
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 Real privilege-escalation gap closed at the correct boundary, reusing the shipped owner predicate, with drift-proof route-enumeration coverage and an honest, tracked scope cut. [DESIGN-REVIEWED] 79ceb08 |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of I've read the contract, the intent file, and the patch, and verified the surrounding code: the First-Principles-Verdict: CONCERNS The fix is real and derived, but What this change shipsIntent: stop any authenticated non-owner from rewriting the machine-global agent store — a FIX (closes #4944).
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 79ceb08 |
b0887bd to
3b8e438
Compare
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsThe change is a clean, well-scoped security hardening. The discovery pass found no candidates, and my own falsification confirms:
No grounded defect at 80+ in the changed lines, and nothing new to add. No findings. [OPUS-REVIEWED] 79ceb08 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
|
Dispositions for the First Principles CONCERNS on
|
UX Review (Fable 5) — ⏭️ skippedRevision |
Ten mutating handlers in dashboard/handlers/agents.py wrote to the machine-global agent store (~/.kiro/agents/ or cfg.agents) without checking is_owner_dashboard_request(request), so an app token or any authenticated non-owner dashboard subject could drive them. A write there installs tool grants and MCP server commands that every later session on the host executes, and the same predicate already gates the sibling surfaces (mcp_apps.py, handlers_instances.py, handlers_cloud.py, chat_handlers.py) - this closes the completeness gap in that existing owner-only boundary. A shared _require_owner() helper (mirroring the mcp_apps.py denial shape: SEL log_api_access outcome="denied", 403 "owner authorization required" before any body parse or write) now guards every mutating route in the module: - api_agent_config (PUT) and api_default_agent (PUT) - api_capability_mcp_install / _uninstall - api_capability_skills_install / _uninstall - _mutate_agent_package (single chokepoint covering the capability/agents install + uninstall wrappers) - api_capability_plugins_sync - api_agent_detail (DELETE and PATCH; GET stays open) - api_kirocrew_agents_create / _update / _delete - api_kirocrew_agents_sync The last four routes beyond the issue's list (plugins/sync, agents/sync, agent delete, agent-detail PATCH) mutate the same machine-global state and share the threat model, so they are gated too - the issue's own "decide as one change so a reviewer can check the set is complete". test/test_agents_endpoints_owner_auth.py walks the real route registrars and asserts every mutating verb resolving to this module refuses non-owner subjects and app tokens with 403 (proven red on base), plus a sanity floor so the enumeration cannot go vacuous and a read-paths-stay-open pin. Existing handler tests gain an autouse owner-identity fixture, since the boundary now has its own dedicated coverage. learn-cron-dashboard.md documents the boundary in the same commit. Closes #4944 The owner-gate SEL audit runs off the event loop (asyncio.to_thread, awaited at all 13 call sites): the FIRST sel() of a process constructs the log (trust-dir creation, key validation, on Windows an icacls subprocess), so a fresh gateway whose first mutating request is non-owner would otherwise stall every request before returning the 403. Same pattern as connections.py's pre-response audit.
022d1c2 to
79ceb08
Compare
|
Review-ready again at The GPT 5.6 blocking finding (synchronous SEL audit in All checks green, GPT ✅, Opus ✅, Design ✅; First Principles CONCERNS remains advisory (dispositioned in #5014). |
… (kirodotdev#5011) Ten mutating handlers in dashboard/handlers/agents.py wrote to the machine-global agent store (~/.kiro/agents/ or cfg.agents) without checking is_owner_dashboard_request(request), so an app token or any authenticated non-owner dashboard subject could drive them. A write there installs tool grants and MCP server commands that every later session on the host executes, and the same predicate already gates the sibling surfaces (mcp_apps.py, handlers_instances.py, handlers_cloud.py, chat_handlers.py) - this closes the completeness gap in that existing owner-only boundary. A shared _require_owner() helper (mirroring the mcp_apps.py denial shape: SEL log_api_access outcome="denied", 403 "owner authorization required" before any body parse or write) now guards every mutating route in the module: - api_agent_config (PUT) and api_default_agent (PUT) - api_capability_mcp_install / _uninstall - api_capability_skills_install / _uninstall - _mutate_agent_package (single chokepoint covering the capability/agents install + uninstall wrappers) - api_capability_plugins_sync - api_agent_detail (DELETE and PATCH; GET stays open) - api_kirocrew_agents_create / _update / _delete - api_kirocrew_agents_sync The last four routes beyond the issue's list (plugins/sync, agents/sync, agent delete, agent-detail PATCH) mutate the same machine-global state and share the threat model, so they are gated too - the issue's own "decide as one change so a reviewer can check the set is complete". test/test_agents_endpoints_owner_auth.py walks the real route registrars and asserts every mutating verb resolving to this module refuses non-owner subjects and app tokens with 403 (proven red on base), plus a sanity floor so the enumeration cannot go vacuous and a read-paths-stay-open pin. Existing handler tests gain an autouse owner-identity fixture, since the boundary now has its own dedicated coverage. learn-cron-dashboard.md documents the boundary in the same commit. Closes kirodotdev#4944 The owner-gate SEL audit runs off the event loop (asyncio.to_thread, awaited at all 13 call sites): the FIRST sel() of a process constructs the log (trust-dir creation, key validation, on Windows an icacls subprocess), so a fresh gateway whose first mutating request is non-owner would otherwise stall every request before returning the 403. Same pattern as connections.py's pre-response audit. Co-authored-by: Joe Guo <zejiangg@amazon.com>
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. |
Problem / Motivation
Ten mutating HTTP handlers in
src/kiro_crew/dashboard/handlers/agents.pychange the machine-global agent store (~/.kiro/agents/andcfg.agents) without first callingis_owner_dashboard_request(request). Any authenticated non-owner dashboard subject — including an app-scoped token — could rewrite an agent spec (PUT /api/agent/config), switch the default agent, install/uninstall MCP servers, skills, and agent packages, sync plugins, or create/update/delete Kiro Crew agents.Why it matters
An agent-spec write installs tool grants and MCP server commands that every later session on the host executes — it is a privilege-escalation surface, not a preference toggle. The same predicate already gates the sibling owner-only surfaces (
mcp_apps.py,handlers_instances.py,handlers_cloud.py,chat_handlers.py), so these routes were a completeness gap in an existing security boundary. PR #2383 added the check to its two NEW installed-agent routes but deliberately left the pre-existing handlers alone, which is why they were still open.What changed (motivation → approach → change)
Symptom: mutating agents-module routes answer any authenticated subject. Root cause: the owner boundary was enforced per-surface, and this module predates it. Change: a shared
_require_owner(request, operation)helper — mirroring the reference denial inmcp_apps.api_mcp_apps_call(SELlog_api_access(..., outcome="denied"), then a 403{"error": "owner authorization required", "code": "owner_only"}) — now runs at the top of every mutating handler in the module, before any body parse or write:api_agent_config(PUT) andapi_default_agent(PUT)api_capability_mcp_install/_uninstallapi_capability_skills_install/_uninstall_mutate_agent_package(single chokepoint covering the capability agents install + uninstall wrappers)api_capability_plugins_syncapi_agent_detail(DELETE and PATCH; GET stays open)api_kirocrew_agents_create/_update/_deleteapi_kirocrew_agents_syncThe four routes beyond the issue's list (plugins/sync, agents/sync, agent delete, agent-detail PATCH) mutate the same machine-global state and share the threat model, so they are gated in the same change so a reviewer can check the MODULE's set is complete. The boundary here is deliberately module-scoped: ~13 mutating routes in other handler modules (
mcp.py,mcp_custom.py,mcp_discover.py,core.py) write the same machine-global state and remain open — several are reachable from internal loopback (X-Internal-Secret) callers and need a per-routeinternal_authcarve-out decision, so that sweep is tracked separately in #5014 rather than implied closed here. Identity comes from the token-auth middleware claims (request["user"]/request["app"]), never from client-set headers, and no internal loopback (X-Internal-Secret) caller reaches these endpoints, so no legitimate caller regresses. Thelearn-cron-dashboard.mdmodule spec documents the boundary in the same commit.Tests
test/test_agents_endpoints_owner_auth.py(new) — enumerate-the-invariant coverage: walks the real route registrars and asserts every mutating verb resolving to this module refuses (a) a non-owner subject and (b) an app-scoped token with a 403 carryingcode: owner_only, before any body parse (requests deliberately carry no JSON body, so a handler that parsed first would answer 400, not 403). Proven red on base. A sanity floor pins the 15 known mutating routes so the walk cannot go vacuous, and a read-path test pins that GETs stay open.Manual verification
N/A — unit coverage sufficient: the enumeration test exercises every gated route through a real aiohttp TestServer, and the denial shape byte-matches the already-shipped
mcp_apps.pyreference site.Related Issues
Closes #4944
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)