db
db
¶
SQLite-backed credential store, SSH key registry, and phantom token registry.
Provides host-side storage for the three kinds of secret material the vault mediates:
- Provider credentials (API keys, OAuth tokens) stored as JSON blobs keyed
by
(credential_set, provider). - SSH keys stored as unencrypted PKCS#8 DER + SSH wire-format public blob, deduplicated by standard-format fingerprint, linked to project scopes through an assignments join table.
- Phantom tokens minted per-
(scope, subject)so containers can authenticate to the vault without ever seeing real credentials.subjectis an opaque caller-supplied correlation label — the sandbox stores it verbatim and never interprets its contents. Callers (the orchestrator) decide what it identifies; today terok puts the task id there, but the sandbox treats it as a string label.
The database is never mounted into task containers — only the vault daemon reads it. sqlite3 in WAL mode gives lock-free concurrent reads across multiple terok processes (CLI commands, vault daemon, task runners).
Schema declarations and forward migrations live in
terok_sandbox.vault.store.migrations
— this module is the data-access layer only.
The on-disk file is always SQLCipher-encrypted; the passphrase
resolution chain (keyring → credentials.passphrase config field)
and the SQLCipher open helpers live in
terok_sandbox.vault.store.encryption.
__all__ = ['CredentialDB', 'InvalidScopeName', 'NoPassphraseError', 'PlaintextDBFoundError', 'SSHKeyRecord', 'SSHKeyRow', 'UnsafeCommentError', 'WrongPassphraseError', 'ensure_credentials_schema', 'migrate_credential_db_schema', 'open_credential_db', 'open_credential_db_with_source']
module-attribute
¶
NoPassphraseError
¶
Bases: RuntimeError
No SQLCipher passphrase resolved — the DB cannot be opened.
WrongPassphraseError
¶
Bases: RuntimeError
SQLCipher could not decrypt the DB — passphrase doesn't match its encryption key.
InvalidScopeName
¶
Bases: ValueError
Raised when a scope name would be unsafe as a filesystem path segment.
Scopes are embedded verbatim in per-scope Unix-socket paths
(ssh-agent-local-<scope>.sock), so unrestricted input could lead
to traversal (../) or oversized sockaddr strings. Every write
path that persists a scope validates through this helper first, so
a malicious or buggy caller can't slip a hostile name past the CLI.
UnsafeCommentError
¶
Bases: ValueError
Raised when a comment contains control characters or is too long.
Comments flow into SSH authorized_keys lines, public-line rendering,
ssh-add -L output, and terminal summaries — so embedded newlines or
escape sequences could break the wire format or spoof terminal output.
Rejection happens at the storage entry points; every display site then
trusts the DB to hold only safe strings.
SSHKeyRow(id, key_type, fingerprint, comment, created_at)
dataclass
¶
SSH key metadata — everything except the private material.
Returned from listing operations where the caller wants to render information about what is stored without decoding the private key.
SSHKeyRecord(id, key_type, private_der, public_blob, comment, fingerprint)
dataclass
¶
SSH key record carrying both metadata and raw key bytes.
Returned from loading operations that feed the signer. The raw bytes are not decoded here — decoding is the signer's responsibility so the storage layer stays free of cryptography imports.
PlaintextDBFoundError
¶
Bases: RuntimeError
A legacy plaintext sqlite DB was found where an encrypted one was expected.
CredentialDB(db_path, *, passphrase)
¶
SQLite-backed store for provider credentials, SSH keys, and phantom tokens.
The on-disk file is always SQLCipher-encrypted. Callers either
supply passphrase explicitly or leave it None to walk the
runtime resolution chain (keyring → credentials.passphrase).
A missing passphrase raises NoPassphraseError;
a stale plaintext file raises PlaintextDBFoundError
— both are diagnostic-only. Operator-facing remediation (which CLI
verb to run, which doc page to read) is the caller's job: library
code shouldn't bake one frontend's verbs into its exception text.
Source code in src/terok_sandbox/vault/store/db.py
transaction()
¶
Run the body in an explicit BEGIN IMMEDIATE transaction.
Take the write lock up front so callers can compose
read-then-write sequences and trust the whole thing serialises
against concurrent writers. Every mutating method on this
class (credentials, SSH keys, phantom tokens) consults the
self._in_outer_tx flag this context manager sets and skips
its own per-call commit — so the API contract is "any
composition of write methods inside with db.transaction():
is atomic", with no kwarg plumbing at the call site.
On exit: COMMIT on clean exit, ROLLBACK on any
BaseException (KeyboardInterrupt / SystemExit
included — leaving a half-written %scope keypair around
would be worse than a re-mint on retry).
Source code in src/terok_sandbox/vault/store/db.py
store_credential(credential_set, provider, data)
¶
Insert or replace a credential entry.
Source code in src/terok_sandbox/vault/store/db.py
load_credential(credential_set, provider)
¶
Return the credential dict, or None if not found.
Source code in src/terok_sandbox/vault/store/db.py
list_credentials(credential_set)
¶
Return provider names that have stored credentials.
Source code in src/terok_sandbox/vault/store/db.py
list_credential_sets()
¶
Return distinct credential-set names with at least one stored credential.
Source code in src/terok_sandbox/vault/store/db.py
delete_credential(credential_set, provider)
¶
Remove a credential entry (idempotent).
Source code in src/terok_sandbox/vault/store/db.py
store_ssh_key(key_type, private_der, public_blob, comment, fingerprint)
¶
Register a keypair, dedup-by-fingerprint; return the ssh_keys.id.
When a row with the same fingerprint already exists the stored bytes
and comment are left untouched (the caller is re-asserting an
already-known key, which is expected on repeat ssh-import).
Auto-commits unless called inside a
transaction()
scope — in which case the outer block owns the commit.
Source code in src/terok_sandbox/vault/store/db.py
get_ssh_key_by_fingerprint(fingerprint)
¶
Look up a key by fingerprint; returns metadata only.
Source code in src/terok_sandbox/vault/store/db.py
get_ssh_public_key(key_id)
¶
Return an OpenSSH public line without reading private key material.
Source code in src/terok_sandbox/vault/store/db.py
set_ssh_key_comment(fingerprint, comment)
¶
Update the comment of the key with fingerprint.
Returns True if a row was updated, False if the fingerprint
is unknown. The comment is validated by the same safety helper
that gates import_ssh_keypair — control characters and
overlong strings raise
UnsafeCommentError
so the storage-entry-point invariant holds for this path too.
The new comment surfaces to subsequent ssh-add -L queries from
the container because the signer resolves keys fresh from the DB
on every request.
Source code in src/terok_sandbox/vault/store/db.py
assign_ssh_key(scope, key_id, *, allow_infra=False)
¶
Grant scope access to key_id, defaulting only its first key.
Reassigning an existing link leaves the scope's default unchanged.
Rejects unsafe scope names with InvalidScopeName — the
value is later embedded in per-scope Unix-socket paths, so
traversal-like strings (../, /) must not be persisted.
By default also rejects %-prefixed infrastructure scopes so
callers driven by user input can't write to sandbox-reserved
names (%host for the krun host-side keypair, future
%name slots). Sandbox internals that legitimately provision
infrastructure scopes pass allow_infra=True.
Auto-commits unless called inside a
transaction()
scope — in which case the outer block owns the commit.
Source code in src/terok_sandbox/vault/store/db.py
set_default_ssh_key(scope, key_id, *, allow_infra=False)
¶
Offer an already assigned key first for this scope only.
Raises ValueError if the key is not assigned to scope.
The previous default remains unchanged when validation fails.
Source code in src/terok_sandbox/vault/store/db.py
unassign_ssh_key(scope, key_id, *, allow_infra=False)
¶
Revoke scope's access to key_id; drop the key row if orphaned.
Refuses %-prefixed infrastructure scopes by default — pair
with allow_infra=True for sandbox internals that need to
decommission a reserved scope.
Source code in src/terok_sandbox/vault/store/db.py
replace_ssh_keys_for_scope(scope, *, keep_key_id, allow_infra=False)
¶
Atomically make keep_key_id the scope's sole assigned key.
Wraps the "assign new + revoke every other" sequence in a single
SQLite transaction so two concurrent init(force=True) calls
can't both leave their own keys assigned — whichever transaction
commits last wins the scope, and exactly one primary survives.
Orphaned ssh_keys rows for revoked keys are cleaned up in the
same step via unassign_ssh_key semantics.
Refuses %-prefixed infrastructure scopes by default; sandbox
internals provisioning infra keys pass allow_infra=True.
Source code in src/terok_sandbox/vault/store/db.py
unassign_all_ssh_keys(scope, *, allow_infra=False)
¶
Revoke every key currently assigned to scope. Returns count removed.
Refuses %-prefixed infrastructure scopes by default — pair
with allow_infra=True for sandbox internals.
Source code in src/terok_sandbox/vault/store/db.py
delete_ssh_key(key_id, *, allow_infra=False)
¶
Delete a keypair outright, removing it from every scope at once.
The atomic counterpart to revoking a key from each project one
cell at a time: the keypair and all its scope assignments go
together. Returns True when a key was removed, False when
key_id was already absent.
Refuses by default to delete a key still bound to a %-prefixed
infrastructure scope — pair with allow_infra=True for sandbox
internals decommissioning a reserved key.
Auto-commits unless called inside a
transaction()
scope — in which case the outer block owns the commit.
Source code in src/terok_sandbox/vault/store/db.py
list_ssh_keys_for_scope(scope)
¶
Return metadata rows for every key assigned to scope.
The default is first, followed by assigned_at and k.id so
two assignments inside the same SQLite-second (datetime('now')
has 1-second resolution) have a deterministic key-ID tiebreak.
Source code in src/terok_sandbox/vault/store/db.py
load_ssh_keys_for_scope(scope)
¶
Return full records (with raw bytes) for every key assigned to scope.
Same deterministic ordering as
list_ssh_keys_for_scope
— default first, then assignment time and key ID.
Source code in src/terok_sandbox/vault/store/db.py
list_scopes_with_ssh_keys()
¶
Return every scope that currently has at least one assigned key.
Source code in src/terok_sandbox/vault/store/db.py
list_all_ssh_keys()
¶
Return metadata for every stored keypair, across all scopes.
The row axis of a keys×scopes routing view — pair with
list_ssh_key_assignments
for the edges. Every stored key has at least one assignment, so
this lists exactly the keys reachable through some scope. Ordered
by id for a stable listing.
Source code in src/terok_sandbox/vault/store/db.py
list_ssh_key_assignments()
¶
Return every (scope, key_id) assignment edge in the registry.
The complete scope↔key graph — the edge set a routing view
consumes whole, rather than sweeping it scope by scope. Ordered
by (scope, key_id) for a deterministic listing.
Source code in src/terok_sandbox/vault/store/db.py
list_ssh_key_defaults()
¶
Return the default key ID for each populated scope.
Source code in src/terok_sandbox/vault/store/db.py
count_ssh_keys()
¶
Return the number of distinct keypairs stored in the DB.
Counts ssh_keys rows (deduplicated by fingerprint) rather
than ssh_key_assignments rows — a single key shared across
scopes is one stored key, not N. Surfaces to TUI/CLI status
consumers so they can show a count without opening the DB
themselves.
Source code in src/terok_sandbox/vault/store/db.py
create_token(scope, subject, credential_set, provider)
¶
Mint a phantom token bound to (scope, subject, credential_set, provider).
subject is an opaque caller-supplied correlation label — the
sandbox stores it verbatim and never interprets its contents.
Today terok puts the orchestrator's task id there; the sandbox
treats the value as a string.
Token format: terok-p-<32 hex chars>.
Source code in src/terok_sandbox/vault/store/db.py
lookup_token(token)
¶
Return {scope, subject, credential_set, provider} or None.
Source code in src/terok_sandbox/vault/store/db.py
list_tokens()
¶
Return every proxy-token row as a list of dicts.
Read-only inventory for operator-facing CLI inspection
(terok vault list --include-tokens). The raw token value
is included so the operator can cross-reference what's actually
mounted into containers; callers MUST mask it before display.
Source code in src/terok_sandbox/vault/store/db.py
revoke_tokens(scope, subject)
¶
Revoke every phantom token bound to (scope, subject).
Returns the number of rows removed. The sandbox makes no claim
about what subject identifies; callers (the orchestrator) pass
whatever opaque label they used at
create_token
time.
Source code in src/terok_sandbox/vault/store/db.py
close()
¶
__del__()
¶
ensure_credentials_schema(conn)
¶
Create the credential / SSH-key / phantom-token tables if missing.
Idempotent — every statement is IF NOT EXISTS. Exposed at module
level so every opener of the DB file runs it before issuing queries.
Without this, a daemon that opens an empty DB on a fresh install
(before any CLI command has touched the file) hits no such table:
credentials on the first query and crashes the unit.
Source code in src/terok_sandbox/vault/store/migrations.py
migrate_credential_db_schema(conn)
¶
Walk legacy credential-DB rows forward to the current schema.
Tracked via PRAGMA user_version so the whole function is a no-op
on already-upgraded DBs. Each current < N branch handles one
forward step; the final PRAGMA user_version set commits the
whole upgrade in one go.
Exposed at module level so every opener of the DB file
(CredentialDB for
writers, _TokenDB in the vault daemon for readers) runs it
before issuing queries — otherwise a daemon that restarts before any
CLI command has touched the DB would hit "no such column: …" on a
freshly-upgraded host.
The cryptography import is scoped to the v0 → v1 branch so
already-migrated DBs (the common case) don't pay an import cost,
and the storage module keeps tach-clean at import time.
Source code in src/terok_sandbox/vault/store/migrations.py
open_credential_db_with_source(db_path, *, systemd_creds_file=None, use_keyring=False, passphrase_command=None, prompt_on_tty=False)
¶
Same as open_credential_db
but also returns which tier the passphrase came from.
Lets a TUI/CLI status display label the unlocked vault by its source without re-walking the chain itself.
Source code in src/terok_sandbox/vault/store/db.py
open_credential_db(db_path, *, systemd_creds_file=None, use_keyring=False, passphrase_command=None, prompt_on_tty=False)
¶
Open the credential DB, resolving the passphrase via the runtime chain.
Walks: systemd_creds_file (sealed credential decrypted via
systemd-creds(1)) → OS keyring (when use_keyring) → the kernel
keyring (volatile unlock cache) → passphrase_command
(operator-supplied helper, e.g. pass show … / op read …) →
(when prompt_on_tty and a TTY is attached) interactive prompt.
CLI consumers pass prompt_on_tty=True; daemons leave it
False so they fail fast instead of blocking on stdin.