Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3e297d4
feat(session): establish the Brain Session from the Desktop login coo…
aimeritething Sep 15, 2026
2be033c
fix(session): address review findings on the Brain Session
aimeritething Sep 15, 2026
f139af3
feat(shell): brand row and Workspace Switcher on the App Sidebar (AIM…
aimeritething Sep 15, 2026
ddf8ee8
fix(shell): address review findings on the Workspace Switcher (AIM-445)
aimeritething Sep 15, 2026
a137864
feat(workspace): Workspace Area, read-only (AIM-446)
aimeritething Sep 15, 2026
c66639c
fix(workspace): address review findings on the read-only Workspace Ar…
aimeritething Sep 15, 2026
ce0a788
feat(workspace): Workspace Area write operations (AIM-447)
aimeritething Sep 15, 2026
ce59134
fix(workspace): address review findings on the Workspace Area writes …
aimeritething Sep 15, 2026
5571f89
feat(billing): Workspace Creation in the Billing Area (AIM-448)
aimeritething Sep 15, 2026
af285bb
fix(billing): address review findings on Workspace Creation (AIM-448)
aimeritething Sep 15, 2026
35c62c0
fix(session): close the CSRF, referrer, and wrong-Workspace holes
zjy365 Sep 18, 2026
36fbfe5
fix(workspace): close the invite holes and the guard's failed-refresh…
zjy365 Sep 18, 2026
92e9f85
fix(billing): close Workspace Creation's unobserved-create and refres…
zjy365 Sep 18, 2026
24ba9ec
fix(session): refuse Origin null and plain-HTTP origins on the sessio…
zjy365 Sep 18, 2026
d5f2dbc
fix(workspace,projects): judge the guard on its payload; build invite…
zjy365 Sep 18, 2026
19988f3
fix(billing): reset the creation dialog on close; spend the pending r…
zjy365 Sep 18, 2026
0adbab4
fix(billing): fail closed on a pending-creation record without a pay id
zjy365 Sep 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/helm-chart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ jobs:

- name: Verify account-service URL configuration
run: bash charts/brain-system/tests/account-api-env.sh

- name: Verify Desktop URL configuration
run: bash charts/brain-system/tests/desktop-api-env.sh
54 changes: 51 additions & 3 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,15 @@ _Avoid_: Favorite Project, starred Project, recent Project.

### App Sidebar

The persistent left-edge product navigation surface containing product-level navigation, Project navigation (Pinned Projects and all other Projects), and app-level actions. It is outside the Project Canvas and is not a Side Pane or a Project list. It has exactly two user-controlled states, and these are their canonical names: **Expanded** (icons with text labels) and **Collapsed** (an icon rail with tooltips). Before the user has ever changed it, the App Sidebar is Collapsed; thereafter the user's last chosen state is remembered per browser. State changes only by explicit user action and is independent of viewport width.
The persistent left-edge product navigation surface containing product-level navigation, Project navigation (Pinned Projects and all other Projects), and app-level actions. It is outside the Project Canvas and is not a Side Pane or a Project list. It has exactly two user-controlled states, and these are their canonical names: **Expanded** (icons with text labels) and **Collapsed** (an icon rail with tooltips). Before the user has ever changed it, the App Sidebar is Collapsed; thereafter the user's last chosen state is remembered per browser. State changes only by explicit user action and is independent of viewport width. Its brand slot — the Sealos mark at the top — is the App Sidebar's only collapse/expand control: it shows the mark while the pointer is away and becomes the control (collapse when Expanded, expand when Collapsed) while the pointer is over the App Sidebar or the control has focus; there is no separate collapse button.

_Avoid_: Project list, left Side Pane, Project Shortcut (retired term), open/closed sidebar, full/mini sidebar, rail mode.
_Avoid_: Project list, left Side Pane, Project Shortcut (retired term), open/closed sidebar, full/mini sidebar, rail mode, collapse button (as a control apart from the brand slot).

### Workspace Switcher

The row under the App Sidebar's brand slot that names the current Workspace — its Workspace avatar, display name, and the plan of its Workspace Subscription (Pay-As-You-Go when it has none) — and the popover that row opens: a card for the current Workspace (avatar, Personal or the user's Workspace Role, plan), the other Workspaces the user belongs to in the current region with their Workspace Role and plan, a create-Workspace row, and the manage row that is the Workspace Area's single entry. The plan is a Workspace fact and shows here, not on the account row; when the Workspace Subscription needs attention (payment-due, cancelling) the row grows a second line carrying that hint. In the Collapsed rail only the avatar remains and still opens the popover. It carries no pending-invitation count and no Sealos wordmark. Choosing another Workspace here is the only action that switches; afterwards the user stays on the same page of the Billing Area or Workspace Area, and lands on the Project list from anywhere inside a Project.

_Avoid_: team switcher, namespace switcher, workspace dropdown, workspace menu.

### Sealos Desktop Entry

