Skip to content
Agentic Control Plane

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 originSub from parent (ADCS §5 origin invariant — never accepted from request body).
  • Computes effectiveScopes = intersect(parent.effective, profile.scopes, request.scopes). Strips bench.impersonate (meta-scope; never propagates).
  • Atomically decrements parent’s remainingBudgetCents by 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; check details)
  • 403 profile_not_delegatable (set delegatable: true on the profile)
  • 404 profile_not_found
  • 409 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_expired
  • 429 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; OR admin.connectors.read scope
  • POST / PATCH — owner or admin; OR admin.connectors.write
  • DELETE — owner only; OR admin.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; OR admin.policies.read scope
  • POST / PATCH / DELETE — owner or admin; OR admin.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/... returns 403 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 POST mutations are audited; the audit row is fire-and-forget (never blocks the response). Look up rows via GET /admin/audit?tool=admin.connector.create (etc.).
  • client_secret, secrets[*], claimToken, and apiKey are 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 for secrets slots on custom connectors.