Encrypted workspaces

Operator guide

This is the guide for running an encrypted workspace: turning encryption on, enrolling and approving the devices and agents that read it, rotating the key epoch, offboarding a member, and turning tenant recovery on or off.

Encryption is per workspace and one way. A workspace is either plaintext or encrypted, and there is no operation in the product that turns it back off. Read the whole of the first procedure before you run any of it.

Two of these procedures are dashboard flows and three are requests against core-api. The requests are shown as curl so they can be run and read exactly. Every one of them is also what the dashboard sends, so the two paths cannot disagree.

preview, independent audit not completeOne way, per workspaceKeys never leave the endpoint
Read first

What encryption does not do

Three limits shape every procedure below. They are properties of the design rather than gaps waiting to be closed, and they are published in full on the security page a customer can read before they buy.

Removing a member stops future reads, not past ones

Removing a member or a device raises the workspace key epoch, and new envelopes are encrypted only to the remaining recipients. It does not un-share envelopes the removed device could already read. Anything it decrypted or copied before removal stays readable to whoever holds that copy.

The same applies to recovery. Turning recovery off stops the recovery key from being added to new envelopes. It does not remove the recovery key from envelopes that already list it as a recipient.

For an offboarding that must exclude past content, run a workspace rewrap. An authorized client downloads every envelope, decrypts it, re-encrypts it under a fresh epoch, and re-uploads it. It is expensive and it is the honest answer.

Recovery is a recipient you control, not a key we hold

Recovery is opt in per tenant and off by default. When you enable it, your tenant's recovery public key becomes an additional recipient on every new envelope. You hold the private half. Doug Fir Labs does not hold it, does not escrow it, and cannot reconstruct it.

If you do not enable recovery and every device key is lost, the data is unrecoverable. Not slow to recover, not recoverable with a support ticket. Gone. We say this before you turn strict mode on, because there is no version of that conversation that goes well afterward.

Enabling recovery, disabling it, and every use of it are recorded in an audit chain that workspace members can see.

A hosted model provider still sees what you send it

End-to-end encryption protects your traces in storage and in transit between your agent hosts and Traceblox. It cannot hide content from a hosted model provider that has to process that content to answer a prompt. If your agent sends reasoning to a third party model API, that provider sees it in the clear, and no setting here changes that.

  • Run inference locally, so no third party receives the content.
  • Minimize on the client, so only the parts a model needs leave the endpoint.
  • Use attested confidential compute, where the provider can prove what runs on the content.

Who may run these procedures

  • Reading and appending envelopes needs workspace:read. The payload is already opaque to the server, so ordinary members do this.
  • Enrolling an identity, bumping an epoch, and every migration control route need members:manage, which is the owner or admin role. These change who can read the workspace.
  • A credential without members:manage gets HTTP 403 with the error code e2ee/forbidden. That is the correct answer, not a bug, and the fix is to ask an owner or an admin.
Procedure

Turn on encryption for a workspace

The working path today is the migration flow on the encryption settings page. It encrypts every trace, atom, edge, and summary in the browser, uploads the encrypted copies, reads every one of them back and decrypts it, and only then removes the readable copies.

Nothing readable is removed until every planned record has been read back and decrypted successfully. If any record fails that check the run stops and the readable copies stay exactly as they are. The database enforces this a second time and refuses the flip on its own.

