Repository navigation
feat: request-scoped Workspaces — each person works in their own - #356
Open
Yuge Zhang (ultmaster) wants to merge 9 commits into
Open
Yuge Zhang (ultmaster) wants to merge 9 commits into
Yuge Zhang (ultmaster) wants to merge 9 commits into
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A request or background job can now carry the Workspace it runs in through AsyncLocalStorage. The Workspace getters, the SQL stores, the Disk folder index and the SQL World id read that scope before the process-active Workspace, and operation leases count per Workspace. Local mode never opens a scope, so it behaves as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bubble mode selects each admitted request's Workspace from the caller's stored current Workspace, creating a personal one for a new member. Roles become Workspace owner/editor/viewer plus Space owner, with read/write/manage flowing down Workspace -> Space -> child Space and huabu.admin on the system. Space grants name their Workspace (<workspaceId>:<canvasId>); microsoft#237's grants are rewritten at startup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Routes can declare spaceAccess: 'manage'; the Space delete, delete-tree and rename routes now do, so a Workspace editor can change Spaces but not remove them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…orkspaces In Bubble mode the Workspace routes act for the caller alone: they list the Workspaces the caller may enter, create one by name (on Disk, a folder under the data directory) owned by its creator, switch only the caller's current Workspace, and rename or unregister by huabu.manage. Folder picking and paths stay with administrators. GET /api/workspace reports namedWorkspaces so the web app can offer a name-based switcher. A Disk and a Postgres suite run two members and an administrator through the real routes at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An agent's reachback URL now names its Workspace (/api/rfs/w/<workspaceId>/<canvasId>), and the RFS routes are also served under that prefix in that Workspace. A run no longer follows its starter's later switches or the process-active Workspace. The old unscoped URL keeps working for runs started before the upgrade. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the Server reports namedWorkspaces, boot keeps the Workspace the Server chose for the caller instead of activating the first registered one, the setup page lists the caller's Workspaces and creates one by name instead of picking a folder, and the header calls it a Workspace. Local mode renders as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fold PR A into the architecture docs: the Workspace/Space role model, Workspace-qualified Space grants and their migration, per-person current Workspaces, the Workspace routes for members, the per-call storage scope and the Workspace-scoped RFS URL. The proposal records what PR A built and what it deliberately left. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved authorization flaws allow revoked access and privilege escalation, while migration can permanently delete ownership grants when Workspaces are temporarily unreadable.
6 open issues
Require manage access when renaming Spaces · New Require manage access before detaching Spaces · New Preserve grants when Workspace ownership lookup is unavailable · New Revalidate Workspace access before reusing saved preference · New Require Workspace write access for root Space creation · New Bind legacy machine-token requests to the active Workspace · New
What changed in this PR
This PR extends Bubble identity toward #228 by giving each person an independent current Workspace on one server. It introduces request-scoped storage and authorization, while retaining the process-active Workspace model for local mode.
Changes:
- Adds per-person Workspace selection, provisioning, management, and Workspace-qualified Space grants.
- Makes storage namespaces and agent reachback Workspace-aware.
- Adds named Workspace selection in the web app, integration tests, and architecture documentation.
| File | Description |
|---|---|
packages/shared/src/types/api/workspace.ts |
Adds the named-Workspaces capability. |
docs/README.md |
Indexes the multi-user proposal. |
docs/proposals/multi-user-service.md |
Records the two-stage multi-user design. |
docs/architecture/deployment-security.md |
Documents Workspace selection and authorization. |
docs/architecture/canvas-storage.md |
Documents request-scoped storage behavior. |
docs/architecture/agent-reachback.md |
Documents Workspace-qualified RFS URLs. |
apps/web/src/store/workspaceStore.ts |
Preserves server selection and creates named Workspaces. |
apps/web/src/store/workspaceStore.test.ts |
Tests named Workspace initialization and creation. |
apps/web/src/pages/WorkspaceSetupPage.tsx |
Adds named Workspace selection and creation UI. |
apps/web/src/pages/CanvasListPage.tsx |
Displays named Workspace labels. |
apps/web/src/i18n/resources/zh-CN/common.json |
Adds Chinese Workspace UI strings. |
apps/web/src/i18n/resources/en/common.json |
Adds English Workspace UI strings. |
apps/web/src/api/workspace.ts |
Adds the named Workspace creation client. |
apps/server/src/modules/workspaces.route.ts |
Adds caller-specific Workspace management. |
apps/server/src/modules/workspace/scoped-routes.ts |
Binds routes to an explicit Workspace. |
apps/server/src/modules/workspace/scoped-routes.test.ts |
Tests scoped routes and reachback URLs. |
apps/server/src/modules/workspace/context.ts |
Introduces asynchronous Workspace context. |
apps/server/src/modules/workspace.ts |
Makes getters scope-aware and leases per Workspace. |
apps/server/src/modules/workspace.route.ts |
Reports personal capabilities and hides member paths. |
apps/server/src/modules/workspace.route.test.ts |
Updates the identity test fixture. |
apps/server/src/modules/workspace-activation.ts |
Creates named Workspaces across backends. |
apps/server/src/modules/storage/storage.ts |
Opens scoped Workspaces and caches their World IDs. |
apps/server/src/modules/storage/index.ts |
Exports scoped Workspace opening. |
apps/server/src/modules/storage/backends/sqlite/database.ts |
Resolves SQLite namespaces from request scope. |
apps/server/src/modules/storage/backends/postgres/database.ts |
Resolves PostgreSQL namespaces from request scope. |
apps/server/src/modules/storage/backends/disk/canvas-dirs.ts |
Keeps directory indexes per Workspace. |
apps/server/src/modules/security/space-access.ts |
Supports manage-level Space checks. |
apps/server/src/modules/security/space-access.test.ts |
Tests manage versus write permissions. |
apps/server/src/modules/remote_fs/public-skill.ts |
Recognizes scoped public bootstrap URLs. |
apps/server/src/modules/identity/workspace-selection.ts |
Selects and provisions personal Workspaces. |
apps/server/src/modules/identity/service.ts |
Extends Workspace and permission interfaces. |
apps/server/src/modules/identity/resources.ts |
Resolves resources across Workspaces. |
apps/server/src/modules/identity/request.ts |
Stores request-level Workspace selection callbacks. |
apps/server/src/modules/identity/preferences.ts |
Persists each principal’s current Workspace. |
apps/server/src/modules/identity/multi-user.test.ts |
Runs multi-user scenarios on Disk. |
apps/server/src/modules/identity/multi-user.scenarios.ts |
Tests isolation, switching, and viewer restrictions. |
apps/server/src/modules/identity/multi-user.remote.test.ts |
Runs multi-user scenarios on PostgreSQL. |
apps/server/src/modules/identity/index.ts |
Wires preferences and Workspace selection. |
apps/server/src/modules/identity/http.ts |
Opens and fills request Workspace scopes. |
apps/server/src/modules/identity/bubble.ts |
Defines roles, qualified grants, and migration. |
apps/server/src/modules/identity/bubble.test.ts |
Tests provisioning, roles, sharing, and migration. |
apps/server/src/modules/canvas/space-hierarchy.test.ts |
Updates the ownership test fixture. |
apps/server/src/modules/canvas/space-hierarchy.route.ts |
Requires manage for subtree deletion. |
apps/server/src/modules/canvas/space-card.route.ts |
Requires manage for the title endpoint. |
apps/server/src/modules/canvas/preprocess-access.route.test.ts |
Updates the preprocessing identity fixture. |
apps/server/src/modules/canvas/member-spaces.route.test.ts |
Updates member access fixtures. |
apps/server/src/modules/canvas/canvas.route.ts |
Requires manage for Space deletion. |
apps/server/src/modules/artifact/send-blob.test.ts |
Updates artifact access fixtures. |
apps/server/src/modules/agent/memory/op-counter-hook.test.ts |
Updates the memory-hook identity fixture. |
apps/server/src/modules/agent/acp/reachback-env.ts |
Includes Workspace identity in reachback URLs. |
apps/server/src/app.ts |
Mounts Workspace-qualified RFS routes. |
Review effort: Balanced
| message: 'Unable to rename this Space', | ||
| app.patch( | ||
| '/:canvasId/title', | ||
| { config: { spaceAccess: 'manage' } }, |
Comment on lines
+115
to
118
| 'huabu.workspace.editor': { | ||
| grantableOn: [TARGET.workspace], | ||
| capabilities: [READ, WRITE], | ||
| managementCapability: MANAGE, |
| role: grant.role, | ||
| grantedByPrincipalId: grant.grantedByPrincipalId ?? null, | ||
| }); | ||
| await store.grants.remove(grant.grantId); |
Comment on lines
+94
to
+96
| const already = await preferences.current(principalId); | ||
| const existing = already ? await resources.openWorkspace(already) : null; | ||
| if (existing) return existing; |
| } | ||
|
|
||
| const checks: Array<['read' | 'write', string]> = []; | ||
| const checks: Array<[AccessLevel, string]> = []; |
Comment on lines
+83
to
+86
| if (service.provider === 'bubble') | ||
| app.addHook('onRequest', (_request, _reply, done) => | ||
| openWorkspaceScope(done), | ||
| ); |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


