fix(dashboard): show public-repo PR/MR chip status to any dashboard user - #6789
Conversation
f8e0bce to
f58c917
Compare
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsFINDING -- src/kiro_crew/dashboard/handlers/source_providers.py:5643 -- after a cached-public repo becomes private on its first post-TTL refresh, |
Design Review (Fable 5, fork) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Sound public-visibility gate, but it side-steps the stated root cause and buys a two-cache coherence problem that already took 15 rounds of race patches. Watch
Suggestions
[DESIGN-REVIEWED] d7d2f20 |
First Principles Review (Fable 5, fork) — 🟡 CONCERNSPremise-level review of I have enough to write the review. Key finding confirmed: the codebase already recognizes the exact root cause the PR names — a pre-owner session stuck on a First-Principles-Verdict: CONCERNS #6786 is an owner's stale-subject session misclassified as non-owner; this ships a public-repo visibility subsystem for all non-owners and leaves that reporter's private-repo chips still bare. What this change shipsIntent: restore/expand PR-MR chip status for authenticated dashboard users — typed
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] d7d2f20 |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed Review detailsI've verified enough. The base file confirms the codebase convention: Assessing the three candidates:
Neither surviving finding is a security boundary, crash, or data-loss — both are advisory. No Step-2 additions warranted; the security gate logic is fail-closed and extensively tested. Two advisory findings: new per-key bookkeeping dicts on the hot broadcast/refresh path lack the eviction every sibling FINDING — src/kiro_crew/dashboard/state.py:2214 — FINDING — src/kiro_crew/dashboard/handlers/source_providers.py:5453 — [OPUS-REVIEWED] d7d2f20 |
f58c917 to
c30a1c7
Compare
|
Thanks — dispositions on the current head GPT 5.6 (BLOCKING) — fixed
Design / First-Principles (advisory CONCERNS)
|
c30a1c7 to
b919781
Compare
b919781 to
4caa6a3
Compare
4caa6a3 to
236a487
Compare
GPT 5.6 — addressed on
|
Head branch was pushed to by a user without write access
bc87713 to
c549e75
Compare
GPT 5.6 — addressed on
|
GPT 5.6 — addressed on
|
GPT 5.6 — addressed on
|
GPT 5.6 — addressed on
|
GPT 5.6 — addressed on
|
GPT 5.6 — addressed on
|
GPT 5.6 — both addressed on
|
GPT 5.6 + Opus 4.8 — all addressed on
|
GPT 5.6 round-13 — respectfully declining this one (design disagreement)This finding asks to "gate visibility refreshes on owner status across HTTP, WebSocket, and turn-boundary paths." I'm not applying it, because — unlike rounds 10–12, which were genuine status-read leaks I fixed — this prescription removes the feature this PR exists to add (issue #6786), and its premise does not hold up: 1. The visibility probe is the feature's foundation, not a leak. To show a non-owner a PUBLIC-repo chip, the system must first learn the repo is public. That determination IS 2. It reads visibility METADATA only — no private content. The call requests exactly 3. The quota/abuse concern is already bounded. If the maintainers' security posture is that a non-owner must never cause ANY operator-credentialed provider call — even a bounded visibility-flag read — then the correct resolution is to close #6786 as won't-fix, not to merge a feature gutted to a no-op. I've surfaced this to the repo owner for a decision rather than churn. Opus 4.8 passed with no blocking findings on this same head; Design / First-Principles are advisory CONCERNS; UX skipped (no UI surface); CI is fully green. |
GPT 5.6 round-13 — resolved by code on
|
GPT 5.6 round-14 — addressed on
|
GPT 5.6 round-15 — addressed on
|
Sidebar PR/MR chips dropped their lifecycle status (merged/closed/open/ draft) and CI glyph for any connection not classified as the configured owner -- even for PUBLIC repos, whose status is already world-visible on the provider website. Gate chip status on repo visibility in addition to owner identity: the owner sees status for any repo; an authenticated dashboard user sees it for a KNOWN-public repo; private/unknown/stale repos stay owner-only (fail closed) and app tokens are stripped of status entirely. - Add a per-repo public/private visibility cache + provider fetch + scheduler in source_providers, refreshed on the same cadence as chip status. A forced turn-boundary refresh SYNCHRONOUSLY invalidates a cached public flag before the concurrent refresh starts and restores it only on positive reconfirmation, so fresh status can never be broadcast against stale-public visibility (no non-owner private leak). is_repo_public TTL-expires stale entries; a failed refresh never extends a stale public flag; visibility refresh pushes a slots update when a repo's public flag flips. GitLab visibility uses the full quoted project path so subgroup repos resolve. - Thread a dashboard_user gate through _project_source_links and the serialize_slots / to_dict / source_links_payload chain; project only an explicit chip-status key allowlist so a widened cache cannot leak a new field into any frame. - Public-repo status rides ONLY the WS dashboard-user frame; the general broadcast list and the unfiltered SSE stream stay bare, and app-token frames are stripped in filter_slots_for_app, so no app scope receives credential-backed status via any transport. - Run the periodic check + visibility refresh driver for any dashboard connection, not only the owner.
Problem / Motivation
Sidebar session rows render PR / MR / issue chips, but the lifecycle status (merged / closed / open / draft) and the CI rollup glyph are stripped for any dashboard connection not classified as the configured owner — even when the repository is public, where that same status is world-visible on the provider's website.
The status pipeline was double-gated on
is_owner_dashboard_request:dashboard/ws.py— the periodic chip refresh loop and connect-time refresh ran only for an owner connection, so a non-owner never populated the chip-status cache.dashboard/state.py— the status-bearing slots frame was sent only to owner WS clients.is_owner_dashboard_requestreturns true only when the token subject equalsowner_id. A dashboard session minted beforeKIROCREW_OWNER_IDwas configured carries alocal-app/local-startupsubject for its whole life (the token re-mints from the incoming subject on refresh), so once an owner exists that session is denied owner status and every chip renders as a bare number — uniformly across GitHub, GitLab, and Jira.Why it matters
For a public repo the PR's merge/close/CI state is not sensitive — it is visible to anyone on the provider website. Withholding it from a legitimate authenticated dashboard user provides no confidentiality benefit while removing the single most useful signal the chip carries (is this PR merged / closed / green). The symptom reads as a regression even though the chip code never changed — what changed was the session's relationship to
owner_id.What changed (motivation → approach → change)
Symptom: public-repo PR/MR chips lose their status for a non-owner dashboard session. Root cause: status is gated purely on connection-owner identity, never on whether the underlying repo is public. Change: gate chip status on repository visibility in addition to owner identity.
Implementation:
public/private/unknown) inhandlers/source_providers.py, keyedprovider|host|owner|repo, with a long TTL, a bounded fire-and-forget fetch (gh repo view --json isPrivate/glab api projects/...— GitLabinternalis treated as non-public), inflight dedup, and a fail-closed readeris_repo_public.dashboard_usergate threaded through_project_source_linksand theserialize_slots/to_dict/source_links_payloadchain.filter_slots_for_appso no app scope can receive it.Tests
test/test_public_repo_chip_status.py(16 tests):is_repo_publicreader; GitHub/GitLab visibility fetch + fail-closed on error; refresh keeps prior known value on failure; scheduler repo-dedup + TTL + issue/Jira skip; and the full_project_source_linksgate matrix — owner/private, dashboard-user/public, dashboard-user/private, dashboard-user/unknown (fail-closed), app-token, and issue link.test/test_dashboard_state_ws.py: public-repo status now rides the general/SSE frame; owner still gets the dedicated full-status frame; a new test proves app-token frames are stripped of status; the refresh-loop tests now assert app tokens never start the driver while a non-owner dashboard user does (with visibility refresh scheduled).Manual verification
N/A — unit coverage is sufficient. The change is a backend gate with no new UI; the gate matrix, the broadcast routing (owner / dashboard-user / app-token), and the visibility cache are all exercised by the automated tests above. Local gates all green:
pytest(touched suites),isort,flake8,mypy src/kiro_crew/, and the baselinedblackgate.Related Issues
Fixes #6786
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)Pattern harvest
Rule candidate: semgrep
Pattern: when a permission gate is widened from owner-only to a broader audience (here a non-owner dashboard user, gated on repo visibility), EVERY code path that acts on the gated resource must apply the same gate — not only the data-serving path but every scheduling-time credentialed side-read it triggers. In this PR the identical non-owner-drives-credentialed-read-on-private-repo leak reappeared at four distinct sites (
ws.pyconnect scheduler,ws.pyperiodic refresh loop,chat_handlers.pyGET /api/chat/slots,state.pyturn-completion refresh) because the visibility gate was added to the render/serialize path but each independentschedule_check_refresh/request_check_refresh_nowcall site had to be gated separately. Generalized rule: flag a call that fans a user-reachable request out to an operator-credentialed subprocess/API read (schedule_check_refresh,request_check_refresh_now,gh/glabinvocations) when it is reachable by a non-owner principal but not guarded by the same positive authorization predicate (is_repo_public(url) is True/ owner check) that gates the corresponding response projection. Companion rule: a positive authorization GRANT (not only a denial) must emit an SEL audit event.