Skip to content

refactor: retire Workspace as a product and domain concept #167

Description

@Gandy2025

Retire Workspace as a product and domain concept

Status: accepted product direction. Phase 1 is ready to implement. Phase 2 is gated on a one-off data migration.

Summary

OpenTag's management surface is administered by one person. Sharing happens in Feishu and Slack, where colleagues talk to an Agent without an OpenTag Account. A user installs Claude Code or Codex on their own laptop, creates one or more Agents on it, and connects each Agent to one app in their IM tenant. Nobody else needs access to the management surface to benefit from those Agents.

Under that definition Workspace has no work left to do. It was designed as a team boundary: a set of people who completely trust one another to manage the same Agents and machines. That population does not exist in the product as defined.

This issue retires Workspace in two stages. Phase 1 removes it from every user-facing and API surface without touching storage. Phase 2 folds workspaces into users and drops the admin-grant and invitation tables. Between them sits a one-off data migration that is a hard precondition for Phase 2, not an optional cleanup.

The end state is four layers: Account → Computer enrollment → Agent → IM binding.

This ownership spine does not preclude shared durable context. Context Tree access is orthogonal to Account, Computer enrollment, Agent, and IM binding ownership. Agents owned by different Accounts may eventually be authorized to read or write the same portable Git-hosted Context Tree without joining a shared OpenTag management scope.

A Context Tree is not a Workspace or Project. It holds durable, reviewable decisions and context, not Agent ownership, runtime state, Tasks, ordinary working files, credentials, or Secrets.

Relationship to #125

#125 established that Workspace "remains the collaboration, authorization, and data-isolation scope, but it stays implicit when an Admin can access only one," and that it is "still necessary whenever one Account administers multiple independent trust scopes."

This issue supersedes that conclusion. The multi-trust-scope case is not the product: an Account administers its own Agents and nothing else. What #125 called progressive disclosure becomes permanent removal.

Everything else #125 established stands and is a precondition for this work — machine credentials scoped to one enrollment, account tokens barred from the Runtime WebSocket, cross-scope non-disclosure, and creation identity as audit metadata rather than an authorization rule.

Why this is changing

The two populations do not both need a boundary

#125 already separated Admins from IM Users. What it did not resolve is that the Admin population is one person per set of Agents. A boundary that groups people is only worth its cost when more than one person is inside it.

The schema confirms the separation is complete: sessions holds no user reference and there is no representation of an IM sender anywhere in the schema. The people using an Agent are invisible to the account model by design. Removing Workspace therefore cannot affect them.

Workspace's responsibilities do not survive the definition

Workspace carries four things today. Under one-Account-per-scope, three disappear outright rather than moving somewhere else:

  • Authorization boundary — admin grants and invitations. With exactly one administrator there is nothing to grant. requireAdmin degrades to "does this belong to you".
  • Isolation boundary — becomes account isolation, which is already implied by the account.
  • Unique naming — Agent names become unique per Account.
  • Enrollment scope — the only one that needs a new host, and users can hold it.

The enrollment case was the apparent blocker, because an enrollment must exist before an Agent is created and therefore could not hang off the Agent. It resolves by hosting enrollments on the Account, which is available at signup. An earlier objection — that an account-scoped credential would serve several Agents and widen its blast radius — does not hold: co-located Agents already share one Codex home and one Claude Code home, because CODEX_HOME and CLAUDE_CONFIG_DIR are resolved once per daemon process. The isolation that objection tried to protect does not exist a layer down.

The concept taxes every decision

Every feature discussion has to answer "is this Workspace-level or Agent-level" before it can start, and the answer is always the same once there is one Account per Workspace. That question disappears with the concept.

Current state

Measured on main at c2556e2, and against staging on 2026-08-26.

Non-test source files mentioning workspace 111 (server 46, cli 20, client 19, shared 13, web 13)
Occurrences in those files 2431
HTTP path constants containing workspace 27
Tables with a foreign key to workspaces 5
workspaces own columns name, displayName, setupCompletedAt

The production data is the shape being retired: one Workspace holding 8 Agents across 5 Computers with 5 equal Admins. A second, auto-provisioned Workspace on the same Account holds nothing.

Phase 1 — remove Workspace from every surface

No schema change. Reversible. Existing data is untouched, so current Admins keep every grant they have; they simply cannot add more.

The honest outcome is narrower than "OpenTag becomes single-administrator": new Accounts get the full self-administered model, and existing shared Workspaces stop growing. Existing users see exactly what they see today.

Delete

Layer Item
Server api/invitations.ts — both routes (52 lines)
Server services/invitations/ (32 lines)
Server api/workspaces.ts — list admins, revoke admin, create Workspace
Server workspace-admin-access.tslistAdmins, listAdminsConfig, createInvitation, previewInvitation, acceptInvitation, revokeAdmin, lockAccountForGrantWrite, requireWorkspaceHeadroom, createWorkspaceWithAdmin
Web /workspace, /workspaces/new, /invites/:token
Web Workspace entry and Workspace switcher in the account menu
CLI opentag admin group — list, invite, revoke (88 lines)
Shared Invitation and admin contracts, and their HTTP path constants

