The problem
There are two controls that look similar, live in different places, and have radically
different blast radius — and the safe one reports itself using the same word as the
destructive one.
Unassign — the X in harness detail → Keys
(app/(dashboard)/harnesses/[id]/page.tsx:316 unassignKey)
PUT /api/keys/{id} with assignedTo filtered to drop just this harness
- The key survives and stays attached to every other agent
- Correctly recreates this agent so the credential is stripped from the running
process (the inline comment citing D3 explains why recreate and not quick —
good, that part is right)
- But the toast says
"{provider} key removed"
Delete — the Keys page (app/(dashboard)/keys/page.tsx:461 → confirm → deleteKey,
handler at app/api/keys/[id]/route.ts:50)
services.keys.remove(id) drops it from the store and strips the credential from
every assigned agent's .env
- Then recreates every affected agent
- Irreversible: the secret value is gone
Why it matters
An operator who clicks the X and reads "key removed" has no way to know whether they
just detached one agent or destroyed a credential shared across the fleet. That ambiguity
was hit for real today: a gh key (nimbleco-interim) was removed and the operator could
not tell afterwards whether it had been deleted everywhere or merely unassigned.
The only way to find out is to go and look at whether the key is still listed on the Keys
page — which is a diagnosis step that shouldn't be necessary.
Suggested fixes
- Change the unassign toast to name the scope:
"{provider} key unassigned from {agent}", and if the key is still attached elsewhere, "still assigned to N other agent(s)". Never the bare word removed for a non-destructive action.
- Make the delete confirmation state the blast radius before it happens: "Delete
{provider} key? This removes the stored secret and strips it from N agent(s), which
will be recreated. This cannot be undone." Currently the confirm button carries no
count.
- Distinguish the affordances — the per-agent control should read
Unassign (or
carry an unlink icon), not a bare X that reads as a delete.
- Optional: show the assignment count next to each key on the harness Keys list, so the
shared-credential case is visible at the point of action.
Note
The underlying behaviour looks correct in both paths — this is a labelling and
disclosure defect, not a logic bug. The recreate-not-restart handling in both paths is
right and worth keeping.
The problem
There are two controls that look similar, live in different places, and have radically
different blast radius — and the safe one reports itself using the same word as the
destructive one.
Unassign — the
Xin harness detail → Keys(
app/(dashboard)/harnesses/[id]/page.tsx:316unassignKey)PUT /api/keys/{id}withassignedTofiltered to drop just this harnessprocess (the inline comment citing D3 explains why
recreateand notquick—good, that part is right)
"{provider} key removed"Delete — the Keys page (
app/(dashboard)/keys/page.tsx:461→ confirm →deleteKey,handler at
app/api/keys/[id]/route.ts:50)services.keys.remove(id)drops it from the store and strips the credential fromevery assigned agent's
.envWhy it matters
An operator who clicks the
Xand reads "key removed" has no way to know whether theyjust detached one agent or destroyed a credential shared across the fleet. That ambiguity
was hit for real today: a
ghkey (nimbleco-interim) was removed and the operator couldnot tell afterwards whether it had been deleted everywhere or merely unassigned.
The only way to find out is to go and look at whether the key is still listed on the Keys
page — which is a diagnosis step that shouldn't be necessary.
Suggested fixes
"{provider} key unassigned from {agent}", and if the key is still attached elsewhere,"still assigned to N other agent(s)". Never the bare word removed for a non-destructive action.{provider} key? This removes the stored secret and strips it from N agent(s), which
will be recreated. This cannot be undone." Currently the confirm button carries no
count.
Unassign(orcarry an unlink icon), not a bare
Xthat reads as a delete.shared-credential case is visible at the point of action.
Note
The underlying behaviour looks correct in both paths — this is a labelling and
disclosure defect, not a logic bug. The
recreate-not-restarthandling in both paths isright and worth keeping.