From 831c62fc628905f63a82d43886af65fb61a766d9 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:50:41 +0000 Subject: [PATCH 1/2] fix(site): include owner context in Agents org picker permission check The per-org authcheck behind the Agents org picker checked chat:create without owner_id, but the agents-access role grants chat permissions at org-member scope, which requires the checked object to be owned by the caller. Every org check returned false, so the picker never rendered and the workspace attach list stayed filtered to the default org, showing "No workspaces found" for users whose workspaces live in another org. Pass owner_id: "me" so the per-org check matches the page-level createChat check semantics. --- .../components/AgentCreateForm.stories.tsx | 37 ++++++++++++++++++- .../AgentsPage/components/AgentCreateForm.tsx | 2 +- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx b/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx index 953e4abcef2..8639fb3f430 100644 --- a/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx +++ b/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx @@ -28,7 +28,7 @@ import { AgentCreateForm } from "./AgentCreateForm"; const permittedOrgsKey = [ "organizations", "permitted", - { object: { resource_type: "chat" }, action: "create" }, + { object: { resource_type: "chat", owner_id: "me" }, action: "create" }, ]; const modelConfigID = "model-config-1"; @@ -1104,3 +1104,38 @@ export const PermittedOrgsResolvesToSubset: Story = { expect(options.organizationId).toBe(MockOrganization2.id); }, }; + +/** + * Member-scoped roles like agents-access grant chat:create only on + * chats the user owns, so the per-org check must carry owner context + * for the picker to render. + */ +export const MemberScopedPermissionsShowOrgPicker: Story = { + parameters: { + showOrganizations: true, + organizations: [MockDefaultOrganization, MockOrganization2], + }, + beforeEach: () => { + spyOn(API, "getOrganizations").mockResolvedValue([ + MockDefaultOrganization, + MockOrganization2, + ]); + spyOn(API, "checkAuthorization").mockImplementation(async ({ checks }) => + Object.fromEntries( + Object.entries(checks).map(([id, check]) => [ + id, + check.object.owner_id === "me", + ]), + ), + ); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const picker = await canvas.findByRole( + "button", + { name: /^Organization:/ }, + { timeout: 3000 }, + ); + expect(picker).toBeInTheDocument(); + }, +}; diff --git a/site/src/pages/AgentsPage/components/AgentCreateForm.tsx b/site/src/pages/AgentsPage/components/AgentCreateForm.tsx index e70a0914eee..51032010dda 100644 --- a/site/src/pages/AgentsPage/components/AgentCreateForm.tsx +++ b/site/src/pages/AgentsPage/components/AgentCreateForm.tsx @@ -461,7 +461,7 @@ export const AgentCreateForm: FC = ({ const permittedOrgsQuery = useQuery({ ...permittedOrganizations({ - object: { resource_type: "chat" }, + object: { resource_type: "chat", owner_id: "me" }, action: "create", }), enabled: showOrganizations, From 6635b94ed66a00e24eb63b9df14912142345b3d8 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 12 Aug 2026 15:53:33 +0000 Subject: [PATCH 2/2] fix(site): address Codex review on org picker regression story Export permittedOrganizationsKey from api/queries/organizations and use it in the stories instead of hand-building the private query key (FE7). Exercise the picker in the regression story: open it, assert both orgs are offered, and select the non-default org (FE1). --- site/src/api/queries/organizations.ts | 8 ++++++- .../components/AgentCreateForm.stories.tsx | 24 +++++++++++++------ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 1dcaac36596..6caf0d6f958 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -294,6 +294,12 @@ export const provisionerJobs = ( }; }; +export const permittedOrganizationsKey = (check: AuthorizationCheck) => [ + "organizations", + "permitted", + check, +]; + /** * Fetch organizations the current user is permitted to use for a given * action. Fetches all organizations, runs a per-org authorization @@ -301,7 +307,7 @@ export const provisionerJobs = ( */ export const permittedOrganizations = (check: AuthorizationCheck) => { return { - queryKey: ["organizations", "permitted", check], + queryKey: permittedOrganizationsKey(check), queryFn: async (): Promise => { const orgs = await API.getOrganizations(); const checks = Object.fromEntries( diff --git a/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx b/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx index 8639fb3f430..ada70dee893 100644 --- a/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx +++ b/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx @@ -9,6 +9,7 @@ import { within, } from "storybook/test"; import { API } from "#/api/api"; +import { permittedOrganizationsKey } from "#/api/queries/organizations"; import type * as TypesGen from "#/api/typesGenerated"; import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog"; import { MockChatModelConfig } from "#/testHelpers/chatModels"; @@ -24,12 +25,10 @@ import { } from "../utils/reasoningEffort"; import { AgentCreateForm } from "./AgentCreateForm"; -// Query key used by permittedOrganizations() in the form. -const permittedOrgsKey = [ - "organizations", - "permitted", - { object: { resource_type: "chat", owner_id: "me" }, action: "create" }, -]; +const permittedOrgsKey = permittedOrganizationsKey({ + object: { resource_type: "chat", owner_id: "me" }, + action: "create", +}); const modelConfigID = "model-config-1"; const claudeModelConfigID = "model-config-claude"; @@ -1136,6 +1135,17 @@ export const MemberScopedPermissionsShowOrgPicker: Story = { { name: /^Organization:/ }, { timeout: 3000 }, ); - expect(picker).toBeInTheDocument(); + await userEvent.click(picker); + await screen.findByRole("option", { + name: MockDefaultOrganization.display_name, + }); + await userEvent.click( + screen.getByRole("option", { name: MockOrganization2.display_name }), + ); + expect( + canvas.getByRole("button", { + name: `Organization: ${MockOrganization2.display_name}`, + }), + ).toBeInTheDocument(); }, };