Slice 2: machine principal, create_workspace and start_workspace #159

Open
opened 2026-09-15 18:25:48 +02:00 by lz · 0 comments
Owner

The two tools that need a vault key, and the credential class that lets them have one. Deliberately held back from slice 1 so the privilege escalation gets its own design attention instead of competing with ten tools for it.

Depends on the whole of slice 1 (#154 at minimum).

Why these two are different

Verified: spawnWorker(db, docker, config, masterKey, input) decrypts the provider token to build the authenticated clone URL, and startWorker re-injects secrets. Both need the vault key. createSession, deleteSession, stopWorker and removeWorker do not.

The key by design exists only in the operator's unlocked web session, and a bearer credential deliberately never sets locals.masterKey (AGENTS.md fact #24). So an MCP credential cannot reach these without a new principal.

What the envelope vault already gives us

PR #146 shipped the seam. vault_keys(principal_kind, principal_id, kdf_salt, kdf_params, wrapped_dek) holds one row per principal; exactly one exists today, ('master','master'). auth/vault-envelope.ts exports newDek, unwrapDek, readPrincipal, insertPrincipal, upsertPrincipal. A machine principal is a row insert — that was the entire point.

Open questions this issue must answer

The vault spec deliberately deferred them: "Deciding a token's KDF (a 256-bit CSPRNG credential argues for HKDF over Argon2id, by the same reasoning AGENTS.md fact #24 gives for hashing tokens with SHA-256) belongs to whoever adds that principal."

  1. KDF. Argon2id defends a low-entropy human secret. A CSPRNG machine credential is not one, so HKDF is the likely answer — but say why in the spec, not in a commit message.
  2. Minting. The DEK exists only in an unlocked session, so the principal can only be created while the operator is unlocked. That is a real constraint on the UI flow.
  3. Rotation. rotateMasterPassphrase re-wraps the master's DEK and touches no data column. What happens to a machine principal's wrapper? Nothing, if it wraps the same DEK — confirm and state it.
  4. Revocation. Deleting the row must actually revoke, and insertPrincipal vs upsertPrincipal matters: only rotation may overwrite an existing wrapper, because replacing one with a DEK the rows were never sealed under destroys the vault silently.

The hard constraint

A vault-bearing credential must be a separate credential class from the GET-only nxs_ tokens, which must not silently acquire it. This is stated in both #140 and the vault design. It also means the consent screen changes: slice 1 shows "Create or start a workspace" as explicitly not granted, and that line has to become real rather than quietly disappearing.

Granting a machine principal a wrapped DEK gives that credential the full vault — every connection token, every sealed env var. That is a deliberate escalation and the UI must say so at the point of granting, not in a doc.

The open vault follow-ups from PR #146#141, #142, #143, #144, #145 — are worth resolving before adding a second principal, since several concern what happens when a vault_keys row is lost or unopenable, and a second row makes each of those situations more likely rather than less.

Done when

Claude can create a workspace from a phone; the credential granting that is visibly distinct from a read-only one; and losing or revoking it is a tested path rather than an assumed one.

The two tools that need a vault key, and the credential class that lets them have one. Deliberately held back from slice 1 so the privilege escalation gets its own design attention instead of competing with ten tools for it. Depends on the whole of slice 1 (#154 at minimum). ## Why these two are different Verified: `spawnWorker(db, docker, config, masterKey, input)` decrypts the provider token to build the authenticated clone URL, and `startWorker` re-injects secrets. Both need the vault key. `createSession`, `deleteSession`, `stopWorker` and `removeWorker` do not. The key by design exists only in the operator's unlocked web session, and a bearer credential deliberately never sets `locals.masterKey` (AGENTS.md fact #24). So an MCP credential cannot reach these without a new principal. ## What the envelope vault already gives us PR #146 shipped the seam. `vault_keys(principal_kind, principal_id, kdf_salt, kdf_params, wrapped_dek)` holds one row per principal; exactly one exists today, `('master','master')`. `auth/vault-envelope.ts` exports `newDek`, `unwrapDek`, `readPrincipal`, `insertPrincipal`, `upsertPrincipal`. **A machine principal is a row insert** — that was the entire point. ## Open questions this issue must answer The vault spec deliberately deferred them: *"Deciding a token's KDF (a 256-bit CSPRNG credential argues for HKDF over Argon2id, by the same reasoning AGENTS.md fact #24 gives for hashing tokens with SHA-256) belongs to whoever adds that principal."* 1. **KDF.** Argon2id defends a low-entropy human secret. A CSPRNG machine credential is not one, so HKDF is the likely answer — but say why in the spec, not in a commit message. 2. **Minting.** The DEK exists only in an unlocked session, so the principal can only be created *while the operator is unlocked*. That is a real constraint on the UI flow. 3. **Rotation.** `rotateMasterPassphrase` re-wraps the master's DEK and touches no data column. What happens to a machine principal's wrapper? Nothing, if it wraps the same DEK — confirm and state it. 4. **Revocation.** Deleting the row must actually revoke, and `insertPrincipal` vs `upsertPrincipal` matters: only rotation may overwrite an existing wrapper, because replacing one with a DEK the rows were never sealed under destroys the vault silently. ## The hard constraint **A vault-bearing credential must be a separate credential class** from the GET-only `nxs_` tokens, which must not silently acquire it. This is stated in both #140 and the vault design. It also means the consent screen changes: slice 1 shows "Create or start a workspace" as explicitly **not granted**, and that line has to become real rather than quietly disappearing. Granting a machine principal a wrapped DEK gives that credential the **full vault** — every connection token, every sealed env var. That is a deliberate escalation and the UI must say so at the point of granting, not in a doc. ## Related The open vault follow-ups from PR #146 — #141, #142, #143, #144, #145 — are worth resolving before adding a second principal, since several concern what happens when a `vault_keys` row is lost or unopenable, and a second row makes each of those situations more likely rather than less. ## Done when Claude can create a workspace from a phone; the credential granting that is visibly distinct from a read-only one; and losing or revoking it is a tested path rather than an assumed one.
lz added this to the MCP support (#140) milestone 2026-09-15 18:25:48 +02:00
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lz/agent-nexus#159
No description provided.