Skip to content

fix(mobile): show a no-access state for an organization the reader lost - #8493

Merged
iscekic merged 7 commits into
mainfrom
drain/security-agent-lost-org-access
Oct 22, 2026
Merged

iscekic merged 7 commits into
mainfrom
drain/security-agent-lost-org-access

Conversation

@iscekic

@iscekic iscekic commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

What changed

A reader whose membership in the selected organization is gone saw a first-run GitHub setup instead of a statement that access was lost. Two paths produced it:

  1. getProfileAgentScope substituted the personal scope when the settled membership list no longer contained the stored organization, so Profile → Security Agent opened the personal landing and offered "Connect GitHub to get started".
  2. Entering the organization's scope directly (deep link, notification, or before the list refetched) showed only the generic "Could not load Security Agent" error with Retry, and with cached data it rendered the stale organization dashboard with inline load errors.

Changes, all mobile:

  • apps/mobile/src/lib/profile-agent-navigation.ts — the stored organization is kept once the membership list settles, even when the list no longer contains it. Both agent rows now reach the organization scope, which explains the loss instead of switching product scope. Loading/refreshing still yields undefined (rows disabled, unchanged).
  • apps/mobile/src/lib/organization-scope-access.ts (classifyOrganizationScopeAccess) — one shared admission, decided by the organization scope's own query errors rather than by the client's membership list: a permission loss denies, NOT_FOUND reports the unavailable state, anything else keeps the caller's own loading or retryable-error state. The server is the authority on access (a Kilo admin reaches any organization without a membership row, and a parent owner, admin or billing manager reaches each child organization), so absence from the list is never treated as proof. The error is read from each query's error, not from isError, so cached data cannot outvote a denial. A personal scope is never denied. The permission-loss definition moves to apps/mobile/src/lib/trpc-error.ts (isPermissionDeniedError) and is shared with the query cache, so a session-level UNAUTHORIZED (authRequired) stays an ordinary load failure, and shaped server errors are read too.
  • apps/mobile/src/components/organization/scope-access-denied-screen.tsx — the shared surface: QueryError with the permission variant ("Access denied"), or the not-found variant for a deleted organization, plus two recoveries: Retry re-resolves membership and the scope's queries in place, and Back to profile leaves the stale selection. Copy reuses the existing organization.boundary.* and queryError strings, so no locale catalog changes.
  • apps/mobile/src/components/security-agent/scope-entry-screen.tsx + scope-entry-render.ts — new denied landing state, placed after loading and before the generic error and every setup gate. No install-state token is minted for a denied scope.
  • apps/mobile/src/components/code-reviewer/platform-list-screen.tsx — the same admission for the Code Reviewer organization landing; Retry refetches membership and all three platform statuses.
  • apps/mobile/src/components/platform-error-screen.tsx — accepts a caller-composed action row (the two-button recovery).

States covered by the Security Agent landing: first run without an integration still gets the setup, loading still gets the skeleton, a denied scope gets the no-access surface, a session-level unauthorized response and other failures keep the retryable server-error screen, and a connected enabled organization still gets the dashboard. The Code Reviewer landing keeps the organization name as its header when the membership list still supplies it and falls back to the generic "Organization" title once it does not.

Verification

Unit and mounted suites (VM, local Postgres): organization-scope-access, scope-entry-render, scope-entry-screen (mounted, mocked tRPC transport: denied scope instead of setup, a single denied query overriding cached permission/config data, Retry after access is restored, personal scope unaffected, session-level unauthorized staying a load failure), scope-access-denied-screen (variant selection, Retry, Back to profile destination), platform-list-screen (denied surface, title fallback, Retry fan-out, granted rows), profile-agent-navigation, and the existing profile-screen suites. The full mobile suite passes (1259 files, 18637 tests, on the head with main merged in). tsc --noEmit clean, oxlint clean on the changed files, knip clean.