Owner or admin, on a browser that already holds an approved key.

  1. DoOpen the dashboard, then Settings, then Encryption, at /settings/encryption.ExpectThe Encryption card shows this workspace's state. A plaintext workspace shows the card headed "Migrate this workspace to encryption".If notA redirect to /login means the session expired. Sign in again and return to /settings/encryption.
  2. DoMake sure this browser holds an approved key. If the enrollment card says this browser has no key, run the enrollment procedure below first and come back.ExpectThe enrollment card reads "This browser can read this workspace".If notStarting without one is refused before anything is written, with the blocker no-enrolled-device. The related blockers are device-revoked, no-recipients, and sender-not-a-recipient, and each names the key that is missing.
  3. DoChoose recovery. Select either "Turn recovery on. Our organization holds the recovery key." or "Leave recovery off. Only the keys on the recipient list can read this workspace."ExpectOne of the two options is selected. The choice applies to everything this migration encrypts.If notThere is no default and no third answer. Leaving it unselected holds the start button disabled and reports the blocker recovery-decision-missing.
  4. DoType the confirmation literal encryption-is-one-way in the confirmation field.ExpectThe button "Encrypt this workspace" becomes available.If notWithout it the blocker is acknowledgement-missing. The server checks the same literal, so a request that skips the browser is refused too, with HTTP 400 and the hint that names the literal.
  5. DoSelect "Preview the plan" to see the counts before you commit to them.ExpectThe plan shows how many dags, atoms, edges, and summaries will be encrypted, and an estimate of how long it takes. The counts are read from the workspace as it stands right now.If notA preview writes nothing, so there is no destructive failure here. An empty plan means the workspace has no content to migrate, and encryption still applies to everything written afterwards.
  6. DoSelect "Encrypt this workspace".ExpectProgress moves through planning, uploading, verifying, and completing, and ends at done. The phase text says what is happening and what is still in place.If notA verification mismatch stops the run and aborts it. Nothing is removed and the workspace is unchanged. See the runbook for a failed or interrupted migration.
  7. DoReload Settings, then Encryption, at /settings/encryption and read the Encryption card.ExpectThe card shows the encrypted state, and "Who can read this workspace" lists every key on the recipient set including the tenant recovery key if you enabled one.If notIf the card still shows plaintext, the flip did not happen. Check the migration state before assuming data loss: the readable copies are only removed after the flip, so a workspace that is still plaintext still has everything.
  • Server-side search stops working in this workspace. Traceblox cannot match text it cannot read, so each browser searches a copy it decrypted itself. The first search in a new browser downloads the workspace and decrypts it, and shows progress while it does.
  • Trace pages stop rendering on the server. A browser with no approved key sees an empty trace rather than the content, and the page says so rather than spinning.
  • Billing changes shape. An encrypted workspace is metered on encrypted bytes stored and served instead of on search scans.
  • There is no route that reverses the transition and there will not be one. Try it on a workspace you are willing to keep encrypted.
Procedure

Enroll a browser or an agent, and approve it

A device generates its own key pair and publishes only the public halves. It becomes able to read the workspace when an approver adds its key id to the recipient set, which happens on an epoch bump and is recorded with the reason, the actor, and the resulting member set.

There is no server-side pending flag. A device is readable when an envelope names its key as a recipient and it is not readable when none does, and the dashboard reports exactly that rather than a status field.

Anyone may enroll their own key. Approving one needs members:manage.

  1. DoOn the new browser, open Settings, then Encryption, at /settings/encryption, type a name in "Name this browser", and select "Generate a key for this browser".ExpectThe key pair is generated in the browser and only the public halves are published. The panel changes to "Waiting for approval" and shows this browser's key id.If not"Could not publish this browser's public key" means the publish request failed. The key stays on this browser, so retrying is safe. A body that carries a private half is refused with e2ee/private-key-material-rejected, which is the gate working.
  2. DoCopy the key id shown under "Give this key id to an approver" and send it to an approver.ExpectThe approver has the exact recipient key id, which is what they will confirm against.If notIf no key id is shown, this browser has not generated a key yet. Generate one first.
  3. DoOn a browser that already reads this workspace, open the same page and find the new key under "Browsers waiting for approval". Confirm the key id matches, then select "Approve".ExpectThe epoch bumps with the reason member_added, and the recipient list gains a row for the new key. Content encrypted from the new epoch onward is readable by it.If notHTTP 403 with e2ee/forbidden means this session lacks members:manage. Ask an owner or an admin. The button is shown rather than hidden so the answer is "ask an admin" and not "nothing happened".
  4. DoBack on the new browser, reload the page.ExpectThe enrollment card reads "This browser can read this workspace".If notStill pending means the approval did not include this key id. Read the directory with GET /e2ee/identities and confirm the key is present, not revoked, and in the member set the last epoch bump established.
  • Approval is forward-only. The new key can read content encrypted from its first epoch onward and cannot read what came before. If the device must read the history, that is a workspace rewrap, not a second approval.
  • An agent identity enrolls the same way, with kind agent, and the agent host holds its own private half. An MCP peer that writes to an encrypted workspace must also negotiate the com.dougfirlabs/scholia-e2ee capability, or it is refused with e2ee/capability-required rather than downgraded to plaintext.
  • An epoch bump replaces the member set, it does not add to it. Every approval sends the whole set it intends to leave in place, so a hand-written request that names only the new key removes everybody else.
Procedure

Rotate the key epoch

A workspace has a monotonically increasing key epoch. New envelopes are wrapped to the recipient set the current epoch established, so rotating is how the recipient set changes.

Rotation on its own is not revocation and it is not re-encryption. It changes who can read what is written next.

