The rationale for sticky placement and the remote mutual-TLS trust boundary is recorded in ADR 0006.
LemmaComputer has one Docker/KasmVNC workspace runtime and two placements. A
colocated node runs beside the reference application stack for
customer-managed installations and worktrees. A remote node runs the same
workspace image and adapter in a private compute boundary. Hosted deployments
require remote; they do not use the commercial Kasm control plane or its
Developer API.
Control never receives Docker authority. It calls the workspace-bound
/internal/v2/workspaces/:workspaceId/... node API. Remote calls use HTTPS,
mutual TLS, the internal bearer credential, and the stable node identity.
Create and egress changes additionally carry a signed policy bundle bound to
tenant, subject, workspace, access generation, policy digest/key, and fixed
gateway/Control routes. The node rechecks the provider's workspace label on
status, open, egress update, and destroy so a provider ID cannot be substituted
across workspaces.
Hosted Control uses a durable registry rather than load-balancing lifecycle
requests across node controllers. The registry stores a stable node id, private
HTTPS endpoint, expected certificate name, and one of active, draining, or
disabled. It does not store private keys or bearer credentials. A tenant has
one default node assignment for new workspaces, and every created workspace
copies that node id into its own row. Changing the tenant default therefore
affects only future workspaces; status, open, policy update, restart, stop,
delete, and tenant-cleanup calls continue to use each workspace's persisted
owner.
Only a step-up-authenticated platform administrator can mutate placement:
POST /v1/platform/workspace-nodesregisters a private node endpoint;PATCH /v1/platform/workspace-nodes/:workspaceNodeId/statedrains or disables a node;PUT /v1/platform/tenants/:tenantId/workspace-nodechanges the default for new workspaces; and- the corresponding GET routes expose nodes and assignments to the platform operator realm, never the customer realm.
The platform operator UI exposes those registry, admission, and assignment
operations. It also writes workspace.defaultSharedNodeId through the audited
platform-configuration boundary. When that setting names an active registered
node, hosted self-service onboarding atomically assigns new personal and
organization tenants to it. The worktree development harness mirrors this
placement rule in its real database; remote-node qualification then makes the
persisted assignment authoritative for runtime routing so the same onboarding
path is testable before hosted qualification. A missing setting leaves explicit
operator placement available; a configured but invalid or unavailable node fails
onboarding closed instead of silently choosing another node. Dedicated
placement is an explicit operator or entitlement decision and is never inferred
from SSO configuration.
Registration starts a node as active. A draining node owns and serves its
existing workspaces but is not eligible when a new workspace copies its tenant
assignment. A disabled node is fail-closed for all routed lifecycle calls.
Before disabling a node, operators must destroy or otherwise disposition its
persisted workspaces. For a first upgrade from the previous single-node hosted
shape, the assignment request may explicitly backfill unplaced workspace rows;
that option is safe only when the operator has confirmed that the registered
node is their actual legacy owner. Every mutation and backfill count is written
to the platform audit ledger.
The C-minus scheduler deliberately has no heartbeat, capacity scoring, automatic failover, or live workspace migration. An absent assignment, absent workspace owner, node mismatch, disabled node, or purge receipt bearing another node id fails closed. These omissions buy a productionizable multi-node shape without pretending that rescheduling a stateful Docker volume is safe.
Control selects placement-aware routing from workspace-node topology, not from
the deployment-profile name. remote topology in hosted and in the worktree
development harness resolves every lifecycle call through the persisted
workspace owner in the node registry. colocated development and
customer-managed single-node deployments retain the explicitly configured
direct controller client. This lets local remote qualification exercise the
same C-minus router used by hosted without pretending that local Compose is AWS.
Expose only these private paths:
- Control to node API over mTLS HTTPS;
- workspace ingress to the node's private mTLS desktop relay ports;
- node application relays over mTLS to the two configured private endpoints for LiteLLM and Control;
- the governed per-workspace egress proxy to its inspected egress network.
The per-workspace network is Docker-internal. Workspaces receive fixed local
aliases (lemmacomputer-gateway and lemmacomputer-control), not arbitrary
upstream destinations. Hardened relay containers alone join the pre-created
application network and validate the configured upstream CA. Do not attach the
workspace container to an application, control, default-bridge, host, or
general outbound network. Block instance metadata at routing and firewall
layers as well as in the governed proxy.
Workspace containers drop the entire Docker default Linux capability set, then
add only CHOWN, DAC_OVERRIDE, FOWNER, SETGID, and SETUID. The root
entrypoint needs these capabilities to repair ownership and modes, replace
root-managed configuration inside the user-owned persistent home, and enter
the fixed kasm-user UID/GID during initialization. The final desktop process
runs as UID/GID 1000 without effective capabilities. Electron sandboxing and
Cowork add no capability; in particular, the runtime does not add network,
namespace, mount, device, MKNOD, or SYS_CHROOT capabilities.
Applications and AI agents are selections inside one workspace contract; they are not separate workspace modes. Both arrays may be empty. An empty selection still provisions the sandbox, persistent home, desktop service, and authenticated desktop ingress relay. The node exposes no catalog application launchers, starts no agent broker, receives no model alias or gateway grant, and creates no LiteLLM or Control application relay. Public egress remains a separate policy decision and may still require the governed egress proxy.
Selecting the first AI agent makes a model alias mandatory and causes Control to issue only that agent's governed gateway and bridge authority. Removing the last agent sets the model selection to null and revokes the workspace's AI grants. Selecting applications exposes only their reviewed launchers; it does not imply an AI provider or agent runtime.
The application boundary and reasons for catalog gating are recorded in ADR 0005. The profile below is not a general-purpose compatibility mode: only release-qualified application identifiers can select it, and software installed inside a workspace cannot change the enforced container profile.
Chrome, Visual Studio Code, and Obsidian must run with their upstream Chromium
sandbox enabled. A workspace that selects any of those applications receives
the fixed lemmacomputer-workspace-electron AppArmor profile. The profile
retains Docker's default confinement shape and adds only the AppArmor userns
permission needed by Chromium when no-new-privileges is active. Its seccomp
profile retains the pinned Moby default and adds three argument-filtered rules:
clone and unshare are allowed when the CLONE_NEWUSER bit is present, and
one additional clone rule permits exactly CLONE_NEWPID among the namespace
flags. Chromium needs that PID-only transition after it has entered the
unprivileged user namespace. The kernel still requires the caller to hold the
relevant capability in its current user namespace. clone3 retains Moby's
ENOSYS fallback, unrelated namespace combinations remain denied, and
Cowork's AF_VSOCK exception is absent unless Cowork is also selected. The
runtime must not use --no-sandbox, apparmor=unconfined,
seccomp=unconfined, a privileged container, added capabilities, or the host
PID/user namespace to make these applications start.
This exception is scoped to the selected workspace container, not to an individual process inside it. All processes in that container can request a user namespace, while the AppArmor, seccomp, capability, network, storage, PID, and memory boundaries continue to constrain the resulting processes. A hosted deployment may enable this profile only on a remote workspace node. The node capability switch, exact enforced profile label, and an unprivileged namespace probe are all fail-closed preconditions to workspace readiness.
Set the workspace-node contract through .env and run npm run env:check -- --profile=hosted. Remote deployments require the bootstrap/qualification
node URL and both private
application URLs to use HTTPS, complete client and server mTLS certificate
material, a private advertised desktop host, a private bind address, the
restricted application network, and workspace-ingress upstream TLS
verification. Certificate private keys remain with their owning workloads:
the node server key reaches only the node, the Control client key reaches only
Control, the ingress client key reaches only workspace ingress, and the
application-gateway client key reaches only the remote node controller.
Hosted Control uses the registry endpoint and certificate name for each
lifecycle call; the configured Control client CA/certificate/key and internal
node token are shared C-minus credentials and should be rotated as a single
node-fleet trust domain. Per-node credentials are a later hardening step, not a
prerequisite for sticky routing.
Persistent homes are Docker volumes named and labeled by workspace and access generation. Encrypt the node's Docker data root with the platform's managed block-storage encryption and restrict snapshots/backups to the same tenant and operator boundary. Runtime destroy preserves the home volume. Permanent delete removes only volumes at or below the authorized generation, re-enumerates the node, and returns a verified purge receipt. Control must not mark purge complete without that receipt.
Removal order is: stop assigning new work, drain/destroy runtimes, execute and retain verified purge receipts, remove node credentials from Control, revoke the client/server certificates and internal token, remove private ingress and application-network routes, then destroy encrypted disks and snapshots under the platform retention policy. A failed verification is retryable and must leave the cleanup operation incomplete.
Run npm run qualify:internal-mtls locally to exercise the long-lived service
mTLS listeners in one shot: Control to the LiteLLM administration listener and
Control to a remote workspace node. The qualification creates separate
ephemeral test CAs and Control leaf keys, starts the real TLS listeners with
mock non-TLS backends, and proves accepted identity plus missing-certificate,
wrong-identity, wrong-token, cross-CA, and hostname rejection. It does not
install or reuse a developer CA.
Desktop ingress and node application relay routes also require client certificates. Their focused tests prove an actual workspace-ingress handshake, node relay identity enforcement, and application-relay client-key projection.
An isolated development worktree can run the same split boundary without manually creating Compose overlays or copying a database:
npm run qualify:remote-workspace-node -- up --cowork
npm run qualify:remote-workspace-node -- status
npm run qualify:remote-workspace-node -- downThe command creates short-lived worktree-local test authorities and preserves the worktree's existing users, organizations, providers, policies, databases, and volumes. It is qualification tooling, not production PKI automation. See the remote workspace-node and Cowork workflow for the full architecture, mTLS matrix, exact setup sequence, generated Compose topology, manual acceptance checklist, hosted gaps, and troubleshooting.
Before promotion, also run npm run qualify:deployment-profiles, the focused
controller/adapter/ingress tests, and npm run verify:quick. In a representative
private node, prove workspace start/open, governed model and tool traffic,
desktop reconnect through the TLS relay, egress allow/deny behavior, restart
persistence, cross-workspace substitution denial, verified purge, and secret-
free lifecycle audit events. Hosted Claude Cowork additionally requires a
remote node with nested virtualization, /dev/kvm, and /dev/vhost-vsock;
the colocated hosted topology remains fail-closed.