End-to-end on the Android emulator against the local dev stack, with an owned fake account and a seeded organization plus GitHub integration:

  1. Connected organization, membership present: Profile → Security Agent renders the organization dashboard.
  2. Membership removed, app cold-started so the membership list refetches and every scope query is answered with a denial: the stored organization still selected, Profile → Security Agent now renders "Access denied / You do not have access to this organization." with Retry and Back to profile; Back to profile returns to the Profile tab.
  3. Membership restored, Retry pressed: the organization dashboard comes back in place.
  4. Same state on the Code Reviewer row: the organization landing renders the same no-access surface under the CODE REVIEWER eyebrow instead of three unavailable platform rows.
  5. Before the change, on the same data, the same path rendered "Connect GitHub to get started" with an "Install GitHub App" button.

Visual proof is attached in a comment on this pull request.

After merging main into the branch, the Android run was repeated on the merged head with the same fixture: the denied state appears exactly as above, and Retry after the membership was restored returns the organization landing. The merged head also keeps a regression test for the class invariant: an organization missing from the membership list alone still reaches its landing, because access is decided by the organization's own queries.

Unproven by this run: the not-found variant against a soft-deleted organization (unit-tested only), and iOS (no iOS-specific code in this change; proof is on Android).

Proxy answers

The owner's proxy gave these answers in the owner's place.

  1. Question: Profile row: Reproduced: after the selected org is lost, Profile's Security Agent row silently resolves to the personal scope and shows the personal first-run 'Connect GitHub to get started' setup. What should the row do when the selected organization is no longer a membership?
    Answer: Fallback for both products
    Confidence: 0.50 · Precedent: Fix the root cause and the class of the issue on every affected surface: "fix this for all shared services model pickers on all surfaces"; the shared getProfileAgentScope silently switches Code Reviewer to the personal scope too
  2. Question: Landing state: What should the Security Agent org landing show when the organization is not in the user's loaded org list (access lost / org removed)?
    Answer: QueryError permission variant + Retry, plus Back to profile
    Confidence: 0.80 · Precedent: Make plans cover the happy, loading, retryable-error, non-retryable-error, and empty states: listed the four states; the brief also asks for QueryError with the permission variant plus Retry
  3. Question: Cached data: While the org list still caches the lost org (up to ~30 s), re-opening Security Agent currently renders the cached org dashboard with inline 'Could not load repositories' errors. When the agent queries report UNAUTHORIZED/FORBIDDEN, should the landing suppress the stale org data?
    Answer: Yes: denial overrides cached data
    Confidence: 0.80 · Precedent: Fix the root cause and the class of the issue, not the single symptom: "fix class of issue preventing progress"; the brief also says not to show org state after access is lost
  4. Question: Profile row: After membership loss, Profile → Security Agent must reach the no-access state for that org. That requires the shared getProfileAgentScope to keep the lost org id instead of substituting personal (precedent: the shared helper also silently switches Code Reviewer to personal). Keep the lost org id, or keep the personal fallback and only fix the org scope screen?
    Answer: Keep the lost org id
    Confidence: 0.85 · Precedent: Fix the root cause and the class of the issue: "if anything repeats the same mistake over and over again, fix the class"; the personal fallback leaves the explorer's exact path broken.
  5. Question: Code Reviewer: Because the helper is shared, Code Reviewer's org landing for a lost org will show three 'status unavailable' platform rows. Extend the same denied state to Code Reviewer's org landing in this PR, or keep this PR to Security Agent and state the limitation?
    Answer: Both landings
    Confidence: 0.60 · Precedent: Fix the class on every affected surface: "fix this for all shared services model pickers on all surfaces"; and 'if you saw a gap, fix it'. The shared helper change creates the Code Reviewer gap, so fix it here.
  6. Question: Copy: The no-access state needs a title, a description, Retry and Back to profile. Reuse the existing organization-boundary strings (common.accessDenied 'Access denied', organization.boundary.accessDeniedDescription 'You do not have access to this organization.', organization.boundary.backToProfile), or add Security-Agent-specific keys?
    Answer: Reuse existing keys
    Confidence: 0.75 · Precedent: Least work with no drop in quality: "do the least amount of work possible, with no drop in quality"; the existing boundary strings are accurate and consistent across surfaces.
  7. Question: Deleted org: A soft-deleted organization returns NOT_FOUND from the org procedures. Show it in the same no-access surface (not-found variant, same Retry + Back to profile), or leave tonight's generic load-error screen for NOT_FOUND?
    Answer: Same surface, not-found variant
    Confidence: 0.80 · Precedent: Fix the class of the issue: "fix class of issue preventing progress"; a Retry button that can never succeed is a gap to fix.
  8. Question: Review on merged head: the merge conflict was resolved and pushed as a merge commit; CI on that head was green, but the review app created no check run for it, while the previous normal commit was reviewed in minutes. The end-of-session gate needs a completed review of the head. How should this be unblocked?
    Answer: Push a real follow-up commit
    Confidence: 0.72 · Precedent: Autonomy: never accept blocked as an end state; a work item must converge, and the commit must carry a genuine test rather than filler.

