openapi: 3.1.0
info:
  title: Agentic Control Plane API
  version: "2026-06-25"
  summary: Govern, run, and audit AI agents over HTTP.
  description: |
    The ACP control-plane API. Everything the console does, from code:
    provision an agent, set the policy that governs it, run it, and read back
    every tool call it made.

    ## Surfaces
    ACP governs two things, so the API has two surfaces, both authenticated
    with the same `gsk_` key:
      - **Agents** — `/api/v1/agents`. Slug-free; the workspace is resolved
        from the key (`gsk_<workspace>_<random>`).
      - **Governance** — `/{workspace}/admin/…`. The policy that governs every
        agent and the audit trail of what they did. The `{workspace}` in the
        path must match the slug embedded in your key.

    ## Conventions
    JSON in, JSON out. Writes return `{ "ok": true, … }`; reads return the
    resource. Any non-2xx response carries `{ "error": "<reason>" }`. New
    fields may be added under a major version; existing fields won't change
    meaning — tolerate unknown keys.
  contact:
    name: ACP docs
    url: https://agenticcontrolplane.com/docs/api/
  license:
    name: Proprietary
    url: https://agenticcontrolplane.com/terms

servers:
  - url: https://api.agenticcontrolplane.com
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Agents
    description: Create, configure, run, and delete agent profiles.
  - name: Policies
    description: Read and write the four-layer governance policy.
  - name: Logs
    description: Query the audit trail of governed tool calls.

