Skip to content
Agentic Control Plane

Agents building agents — ACP

Audience: AI agents. This doc is plain markdown, dense, copy-pasteable. No images, no marketing prose. If you are a human reading this, that’s fine — but it’s optimized for an agent ingesting it as context and acting on it programmatically.

In ACP, every agent your agent creates is automatically governed under your identity, with scopes a strict subset of yours. There’s nothing to configure per-spawn. The framework does it for you, deterministically, via the delegation chain primitive.

This doc covers, in priority order:

  1. The primary path: a parent agent mints a scope-narrowed child API key via POST /api/v1/keys/child before spawning a subagent. The chain is preserved in audit. This is what to use when your agent is running inside an existing tenant (you have a gsk_ key that’s already attributed to a human).
  2. The tire-kicker path: POST /v1/sandbox-tenants for AI agents that have no existing tenant yet — typically third-party developers evaluating ACP. Six-curl onboarding, no human in the loop.
  3. The full config-as-code surface — everything the dashboard configures is also a REST endpoint.

The delegation primitive (POST /api/v1/keys/child)

The keystone. When parent agent A (already attributed to human H) creates child agent B, the parent calls one endpoint to mint B’s API key. The gateway:

  • Sets originSub = H (preserved through the whole chain — this is the audit trail back to the human)
  • Computes effectiveScopes = intersect(A.scopes, profile.scopes, request.scopes) — child scopes are NEVER broader than parent
  • Strips bench.impersonate (meta-scope; never propagates)
  • Decrements parent’s remaining budget by the child’s allocation (atomic transaction — fan-out can’t escape the parent’s budget cap)
  • Sets expiresAtmin(parent.expiresAt, request, 24h) (child can never outlive parent)
  • Persists chain metadata so audit logs trace through the whole chain back to H
  • Rejects cycles across the entire ancestor chain (not just the immediate parent — ADCS §8)
curl -X POST https://api.agenticcontrolplane.com/api/v1/keys/child \
  -H "Authorization: Bearer $PARENT_GSK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "lead-research-bot",
    "scopes": ["github.repos.read"],
    "ttlSeconds": 600,
    "maxBudgetCents": 50,
    "reason": "summarizing inbound lead xyz"
  }'

Expected 201:

{
  "apiKey": "gsk_acme_<32hex>",
  "keyId": "...",
  "expiresAt": "2026-04-30T17:00:00.000Z",
  "effectiveScopes": ["github.repos.read"],
  "effectiveTools": [...],
  "remainingBudgetCents": 50,
  "chain": {
    "originSub": "human-1",
    "depth": 2,
    "agentProfileId": "lead-research-bot",
    "agentRunId": "...",
    "parentKeyId": "..."
  }
}

The apiKey is shown ONCE. The subagent uses it as its bearer token. Every governed tool call the subagent makes carries the chain forward in audit logs — no extra code needed on your side.

Errors:

  • 400 validation_failed — body shape wrong; check details. Note: originSub cannot be set from request body (ADCS §5 origin invariant); it’s always derived from the parent.
  • 403 profile_not_delegatable — the agent profile must have delegatable: true.
  • 404 profile_not_found — typo or wrong tenant.
  • 409 delegation_cycle — the requested profileId already appears somewhere in the parent’s chain. Pick a different profile or restructure your delegation.
  • 409 delegation_depth_exceeded — the chain is already at the cap (5).
  • 409 parent_budget_insufficient — the parent’s remainingBudgetCents is below what this child would allocate.
  • 410 parent_key_already_expired — re-mint your parent key first.
  • 429 child_mint_rate_limit — 30 mints/hour per parent key.

SDK helpers (do this, not raw curl)

In production, use the SDK wrappers — they handle context binding so every @governed tool call inside a with child_context(...) block is reported under the child token automatically:

from acp_governance import spawn_subagent, child_context

child = spawn_subagent(
    profile_id="lead-research-bot",
    scopes=["github.repos.read"],
    ttl_seconds=600,
)

