Skip to content

Latest commit

 

History

History
275 lines (187 loc) · 14.2 KB

File metadata and controls

275 lines (187 loc) · 14.2 KB

authsome

authsome is the local auth layer for AI agents — it answers which agent, acting on behalf of whom, accessed what credential, and was that allowed.

Tenancy Model

authsome is single-tenant by design. There is no concept of an organisation, team, or multi-tenant namespace. A Principal represents a single human or agent identity, not a group. Features that require shared vaults, org-level permissions, or team credential distribution are outside the current architecture and would require introducing multi-tenancy as a first-class primitive before they could be built.

Module Responsibilities

Each module has one job. identity/, auth/, vault/, and audit/ are leaf modules — they import nothing from this codebase and can be used and tested in isolation. server/ is the only composition root.

identity/  ←─┐
auth/      ←─┤
vault/     ←─┤  server/  ←── cli/   (via HTTP, not Python import)
audit/     ←─┘            ←── proxy/ (via HTTP, not Python import)

identity/ — Cryptographic identity primitives

Think of this as the OpenID Connect layer. Handles key material, DIDs, and proof-of-possession tokens.

Owns:

  • Ed25519 key pair generation and serialization (local.py)
  • did:key DID derivation from public keys (local.py)
  • IdentityMetadata model — client-side cached state for a local identity
  • IdentityRegistration model — the server's record of a registered handle/DID binding
  • PoP JWT creation and validation (proof.py)
  • ClaimStatus, PrincipalRecord, VaultRecord, IdentityClaimRecord, PrincipalVaultBindingRecord — shared domain models

Does not own:

  • Filesystem-backed registries (those are server state, not identity primitives)
  • Client config management (that is cli/ territory)
  • Principal/vault lifecycle decisions (that is server/ territory)

Imports nothing from this codebase. Used by: server/, cli/


auth/ — OAuth and API key flow implementations

Think of this as the OAuth 2.0 protocol library. Each flow takes provider config and credentials in, returns tokens out. No storage, no audit, no identity imports.

Owns:

  • OAuth 2.0 flows: PKCE, Device Code, DCR+PKCE (flows/)
  • API key collection flow (flows/api_key.py)
  • Flow base class and token refresh logic
  • Provider models: ProviderDefinition, OAuthConfig, ApiKeyConfig, bundled provider JSON
  • Credential models: ConnectionRecord, ProviderClientRecord, ProviderMetadataRecord, ProviderStateRecord
  • AuthSession — transient flow session state

Does not own:

  • Credential persistence (that is vault/ + server/ territory)
  • Audit logging (that is audit/ + server/ territory)
  • Proxy route catalog building
  • Server registry reads

Imports nothing from this codebase. Used by: server/


server/ — CredentialService and application orchestration

server/ owns CredentialService (server/credential_service.py) — the stateful coordinator that wires auth/ flows with vault/ storage and audit/ logging. It is the only place where flows, storage, and audit are combined.

CredentialService is constructed per-request by the server with keyword-only (credentials, providers, identity, principal_id, principal_role, vault_id) and calls auth/ flows to execute protocols, vault/ (through the injected CredentialRepository) to persist results, and audit/ to record events.


vault/ — Encrypted credential storage

Think of this as the secrets layer. Encrypts and decrypts credential blobs transparently.

Owns:

  • Vault — AES-256-GCM encrypted KV wrapper over AsyncKeyValue
  • VaultCrypto — key management (local file, OS keyring)
  • Encrypted get/put/delete/list over named collections

Does not own:

  • Server filesystem layout or path resolution
  • Registry lookups
  • Business logic about which vault belongs to which principal

Imports nothing from this codebase. Imported by: auth/, server/


audit/ — Structured event recording

Think of this as the audit instrumentation layer. Defines what happened; server/ decides where it goes.

Owns:

  • AuditEvent domain model — mandatory fields: identity, principal_id, provider, connection; optional: method, path, status, metadata
  • log() / alog() — emit an AuditEvent as an OTel LogRecord via get_logger_provider()
  • Translation from AuditEvent → OTel LogRecord

Does not own:

  • Storage — no file I/O, no database
  • Provider lifecycle (setup() / clear() removed — owned by server/)
  • Knowledge of where events are routed