The Profile agent rows resolved a stored organization that the settled
membership list no longer contains to the personal scope, so the Security Agent
landing offered its personal first-run GitHub setup under a lost organization's
name. Entering that organization's scope directly showed only a generic load
error.

- Keep the stored organization once the membership list settles, so both agent
  rows reach the organization scope instead of silently changing scope.
- Classify organization scope access from the membership list and the tRPC
  error codes of the scope's own queries, so a denial outranks cached data, and
  render one no-access surface: access-denied or unavailable copy, Retry (which
  re-resolves membership) and Back to profile.
- Use it in the Security Agent and Code Reviewer organization landings.
@iscekic iscekic added drain Work item from the findings drain explorer:android Finding surfaced by the Android explorer labels Oct 21, 2026
@iscekic

iscekic commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Android emulator against the local dev stack, with an owned fake account and a seeded organization plus GitHub integration. The selected organization's membership was removed, the app cold-started so the membership list refetched, then Profile → Security Agent.

  • Before the change, on the same data: the personal first-run GitHub setup appeared under the lost organization (first image).
  • After the change: "Access denied / You do not have access to this organization." with Retry and Back to profile. No setup prompt, no stale organization dashboard.
  • After restoring the membership, Retry brings the organization dashboard back in place.
  • The Code Reviewer row reaches the same surface for the same organization.
  • Back to profile returned to the Profile tab.

Unit and mounted suites cover the state precedence, the denial over cached data, the deleted-organization variant, the Retry fan-out, and the personal scope staying unaffected.

Earlier build on the same data: personal first-run setup under the lost organization

Fixed state: no-access surface with Retry and Back to profile

Retry after restoring the membership returns the organization dashboard

Code Reviewer landing shows the same no-access surface

…cope

A personal scope has no membership to resolve, so the access check must not
add an active membership observer there.
@iscekic iscekic added merge:blocked:kilobot-not-ready Review bot has not recommended merge at this head merge:human Substantive functional risk requires human review. labels Oct 21, 2026
@kilo-code-bot

kilo-code-bot Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The only new changes since 05aa8ae are a regression test asserting that an organization absent from the membership list still reaches the organization landing, plus the origin/main merge resolution that adopts the admin wording in the role-restriction test; the previously reported findings remain fixed on current HEAD.

Assumptions: [INFERENCE] Typecheck and tests were not executed in this read-only review; correctness of the moved isPermissionDeniedError/readTrpcErrorData helpers and of the new mounted test is inferred from reading the current code. The git diff 05aa8ae..HEAD range also contains the origin/main merge, so unrelated main-authored changes in that range were not reviewed; only the PR's own changes were re-checked against base fbae0d5.

Files Reviewed (9 files)
  • apps/mobile/src/components/security-agent/scope-entry-screen.test.ts - new list-absence test and merge-resolved role-restriction cases, no issues
  • apps/mobile/src/components/security-agent/scope-entry-screen.tsx
  • apps/mobile/src/components/security-agent/scope-entry-render.ts
  • apps/mobile/src/components/code-reviewer/platform-list-screen.tsx
  • apps/mobile/src/components/organization/scope-access-denied-screen.tsx
  • apps/mobile/src/components/platform-error-screen.tsx
  • apps/mobile/src/lib/organization-scope-access.ts
  • apps/mobile/src/lib/query-client.ts
  • apps/mobile/src/lib/trpc-error.ts
  • apps/mobile/src/lib/profile-agent-navigation.ts
Previous Review Summaries (3 snapshots, latest commit 05aa8ae)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 05aa8ae)

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental commit 05aa8ae7e deletes the orphaned useOrganizationScopeAccess hook, resolving the prior kilo-app typecheck failure; no references to the hook or its module remain in the repository.

