Intent API Admin
Console-issued admin tokens and zero-boilerplate admin surfaces for any Intent API product.
intent-api-admin plugs into intent-api to add a dedicated admin-machine surface, Ed25519/JWKS-verified admin tokens, an automatic CRUD admin generator from your SQLAlchemy models, role-aware role gates, audit correlation, and a schema document the console can render directly.
Install
pip install intent-api-admin
Requires intent-api>=0.5.0 and Python 3.10+.
Three-step integration
from intent_api import IntentRouter
from intent_api_admin import (
configure_admin_auth, auto_admin, admin_machine_auth,
)
configure_admin_auth(
issuer="https://console.example.com",
audience="myapp",
base=Base,
)
router = IntentRouter()
router.register("User", UserService())
auto_admin(router, expose=["User", "Team"])
app.include_router(router.build(get_user=get_clerk_user, get_db=get_db))
app.include_router(router.build_admin_machine(get_user=admin_machine_auth(), get_db=get_db))
That is the entire integration. Console operators now get full read/write CRUD on every model in expose=[...], and your handlers are unchanged.
Impersonation (Clerk actor tokens)
Impersonation consumes Clerk's native actor tokens, minted per-product via the product's own Clerk SDK. The console never holds product Clerk credentials and never mints Clerk tokens itself.
The visual indicator is a UX hint, not a guarantee. Always confirm impersonation state via the console's session list before assuming the absence of an indicator means you're not impersonating. The security boundary is server-side.
Two-step integration
Step 1 — Mint. Add a User.create_clerk_actor_token custom action that calls clerk_sdk.actor_tokens.create(...). The full reference adapter is in docs/CLERK_ACTOR_TOKEN_ADAPTER.md.
Step 2 — Enforce. Call enforce_actor_writes at the top of any write handler, OR install actor_writes_middleware once in your runtime.
# Per-handler
from intent_api_admin import enforce_actor_writes
@custom_action(admin_only=True)
async def ban(self, *, db, user, context, id, payload, session_claims=None):
enforce_actor_writes(session_claims or {}, action="custom",
is_read_only_custom_action=False)
...
# Or middleware (one-time install)
from intent_api_admin import actor_writes_middleware
runtime = build_runtime(middlewares=[actor_writes_middleware])
Audit context — one line in get_user
bind_impersonation_context populates acting_admin_id and impersonation_session_id as structlog contextvars so every log line in the handler inherits them. One line in your auth dependency:
from intent_api_admin import bind_impersonation_context
async def get_current_user(...):
auth_state = clerk_sdk.authenticate_request(...)
bind_impersonation_context(auth_state.payload) # one-liner
context.clerk_claims = auth_state.payload
...
CSP — frame-ancestors 'none'
The console always launches impersonation in a new tab via window.open(url, "_blank", "noopener,noreferrer"). Set CSP frame-ancestors 'none' on every product response so this architecture self-enforces — even if a future change tries to embed your product, browsers will refuse.
@app.middleware("http")
async def add_csp_headers(request, call_next):
response = await call_next(request)
response.headers["Content-Security-Policy"] = "frame-ancestors 'none'"
return response
See docs/PRODUCT_ADOPTION.md for the full per-product checklist (~60 LOC) and SECURITY.md for the V1 threat model.
Roles
Three roles, ordered by rank:
| Role | Rank | Default for |
|---|---|---|
owner |
3 | delete on auto-admin services |
admin |
2 | update, default for @custom_action(admin_only=True) |
support |
1 | list, read |
A higher-ranked role satisfies any lower-ranked requirement. So an owner token can perform any action that requires support, admin, or owner.
Heuristic defaults
auto_admin() reads each SQLAlchemy model and picks sensible defaults you almost never need to override.
list_fields— primary key +name/email/title/slug/status/created_atif present, then non-text columns up to 6 total.search_fields— string columns withindex=Trueorunique=Trueandlength<=255.default_sort—created_at descif present; otherwise<primary_key> desc.- Auto-redaction patterns — any column whose name starts with
password,secret,api_key,token, or_, or ends with_hash/_digest. Redacted fields are stripped from responses and refused on update.
Override patterns
auto_admin(
router,
expose=["User", "Team", "Order"],
redact_fields={"User": ["last_login_ip"]}, # add to auto-redaction
deny_edit_fields={"User": ["email"]}, # read-only in admin
deny_delete=["Order"], # disallow delete entirely
overrides={ # surgical schema override
"User": {"default_sort": "email asc",
"list_fields": ["id", "email", "name", "plan", "is_active"]},
},
)
Custom admin actions
Use the augmented @custom_action from intent_api_admin for admin-only commands. They are dispatched on the admin-machine surface only — invisible to standard, MCP, machine, and the legacy admin route.
from intent_api_admin import custom_action
class UserService(IntentService):
@custom_action(admin_only=True, min_role="admin")
async def ban(self, *, db, user, context, id, payload):
...
@custom_action(admin_only=True, min_role="support", read_only=True)
async def view_audit(self, *, db, user, context, id, payload):
...
read_only=True is honored by enforce_actor_writes (see above) so the action remains callable under a read-only Clerk actor session.
Common errors and fixes
| Code | When | Fix |
|---|---|---|
ADMIN_AUTH_NOT_CONFIGURED |
auto_admin() called before configure_admin_auth() |
Call configure_admin_auth(...) first. |
CONFIGURATION_ORDER_ERROR |
configure_admin_auth() called again after auto_admin() ran |
Reset only in tests; in prod call exactly once at startup. |
MODEL_NOT_FOUND |
expose=["X"] but X isn't on Base.registry |
Make sure the model is imported before auto_admin(). |
WRONG_SURFACE |
Admin token on /api/intent |
Use /api/admin-machine-intent for admin tokens. |
IMPERSONATION_WRITE_DENIED |
A write ran under a read-only Clerk actor session | Mint the actor token with allow_writes=true, or mark the action read_only=True. |
INSUFFICIENT_ROLE |
Token role rank is below the action's min_role |
Issue a token with the required role. |
FIELD_REDACTED |
Update payload references a redacted field | Drop the field from payload, or remove from redact_fields. |
FIELD_READ_ONLY |
Update payload references a deny_edit_fields field |
Drop the field, or remove the deny entry. |
DELETE_DENIED |
Delete on a deny_delete model |
Remove the model from deny_delete=[...]. |
UNSUPPORTED_FILTER_OP |
List filter uses an op the auto-admin filter parser doesn't support | Use one of eq, ne, lt, lte, gt, gte, contains, in, is_null. |
See TOKEN_CONTRACT.md for the full token spec.
Reference test suite
tests/wbs_reference/ is an exhaustive contract suite that mirrors the WBS success criteria S1–S52. Copy it into your repo to validate every adoption.
Release files for intent-api-admin 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| intent_api_admin-0.2.0.tar.gz | 52.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| intent_api_admin-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 89.9 kB
Release files / intent_api_admin-0.2.0.tar.gz
| Download URL | intent_api_admin-0.2.0.tar.gz |
|---|---|
| Size | 52.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0a7127fd34d8fc77a8b75ec53c84705fe5cf3ef1e0ac23ed5e01fbb090faf257
|
|
BLAKE2b-256 checksum How to use checksums |
9631ccee81f19867479c07016400ad969dbe5a65d5f9315d6497fff466c82e35
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.13
|
Release files / intent_api_admin-0.2.0-py3-none-any.whl
| Download URL | intent_api_admin-0.2.0-py3-none-any.whl |
|---|---|
| Size | 37.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e20dd46a87681c1340a95de5c49b222c6af728070562be4325e0147aebd150b6
|
|
BLAKE2b-256 checksum How to use checksums |
a64655c0fb6178669167445728f48c476e8b93427f8f38f326794bd5378ba3bf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.13
|