Imports: opentelemetry-api only (no SDK, no storage). Imports nothing from this codebase. Imported by: server/, proxy/


server/ — Application orchestration and server-owned state

Think of this as the daemon process. Wires identity + auth + vault + audit together. Owns all server-side persistence.

Owns:

  • server/store/repositories.py — all relational (SQLite/Postgres) registry implementations:
    • IdentityRegistry (handle → DID)
    • PrincipalRegistry (principal_id → email)
    • VaultRegistry (vault_id → handle)
    • IdentityClaimRegistry (identity → principal + ClaimStatus)
    • PrincipalVaultBindingRegistry (principal → default vault)
  • server/ownership.pyOwnershipResolver (local and hosted variants), ResolvedOwnership
  • server/identity_bootstrap.py — deployment-specific identity registration behavior
  • server/dependencies.py — infrastructure wiring (paths, store, vault, config)
  • server/app.py — FastAPI application factory and lifespan
  • server/routes/ — HTTP API surface
  • server/schemas.py — API response schemas
  • server/audit_store.pySQLiteLogExporter (OTel LogExporter impl) + AuditStore query interface; LoggerProvider lifecycle (setup at startup, shutdown at teardown)
  • server/routes/audit.pyGET /audit/events (admins see all events; other principals see only their own PrincipalId)
  • POST /audit/events — ingest endpoint for proxy-side external AuditEvents; server enriches principal_id from PoP JWT

All filesystem interaction for server-owned state lives here. No other module writes to server-owned paths.

Imported by: nothing (top of the import graph)


proxy/ — Credential injection proxy

A mitmproxy-based HTTPS proxy. Intercepts outgoing agent requests and injects auth headers.

Owns:

  • proxy/server.py — mitmproxy addon that intercepts requests
  • proxy/runner.py — background thread lifecycle
  • proxy/router.pyRouteMatch / RouteResolution types
  • proxy/certs.py — CA certificate management

Does not own:

  • Credential loading (asks the server)
  • Route catalog construction (asks the server)
  • Provider definitions
  • Audit storage — ships External AuditEvents to server via POST /audit/events (fire-and-forget); does not call audit.log() directly

Imported by: cli/


cli/ — Client to the daemon

Click-based CLI and HTTP client. Everything here is a client to the server HTTP API.

Owns:

  • cli/main.py — Click command tree
  • cli/client.pyRuntimeClient (async HTTP client for daemon requests, attaches PoP JWT)
  • cli/client_config.py — client-owned config (active_identity, vault_id, proxy settings)
  • cli/context.pyCliRuntime wiring container
  • cli/daemon_control.py — start/stop the daemon process

Does not own:

  • Server registry operations
  • Direct vault or store access
  • Identity key generation (delegates to identity/, result stored by CLI via identity/local.py)

Imported by: nothing (entry point)


Domain Language

Identity & Authentication

Identity: The cryptographic agent — Ed25519 key pair, did:key DID, and human-readable Handle. Created locally; registered with the daemon. Is not a credential namespace.

Handle: Human-readable name for an Identity (e.g., brisk-boldly-clearly-1234). Used as sub in PoP JWTs.

DID: did:key Ed25519 identifier derived from the Identity's public key. Used as iss in PoP JWTs.

PoP JWT: Short-lived (60 s) Proof-of-Possession token signed with the Identity's Ed25519 private key. Bound to htm, htu, body_sha256. Sent as Authorization: PoP <token>.

Principal: Non-cryptographic logical partition (human or team) that owns Vaults. Identified by an opaque PrincipalId (e.g., principal_abc123def456). Has no cryptographic key. Carries exactly one PrincipalRole. Avoid: User, account, PrincipalHandle, profile

PrincipalId: Opaque stable identifier for a Principal. Never the email or handle — those can change; the PrincipalId cannot. Avoid: principal_handle, principal_name, username

PrincipalRole: Authorization tier for a Principal. Either admin or user. The first Principal created on a server is always admin; all subsequent Principals are user. Stored as a column on the Principal record — not in environment variables or a separate table. Avoid: permission level, access level, user type

Vault: Named credential store owned by exactly one Principal. Identified by an opaque VaultId (e.g., vault_a1b2c3d4e5f6). All credential store keys are prefixed vault:<vault_id>:.... Avoid: credential store, token store, secret store, profile store