Expand Down Expand Up @@ -238,12 +244,48 @@ The condition where a domain's observed desired configuration changes to a value

## Authorization & Identity

### Workspace

The user-visible collaboration boundary on the platform: one Kubernetes `ns-…` namespace and the Desktop "team" it corresponds to, holding Projects, workloads, and a Workspace Subscription. Every user has exactly one **Personal Workspace** — created with the account, never deletable, never transferable — and may own or belong to any number of **Team Workspaces**. A Workspace is identified by its stable uid; its display name is a label users may change.

_Avoid_: team, namespace (as user-visible words), ns.

### Workspace Role

The membership level a Workspace Actor holds in one Workspace: Owner (exactly one per Workspace, the Workspace Owner), Manager, or Developer. It comes from the platform's membership record for that Workspace, not from the subscription record's role field, and a user's role differs per Workspace.

_Avoid_: permission level, team role, subscription role.

### Workspace Area

The product area under the `/workspace` URL prefix where users manage Workspaces themselves — display name, members and their Workspace Roles, invitations, ownership, and deletion. It is entered from a single entry, the manage row in the App Sidebar's Workspace switcher, and presented as one surface: a list of every Workspace the user belongs to beside the detail of the Managed Workspace. Subscription and cost are not its business; those belong to the Billing Area — and so does Workspace Creation, which the list's create row merely opens.

_Avoid_: team center, workspace settings, members page, manage dialog.

### Managed Workspace

The Workspace whose detail the Workspace Area is showing and operating on, chosen from the area's list or named in its URL and defaulting to the current Workspace. It is a selection local to the Workspace Area: changing it never switches the current Workspace that the rest of Brain works in, so a user can manage — or delete — a Workspace they are not currently in.

_Avoid_: selected workspace, current Workspace (for the one being managed), target workspace.

### Workspace Invite Link

The only way a member joins a Team Workspace: a link an Owner or Manager generates in the Workspace Area for one Workspace Role, which the invitee opens and accepts on the Sealos Desktop. It is short-lived, and generating another for the same Workspace and role replaces it. Accepting adds the invitee to the Workspace at once; there is no pending state, so a Workspace's member list never shows someone who has not yet joined, and Brain holds no inbox of invitations awaiting the user.

_Avoid_: invitation (as a pending object), invite by user ID, pending invite, invite request.

### Workspace Actor

The verified human identity acting within a workspace namespace, established by cross-checking the request kubeconfig's live workspace access against the desktop-minted proof binding it to the global user id. Actor verification and namespace authorization are separate checks: one establishes who is acting, the other where that actor may act. A Desktop session user id, an unverified app-token claim, or a namespace-authorized workload ServiceAccount is not a Workspace Actor.

_Avoid_: Desktop user id, namespace member id.

### Brain Session

The set of Desktop-issued credentials Brain holds in one browser tab — the regional token, the app token, and the kubeconfig — exchanged from Desktop's shared login cookie and kept only in page memory; Brain never persists it. The Workspace it points at is Desktop's current Workspace, which Brain follows rather than remembers.

_Avoid_: login, Desktop session, SDK session, token (unqualified).

## Deployment

### Deployment Task
Expand Down Expand Up @@ -664,10 +706,16 @@ Account-level money and workspace subscriptions, owned by the platform's account

### Billing Area

The product area under the `/billing` URL prefix where users manage the current workspace's Workspace Subscription and inspect costs, usage quota, and pricing. It is entered from a single entry — the Billing row in the App Sidebar's account popover and presented as one surface with Plan, Costs, Usage, and Pricing tabs; the Plan view is the area's index and the landing point of a Stripe Checkout Round-Trip.
The product area under the `/billing` URL prefix where users manage the current workspace's Workspace Subscription and inspect costs, usage quota, and pricing, and where Workspace Creation happens. It is entered from the Billing row in the App Sidebar's account popover, or in creation mode from a create row (Workspace Switcher popover, Workspace Area list), and presented as one surface with Plan, Costs, Usage, and Pricing tabs; the Plan view is the area's index and the landing point of a Stripe Checkout Round-Trip.

_Avoid_: cost center, billing app, separate billing pages.

### Workspace Creation

Bringing a new Team Workspace into being from Brain: the user names it and chooses its initial Subscription Plan in one step, in the Billing Area's creation mode, and pays through a Stripe Checkout Round-Trip that lands in the new Workspace. A Workspace is created the moment its name and plan are submitted — before payment — so an abandoned payment leaves a Workspace that exists without a Workspace Subscription; the platform reports it as Pay-As-You-Go and it subscribes like any other. Creation is open to every signed-in user and is never gated by the current Workspace's subscription state. Brain creates no Pay-As-You-Go Workspace: a plan is always chosen, which is why creation lives beside subscription rather than in the Workspace Area.

_Avoid_: new team, add workspace, create mode (as the name of the concept), PAYG workspace creation.

### Billing Region

