# Machines

A **device** is an enrolled machine in one workspace. A computer can be enrolled in several workspaces; each enrolment is a separate device with its own key and scope.

## Owners and keys

A device is **owned** by the member whose machine it is, or by a team. It holds an ES256 (P-256) key:

| Backend | Where | Key storage |
| --- | --- | --- |
| `secure-enclave` | macOS on Apple silicon, chosen by default; the Secure Enclave helper is embedded in the macOS binary and extracted under `MEHEN_HOME/bin` on first use | A non-exportable Secure Enclave key. Only an enclave-wrapped blob sits in a 0600 file. Keys use "after first unlock" protection so background agents can sign while the screen is locked. |
| `software` | Linux, CI, containers, or `--backend software` | A P-256 key in a 0600 file in the workspace's directory on the machine. `mehen` refuses a key file that other users can read. |

There is no bearer token. Every machine request carries a proof signed by this key ([Keys, proofs and signing](https://docs.mehen.dev/security/keys-and-signing.md#device-proofs)). A key belongs to one workspace at one vault: its scope file records both, a key made for another is never reused, and the same computer enrolled in two workspaces holds two keys in two slots.

Key and configuration files must be regular files, owned by this user, private and not symlinks. Mehen makes an owned state directory private if its permissions are too wide. The ledger's rules differ: an unsafe or symlinked ledger is not trusted as existing state, and is read as empty and replaced.

## Enrolment

1. **The machine asks.** `mehen enrol --workspace <slug> --name my-laptop` (`--team <team-slug>` for a machine a team will own) reuses the key in the workspace's slot when it can vouch for it, otherwise it creates one; `--fresh-key` always creates one. A key is scoped to the vault and workspace it was made for: with another `MEHEN_VAULT_URL` it is refused with `key_scope`, never overwritten.
2. **The vault answers with a link.** It returns an 8 character code, the link `https://app.mehen.dev/w/<slug>/e/<code>`, a lease that ends in 60 minutes and a hard cap 24 hours out. `--connect <code>`, with a connect code from the workspace's Connect an agent page, **claims** the attempt for that account: only that account can accept it, and it lands in that account's Needs you. The CLI computes the key's fingerprint itself and prints the ask: the link, the fingerprint, the four words and which group the page hides. There is one live link per key: a new attempt for the same key supersedes the older one.
3. **The person whose machine it is accepts it.** They open the link, sign in, and see the machine's name, host, backend, when and from where it asked, the words and three of the four fingerprint groups, and the complete scope it will get. They type the hidden group and accept with a passkey that signs the workspace, the enrolment, the key's full thumbprint, their own account and membership, the whole scope including the private-credentials tick, and the start context. The server checks the typed group against the stored key before it honours the passkey. Nobody, owner or admin, can accept a machine on a member's behalf. A guest's machine then also needs **admission** by an insider who manages every project the scope names, a distinct action that never counts as acceptance; a team's machine is admitted by an owner or admin instead of accepted. Owners and admins are told that a machine was added, with a link to narrow or revoke it.
4. **The machine finishes.** `mehen enrol` records the attempt, prints the ask and exits 0 (`--wait` waits in place). `mehen wait enr_...` (or `vault_wait`) from any process polls until the attempt is active. A live attempt is listed and can be accepted; when nothing renews it, it **lapses** (it cannot be accepted, "This link has lapsed") but stays revivable: `mehen wait` renews it in place, with the same id, code, link and fingerprint, when under five minutes remain, at most once a minute and never past the hard cap. An attempt is **terminal** when it is active, rejected or expired. Only a terminal `expired` calls for a new attempt with `mehen enrol`, which keeps the key. Five wrong typed groups reject an attempt across any number of renewals.

On `active`, `mehen` checks the answer before it writes anything: the returned workspace id agrees with what it asked for, the slug matches the requested workspace, the owner is of the requested kind (self or team), and the scope is complete. That does not by itself verify a particular human account or team; read the accepted owner that `mehen wait` prints. If activation does not match, `mehen` writes no new configuration, attempts to revoke the returned device and removes only files that belong to that attempt. Cleanup can fail; read the reported outcome, and keep any device id it names visible for a human to revoke. Two processes finishing the same attempt with the same key is one success, never a revoke; two different devices approved for one workspace on one machine is `already_enrolled`, which revokes and deletes nothing.

## Scope and reach

A device's scope is a **ceiling under its owner's rights**, never a grant of its own:

| Field | Meaning |
| --- | --- |
| `tags` | Items the device may use: an item must carry one of these tags. Empty means every item its owner may use. |
| `projects` | Projects the device may use. Empty means every project its owner may use, and only such a member-owned device may create projects. |
| `maxEnvironment` | `unprotected` or `protected`: whether the device may ever use a protected environment or a protected credential, given that its owner has the protected right there. |
| `modes` | Which rungs of the [exposure ladder](https://docs.mehen.dev/concepts/credentials.md#the-exposure-ladder) this device may use at all. Never `reveal`. |
| `privateCredentials` | Whether the device may use its owner's private credentials and private overrides. Only a member-owned device; a team's is always false. Without it, the owner's private credentials are `not_found` to the machine. |

**Reach** is what a device can actually do: for each project, its owner's effective access, with protected runs only when `maxEnvironment` is `protected`; a workspace credential only for a member's device (owner, admin or member, never a guest's or a team's); a project credential where the owner has `use`; a credential a protected environment references only inside that environment, with the owner's protected right; a private credential only for the owning member's ticked device, and never in a protected environment. Reach is re-evaluated on every request: a suspended or removed owner makes every call `owner_inactive`, an expired share or a lost project `not_found` or `project_unshared`, and a device never outlives its owner's standing.

## Narrowing, widening and revoking

A device is active or revoked. A revoked device's proofs are refused with `device_revoked`. A device can revoke itself (`mehen revoke`); its owner, an owner or an admin can revoke it from the browser with a passkey.

**Narrowing** a device's scope (every field a subset, `maxEnvironment` not raised, the private tick not turned on) is done by its owner, an owner or an admin with a passkey. **Widening** one is proposed and then accepted by the device's own owner with their passkey, as at enrolment; a team's device is widened by an owner or admin, and can never get the private tick. A change applies from the device's next request, live runs included. The audit records the scope before and after.

Revocation stops subsequent uses through Mehen. It cannot recall an upstream request already sent, undo what happened at the provider, erase a value already leased into a process, or revoke a credential at its provider; rotate an exposed credential at the provider ([A machine or a key is compromised](https://docs.mehen.dev/guides/incident.md)).

The Machines page shows each device as a card with its key's four words, key type, owner, last seen, status and what it may use. A member sees their own machines and their teams'; owners and admins see all.

A non-exportable key cannot be copied off the machine, but any process running as the same user can ask it to sign. The device key proves which machine is asking, not which process. It is not what keeps secrets from an agent: that is the vault never returning values, the bindings, the owner's rights capping the machine, and approvals that need a passkey.

## Fingerprints and the hidden group

A key's fingerprint is what a human compares between the terminal and the enrol page. The CLI, the server and the page derive it the same way:

- **Glyphs.** Four groups of four characters from an alphabet with no I, L, O or U, such as `5NZV DP67 PXT0 DB6E`. When a human types a group, `I` and `L` read as `1` and `O` as `0`.
- **Words.** Four words, each with a colour name: `puppy (violet) potato (indigo) juice (mint) oven (olive)`. The colour name is always printed beside the swatch, so colour alone never carries the check. The words are a quick look; the glyphs and the typed group are the check.
- **The hidden group.** The enrol page shows three groups and renders the fourth as dots. Which one is hidden depends on the enrolment code, so it moves when the link is re-issued. The page never carries the hidden group, the full fingerprint or the key; the server compares what was typed when the owner accepts, counts every attempt, and rejects the enrolment at the fifth wrong group.

`mehen status` prints the fingerprint recorded at enrolment and the Machines page shows the words, so a machine can be matched to a card at any time. The derivation is in [Keys, proofs and signing](https://docs.mehen.dev/security/keys-and-signing.md#fingerprints).

## Sessions

A session is a label an agent puts on its requests: `MEHEN_SESSION` for `mehen` and `mehen mcp`, 1 to 120 letters, digits, `.`, `:`, `_` or `-`. It might be a session id, a task id, a subagent run, or a label an app sets from its conversation. It is for the audit log and for the details of an intake, connect or approval page. It grants nothing, and because an agent chooses it, no page infers who asked from it: the "who asked" line is always the enrolled device's name ("An agent on my-laptop"), which its owner accepted at enrolment. Grants are per device and request digest, not per session.

## Workspaces on a machine

A machine identity's state lives under one root, `MEHEN_HOME` (default `~/.config/mehen`): one **slot** per workspace, `workspaces/<origin>/<tn_id>/` once active (its configuration, the device key and its scope file, and the ledger) and `pending/<origin>/<slug>/` while an enrolment waits. Slots are keyed by origin and workspace id, so a rename costs nothing. They are discovered from disk, and a directory that is not a slot is named by `mehen doctor` and never deleted. Locks serialise enrolment, finishing and revocation per slot.

Harness registrations and skills remain per OS user, and installation and update state sits beside the executable, so separate homes do not create separate MCP profiles or separate updater installations.

**Selection.** Every command selects a workspace before it signs or sends anything:

1. **The ceiling** `MEHEN_WORKSPACES` (comma-separated slugs, `tn_` ids or `<origin>/<slug or id>`; unset or `*` for every slot, empty for none) is the set this process may use. It only narrows, and nothing inside a process widens it.
2. A checkout's `mehen.toml` without `workspace` while several slots are allowed is `workspace_unbound`.
3. Then `--workspace <slug or id>` or `MEHEN_WORKSPACE`; then the checkout's `mehen.toml` (`workspace` and `vault`); for `mehen wait`, the one slot that records the id; then the only active slot.

There is no default: several and nothing deciding is `context_ambiguous` for a command that needs one, while `status`, `doctor`, `ls`, `pending` and `workspace list` span the allowed set. These refusals are local, exit 2, and sent before anything is signed: `workspace_not_allowed`, `workspace_unknown`, `workspace_mismatch`, `context_ambiguous`, `workspace_unbound`, `config_dir_retired`. A workspace's name is data from the vault, never a selector; only the id and the canonical slug select. `mehen status` reports a renamed workspace's current slug and name without recording them; `mehen workspace list --check` and `vault_status` can each record them for the slot after a successful signed check. `mehen workspace list --check` refuses an answer for another workspace or origin, or a slot that changed while it asked, as `workspace_mismatch` and changes nothing; `vault_status` keeps the recorded names when the answer names another workspace.

The ceiling is an accident control, not an identity boundary: an environment variable can be edited. **A separate `MEHEN_HOME`** selects a separate local identity store, a separate machine to the vault with its own keys and slots. Commands using one home do not discover the other's slots. That is organisation under one OS user, not isolation from other processes running as the same user. [Several workspaces on one machine](https://docs.mehen.dev/guides/several-workspaces.md) and [A second identity on the same machine](https://docs.mehen.dev/guides/second-identity.md) are the how-to.

**Managed hosts.** An agent launcher that pins each agent to one workspace places a marker under the user's home directory as the system's user database records it, never as `HOME` says. Commands that open Mehen's state fail closed with `key_insecure` when that home cannot be resolved or its path is unsafe. There the pinned slot and `MEHEN_MANAGED_CONTEXT`, checked against the host's pin map, are the sole authority: no manifest or ceiling widens it, and a selector that disagrees refuses. Managed selections fail closed: an outdated managed-agent configuration is `pin_outdated`, and only the harness owner can repair it.

**One MCP server.** One `mehen mcp` server can serve several allowed workspaces, with a separate identity latch for each. Its startup workspace, the checkout's manifest, a managed pin and the harness ceiling may restrict that set, and a tool argument cannot widen it. A slot whose enrolment later changes device, workspace or origin, or vanishes, is refused for the life of the process with restart advice, while the other slots keep serving. `mehen setup` writes one `mehen` entry per harness, carrying the ceiling in `MEHEN_WORKSPACES` whenever it is restricted, so setup never makes a server wider than the shell it ran in.
