Skip to content

Team Workspaces and control Nodes

Status: the first-owner bootstrap is a Source candidate: all five implementation-slice steps are source-implemented behind release-disabled methods (releaseQualified: false), and qualification is pending. Every later delivery step is Planned, including the mapping from the profile bearer to the first owner. The slice adds no listener, credential, key, dependency, remote identity provider or runtime extension. An earlier prototype (a Node scope and module-local protocol schemas) was deleted in commit 9cdbe82; this slice does not reuse it. This is the identity foundation of the organization hub direction (decision) and the administrator bootstrap that MN0 depends on.

A team Workspace is the shared scope of an organization on its Control Node: the people (Users) who are members, the Devices and clients they use, and the grants that let their agents connect and delegate through that Node. One Control Node is the only durable writer (decision). The team Workspace is a new aggregate (kind: team-workspace). It does not reuse the execution Workspace (workspace_*) or reinterpret the organizational container_* Workspace; see the naming contract.

Delivery order

Each step needs the one before it (roadmap):

  1. First owner: an empty local Control Node creates its first User, local Device, CLI client, team Workspace and owner membership (this document).
  2. Grants and machine enrollment (MN0): client and machine principals, explicit grants and revocation, as specified in Node identity and tenancy.
  3. Members: invitations, roles, per-user Project grants and per-client context.
  4. Other Devices and Nodes: cross-domain client pairing and clients that talk to more than one Control Node.

Rules for clients of several Control Nodes:

  • Keep a separate connection, credential, Node epoch and cache per Control Node.
  • Never federate authority between Nodes or retarget an open command when the user switches Node.
  • Preserve legacy execution workspace_* identities through the R1b naming migration.

Person identity (SSO/OIDC), authenticated remote access, isolation between people on a shared Node and cross-person delegation policy are also required before an organization deployment; the roadmap direction lists them.

First-owner bootstrap

The smallest prerequisite: one empty local Control Node creates its first owner after an explicit local confirmation. Invitations, other members or Devices, grants, remote pairing, multiple Nodes, active-context selection and execution Workspace renaming are out of scope for this slice.

Authority

The local Node API bearer is transport authentication only. Holding it never lets a caller choose or create the first owner. Authority comes from the OS account that owns the profile (decision):

  • The current OS account owns the exact profile runtime directory and its private credential and descriptor. Reads are bounded, no-follow and metadata-stable, followed by an authenticated node.status identity probe.
  • The Node binds the resulting local principal server-side. Requests cannot submit an OS identity, User ID, Device ID, role, Node ID, profile path, credential or authority claim.
  • This is an OS-account trust boundary. The terminal prompt is an explicit operator ceremony, not proof of physical user presence. Keychain, biometric, managed-device and remote identity claims are out of scope.

One-shot confirmation channel

A bearer-only HTTP request cannot complete bootstrap. The local launcher owns a bootstrap-only inherited channel:

  1. maia node bootstrap --profile <name> starts an ordinary empty local Node with a private child-process pipe inherited as file descriptor 3. The launcher hands exclusive ownership of the readable end to the confirmation reader, which uses Effect's Node readable adapter (not raw file reads or a named FIFO). No path, descriptor number or pipe bytes enter a protocol request or result.
  2. Preview uses the ordinary loopback request transport. The CLI prints the reserved identities, names, Node epoch, digest and expiry.
  3. The CLI opens its controlling terminal directly, refuses redirected or missing input, and requires YES <preview-id>. It then writes one confirmation record (preview ID, input digest, command ID, Node epoch) to the pipe.
  4. Apply uses the ordinary loopback transport. The CLI sends it once the confirmation write returns, which can precede the Node reading the record. The Node therefore waits up to five seconds for the reader to settle. It commits only when the unexpired in-memory preview, the apply request and the unread confirmation record match exactly. On a Node with an open channel, a bearer-only apply waits the same five seconds, then fails with bootstrap_confirmation_required and opens no transaction. Only one bootstrap call runs at a time: a concurrent preview or apply is refused at once with the retryable saturated error (admission), so calls that wait for a confirmation never queue and never hold more than one request slot.

