You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Consumers that hold a credential under single-owner custody (no sidecar; the plugin's local copy is a tombstone after import) have no recovery path from a needs_reauth latch today: import --replace reads a source file, and the source is gone. The only recovery is a fresh login, and that login must write the new family into the vault. On the wire there is get / status / report_auth_failure only; import is CLI-only (master-key challenge or the offline lease). So a plugin's recovery story is currently "the operator runs ck auth", which is coherent (single-owner = the vault owns login) but blocks unattended fallbacks. Raised by the openai-auth adoption (four accounts, single-owner directive); anthropic-auth's C10 (login auto-import) is deferred on the same gap.
Threat-model constraint
Capability handles are read capabilities. A leaked handle must stay a read-only loss — never a record-poisoning or latch-by-write one. So the verb must not be handle-bound.
Proposal
One verb, existing records only:credential.replace { credential_id, material } — creation stays CLI (an id is an operator decision). Returns the new record_version; bumps version; audited as its own AuditOp::Replace with the principal.
Authority = a principal-scoped grant.read_grants already carries an operation enum per (principal_kind, principal_id, credential_prefix); add GrantOperation::Replace. Route-bound principal only (the daemon names it); minted by the operator with ck auth grant … --op replace.
Material is verified by use before commit. The auth: records carry an account identity; set-identity and import set it, replace keeps it #30 label-vs-claim guard is not a security boundary (account_id_for_adapter parses the JWT claim unverified; a forged claim passes). The daemon must make one upstream call with the incoming family — the adapter's non-mutating validity check where one exists, else a refresh — and commit only on success. A replace that fails verification writes an auth_events row and leaves the record untouched.
Rate/abuse: counted against the connection's fetch limiter like report_auth_failure; a failed verification is an alarm row, not a refusal.
Non-goals
Not a consumer-side create; not a handle-bound write; not a replacement for the CLI path (which stays the operator's tool and the audit's route-admin actor).
Why
Consumers that hold a credential under single-owner custody (no sidecar; the plugin's local copy is a tombstone after import) have no recovery path from a
needs_reauthlatch today:import --replacereads a source file, and the source is gone. The only recovery is a fresh login, and that login must write the new family into the vault. On the wire there isget/status/report_auth_failureonly;importis CLI-only (master-key challenge or the offline lease). So a plugin's recovery story is currently "the operator runsck auth", which is coherent (single-owner = the vault owns login) but blocks unattended fallbacks. Raised by the openai-auth adoption (four accounts, single-owner directive); anthropic-auth's C10 (login auto-import) is deferred on the same gap.Threat-model constraint
Capability handles are read capabilities. A leaked handle must stay a read-only loss — never a record-poisoning or latch-by-write one. So the verb must not be handle-bound.
Proposal
credential.replace { credential_id, material }— creation stays CLI (an id is an operator decision). Returns the newrecord_version; bumps version; audited as its ownAuditOp::Replacewith the principal.read_grantsalready carries an operation enum per(principal_kind, principal_id, credential_prefix); addGrantOperation::Replace. Route-bound principal only (the daemon names it); minted by the operator withck auth grant … --op replace.account_id_for_adapterparses the JWT claim unverified; a forged claim passes). The daemon must make one upstream call with the incoming family — the adapter's non-mutating validity check where one exists, else a refresh — and commit only on success. A replace that fails verification writes anauth_eventsrow and leaves the record untouched.report_auth_failure; a failed verification is an alarm row, not a refusal.Non-goals
Not a consumer-side create; not a handle-bound write; not a replacement for the CLI path (which stays the operator's tool and the audit's
route-adminactor).Related: #17, #30 (guard), #31 (refresh policy), anthropic-auth C10.