Owner or admin. The route is gated on members:manage.

  1. DoRead the current directory so you can send the full intended member set. Every key id must belong to an enrolled, non-revoked identity.ExpectA JSON list of identities, each with recipient_key_id, kind, revoked, and eligible_recipient.If notAn empty list means nothing is enrolled in this tenant yet, and there is nobody to encrypt to.
    shell
    curl -sS "$CORE_API_ORIGIN/e2ee/identities" \
      -H "authorization: Bearer $SCHOLIA_SESSION_JWT"
  2. DoPost the bump with a reason from the fixed list, the identity performing it, and every key id that should still read this workspace.ExpectHTTP 201 and an epoch entry carrying the new epoch number, the reason, the actor, and the member set. The server always writes current_epoch plus one.If note2ee/epoch-rollback (409) means the epoch you named is at or below the current one. Omit epoch entirely, or send exactly current plus one. e2ee/recipient-not-enrolled (400) names a key id that is unknown or revoked. e2ee/actor-not-enrolled (400) means actor_identity_id is not an enrolled, non-revoked identity. e2ee/no-recipients (400) means member_key_ids was empty. e2ee/workspace-not-encrypted (409) means this workspace is still plaintext and there is no epoch to bump.
    shell
    curl -sS -X POST "$CORE_API_ORIGIN/e2ee/epoch" \
      -H "authorization: Bearer $SCHOLIA_SESSION_JWT" \
      -H "content-type: application/json" \
      -d '{
        "reason": "manual_rotation",
        "actor_identity_id": "<your enrolled identity id>",
        "member_key_ids": ["<every key id that should still read this workspace>"]
      }'
  3. DoConfirm the result on Settings, then Encryption, at /settings/encryption under "Who can read this workspace".ExpectThe list matches the member set you sent, and nothing you intended to keep is missing.If notA key you meant to keep is missing because the bump replaced the set rather than adding to it. Bump again with the complete set. The removed key could still read everything up to the previous epoch, so this is a mistake to correct, not one to ignore.
  • The reason must be one of workspace_created, member_added, member_removed, device_revoked, recovery_enabled, recovery_disabled, rewrap, or manual_rotation. An unrecognized reason is refused with e2ee/invalid-request and a hint listing the accepted values.
  • Every bump is appended to the epoch log with its previous-entry hash, so a rotation nobody can explain is not a state this system allows.
Procedure

Offboard a member

Offboarding has two halves that are easy to confuse. Removing a person's workspace membership stops them reaching the API. Revoking their key stops their key being a recipient of anything written from the next epoch onward. Neither takes back what that key could already read.

Do both, then decide whether this offboarding needs the third thing, which is a workspace rewrap.

Owner or admin, in this order. Skipping a step leaves the offboarding incomplete.

  1. DoRemove the person's workspace membership from the Members page at /members.ExpectThey can no longer authenticate to the workspace, so they cannot pull envelopes from Traceblox at all.If notHTTP 403 means this session lacks members:manage. This step is authorization, and encryption does not replace it: leaving membership in place leaves a working credential in the hands of someone you removed.
  2. DoRevoke every key that belongs to them. Republish the identity record with revoked set to true, once per identity id.ExpectHTTP 200 with revoked true and eligible_recipient false. The recipient list shows the key with a Revoked badge rather than removing the row.If note2ee/identity-immutable (409) means the request tried to change the published keys rather than the revocation state. Revocation is one way and there is no request shape that clears it, so a revocation made in error is corrected by enrolling a new key, not by undoing this one.
    shell
    curl -sS -X POST "$CORE_API_ORIGIN/e2ee/identities" \
      -H "authorization: Bearer $SCHOLIA_SESSION_JWT" \
      -H "content-type: application/json" \
      -d '{ "identity": <the published identity record>, "revoked": true }'
  3. DoBump the epoch with the reason member_removed, naming every key that should still read the workspace. Use device_revoked instead when you are removing one device rather than a person.ExpectHTTP 201. New envelopes are wrapped only to the remaining recipients, and an attempt to wrap one to the revoked key is refused with e2ee/recipient-not-enrolled.If note2ee/recipient-not-enrolled here usually means the revoked key is still in the list you sent. Remove it and send the set again.
    shell
    curl -sS -X POST "$CORE_API_ORIGIN/e2ee/epoch" \
      -H "authorization: Bearer $SCHOLIA_SESSION_JWT" \
      -H "content-type: application/json" \
      -d '{
        "reason": "member_removed",
        "actor_identity_id": "<your enrolled identity id>",
        "member_key_ids": ["<every remaining active key id>"]
      }'
  4. DoDecide whether this offboarding needs a workspace rewrap. Read the note below before you answer, and record the decision.ExpectA written decision either way. A rewrap that nobody decided against is an offboarding that quietly left the old content readable by the removed key.If notIf you decide yes and the rewrap fails partway, it is safe to run again. A rewrap skips any envelope already at the target epoch and already wrapped to exactly the target recipient set, so a retried batch does no work twice.
  • Revocation is forward-only. Bumping the epoch stops a removed key reading future envelopes. It cannot un-share the envelopes that key was already a recipient of.
  • A rewrap is warranted when the removed key must actually be excluded from content it could already read: a contested departure, a credential you believe was copied, a device lost while unlocked, or a contractual requirement to exclude. It is not warranted for a routine departure where the person kept no copy and the content is not sensitive to them holding one.
  • A rewrap is expensive and honest. An authorized client downloads every envelope, decrypts it, re-encrypts it under the current epoch and the current recipient set, and re-uploads it. It is proportional to the number of envelopes, it runs on one endpoint, and on a large workspace it is a job with a progress bar rather than a request.
  • A rewrap changes what is on Traceblox. It cannot reach a copy the removed device already downloaded and stored elsewhere. If you believe a copy was taken, the response is content-level, and no key operation substitutes for it.