Channel limits: one strict JSON record of at most 4 KiB of UTF-8, ended by EOF, read within five minutes, no further frames. EOF without a record, extra bytes, invalid UTF-8, an invalid channel, launcher exit, timeout, Node restart or any ownership change closes the channel and disposes the pending preview. So does the two-minute preview expiry: a timer bound to the preview fires without any further request. The scoped reader destroys its readable, awaits closure and clears its frame buffers (this does not zeroize decoded strings or Node's internal buffers). Successful apply disables bootstrap for that profile permanently.

The confirmation record is an internal channel frame, not a third operation or a general IPC protocol.

State and invariants

The Node owns one bootstrap aggregate with revision 0 (unowned) or 1 (owned). There is no way back to unowned and no import, reset, replace-owner, force or all target.

Apply is one SQLite transaction that creates exactly:

  • one ControlNodeIdentity with the Node-reserved ID and current epoch;
  • one active User;
  • one active local Device owned by that User, authentication kind local-os-profile-owner, no generated key material;
  • one active CLI ClientRegistration for that Device;
  • one active team Workspace (kind: team-workspace);
  • one active WorkspaceUserMembership with role owner;
  • one active WorkspaceDeviceMembership that cannot outlive or elevate the User membership;
  • one active ClientActiveContext for that User, client and Workspace;
  • one ordered bootstrap event and one durable receipt.

Every new aggregate and policy revision starts at 1. All rows commit together or not at all. No legacy execution Workspace, workspace_memberships or active_workspace_context row is read, rewritten or treated as team authority.

After commit, the profile bearer stays the process-scoped local transport credential. Apply issues no credential or key.

Resolving that bearer to the fixed bootstrap User, Device and client is Planned; this slice does not build it. The rows exist, but no request resolves to them yet, and the later managed-credential slice must provide the mapping before any request acts as the owner and before remote or additional-device access exists. Until then the bearer's actor is the legacy actor_local_user (D-138).

Operations

Two release-disabled internal operations. They enter the public registry only when protocol, SDK, Node handler, CLI, receipt, event, authorization and contract parity land together in one vertical slice.

team-workspace.bootstrap.preview parameters:

{
  "schemaVersion": 1,
  "commandId": "command_01",
  "expectedBootstrapRevision": "0",
  "ownerDisplayName": "Ada",
  "workspaceName": "Ada's Workspace"
}
  • commandId uses the existing 1–128 character protocol identifier grammar.
  • Names are NFC, contain no NUL or control character, have no leading or trailing whitespace, and encode to 1–160 UTF-8 bytes. Unnormalized input is rejected, not rewritten.
  • No other IDs, role, authority field, credential, proof, path, profile or client timestamp is accepted.

The Node resolves the profile, OS-owner evidence, Node epoch and bootstrap revision, reserves all new IDs, and returns:

{
  "schemaVersion": 1,
  "method": "team-workspace.bootstrap.preview",
  "releaseQualified": false,
  "authoritySource": "local-os-profile-owner",
  "commandId": "command_01",
  "expectedBootstrapRevision": "0",
  "nodeEpoch": "epoch_01",
  "nodeId": "node_01",
  "userId": "user_01",
  "deviceId": "device_01",
  "clientId": "client_01",
  "teamWorkspaceId": "team_workspace_01",
  "role": "owner",
  "inputDigest": "sha256:<64 lowercase hex characters>",
  "previewId": "preview_01",
  "issuedAt": "<RFC 3339 timestamp>",
  "expiresAt": "<RFC 3339 timestamp>"
}

The digest is SHA-256 over canonical UTF-8 JSON (sorted keys, no insignificant whitespace) of the apply method, schema version, command ID, expected revision, both normalized names, Node ID, Node epoch and every reserved ID. Preview writes nothing durable. A profile holds at most one pending preview; it lives two minutes and never survives restart. Preview issues no proof token; the binding values are not authority. The matching inherited confirmation stays Node-private evidence of the approved ceremony.

team-workspace.bootstrap.apply repeats the preview intent and adds only the binding values:

{
  "schemaVersion": 1,
  "commandId": "command_01",
  "expectedBootstrapRevision": "0",
  "ownerDisplayName": "Ada",
  "workspaceName": "Ada's Workspace",
  "previewId": "preview_01",
  "inputDigest": "sha256:<64 lowercase hex characters>"
}
  • Before storage the Node rechecks the OS owner and profile metadata, Node identity and epoch, revision 0, preview expiry, digest, every reserved ID and the matching unread FD 3 confirmation. An apply that waits behind another write is judged again when it runs: it verifies the OS owner once it holds the Node's write permit, and inside the SQLite transaction, after BEGIN IMMEDIATE returns, it re-reads ownership and the revision and judges expiry against a clock read at that moment. The same reading stamps every row and the receipt, so a wait for the database lock cannot carry a stale decision or time into the commit.
  • The result is bounded to the created IDs, the fixed owner role, bootstrap revision 1, delivery (new or exact duplicate) and the existing receipt shape. It returns no proof, credential, key, path, OS identifier or raw authority evidence.
  • Receipt: method team-workspace.bootstrap.apply, target the new Node with type control-node-bootstrap, actor the new User, aggregate revision 1.
  • Same command ID and request returns the prior result without another write. The receipt stores a hash of the whole apply request, preview ID included, and a retry returns duplicate only when its hash matches exactly. Same command ID with another digest fails with idempotency_conflict. Same command ID and digest with another name or preview ID fails with bootstrap_binding_mismatch, after success and after a restart.
  • No third read operation: the apply result is the initial projection. After an unknown transport outcome the CLI calls receipt.get(commandId) on the same profile; its stored result is that same bounded projection (created IDs, owner role, revision 1, delivery). The command stops its Node when it exits, so the recovery output names the profile: start that profile's Node with maia-node --desktop --profile <name>, then read the receipt. If no receipt exists and the Node is still unowned with the same live preview and unread confirmation, apply may be retried with the same values. Any inconclusive retry (saturated, unavailable, deadline, transport or an unrecognised code) leaves the outcome unknown, because the first apply may still be running or may have committed; only a success, a replay or a typed bootstrap refusal replaces it. An owned Node without the matching receipt is bootstrap_integrity_failed, never permission to bootstrap again.

Errors

A closed error set; every failure mutates nothing:

CodeCondition
bootstrap_already_completedany owner or bootstrap revision already exists
bootstrap_authority_unavailableunsafe or changed OS ownership or profile metadata
bootstrap_confirmation_requiredno exact unread inherited confirmation
bootstrap_channel_invalidinherited channel is absent, invalid, oversized, malformed or has extra data
bootstrap_preview_expiredpreview or confirmation deadline elapsed
bootstrap_binding_mismatchdigest, command, preview, names, IDs, Node identity or epoch differ
revision_conflictexpected revision is not the current revision 0
idempotency_conflictcommand ID already binds another normalized digest
bootstrap_integrity_failedpartial or inconsistent ownership evidence is observed
saturated (admission)another bootstrap call is running; retry with the same values

bootstrap_already_completed wins over bearer possession or a new confirmation: there is no implicit takeover. A call that overlaps a running bootstrap call is refused with saturated; the CLI retries that refusal with the same values, and a matching retry after the commit returns duplicate, so the same intent commits once. A conflicting apply fails. Mismatched attempts do not consume a matching confirmation, but the single preview and its deadline bound them. Success, expiry, channel failure, ownership change or restart disposes all pending material.

Acceptance

Required negative evidence, in priority order:

  1. P0, no takeover: an existing owner, revision 1, partial ownership rows or legacy Workspace data never allows a second bootstrap or a repair path.
  2. P0, bearer is not enough: a valid bearer plus exact preview values but no matching confirmation runs no read or write transaction.
  3. P0, exact binding: a wrong command, digest, preview ID, epoch, reserved ID, normalized name, expiry or profile metadata mutates nothing.
  4. P0, atomicity and recovery: a failure injected at every row, event or receipt write leaves revision 0; a commit with a lost response is recovered only through the same-profile receipt.
  5. P1, concurrency: same intent gives one commit and, on retry, one duplicate; different intent gives one commit and one deterministic conflict; an overlapping bootstrap call is refused at once and never queues, and an altered name or preview ID is never acknowledged as the committed request.
  6. P1, disposal: EOF, timeout, extra frame, launcher exit, restart and success each close the channel and dispose the pending confirmation.
  7. P1, strict schema: caller-supplied IDs, role, authority, credential, proof, path, UID, profile, timestamp or unknown fields are rejected.

Schema tests cover three groups: valid preview and apply shapes; rejection of caller authority and malformed bindings; and result, receipt and owner relationships. Authority, transaction, concurrency and disposal evidence belongs to Node and CLI boundary tests.

Implementation slice

One change, in repository order:

  1. the two Effect schemas and their three contract test groups;
  2. both release-disabled methods in the registry with risk, confirmation, receipt, error and CLI-only exposure metadata;
  3. SDK bindings that never expose the inherited confirmation or channel internals;
  4. one scoped Node bootstrap service, two handlers, the FD 3 adapter and one atomic SQLite transaction;
  5. maia node bootstrap only: controlling-terminal confirmation, exact JSON output, receipt recovery and built local tests.

No invitations, member administration, grants, remote transport, Keychain, Desktop or mobile projection, dynamic registry loading or other operation in this slice. No bootstrap or team Workspace capability may be claimed until all five steps and their parity checks land.

Imported from Maia OS docs at 3dc3e07