Skip to main content

Architecture (user-level)

How agent-locksmith composes at runtime — the user-level mental model of what happens when you start the daemon and send it a request.

The daemon

locksmithd runs one process with two concurrent surfaces:

┌─────────────────────────────────────────┐
│ │
│ Agent listener (TCP) │
agent ──HTTPS──┤ :9200 (default) │
bearer/mtls │ • /api/{tool}/{*path} proxy hot path │
│ • /tools, /models discovery │
│ • /skill personalised │
│ • /livez, /readyz health probes │
│ │
│ Admin listener (UDS) │
operator ──UDS─┤ /var/run/locksmith/admin.sock │
bearer │ • /admin/operator/{agents, bootstrap_tokens, tools, models, infra, oauth, audit}
│ • /admin/agent/{status, rotate, register, deregister, tools}
│ │
│ Optional: admin HTTPS (:9201) │
│ │
└─────────────────────────────────────────┘

Both listeners share one process, one SQLite pool, one audit fanout.

The agent's request flow

When an agent calls POST /api/anthropic/v1/messages with Authorization: Bearer lk_...:

1. axum routes the request to proxy::proxy_handler

2. auth_middleware validates the bearer against AgentRepository.
Stamps AgentIdentity into request extensions.
Failure → 401 invalid_credential

3. ACL gate: identity.allows_tool(name)
Failure → 403 tool_not_allowed + audit row

4. Target resolution:
state.catalog.lookup_active(name) — registrations table cache
Failure → 404 unknown tool + audit row

5. OAuth resolution (only if AuthSpec is OAuth):
oauth_runtime.sessions.get(name, label)
If degraded / missing → 503 oauth_refresh_failed + audit
If access token expiring → trigger inline refresh

6. Header strip: agent's Authorization, x-api-key, host
plus the target's own auth header (defense-in-depth).

7. Credential injection (per AuthSpec):
None → nothing.
Header → "<header>: <resolved_creds[name]>"
Bearer → "Authorization: Bearer <resolved_creds[name]>"
OAuth → "Authorization: Bearer <oauth_session.access_token>"

8. Egress route:
egress: proxied → HTTP CONNECT through pipelock
egress: direct → straight to upstream

9. Stream the response back to the agent.

10. Apply response controls if configured:
max_size_bytes, content_type_allowlist, redaction_patterns.

11. Emit one AuditEvent → SQLite + (optional) JSONL mirror.

Steps 1–11 happen per request. The audit row is the operator's single source of truth for who-called-what-when.

Composition root: daemon::run

src/daemon.rs::run is the only place where state gets wired up. Read it first if you're contributing code. It does (in order):

  1. Parse + validate AppConfig.
  2. Construct Arc<ArcSwap<AppConfig>> so config can hot-reload.
  3. Resolve tool.auth.value: SecretRef for legacy config.tools entries.
  4. Build admin substrate (only when listen.admin_socket is set):
    • Open SQLite pool, run migrations.
    • Construct repositories (AgentRepository, BootstrapTokenRepository, AuditRepository, RegistrationRepository, OauthSessionRepository).
    • Run the seed loader — populate registrations from /etc/locksmith/seed/catalog.yaml.
    • Run legacy_bootstrap — migrate any pre-Phase-E config.tools entries into the registrations table.
    • Build the in-memory Catalog cache from the registrations table.
    • Resolve registration env vars into the resolved_creds map.
    • Build OAuth runtime (when LOCKSMITH_OAUTH_SEALING_KEY is set).
    • Construct AdminService + BearerAuthenticator + OperatorAuthenticator.
  5. Spawn the audit retention sweeper.
  6. Bind agent listener (TCP for bearer, TLS for mTLS).
  7. Bind admin UDS listener.
  8. Optionally bind admin HTTPS listener.
  9. Optionally bind bootstrap-only listener.
  10. Wait for SIGTERM/SIGINT; shut down both listeners within the drain window.

State that survives restarts

Anything that needs to outlive locksmithd lives in the SQLite DB:

locksmith.db
├── agents — per-agent identity + ACL + revocation
├── bootstrap_tokens — pre-issued enrollment tokens
├── audit — every request + admin write
├── registrations — kind-discriminated catalog
├── registrations_meta — seed catalog version pin
└── oauth_sessions — sealed OAuth tokens

The audit table can also mirror to a JSONL file (rotating, size-capped) so the audit log survives volume recreation.

What's transient (in-memory only)

  • resolved_creds — built at startup from env vars + sealed files. Refreshed when an admin write touches a registration.
  • Catalog — in-memory mirror of the registrations table. Refreshed by admin writes.
  • OauthRuntime — sealing key + session repo + refresh lock map + shared HTTP client.
  • ResponseControls cache — compiled regex patterns per tool.
  • ClientPool — cached reqwest::Client per (name, timeouts, egress).

Auth surfaces are independent

Three auth surfaces, each picks its own mode:

SurfaceModesNotes
Agent listenerbearer | mtls | bothlisten.auth_mode
Admin UDSbearer onlyOS peer-uid is the gate
Admin HTTPS (optional)bearer | mtls | bothlisten.admin_https.auth_mode

You can run agents on bearer + admin HTTPS on mTLS, or vice versa. The settings are independent.

See also