Summary
AdCP framework adopters (salesagent, presumably others) ship a buyer-facing surface — AdCP via MCP/A2A — that the framework already nails. They also need an operator-facing management surface for the same per-tenant data: tenants, principals, tokens, products, adapter configs.
Today every adopter writes their own. salesagent has a Flask admin UI with no formal API contract; the next adopter will write something different. The framework could ship a Protocol-based management API that adopters wire to their persistence and get a stable surface for free.
Two surfaces, one philosophy
Super-admin API — global, manages tenants:
```
POST /manage/tenants create
GET /manage/tenants list
GET /manage/tenants/{tenant_id} read
PATCH /manage/tenants/{tenant_id} update (subdomain, ad_server, status)
DELETE /manage/tenants/{tenant_id} deactivate (soft)
POST /manage/tenants/{tenant_id}/domains add a domain alias (multi-domain support)
```
Per-tenant management API — scoped to a tenant:
```
GET /manage/tenants/{tenant_id}/principals
POST /manage/tenants/{tenant_id}/principals
PATCH /manage/tenants/{tenant_id}/principals/{principal_id}
DELETE /manage/tenants/{tenant_id}/principals/{principal_id}
POST /manage/tenants/{tenant_id}/principals/{principal_id}/tokens issue
DELETE /manage/tenants/{tenant_id}/principals/{principal_id}/tokens/{token_id} revoke
GET /manage/tenants/{tenant_id}/products
POST /manage/tenants/{tenant_id}/products
PATCH /manage/tenants/{tenant_id}/products/{product_id}
DELETE /manage/tenants/{tenant_id}/products/{product_id}
GET /manage/tenants/{tenant_id}/adapter
PUT /manage/tenants/{tenant_id}/adapter
```
Three reasons to ship this in the SDK rather than every adopter writing their own:
- Protocol contract. Today adopters' admin UIs talk directly to ORM. The next time the schema changes, every UI breaks. A management API contract is a stable seam between persistence and operator-facing tooling.
- Composability. Hosted multi-tenant operators (Scope3 et al.) want to wire their universe directly into the management API of every adopter they host. ScalarFn adoption only happens if there's a stable ScalarFn-side API to call — same logic here.
- "Headless from day one." The dominant adopter pattern today is "build a CRUD-shaped admin UI to talk to the DB." If the framework ships the API, the UI becomes a thin client to it (and the same UI works across deployments). Adopters who don't need a UI ship operations as direct API calls.
Proposed shape
adcp.management package with:
```python
class TenantStore(Protocol):
"""Adopter-supplied persistence — same shape as MediaBuyStore et al."""
async def create(self, payload: TenantCreate) -> Tenant: ...
async def list(self, *, status: TenantStatus | None = None) -> list[Tenant]: ...
async def get(self, tenant_id: str) -> Tenant | None: ...
async def update(self, tenant_id: str, patch: TenantPatch) -> Tenant: ...
async def deactivate(self, tenant_id: str) -> None: ...
# similar for principals, tokens, products, adapter_config
def make_management_api(
store: TenantStore,
*,
auth: ManagementAuthMiddleware, # super-admin gate
audit_sink: AuditSink | None = None,
) -> ASGIApp: ...
```
The ASGIApp can mount alongside serve()'s buyer-facing app:
```python
buyer_app = build_asgi_app(platform)
management_app = make_management_api(my_store, auth=...)
starlette_app = Starlette(routes=[
Mount("/mcp", app=buyer_app),
Mount("/manage", app=management_app),
])
```
What this unblocks for salesagent specifically
The salesagent admin UI is currently a Flask app speaking directly to ORM. As we kill nginx (in flight) and move to a single-process model, we want to:
- Stop having an HTML admin UI as the primary management surface — make the management API primary
- Wire Scope3-side super-admin tooling directly to
/manage/tenants/*
- Eventually rebuild the operator-facing UI as a thin frontend over the management API (or skip it entirely for ops who'd rather use API directly)
This isn't gated on the framework supporting it (we can build management API in salesagent's core/management_api.py), but it'd be much higher leverage if the framework owns the Protocol surface so other adopters benefit.
Adjacent considerations
Auth shape. Super-admin auth is a different beast than buyer auth — typically non-AdCP, often OAuth2 service-account or signed-request tokens scoped per-environment. Worth a parallel ManagementAuthMiddleware Protocol that mirrors BearerTokenAuthMiddleware but with super-admin gate semantics.
Audit. Every management mutation should emit an audit event (who did what, when, on which tenant). The existing AuditSink Protocol fits cleanly — emit on mutation paths.
Spec coordination. Is the management API in scope for the AdCP spec, or strictly a framework adopter convenience? The buyer-facing AdCP is the spec-compliant surface; management is operationally distinct. Lean toward "framework convenience, no spec coordination needed" but worth a sanity check.
Pre-existing salesagent-specific business logic. salesagent has accumulated business logic around proposal manager workflows, governance flows, audit trails. Some of that should land in the framework (proposal manager v1.5 tracks separately at #538); some is salesagent-specific. The management API is a clean place to draw the line — anything in the management API surface is shared across adopters; anything outside is salesagent-private.
Question for the team
Worth scoping in the framework? I'd build it in salesagent first either way (we need it for the headless-API direction this team has been moving toward), but if the framework is open to it I'd happily port the design upstream as a Protocol surface + reference impl in adopter-supplied form.
Context
🤖 Filed via Claude Code
Summary
AdCP framework adopters (salesagent, presumably others) ship a buyer-facing surface — AdCP via MCP/A2A — that the framework already nails. They also need an operator-facing management surface for the same per-tenant data: tenants, principals, tokens, products, adapter configs.
Today every adopter writes their own. salesagent has a Flask admin UI with no formal API contract; the next adopter will write something different. The framework could ship a Protocol-based management API that adopters wire to their persistence and get a stable surface for free.
Two surfaces, one philosophy
Super-admin API — global, manages tenants:
```
POST /manage/tenants create
GET /manage/tenants list
GET /manage/tenants/{tenant_id} read
PATCH /manage/tenants/{tenant_id} update (subdomain, ad_server, status)
DELETE /manage/tenants/{tenant_id} deactivate (soft)
POST /manage/tenants/{tenant_id}/domains add a domain alias (multi-domain support)
```
Per-tenant management API — scoped to a tenant:
```
GET /manage/tenants/{tenant_id}/principals
POST /manage/tenants/{tenant_id}/principals
PATCH /manage/tenants/{tenant_id}/principals/{principal_id}
DELETE /manage/tenants/{tenant_id}/principals/{principal_id}
POST /manage/tenants/{tenant_id}/principals/{principal_id}/tokens issue
DELETE /manage/tenants/{tenant_id}/principals/{principal_id}/tokens/{token_id} revoke
GET /manage/tenants/{tenant_id}/products
POST /manage/tenants/{tenant_id}/products
PATCH /manage/tenants/{tenant_id}/products/{product_id}
DELETE /manage/tenants/{tenant_id}/products/{product_id}
GET /manage/tenants/{tenant_id}/adapter
PUT /manage/tenants/{tenant_id}/adapter
```
Three reasons to ship this in the SDK rather than every adopter writing their own:
Proposed shape
adcp.managementpackage with:```python
class TenantStore(Protocol):
"""Adopter-supplied persistence — same shape as MediaBuyStore et al."""
async def create(self, payload: TenantCreate) -> Tenant: ...
async def list(self, *, status: TenantStatus | None = None) -> list[Tenant]: ...
async def get(self, tenant_id: str) -> Tenant | None: ...
async def update(self, tenant_id: str, patch: TenantPatch) -> Tenant: ...
async def deactivate(self, tenant_id: str) -> None: ...
# similar for principals, tokens, products, adapter_config
def make_management_api(
store: TenantStore,
*,
auth: ManagementAuthMiddleware, # super-admin gate
audit_sink: AuditSink | None = None,
) -> ASGIApp: ...
```
The ASGIApp can mount alongside
serve()'s buyer-facing app:```python
buyer_app = build_asgi_app(platform)
management_app = make_management_api(my_store, auth=...)
starlette_app = Starlette(routes=[
Mount("/mcp", app=buyer_app),
Mount("/manage", app=management_app),
])
```
What this unblocks for salesagent specifically
The salesagent admin UI is currently a Flask app speaking directly to ORM. As we kill nginx (in flight) and move to a single-process model, we want to:
/manage/tenants/*This isn't gated on the framework supporting it (we can build management API in salesagent's
core/management_api.py), but it'd be much higher leverage if the framework owns the Protocol surface so other adopters benefit.Adjacent considerations
Auth shape. Super-admin auth is a different beast than buyer auth — typically non-AdCP, often OAuth2 service-account or signed-request tokens scoped per-environment. Worth a parallel
ManagementAuthMiddlewareProtocol that mirrorsBearerTokenAuthMiddlewarebut with super-admin gate semantics.Audit. Every management mutation should emit an audit event (who did what, when, on which tenant). The existing
AuditSinkProtocol fits cleanly — emit on mutation paths.Spec coordination. Is the management API in scope for the AdCP spec, or strictly a framework adopter convenience? The buyer-facing AdCP is the spec-compliant surface; management is operationally distinct. Lean toward "framework convenience, no spec coordination needed" but worth a sanity check.
Pre-existing salesagent-specific business logic. salesagent has accumulated business logic around proposal manager workflows, governance flows, audit trails. Some of that should land in the framework (proposal manager v1.5 tracks separately at #538); some is salesagent-specific. The management API is a clean place to draw the line — anything in the management API surface is shared across adopters; anything outside is salesagent-private.
Question for the team
Worth scoping in the framework? I'd build it in salesagent first either way (we need it for the headless-API direction this team has been moving toward), but if the framework is open to it I'd happily port the design upstream as a Protocol surface + reference impl in adopter-supplied form.
Context
🤖 Filed via Claude Code