Procedure

Turn tenant recovery on or off

Recovery is a second recipient your organization controls. It is off unless you turn it on. Its private half is generated and held by you, and enabling it, disabling it, and every use of it are recorded where workspace members can see them.

The moment to make this decision is before a workspace is encrypted, because that is the choice the migration applies to everything it encrypts. Turning it on later covers what is written later.

Owner or admin, plus whoever in your organization will custody the private half.

  1. DoGenerate the tenant recovery identity on a machine your organization controls, with kind tenant_recovery, and store the private half the way you store a root credential.ExpectA key pair whose private half never leaves that machine, and a custodian who can say where it is.If notIf nobody can name the custodian, recovery is not enabled, it is theatre. Stop here and fix that first.
  2. DoPublish the public halves to the tenant directory.ExpectHTTP 201 and a directory entry with kind tenant_recovery.If note2ee/private-key-material-rejected (400) means the body carried something shaped like a private half. The check matches on the field name with punctuation removed and rejects private, secret, seed, mnemonic, passphrase, keystore, dek, and wrapped_dek. Remove the field and republish the public record only.
    shell
    curl -sS -X POST "$CORE_API_ORIGIN/e2ee/identities" \
      -H "authorization: Bearer $SCHOLIA_SESSION_JWT" \
      -H "content-type: application/json" \
      -d '{ "identity": <the public tenant_recovery identity record> }'
  3. DoBump the epoch with the reason recovery_enabled, including the recovery key id in member_key_ids alongside every existing active key.ExpectHTTP 201. Every envelope sealed from this epoch onward lists the recovery key as a recipient.If note2ee/recipient-not-enrolled means the recovery key id is not in the directory yet. Publish it first.
  4. DoConfirm on Settings, then Encryption, at /settings/encryption under "Who can read this workspace".ExpectA row labelled Tenant recovery key, visible to every workspace member rather than to admins only.If notIf members cannot see it, that is a defect worth reporting. A standing second reader that members cannot see is the thing this product refuses to have.
  5. DoTo turn recovery off, revoke the recovery identity the same way you revoke any key, then bump the epoch with the reason recovery_disabled. The acknowledgement literal for this operation is existing-envelopes-stay-recoverable.ExpectThe recovery key stops being added to envelopes sealed from the new epoch onward, and the recipient list shows it as Revoked.If notThere is no failure mode that makes this retroactive, and that is the point. See the note below before you tell anyone that recovery has been removed.
  • Disabling recovery is not retroactive. Turning it off stops the recovery key from being added to new envelopes. Envelopes that already list it as a recipient keep it, and that key can still read them.
  • A workspace rewrap is the only operation that changes this, and it re-encrypts every envelope. If a customer needs the recovery key excluded from past content, a rewrap is the answer and the cost is real.
  • The private half is yours. Doug Fir Labs does not hold it, does not escrow it, and cannot reconstruct it, so losing it and every device key is the unrecoverable case.
When something goes wrong

Incident runbooks

Lost keys, a compromised device, a support request for data the platform cannot decrypt, and a migration that stopped partway all have their own procedures.

Read the incident runbooks