feat(THU-578): scope all FE DAL methods to active workspaceId#951
Conversation
Semgrep Security ScanNo security issues found. |
PR Metrics
Updated Mon, 08 Jun 2026 13:44:38 GMT · run #1814 |
b2928ae to
2d8f4af
Compare
04c0769 to
af7cc1f
Compare
442f7d0 to
527dcf9
Compare
9e7b109 to
b325483
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b325483. Configure here.
| const nextSessions = new Map(sessions) | ||
| nextSessions.delete(id) | ||
| useChatStore.setState({ sessions: nextSessions }) | ||
| // fall through to full create path |
There was a problem hiding this comment.
Ready flag during workspace rehydrate
Medium Severity
When an existing chat session is removed because the active workspace changed, hydrateChatStore does not set isReady back to false before the async rebuild. The chat UI can stay mounted while the session entry is gone, leading to missing-session errors or a stale chat instance until the new session is created.
Reviewed by Cursor Bugbot for commit b325483. Configure here.
| if (session.chatThread) { | ||
| await updateChatThread(db, session.chatThread.id, { agentId: agent.id }) | ||
| if (session.chatThread?.workspaceId) { | ||
| await updateChatThread(db, session.chatThread.workspaceId, session.chatThread.id, { agentId: agent.id }) |
There was a problem hiding this comment.
Agent persist checks thread workspace
Low Severity
setSelectedAgent only writes agentId to the database when session.chatThread?.workspaceId is truthy, even though each ChatSession already carries a required workspaceId. If the in-memory thread row lacks workspaceId while the session was hydrated with a valid one, the per-thread agent choice updates in memory but never persists.
Reviewed by Cursor Bugbot for commit b325483. Configure here.


Summary
Follow-up to THU-550 (PR #944). PR #944 shipped the BE-side enforcement (every workspace-scoped row has
workspace_id NOT NULL, sync rules are per-workspace, the upload-handler factory rejects cross-workspace writes) plus a partial FE pattern where workspace-scoped tables' DAL inserts auto-resolve the active workspace via an internalgetActiveWorkspaceId(db)call. Reads, updates, and deletes were completely unscoped.This PR replaces the auto-resolution pattern with explicit
workspaceIdthreading on every DAL function — reads, writes, deletes, creates — across all 11 workspace-scoped DAL files. Day-1 risk is low (v1 ships with personal workspaces only, so every user sees exactly oneworkspaceId), but the moment shared workspaces ship, every unfiltered query would become a cross-workspace data leak — a chat search from workspace A returning threads from workspace B. The BE handler factory prevents writes from leaking, but reads/updates/deletes are pure FE concerns; the backend can't help.Convention
Every workspace-scoped DAL function takes
workspaceIdas its second positional arg:eq(table.workspaceId, workspaceId)in every WHERE.workspaceIdmerged into the values payload (replaces the auto-resolve call).Partial<TableType>:workspaceIdstripped from the SET payload so callers can't reassign a row across workspaces by passing a different value.await getActiveWorkspaceId(db)calls insidesrc/dal/**— all resolution moved to call sites.In scope (11 DAL files)
src/dal/chat-threads.tsuser_id = mefiltersrc/dal/chat-messages.tssrc/dal/tasks.tssrc/dal/models.tsworkspaceIdthroughgetDefaultModelForThread→getLastMessagesrc/dal/mcp-servers.tsupdateMcpServerDAL (replaces a rawdb.update(mcpServersTable)insettings/mcp-servers.tsxthat bypassed workspace scoping)src/dal/prompts.tsrunAutomation→getModel/createChatThreadsrc/dal/skills.tssrc/dal/triggers.tssrc/dal/modes.tssrc/dal/model-profiles.tsworkspaceId: nullso the row stays in its workspacesrc/dal/agents.tsagents_system+agents_secretsstay local-only (no workspaceId) — only the syncedagentstable is scopedOut of scope (user/device-scoped, no workspace filter):
settings,devices,integrations, and the workspace-table DAL itself.New foundation helpers (
src/lib/active-workspace.ts)useActiveWorkspaceId(): string | null— React hook. Reads the active user from the trust-domain registry (Zustand store) rather thanuseAuth()/useSession()so it works in any React tree where<DatabaseProvider>is mounted, not just inside<AuthProvider>. Lives-queries the personal workspace via the newgetPersonalWorkspaceByOwnerQueryDAL helper.requireActiveWorkspaceId(db): Promise<string>— non-React entry-point variant. ThrowsError('No active workspace')on null. Used by extension tools, AI pipeline, background jobs.Call-site migration
React (~12 components/hooks):
useActiveWorkspaceId()+workspaceIdin everyqueryKey+enabled: !!workspaceIdguard. Mutations throw if null.Non-React (
extensions/tasks/tools.ts,ai/fetch.ts,ai/eval/runner.ts,lib/data-migrations/automations-to-skills.ts): resolve once at entry viarequireActiveWorkspaceId(db), thread through.Bootstrap:
runDataMigrations(db, workspaceId)now takes the workspace explicitly;post-auth-bootstrappasses the personal workspace id.Chat plumbing:
createChatInstance/createAgentRoutingFetchfactories takeworkspaceIdso thecustomFetchclosure can persist ACP session ids back viaupdateChatThread(db, workspaceId, ...).useChatStore.setSelectedAgentresolvesworkspaceIdfrom the active session'schatThread.workspaceId.Shared DAL query for the personal-workspace lookup
src/dal/workspaces.tsgainsgetPersonalWorkspaceByOwnerQuery(db, userId)returning the Drizzle query (use withtoCompilableQueryfor live React subscription orawaitfor one-shot).getPersonalWorkspaceByOwner(async) now delegates to it — single source of truth for the personal-workspace WHERE. BothuseActiveWorkspaceIdand<WorkspaceGate>consume the query helper so they can't drift.Test infrastructure
src/dal/test-utils.ts: exportswsId/otherWsId/testUserIdconstants;setupTestDatabase/resetTestDatabaseseed a personal workspace row owned bytestUserIdsouseActiveWorkspaceIdresolves in component tests.src/test-utils/powersync-reactivity-test.tsx: opt-inseedTestTrustDomain()/resetTestTrustDomain()helpers — call frombeforeEachin tests that render components consuminguseActiveWorkspaceId. Not invoked automatically because the trust-domain registry is a process-global Zustand singleton; auto-seeding would leak across files.mcp-servers,modes,triggers,model-profiles,tasks,agents,skills): seed rows underwsId+otherWsId, query with one, assert the other's rows don't leak.Test results
bun run type-checkgreen.bun run lintgreen (0 errors; pre-existing warnings unchanged).proxy-fetch-context,PendingDeviceModal,createAuthenticatedClientdevice headers,usePowerSyncCredentialsInvalidListener.Risk if not landed
The moment shared workspaces ship, every unfiltered FE query becomes a cross-workspace data leak. The BE upload handler prevents writes from crossing workspaces but reads/updates/deletes are pure FE concerns; the backend can't help. Day-1 (personal-only) safe; ship-blocker for shared workspaces.
Test plan
/tasks): create / edit / complete / delete — all scoped + extension tools (addTasks,getTasks,deleteTasks) work viarequireActiveWorkspaceId.ai/fetch.ts(it resolves workspace once at the top and threads it throughgetModel/getModelProfile/getAllSkills).make checkandbun testgreen.Note
High Risk
Broad refactor of data access across chat, AI inference, automations, and settings; incorrect workspace threading would leak or mutate cross-workspace data once shared workspaces ship.
Overview
This PR threads an explicit
workspaceIdthrough every workspace-scoped DAL read/write/update/delete (fn(db, workspaceId, …)), replacing internalgetActiveWorkspaceId(db)resolution on inserts only. Queries and mutations now filter witheq(table.workspaceId, workspaceId); partial updates stripworkspaceIdfrom SET payloads so rows can’t be reassigned across workspaces.Call sites resolve workspace once via
useActiveWorkspaceId()(React, query keys +enabledguards) orrequireActiveWorkspaceId(db)(e.g.ai/fetch.ts, eval runners, trigger scheduler). Chat plumbing passesworkspaceIdintocreateChatInstance/createAgentRoutingFetchand stores it onChatSession; hydration evicts sessions when the active workspace changes and usesgetActiveWorkspaceIdat save/hydrate time to avoid stale closures.Supporting changes: shared
getPersonalWorkspaceByOwnerQueryforWorkspaceGateand the hook; newupdateMcpServerwith workspace scoping; eval/test helpers seed trust domain + testwsId; cross-workspace isolation tests on several DAL modules.Reviewed by Cursor Bugbot for commit b325483. Bugbot is set up for automated code reviews on this repo. Configure here.