# Team environments (/docs/environments/team)

Share one environment with your team — roles, a Linux account and port range per member, a shared repository, the build lock and isolation modes.

Several people (and their agents) can work in the same environment. Each member gets their own identity inside the machine so one person's credentials are never readable by another, and the daemon coordinates the things that must not run twice.

## Roles [#roles]

| Role | May |
| --- | --- |
| `viewer` | See the environment, its members, ports and events; attach. No Linux account is provisioned for SSH. |
| `developer` | Everything a viewer may, plus publish ports up to `authenticated`, list secret names and register their SSH key. |
| `maintainer` | Plus start, stop, archive, rename, change the idle timeout, add/remove members, publish `public` ports, set and delete secrets, mint enrolment tokens (BYO). |
| `owner` | Plus destroy, grant or remove `owner`. An environment always keeps at least one owner (`409 last_owner`). |

The environment's creator is its owner. Org owners and admins of the environment's org act as `owner`; plain org members see it as `viewer`. Anyone may leave an environment.

**Add a member** in the hub (environment page → *Members* → e-mail + role; the person needs a codai account with that address) or with `POST /v1/environments/{id}/members` and `{ "email": "ana@example.com", "role": "developer" }`.

## Per-member identity [#per-member-identity]

When the daemon runs as a root system service in `per-user` mode (`CODAI_SERVICE_SCOPE=system`, the default for managed machines), every member gets:

| What | Detail |
| --- | --- |
| Linux account | `u-` plus the first 8 hex characters of your user id — the same on every machine. Created on first use (`useradd -m`, group `codai-dev`, home `0750`), shown as *Linux user* in the hub. Shell commands and terminals drop to this account. |
| Git identity | `~/.gitconfig` with the member's name and e-mail. |
| Worktree | `~/work/<project-slug>` on the default branch, or on `u/<user>` when that branch is already checked out by someone else. Your file operations are confined to your worktree and home. |
| Port range | A block of 100 ports starting at 30000, 30100, … Exported to your commands as `PORT` (the first port) and `CODAI_PORT_RANGE_START`, `CODAI_PORT_RANGE_END`, `CODAI_PORT_RANGE`, so two members' dev servers do not collide. |
| SSH access | Paste your public key under *Your SSH key* (or `PUT /v1/environments/{id}/members/me/ssh-key`). The daemon writes it to your `~/.ssh/authorized_keys` and the hub shows *Installed*. Viewers are not provisioned. |

### Removing a member [#removing-a-member]

When someone is removed from the environment — or demoted to `viewer` — the daemon offboards their Linux account in order, and each step appears in the environment's events (`member.offboard.<step>`):

1. **locked** — the account is locked and expired, and their SSH key is removed;
2. **killed** — their sessions, processes and container are stopped;
3. **archived** — any commits not pushed anywhere and any uncommitted changes are saved to the shared repository as `refs/archive/<user>/<time>/<branch>` (their branch is not moved; archive refs are never deleted);
4. **deleted** — 30 days later, if they have not come back, the account and home are deleted.

Adding them back within 30 days unlocks the account (**restored**) with their home and worktrees intact.

If the daemon runs without root (`shared` identity — the default for a user-scope BYO install), everyone runs as the same Unix user, `codaid status` reports `identity_unsafe: true`, and the agent is told to treat the machine as untrusted for secrets. Use shared mode only for a machine you use alone.

## Shared repository [#shared-repository]

A project with a `repo_url` gets one shared bare repository at `/srv/codai/repos/<slug>.git` (group `codai-dev`), fetched once and used by every member's worktree. To clone a private repository, store a token as the environment secret `CODAI_GIT_TOKEN` (`PUT /v1/environments/{id}/secrets/CODAI_GIT_TOKEN`); the daemon passes it to git through an askpass helper, never on the command line. Delivery of the secret value to the daemon is rolling out — until your gateway exposes it, the daemon clones without a token (public repositories only).

## Build lock [#build-lock]

Two builds in one repository at the same time corrupt each other's output. A shell command sent with `"lock": "build"` takes an exclusive, non-blocking OS lock:

- per repository per environment in per-user mode (every member's worktree of the same project shares one lock);
- released automatically if the holder crashes;
- a second caller gets `{ "ok": false, "error": "lock_held: <holder>" }` immediately instead of waiting.

Held locks appear in the relay status (`locks`), on the desktop card as a lock glyph, and in `codaid status`. Who is attached — people and agents — is shown in the same places.

## Isolation modes [#isolation-modes]

Chosen per project (`isolation`), by whoever manages the project:

| Mode | What you get | Status |
| --- | --- | --- |
| `shared_vm` (default) | One machine; each member works as their own Linux user with their own worktree and port range. | Live |
| `container` | One rootless podman container per member (`codai-<slug>-<user>`), built from the repository's `devcontainer.json` (`.devcontainer/devcontainer.json` or `.devcontainer.json`; falls back to `mcr.microsoft.com/devcontainers/base:ubuntu-24.04`), running as the member's own uid with the shared repository and worktree mounted at their host paths, on the host network with the member's port range. Shells and terminals run inside the container; file operations stay on the host path. | Live (per-user daemon + `podman`) |
| `vm_per_dev` | One environment per developer, pinned to them (`for_user_id`). Creating a second answers `409 already_has_env` with the id to reuse; such environments have exactly one member. | Live |

Supported `devcontainer.json` fields in `container` mode: `image`, `build.{dockerfile,context,args,target}`, `forwardPorts`, `containerEnv`, `remoteEnv`, `containerUser`, `remoteUser`, `postCreateCommand` (once), `postStartCommand`, `mounts`, `runArgs`, `workspaceFolder`, `overrideCommand`. Docker Compose is not supported.

**Limits.** Each member container may use every CPU of the machine but no more (`--cpus`), 75 % of its RAM with no extra swap, and 4096 processes; it runs with `no-new-privileges` and all capabilities dropped except CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID, KILL and NET_BIND_SERVICE. If the machine does not delegate the cgroup CPU/memory controllers, the container starts without the CPU/memory limit and the daemon logs a warning.

**`runArgs` allowlist.** Only `--add-host`, `-e`/`--env`, `--shm-size`, `--init` and `--label` are accepted. Anything else — for example `--privileged`, `--network`, `--pid`, `--ipc`, `--cap-add`, `--security-opt`, `--device`, `--user` — fails the container start with an error naming the flag; remove it from `runArgs`. `mounts` and `-v` may only point inside your worktree or home (or be a named volume or tmpfs).

In the hub, `vm_per_dev` projects group environments by developer (*Your machine*, *Machine of …*), and each developer creates their own from the project page.
