cadrumo.application.user_profile.login_session module

Transactional profile login over current password custody.

aeat config login resolves a committed capsule, evaluates its throttle, and authenticates that exact envelope-and-sentinel pair into an unbound candidate DEK session. It never consults a provider, a shared master key, or recovery material. Until candidate authentication, pointer compare-and-swap, and local binding have all succeeded, the active profile remains untouched.

Three distinct artefacts meet in this module, and reading any of them as another has twice produced a wrong conclusion about what the code does. They belong to different custody classes with different key requirements:

  • the acceleration receipt – keyring key plus a keystore sidecar record, OUTSIDE the encrypted store, wrapping an already-unlocked DEK. Revocable with no unlocked profile, which is why retirement can complete during recovery. Not a session: no counterparty, no protocol.

  • the live bucket session – the in-process BucketSession holding the unlocked DEK. Purely process-local, so it is absent in an ordinary invocation, every one of which is a fresh process.

  • the AEAT authority session – an encrypted row INSIDE the bucket, revocable only with the key. Owned by the auth package; named here only to keep it distinct.

Unqualified “session” in this module means the live bucket session and nothing else.

The optional acceleration receipt is exactly that: acceleration. Its absence or failure leaves a valid live bucket session; it never changes the outcome of password authentication. A prior profile is retired only after B is durably selected and locally bound.

bind_resumed_profile_session() is the read-side counterpart and the SINGLE resume authority: the login no-op path, the CLI root callback and the named-profile read path all call it, so the surfaces cannot drift.

See also

cadrumo.adapters.persistence.storage.custody.acceleration_receipt

The split-knowledge receipt this module mints and resumes.

logout_active_profile()

The symmetric strong close, which reuses close_profile_session_artefacts() from here.

exception ProfileLoginThrottledError(*, remaining_seconds)[source]

Bases: UserProfileError

Refuse a login attempt while the failed-attempt backoff is in force.

Raised BEFORE any Argon2id derivation runs, so a caller hammering passphrases cannot use the key-derivation function as a timing oracle. The remaining wait rides in context so the CLI can render it without re-parsing the message.

Parameters:

remaining_seconds (int)

Return type:

None

code
class ProfileCustodySessionOwnerEffect(*values)[source]

Bases: StrEnum

Verified outcome of one local custody session-owner operation.

REVOKED
REMOVED
VERIFIED_ABSENT
class ProfileLoginOutcome(**data)[source]

Bases: BaseModel

Typed result of one login invocation.

Carries no key material: the unlocked BucketSession is bound to the process through the active-session context variable, never to this record.

Variables:
  • bucket_id – Immutable UUID identity of the authenticated profile.

  • label – Operator-facing display label of that profile.

  • authenticated_at – UTC instant the session was established. On an idempotent no-op this is the ORIGINAL login instant, never re-stamped.

  • idle_deadline – Current sliding idle deadline.

  • absolute_deadline – Immutable absolute session cap.

  • session_persisted – False when the host has no usable OS keychain, so the session is process-scoped only.

  • already_authenticated – True when a still-valid persisted session was resumed as a no-op (no re-prompt, no new record).

  • closed_previous_bucket_id – Bucket whose session this login closed during a cross-profile handover, or None when the login re-entered the profile already selected. Populated whether or not that profile still had a live session in this process.

Parameters:

data (Any)

bucket_id: BucketId
label: str
authenticated_at: datetime
idle_deadline: datetime
absolute_deadline: datetime
session_persisted: bool
already_authenticated: bool
closed_previous_bucket_id: BucketId | None
close_profile_session_artefacts(*, storage_root, bucket_id)[source]

Tear down every session artefact owned by bucket_id (idempotent).

The single authority for “this profile is no longer logged in”: clears the process-local record authority, then revokes the durable artefacts through _revoke_profile_session_artefacts(). Composed by the strong close in logout_active_profile(), so neither surface owns a second teardown path.

Parameters:
  • storage_root (Path) – The Cadrumo storage root owning the bucket keystore.

  • bucket_id (str) – Identifier of the profile whose session to tear down.

Return type:

None

has_live_profile_session()[source]

Report whether this process currently holds an open profile session.

A durable active pointer is NOT the same fact: a registered profile keeps its selection across a logout, so the pointer can name a profile whose session is closed. A caller that needs profile-bound encrypted storage – anything that reads or writes through the secure object store – must ask this rather than reading the pointer, because only an open session carries the key that store needs.

Return type:

bool

logout_active_profile()[source]

Strong-close the selected profile without any provider fallback.

Logout is the explicit session-revocation owner: it clears the live DEK binding, its current record binding, the UUID-paired acceleration receipt, and the durable active pointer while the canonical root lock is held. It returns the authenticated profile UUID when there was a session or selection to revoke, otherwise None for an idempotent logged-out call.

Return type:

str | None

revoke_live_profile_secret_for_custody_delete(*, bucket_id)[source]

Zeroise this process’s live DEK only when it serves bucket_id.

This is deliberately narrower than logout: a custody deletion must never tear down an unrelated profile’s active session. The active-session owner performs both the identity query and the zeroisation; callers only receive a durable, non-secret outcome they can receipt.

Return type:

ProfileCustodySessionOwnerEffect

Parameters:

bucket_id (str)

remove_profile_session_acceleration_for_custody_delete(*, storage_root, bucket_id)[source]

Remove the actual persisted session acceleration and verify its absence.

Return type:

ProfileCustodySessionOwnerEffect

Parameters:
  • storage_root (Path)

  • bucket_id (str)

bind_resumed_profile_session(*, bucket_id, now=None, profile_decode_context)[source]

Resume bucket_id’s persisted session and bind it to this process.

The SINGLE resume authority, shared by the login idempotence guard, the CLI root callback, and the named-profile read path so they cannot drift. Fail-closed throughout: the adapter deletes stale artefacts and reports a typed refusal, and this function opens NOTHING on any refusal branch.

bucket_id is an explicit target, NOT the active profile. The name once said “active”, which was false of the parameter and cost a real defect: the named-profile read path concluded this authority could not serve a target it had resolved itself, skipped the resume, and reported records it could not open as missing. Binding is what distinguishes this from the substrate’s own resume_profile_session, which returns material and binds nothing.

On success the sliding idle deadline is advanced and re-persisted exactly once, so activity in a later process keeps the session alive without re-writing the record on every storage access.

Parameters:
  • bucket_id (str) – Identifier of the profile whose session to resume.

  • now (datetime | None) – UTC evaluation instant; the canonical clock when omitted.

  • profile_decode_context (ProfileDecodeContext) – Decode context supplied by the enclosing pinned authority operation. Resuming a record without it is refused; no bundled-schema fallback exists.

Return type:

ProfileSessionRefusalReason | None

Returns:

None when the session resumed and is now the active BucketSession, otherwise the typed ProfileSessionRefusalReason naming why not.

publish_created_profile_session(*, bucket_id, dek, now=None, profile_decode_context)[source]

Publish a newly created profile’s session exactly as a login would.

Registration already proves the operator’s credential – it CHOSE the passphrase and the create span unlocked the bucket with it – so requiring that same passphrase again before the new profile can be used is asking a question already answered. The session was simply never published: the create span held its DEK locally and dropped it, so every surface that asked afterwards was correctly told nobody was logged in.

This is deliberately the same publication login performs, and not a second, weaker one: one live bucket session bound process-wide, and the record authority derived beside it from the capsule’s COMMITTED envelope rather than from the in-memory one, so a fact read after registration proves the same two things it proves after a login.

No acceleration receipt is minted. A receipt is what carries a session into the NEXT process, which is a separate decision about how long a credential should last, and creating a profile does not settle it. The operator’s next process therefore authenticates normally.

Parameters:
  • bucket_id (str) – The newly created profile’s UUID.

  • dek (bytes) – The unlocked data-encryption key the create span established. Copied into the session’s own buffer; the caller keeps ownership of the bytes it passed.

  • now (datetime | None) – UTC evaluation instant; the canonical clock when omitted.

  • profile_decode_context (ProfileDecodeContext) – Decode context of the pinned authority operation the registration ran under.

Return type:

None

resolve_login_target(name)[source]

Resolve a login NAME target from an unambiguous UUID or exact label.

Delegates to the workflow’s one live-profile resolver. That authority accepts the immutable bucket UUID or the exact operator label (including a sandbox’s canonical sandbox:<name> label) and excludes tombstoned buckets for both forms. A bare sandbox short name carries no sandbox: prefix, so it remains an unknown profile rather than being implicitly namespaced.

Public because the login SCREEN must resolve the same named target to preselect its row, and must refuse an unknown one identically. Left private, that arm would have had to re-derive the resolution — and a second derivation is a second refusal wording and a second answer to “is a bare sandbox name a profile?”, for one question that has one answer.

Return type:

ProfileBucketPointer

Parameters:

name (str)

login_profile(*, name=None, now=None, passphrase_callback=None, profile_decode_context)[source]

Authenticate B before replacing an active A session.

Target resolution and throttle preflight happen before Argon2. Password authentication creates only transaction-owned candidate memory. The exact pointer capture is compared immediately before publication; then B becomes the process binding and optional acceleration is attempted. A is not closed until those operations have succeeded.

Return type:

ProfileLoginOutcome

Parameters:
authenticate_profile_for_invocation(*, name, now=None, passphrase_callback=None, profile_decode_context)[source]

Unlock one profile for the current process WITHOUT selecting it.

login_profile is a selection: it publishes the active-profile pointer, journals a handover and retires the profile it displaces. That is correct for config login NAME, and wrong for every command that merely names a target for this one invocation – --profile X or a leaf’s own profile argument. Routing those through the selecting door meant that on a host with no usable keychain, where each command must re-supply its passphrase, reading one profile silently reselected it for every later command.

So this door authenticates and binds, and does nothing durable: no pointer transaction is opened, no handover journal is written, no activation event is recorded, no acceleration receipt is minted, and the previously selected profile keeps its pointer entry and its receipt. A different live session is closed in-process only, because the process holds exactly one.

The result is process-scoped by construction, so session_persisted is always False; the caller’s existing not-persisted notice already tells the operator the next process must authenticate again.

Return type:

ProfileLoginOutcome

Parameters: