Skip to content

Multi-account credentials per provider family: id convention + import sources (custody side of insula#1 GAP-1/2) #2

Closed

Description

@iceteaSA

Hit this while provisioning the vault on our Linux box (context: insula#1, filed by us — GAP-1/GAP-2 there cover the consumption side; this issue is the custody side).

Problem

There is no way to seal a second account in the same provider family, and no id convention for one if there were.

Concretely, on this box:

  • anthropic: two OAuth accounts (main + the fallback account). ck auth import --source opencode --provider anthropic reads opencode's auth.json, which only carries the currently-active account. The fallback account lives in the harness auth plugin's own state file (anthropic-auth.json, accounts array keyed by account uuid) — no import source reads it.
  • openai: three Codex accounts, same shape — auth.json has the active one, the other two live in the openai-auth plugin's sidebar state.
  • antigravity: one paid + three free accounts in antigravity-accounts.json. The free accounts' tokens rotate constantly under the plugin's own rotation logic, so a vault snapshot of them goes stale in hours — these probably should NOT be sealed until the vault is the refresher, which makes the id question sharper for the paid+free split.

We stopped at one credential per family (oauth:anthropic, chatgpt:openai, antigravity:google) rather than inventing an id scheme (oauth:anthropic:<uuid>?) that triage here might contradict.

Asks

  1. Is a multi-account id convention planned? If you name one (suffix, separate namespace, whatever), we'll adopt it immediately and report back from a real multi-account box.
  2. Should import grow sources for harness multi-account state files, or is the intended path "run ck auth login once per account" (which mints vault-native credentials and sidesteps the import question)?

Related: the consumer side (how insula's family map would address a second account) is GAP-1/GAP-2 in insula#1 — answering it in either repo likely answers both.

Dual-custody note

Until the vault is the sole refresher, every sealed OAuth credential has two independent refreshers (harness auth plugin + vault adapter) racing on the same refresh token. For our five primaries that risk is worth it because insula consumes them; for fallback accounts there is no consumer yet, so we're deliberately holding them out. If there's a recommended posture here (e.g. seal-but-disable-refresh), the runbook is where we'd expect it.

Activity

  1. iceteaSA commented on Aug 9, 2026

    @iceteaSA
    ContributorAuthor

    Dual-custody datum upgraded from anecdote to deterministic: three occurrences now, and the trigger is identified — every harness restart re-authenticates the Anthropic OAuth account (token rotation), which invalidates the vault's sealed snapshot and flips it to needs_reauth. On this box that means the vault copy of an actively-refreshed OAuth credential goes stale on EVERY restart of the harness that owns the live refresher, and the day-N re-seal (module stop → import --replace → start) is now routine maintenance rather than exception handling.

    This strengthens the case for whichever direction triage picks: either the vault becomes the sole refresher (harness reads through handles, snapshot never stales) or import grows a watch/re-import mode for externally-refreshed credentials. Happy to test either on this deployment — we hit the path several times a day.

  2. iceteaSA commented on Aug 12, 2026

    @iceteaSA
    ContributorAuthor

    Trigger-analysis data from three restart events since the last comment, because they discriminate between the two candidate mechanisms:

    • 2026-08-10 14:42 — daemon OOM-kill + auto-restart, ~41h since last re-seal: oauth:anthropic came back needs_reauth. Re-sealed.
    • 2026-08-10 14:56 — full box reboot, 14 minutes after that re-seal: came back clean, no re-seal needed. (Two opencode-harness restarts the same evening: also clean.)
    • 2026-08-12 ~15:30 — module drain-restart (binary swap), ~2 days since last re-seal: needs_reauth again. Re-sealed.

    So the discriminator held: restart class doesn't matter — elapsed harness re-auths since the last import do. A restart 14 minutes after a fresh import is clean regardless of being a full reboot; a restart days later surfaces staleness regardless of being a gentle drain. The restart isn't the trigger, it's the reveal — the vault copy went stale at whatever moment opencode last rotated the refresh token, and the module only notices when it next constructs the credential.

    Operationally that means the re-seal cadence is set by the harness's rotation frequency, not by daemon lifecycle events, and a scheduled re-import (or the fetch-on-miss the issue proposes) would close it independent of restart hygiene.

  3. ualtinok commented on Aug 15, 2026

    @ualtinok
    Contributor

    The id convention exists and is shipped — you stopped one step short of something already supported, so please go ahead.

    The grammar is <method>:<provider>[:<label>], parsed in credentials-core/src/credential_id.rs. The label is a free operator-chosen string, not a uuid. Method is what selects the refresh adapter, which is why it leads.

    Live inventory on my own vault right now:

    oauth:anthropic
    oauth:anthropic:ufuk2
    oauth:anthropic:yiyi
    chatgpt:openai
    chatgpt:openai:gmail
    apikey:openai
    

    Three Anthropic accounts and two ChatGPT ones side by side, plus an OpenAI API key that does not collide with the OAuth entries because the method differs. apikey:openai and chatgpt:openai are different credentials with different adapters.

    A label is worth choosing for a human, not for uniqueness — oauth:anthropic:work reads better in ck auth list and in an audit row than an account uuid, and the vault never needs the id to match the provider's own identifier.

    Two things in your report that are correct and are not solved by the convention:

    1. Import sources only read what the harness exposes. --source opencode reads auth.json, which carries the active account only. The fallback accounts living in plugin-private state files have no import source, and you are right that this is a gap in the import path rather than in the id scheme.

    2. You are right to hold off on the antigravity free accounts. Sealing a credential whose tokens rotate under someone else's control produces a snapshot that goes stale in hours — that is dual custody, and the staleness is the symptom. See my reply on Vault-as-mirror: re-import from the registered source on change, so a rotating harness credential stops requiring manual re-seals #4: the fix is not a better mirror, it is not having one.

    For accounts you can obtain interactively, ck auth login --provider anthropic --id oauth:anthropic:work mints an independent credential directly, with no import source needed and no plugin state to read.

  4. ualtinok commented on Aug 15, 2026

    @ualtinok
    Contributor

    Your trigger analysis predates my earlier reply and I had not read it when I wrote that — worth saying, because your discriminator is better evidence than the argument I made, and it independently reaches the same place.

    "The restart isn't the trigger, it's the reveal" is exactly right, and the three-event table proves it in the only way that counts: a full reboot 14 minutes after a fresh import came back clean, while a gentle drain-restart two days later did not. Restart class varied and outcome did not; elapsed harness re-auths varied and outcome followed. That is a controlled comparison, not an anecdote, and it rules out the mechanism most people would have assumed.

    It also means the re-seal cadence is set by the harness's rotation frequency — which is not a number you control, and it is why the treadmill has no stable maintenance interval. Four occurrences in 24h on your box, per your note on #4.

    Where it lands: your conclusion offered two directions, and the first is already built. ck auth login makes the vault the sole refresher, so the snapshot cannot stale because there is no snapshot. The second (watch/re-import for externally-refreshed credentials) is the one I declined on #4, for the reason you accepted there: it keeps a violation of the one-refresher invariant fresh rather than removing it.

    The gap your data does leave open is the one this issue actually filed: import sources only read what the harness exposes. For an account you can log into interactively, native login sidesteps it entirely. For one you cannot — no interactive path, credential only obtainable from a plugin's private state — import remains the only door, and that door reads one account per family. That is a real limitation, unaddressed, and it is separate from the staleness treadmill your restart data characterises.

    If you hit a provider in that category during your migration wave, say which one. A concrete case is worth more than the general shape, and it would tell me whether the missing piece is more import sources or a way to hand the vault a credential the harness already holds.

  5. iceteaSA commented on Sep 4, 2026

    @iceteaSA
    ContributorAuthor

    Update: the ID convention is shipped as <method>:<provider>[:<account>] (credential_id.rs), and record identity landed in fa4cec4 (PR #30). The remaining open part is import of plugin-private fallback-account state, which is now taking shape as a writer-side ck auth migrate-plugin verb (preserved at tag followup/manifest-lock-and-exports on the fork, to follow PR #28).

  6. iceteaSA commented on Sep 23, 2026

    @iceteaSA
    ContributorAuthor

    @ualtinok, following up on this one. It has been quiet since my 09-04 update, and I think the direction note on #5 has overtaken it.

    On your question about the concrete case: there is one, and it didn't need a new import source. On 09-18 two OpenAI fallback accounts that existed only in the openai-auth plugin's private state went into the vault as chatgpt:openai:acct-a (audit seq 1639) and chatgpt:openai:<label> (seq 1640). I staged each account's bare {refresh, access, expires} entry to tmpfs and ran ck auth import without --provider, passing --account-id so the identity check ran against the token's claim. Both have served since then. oauth:anthropic:<label> came in the same way on 08-31, and was later replaced by a real ck auth login on 09-22 after it latched.

    So the gap this issue was left open for (plugin-private fallback accounts) has been handled by hand-staging plus import. The migrate-plugin verb I mentioned in the last update is superseded by #5: the OpenCode verbs collapse into ck auth import, and the vault stops writing consumer files.

    Unless you want this open for a different import source, I'd close it in favour of #5. Is that fine with you?

  7. ckcred-alfonso commented on Oct 5, 2026

    @ckcred-alfonso

    Closing in favour of #5, as you suggested.

    The ID convention (<method>:<provider>[:<account>]) and record identity shipped, and the plugin-private fallback accounts went in through ck auth import with --account-id, so no new import source was needed. The vault serves several accounts per provider today: five Claude accounts and two ChatGPT accounts.

    Thanks for carrying this one through.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions