refactor: migrate inline owner-denial sequences to shared helper - #6245
Conversation
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Sound two-tier consolidation; the new invariant's safety net covers only 2 of 13 registrars, and a confirmed non-owner-deletable route ships open. Watch
Suggestions
[DESIGN-REVIEWED] 0ff834a |
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: |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All facts verified. Writing the review. First-Principles-Verdict: CONCERNS The PR ships What this change shipsIntent: stop nine handler modules from each hand-copying the owner-denial sequence, and close four ungated mutating routes — an ADDITION (consolidation + new gates) despite the
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 0ff834a |
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: |
d538875 to
2733d36
Compare
2733d36 to
4cc7d1a
Compare
1cb2178 to
93aa792
Compare
93aa792 to
0ff834a
Compare
|
🤖 Response to Design Review and First Principles Review on Adopted — the Adopted — the same-file sibling First Principles caught. Adopted — the tautological assertion. Declined, with reasoning — Declined, with reasoning — deleting Noted as follow-up — collapsing the two tiers into one entry point (a custom-audit callable on the async helper, per Design Review's suggestion). Agreed that it would remove the predicate + comment boilerplate, and agreed it does not belong here: it changes the shared helper's signature while nine call sites are mid-migration. |
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. |
Summary
Closes #6224.
Migrates the remaining inline owner-denial sequences (predicate → off-thread SEL
audit → stale-session 401 relabel → 403
owner_only) to a two-tier sharedhelper in
_shared.py, adds owner gates to 4 previously-unguarded mutatingroutes, and introduces a registrar-walk invariant test that catches future
ungated routes without filtering by handler module.
Changes
Shared helpers (
src/kiro_crew/dashboard/handlers/_shared.py)require_owner_dashboard_request(request, operation)— full async lifecycle:predicate → off-thread SEL audit → stale-session 401 relabel → 403
_owner_denial_response(request, error_message, error_code)— the sync tailfor domain wrappers that keep their own audit and/or error code
Inline sequences migrated
Two tiers, and which one a site gets is decided by whether its audit record or its
wire body is domain-specific — not by preference:
Full delegation (generic
non_owner_blockaudit, standardowner_onlybody):handlers/agents.py—_require_ownerhandlers/mcp_apps.pyTail only (
_owner_denial_response, so the module keeps its own audit recordand its own error code — delegating these would have changed either the SEL
payload or the wire body):
handlers/ask_question.py— keepsresources="/api/ask-question"+ itserrorreason, and its
{"error": "forbidden", "code": "owner_only"}bodychat_handlers.py— keepserror="not the dashboard owner"and itsforbiddenbody (now additionally coded
owner_only)handlers_instances.py— both sites (api_instances_search_sessionsand itssame-file sibling
api_instances_proxy) keep_audit(...), which emitslog_tool_invocationunderinstances_*/dashboard:instances— the stream thismodule's SEL consumers watch — and keep their own messages ("federated session
search is owner-only" / "remote-crew proxy is owner-only")
handlers/aws_consent.py,handlers/messaging.py,handlers_cloud.py,apps/builtins/aws_control/backend/routes.py— each keeps its own auditOne site deliberately NOT migrated
source_providers._authorize_owner_requestkeeps its inline tail. It is not thesame four-step shape: it has four separate denial branches and only the
caller != owner_idone reaches the stale relabel, its body is a bare{"error": "forbidden"}with nocode, and that exact body is pinned by equalityassertions in two test files. Routing it through
_sharedwould also have themodule that DEFINES
stale_owner_session_responsecall back into the module thatimports it. Adding a
codethere is a wire change that belongs in the error-codetrack, not in this consolidation.
New owner gates (4 routes)
PUT /api/dashboard/config(files.py) — the gate fires BEFORE the config-load I/OPOST /api/mcp/oauth/relay,POST /api/connections/mint,POST /api/connections/cancel(connections.py)Registrar-walk invariant test (
test/test_agent_config_owner_gate_invariant.py)Walks ALL mutating routes from both registrars (no handler-module filter) and
asserts a non-owner gets 401/403 on every route outside the two documented
exclusion sets. A coherence floor prevents a vacuous pass, and
_MAX_KNOWN_UNGATED_ROUTESmakes the debt list a shrink-only ratchet: addinga route to it requires raising the ceiling in the same diff.
It paid for itself on its first run — it surfaced 4 mutating routes with no
owner gate that were documented nowhere:
POST/PUT/DELETE /api/workspaces{,/{name}}and
POST /api/members/{slug}/thread.api_workspaces_deleteremoves a workspacefrom
config.jsonfor any authenticated non-owner (e.g. the dashboard token mintedfor an allow-listed Slack user, which has an empty app identity). Gating them
changes WHO may manage workspaces — a product decision, not this migration — so
they are recorded with a justification and tracked in #6470.
Test-fixture updates (why 14 test files are touched)
PUT /api/dashboard/configand the three connections routes are now gated, and thegate reads
request.app["state"]plus the authenticated claims. Fixtures thatregistered those handlers on a bare
web.Application()answered 500 (nostate)or 403 (no owner claim) before the branch each test names.
test/dashboard_owner_helpers.pyinstalls exactly what the gate needs (
as_owner(app)), and only when the app has nostateof its own, so a fixture carrying a realDashboardStatekeeps it. The gate'sown behaviour is tested directly, not incidentally, in
test_dashboard_files_coverage.TestDashboardConfigPutOwnerGate.error-code-baseline.jsonRegenerated via the sanctioned
python test/test_error_code_contract.py --update.Both directions improve:
missing_code1312 → 1310 (two prose-only refusals becamecoded by routing through the shared tail) and
_compliant1285 → 1294. Theper-module ratchet in
test_ask_question_roundtrip.pydrops 21 → 20 because thescanner is per-file and that one coded site now lives in
_shared.py— the wirecontract is unchanged.
Verification
24cb5e76c; squashed to one commitflake8,isort,mypy,scripts/check_black_formatting.py,scripts/check_brand_name.py(diff-scoped) all cleanreferences the changed symbols or the four newly-gated endpoints
maincontrol from the same worktree