# ACP agents-building-agents quickstart — delegation chain primitive

How an AI agent mints a scope-narrowed child API key for a subagent it spawns. POST /api/v1/keys/child preserves originSub, intersects scopes, decrements parent budget atomically, and produces audit logs that trace through the chain back to the originating human. Plus the secondary sandbox path for agents with no existing tenant.

# 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. Humans setting up ACP for the first time should start at [Getting started with ACP](/getting-started), the canonical start page.

In ACP, **every agent your agent creates is automatically controlled 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 `expiresAt` ≤ `min(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)

```bash
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`:

```json
{
  "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 controlled 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:

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

```bash
# 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 control-plane demo, not an LLM proxy).

To run a controlled tool call:

```bash
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, hand your human the **`nextSteps.claimPageUrl`** from the create response — one link, mid-conversation. They sign in, click Claim, and the sandbox becomes their permanent workspace: expiry removed, restrictions lifted, your sandbox key revoked, and a fresh owner key minted for them to hold (or hand back to you). The claim token rides in the URL fragment, so it never appears in any server log. (The raw API also works: a signed-in human POSTs `/v1/sandbox-tenants/:id/claim` with the `claimToken`.) 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 control, 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](/agents/configure-as-code/) 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-control:** 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

- Source: [github.com/agentic-control-plane](https://github.com/agentic-control-plane)
- Open benchmark: [github.com/agentic-control-plane/agentgovbench](https://github.com/agentic-control-plane/agentgovbench) — 48 control scenarios, mapped to NIST AI RMF
- ADCS spec: [github.com/agentic-control-plane/delegation-chain-spec](https://github.com/agentic-control-plane/delegation-chain-spec)
- SDK: [github.com/agentic-control-plane/acp-governance-sdks](https://github.com/agentic-control-plane/acp-governance-sdks) — Python `acp-governance` package with `spawn_subagent` + `child_context`
- Issues: file at the gateway repo above

## OpenAPI

The gateway serves its OpenAPI description at `https://api.agenticcontrolplane.com/openapi.json`; it covers `POST /api/v1/keys/child` and `POST /v1/sandbox-tenants`. The same request and response shapes live in source as zod definitions in `apps/tenant-gateway/src/admin/_shared/schemas.ts` (search for `ChildKeyMintSchema`, `SandboxCreateSchema`, etc.).
