v0.13.0 — Per-Table Principal Access Control
Highlights
Add optional per-table allow/blocklist gates keyed on an opaque caller identity (email, Webex ID, employee number — any string). Designed for two deployment shapes:
- Chainlit app — one authenticated user per session. Pass a static string:
create_tools(contract, caller_principal="alice@co.com"). - Webex room bot — one long-lived bot instance serving multiple users. Pass a zero-arg callable reading a
contextvars.ContextVarset per incoming message:create_tools(contract, caller_principal=lambda: current_sender.get()).
Fail-closed by default: any allowed_principals or blocked_principals field on a table requires identification. Query-time gating is authoritative — denied queries never reach the database (verified by a spy-adapter integration test).
YAML
allowed_tables:
- schema: analytics
tables: [orders] # open to all
- schema: hr
tables: [salaries]
allowed_principals: [alice@co.com] # allowlist
- schema: raw
tables: [audit_log]
blocked_principals: [intern@co.com] # blocklistAPI
New keyword-only caller_principal parameter on both Validator and create_tools, accepting str | Callable[[], str | None] | None. Principal type alias and resolve_principal helper are re-exported from the package root.
Error messages
Two-tier diagnostics distinguish "not declared" from "declared but restricted to other principals (caller: 'X')" — actionable for both humans and LLMs.
Example
examples/ops_agent now demonstrates the feature end-to-end. Try:
uv run python examples/ops_agent/agent.py --caller intern@co.com "Show recent deploys"The intern is in blocked_principals on sre.deploys, so the query is denied; sre.incidents remains queryable (principal access is per-table).
Known limitation
to_system_prompt() currently renders the unscoped table list — an LLM serving a restricted caller may still be told about tables it can't query, wasting retry budget on queries that will be blocked at validation time. Query-time gating remains authoritative. Principal-aware prompt rendering is a candidate follow-up.
Bundled dependency cleanup
This release also requires ai-agent-contracts>=0.3.1, which moved litellm to an optional extra. The default lock shrinks from 108 → 78 resolved packages, dropping aiohttp (10 CVEs), openai, tiktoken, tokenizers, and friends from the core graph.
See CHANGELOG.md for details.