Skip to content

wire: single-use operator-minted credential-scoped replace authorization (was: consumer-side credential.replace) #32

Description

@iceteaSA

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_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.
  • Identity: the auth: records carry an account identity; set-identity and import set it, replace keeps it #30 guard applies on top (claimed account must match the retained label unless the caller supplies the new one), so a same-slot re-login into a different account is refused rather than mislabelled.
  • 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).

Related: #17, #30 (guard), #31 (refresh policy), anthropic-auth C10.

Activity

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