ontolith.identity¶
ontolith.identity ¶
Identity and authorization layer.
This module contains: - Principal model (humans, AI agents, services) - AuthProvider port (authentication abstraction) - Capability management - Delegation support
TokenAuthProvider/hash_token (identity/token_auth.py, the concrete
AuthProvider implementation, ADR-0014 — the one every create_rest_app/
create_graphql_app/create_mcp_server docstring example actually uses)
are exported below, but the import order matters: token_auth.py imports
StorageBackend from store.base, and store.base imports Principal
from this package — reaching back into ontolith.identity while it's
still initializing. That reverse import only succeeds if Principal has
already been bound in this module's namespace by the time it's reached,
which requires the principal import below to run before the
token_auth one — reproduced directly: swapping the two order raises
ImportError: cannot import name 'Principal' from partially initialized
module. This isn't a silent footgun: the required order (token_auth
alphabetically last) is exactly what ruff's own isort rule (I001,
already CI-blocking) enforces, so any reordering that broke this would
also fail ruff check before merge. Round-1 review of this ADR-0019
Update found and corrected an earlier, inaccurate version of this
docstring claiming the import was circular unconditionally — verified
false; it depends on order, and the enforced order happens to be the
working one.
AdminAction
module-attribute
¶
The admin-capability-gated action kinds an AdminEvent records.
AdminEvent ¶
Bases: BaseModel
A single admin-tier action, recorded for forensic traceability.
Deliberately does NOT cover token issuance/revocation — those are
attributed directly on PrincipalCredential.issued_by/.revoked_by
instead, since every credential row already has a natural home for its
own attribution and doesn't need a separate event row to point back to.
target is free text, not a foreign key: unlike assertion_event/
proposal_event (each referencing exactly one row type), the three
actions here target a principal id, a namespace:version schema
descriptor, and a plugin name respectively — no single column type
could reference all three, so this follows ProposalEvent.type's own
precedent of trusting Pydantic's Literal at construction rather than
a DB-level CHECK/FOREIGN KEY.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Unique event ID (ULID) |
actor |
str
|
Principal ID who performed the action (admin capability
required for apply_schema/register_plugin, checked by the
caller before this event is ever constructed; create_principal
itself is deliberately ungated per ADR-0022, so |
action |
AdminAction
|
Which admin action this event records |
target |
str
|
Free-text identifier of what was acted on — a principal
id, a |
at |
datetime
|
When the action occurred |
detail |
str | None
|
Optional free-text detail |
PrincipalCredential ¶
Bases: BaseModel
A single issued API-key credential for a principal.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Unique identifier (ULID) — a stable handle for revocation, independent of the (never-stored) raw token |
principal_id |
str
|
Principal this credential authenticates as |
token_hash |
str
|
SHA-256 hex digest of the raw token |
created_at |
datetime
|
When this credential was issued |
revoked_at |
datetime | None
|
When this credential was revoked, if it has been |
issued_by |
str | None
|
Principal ID of the admin who issued this credential
(KI-060) — |
revoked_by |
str | None
|
Principal ID of the admin who revoked this credential
(KI-060), or |
AuthProvider ¶
Bases: Protocol
Abstract port for resolving a caller's credential to a Principal.
AuthProviders resolve a bearer credential (e.g. an API-key token) to the
principal it authenticates as — the principal is never taken as a
caller-asserted ID. See ADR-0014 for the interim per-principal API-key
model implemented by the concrete TokenAuthProvider; full OIDC/
workload-identity support remains future work.
resolve ¶
Resolve a raw bearer token to its Principal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
Raw bearer token supplied by the caller |
required |
Returns:
| Type | Description |
|---|---|
Principal
|
The Principal the token authenticates as |
Raises:
| Type | Description |
|---|---|
AuthError
|
token is invalid, unknown, or revoked |
Source code in src/ontolith/identity/ports.py
Principal ¶
Bases: BaseModel
Identified actor in the system.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Email (human) or slug (ai/service) |
kind |
Literal['human', 'ai', 'service']
|
Type of principal |
owner |
str | None
|
Required for AI principals - accountable human/team |
auth_method |
Literal['oidc', 'workload', 'apikey']
|
How this principal authenticates |
default_capability |
Literal['read', 'propose', 'write', 'review', 'admin']
|
Default permission level |
trust_level |
int
|
Base trust score |
created_at |
datetime
|
When this principal was created |
metadata |
dict[str, Any]
|
Open JSON blob for future extension |
ai_must_have_owner ¶
AI principals must declare an accountable owner (SPEC §8.1, ADR-0003).
Source code in src/ontolith/identity/principal.py
TokenAuthProvider ¶
AuthProvider that resolves API-key tokens via the abstract StorageBackend port.
Depends only on StorageBackend (the port), the same pattern Ontology
itself uses (backend: StorageBackend) — keeps identity/ from importing
a concrete adapter, satisfying the dependency rule.
Source code in src/ontolith/identity/token_auth.py
resolve ¶
Resolve a raw bearer token to its Principal.
Raises:
| Type | Description |
|---|---|
AuthError
|
token is invalid, unknown, or revoked |
Source code in src/ontolith/identity/token_auth.py
min_capability ¶
Return the lower of two capability levels (SPEC §8.3 ordering).
Ordering: read < propose < write < review < admin.
hash_token ¶
Hash a raw bearer token for storage/lookup.
SHA-256, not a slow password hash (bcrypt/scrypt/argon2): the input is
already a high-entropy random secret (secrets.token_urlsafe(32)), not a
low-entropy human password, so brute-force resistance from a slow hash
buys nothing here — it would only add latency to every authenticated call.