VaultId: Opaque stable identifier for a Vault. Used as the storage key segment. Stable across naming changes. Avoid: vault_name, vault_handle

VaultHandle: Human-readable name for a Vault (e.g., default). Used in UIs and CLI; the VaultId is authoritative in storage.

IdentityClaimRecord: Binding from an Identity (Handle) to a Principal (PrincipalId) with a ClaimStatus. Created when an authenticated Principal confirms the browser claim that authsome onboard initiates. Vault access is gated until the claim is accepted. Avoid: Claim, IdentityRegistration (as claim), join request

ClaimStatus: Lifecycle state: pendingaccepted | rejected.


Initialization & Claim Flow

There is a single flow for every deployment — no deployment mode (see ADR 0007). authsome onboard creates an Identity and registers it; the daemon returns registration_status = "claim_required" with a browser claim URL. The user opens the URL and registers (or logs in) with email + password: the first Principal created on a server becomes admin, all subsequent Principals are user. The authenticated Principal then confirms the claim, which binds the Identity to the Principal and creates the Principal's default Vault. Until the claim is accepted, all vault operations return 403. The CLI opens the claim URL automatically and polls for completion (and prints the URL to stderr for headless use).


Key Relationships

  • An Identity is a cryptographic agent. It does not own credentials directly.
  • An Identity claims a Principal via an IdentityClaimRecord. Claim must be accepted for vault access.
  • A Principal owns one or more Vaults via PrincipalVaultBindingRecords. The server resolves the default Vault before constructing CredentialService.
  • A Vault contains zero or more Connections, each scoped to one Provider.
  • Multiple Identities may share one Vault by claiming the same Principal.
  • A ConnectionRecord belongs to exactly one Vault. vault:<vault_id>:... is the key prefix.
  • ClientCredentials are server-scoped — one ProviderClientRecord per Provider, shared across all Vaults.

CredentialService Contract

CredentialService is a per-request credential lifecycle object constructed by the server:

CredentialService(
    credentials=CredentialRepository(vault, identity=handle, principal_id=pid, vault_id=vid),
    providers=provider_repository,
    identity=handle,
    principal_id=pid,
    principal_role=role,
    vault_id=vid,
)
  • identity — agent Handle, used for audit logging only
  • principal_id — resolved by OwnershipResolver from the PoP JWT subject
  • principal_role — the Principal's role (admin/user); gates provider-config and revoke operations
  • vault_id — resolved from PrincipalVaultBindingRegistry by the server before constructing CredentialService
  • credentials — a CredentialRepository over the encrypted KV store; CredentialService reads/writes only through this

CredentialService does not query registries, does not know about server filesystem paths, and does not build proxy route catalogs.


Audit Contract

Every AuditEvent carries identity (the agent Handle) and principal_id (the PrincipalId). Both are required — every auditable action has an acting agent and an owning principal.

External AuditEvent: An event produced by the proxy layer — records an outbound HTTP call an agent made through the proxy to a third-party API (e.g., a call to api.github.com). Classified by provider and connection. Mandatory fields: identity, principal_id, provider, connection. Optional fields: HTTP method, path, response status. Avoid: proxy event, API event, outbound event

Internal AuditEvent: An event produced by the server layer — records credential lifecycle operations (login, logout, token refresh, revocation) and auth flow steps. Avoid: server event, auth event, lifecycle event

Audit delivery: External AuditEvents are shipped from the proxy to the server via POST /audit/events (fire-and-forget, best-effort). The proxy does not write to a local audit file. The server is the single source of truth for all audit events. principal_id is resolved server-side from the PoP JWT on the ingest request — the proxy does not need to supply it.


Flagged Ambiguities

  • "PrincipalHandle" — retired. The Principal is now identified by an opaque PrincipalId. Do not use PrincipalHandle in new code.
  • "VaultHandle" — the human-readable display name. Do not use VaultHandle as a storage key; use VaultId.
  • "Claim" — use IdentityClaimRecord for the binding object; use "claim" (lowercase) only as a verb.
  • "identity=server" — retired. Previously a startup hack where the credential service was instantiated without a real identity; CredentialService is now built per-request in routes/_deps.py.
  • "credential" — use Connection for the full authenticated session; use access token / API key for the individual secret.