paths:
  /api/v1/agents:
    get:
      tags: [Agents]
      operationId: listAgents
      summary: List agent profiles
      description: Every agent profile in the workspace, newest first.
      responses:
        "200":
          description: The agent profiles.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean, const: true }
                  profiles:
                    type: array
                    items: { $ref: "#/components/schemas/AgentProfile" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Agents]
      operationId: createAgent
      summary: Create an agent profile
      description: |
        `name` and `model` are required; every other field takes its default.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AgentProfileCreate" }
      responses:
        "200":
          description: Created. Returns the new profile id.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean, const: true }
                  id: { type: string, example: "a7Kf9_Qe2" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /api/v1/agents/{id}:
    parameters:
      - $ref: "#/components/parameters/AgentId"
    get:
      tags: [Agents]
      operationId: getAgent
      summary: Get an agent profile
      responses:
        "200":
          description: The agent profile.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean, const: true }
                  profile: { $ref: "#/components/schemas/AgentProfile" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    put:
      tags: [Agents]
      operationId: replaceAgent
      summary: Update an agent profile (permissive)
      description: |
        Send any subset of the writable fields; each present field is written,
        absent fields are left untouched. Unknown fields are ignored. For
        validated writes from automation, prefer `PATCH`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AgentProfileWrite" }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Agents]
      operationId: patchAgent
      summary: Update an agent profile (validated)
      description: |
        The strict path — preferred for automation. The body is validated at
        the boundary: unknown fields are rejected, `model` must be a recognized
        model, and numeric caps must fall within sane bounds. Rate limited and
        audit-logged.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AgentProfilePatch" }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      tags: [Agents]
      operationId: deleteAgent
      summary: Delete an agent profile
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /api/v1/agents/{id}/run:
    parameters:
      - $ref: "#/components/parameters/AgentId"
    post:
      tags: [Agents]
      operationId: runAgent
      summary: Run an agent
      description: |
        Kick off a governed run. Every tool call inside the run is checked
        against workspace policy, and the run obeys the profile's caps
        (`maxBudgetCents`, `maxToolCalls`, `maxDurationMs`, `maxToolRounds`).
        Set `stream: true` for Server-Sent Events.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RunRequest" }
      responses:
        "200":
          description: |
            The completed run (non-streaming). With `stream: true`, the
            response is an SSE stream of `text`, `tool_calls`, `governance`,
            `tool_result`, `error`, and `done` events instead.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RunResult" }
            text/event-stream:
              schema:
                type: string
                description: SSE stream; see the Run triggers guide.
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429":
          description: Per-user daily LLM cost cap reached.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /{workspace}/admin/workspacePolicy:
    parameters:
      - $ref: "#/components/parameters/Workspace"
    get:
      tags: [Policies]
      operationId: getWorkspacePolicy
      summary: Read the workspace policy
      description: The baseline policy for the workspace. Readable by any member.
      responses:
        "200":
          description: The policy document, or `{}` if none is set.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PolicyDocument" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    put:
      tags: [Policies]
      operationId: setWorkspacePolicy
      summary: Update the workspace policy
      description: Merge update — send only what changes. Owner/admin only.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PolicyDocument" }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Policies]
      operationId: clearWorkspacePolicy
      summary: Clear the workspace policy
      description: Removes the workspace layer; built-in defaults apply. Owner/admin only.
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /{workspace}/admin/rolePolicies:
    parameters:
      - $ref: "#/components/parameters/Workspace"
    get:
      tags: [Policies]
      operationId: listRolePolicies
      summary: List all role policies
      description: A map of role → policy document. Owner/admin only.
      responses:
        "200":
          description: Role policies keyed by role.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: { $ref: "#/components/schemas/PolicyDocument" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /{workspace}/admin/rolePolicies/{role}:
    parameters:
      - $ref: "#/components/parameters/Workspace"
      - name: role
        in: path
        required: true
        schema: { type: string, enum: [owner, admin, member] }
    get:
      tags: [Policies]
      operationId: getRolePolicy
      summary: Read one role's policy
      responses:
        "200":
          description: The role policy, or `{}` if unset.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PolicyDocument" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/Forbidden" }
    put:
      tags: [Policies]
      operationId: setRolePolicy
      summary: Upsert a role's policy
      description: Merge update. Owner/admin only.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PolicyDocument" }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Policies]
      operationId: clearRolePolicy
      summary: Clear a role's policy
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /{workspace}/admin/agentTypePolicies:
    parameters:
      - $ref: "#/components/parameters/Workspace"
    get:
      tags: [Policies]
      operationId: listAgentTypePolicies
      summary: List all agent-type policies
      description: A map of `client::tier::name` → policy document. Owner/admin only.
      responses:
        "200":
          description: Agent-type policies keyed by identity.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: { $ref: "#/components/schemas/PolicyDocument" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /{workspace}/admin/agentTypePolicies/{key}:
    parameters:
      - $ref: "#/components/parameters/Workspace"
      - name: key
        in: path
        required: true
        description: The agent identity, `client::tier::name` (URL-encoded).
        schema: { type: string, example: "Claude Code::background::" }
    get:
      tags: [Policies]
      operationId: getAgentTypePolicy
      summary: Read one agent-type policy
      responses:
        "200":
          description: The agent-type policy, or `{}` if unset.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PolicyDocument" }
        "403": { $ref: "#/components/responses/Forbidden" }
    put:
      tags: [Policies]
      operationId: setAgentTypePolicy
      summary: Upsert an agent-type policy
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PolicyDocument" }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Policies]
      operationId: clearAgentTypePolicy
      summary: Clear an agent-type policy
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /{workspace}/admin/userPolicies/{uid}:
    parameters:
      - $ref: "#/components/parameters/Workspace"
      - name: uid
        in: path
        required: true
        description: The member's user id.
        schema: { type: string }
    get:
      tags: [Policies]
      operationId: getUserPolicy
      summary: Read a user's policy override
      description: Self or admin. Returns `{}` if no override exists.
      responses:
        "200":
          description: The user policy override, or `{}`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserPolicyDocument" }
        "403": { $ref: "#/components/responses/Forbidden" }
    put:
      tags: [Policies]
      operationId: setUserPolicy
      summary: Upsert a user's policy override
      description: |
        Merge update. A member may set stricter rules on themselves (self-edit);
        editing another user requires owner/admin. Stricter layers still apply
        above this one, so a self-edit can't weaken org policy.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UserPolicyDocument" }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [Policies]
      operationId: clearUserPolicy
      summary: Clear a user's policy override
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /{workspace}/admin/audit:
    parameters:
      - $ref: "#/components/parameters/Workspace"
    get:
      tags: [Logs]
      operationId: queryAudit
      summary: Query the audit log
      description: |
        Every governed tool call, newest first. Requires a key with the
        `admin.audit.read` scope (or `*`), or an owner/admin session. Page by
        time using `since` — there is no cursor.
      parameters:
        - name: since
          in: query
          description: ISO-8601; only entries at or after this time. Default 15 minutes ago.
          schema: { type: string, format: date-time }
        - name: limit
          in: query
          description: Max entries. Clamped to 1–1000.
          schema: { type: integer, minimum: 1, maximum: 1000, default: 200 }
        - name: tool
          in: query
          description: Optional exact tool-name filter.
          schema: { type: string }
      responses:
        "200":
          description: Matching audit entries.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AuditResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403":
          description: The key lacks the `admin.audit.read` scope, or the caller isn't an admin.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: gsk_
      description: |
        A workspace API key as a bearer token: `Authorization: Bearer gsk_…`.
        The workspace slug is embedded in the key. Scopes bound what the key
        can do (`*`, `admin.audit.read`, `agents.write`, `agents.read`).

  parameters:
    Workspace:
      name: workspace
      in: path
      required: true
      description: Your workspace slug. Must match the slug embedded in the key.
      schema: { type: string, example: "acme" }
    AgentId:
      name: id
      in: path
      required: true
      description: The agent profile id.
      schema: { type: string, example: "a7Kf9_Qe2" }

  responses:
    Ok:
      description: Success.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Ok" }
    BadRequest:
      description: Malformed request.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Missing, malformed, or revoked key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: Authenticated, but scopes or role don't permit this.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: The resource does not exist in this workspace.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Rate limit exceeded. Back off and retry.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    Ok:
      type: object
      properties:
        ok: { type: boolean, const: true }
      required: [ok]
    Error:
      type: object
      properties:
        error: { type: string, description: "Human-readable reason. Branch on status, not this string." }
      required: [error]

    AgentProfile:
      type: object
      description: The unit you govern — a model, a prompt, allowed tools, and hard caps.
      properties:
        id: { type: string, description: Server-assigned identifier. }
        name: { type: string, description: Appears on every audit entry the agent produces. }
        model: { type: string, example: "claude-sonnet-4-6" }
        systemPrompt: { type: string, default: "You are a helpful autonomous agent." }
        description: { type: string, default: "" }
        icon: { type: string, default: "" }
        enabledTools:
          type: array
          items: { type: string }
          description: Tools the agent may call. Each call is still checked against policy.
        scopes:
          type: array
          items: { type: string }
        maxToolCalls: { type: integer, default: 50, description: Hard cap on tool calls per run. }
        maxBudgetCents: { type: integer, default: 1000, description: "Hard spend cap per run, in cents." }
        maxDurationMs: { type: integer, default: 1800000, description: Wall-clock cap per run (default 30 min). }
        maxToolRounds: { type: integer, default: 10, description: Cap on model→tools→model rounds. }
        delegatable: { type: boolean, default: true }
        canDelegate: { type: boolean, default: false }
        maxDelegationDepth: { type: integer, description: "If set, caps this agent's delegation depth." }
        createdAt: { type: string, format: date-time, readOnly: true }
        updatedAt: { type: string, format: date-time, readOnly: true }
    AgentProfileWrite:
      type: object
      description: Writable agent fields. Send any subset.
      properties:
        name: { type: string }
        model: { type: string }
        systemPrompt: { type: string }
        description: { type: string }
        icon: { type: string }
        enabledTools: { type: array, items: { type: string } }
        scopes: { type: array, items: { type: string } }
        maxToolCalls: { type: integer }
        maxBudgetCents: { type: integer }
        maxDurationMs: { type: integer }
        maxToolRounds: { type: integer }
        delegatable: { type: boolean }
        canDelegate: { type: boolean }
        maxDelegationDepth: { type: integer }
    AgentProfileCreate:
      allOf:
        - $ref: "#/components/schemas/AgentProfileWrite"
        - type: object
          required: [name, model]
    AgentProfilePatch:
      description: |
        Strict update — unknown fields are rejected, `model` is allowlisted,
        and numeric caps are bounds-checked.
      allOf:
        - $ref: "#/components/schemas/AgentProfileWrite"
      unevaluatedProperties: false

    RunRequest:
      type: object
      required: [input]
      properties:
        input: { type: string, description: The goal or instruction for the agent. }
        context:
          type: object
          additionalProperties: { type: string }
          description: Key-value pairs injected into the agent's system prompt.
        stream: { type: boolean, default: false, description: "`true` returns an SSE stream." }
    RunResult:
      type: object
      properties:
        id: { type: string, example: "run_abc123" }
        status: { type: string, enum: [completed] }
        output: { type: string, description: The agent's final text response. }
        conversationId: { type: string }
        usage:
          type: object
          properties:
            promptTokens: { type: integer }
            completionTokens: { type: integer }
            toolCallCount: { type: integer }
            estimatedCostCents: { type: number }
            model: { type: string }
            durationMs: { type: integer }
        stopReason: { type: string, enum: [goal_complete, max_tool_calls, max_rounds] }

    PolicyRule:
      type: object
      description: What happens to a tool call in a given tier.
      properties:
        permission: { type: string, enum: [allow, deny] }
        rateLimit: { type: integer, description: Optional. Max calls per window before throttling. }
        transform:
          type: string
          enum: [log, redact]
          description: "`log` records as-is; `redact` strips detected secrets/PII."
    TierRules:
      type: object
      description: Per-tier rules. Tiers describe how the agent runs, not who wrote it.
      properties:
        interactive: { $ref: "#/components/schemas/PolicyRule" }
        subagent: { $ref: "#/components/schemas/PolicyRule" }
        background: { $ref: "#/components/schemas/PolicyRule" }
        api: { $ref: "#/components/schemas/PolicyRule" }
      additionalProperties: { $ref: "#/components/schemas/PolicyRule" }
    PolicyDocument:
      type: object
      description: |
        A governance policy layer. Evaluated most-specific-wins across the four
        layers; the strictest decision holds.
      properties:
        mode:
          type: string
          enum: [enforce, audit, audit-only]
          description: |
            `enforce` applies decisions; an audit value logs what *would* have
            happened without blocking. Note a current inconsistency: the
            **workspace** layer uses `audit`, while the **role / agent-type /
            user** layers use `audit-only`. (Tracked for unification.)
        defaults: { $ref: "#/components/schemas/TierRules" }
        tools:
          type: object
          description: Per-tool overrides, keyed by tool name.
          additionalProperties: { $ref: "#/components/schemas/TierRules" }
    UserPolicyDocument:
      description: |
        A per-user override. Same as a policy document, plus `agentTypes` for
        per-(client, tier, name) overrides scoped to this user.
      allOf:
        - $ref: "#/components/schemas/PolicyDocument"
        - type: object
          properties:
            agentTypes:
              type: object
              additionalProperties: { $ref: "#/components/schemas/TierRules" }

    AuditEntry:
      type: object
      properties:
        id: { type: string }
        tool: { type: string }
        decision: { type: string, enum: [allow, deny] }
        decisionReason: { type: string }
        agentName: { type: string }
        agentTier: { type: string, enum: [interactive, subagent, background, api] }
        sub: { type: string, description: "The identity that made the call (`apikey:…`, a UID, or a delegated subject)." }
        userEmail: { type: string }
        sessionId: { type: string, description: Groups all calls in one run. }
        requestId: { type: string }
        ts: { type: string, format: date-time }
        hookEvent: { type: string }
        client:
          type: object
          properties:
            name: { type: string }
        originSub: { type: string, description: "Delegation: the root identity the chain descends from." }
        depth: { type: integer, description: "Delegation: hops deep." }
        chain: { description: "Delegation: the profile chain." }
        runChain: { description: "Delegation: the run chain." }
        parentProfileId: { type: string, description: "Delegation: the immediate parent profile." }
    AuditResponse:
      type: object
      properties:
        entries:
          type: array
          items: { $ref: "#/components/schemas/AuditEntry" }
        count: { type: integer }
        since: { type: string, format: date-time }
        limit: { type: integer }
