# ACP config-as-code reference — child keys, agent profiles, connectors, PII patterns

Field-level REST reference for the ACP config-as-code surface. Mint scope-narrowed child API keys for subagents, define agent profiles, OAuth and custom HTTP connectors, custom PII patterns, and resolve effective policies — all programmatically. Designed for AI agents and CI policy pipelines.

# 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](/agents/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).

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

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

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

```bash
# 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).

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

```bash
# 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 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").

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

```json
{
  "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](/agents/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 policy decisions for forensics and CI assertions.

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