ACP config-as-code reference
Audience: AI agents and developers writing CI policies. Anything you can configure in the dashboard you can configure via REST. Plain markdown, dense, copy-pasteable.
This is the field-level reference. For an end-to-end agent quickstart, see the agent quickstart.
Child API key minting — POST /api/v1/keys/child
The keystone endpoint for agent-to-agent delegation. A parent gsk_ key mints a scope-narrowed child gsk_ key bound to an agent profile. The gateway:
- Sets
originSubfrom parent (ADCS §5 origin invariant — never accepted from request body). - Computes
effectiveScopes = intersect(parent.effective, profile.scopes, request.scopes). Stripsbench.impersonate(meta-scope; never propagates). - Atomically decrements parent’s
remainingBudgetCentsby the child’s allocation (transaction). Fan-out can’t escape parent’s budget cap. - Sets
expiresAt = min(parent.expiresAt, request.ttlSeconds, 24h cap). Child can’t outlive parent. - Persists chain metadata (
parentKeyId,originSub,agentProfileId,agentRunId,chainDepth) so audit logs preserve the chain. - Rejects cycles across the entire ancestor chain (ADCS §8 — stricter than depth-limit alone).
curl -X POST "https://api.agenticcontrolplane.com/api/v1/keys/child" \
-H "Authorization: Bearer $PARENT_KEY" \
-H "Content-Type: application/json" \
-d '{
"profileId": "lead-research-bot",
"scopes": ["github.repos.read"],
"ttlSeconds": 600,
"maxBudgetCents": 50,
"reason": "summarizing inbound lead xyz"
}'
Body schema:
| Field | Type | Notes |
|---|---|---|
profileId |
string | Required. Profile must have delegatable: true. |
scopes |
string[] | Optional further-narrow beyond profile.scopes. Cannot widen beyond parent. |
maxBudgetCents |
int 0..1_000_000 | Optional ceiling. Server clamps to min(parent.remaining, profile.max, request). |
ttlSeconds |
int 60..86_400 | Optional. Default 3600 (1h), hard cap 86_400 (24h), never longer than parent. |
reason |
string ≤200 | Optional audit label. |
Response:
{
"apiKey": "gsk_...",
"keyId": "...",
"expiresAt": "ISO timestamp",
"effectiveScopes": ["..."],
"effectiveTools": ["..."],
"remainingBudgetCents": 50,
"chain": {
"originSub": "...",
"depth": 2,
"agentProfileId": "...",
"agentRunId": "...",
"parentKeyId": "..."
}
}
Errors:
400 validation_failed(bad shape; checkdetails)403 profile_not_delegatable(setdelegatable: trueon the profile)404 profile_not_found409 delegation_cycle(profile already in chain)409 delegation_depth_exceeded(chain at HARD_DEPTH_CAP=5)409 parent_budget_insufficient(race or fan-out cap hit)410 parent_key_already_expired429 child_mint_rate_limit(30 mints/h per parent)
Use the SDK helpers (Python: acp_governance.spawn_subagent + child_context) instead of raw curl in production code — they handle context binding so every governed tool call inside a with block reports under the child key automatically.
All endpoints below require:
- A
gsk_API key carrying either an admin role on the tenant OR an explicit scope (called out per endpoint). - The tenant slug in the URL:
/{slug}/admin/...for tenant-scoped paths,/api/v1/...for slug-free endpoints (the slug is parsed from the key).
Errors are returned as { error: <code>, ...details } with appropriate HTTP status. Validation failures use { error: "validation_failed", details: { ...zodError } }.
Agent profiles — PATCH /api/v1/agents/:id
Update a saved agent profile. Strict field allowlist — unknown keys return 400 validation_failed.
curl -X PATCH "https://api.agenticcontrolplane.com/api/v1/agents/$ID" \
-H "authorization: Bearer $KEY" \
-H "content-type: application/json" \
-d '{
"name": "research-bot-v2",
"model": "claude-sonnet-4-6",
"maxBudgetCents": 500,
"maxToolCalls": 50
}'
Allowed fields and their bounds:
| Field | Type | Bounds |
|---|---|---|
name |
string | 1–120 chars |
description |
string | ≤2000 chars |
icon |
string | ≤120 chars |
model |
enum | see allowlist below |
systemPrompt |
string | ≤20000 chars |
enabledTools |
string[] | ≤200 entries; each ^[a-zA-Z][a-zA-Z0-9._-]{0,79}$ |
scopes |
string[] | ≤100 entries; each ≤200 chars |
maxToolCalls |
int | 0–10000 |
maxBudgetCents |
int | 0–1000000 |
maxDurationMs |
int | 0–86400000 (24h) |
maxToolRounds |
int | 0–1000 |
delegatable, canDelegate |
bool | |
maxDelegationDepth |
int | 0–10 |
Allowed model values (kept tight — extend the allowlist in apps/tenant-gateway/src/admin/_shared/schemas.ts:ALLOWED_AGENT_MODELS when shipping support for a new one):
claude-opus-4-7
claude-sonnet-4-6
claude-haiku-4-5-20251001
claude-3-5-sonnet-20241022
claude-3-5-haiku-20241022
gpt-5
gpt-5-mini
gpt-4o
gpt-4o-mini
gemini-2.5-pro
gemini-2.5-flash
OAuth connectors — /{slug}/admin/connectors/:provider
Manage OAuth connector credentials. Secrets round-trip through Google Secret Manager and are NEVER returned in GET. The Firestore doc carries only secretConfigured: boolean.
Authorization:
GET— owner, admin, or member; ORadmin.connectors.readscopePOST/PATCH— owner or admin; ORadmin.connectors.writeDELETE— owner only; ORadmin.connectors.write
Valid providers: see OAUTH_CONNECTOR_IDS in apps/tenant-gateway/src/firestore.ts. Providers not in that list return 400 unknown_provider.
# Upsert
curl -X POST "https://api.agenticcontrolplane.com/$SLUG/admin/connectors/github" \
-H "authorization: Bearer $KEY" \
-H "content-type: application/json" \
-d '{
"client_id": "Iv1.abc123",
"client_secret": "ghp_xxx",
"redirect_uri": "https://your-app.example.com/oauth/callback",
"scopes": ["read:user", "repo"]
}'
# Patch (omit client_secret = leave unchanged; explicit null = clear)
curl -X PATCH "https://api.agenticcontrolplane.com/$SLUG/admin/connectors/github" \
-H "authorization: Bearer $KEY" \
-H "content-type: application/json" \
-d '{"scopes": ["read:user"]}'
# Delete (owner only)
curl -X DELETE "https://api.agenticcontrolplane.com/$SLUG/admin/connectors/github" \
-H "authorization: Bearer $KEY"
redirect_uri must be https://. client_secret is optional on PATCH (omit = leave; null = clear).
Custom HTTP connectors — /{slug}/admin/connectors/custom[/:key]
Define a connector that wraps an arbitrary HTTPS API. SSRF-protected: baseUrl must be https:// and not resolve to a private IP (RFC1918, loopback, link-local).
curl -X POST "https://api.agenticcontrolplane.com/$SLUG/admin/connectors/custom" \
-H "authorization: Bearer $KEY" \
-H "content-type: application/json" \
-d '{
"key": "myapi",
"displayName": "My API",
"baseUrl": "https://api.example.com",
"allowedHosts": ["api.example.com", "*.cdn.example.com"],
"authType": "api_key",
"secrets": { "apikey": "sk-rotation-1" }
}'
Field reference:
| Field | Type | Bounds / notes |
|---|---|---|
key |
string | ^[a-z0-9_-]{2,32}$. Used in tool names: custom.{key}.{tool} |
displayName |
string | 1–120 chars |
baseUrl |
string | https only, non-private |
allowedHosts |
string[] | ≤50 entries; each a hostname (wildcard allowed only at left-most label) |
authType |
enum | none, bearer, header, basic, oauth2, api_key, forward_bearer |
secrets |
record<string,string|null> | Each value routed to Secret Manager under {key}-{slot}. Existing runtime expects slot names secret (OAuth) and apikey (API key). |
config |
record<string,unknown> | Non-secret extras. Open-shape for forward compat. |
PATCH semantics for secrets: omit = leave unchanged; explicit null for a slot = delete that secret. Setting any slot flips auth.secretConfigured: true.
Custom PII patterns — /{slug}/admin/pii-patterns[/:type]
CRUD over the customPiiPatterns array attached to tenants/{id}/policies/governance. Patterns are applied to every governed message; a malicious regex would hang the gateway, so every submitted pattern goes through a ReDoS guard before being persisted.
Authorization:
GET— owner, admin, or member; ORadmin.policies.readscopePOST/PATCH/DELETE— owner or admin; ORadmin.policies.write
# Add
curl -X POST "https://api.agenticcontrolplane.com/$SLUG/admin/pii-patterns" \
-H "authorization: Bearer $KEY" \
-d '{"type":"client_code","pattern":"\\b[A-Z]{3}-\\d{4}\\b","flags":"g"}'
# List
curl "https://api.agenticcontrolplane.com/$SLUG/admin/pii-patterns" \
-H "authorization: Bearer $KEY"
# Update (partial)
curl -X PATCH "https://api.agenticcontrolplane.com/$SLUG/admin/pii-patterns/client_code" \
-H "authorization: Bearer $KEY" \
-d '{"description": "ticket-reference codes from internal tools"}'
# Delete
curl -X DELETE "https://api.agenticcontrolplane.com/$SLUG/admin/pii-patterns/client_code" \
-H "authorization: Bearer $KEY"
Errors:
400 pattern_unsafe { reason: "static_prefilter" | "compile_error" | "timeout" }— pattern rejected by the ReDoS guard. Fix: avoid nested quantifiers ((a+)+,(a*)+, etc.) and alternation under quantifiers.400 validation_failed— bad shape (flags must match^[gimsuy]*$, etc.)409 pattern_type_exists— use PATCH instead
Effective policy — GET /{slug}/admin/policies/effective
Compute the merged policy that governance would apply for a given uid + agent context. Useful for verification (“if Alice calls Read, what happens?”) and for CI gates (“assert this user is blocked from this tool”).
curl -G "https://api.agenticcontrolplane.com/$SLUG/admin/policies/effective" \
-H "authorization: Bearer $KEY" \
--data-urlencode "uid=alice@acme.com" \
--data-urlencode "agentTypeKeys=claude.code.opus,claude.code" \
--data-urlencode "toolName=Read"
Query params (all optional):
| Field | Type | Notes |
|---|---|---|
uid |
string | The principal whose effective policy you want. Members can only query their own uid; admins/owners can query any uid in the tenant. |
agentTypeKeys |
comma-separated | Up to 5 entries, each ≤64 chars. Most-specific first; merged in order. |
toolName |
string | Optional projection. When set, response includes a tool: { name, spec } field with that tool’s resolved per-tier rules. |
Response shape:
{
"policy": {
"mode": "enforce" | "audit",
"defaults": {
"interactive": { "permission": "allow|flag|deny", "transform": "off|log|redact|block", "rateLimit": 60 },
"subagent": { ... },
"background": { ... },
"api": { ... }
},
"tools": {
"Read": { "interactive": { "permission": "allow" } }
}
},
"tool": { "name": "Read", "spec": { ... } }
}
The four-layer merge (most-specific wins): workspace baseline → role default → agent-type override → per-user override.
Sandbox tenants — /v1/sandbox-tenants[/:id/claim]
Covered in the agent quickstart. Quick reference:
POST /v1/sandbox-tenants— unauthenticated; returns ephemeral tenant + key + claim token.POST /v1/sandbox-tenants/:id/claim— Firebase ID token + claim token; transfers to a real owner.
Sandbox-mode restrictions:
- Cannot configure real OAuth or custom connectors (
/admin/connectors/...returns403 sandbox_tenants_cannot_configure_connectors). - API key carries restricted scopes (
sandbox.govern,sandbox.read). - Auto-deleted 24h after creation unless claimed.
Audit logs — GET /{slug}/admin/audit
Existing endpoint, unchanged by this work. Pull recent governance decisions for forensics and CI assertions.
curl "https://api.agenticcontrolplane.com/$SLUG/admin/audit?limit=100" \
-H "authorization: Bearer $KEY"
Filters: since (ISO timestamp; default 15min ago), limit (max 1000), tool (optional substring match).
Schema source of truth
All zod schemas referenced above live in:
apps/tenant-gateway/src/admin/_shared/schemas.ts
If your tooling generates clients from schemas, point it at that file or wait for the OpenAPI export (tracked separately).
Feature flags
These endpoints are gated by env vars on the gateway. In a default deploy, all are off. Check whether your target environment has them enabled by running any of the endpoints — 404 from the route means the flag is off, while a 4xx from the handler means it’s on but you’ve sent a bad request.
| Env var | Gates |
|---|---|
AGENT_PATCH_API_ENABLED |
PATCH /api/v1/agents/:id (the new field-allowlisted variant) |
ADMIN_CONNECTORS_API_ENABLED |
/admin/connectors/... |
PII_PATTERNS_API_ENABLED |
/admin/pii-patterns/... |
EFFECTIVE_POLICY_API_ENABLED |
/admin/policies/effective |
SANDBOX_ENABLED |
/v1/sandbox-tenants/... |
Conventions
- All
POSTmutations are audited; the audit row is fire-and-forget (never blocks the response). Look up rows viaGET /admin/audit?tool=admin.connector.create(etc.). client_secret,secrets[*],claimToken, andapiKeyare NEVER logged or echoed back in any response other than the original creation response.- Rate limits: 60 mutations/min per tenant for admin endpoints; 10 reads/sec for
/admin/policies/effective. - Idempotency for connector PATCH: omitting a secret = no change; explicit
null= clear. Same convention forsecretsslots on custom connectors.