Keep

  • establishDefaultWorkspaceForNewAccount — Phase 1 depends on it to create the now-invisible Workspace at signup. Note this reverses feat(web): create Teams explicitly instead of provisioning them at sign-in #44, which made Team creation explicit; auto-creation is required once the concept is hidden, because a user cannot explicitly create something they never see.
  • bootstrapAdminInTransaction — the single grant row is what marks a Workspace as belonging to its Account.
  • The requireAdmin family. Call sites are unchanged; only the meaning narrows.

Add

  • A Computers entry in the account menu, pointing at /agents/computers. Self-administration raises the frequency of enrolling a machine — every user now provisions their own — and the only current paths to that page are recovering an already-offline Agent (PR fix(web): let an offline Computer be reconnected from its Agent #166) or opening the New Agent flow.
  • An assertion that an Account holds at most one Workspace. This holds naturally once creation is gone; the assertion prevents regression.

Preflight

Deleting the Workspace switcher strands anything held in a second Workspace. The switcher renders only when me.workspaces.length > 1. The reporter's second Workspace was verified empty, but other Accounts were not. Run before deleting:

select u.email, count(*) as workspaces,
       sum(case when exists(select 1 from agents a
             where a.workspace_id = g.workspace_id and a.status <> 'deleted')
           then 1 else 0 end) as with_agents
from workspace_admin_grants g join users u on u.id = g.user_id
where g.revoked_at is null group by u.email having count(*) > 1;

Proceed only when every with_agents is at most 1. Also check for outstanding invitations, which the deletion will strand:

select count(*) from admin_invitations
where accepted_at is null and revoked_at is null and expires_at > now();

Accepted costs

  • Revoking an administrator becomes a SQL operation. The five existing grants on the shared Workspace remain and have no product surface for removal. Acceptable while OpenTag is internal.
  • A colleague who needs access to an existing Agent has no product path. Under self-administration this is the intended answer: they create their own Agent.

Data migration — precondition for Phase 2

A one-off operational task, deliberately not built as a product capability.

workspaces cannot be folded into users while one Workspace holds five administrators and eight Agents belonging to different people. The fold requires each Workspace to correspond to exactly one Account.

The split is pure data and needs no schema change. In one transaction: create a Workspace and grant per Account, then update workspace_computers.workspace_id and agents.workspace_id together so the composite foreign key holds throughout. Agent ids, IM bindings, session history, and machine credentials all survive, because im_bindings references agent_id and session_placements references the enrollment id, and neither changes.

Two things to decide before running it:

  • Naming. Agent name uniqueness is scoped to the Workspace. After splitting, two Accounts may each hold an Agent named reviewer, and merging back later collides with agents_workspace_name_active_unique. If reunification is even plausible, adopt a prefixed convention during the split; the cost is zero now and non-trivial later.
  • Shared machines. A Computer whose Agents belong to more than one person must be enrolled once per Account. The model supports it — same computerId, several enrollments — but it requires running computer connect again on that host. debian-devbox carries two Agents and is a candidate.

Phase 2 — remove Workspace from storage

Fold workspaces into users; drop workspace_admin_grants and admin_invitations; rehost the enrollment and Agent foreign keys on the Account; retire the CLI --workspace option. Invisible to users, who stopped seeing Workspace in Phase 1.

Most of the 2431 occurrences are a mechanical rename. The risk is concentrated in the migration itself, and this is a cutover of the same magnitude as #161.

Recommendation: do not schedule a dedicated release for it. Once Phase 1 lands, what remains is an inert table with one row per Account and a foreign key that always resolves to the same Account. Carry it until the next change that already touches the schema, and fold this in there.

When Workspace would have to come back

Some plausible triggers have cheaper answers and should not be met by restoring Workspace: sharing a single Agent with one colleague is a per-Agent collaborator list, and handing an Agent to someone else is an ownership-transfer operation.

A grouping layer is genuinely required when:

  • One grant must cover many Agents. The signal is noticing that the same authorization is being repeated across Agents.
  • A shared machine needs an owner. debian-devbox already fits: it is nobody's laptop, and under self-administration no Account owns it, so patching and access have no responsible party.
  • Billing or quota applies to an organization rather than a person.
  • An audit or compliance boundary spans people.
  • A shared execution lifecycle or policy spans independently owned Agents. Shared Tasks, working files, Secrets, or one lifecycle that must survive any participating Agent may justify a new grouping entity. That entity must be designed from the concrete requirement and must not inherit the retired Workspace model by default.

Agent-to-Agent routing or shared Context Tree access does not by itself require Workspace to return. Sparse interaction can use Agent-scoped capabilities, and durable shared knowledge can remain an independently authorized Context Tree.

If a future requirement is genuinely Workspace-shaped, restoring it is cheaper than removing it was. Removal collapses foreign keys onto users; restoration inserts a layer above users with a one-to-one mapping already in place, and by then the requirement is known rather than speculative.

Non-goals

  • Introducing a Project, Collaboration, Agent Group, or other shared execution-resource container. Shared durable context remains a separate Context Tree concern; a broader grouping model requires a concrete future use case.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions