Conversation
A derived identifier encodes a convention as a constraint. The custody handle manifest previously derived the expected vault credential id from the account label (`oauth:anthropic:<label>`) and rejected any entry that disagreed. That encodes the writer's naming convention as a parser invariant, so the parse-time comparison can only catch two fields disagreeing that the same writer wrote together — it is a corruption check, not an authorization check. The live flip it blocked: the main account's vault credential id is the unlabelled provider-default `oauth:anthropic`, which the derivation rule rejected as non-canonical. Writing the true id yielded `corrupt-binding` at parse time; writing the derived id yielded `credential_identity_mismatch` at runtime because the vault returns the real one. Carry the identifier verbatim. `readCustodyHandles` no longer requires `entry.credential_id === custodyCredentialId(label)`; `resolveCustodyHandle` matches on label alone and returns the entry's own `credentialId`. The runtime fence in `packages/opencode/src/custody-mode.ts` (`credential.credentialId !== binding.credentialId` -> refuse) keeps vault ground truth on one side and is unchanged. Label becomes the sole lookup key, so a new guard rejects duplicate labels per provider block: every entry sharing the duplicated label goes to `corruptLabels` rather than silently picking a winner. The preflight freshness gate was passing `min_ttl_ms = refresh_floor + 30min` (~4.5 h) to `cache.get`. In Claustrum that value is a staleness FLOOR — a get below it forces an upstream refresh. The vault refreshes these credentials close to their token lifetime, so a credential sits below a 4.5 h floor for most of its cycle, silently rotating during a readiness check. Replace with a small serving margin (5 min, named constant) — the vault owns rotation, and the preflight only needs "fresh enough to serve the takeover". The `TAKEOVER_INCOMPLETE_MAIN_REAL` refusal still pointed at `ck auth migrate-plugin`, which does not exist on a deployed Claustrum binary. Reword to the supported path: mint a handle with `ck auth mint-handle` and the manifest entry is written by this plugin.
There was a problem hiding this comment.
1 issue found across 8 files
Confidence score: 3/5
- In
packages/core/src/claustrum.ts, local login can resolve a valid vault credential under a non-derived ID but fail to remove that manifest binding because cleanup still enforcesoauth:anthropic:${label}; this can leave stale custody state behind. Update cleanup to use the resolved credential ID and add coverage for mismatched IDs.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/core/src/claustrum.ts">
<violation number="1" location="packages/core/src/claustrum.ts:412">
P1: When a valid vault credential ID differs from `oauth:anthropic:${label}`, local login resolves the new manifest binding but cannot clear it because `removeCustodyHandleManifestEntry` still enforces the derived ID. Accept any validated non-empty credential ID in the removal validation.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| !isValidCustodyCredentialId(entry.credential_id) || | ||
| (provider === 'anthropic' && | ||
| entry.credential_id !== custodyCredentialId(entry.label)) | ||
| !isValidCustodyCredentialId(entry.credential_id) |
There was a problem hiding this comment.
P1: When a valid vault credential ID differs from oauth:anthropic:${label}, local login resolves the new manifest binding but cannot clear it because removeCustodyHandleManifestEntry still enforces the derived ID. Accept any validated non-empty credential ID in the removal validation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/claustrum.ts, line 412:
<comment>When a valid vault credential ID differs from `oauth:anthropic:${label}`, local login resolves the new manifest binding but cannot clear it because `removeCustodyHandleManifestEntry` still enforces the derived ID. Accept any validated non-empty credential ID in the removal validation.</comment>
<file context>
@@ -388,13 +409,15 @@ export function readCustodyHandles(
- !isValidCustodyCredentialId(entry.credential_id) ||
- (provider === 'anthropic' &&
- entry.credential_id !== custodyCredentialId(entry.label))
+ !isValidCustodyCredentialId(entry.credential_id)
) {
corruptLabels.add(entry.label)
</file context>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found by attempting the custody flip on a live deployment. The preflight refused, correctly, and the refusal was unfixable from config — the main account's vault credential id cannot be expressed in the handle manifest at all.
The blocker
The manifest binds an account label to a Claustrum credential id. The parser derived the expected id from the label (
oauth:anthropic:<label>) and rejected any entry that disagreed. Main's credential lives on the unlabelled provider-default lane,oauth:anthropic, so both encodings refuse:credential_idoauth:anthropic(the real one)corrupt-bindingat parseoauth:anthropic:main(the derived one)credential_identity_mismatchat runtime — the vault returns the real idVerified against the parser before writing anything, and again after the fix:
Not specific to main.
antigravity:googleis unlabelled the same way; anything whose id does not happen to match its label hits this.Why carry the id rather than special-case main
A derived identifier encodes a convention as a constraint. Conventions change; parser constraints do not.
The parse-time equality check compared the manifest against itself — two fields the same writer wrote in the same breath. It can catch corruption, not a wrong id. The check that means something is the runtime fence in
custody-mode.ts:That one has vault ground truth on one side. It is unchanged here. Dropping the derivation loses no failure path: a typo'd id gets
not_foundfrom the vault (uniform mask, fails closed), a mismatched id gets the fence.Special-casing
mainwould leave the derivation in place and add an exception to it — working today, broken again at the next unlabelled credential.Changes
readCustodyHandlescarriescredential_idverbatim; all other validation (handle shape, label validity, superseded parsing, corrupt-label path) is untouched.resolveCustodyHandlematches on label and returns the entry's own id.corruptLabels. This replaces what the derivation was incidentally doing.refresh_floor + 30min(~4.5h) tocache.get. In Claustrum that value is a staleness floor — a get below it forces an upstream refresh, so a readiness check silently rotated the credential it was about to use. Because the vault refreshes on a cadence close to the token lifetime, a credential sits below a 4.5h floor for most of its cycle: main is below it for ~4.5h of every ~8h. Replaced with a 5-minute serving margin (CUSTODY_PREFLIGHT_MIN_TTL_MS). The vault owns rotation; the preflight only needs the credential to serve the takeover.TAKEOVER_INCOMPLETE_MAIN_REALpointed atck auth migrate-plugin, which exists on no deployed Claustrum binary — it is parked on an unmerged branch. Reworded to the supported path (ck auth mint-handle; this plugin writes the manifest entry).Verification
Each behaviour is pinned by a mutation of implemented logic, not a missing symbol.
returns the entry's own credential id verbatim(Expected "resolved", received "unresolved") and the duplicate-label test (Set {} vs Set { "alice" }).preflight hands a small serving margin to cache.get even when expiry is far below the refresh floor— the credential expires in 1h, comfortably above the 5-minute margin and far below the old floor. The test asserts the argument handed tocache.get, not just the outcome, so it cannot pass for the wrong reason.ck auth mint-handleassertion.Not included
Whether main should eventually be re-homed onto a 3-segment id in the vault. That is a real option with a migration — Insula's handle binding, the sealer, both recorders, the latch-watch and the rotation probe all key on the literal
oauth:anthropic— and it should not happen as a side effect of unblocking a flip.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Fixes custody takeover preflight being blocked for credentials whose vault id doesn't match their label, like the main account's
oauth:anthropic. The manifest previously derived the expected credential id from the label and rejected real ids at parse time; it now carriescredential_idverbatim, and the runtime fence incustody-mode.tsremains the authority.Bug Fixes
refresh_floor + 30minasmin_ttl_mstocache.get, which treats it as a staleness floor and forced an upstream refresh during a readiness check; it now uses a 5-minute serving margin (CUSTODY_PREFLIGHT_MIN_TTL_MS).TAKEOVER_INCOMPLETE_MAIN_REALguidance pointed at the nonexistentck auth migrate-plugin; it now suggestsck auth mint-handle.Written for commit 5659207. Summary will update on new commits.