tenant-iso-leftovers
Fixes the five items in discussion #219 (2026-10-03 tenant-isolation audit). Each one is a case where something outlived the app or the membership it belonged to.
What changes
- Purge leaves no credentials behind.
PurgeAppRowdeletes the app'scli_tokensrows (kindappandservice) in the same transaction as the row. They are deleted rather than revoked becauseRenameAppInandMoveApprefuse a name that still has any such row, revoked or not. Door keys are revoked, and the revision is bumped so the empty record outranks what boxes already admitted. The console purge door publishes the record right away. The auto-purge path (MaterializeApps) leaves it to the 12-hour re-sign. - The finding was partly wrong.MoveAppalready refused stale destination tokens. What it missed was door keys. It now revokes the name's door keys on both sides. Keys carry their account's sqid, so they could never have followed the app anyway. The move door republishes both records. - Purge sweeps ledgers.
release.PurgeAppalso deletes everything underledgers/<acct>/<app>/, before the marker.release.LedgerAppPrefixis now the one definition of that prefix;internal/ledgerand the console's ledger list build on it. - Consumers that parse the shape. The edge's status cache served a stale copy on any store error, including not-found. Without a fix there, a purged public ledger would keep serving from every edge that had read it until the process restarted. The cache now drops a gone ledger object. A blip still serves stale, and status-page files keep the old rule. -TestLedgerServingStaleManifestHonoredused to simulate an outage by deleting the directory. It now usesflakyStore. - Legacy app-token revoke requires membership.
/auth/cli/tokens/revokerefuses a kindapptoken unless the minter is still a member of the token's account. The refusal comes before the passkey step and fails closed on a read error. - Invite accept is a POST.
GET /invite/{token}now only renders a confirm card. The card checks the same three gates through the new read-onlyPendingInviteFor.POST /invite/{token}runs session, then CSRF, thenAcceptInvite's gates, which play the owner check's part. Existing links land on the card. - NorequireFreshAuth. Joining gives the joiner no power over anyone else, and step-up fails closed when Keymail cannot do it. That would block every invite. A person should confirm this choice. - Fleet names: no change. Fleet names are keys in the deployment bucket (
fleets/<fleet>/…, read by edge/storedoor, host/agent and the economics pass) and in channel identities, so scoping them per account is a wire change. The 409 already says only "that name is taken", never by whom. It is the same whether the caller's own account holds the name or someone else's does. Probing and squatting are bounded by the per-day create budget (allowCreate). Nothing further can be done without breaking uniqueness.
Tests
Each new test failed before its fix:
TestPurgeAppRowLeavesNoCredentialsForTheNameTestPurgeAppRowKeepsALiveAppsCredentialsTestMoveAppLeavesNoLiveDoorKeysForTheNameTestDeleteNowSweepsLedgersAndDoorKeys(handler: ledgers swept, empty door-key record published)TestPurgeAppSweepsTheAppsLedgersTestLedgerServingStopsAfterPurgeTestPersonalSettingsRefusesAnAppTokenRevokeAfterRemovalTestInviteAcceptCreatesMembership, now extended: the GET joins nothing, a POST without CSRF gets 403, and the link is single use
Gate results:
gofmtandgo vetare clean.go buildpasses, but neededGOFLAGS=-buildvcs=falselocally because of a stray~/.cache/tmp/.git.make testhas failures that are all environmental: unix-socket paths too long under a long TMPDIR (activator, edge purge listener, instanceexec), and the root package exiting on the operator's own~/.carlos/config. The root package fails the same way on clean main.
Deliberately not done
- No migration for tokens or door keys left by purges that happened before this change. A token bound to a name with no app row may be a legacy owners-table app, so cleaning those up needs a human decision.
carlos accounts migratedoes not move ledgers. That predates this change and is out of scope.
Commits
0
Discussions
1
1 open
Tasks
0/0
done
Discussions 1 open
Tasks
- No tasks yet.
Activity
- claude agent, operated by paul@keymail.dev replied to Purge and membership leftovers: tokens, door keys and ledgers outlive their app; GET invite accept; global fleet names
- claude agent, operated by paul@keymail.dev replied to Purge and membership leftovers: tokens, door keys and ledgers outlive their app; GET invite accept; global fleet names
- claude agent, operated by paul@keymail.dev opened Purge and membership leftovers: tokens, door keys and ledgers outlive their app; GET invite accept; global fleet names