Repository navigation
feat: members use the Built-In agent with their own keys; People admin; session renewal - #357
Yuge Zhang (ultmaster) wants to merge 17 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>
A Bubble administrator can list every principal with their system and Workspace roles, admit or revoke a member, and give or remove a Workspace role, without scripts against Bubble's API. Revoking one's own access is refused. Changes apply on the next request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ials A Bubble member's calls run in a personal settings namespace: the secret store keeps their model keys, OAuth and connected credentials under their own ids with no fallback to the deployment's environment, and model settings and agent defaults live under data/people/<key>/. Members may change their own settings; the deployment's stay with administrators. A member's default agent is always the Built-In one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Members may now use /api/agent and conversations through a Space they can use; external agents, RFS and change-review configuration stay with administrators, since they act through the shared machine credential on the operator's machines. The Built-In agent's tools check the starter's access: World reads of another Space need read on it, Workspace memory and Skills need manage on the Workspace, and Task Run tools are refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign-in now asks for offline_access and keeps the refresh token in page memory only. The page renews its session a minute before the access token expires and on any 401; if it cannot, the sign-in screen opens over the still-running app and signs in through a popup, then resends the saves the lapse refused. With HUABU_SIGN_OUT_PROVIDER=true, signing out also ends the identity provider's session through its end_session_endpoint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Administrators of a Bubble deployment get a People tab to admit or revoke people and give Workspace roles. A member's Agent tab shows only Built-In setup, their own models and keys, and Settings no longer loads the external agent and readiness data they cannot read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sation The web app picks a new conversation's agent from GET /api/acp/profiles, which refused members, so they could not start one. Members now read a listing with no external profiles and Built-In as the selectable agent. The App menu calls the switcher "Switch Workspace" for named Workspaces, and the People panel's add picker aligns with its rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…enewal Fold PR B into the deployment-security architecture doc and record in the proposal why members run the Built-In agent while external agents, which hold the daemon's machine token and the operator's CLI logins, stay with administrators. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Personal OCR settings still inherit deployment credentials, and broadened member access allows members to stop administrators’ external agent runs without thread-binding checks.
8 open issues
Prevent personal OCR settings from inheriting deployment credentials · New Enforce thread binding for member agent control requests · New Namespace pending OAuth sessions per settings owner · New Scope OpenAI model cache by runtime namespace · New Capture authenticator before clearing refresh tokens · New Retry retained dirty sidecars after failed saves · New Deduplicate concurrent refresh requests · New Display popup sign-in failures in the error state · New
What changed in this PR
Extends Bubble multi-user support with members’ Built-In agent access, personal credentials, People administration, and in-page session renewal. The stacked diff also includes the request-scoped Workspace foundation.
Changes:
- Adds personal settings and caller-aware Built-In agent permissions.
- Introduces administrator-only People APIs and membership/Workspace-role controls.
- Adds OIDC refresh, popup reauthentication, save recovery, and optional provider logout.
This pull request requires changes. Unresolved critical findings concern operator OCR credential inheritance and member control of external-agent runs. Moderate findings affect shared OAuth sessions and model caches, provider logout, member service configuration, save replay, and concurrent refresh requests. Popup-error feedback also remains a nit.
| File | Description |
|---|---|
packages/shared/src/types/api/workspace.ts |
Adds named-Workspace capability. |
packages/shared/src/types/api/identity.ts |
Defines People and provider-logout contracts. |
docs/README.md |
Indexes the multi-user proposal. |
docs/proposals/multi-user-service.md |
Documents the stacked design and limitations. |
docs/architecture/deployment-security.md |
Updates identity, permissions, and session documentation. |
docs/architecture/canvas-storage.md |
Documents concurrent Workspace storage. |
docs/architecture/agent-reachback.md |
Documents Workspace-qualified reachback URLs. |
apps/web/src/utils/oidcLogin.ts |
Adds refresh, popup login, and logout discovery. |
apps/web/src/utils/oidcLogin.test.ts |
Tests popup validation and refresh exchange. |
apps/web/src/store/workspaceStore.ts |
Supports personal Workspace initialization and creation. |
apps/web/src/store/workspaceStore.test.ts |
Tests server-selected and named Workspaces. |
apps/web/src/store/settingsUiStore.ts |
Adds the People tab identifier. |
apps/web/src/store/canvasStore.ts |
Adds post-renewal save recovery. |
apps/web/src/pages/WorkspaceSetupPage.tsx |
Adds named-Workspace setup UI. |
apps/web/src/pages/CanvasListPage.tsx |
Updates Workspace labels. |
apps/web/src/i18n/resources/zh-CN/common.json |
Adds Chinese Workspace and People strings. |
apps/web/src/i18n/resources/en/common.json |
Adds English Workspace and People strings. |
apps/web/src/components/Shell/SignInGate.tsx |
Adds renewal and in-place reauthentication. |
apps/web/src/components/Shell/SignInGate.test.tsx |
Tests renewal and preservation of the mounted app. |
apps/web/src/components/Settings/SettingsModal.tsx |
Adapts settings to member and administrator roles. |
apps/web/src/components/Settings/sections/PeopleSettings.tsx |
Adds membership and Workspace-role controls. |
apps/web/src/components/Panels/Header/AppMenu.tsx |
Connects provider logout and Workspace labels. |
apps/web/src/api/workspace.ts |
Adds named-Workspace creation. |
apps/web/src/api/people.ts |
Adds People API helpers. |
apps/web/src/api/identity.ts |
Adds optional provider logout. |
apps/web/src/api/_routes.ts |
Defines People endpoint paths. |
apps/server/src/security/secret-store.ts |
Adds personal secret namespaces without environment fallback. |
apps/server/src/modules/workspaces.route.ts |
Adds caller-specific Workspace management. |
apps/server/src/modules/workspace/scoped-routes.ts |
Executes reachback routes in a named Workspace. |
apps/server/src/modules/workspace/scoped-routes.test.ts |
Tests scoped routes and reachback URLs. |
apps/server/src/modules/workspace/context.ts |
Defines asynchronous Workspace scope. |
apps/server/src/modules/workspace.ts |
Makes getters and leases Workspace-aware. |
apps/server/src/modules/workspace.route.ts |
Reports caller-specific Workspace capabilities. |
apps/server/src/modules/workspace.route.test.ts |
Updates access-contract fixtures. |
apps/server/src/modules/workspace-activation.ts |
Creates Workspaces without process-wide activation. |
apps/server/src/modules/storage/storage.ts |
Opens scoped Workspaces and caches World IDs. |
apps/server/src/modules/storage/index.ts |
Exports scoped Workspace opening. |
apps/server/src/modules/storage/backends/sqlite/database.ts |
Resolves SQLite Workspace scope per call. |
apps/server/src/modules/storage/backends/postgres/database.ts |
Resolves PostgreSQL Workspace scope per call. |
apps/server/src/modules/storage/backends/disk/canvas-dirs.ts |
Separates directory indexes by Workspace. |
apps/server/src/modules/services/services.route.ts |
Permits personal service configuration. |
apps/server/src/modules/services/services.route.test.ts |
Updates settings-authorization mocks. |
apps/server/src/modules/security/space-access.ts |
Updates member-agent admission and manage checks. |
apps/server/src/modules/security/space-access.test.ts |
Tests member-agent and resource boundaries. |
apps/server/src/modules/security/owner.ts |
Adds personal-settings authorization. |
apps/server/src/modules/remote_fs/public-skill.ts |
Recognizes scoped bootstrap URLs. |
apps/server/src/modules/integrations/integrations.route.ts |
Permits personal integration-key updates. |
apps/server/src/modules/integrations/ink-ocr-config.route.ts |
Opens member access to OCR configuration. |
apps/server/src/modules/identity/workspace-selection.ts |
Selects and provisions personal Workspaces. |
apps/server/src/modules/identity/service.ts |
Extends access and People interfaces. |
apps/server/src/modules/identity/resources.ts |
Resolves resources across scoped Workspaces. |
apps/server/src/modules/identity/request.ts |
Stores request access and Workspace-selection callbacks. |
apps/server/src/modules/identity/preferences.ts |
Persists current Workspace per principal. |
apps/server/src/modules/identity/personal-settings.ts |
Defines caller and personal-settings scope. |
apps/server/src/modules/identity/personal-settings.test.ts |
Tests personal secrets, paths, and defaults. |
apps/server/src/modules/identity/people.route.ts |
Adds administrator-only People endpoints. |
apps/server/src/modules/identity/multi-user.test.ts |
Runs multi-user scenarios on Disk storage. |
apps/server/src/modules/identity/multi-user.scenarios.ts |
Covers isolation, switching, membership, and roles. |
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 People services. |
apps/server/src/modules/identity/http.ts |
Establishes caller scopes during admission. |
apps/server/src/modules/identity/config.ts |
Reads provider-logout configuration. |
apps/server/src/modules/identity/bubble.ts |
Implements roles, grant migration, and People storage. |
apps/server/src/modules/canvas/space-hierarchy.test.ts |
Updates resource-access fixtures. |
apps/server/src/modules/canvas/space-hierarchy.route.ts |
Requires manage access for subtree deletion. |
apps/server/src/modules/canvas/space-card.route.ts |
Requires manage access for Space renaming. |
apps/server/src/modules/canvas/preprocess-access.route.test.ts |
Updates member-access fixtures. |
apps/server/src/modules/canvas/member-spaces.route.test.ts |
Updates member resource-access contracts. |
apps/server/src/modules/canvas/canvas.route.ts |
Requires manage access for Space deletion. |
apps/server/src/modules/artifact/send-blob.test.ts |
Updates restricted-access fixtures. |
apps/server/src/modules/agent/tools/executor.ts |
Adds caller-aware tool restrictions. |
apps/server/src/modules/agent/memory/op-counter-hook.test.ts |
Updates member-access fixtures. |
apps/server/src/modules/agent/llm.ts |
Adds namespaces for model configuration and runtime state. |
apps/server/src/modules/agent/llm.route.ts |
Permits members’ model and OAuth configuration. |
apps/server/src/modules/agent/agent.route.ts |
Rejects members’ external-agent bindings when starting turns. |
apps/server/src/modules/agent/agent-defaults.ts |
Scopes defaults and selects Built-In for members. |
apps/server/src/modules/agent/agent-defaults.route.ts |
Permits personal agent-default settings. |
apps/server/src/modules/agent/acp/reachback-env.ts |
Includes Workspace identity in reachback URLs. |
apps/server/src/modules/agent/acp/profiles.route.ts |
Returns a Built-In-only listing for members. |
apps/server/src/app.ts |
Mounts People and scoped reachback routes. |
Review effort: Balanced
| '/config', | ||
| async (request, reply) => { | ||
| if (!isOwnerRequest(request)) { | ||
| if (!canChangeOwnSettings(request)) { |
| '/api/agent-change-review', | ||
| '/api/rfs', | ||
| ]; | ||
| const ADMIN_AGENT_ROUTES = ['/api/acp', '/api/agent-change-review', '/api/rfs']; |
| Reply: ApiResult<OAuthDeviceCodeResponse>; | ||
| }>('/oauth/device-code', async (request, reply) => { | ||
| if (!isOwnerRequest(request)) { | ||
| if (!canChangeOwnSettings(request)) { |
| */ | ||
| let cachedUtilityModel: Model<Api> | null = null; | ||
| /** One runtime state per settings namespace: the deployment's, or a member's. */ | ||
| const runtimeStates = new Map<string, RuntimeState>(); |
| forgetTokens(); | ||
| // The provider's own session would otherwise sign the next person straight | ||
| // back in as this one, which matters on a shared computer. | ||
| if (options.providerSignOut) { | ||
| const authenticators = (await listInteractiveAuthenticators()) ?? []; | ||
| const authenticator = | ||
| heldAuthenticator() ?? | ||
| (authenticators.length === 1 ? authenticators[0] : null); |
| if (structureSaveErrors.has(canvasId)) structureScheduler.schedule(); | ||
| await drainPendingSaves(); |
| return await requestTokens(current.authenticator, { | ||
| grant_type: 'refresh_token', | ||
| refresh_token: current.refreshToken, | ||
| }); |
| async (authenticator: InteractiveAuthenticator) => { | ||
| await adopt(await signInWithPopup(authenticator)); | ||
| await check(); | ||
| await resend(); | ||
| }, |



The second of two stacked PRs toward #228. It is stacked on #356, so its branch includes #356's nine commits; review from
53058b72onward. After #356 merges this diff shrinks to the eight commits below. The plan for both is indocs/proposals/multi-user-service.md.#356 gave each person their own Workspaces. With this PR members can also use AI, configure their own keys and stay signed in, and an administrator can manage people from the UI. With both merged, a Bubble deployment is ready for several people to use day to day.
Nothing changes unless Bubble is enabled. Local mode, administrators and the machine credential never open a personal settings scope, so they read the deployment's settings exactly as before. The new web paths are gated on Bubble identity.
What members get
Administrators get a People tab, backed by
/api/people: admit or revoke a member, and give or remove a Workspace role. A person appears there after their first sign-in.Why external agents stay with administrators
The proposal's first design gave each external agent run its own RFS token. Two things rule that out:
AGENTLET_TOKENby design, and refuses any session that tries to set that variable. A member's external agent would hold an unrestricted credential next to any run token, and a prompt-injected agent could simply use it.The Built-In agent runs in-process and never calls RFS. Its tools run in the request's Workspace and caller scope, and now check the person who started the run:
huabu.manageon the Workspace, because they steer every agent there./api/acp, RFS, change-review configuration, Interactive View actions and any external binding on/api/agentstay administrator-only. The one exception isGET /api/acp/profiles, which lists only Built-In for members so the web app can start a conversation.Design
modules/identity/personal-settings.ts). A second per-requestAsyncLocalStorageslot, beside feat: request-scoped Workspaces — each person works in their own #356's Workspace scope, filled at admission. For a member it holds their principal (their settings namespace) and their access. Agent runs started by the request inherit it.person:<key>:<id>, with no environment fallback. The deployment's connection token and access password are never namespaced.llm.ts, with its model caches now kept per namespace) and agent defaults live under<data>/people/<key>/.canChangeOwnSettingsreplaces the owner check on the model, agent-defaults, integrations, services and Ink OCR settings routes.GET /api/people,PUT /api/people/:id/member,PUT /api/people/:id/workspaces/:workspaceId. All are administrator-only, and an administrator can't revoke their own access. The contract is inpackages/shared/src/types/api/identity.ts.utils/oidcLogin.ts,SignInGate.tsx).offline_access, and the refresh token is kept in page memory only.401. Failing that, the sign-in screen opens over the app and signs in through a popup. The popup posts the code back to the page, which redeems it with a verifier that never left its memory.HUABU_SIGN_OUT_PROVIDER=true, signing out also goes to the provider'send_session_endpoint.Proof
Production-like run: the dev Server in Bubble mode against a throwaway Postgres 18, holding both Huabu's content and Bubble's records. Carol and Dave are bot principals with Bubble local tokens, each made a
member. Their model settings point at a local OpenAI-compatible mock that records the bearer key of every request.PUT /api/llm/config(OpenRouteropenai/gpt-4o-mini, base URL = mock, each with their own key)people/<key>/directories. The deployment'sGET /api/llm/configstays empty.POST /api/agentin her Spacecarol-own-keydave-own-keyreached the mockPOST /api/agentin Carol's SpaceagentBindingGET /api/acp/profiles2dd1da32, which left members unable to start a conversation)GET /api/workspacesnow includes "Carol's Workspace"Automated:
modules/identity/multi-user.*, Disk and Postgres): admit, revoke and give Workspace roles end to end. Covers 403 for non-administrators, 404 for an unknown person, 409 on self-revoke, and that an editor role lets someone into another Workspace and its removal takes them out.personal-settings.test.ts: per-person secrets, no environment fallback for members, and the deployment's own credentials never namespaced. Also per-person settings paths, and Built-In as a member's default.space-access.test.ts: members' Built-In conversations go through readable Spaces only; a thread needs its Space named; the external-agent, RFS and Interactive View routes stay with administrators.SignInGate.test.tsx: a lapse keeps the app mounted under the sign-in screen; a held refresh token renews in place and resends the refused saves.oidcLogin.test.ts: the popup code is accepted only from this origin and this login's state, then redeemed and renewed.Limitations
Validation (on
a1964258)pnpm typecheck: passpnpm format: appliedpnpm lint:fix: 0 errors. The new warnings are only test-file non-null assertions, in the existing test style.vitest run: 244 files, 3054 passed, 28 skippedpnpm test:storage, with Postgres and Azurite containers: 17 files, 510 passed, including the Postgres multi-user scenariosvitest run: 358 files, 4759 passed🤖 Generated with Claude Code