One entry in the platform's global region catalog served by account-service: a cluster identified durably by an opaque uid and addressably by a unique domain. account-service stores each Workspace Subscription under the workspace plus the Billing Region's domain, so every subscription query and payment action is region-addressed. The catalog's order carries no meaning — no position in it designates any particular region.
Expand Down
24 changes: 18 additions & 6 deletions apps/ui/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
API_URL=
NEXT_PUBLIC_DEV_ENCODED_KUBECONFIG=
# Optional canonical app origin for GitHub App install callbacks. Leave empty
# to derive it from request headers at runtime.
APP_URL=
Expand Down Expand Up @@ -34,12 +33,22 @@ BILLING_LOCAL_REGION_DOMAIN=
BILLING_GPU_ENABLED=false

# App-token verification for personal-resource routes (ADR-0059). Required in
# production (startup fails fast without it). For local dev pick any secret,
# then mint a matching token for the dev kubeconfig with
# bun scripts/mint-dev-app-token.mjs
# and paste it into NEXT_PUBLIC_DEV_APP_TOKEN.
# production (startup fails fast without it). The Brain Session (ADR-0083)
# hands the page the app token that Desktop mints, so local development
# needs the real JWT_INTERNAL of the Desktop DESKTOP_API_BASE_URL points at
# (staging), not a made-up secret.
JWT_INTERNAL=
NEXT_PUBLIC_DEV_APP_TOKEN=

# Desktop upstream for the Brain Session (ADR-0083): POST /api/session
# exchanges the shared login cookie here. The Helm chart derives the
# in-cluster Service URL when its value is empty; local development points
# it at a staging Desktop, e.g. https://<staging cloudDomain>.
DESKTOP_API_BASE_URL=
# Local development only: stands in for the shared login cookie when the
# request carries none. Copy the `sealos_auth_token` cookie value from a
# browser signed in to the staging Desktop above. Ignored by production
# builds.
DEV_GLOBAL_TOKEN=

# Platform-funded Chat Agent connection. Eligible Active Free Trial workspaces
# use it for FREE_CHAT_TURNS successful turns, then use the caller's AI Proxy.
Expand All @@ -54,6 +63,9 @@ ASSISTANT_GATEWAY_MODEL=

DEVBOX_API_BASE_URL=
TEMPLATE_PROVIDER_URL=
# Only for `bun scripts/devbox-api-smoke.mjs`: a URL-encoded kubeconfig the
# smoke script calls the Devbox API with directly (not the Brain Session).
DEVBOX_SMOKE_ENCODED_KUBECONFIG=

DEVBOX_JWT_SIGNING_KEY=
# Required for managed Skills: immutable sandbox/v1 image built with the offline Skill bundle.
Expand Down
4 changes: 3 additions & 1 deletion apps/ui/scripts/devbox-api-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ function namespaceFromKubeconfig(kubeconfig) {
}

const env = readDotenv(".env");
// This smoke script talks to the Devbox API directly with a kubeconfig of
// its own; it is not on the Brain Session path.
const kubeconfig = decodeURIComponent(
env.NEXT_PUBLIC_DEV_ENCODED_KUBECONFIG || ""
env.DEVBOX_SMOKE_ENCODED_KUBECONFIG || ""
);
const namespace = namespaceFromKubeconfig(kubeconfig);
const devboxApiBaseUrl = (env.DEVBOX_API_BASE_URL || "").replace(/\/+$/, "");
Expand Down
108 changes: 0 additions & 108 deletions apps/ui/scripts/mint-dev-app-token.mjs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { BILLING_ROUTES } from "@/features/billing/server/billing-route-table";
import { withBillingDevMock } from "@/features/billing/server/create-billing-route";
import { createBillingWorkspaceCreateRetryPaymentHandler } from "@/features/billing/server/workspace-creation-handlers";
import { requestAccountService } from "@/lib/account-service/client";
import { authorizeWorkspaceActor } from "@/lib/request-kubeconfig-auth";

export const dynamic = "force-dynamic";
export const runtime = "nodejs";

export const POST = withBillingDevMock(
BILLING_ROUTES.workspaceCreateRetryPayment,
createBillingWorkspaceCreateRetryPaymentHandler({
authorizeWorkspaceActor,
requestAccountService,
})
);
16 changes: 16 additions & 0 deletions apps/ui/src/app/api/billing/workspace-create/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { BILLING_ROUTES } from "@/features/billing/server/billing-route-table";
import { withBillingDevMock } from "@/features/billing/server/create-billing-route";
import { createBillingWorkspaceCreateHandler } from "@/features/billing/server/workspace-creation-handlers";
import { requestAccountService } from "@/lib/account-service/client";
import { authorizeWorkspaceActor } from "@/lib/request-kubeconfig-auth";

export const dynamic = "force-dynamic";
export const runtime = "nodejs";

export const POST = withBillingDevMock(
BILLING_ROUTES.workspaceCreate,
createBillingWorkspaceCreateHandler({
authorizeWorkspaceActor,
requestAccountService,
})
);
Loading
Loading