First of two stacked PRs toward #228, following #237. The plan for both is in
docs/proposals/multi-user-service.md.After #237, several people could sign in, but they all shared the Server's single active Workspace, and only the administrator could switch it. With this PR each person works in their own Workspaces, at the same time, on one Server. A new member gets a personal Workspace on first sign-in, can create more and switch between them, and switching moves nobody else.
Nothing changes unless Bubble is enabled. In local mode no request carries a Workspace scope, so every getter answers from the process-active Workspace exactly as on
main, with the same routes and the same UI.Members still cannot run agents, as in #237. The stacked #357 lets them run the Built-In agent with their own model keys and credentials, and adds a People admin panel and session renewal.
How to use it
Configuration is #237's. In Bubble mode:
GET /api/identityright after sign-in, creates "'s Workspace", owned by them.Roles and capabilities (replacing #237's)
The same three capabilities apply on a Workspace and on a Space. They flow down Workspace → root Space → child Space, never up or sideways.
huabu.readGET, search, live streamshuabu.writehuabu.managehuabu.admin(system)huabu.manage.owner/member(Bubble core, system)huabu.adminforownerhuabu.workspace.ownerhuabu.workspace.editorhuabu.workspace.viewerhuabu.space.owner<workspaceId>:<canvasId>, so it can never reach a Space with the same id in another Workspace, and a single Space can be shared later. At startup, feat: per-user Workspace and Space access through Bubble #237's grants are rewritten to the Workspace that holds each Space; grants on Spaces that no longer exist are dropped.huabu.manageon a Workspace or Space may grant roles on it. System roles stay with the administrator.huabu.use.huabu.space.owneron it.manage, so an editor can change Spaces but not remove them.Design
AsyncLocalStoragescope (modules/workspace/context.ts) is opened by a callbackonRequesthook and filled by admission once the caller is known.getWorkspaceHandle,getWorkspacePath,getWorkspaceKey) read it first, so the ~145space(canvasId)call sites are unchanged.huabu_identity.current_workspaces, lives in the Postgres Bubble already requires.modules/identity/workspace-selection.ts, behindResourceDirectory./api/workspaceslists what the caller may enter. Create by name works on any backend; on Disk it makes a folder under the data directory. Activate sets only the caller's current Workspace. Rename and unregister needhuabu.manage.GET /api/workspacereportscapabilities.namedWorkspaces./api/rfs/w/<workspaceId>/<canvasId>. RFS is also mounted under that prefix (modules/workspace/scoped-routes.ts) and runs in that Workspace, so a run neither follows its starter's later switches nor falls back to the process-active Workspace.Proof
Automated. These suites run Bubble admission, the Space guard and the real routes over real storage, with two members and an administrator working at once:
modules/identity/multi-user.test.ts, on Diskmodules/identity/multi-user.remote.test.ts, on PostgresThey cover:
bubble.test.tscovers each role's capabilities, Workspace-scoped Space grants, who may share, a personal Workspace created once under five racing first requests, switching only into enterable Workspaces, and the #237 grant migration.scoped-routes.test.tscovers the Workspace-scoped RFS mount and reachback URL.Live, production-like. Dev Server in Bubble mode, Postgres content and Bubble records in a throwaway Postgres 18, with two bot members holding Bubble local tokens. Browser rows use a session cookie made from Carol's token.
GET /api/workspacefor Carol, Dave, adminnamedWorkspaces: trueandpath: null//setupLimitations
namedWorkspaces.Validation (on
0116f51f)pnpm typecheck: passpnpm format: appliedpnpm lint:fix: 0 errors; the 359 warnings were already therevitest run: 243 files, 3050 passed, 28 skippedpnpm test:storage(Postgres and Azurite containers): 17 files, 509 passedmulti-user.remote.test.ts: 3 passedvitest run: 357 files, 4756 passed🤖 Generated with Claude Code