with child_context(child, agent_name="lead-research-bot"):
    run_my_subagent()  # every governed tool call inside is on the child key

For Claude Code: install the acp-spawn skill (in claude-code-acp-plugin). When the user (or your agent) decides to delegate work to a subagent, the skill mints the child key and runs the subagent with that token — same delegation chain, same audit, no manual curl.

Sandbox path (use only if you have no tenant yet)

The unauthenticated POST /v1/sandbox-tenants flow is for agents evaluating ACP — typically third-party developers running a benchmark or starter. After 24h the sandbox is auto-deleted unless a human claims it. Use the delegation chain primitive above for any agent-creates-agent flow within an existing tenant.

# Get an ephemeral tenant + scoped key with no signup.
curl -X POST https://api.agenticcontrolplane.com/v1/sandbox-tenants \
  -H 'content-type: application/json' \
  -d '{"intent":"crewai-eval"}'

Returns { tenantId, workspaceSlug, apiKey, claimToken, expiresAt }. The apiKey is a sandbox-scoped gsk_ key (no bench.impersonate, no real LLM keys, no real OAuth or custom HTTP connectors — the sandbox is a governance demo, not an LLM proxy).

To run a governed tool call:

SLUG=sb-xyz12345
KEY=gsk_sb-xyz12345_…

curl -X POST "https://api.agenticcontrolplane.com/$SLUG/govern/tool-use" \
  -H "authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{
    "tool_name": "Read",
    "tool_input": {"file_path": "/etc/hosts"},
    "session_id": "sess-1",
    "agent_name": "my-eval-agent"
  }'

Expected: {"decision":"allow","reason":"...","tier":"interactive"}.

To convert the sandbox to a real workspace, a human signs in to https://cloud.agenticcontrolplane.com and POSTs /v1/sandbox-tenants/:id/claim with the claimToken. The original sandbox key is revoked at claim time; the human gets a fresh owner key. From there, every agent the human’s agents create flows through POST /api/v1/keys/child (the delegation primitive above).

What’s blocked in sandbox:

  • Real OAuth or custom HTTP connectors (/admin/connectors/... returns 403 sandbox_tenants_cannot_configure_connectors)
  • LLM provider keys (sandbox is for governance, not LLM proxying)
  • admin.audit.read and admin.*.write scopes (sandbox keys carry only sandbox.govern and sandbox.read)
  • 24h TTL

Config-as-code surface

Once you have any gsk_ key (root or chain), every dashboard configuration is a REST endpoint. See the config-as-code reference for the full reference. Short version:

Method Path Purpose
PATCH /api/v1/agents/:id Update agent profile (field-allowlisted, model-allowlisted)
POST /api/v1/keys/child Mint child key (delegation chain)
GET\|POST\|PATCH\|DELETE /{slug}/admin/connectors[/...] OAuth + custom HTTP connector CRUD
GET\|POST\|PATCH\|DELETE /{slug}/admin/pii-patterns[/...] Custom PII regex patterns (with ReDoS guard)
GET /{slug}/admin/policies/effective Resolve effective policy for a uid + agent context

Common patterns for agents

  • Spawn-and-govern: parent calls /api/v1/keys/child, runs the subagent with the child token, audit logs trace the full chain back to the human. Use the SDK helpers — never construct child keys manually in production code.
  • CI policy verification: keep agent profiles, PII patterns, and policy configs in version control. Apply via the REST endpoints. Use GET /admin/policies/effective as a unit-test assertion: “given uid X and tool Y, the effective decision should be block.”
  • Provision-and-evaluate: create a sandbox, run your harness against /govern/tool-use, look at the audit logs. Useful for first-time evaluators with no existing tenant.

Source and feedback

OpenAPI

Coming soon. For now, schemas live as zod definitions in apps/tenant-gateway/src/admin/_shared/schemas.ts (search for ChildKeyMintSchema, SandboxCreateSchema, etc.).