Assumptions: [INFERENCE] The typecheck is not executed in this read-only review; the fix is inferred from the deletion plus a repository-wide search for useOrganizationScopeAccess and use-organization-scope-access returning no matches. Previous suggestions were re-checked on current code: classifyOrganizationScopeAccess now uses isPermissionDeniedError/readTrpcErrorField, the cached-data test denies only getConfig, and the lost-org title fallback is asserted by the mounted test.

Files Reviewed (1 file)
  • apps/mobile/src/lib/hooks/use-organization-scope-access.ts (deleted) - no issues

Previous review (commit 99b6244)

Status: 1 Issues Found | Recommendation: Address before merge

Executive Summary

The access classifier was correctly rewritten to trust the server's denial, but the now-unused useOrganizationScopeAccess hook still calls the old signature, which breaks kilo-app typecheck.

Assumptions: [INFERENCE] the type failure is read from the current signatures (organization-scope-access.ts:16-24 vs. the hook's call) rather than from an executed typecheck, since this review is read-only; previous findings are fixed in this revision (membership inference, readTrpcErrorField, authRequired, and the cached-data test are all addressed).

Overview

Severity Count
CRITICAL 1
WARNING 0
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
apps/mobile/src/lib/hooks/use-organization-scope-access.ts 30 Hook still passes organizationId/membershipIds to classifyOrganizationScopeAccess, which now requires isOrganizationScope; the file is orphaned and fails kilo-app typecheck
Files Reviewed (8 files)
  • apps/mobile/src/components/code-reviewer/platform-list-screen.mounted.test.tsx - no new issues
  • apps/mobile/src/components/code-reviewer/platform-list-screen.tsx - no new issues
  • apps/mobile/src/components/security-agent/scope-entry-screen.test.ts - no new issues
  • apps/mobile/src/components/security-agent/scope-entry-screen.tsx - no new issues
  • apps/mobile/src/lib/hooks/use-organization-scope-access.ts - 1 issue
  • apps/mobile/src/lib/organization-scope-access.test.ts - no new issues
  • apps/mobile/src/lib/organization-scope-access.ts - no new issues
  • apps/mobile/src/lib/query-client.ts - no new issues
  • apps/mobile/src/lib/trpc-error.ts - no new issues

Fix these issues in Kilo Cloud

Previous review (commit 8d49aa7)

Status: 5 Issues Found | Recommendation: Address before merge

Executive Summary

The new no-access flow is well structured, but inferring "lost access" from a membership-id list that omits inherited child organizations (and admins) can replace a landing the server would still authorize.

Assumptions: [INFERENCE] the inherited child-org regression needs a [scope] route/deep-link entry, because the mobile scope picker lists direct memberships only; the authRequired misclassification is a transient window before sign-out; no code was executed and iOS was not verified.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 4
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/lib/hooks/use-organization-scope-access.ts 32 Membership ids omit inheritedChildren, so a parent-role holder on a child org (and Kilo admins) is classified denied although ensureOrganizationAccess authorizes them

SUGGESTION

File Line Issue
apps/mobile/src/lib/organization-scope-access.ts 25 Error-code parsing reads only data.code; reuse readTrpcErrorField for shape.data.code / top-level code
apps/mobile/src/lib/organization-scope-access.ts 48 Every UNAUTHORIZED becomes an org denial, including authRequired session failures
apps/mobile/src/components/code-reviewer/platform-list-screen.tsx 91 Denied header falls back to "Organization" on the lost-membership path; the mounted test asserts a title that path cannot produce
apps/mobile/src/components/security-agent/scope-entry-screen.test.ts 298 Cached-data precedence test also denies the uncached permission query, so it cannot fail on the behavior it names
Files Reviewed (14 files)
  • apps/mobile/src/components/code-reviewer/platform-list-screen.mounted.test.tsx - 1 issue
  • apps/mobile/src/components/code-reviewer/platform-list-screen.tsx - 1 issue
  • apps/mobile/src/components/organization/scope-access-denied-screen.mounted.test.tsx - no new issues
  • apps/mobile/src/components/organization/scope-access-denied-screen.tsx - no new issues
  • apps/mobile/src/components/platform-error-screen.tsx - no new issues
  • apps/mobile/src/components/security-agent/scope-entry-render.test.ts - no new issues
  • apps/mobile/src/components/security-agent/scope-entry-render.ts - no new issues
  • apps/mobile/src/components/security-agent/scope-entry-screen.test.ts - 1 issue
  • apps/mobile/src/components/security-agent/scope-entry-screen.tsx - no new issues
  • apps/mobile/src/lib/hooks/use-organization-scope-access.ts - 1 issue
  • apps/mobile/src/lib/organization-scope-access.test.ts - no new issues
  • apps/mobile/src/lib/organization-scope-access.ts - 2 issues
  • apps/mobile/src/lib/profile-agent-navigation.test.ts - no new issues
  • apps/mobile/src/lib/profile-agent-navigation.ts - no new issues

Fix these issues in Kilo Cloud


Reviewed by deepseek-flash-latest · Input: 148.5K · Output: 12.7K · Cached: 1.2M

Review guidance: REVIEW.md from base branch main

…mbership list

The membership list is not the access rule: a Kilo admin reaches any
organization without a membership row, and a parent owner, admin or billing
manager reaches each child organization, so a scope missing from the list can
still be fully accessible. Inferring denial there hid landings the server would
have served.

Judge access from the scope's own query errors instead, through the shared
tRPC error readers: a permission loss (FORBIDDEN, or a procedure-level
UNAUTHORIZED) denies, NOT_FOUND reports the unavailable state, and a
session-level UNAUTHORIZED stays an ordinary load failure. Cached data cannot
outvote the denial because the error is read whether or not data is present.
The permission-denial reader moves to lib/trpc-error.ts so the cache and the
landing share one definition.
@iscekic iscekic removed merge:human Substantive functional risk requires human review. merge:blocked:kilobot-not-ready Review bot has not recommended merge at this head labels Oct 21, 2026
Access is decided from the scope's own query errors now, so the hook that read
the membership list is gone and both landings call the pure classifier.
@iscekic

iscekic commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Re-ran the Android verification on the final head (05aa8ae7e7, where access is decided by the server's denial rather than the membership list), against the local dev stack with the owned fake account and the seeded organization.

  • Membership removed, app cold-started so every scope query is answered with a denial: Profile → Security Agent renders "Access denied / You do not have access to this organization." with Retry and Back to profile. No setup prompt, no stale dashboard, no install-state token minted.
  • Membership restored, Retry pressed: the organization dashboard returns in place.

The earlier images in this pull request were captured on an intermediate commit. The surface is unchanged; these are the final-head captures.

Final head: no-access surface with Retry and Back to profile for the lost organization

Final head: Retry after restoring access returns the organization dashboard

@iscekic iscekic added the merge:human Substantive functional risk requires human review. label Oct 21, 2026
…lost-org-access

# Conflicts:
#	apps/mobile/src/components/security-agent/scope-entry-screen.test.ts
@iscekic iscekic removed the merge:human Substantive functional risk requires human review. label Oct 22, 2026
@iscekic

iscekic commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Re-ran the Android verification on the merged head (ea87bc1ab0, main merged in) against the local dev stack, with an owned fake account and a seeded organization plus GitHub integration.

  • Membership removed, app cold-started so the scope queries are answered with a denial: Profile → Security Agent renders the no-access state instead of the setup prompt, and no install-state token is minted.
  • Membership restored, Retry pressed: the organization landing returns in place.

Captures from that run.

Security Agent showing the no-access state for the organization

Organization landing after Retry once access was restored

…omits it

Access is decided by the organization scope's own query errors, so a list that no longer contains the organization must not produce the no-access state for an admin or an inherited child scope.
@iscekic iscekic added the merge:human Substantive functional risk requires human review. label Oct 22, 2026
@iscekic
iscekic added this pull request to the merge queue Oct 22, 2026
Merged via the queue into main with commit 4efa496 Oct 22, 2026
53 checks passed
@iscekic
iscekic deleted the drain/security-agent-lost-org-access branch October 22, 2026 20:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

drain Work item from the findings drain explorer:android Finding surfaced by the Android explorer merge:human Substantive functional risk requires human review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants