Index
axm_vault
axm-vault.
Catalog-resolver secrets manager (keyring + SecretStr) for AXM
SERVICE = 'axm-vault'
module-attribute
The fixed keyring service name under which every AXM secret is stored.
Layer = Literal['env', 'file', 'keyring', 'default', 'prompt']
Resolution layer a credential may be sourced from.
Provenance = dict[str, dict[str, str | bool]]
Per-credential report: {"group.name": {"layer": str, "present": bool}}.
For a SECRET spec whose keyring backend is unavailable (headless host), the
entry additionally carries "keyring": "unavailable" so the doctor surfaces
the outage rather than silently reporting the credential as merely missing.
Catalog
Bases: BaseModel
An in-memory index of credential groups, keyed by group id.
Source code in packages/axm-vault/src/axm_vault/catalog.py
all_specs()
Return every (group_id, spec) pair across all groups.
for_package(package)
group(gid)
Return the group identified by gid.
Raises:
| Type | Description |
|---|---|
KeyError
|
if no group with that id is registered. |
Source code in packages/axm-vault/src/axm_vault/catalog.py
| Python | |
|---|---|
CredentialGroup
Bases: BaseModel
A bundle of credential specs declared by a package.
Source code in packages/axm-vault/src/axm_vault/models.py
spec(name)
Return the spec named name.
Raises:
| Type | Description |
|---|---|
KeyError
|
if no spec with that name exists in the group. |
Source code in packages/axm-vault/src/axm_vault/models.py
CredentialSpec
Bases: BaseModel
Schema for a single credential — value-less by construction.
Source code in packages/axm-vault/src/axm_vault/models.py
KeyringStore
Store and retrieve secrets in the OS keyring under :data:SERVICE.
The store is stateless: every call delegates to the process-wide
keyring backend, so tests can swap in an in-memory backend via
keyring.set_keyring(...) without touching the real Keychain.
Source code in packages/axm-vault/src/axm_vault/store.py
delete(group, name, instance=None)
Remove the credential from the keyring (no-op if already absent).
The real macOS Keychain backend raises
:class:keyring.errors.PasswordDeleteError (Item not found) when
the credential is absent; it is suppressed here so the call is a true
no-op, as the contract promises and as rotation/cleanup callers rely on.
Source code in packages/axm-vault/src/axm_vault/store.py
get(group, name, instance=None)
Return the stored secret, or None if no such credential exists.
Raises :class:KeyringUnavailableError when no usable backend exists
(headless host) rather than surfacing a raw backend traceback.
Source code in packages/axm-vault/src/axm_vault/store.py
set(group, name, value, instance=None)
Store value under (group, name[, instance]) in the keyring.
Raises :class:KeyringUnavailableError when no usable backend exists
(headless host) rather than surfacing a raw backend traceback.
Source code in packages/axm-vault/src/axm_vault/store.py
username(group, name, instance=None)
staticmethod
Compose the keyring username for a credential.
The instance segment is omitted cleanly when instance is None
({group}.{name}) and inserted between group and name otherwise
({group}.{instance}.{name}).
. is the structural separator, so each segment is percent-escaped
before joining: a literal . (or %) inside any segment is
encoded so that distinct (group, name, instance) tuples can never
collapse onto the same username (e.g. ("a.b", "c") vs
("a", "b.c")). The encoding is reversible and leaves dot-free
segments untouched, so existing usernames are unchanged and the
reserved .prev rotation slot stays distinct from its base name.
Source code in packages/axm-vault/src/axm_vault/store.py
MissingCredentialError
Resolved
Bases: BaseModel
The outcome of resolving a single credential.
Carries the resolved value, the layer it was sourced from and the
originating spec — but never masks: callers wrap secrets themselves
(e.g. via :func:~axm_vault.secrets.as_secret).
Source code in packages/axm-vault/src/axm_vault/resolver.py
Resolver
Walk the layer precedence to resolve a credential value.
The resolver is stateless and cheap to instantiate; a process-wide
:data:resolver singleton is provided for the common case. Set
interactive=True to enable the prompt layer (off by default so the
resolver is safe in non-interactive contexts).
Source code in packages/axm-vault/src/axm_vault/resolver.py
| Python | |
|---|---|
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | |
keyring_available()
Report whether the OS keyring backend is usable, value-free.
Probes the backend with a read that resolves no real credential; a
:class:KeyringUnavailableError means no usable backend (headless
host). The doctor uses this to flag keyring unavailable without
ever reading a secret value.
Source code in packages/axm-vault/src/axm_vault/resolver.py
probe(layer, spec, group, instance=None)
Report whether layer supplies spec, value-free.
Reduces the layer's value to a boolean the instant it is read, so the value never escapes — the seam the doctor uses to build provenance without violating the never-leak invariant.
Source code in packages/axm-vault/src/axm_vault/resolver.py
resolve(group, name, instance=None)
Resolve group.name by walking :data:PRECEDENCE.
Returns the first layer that yields a value. Raises
:class:MissingCredentialError when a required spec resolves to
nothing across every layer.
Source code in packages/axm-vault/src/axm_vault/resolver.py
Sensitivity
Bases: StrEnum
Classification of how sensitive a credential value is.
Source code in packages/axm-vault/src/axm_vault/models.py
VaultDoctorTool
Report credential provenance (layer + presence), never a value.
Source code in packages/axm-vault/src/axm_vault/tools.py
name
property
Unique tool identifier.
execute(*, package=None, instance=None)
Return value-free provenance for the catalog (or one package).
Source code in packages/axm-vault/src/axm_vault/tools.py
VaultSetTool
Store a credential by group.name — keyring (SECRET) or config.
NONSENSITIVE credentials are environment-only and are rejected outright: storing them would create a second, stale source of truth. The stored value is never echoed back — only the storage target is reported.
Source code in packages/axm-vault/src/axm_vault/tools.py
name
property
Unique tool identifier.
execute(*, group='', name='', value='', instance=None)
Route group.name to its store by sensitivity; never echo value.
Source code in packages/axm-vault/src/axm_vault/tools.py
as_secret(value)
Coerce a raw secret string into a :class:~pydantic.SecretStr.
None passes through unchanged (for optional secrets), and an
existing :class:~pydantic.SecretStr is returned as-is (idempotent).
Source code in packages/axm-vault/src/axm_vault/secrets.py
atomic_write(path, data, *, encoding='utf-8')
Write data to path atomically (temp file + os.replace).
The write goes to a temporary file in the destination directory, is flushed
and fsync-ed, then atomically renamed over path so a concurrent
reader never observes a partially written file. After the rename the parent
directory is itself fsync-ed so the new directory entry is durable —
without that, a crash right after os.replace could lose the rename even
though the file content reached disk. The destination directory must
already exist — vault does not create it (that is axm-config's
responsibility). Intended for the OAuth refresh-token rotation case.
Source code in packages/axm-vault/src/axm_vault/store.py
bind(model, group, instance=None)
Build model from the resolved values of every spec in group.
Each field is keyed by spec.name; SECRET specs are wrapped with
:func:~axm_vault.secrets.as_secret so the consumer model holds
SecretStr. A missing required spec propagates
:class:MissingCredentialError. The return type is the concrete model
type (generic over type[_ModelT]), so a caller need not cast the
result back to its model.
Source code in packages/axm-vault/src/axm_vault/resolver.py
doctor_data(package=None, *, catalog=None, instance=None)
Report the winning layer and presence of every credential, value-free.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
package
|
str | None
|
When given, restrict the report to credential groups contributed by that package; otherwise cover the whole catalog. |
None
|
catalog
|
Catalog | None
|
Catalog to inspect; defaults to the discovered
:func: |
None
|
instance
|
str | None
|
Optional multi-instance segment forwarded to the probe. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
Provenance
|
data: |
Provenance
|
|
|
Provenance
|
|
|
Provenance
|
itself is NEVER included (security invariant). |
Source code in packages/axm-vault/src/axm_vault/doctor.py
get(group, name, instance=None)
Resolve group.name via the singleton :data:resolver.
Convenience over resolver.resolve(load_catalog().group(group), name)
returning just the resolved value.
Source code in packages/axm-vault/src/axm_vault/resolver.py
load_catalog()
cached
Discover and index all axm.credentials groups.
Reads the axm.credentials entry-points, calls each (a callable
returning list[CredentialGroup]) and indexes the groups by id.
Returns an empty catalog when no entry-point is registered — the
nominal state for vault itself. Cached so discovery runs once.
Source code in packages/axm-vault/src/axm_vault/catalog.py
redact(text, *secrets)
Mask occurrences of known secret substrings for log scrubbing.
Each qualifying secret found in text is replaced by :data:MASK. A
:class:~pydantic.SecretStr argument is accepted and unwrapped via
:meth:~pydantic.SecretStr.get_secret_value. Secrets are masked
longest-first so a short secret that is a prefix of a longer one cannot
leave the longer one's tail unmasked. Secrets shorter than
:data:MIN_REDACT_LEN (and empty ones) are ignored, since masking a tiny
substring over-redacts surrounding text without protecting anything.
This is best-effort log scrubbing, not a security boundary: it only
masks the exact substrings it is given, in the casing they are given, and
cannot catch transformed or partial echoes. The authoritative never-leak
surface is :class:~pydantic.SecretStr.
Source code in packages/axm-vault/src/axm_vault/secrets.py
rotate_secret(group, name, value, instance=None)
Rotate a keyring secret, retaining the previous value for one cycle.
The prior cycle's backup is purged first, the current value (if any) is
copied to the reserved {name}.prev slot, then value is written over
{name}. Retention is strictly one cycle: a stale .prev never
lingers across rotations. Keeping the previous secret one rotation lets a
caller fall back during an in-flight credential roll. No value is ever
returned or logged.
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
KeyringUnavailableError
|
when the OS keyring backend is unavailable. |
Source code in packages/axm-vault/src/axm_vault/store.py
run_setup(only=None)
Prompt for and store every storable credential in the catalog.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
only
|
str | None
|
When given, restrict setup to the single |
None
|
The function refuses to run without an interactive TTY (writes
:class:SystemExit 1 to stderr). NONSENSITIVE specs are skipped
(environment-only); SECRET specs are read with :func:getpass.getpass
and CONFIG specs with :func:input. A blank answer keeps the existing
value, making the driver idempotent across re-runs.