Metadata-Version: 2.4
Name: intent-api-admin
Version: 0.1.1
Summary: Admin-machine surface, impersonation, and auto-admin for Intent API products.
Author: Chris Bora
License-Expression: LicenseRef-IACL
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: intent-api>=0.5.0
Requires-Dist: pydantic>=2.0.0
Requires-Dist: sqlalchemy>=2.0.0
Requires-Dist: pyjwt[crypto]>=2.8.0
Requires-Dist: httpx>=0.24.0
Requires-Dist: cryptography>=41.0.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
Requires-Dist: fastapi>=0.100.0; extra == "dev"
Requires-Dist: httpx>=0.24.0; extra == "dev"
Dynamic: license-file

# Intent API Admin

**Console-issued admin tokens, impersonation, and zero-boilerplate admin surfaces for any Intent API product.**

`intent-api-admin` plugs into [`intent-api`](https://pypi.org/project/intent-api/) to add a dedicated admin-machine surface, Ed25519/JWKS-verified admin and impersonation 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

```bash
pip install intent-api-admin
```

Requires `intent-api>=0.5.0` and Python 3.10+.

## Three-step integration

```python
from intent_api import IntentRouter
from intent_api_admin import (
    configure_admin_auth, auto_admin,
    admin_machine_auth, impersonation_aware_auth,
)

configure_admin_auth(
    issuer="https://console.example.com",
    audience="myapp",
    impersonation_user_query=lambda db, uid: db.query(User).get(uid),
    base=Base,
)

router = IntentRouter()
router.register("User", UserService())
auto_admin(router, expose=["User", "Team"])

app.include_router(router.build(get_user=impersonation_aware_auth(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=[...]` plus impersonation on top of your existing user auth, and your handlers are unchanged.

## 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_at` if present, then non-text columns up to 6 total.
- `search_fields` — string columns with `index=True` or `unique=True` and `length<=255`.
- `default_sort` — `created_at desc` if 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

```python
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.

```python
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` makes the command callable under read-only impersonation sessions. Without it, the precondition layer raises `IMPERSONATION_WRITE_DENIED` (HTTP 403).

## 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` or impersonation token on `/api/admin-machine-intent` | Use the correct surface for the token mode. |
| `IMPERSONATION_WRITE_DENIED` | A write action ran under a read-only impersonation token            | Issue a 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](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.
