API Reference¶
Docstrings on this page are pulled directly from the source under src/ by mkdocstrings. Edit the docstring, rebuild, and the rendered page updates.
lex_align_client¶
The thin client used by developers and AI agents. Provides the lex-align-client CLI as well as the Claude Code and git pre-commit hooks.
lex_align_client ¶
api ¶
HTTP client for the lex-align server.
Synchronous; the CLI is one-shot, so the simplicity of httpx.Client is worth more than the throughput of an async client. Failure semantics:
- connection error and
fail_open=true→ return a synthetic ALLOWED verdict withtransport_error=Trueso the caller can warn. - connection error and
fail_open=false→ raise ServerUnreachable. - server 4xx/5xx → raise ServerError(detail).
LexAlignClient ¶
LexAlignClient(config: ClientConfig, http_client: Client | None = None, *, agent_model: Optional[str] = None, agent_version: Optional[str] = None)
Source code in src/lex_align_client/api.py
pending_approvals ¶
List PENDING_REVIEW approval requests for project.
Defaults to the project bound to this client. Used by lex-align-client status and the SessionStart brief to surface the queue without an extra trip to the dashboard.
Source code in src/lex_align_client/api.py
security_report ¶
Project-scoped CVE-driven denial rollup.
Used by status and the SessionStart brief to surface critical CVE pressure on packages this project has been touching, so a freshly-published advisory is visible the next time a session starts — not at commit time.
Source code in src/lex_align_client/api.py
audit ¶
Bulk-audit a project's runtime dependencies without committing.
lex-align-client audit is the read-only sibling of the pre-commit hook: walk [project].dependencies, evaluate each one against the server, and print a summary. Useful when adopting lex-align on an existing project or before sending a PR for review — you don't have to git commit just to find out which packages are out of policy.
evaluate ¶
evaluate(project_root: Path, config: ClientConfig, *, agent_model: str | None = None, agent_version: str | None = None) -> AuditReport
Evaluate every runtime dep in project_root/pyproject.toml.
Source code in src/lex_align_client/audit.py
format_report ¶
Render an :class:AuditReport as a human-readable summary.
Source code in src/lex_align_client/audit.py
run ¶
run(project_root: Path, config: ClientConfig, *, as_json: bool, agent_model: str | None = None, agent_version: str | None = None) -> int
CLI entry point. Returns the desired exit code (0/1/2).
Source code in src/lex_align_client/audit.py
claude_hooks ¶
Claude Code session hooks (Advisor surface).
SessionStart and PreToolUse proxy through to the server's /evaluate so the agent sees blocked / provisionally-allowed verdicts during planning, before the pre-commit gate fires.
handle_pre_tool_use ¶
handle_pre_tool_use(event: dict, project_root: Path, config: ClientConfig) -> Optional[tuple[str, str | None]]
Return (decision, message) for pyproject.toml edits, else None.
Source code in src/lex_align_client/claude_hooks.py
claudemd ¶
CLAUDE.md integration: write lex-align usage instructions into the project's CLAUDE.md.
install_claude_md ¶
Create CLAUDE.md or append the lex-align section if not already present.
Returns (path, created_or_updated). Returns False for the second element when the section was already present and no write was performed.
Source code in src/lex_align_client/claudemd.py
cli ¶
lex-align-client CLI: init, check, request-approval, hook, precommit.
main ¶
init ¶
init(yes: bool, server_url: str | None, project_name: str | None, mode: str | None, no_claude_hooks: bool, no_precommit: bool, no_claude_md: bool) -> None
One-time setup: write .lexalign.toml and install hooks.
Source code in src/lex_align_client/cli.py
check ¶
check(package: str, version: str | None, as_json: bool, agent_model: str | None, agent_version: str | None) -> None
Check a package against the server policy.
Source code in src/lex_align_client/cli.py
request_approval ¶
request_approval(package: str, rationale: str, agent_model: str | None, agent_version: str | None) -> None
Submit a non-blocking request to add package to the registry.
Source code in src/lex_align_client/cli.py
audit ¶
Re-evaluate every runtime dep in pyproject.toml against the server.
Read-only sibling of the pre-commit hook: useful when adopting lex-align on an existing project, or to check policy without needing to stage and commit. Exits 2 if any dep is DENIED.
Source code in src/lex_align_client/cli.py
status ¶
One-screen status: server reachability, hooks, pending approvals, denials.
Source code in src/lex_align_client/cli.py
precommit ¶
uninstall ¶
Remove .claude hooks and the git pre-commit shim. Leaves .lexalign.toml.
Source code in src/lex_align_client/cli.py
config ¶
.lexalign.toml reader/writer.
Schema:
project = "lex-align" server_url = "http://127.0.0.1:8765" mode = "single-user" # or "org" fail_open = true # ignored unless server is unreachable api_key_env_var = "LEXALIGN_API_KEY" # only used when mode = "org" auto_request_approval = true # PROVISIONALLY_ALLOWED → enqueue review # automatically. Defaults to true in # single-user mode, false in org mode.
find_project_root ¶
Walk up looking for .lexalign.toml. Falls back to cwd.
Source code in src/lex_align_client/config.py
precommit ¶
Git pre-commit hook entry point.
Behavior
- Read the staged contents of
pyproject.toml(if it's in the index). - Re-evaluate every runtime dependency against the server. Reading existing deps catches new CVEs published since the last commit.
- If any verdict is DENIED, exit non-zero with a structured stderr message that an AI agent can parse to self-correct.
pyproject_utils ¶
Helpers for parsing and diffing pyproject.toml dependencies.
These are the only pieces of the v1.x reconciler that survive the rewrite — the client uses them to drive the pre-commit hook and the Claude Code edit hook.
get_runtime_deps ¶
Return {normalized_name: raw_spec} from [project].dependencies.
Source code in src/lex_align_client/pyproject_utils.py
diff_deps ¶
Return ({added_name: raw_spec}, {removed_names}).
Source code in src/lex_align_client/pyproject_utils.py
extract_pinned_version ¶
Pull a concrete version out of a spec like 'redis>=5.0' or 'httpx==0.28.1'.
Source code in src/lex_align_client/pyproject_utils.py
apply_edit ¶
Simulate how a Claude Code Edit/Write/MultiEdit affects file content.
Source code in src/lex_align_client/pyproject_utils.py
detect_project_name ¶
Best-effort autodetect for lex-align-client init.
Prefer [project].name from pyproject.toml; fall back to the directory name.
Source code in src/lex_align_client/pyproject_utils.py
settings ¶
Idempotent installers for the Claude Code hooks and the git pre-commit hook.
precommit_installed ¶
True iff the git pre-commit hook contains the lex-align marker.
Source code in src/lex_align_client/settings.py
install_precommit ¶
Install or augment .git/hooks/pre-commit. Returns the path if written.
Source code in src/lex_align_client/settings.py
status ¶
lex-align-client status — one-screen project + server snapshot.
Pulls together the things a developer normally checks across multiple URLs (server health, pending approval queue, recent CVE-driven denials) plus what's only visible on the client (hook install state, local dep count) so you can see the whole picture without leaving the terminal.
The collector tolerates a missing or unreachable server: each section gets a status flag and the rest of the report still renders. This is intentional — if the server's down you still want to know which hooks are wired and how many runtime deps the project carries.
CLI¶
cli ¶
lex-align-client CLI: init, check, request-approval, hook, precommit.
main ¶
init ¶
init(yes: bool, server_url: str | None, project_name: str | None, mode: str | None, no_claude_hooks: bool, no_precommit: bool, no_claude_md: bool) -> None
One-time setup: write .lexalign.toml and install hooks.
Source code in src/lex_align_client/cli.py
check ¶
check(package: str, version: str | None, as_json: bool, agent_model: str | None, agent_version: str | None) -> None
Check a package against the server policy.
Source code in src/lex_align_client/cli.py
request_approval ¶
request_approval(package: str, rationale: str, agent_model: str | None, agent_version: str | None) -> None
Submit a non-blocking request to add package to the registry.
Source code in src/lex_align_client/cli.py
audit ¶
Re-evaluate every runtime dep in pyproject.toml against the server.
Read-only sibling of the pre-commit hook: useful when adopting lex-align on an existing project, or to check policy without needing to stage and commit. Exits 2 if any dep is DENIED.
Source code in src/lex_align_client/cli.py
status ¶
One-screen status: server reachability, hooks, pending approvals, denials.
Source code in src/lex_align_client/cli.py
precommit ¶
uninstall ¶
Remove .claude hooks and the git pre-commit shim. Leaves .lexalign.toml.
Source code in src/lex_align_client/cli.py
Server API client¶
api ¶
HTTP client for the lex-align server.
Synchronous; the CLI is one-shot, so the simplicity of httpx.Client is worth more than the throughput of an async client. Failure semantics:
- connection error and
fail_open=true→ return a synthetic ALLOWED verdict withtransport_error=Trueso the caller can warn. - connection error and
fail_open=false→ raise ServerUnreachable. - server 4xx/5xx → raise ServerError(detail).
LexAlignClient ¶
LexAlignClient(config: ClientConfig, http_client: Client | None = None, *, agent_model: Optional[str] = None, agent_version: Optional[str] = None)
Source code in src/lex_align_client/api.py
pending_approvals ¶
List PENDING_REVIEW approval requests for project.
Defaults to the project bound to this client. Used by lex-align-client status and the SessionStart brief to surface the queue without an extra trip to the dashboard.
Source code in src/lex_align_client/api.py
security_report ¶
Project-scoped CVE-driven denial rollup.
Used by status and the SessionStart brief to surface critical CVE pressure on packages this project has been touching, so a freshly-published advisory is visible the next time a session starts — not at commit time.
Source code in src/lex_align_client/api.py
Configuration¶
config ¶
.lexalign.toml reader/writer.
Schema:
project = "lex-align" server_url = "http://127.0.0.1:8765" mode = "single-user" # or "org" fail_open = true # ignored unless server is unreachable api_key_env_var = "LEXALIGN_API_KEY" # only used when mode = "org" auto_request_approval = true # PROVISIONALLY_ALLOWED → enqueue review # automatically. Defaults to true in # single-user mode, false in org mode.
find_project_root ¶
Walk up looking for .lexalign.toml. Falls back to cwd.
Source code in src/lex_align_client/config.py
Settings¶
settings ¶
Idempotent installers for the Claude Code hooks and the git pre-commit hook.
precommit_installed ¶
True iff the git pre-commit hook contains the lex-align marker.
Source code in src/lex_align_client/settings.py
install_precommit ¶
Install or augment .git/hooks/pre-commit. Returns the path if written.
Source code in src/lex_align_client/settings.py
pyproject.toml utilities¶
pyproject_utils ¶
Helpers for parsing and diffing pyproject.toml dependencies.
These are the only pieces of the v1.x reconciler that survive the rewrite — the client uses them to drive the pre-commit hook and the Claude Code edit hook.
get_runtime_deps ¶
Return {normalized_name: raw_spec} from [project].dependencies.
Source code in src/lex_align_client/pyproject_utils.py
diff_deps ¶
Return ({added_name: raw_spec}, {removed_names}).
Source code in src/lex_align_client/pyproject_utils.py
extract_pinned_version ¶
Pull a concrete version out of a spec like 'redis>=5.0' or 'httpx==0.28.1'.
Source code in src/lex_align_client/pyproject_utils.py
apply_edit ¶
Simulate how a Claude Code Edit/Write/MultiEdit affects file content.
Source code in src/lex_align_client/pyproject_utils.py
detect_project_name ¶
Best-effort autodetect for lex-align-client init.
Prefer [project].name from pyproject.toml; fall back to the directory name.
Source code in src/lex_align_client/pyproject_utils.py
Pre-commit hook¶
precommit ¶
Git pre-commit hook entry point.
Behavior
- Read the staged contents of
pyproject.toml(if it's in the index). - Re-evaluate every runtime dependency against the server. Reading existing deps catches new CVEs published since the last commit.
- If any verdict is DENIED, exit non-zero with a structured stderr message that an AI agent can parse to self-correct.
Claude Code hooks¶
claude_hooks ¶
Claude Code session hooks (Advisor surface).
SessionStart and PreToolUse proxy through to the server's /evaluate so the agent sees blocked / provisionally-allowed verdicts during planning, before the pre-commit gate fires.
handle_pre_tool_use ¶
handle_pre_tool_use(event: dict, project_root: Path, config: ClientConfig) -> Optional[tuple[str, str | None]]
Return (decision, message) for pyproject.toml edits, else None.
Source code in src/lex_align_client/claude_hooks.py
CLAUDE.md rendering¶
claudemd ¶
CLAUDE.md integration: write lex-align usage instructions into the project's CLAUDE.md.
install_claude_md ¶
Create CLAUDE.md or append the lex-align section if not already present.
Returns (path, created_or_updated). Returns False for the second element when the section was already present and no write was performed.
Source code in src/lex_align_client/claudemd.py
lex_align_server¶
The FastAPI service that owns the registry, runs license + CVE checks, persists the audit log, and surfaces report endpoints.
lex_align_server ¶
api ¶
v1 ¶
approval_requests ¶
POST /api/v1/approval-requests — the Good Citizen.
Persists the request, then non-blockingly fires the configured proposer so a PR (or local-file write, etc.) gets opened. The 202 returns immediately — the agent isn't penalized if the proposer hits a transient error; the audit row is still durable, the dashboard will surface it, and the operator can re-trigger from the UI.
The proposer call defaults the YAML status to approved — a safe, explicit "this package is in the registry but isn't blessed as preferred." The PR description tells reviewers they can flip it to preferred (or version-constrained / deprecated) before merge if a stronger classification is appropriate.
evaluate ¶
GET /api/v1/evaluate — the Advisor.
health ¶
GET /api/v1/health — basic liveness/readiness.
registry ¶
Registry endpoints used by the dashboard workshop and the agent request-approval flow.
- GET /registry live registry as JSON.
- GET /registry/pending explicit pending approval requests plus implicit candidates (packages seen in audit_log without an approval-request follow-up). Each row carries a
reasondescribing why it's surfacing. - POST /registry/proposals operator-initiated proposal — opens a PR / commits / writes YAML depending on the configured proposer backend. Replaces the pre-Phase-4 in-memory classify endpoint.
- POST /registry/parse-yaml YAML round-trip validator, used by the dashboard's import button.
- POST /registry/reload operator-only manual reload (fallback for when the merge webhook is missed).
- POST /registry/webhook git-host webhook callback (verified by
REGISTRY_WEBHOOK_SECRET); triggers a pull-and-reload.
The classify endpoint (POST /registry/packages) was removed in Phase 4 — every registry update now flows through a proposer and a reload, so there's exactly one source of truth (registry.yml).
ProposalBody ¶
Bases: BaseModel
Same shape as the legacy classify body. The endpoint emits a proposer call instead of mutating the in-memory registry, so the fields are advisory until the proposal lands (PR merged / file written / commit landed).
pending_requests async ¶
Triage queue for the dashboard.
Combines two streams
explicit— approval requests an agent or operator filed viaPOST /approval-requests.implicit— packages seen inaudit_logover the last 30 days that never generated an approval request (because the agent only calledcheck, or the call returned DENIED, or only the pre-commit hook ran). Each row carries areasonexplaining why it surfaced.
Both streams filter against the live registry — once a package is classified (PR merged → reload → mark_approved_by_package fires), it drops out of both lists automatically.
Source code in src/lex_align_server/api/v1/registry.py
open_proposal async ¶
open_proposal(body: ProposalBody, request: Request, project: str = Depends(get_project), requester: str = Depends(get_requester), agent: AgentInfo = Depends(get_agent_info)) -> dict
Operator-initiated proposal — fires the configured proposer.
Replaces the legacy POST /registry/packages in-memory classify endpoint. The dashboard's "Approve" button calls this; the result payload tells the UI whether a PR was opened (opened / amended), the change was applied directly (applied, local-file / local-git modes), or it was logged only.
Source code in src/lex_align_server/api/v1/registry.py
reload_endpoint async ¶
Manual fallback for the case where the merge webhook is lost.
Re-reads REGISTRY_PATH from disk, validates, and atomically swaps the in-memory registry. Operator-only by convention — when org-mode auth is enabled the auth backend gates this with everything else.
Source code in src/lex_align_server/api/v1/registry.py
webhook_endpoint async ¶
webhook_endpoint(request: Request, x_hub_signature_256: Optional[str] = Header(None), x_github_event: Optional[str] = Header(None)) -> dict
GitHub-style webhook callback.
Verifies the HMAC-SHA256 signature against REGISTRY_WEBHOOK_SECRET. On a merged pull_request event, pulls the merged YAML and triggers a reload. Pings (ping event) return 200 so the operator can use the host's "test webhook" UI.
Source code in src/lex_align_server/api/v1/registry.py
reports ¶
Read-only report endpoints.
Phase 4 dashboards consume these; Phase 3 also exposes them directly so operators can query them with curl.
agents_report async ¶
Aggregate audit rows by (agent_model, agent_version).
Operators use this to answer "which Claude version is doing what" — the headers X-LexAlign-Agent-Model and X-LexAlign-Agent-Version reported by the client propagate into every audit row.
Source code in src/lex_align_server/api/v1/reports.py
audit ¶
SQLite audit log + approval-request store.
Two tables:
audit_log— one row per/evaluatecall. Backs the legal report (license-driven denials) and security report (CVE-driven denials).approval_requests— one row per/approval-requestsPOST. Phase 3 will attach a PR-creation workflow; for now we just persist them.
We use plain aiosqlite rather than an ORM. The schema is small and the report queries are easier to read as straight SQL.
AuditStore ¶
Source code in src/lex_align_server/audit.py
record_cve_alert async ¶
record_cve_alert(*, package: str, cve_ids: list[str], max_cvss: Optional[float], registry_status: Optional[str] = None, project: str = 'lex-align-server', requester: str = 'cve-scanner') -> str
Persist a single CVE_ALERT row from the background scanner.
Reuses the audit_log table so the dashboard, exports, and the client status command see one consistent event stream — the denial_category=DENIAL_CVE_ALERT discriminator keeps these out of the existing CVE-denial rollups.
Source code in src/lex_align_server/audit.py
mark_approved_by_package async ¶
Move every PENDING_REVIEW request for normalized_name to APPROVED.
Called when an operator classifies a pending package via the dashboard and adds it to the in-memory registry. Returns the number of rows flipped, which the dashboard can display in its toast.
Source code in src/lex_align_server/audit.py
legal_report async ¶
License-compliance posture for the audit log.
Returns the existing total_denials / recent fields plus three rollups the dashboard renders as charts:
license_breakdown— every audit row in scope grouped bylicenseand split by verdict, so operators can see how often each license shows up and how the policy classified it.unknown_license— same shape, restricted to rows whose license normalised toUNKNOWN. Surfaces how theunknown_license_policyis performing in production.top_projects— projects ranked by license-driven denials, so operators can tell which repos are pulling the most non-compliant packages.
Source code in src/lex_align_server/audit.py
security_report async ¶
Vulnerability posture for the audit log.
Adds three rollups on top of the base denial report:
severity_distribution— CVE-denied rows bucketed by CVSS severity (critical / high / medium / low / unknown).top_packages— packages with the most CVE-driven denials, carrying their highest-seen CVSS and the CVE ids responsible.top_cves— the CVE identifiers showing up most often.hot_registry_packages— only included whenregistryis provided. Lists packages currently in the registry aspreferred/approved/version-constrainedwhose recent audit rows include a CVE denial or provisional verdict. This catches the "we said yes, then OSV published a critical" scenario the pre-commit hook is meant to backstop.
Source code in src/lex_align_server/audit.py
recent_cve_alerts async ¶
Most recent CVE_ALERT rows written by the background scanner.
Surfaced through security_report so the dashboard and the client's status command see scanner output without any endpoint changes.
Source code in src/lex_align_server/audit.py
list_pending_by_package async ¶
All PENDING_REVIEW approval requests grouped by package.
The dashboard uses this to surface "things developers asked for" as a triage queue. Multiple requests for the same package (across projects/requesters) collapse into a single row carrying the count, the most recent rationale, and the most recent timestamp. Callers are expected to further filter against the live registry.
Source code in src/lex_align_server/audit.py
list_implicit_candidates async ¶
Packages that show up in audit_log but never produced an explicit request-approval row.
This catches the cases where an agent / hook only ever called check: the verdict's recorded but no one is going to file an approval — yet the operator still wants to triage.
Each row is annotated with a reason field explaining why it's surfacing:
provisional-no-rationale — the package got PROVISIONALLY_ALLOWED and the agent didn't follow up with request-approval. Highest signal. repeatedly-denied — the package was DENIED repeatedly, so an explicit registry rule (banned, version-pinned, replaced) would save downstream agents from rediscovering the wall. pre-screened — only check calls, all ALLOWED. Lowest signal, but useful for "things lots of agents poke at."
Callers (the dashboard) further filter against the live registry so packages that just got merged stop appearing immediately.
Source code in src/lex_align_server/audit.py
795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 | |
agents_report async ¶
Aggregate evaluations by (agent_model, agent_version).
Powers the "agents" dashboard, so operators can see exactly which Claude (or other agent) version is making which kinds of requests. Rows where the agent is unknown collapse into one bucket.
Source code in src/lex_align_server/audit.py
auth ¶
Auth dependencies.
Identity resolution lives in the pluggable authenticator on app.state.lex.authenticator (see authn/). The functions here are thin FastAPI Depends shims so endpoints can stay declarative:
requester: str = Depends(get_requester)
identity: Identity = Depends(get_identity)
agent: AgentInfo = Depends(get_agent_info)
Single-user mode ships an AnonymousAuthenticator so the same code path works without any auth setup; org mode swaps in whatever the operator configured via AUTH_BACKEND.
The agent-identity headers (X-LexAlign-Agent-Model / -Version) are tag metadata, not authentication — they carry the model that originated the request (e.g. opus 4.7) and feed the agent-activity dashboards. The Authenticator answers "who is calling" (the human/principal); get_agent_info answers "which agent did the calling for them."
AgentInfo dataclass ¶
Agent identity reported by the client.
Both fields are optional; when the client doesn't send the headers, both are None and reports group those rows under an "(unknown agent)" bucket.
get_identity async ¶
Resolve the requester via the per-app authenticator.
Endpoints depend on this when they need email/groups (e.g. for future per-team authorization). For the common "I just need a string for the audit log" case, depend on :func:get_requester instead.
Source code in src/lex_align_server/auth.py
get_requester async ¶
authn ¶
Pluggable authentication for org mode.
Single-user mode (AUTH_ENABLED=false) bypasses this entirely and uses AnonymousAuthenticator. Org mode (AUTH_ENABLED=true) selects one of the built-in backends via AUTH_BACKEND:
header(recommended) — trustX-Forwarded-User/-Email/-Groupsinjected by an upstream auth gateway (oauth2-proxy, Pomerium, Cloudflare Access, an ingress' OIDC filter, etc.). Zero Python customization; the org configures their existing gateway.webhook— forward the request's bearer token to a URL the org controls (AUTH_VERIFY_URL); the verifier returns user JSON. The org writes one tiny endpoint in any language.apikey— stub for a future built-in API-key store. Currently raisesNotImplementedErrorso an ill-configured deployment fails loud rather than silently allowing anonymous access.module:path.to:ClassName— escape hatch. Drop a Python file implementing :class:Authenticatorinto the container and point the env var at it.
The dependency surface (get_identity/get_requester in auth.py) is stable; orgs only need to swap the backend.
AuthError ¶
Bases: HTTPException
Authentication failed. Maps to HTTP 401 with a WWW-Authenticate header so curl/SDK clients can react sensibly.
Source code in src/lex_align_server/authn/base.py
Authenticator ¶
Bases: ABC
Resolve a :class:Request to an :class:Identity or raise.
Implementations must
- return an :class:
Identityon success; - raise :class:
AuthError(or anyHTTPException) on failure; - be
async— the framework awaits them on every request.
They should not perform authorization (group/role checks): that's a downstream concern. The Authenticator's only job is to answer "who is making this request".
Identity dataclass ¶
Identity(id: str, email: str | None = None, groups: tuple[str, ...] = (), raw: Mapping[str, Any] = dict())
The authenticated principal for a single request.
id is what lex-align records in audit_log.requester and uses to de-dupe approval requests. The other fields are advisory and surface in the dashboards: email for "who asked for this package", and groups for future per-team authorization (e.g. limit registry edits to security-engineers). raw is a free-form dict — backend-specific extras (claims from a JWT, the full user record from a webhook response, etc.) — that custom authenticators can stash for downstream code without inventing new fields.
load_authenticator ¶
Return the authenticator selected by settings.
Single-user mode short-circuits to anonymous regardless of AUTH_BACKEND — so flipping AUTH_ENABLED=false is always a safe escape hatch during incident response.
Source code in src/lex_align_server/authn/loader.py
anonymous ¶
Anonymous authenticator — used when AUTH_ENABLED=false.
Always returns the same identity. Single-user mode runs this so that the audit log still has a valid requester value without any auth setup.
apikey ¶
Built-in API-key store — currently a stub.
Reserved for a future backend that validates bearer tokens against an api_keys SQLite table managed by lex-align-server admin keys. Useful for evaluation or for orgs that don't want to wire up SSO.
Until that's implemented, selecting AUTH_BACKEND=apikey raises at request time so a misconfigured deployment fails loud rather than silently dropping every request through.
base ¶
Authenticator contract.
Custom backends only need to subclass :class:Authenticator and implement authenticate. Everything else (settings parsing, dependency wiring, audit logging) is handled by the framework.
Identity dataclass ¶
Identity(id: str, email: str | None = None, groups: tuple[str, ...] = (), raw: Mapping[str, Any] = dict())
The authenticated principal for a single request.
id is what lex-align records in audit_log.requester and uses to de-dupe approval requests. The other fields are advisory and surface in the dashboards: email for "who asked for this package", and groups for future per-team authorization (e.g. limit registry edits to security-engineers). raw is a free-form dict — backend-specific extras (claims from a JWT, the full user record from a webhook response, etc.) — that custom authenticators can stash for downstream code without inventing new fields.
AuthError ¶
Bases: HTTPException
Authentication failed. Maps to HTTP 401 with a WWW-Authenticate header so curl/SDK clients can react sensibly.
Source code in src/lex_align_server/authn/base.py
Authenticator ¶
Bases: ABC
Resolve a :class:Request to an :class:Identity or raise.
Implementations must
- return an :class:
Identityon success; - raise :class:
AuthError(or anyHTTPException) on failure; - be
async— the framework awaits them on every request.
They should not perform authorization (group/role checks): that's a downstream concern. The Authenticator's only job is to answer "who is making this request".
header ¶
Trust headers from an upstream auth gateway.
This is the recommended backend for organizations: oauth2-proxy, Pomerium, Authelia, Cloudflare Access, ingress-nginx with auth_request, Envoy ext_authz — any of them can authenticate against your existing SSO and forward the result as headers. lex-align then consumes them.
Critical: only trust the headers when the request reaches the server through a known proxy. trusted_proxies is a list of CIDR blocks; any request from outside those blocks is rejected with HTTP 401 even if the headers are present, because an attacker can otherwise spoof them by hitting the server directly. The default of 127.0.0.1/32 assumes the proxy is co-located.
loader ¶
Resolve the configured AUTH_BACKEND to an :class:Authenticator.
Called once from the FastAPI lifespan. The result is stored on app.state.lex.authenticator and reused for every request via the get_identity dependency.
load_authenticator ¶
Return the authenticator selected by settings.
Single-user mode short-circuits to anonymous regardless of AUTH_BACKEND — so flipping AUTH_ENABLED=false is always a safe escape hatch during incident response.
Source code in src/lex_align_server/authn/loader.py
webhook ¶
Forward the bearer token to an org-controlled verifier.
The org writes one tiny endpoint that accepts POST <verify_url> with {"token": "..."} and returns {"id": "...", "email"?: "...", "groups"?: ["..."]} on success or a non-2xx on failure. lex-align treats the returned JSON as the :class:Identity payload.
This is a good fit for orgs that don't have an HTTP-level auth gateway but do have an internal user-info or session-validation service. The verifier can be a Lambda, a sidecar, an internal microservice — any HTTP endpoint that knows how to validate the org's tokens.
cache ¶
Redis-backed JSON cache.
Used by the license and CVE adapters to avoid hammering PyPI/OSV. We intentionally swallow connection errors and degrade to "no cache" so the server keeps serving when Redis is unreachable — the audit log still records every decision.
check_config ¶
lex-align-server check-config — pre-flight checks for a single-team local-file deployment.
Each check is independent: it returns a :class:CheckResult so the CLI can render it as a coloured row and exit non-zero when any of them fails. The checks deliberately stop at what a one-person / single-team install needs:
REGISTRY_PATHis set, exists or can be created, and round-trips through the registry validator.- The audit SQLite directory is writable.
- The cache backend (Redis if configured, in-memory otherwise) is reachable.
- The proposer auto-detects to
local_file(the recommended single- team mode) — anything else surfaces as a warning so operators don't silently end up on a heavier backend. - Authentication is configured deliberately, not by accident.
The validator never connects to GitHub. The PR-based proposer is the escape hatch for large orgs; if you're running check-config you're almost certainly on the local-file path and the GitHub probe would just add noise.
check_registry_path ¶
REGISTRY_PATH must be set to a YAML file path the server can use.
Source code in src/lex_align_server/check_config.py
check_registry_yaml ¶
The registry YAML, if present, must round-trip through the validator.
Source code in src/lex_align_server/check_config.py
check_audit_path ¶
The audit SQLite directory must be writable.
Source code in src/lex_align_server/check_config.py
check_cache async ¶
Ping the configured cache backend.
Cache failure is not fatal — the server degrades to "no cache" and keeps serving — so this is a warning, never a hard failure.
Source code in src/lex_align_server/check_config.py
check_proposer ¶
Recommend local_file for the single-team path.
Anything other than local_file / log_only is surfaced as a warning so the operator notices if auto-detection picked something heavier than they expected.
Source code in src/lex_align_server/check_config.py
check_auth ¶
Surface accidental anonymous-mode deployments.
Anonymous is a fine default (it makes evaluation friction-free), but a server bound to anything other than localhost without auth is almost always a misconfiguration.
Source code in src/lex_align_server/check_config.py
run_checks async ¶
Run every check in the order the CLI prints them.
Source code in src/lex_align_server/check_config.py
run_checks_sync ¶
cli ¶
lex-align-server CLI.
Exposes
serve— run uvicorn against the FastAPI app.quickstart— Docker-free single-user bring-up: materialize a local registry + sqlite, then run the server in-process on 127.0.0.1:8765.init— materialize the operator bundle (compose stack, Dockerfile, registry, .env) into a target dir.check-config— pre-flight checks for a single-team install: REGISTRY_PATH, audit DB, cache, proposer, auth.registry compile— compile a YAML registry to the JSON form the server consumes.selftest— hit/api/v1/healthto confirm a stack is up.admin keys ...— placeholders for the org-mode admin tooling.
main ¶
serve ¶
Start the lex-align FastAPI server via uvicorn.
Source code in src/lex_align_server/cli.py
quickstart ¶
Docker-free single-user bring-up.
Lays down a local registry + sqlite under --target (default ~/.lexalign), then runs the server in-process on http://127.0.0.1:8765. Redis is skipped (the cache layer silently degrades). Stop with Ctrl-C.
For multi-user deployments use lex-align-server init + Docker Compose instead.
Source code in src/lex_align_server/cli.py
init ¶
Materialize the docker-compose stack, Dockerfile, registry, and .env template into TARGET so the server can be built and run with a single docker compose up -d.
Source code in src/lex_align_server/cli.py
registry ¶
registry_compile ¶
Compile a YAML registry SOURCE to the JSON form at DESTINATION.
Source code in src/lex_align_server/cli.py
check_config ¶
Validate the server's configuration for a single-team install.
Verifies REGISTRY_PATH, audit DB writability, cache reachability, the auto-detected proposer, and auth posture. Exits non-zero if any check fails. Warnings (e.g. anonymous auth on a non-loopback bind, Redis unreachable) do not fail the run.
Source code in src/lex_align_server/cli.py
selftest ¶
Probe /api/v1/health and report whether the stack is alive.
Source code in src/lex_align_server/cli.py
admin ¶
config ¶
Server configuration.
All settings are sourced from environment variables. Defaults bias towards the single-user/local-evaluation experience: no auth, bind to localhost, point at a co-located Redis.
get_settings ¶
Build a fresh Settings object. Avoids module-level caching so tests can override via env vars on a per-test basis.
cve ¶
CVE lookup via OSV (osv.dev).
OSV is the Google-sponsored aggregator that ingests GHSA, NVD, PyPA Advisory DB, and others. The free POST /v1/query endpoint takes a package coordinate and returns the list of vulnerabilities with severity scores. We parse out the highest CVSS base score and let GlobalPolicies.cve_blocks decide.
query_osv async ¶
query_osv(package: str, version: Optional[str], osv_url: str, http_client: AsyncClient) -> Optional[list[dict]]
Return raw vulns list from OSV, or None on error/timeout.
Source code in src/lex_align_server/cve.py
resolve_cves async ¶
resolve_cves(package: str, version: Optional[str], cache: JsonCache, cache_ttl: int, osv_url: str, http_client: AsyncClient) -> CveInfo
Cache-or-fetch CVE data for a package@version.
A None version is cached separately from versioned queries — OSV's behaviour with no version is documented as "all versions" so we treat it as a distinct cache entry.
Source code in src/lex_align_server/cve.py
cve_scanner ¶
Background CVE re-scan scheduler.
Walks every package in the live registry on a fixed cadence, re-queries OSV via :func:lex_align_server.cve.resolve_cves, and writes a CVE_ALERT row to the audit log whenever the package's max CVSS now crosses GlobalPolicies.cve_threshold.
The scanner is alert-only. It does not flip registry entries to banned or otherwise change verdict policy — that decision belongs to the operator. The dashboard and the client's status command surface the alerts via the existing security_report endpoint.
Lifecycle mirrors RegistryPoller: start() schedules an asyncio task on the running loop, stop() drains it. Wired into the FastAPI lifespan so the task starts and stops with the server.
CveScanner ¶
Periodic OSV re-scan of every registered package.
The first scan runs after the configured interval, not at startup — server bring-up should not block on a wave of OSV requests, and the /evaluate path already covers fresh additions.
Source code in src/lex_align_server/cve_scanner.py
scan_once async ¶
Run one scan pass over the live registry.
Returns the number of CVE_ALERT rows written. Public so tests and the operator's manual reload paths can drive it.
Source code in src/lex_align_server/cve_scanner.py
dashboards ¶
router ¶
Dashboard pages.
/ is a welcome landing page. A shared side panel (_sidebar.html) links the landing page and every dashboard so operators can switch views without retyping URLs.
Four dashboards render server-side and fetch their data from the JSON API:
/dashboard/securityis a vulnerability-posture view: severity buckets, packages with the worst CVE history, and the "hot" cell — registry-allowed packages that have started attracting CVE denials./dashboard/legalis a license-compliance view: license breakdown by verdict, unknown-license policy performance, projects pulling the most non-compliant packages./dashboard/agentsshows a generic agent-activity report./dashboard/registryis an interactive workshop: it loads the live registry, lets the operator triage pending approval requests, and exports the result as YAML. Classifying a pending request also updates the in-memory registry so live/evaluatecalls see the rule immediately, but persistence still requires exporting the YAML and rebuilding the server image.
evaluate ¶
Evaluation orchestrator.
Combines registry lookup, CVE check, and license check into a single verdict. Emits one audit-log row per call. The order matters:
- registry hard-blocks (banned, deprecated, version-violated)
- CVE check — applies even to registry-allowed packages so a newly published critical CVE on a
preferredpackage still blocks - license check — only when the package is unknown to the registry
evaluate async ¶
evaluate(*, package: str, version: Optional[str], project: str, requester: str, registry: Optional[Registry], cache: JsonCache, audit: AuditStore, settings: Settings, http_client: AsyncClient, agent: Optional[AgentInfo] = None) -> EvaluationResult
Single evaluation. Always writes one audit row before returning.
Source code in src/lex_align_server/evaluate.py
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 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 | |
init ¶
lex-align-server init — materialize the operator bundle into a directory.
Mirrors the design of lex-align-client init: copies the docker-compose stack, Dockerfile, example registry, and .env.example from the wheel's bundled assets into a target directory, then compiles the registry to JSON so docker compose up works on the first try.
asset_names ¶
All asset basenames as they appear in the wheel. Useful for tests.
init_target ¶
Materialize the operator bundle into target.
Idempotent by default: if the marker file exists, raises FileExistsError unless force is set.
Source code in src/lex_align_server/init.py
licenses ¶
License lookup, normalization, and policy evaluation.
For packages not listed in the registry, the server fetches the license from PyPI, normalizes it to an SPDX-ish token, and checks it against global_policies. Results are cached in Redis keyed by package name.
fetch_license_from_pypi async ¶
fetch_license_from_pypi(package: str, version: Optional[str], pypi_base: str, client: AsyncClient) -> tuple[Optional[str], Optional[str]]
Return (raw_license_string, latest_version) from PyPI, or (None, None) on error.
Source code in src/lex_align_server/licenses.py
resolve_license async ¶
resolve_license(package: str, version: Optional[str], cache: JsonCache, cache_ttl: int, pypi_base: str, http_client: AsyncClient) -> tuple[LicenseInfo, Optional[str]]
Resolve (cache-or-fetch) a package's license. Returns (info, latest_version).
Source code in src/lex_align_server/licenses.py
main ¶
FastAPI application factory.
Wires up the routers, builds the per-app state (cache, audit store, HTTP client, registry), and exposes a lifespan so resources are torn down cleanly.
proposer ¶
Pluggable registry-update proposers.
When an agent calls request-approval or an operator clicks "Approve" on the dashboard, the server emits a proposal to update registry.yml with the new (or amended) package rule. How that proposal lands depends entirely on the configured proposer:
log_only— log it; don't touch anything. Safe default for evaluation / read-only demos. Selected when no registry write target is configured.local_file— write directly toREGISTRY_PATH, recompile, reload in-memory. Best fit for single-user mode and local evaluation.local_git— commit to a local git working tree (no remote, no PR). Useful when the operator wants the audit trail of git history without hosting a remote.github— clone, branch, push, open a PR against the configured remote (REGISTRY_REPO_URL). The merge webhook on/api/v1/registry/webhooktriggers the in-memory reload.module:path:Class— escape hatch for custom CI/CD portals.
Selection happens via load_proposer(settings, http_client) — the loader auto-detects from existing settings, so REGISTRY_PROPOSER is optional. See proposer/loader.py for the precedence rules.
The dependency surface for endpoints (ProposedRule, ProposalContext, ProposalResult) is stable across backends; the endpoint code never branches on which proposer is active.
ProposalContext dataclass ¶
ProposalContext(source: str, project: str, requester: str, rationale: str, agent_model: Optional[str] = None, agent_version: Optional[str] = None, extra: Mapping[str, Any] = dict())
Who is asking for the change and why.
source is "agent" for the agent's request-approval flow and "operator" when the dashboard is the originator. Both values end up in commit messages / PR bodies so reviewers can tell at a glance whether the change came from a human triaging the queue or from an agent running unsupervised.
ProposalResult dataclass ¶
ProposalResult(backend: str, status: str, url: Optional[str] = None, branch: Optional[str] = None, commit_sha: Optional[str] = None, detail: str = '')
Outcome surfaced back to the API caller.
status values: * opened — a fresh PR / commit / file write was created. * amended — an existing open proposal for this package was updated (e.g. the agent re-requested with a new rationale). * applied — the change is live (local-file / local-git backends, or a webhook-driven reload after merge). * logged — log-only backend; no durable change happened. * failed — proposer hit an error. detail describes it.
ProposedRule dataclass ¶
ProposedRule(name: str, status: str, reason: Optional[str] = None, replacement: Optional[str] = None, min_version: Optional[str] = None, max_version: Optional[str] = None)
The package classification an agent or operator wants in the registry.
name is the raw display form (Some-Pkg); the proposer normalizes it before writing to YAML. Optional fields mirror the YAML schema 1:1.
to_yaml_rule ¶
Render to the dict shape the YAML schema expects.
Source code in src/lex_align_server/proposer/base.py
Proposer ¶
Bases: ABC
Resolve a :class:ProposedRule into a registry change.
Implementations must be re-entrant — multiple requests for the same package collapse onto the same branch/PR via the backend's own idempotency rules, but the framework doesn't serialize calls.
close async ¶
Optional cleanup. Override for proposers that hold long-lived resources (open file handles, working trees, etc.).
ProposerError ¶
Bases: Exception
Proposer hit a recoverable error. Endpoint code wraps this as a :class:ProposalResult with status="failed" so a transient git outage doesn't cascade into an HTTP 5xx for the agent.
base ¶
Proposer contract.
The dataclasses are deliberately small and serializable so dashboard responses can carry them verbatim without an additional translation layer. Custom backends only need to subclass :class:Proposer and implement propose; close is a no-op default for proposers that don't keep persistent state.
ProposedRule dataclass ¶
ProposedRule(name: str, status: str, reason: Optional[str] = None, replacement: Optional[str] = None, min_version: Optional[str] = None, max_version: Optional[str] = None)
The package classification an agent or operator wants in the registry.
name is the raw display form (Some-Pkg); the proposer normalizes it before writing to YAML. Optional fields mirror the YAML schema 1:1.
to_yaml_rule ¶
Render to the dict shape the YAML schema expects.
Source code in src/lex_align_server/proposer/base.py
ProposalContext dataclass ¶
ProposalContext(source: str, project: str, requester: str, rationale: str, agent_model: Optional[str] = None, agent_version: Optional[str] = None, extra: Mapping[str, Any] = dict())
Who is asking for the change and why.
source is "agent" for the agent's request-approval flow and "operator" when the dashboard is the originator. Both values end up in commit messages / PR bodies so reviewers can tell at a glance whether the change came from a human triaging the queue or from an agent running unsupervised.
ProposalResult dataclass ¶
ProposalResult(backend: str, status: str, url: Optional[str] = None, branch: Optional[str] = None, commit_sha: Optional[str] = None, detail: str = '')
Outcome surfaced back to the API caller.
status values: * opened — a fresh PR / commit / file write was created. * amended — an existing open proposal for this package was updated (e.g. the agent re-requested with a new rationale). * applied — the change is live (local-file / local-git backends, or a webhook-driven reload after merge). * logged — log-only backend; no durable change happened. * failed — proposer hit an error. detail describes it.
ProposerError ¶
Bases: Exception
Proposer hit a recoverable error. Endpoint code wraps this as a :class:ProposalResult with status="failed" so a transient git outage doesn't cascade into an HTTP 5xx for the agent.
Proposer ¶
Bases: ABC
Resolve a :class:ProposedRule into a registry change.
Implementations must be re-entrant — multiple requests for the same package collapse onto the same branch/PR via the backend's own idempotency rules, but the framework doesn't serialize calls.
close async ¶
Optional cleanup. Override for proposers that hold long-lived resources (open file handles, working trees, etc.).
github ¶
GitHub PR proposer.
Production-grade flow for orgs that want PR review on every registry change. Each propose() call:
- Ensures a fresh shallow clone of the registry repo in a working directory (
REGISTRY_REPO_WORKDIR, default/var/lib/lexalign/registry-work). - Branches off the configured default branch as
lex-align/approval/<normalized-package-name>. If the branch already exists on the remote, fetches and re-uses it so the same package never gets two parallel PRs. - Edits the YAML to add / replace the package rule.
- Commits with an author identity scoped to the bot (
REGISTRY_BOT_AUTHOR_NAME/REGISTRY_BOT_AUTHOR_EMAIL). - Pushes the branch.
- Opens a PR via the GitHub REST API, or posts a follow-up comment if one is already open for the same branch.
The merge → reload step is not in this module — the operator wires GitHub's webhook to POST /api/v1/registry/webhook, which pulls the updated YAML and triggers the in-memory reload.
We shell out to git (subprocess) and use httpx for the REST calls. No additional Python dependencies.
GitHubProposer ¶
GitHubProposer(*, repo_url: str, registry_file_path: str, token: str, http_client: AsyncClient, workdir: Path, api_base: str = 'https://api.github.com', default_branch: str = 'main', author_name: str = 'lex-align bot', author_email: str = 'lex-align-bot@users.noreply.github.com')
Bases: Proposer
Source code in src/lex_align_server/proposer/github.py
refresh_local_yaml async ¶
Pull the merged registry YAML into the local REGISTRY_PATH.
Called by the webhook handler on a pull_request.merged event. Without this, the reloader's read-from-disk would still show the pre-merge state until the next git pull happened. Cheap: a depth-1 fetch plus a file copy.
Source code in src/lex_align_server/proposer/github.py
loader ¶
Resolve the configured proposer for the running server.
Called once from the FastAPI lifespan. The result is stored on app.state.lex.proposer and reused for every approval request and every dashboard "Approve" click.
Auto-detection biases towards the single-team / local-file experience. REGISTRY_PROPOSER only needs to be set when overriding the default:
REGISTRY_REPO_URLset explicitly →github(the PR-based backend; opt- in only — never auto- selected from the presence of a GitHub remote alone).REGISTRY_PATHis inside a git working →local_git. tree.REGISTRY_PATHset, parent dir writable →local_file.- Nothing configured →
log_only, with a startup warning.
local_file ¶
Direct-write proposer — edits REGISTRY_PATH in place.
Best fit for single-user mode and small teams without a git host integration. The flow is:
- Read the current YAML (creating an empty registry if absent).
- Insert / replace the package rule.
- Validate the merged document via
validate_registryso an invalid proposal can never produce a corrupt file on disk. - Write atomically (write-to-temp + rename).
- Recompile and reload the in-memory
Registryso the next/evaluatecall sees the change immediately.
There is no review step — the dashboard / agent's request is the authorization. Use local_git if you want a git log audit trail, or the GitHub backend if you want PR review.
local_git ¶
Local-git proposer — commits to a working tree, no remote, no PR.
Same flow as local_file but with a git commit after each write so the operator gets git log as the audit trail. Useful for teams that want history without standing up a GitHub/GitLab integration.
We shell out to the system git rather than pulling in a Python git library — keeps the dependency surface small and the operator's normal git tooling (signed commits, hooks, config) keeps working unchanged.
log_only ¶
Log-only proposer — does nothing durable.
Selected automatically when no registry write target is configured (neither REGISTRY_REPO_URL nor a writable REGISTRY_PATH). Useful for read-only demos and for the "is this thing even talking to my server?" smoke test phase of evaluation. The dashboard's "Approve" button still works — it just won't make the change stick.
quickstart ¶
lex-align-server quickstart — Docker-free single-user bring-up.
The Docker compose path stays the recommended deployment for anything multi-user, but a single developer evaluating the tool on a laptop benefits from a one-command path that:
- materializes a registry into a writable directory next to a sqlite audit DB,
- runs the FastAPI app in-process under uvicorn on 127.0.0.1:8765,
- skips Redis (the cache layer already silently degrades when the configured URL can't be reached, so the server still serves — license and CVE lookups just hit upstream every time).
This is not a substitute for the docker stack in production; it is a shortcut for "I want to try this on my laptop without setting up infrastructure." The marker file (.lexalign-quickstart.toml) keeps the directory layout idempotent and distinct from init's docker bundle.
materialize ¶
materialize(target: Path | None = None, *, bind_host: str = '127.0.0.1', bind_port: int = 8765, force: bool = False) -> QuickstartResult
Lay down target so serve can boot against it.
Idempotent: existing files are preserved unless force is set. The registry YAML is always (re)compiled to JSON because the server reads the JSON form.
Source code in src/lex_align_server/quickstart.py
apply_env ¶
Compute the environment overrides the in-process uvicorn run needs.
Returned as a dict so callers can choose to os.environ.update it (the CLI does) or inspect it (tests do). REDIS_URL is set to "none" so the cache layer skips Redis entirely without attempting a connection or emitting warnings — quickstart is explicitly single- user with no Redis available.
Source code in src/lex_align_server/quickstart.py
registry ¶
Enterprise registry: package policies, license rules, CVE threshold.
The registry is loaded from a local JSON file (compiled from the human-authored YAML by lex-align-server registry compile). The server consults it on every /evaluate call.
registry_schema ¶
Registry-YAML schema validation, shared by the CLI compiler and the dashboard's import endpoint.
This module is intentionally pure: no I/O, no CLI argparse, no FastAPI dependencies. The two entry points that consume it (the lex-align-server registry compile CLI and lex_align_server.api.v1.registry) both depend on the same validator so a registry YAML accepted by the dashboard is guaranteed to compile in CI, and vice versa.
validate_package_rule ¶
Public single-package validator. Raises ValidationError on failure.
Used by the dashboard's classify endpoint so the in-memory mutation has the same guarantees as a YAML round-trip.
Source code in src/lex_align_server/registry_schema.py
reloader ¶
Registry hot-reload coordinator.
Three triggers all funnel through reload_registry:
- the GitHub webhook handler (
POST /api/v1/registry/webhook) on PR merge — it pulls the merged YAML to disk first, then calls in; - the operator's manual
POST /api/v1/registry/reloadfallback for when the webhook gets lost; - the periodic
RegistryPollerthat watchesREGISTRY_PATHmtime everyREGISTRY_RELOAD_INTERVALseconds.
The reload is atomic: in-flight /evaluate calls finish against the old Registry; the next call sees the new one. We also flip matching PENDING_REVIEW approval rows to APPROVED for every package the new registry now contains — so the dashboard's pending queue auto-trims when a PR merges or a YAML edit goes through.
ReloadResult dataclass ¶
ReloadResult(ok: bool, previous_version: Optional[str] = None, new_version: Optional[str] = None, package_count: int = 0, added_packages: int = 0, approved_requests: int = 0, detail: str = '')
Outcome of a reload attempt; surfaced through the manual reload endpoint and recorded in the server log.
RegistryPoller ¶
Background task that reloads on REGISTRY_PATH mtime change.
Cheap: a stat() per tick, recompile only when mtime advances. The webhook is the primary reload signal in production; this exists as a backstop for missed webhooks and for local-file / local-git modes where there is no webhook at all.
Source code in src/lex_align_server/reloader.py
reload_registry async ¶
Re-read REGISTRY_PATH from disk, validate, and atomically swap the in-memory Registry. Idempotent: a no-op when the file hasn't changed.
Caller is responsible for putting the latest YAML on disk (e.g. the webhook handler runs git pull first).
Source code in src/lex_align_server/reloader.py
state ¶
Application state container.
Carried on app.state so the per-request dependencies don't have to know how the cache, audit log, or registry got built.
Application entrypoint¶
main ¶
FastAPI application factory.
Wires up the routers, builds the per-app state (cache, audit store, HTTP client, registry), and exposes a lifespan so resources are torn down cleanly.
CLI¶
cli ¶
lex-align-server CLI.
Exposes
serve— run uvicorn against the FastAPI app.quickstart— Docker-free single-user bring-up: materialize a local registry + sqlite, then run the server in-process on 127.0.0.1:8765.init— materialize the operator bundle (compose stack, Dockerfile, registry, .env) into a target dir.check-config— pre-flight checks for a single-team install: REGISTRY_PATH, audit DB, cache, proposer, auth.registry compile— compile a YAML registry to the JSON form the server consumes.selftest— hit/api/v1/healthto confirm a stack is up.admin keys ...— placeholders for the org-mode admin tooling.
main ¶
serve ¶
Start the lex-align FastAPI server via uvicorn.
Source code in src/lex_align_server/cli.py
quickstart ¶
Docker-free single-user bring-up.
Lays down a local registry + sqlite under --target (default ~/.lexalign), then runs the server in-process on http://127.0.0.1:8765. Redis is skipped (the cache layer silently degrades). Stop with Ctrl-C.
For multi-user deployments use lex-align-server init + Docker Compose instead.
Source code in src/lex_align_server/cli.py
init ¶
Materialize the docker-compose stack, Dockerfile, registry, and .env template into TARGET so the server can be built and run with a single docker compose up -d.
Source code in src/lex_align_server/cli.py
registry ¶
registry_compile ¶
Compile a YAML registry SOURCE to the JSON form at DESTINATION.
Source code in src/lex_align_server/cli.py
check_config ¶
Validate the server's configuration for a single-team install.
Verifies REGISTRY_PATH, audit DB writability, cache reachability, the auto-detected proposer, and auth posture. Exits non-zero if any check fails. Warnings (e.g. anonymous auth on a non-loopback bind, Redis unreachable) do not fail the run.
Source code in src/lex_align_server/cli.py
selftest ¶
Probe /api/v1/health and report whether the stack is alive.
Source code in src/lex_align_server/cli.py
admin ¶
Configuration¶
config ¶
Server configuration.
All settings are sourced from environment variables. Defaults bias towards the single-user/local-evaluation experience: no auth, bind to localhost, point at a co-located Redis.
get_settings ¶
Build a fresh Settings object. Avoids module-level caching so tests can override via env vars on a per-test basis.
State management¶
state ¶
Application state container.
Carried on app.state so the per-request dependencies don't have to know how the cache, audit log, or registry got built.
Evaluation pipeline¶
evaluate ¶
Evaluation orchestrator.
Combines registry lookup, CVE check, and license check into a single verdict. Emits one audit-log row per call. The order matters:
- registry hard-blocks (banned, deprecated, version-violated)
- CVE check — applies even to registry-allowed packages so a newly published critical CVE on a
preferredpackage still blocks - license check — only when the package is unknown to the registry
evaluate async ¶
evaluate(*, package: str, version: Optional[str], project: str, requester: str, registry: Optional[Registry], cache: JsonCache, audit: AuditStore, settings: Settings, http_client: AsyncClient, agent: Optional[AgentInfo] = None) -> EvaluationResult
Single evaluation. Always writes one audit row before returning.
Source code in src/lex_align_server/evaluate.py
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 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 | |
Registry¶
registry ¶
Enterprise registry: package policies, license rules, CVE threshold.
The registry is loaded from a local JSON file (compiled from the human-authored YAML by lex-align-server registry compile). The server consults it on every /evaluate call.
registry_schema ¶
Registry-YAML schema validation, shared by the CLI compiler and the dashboard's import endpoint.
This module is intentionally pure: no I/O, no CLI argparse, no FastAPI dependencies. The two entry points that consume it (the lex-align-server registry compile CLI and lex_align_server.api.v1.registry) both depend on the same validator so a registry YAML accepted by the dashboard is guaranteed to compile in CI, and vice versa.
validate_package_rule ¶
Public single-package validator. Raises ValidationError on failure.
Used by the dashboard's classify endpoint so the in-memory mutation has the same guarantees as a YAML round-trip.
Source code in src/lex_align_server/registry_schema.py
License checks¶
licenses ¶
License lookup, normalization, and policy evaluation.
For packages not listed in the registry, the server fetches the license from PyPI, normalizes it to an SPDX-ish token, and checks it against global_policies. Results are cached in Redis keyed by package name.
fetch_license_from_pypi async ¶
fetch_license_from_pypi(package: str, version: Optional[str], pypi_base: str, client: AsyncClient) -> tuple[Optional[str], Optional[str]]
Return (raw_license_string, latest_version) from PyPI, or (None, None) on error.
Source code in src/lex_align_server/licenses.py
resolve_license async ¶
resolve_license(package: str, version: Optional[str], cache: JsonCache, cache_ttl: int, pypi_base: str, http_client: AsyncClient) -> tuple[LicenseInfo, Optional[str]]
Resolve (cache-or-fetch) a package's license. Returns (info, latest_version).
Source code in src/lex_align_server/licenses.py
CVE checks (OSV)¶
cve ¶
CVE lookup via OSV (osv.dev).
OSV is the Google-sponsored aggregator that ingests GHSA, NVD, PyPA Advisory DB, and others. The free POST /v1/query endpoint takes a package coordinate and returns the list of vulnerabilities with severity scores. We parse out the highest CVSS base score and let GlobalPolicies.cve_blocks decide.
query_osv async ¶
query_osv(package: str, version: Optional[str], osv_url: str, http_client: AsyncClient) -> Optional[list[dict]]
Return raw vulns list from OSV, or None on error/timeout.
Source code in src/lex_align_server/cve.py
resolve_cves async ¶
resolve_cves(package: str, version: Optional[str], cache: JsonCache, cache_ttl: int, osv_url: str, http_client: AsyncClient) -> CveInfo
Cache-or-fetch CVE data for a package@version.
A None version is cached separately from versioned queries — OSV's behaviour with no version is documented as "all versions" so we treat it as a distinct cache entry.
Source code in src/lex_align_server/cve.py
Audit log¶
audit ¶
SQLite audit log + approval-request store.
Two tables:
audit_log— one row per/evaluatecall. Backs the legal report (license-driven denials) and security report (CVE-driven denials).approval_requests— one row per/approval-requestsPOST. Phase 3 will attach a PR-creation workflow; for now we just persist them.
We use plain aiosqlite rather than an ORM. The schema is small and the report queries are easier to read as straight SQL.
AuditStore ¶
Source code in src/lex_align_server/audit.py
record_cve_alert async ¶
record_cve_alert(*, package: str, cve_ids: list[str], max_cvss: Optional[float], registry_status: Optional[str] = None, project: str = 'lex-align-server', requester: str = 'cve-scanner') -> str
Persist a single CVE_ALERT row from the background scanner.
Reuses the audit_log table so the dashboard, exports, and the client status command see one consistent event stream — the denial_category=DENIAL_CVE_ALERT discriminator keeps these out of the existing CVE-denial rollups.
Source code in src/lex_align_server/audit.py
mark_approved_by_package async ¶
Move every PENDING_REVIEW request for normalized_name to APPROVED.
Called when an operator classifies a pending package via the dashboard and adds it to the in-memory registry. Returns the number of rows flipped, which the dashboard can display in its toast.
Source code in src/lex_align_server/audit.py
legal_report async ¶
License-compliance posture for the audit log.
Returns the existing total_denials / recent fields plus three rollups the dashboard renders as charts:
license_breakdown— every audit row in scope grouped bylicenseand split by verdict, so operators can see how often each license shows up and how the policy classified it.unknown_license— same shape, restricted to rows whose license normalised toUNKNOWN. Surfaces how theunknown_license_policyis performing in production.top_projects— projects ranked by license-driven denials, so operators can tell which repos are pulling the most non-compliant packages.
Source code in src/lex_align_server/audit.py
security_report async ¶
Vulnerability posture for the audit log.
Adds three rollups on top of the base denial report:
severity_distribution— CVE-denied rows bucketed by CVSS severity (critical / high / medium / low / unknown).top_packages— packages with the most CVE-driven denials, carrying their highest-seen CVSS and the CVE ids responsible.top_cves— the CVE identifiers showing up most often.hot_registry_packages— only included whenregistryis provided. Lists packages currently in the registry aspreferred/approved/version-constrainedwhose recent audit rows include a CVE denial or provisional verdict. This catches the "we said yes, then OSV published a critical" scenario the pre-commit hook is meant to backstop.
Source code in src/lex_align_server/audit.py
recent_cve_alerts async ¶
Most recent CVE_ALERT rows written by the background scanner.
Surfaced through security_report so the dashboard and the client's status command see scanner output without any endpoint changes.
Source code in src/lex_align_server/audit.py
list_pending_by_package async ¶
All PENDING_REVIEW approval requests grouped by package.
The dashboard uses this to surface "things developers asked for" as a triage queue. Multiple requests for the same package (across projects/requesters) collapse into a single row carrying the count, the most recent rationale, and the most recent timestamp. Callers are expected to further filter against the live registry.
Source code in src/lex_align_server/audit.py
list_implicit_candidates async ¶
Packages that show up in audit_log but never produced an explicit request-approval row.
This catches the cases where an agent / hook only ever called check: the verdict's recorded but no one is going to file an approval — yet the operator still wants to triage.
Each row is annotated with a reason field explaining why it's surfacing:
provisional-no-rationale — the package got PROVISIONALLY_ALLOWED and the agent didn't follow up with request-approval. Highest signal. repeatedly-denied — the package was DENIED repeatedly, so an explicit registry rule (banned, version-pinned, replaced) would save downstream agents from rediscovering the wall. pre-screened — only check calls, all ALLOWED. Lowest signal, but useful for "things lots of agents poke at."
Callers (the dashboard) further filter against the live registry so packages that just got merged stop appearing immediately.
Source code in src/lex_align_server/audit.py
795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 | |
agents_report async ¶
Aggregate evaluations by (agent_model, agent_version).
Powers the "agents" dashboard, so operators can see exactly which Claude (or other agent) version is making which kinds of requests. Rows where the agent is unknown collapse into one bucket.
Source code in src/lex_align_server/audit.py
Caching¶
cache ¶
Redis-backed JSON cache.
Used by the license and CVE adapters to avoid hammering PyPI/OSV. We intentionally swallow connection errors and degrade to "no cache" so the server keeps serving when Redis is unreachable — the audit log still records every decision.
Authentication¶
auth ¶
Auth dependencies.
Identity resolution lives in the pluggable authenticator on app.state.lex.authenticator (see authn/). The functions here are thin FastAPI Depends shims so endpoints can stay declarative:
requester: str = Depends(get_requester)
identity: Identity = Depends(get_identity)
agent: AgentInfo = Depends(get_agent_info)
Single-user mode ships an AnonymousAuthenticator so the same code path works without any auth setup; org mode swaps in whatever the operator configured via AUTH_BACKEND.
The agent-identity headers (X-LexAlign-Agent-Model / -Version) are tag metadata, not authentication — they carry the model that originated the request (e.g. opus 4.7) and feed the agent-activity dashboards. The Authenticator answers "who is calling" (the human/principal); get_agent_info answers "which agent did the calling for them."
AgentInfo dataclass ¶
Agent identity reported by the client.
Both fields are optional; when the client doesn't send the headers, both are None and reports group those rows under an "(unknown agent)" bucket.
get_identity async ¶
Resolve the requester via the per-app authenticator.
Endpoints depend on this when they need email/groups (e.g. for future per-team authorization). For the common "I just need a string for the audit log" case, depend on :func:get_requester instead.