openapi: 3.0.3
info:
 title: AgentNexusAPI
 description: |
  Runtime governance for AI agents — evaluate payloads against policies, route human approvals,
  record audit events, and post execution receipts. Replace server URLs with your deployment.
 version: 0.1.0

servers:
 - url: https://YOUR_HOST
   description: Production (set to your Netlify or other host)
 - url: http://localhost:3000
   description: Local development

tags:
 - name: Evaluate
 - name: Receipt
 - name: Health
 - name: Approval
   description: Magic-link approval flows (token in path)
 - name: Cron
   description: Internal automation (protected by shared secret; not for integrators)
 - name: MCP
   description: Model Context Protocol JSON-RPC gateway (agent tool integrations)

paths:
 /api/mcp:
  post:
   tags: [MCP]
   summary: MCP JSON-RPC (Streamable HTTP subset)
   description: |
    JSON-RPC 2.0 over HTTP for MCP clients. Requires `Accept: application/json, text/event-stream`
    and the same API key headers as `/api/v1/evaluate`. Returns JSON responses only (no SSE) in this version.
    Full contract and examples: repository `docs/MCP_GATEWAY.md`.
   parameters:
    - name: Accept
      in: header
      required: true
      schema:
       type: string
      description: Must include both application/json and text/event-stream.
   requestBody:
    required: true
    content:
     application/json:
      schema:
       type: object
       description: Single JSON-RPC 2.0 request (batch arrays not supported).
       additionalProperties: true
   responses:
    "200":
     description: JSON-RPC response object
     content:
      application/json:
       schema:
        type: object
        additionalProperties: true
    "202":
     description: Accepted (notification-only message)
    "400":
     description: Bad JSON-RPC or missing Accept
    "401":
     description: Missing API key
    "403":
     description: Origin blocked by MCP_ALLOWED_ORIGINS

 /api/v1/evaluate:
  post:
   tags: [Evaluate]
   summary: Run policy evaluation
   description: |
    Resolves workspace from API key hash, evaluates `payload_scope` against the named policy,
    may create approval stages and send emails. When `webhook_url` is set, the service POSTs JSON
    to that URL for human-approval-required evaluations (`event: approval_required`, `resolution: pending_human`)
    and for terminal outcomes (`event: terminal_outcome`). See schema `IntegratorWebhookPayload`.
   parameters:
    - name: Idempotency-Key
      in: header
      required: false
      schema:
       type: string
       maxLength: 128
      description: >-
       Optional; duplicate requests with the same key and identical body (per workspace)
       return the original evaluation response.
    - name: x-request-id
      in: header
      required: false
      schema:
       type: string
      description: Optional correlation id (echoed on responses).
   requestBody:
    required: true
    content:
     application/json:
      schema:
       $ref: "#/components/schemas/EvaluateRequest"
   responses:
    "200":
     description: Evaluation outcome
     headers:
      x-request-id:
       schema:
        type: string
     content:
      application/json:
       schema:
        allOf:
         - $ref: "#/components/schemas/EvaluateResponseCore"
         - type: object
           description: >-
            Extra fields depend on status (e.g. approval_stage_count). Idempotent replays echo
            the original response.
           additionalProperties: true
    "400":
     description: Validation or webhook URL error
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiValidationOrSimpleError"
    "401":
     description: Missing or invalid API key
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "404":
     description: Unknown policy name in workspace
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "409":
     description: Idempotency-Key conflict
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "429":
     description: >-
      Rate limited — Edge middleware may apply per-client-IP limits before the request reaches
      this handler; when Upstash is configured, the route may also enforce per-API-key limits
      (`code: rate_limited`, optional `scope: api_key`). Response may include Retry-After and X-RateLimit-*.
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiRateLimitError"
    "500":
     description: Server error (stable `code`, typically `server_error`)
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiServerError"
   security:
    - ApiKeyAuth: []

 /api/v1/receipt:
  post:
   tags: [Receipt]
   summary: Record execution receipt
   description: After downstream work completes for an approved or auto-approved evaluation.
   parameters:
    - name: x-request-id
      in: header
      required: false
      schema:
       type: string
   requestBody:
    required: true
    content:
     application/json:
      schema:
       $ref: "#/components/schemas/ReceiptRequest"
   responses:
    "200":
     description: Receipt stored or idempotent replay
     headers:
      x-request-id:
       schema:
        type: string
    "400":
     description: Invalid body
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiValidationOrSimpleError"
    "401":
     description: Missing or invalid API key
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "404":
     description: Unknown evaluation for this key
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "409":
     description: Evaluation not eligible for receipt
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "429":
     description: >-
      Rate limited (IP in middleware and/or per API key when Upstash is configured).
      See POST /api/v1/evaluate 429 note.
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiRateLimitError"
    "500":
     description: Server error (`code` typically `server_error`)
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiServerError"
   security:
    - ApiKeyAuth: []

 /api/health:
  get:
   tags: [Health]
   summary: Liveness and optional DB check
   parameters:
    - name: deep
      in: query
      required: false
      schema:
       type: string
       enum: ["1"]
      description: If `deep=1`, runs a trivial database query (503 if DB unreachable).
   responses:
    "200":
     description: OK
    "503":
     description: deep=1 and database unavailable

 /api/approval/{token}/approve:
  post:
   tags: [Approval]
   summary: Approve current stage (POST only)
   description: >
    Called by the hosted approval UI (or your client). Use POST only — do not preapprove via GET.
   parameters:
    - name: token
      in: path
      required: true
      schema:
       type: string
   responses:
    "200":
     description: Approved or advanced to the next stage
     headers:
      x-request-id:
       schema:
        type: string
     content:
      application/json:
       schema:
        type: object
        properties:
         ok:
          type: boolean
    "400":
     description: Missing token
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "403":
     description: Critical OTP stage — verify OTP before approving
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "404":
     description: Invalid or expired token
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "409":
     description: Earlier stage must complete first
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "500":
     description: Server error
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
   security: []

 /api/approval/{token}/reject:
  post:
   tags: [Approval]
   summary: Reject from current stage
   parameters:
    - name: token
      in: path
      required: true
      schema:
       type: string
   responses:
    "200":
     description: Evaluation rejected
     headers:
      x-request-id:
       schema:
        type: string
     content:
      application/json:
       schema:
        type: object
        properties:
         ok:
          type: boolean
    "400":
     description: Missing token
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "404":
     description: Invalid or expired token
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "409":
     description: Wrong stage order
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "500":
     description: Server error
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
   security: []

 /api/approval/{token}/request-otp:
  post:
   tags: [Approval]
   summary: Request email OTP (critical_otp tier)
   parameters:
    - name: token
      in: path
      required: true
      schema:
       type: string
   responses:
    "200":
     description: OTP email dispatched (if configured)
     headers:
      x-request-id:
       schema:
        type: string
     content:
      application/json:
       schema:
        type: object
        properties:
         ok:
          type: boolean
    "400":
     description: Missing token
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "403":
     description: Stage does not use OTP
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "404":
     description: Invalid token
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "500":
     description: Server or email provider error
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
   security: []

 /api/approval/{token}/verify-otp:
  post:
   tags: [Approval]
   summary: Verify OTP code
   parameters:
    - name: token
      in: path
      required: true
      schema:
       type: string
   requestBody:
    required: true
    content:
     application/json:
      schema:
       type: object
       required: [code]
       properties:
        code:
         type: string
   responses:
    "200":
     description: OTP verified; approve may proceed
     headers:
      x-request-id:
       schema:
        type: string
     content:
      application/json:
       schema:
        type: object
        properties:
         ok:
          type: boolean
    "400":
     description: Missing token, invalid JSON, or missing code
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiValidationOrSimpleError"
    "401":
     description: Invalid or expired OTP
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "404":
     description: Invalid token
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "500":
     description: Server error
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
   security: []

 /api/cron/replay-webhook:
  post:
   tags: [Cron]
   summary: Replay a failed integrator webhook delivery
   description: >
    Requires `Authorization: Bearer` equal to `CRON_SECRET`. Intended for ops/cron, not public API keys.
   security:
    - CronBearer: []
   requestBody:
    required: true
    content:
     application/json:
      schema:
       type: object
       required: [delivery_id]
       properties:
        delivery_id:
         type: string
         format: uuid
   responses:
    "200":
     description: Replay succeeded (2xx from integrator after retries)
     content:
      application/json:
       schema:
        type: object
        properties:
         ok:
          type: boolean
    "400":
     description: Bad body or delivery cannot be replayed
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "401":
     description: Missing or wrong bearer secret
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"
    "503":
     description: CRON_SECRET not configured
     content:
      application/json:
       schema:
        $ref: "#/components/schemas/ApiSimpleError"

components:
 securitySchemes:
  CronBearer:
   type: http
   scheme: bearer
   description: "Use the same value as the server's `CRON_SECRET` environment variable."
  ApiKeyAuth:
   type: apiKey
   in: header
   name: Authorization
   description: "Format: `Bearer <api_key>`. Alternatively send `x-api-key: <api_key>`."

 schemas:
  ApiSimpleError:
   type: object
   required: [error]
   properties:
    error:
     type: string
    code:
     type: string
     description: Optional stable code for clients (not raw database codes).
    request_id:
     type: string

  ApiValidationOrSimpleError:
   oneOf:
    - $ref: "#/components/schemas/ApiSimpleError"
    - type: object
      required: [error, details]
      properties:
       error:
        type: string
        example: Validation failed
       details:
        type: object
        description: Zod-style flattened field errors (shape may evolve).

  ApiRateLimitError:
   type: object
   required: [error, code]
   properties:
    error:
     type: string
     example: Too many requests
    code:
     type: string
     enum: [rate_limited]
    scope:
     type: string
     description: e.g. `api_key` when limited per key in the Node handler.
    request_id:
     type: string

  ApiServerError:
   type: object
   required: [error]
   properties:
    error:
     type: string
    code:
     type: string
     description: Database and most internal failures use `server_error` (never raw Postgres/Supabase codes).
     enum: [server_error]
    evaluation_id:
     type: string
     format: uuid
     description: Present on some failures after an evaluation row exists.
    request_id:
     type: string

  EvaluateResponseCore:
   type: object
   required: [evaluation_id, status, policy_version_id, policy_version]
   properties:
    evaluation_id:
     type: string
     format: uuid
    status:
     type: string
     description: Engine / workflow status for this evaluation.
     enum:
      - auto_approved
      - pending_human
      - rejected
      - approved
      - execution_complete
    policy_version_id:
     type: string
     format: uuid
     description: Immutable ruleset snapshot used for this evaluation (see `policy_versions`).
    policy_version:
     type: integer
     minimum: 1
     description: Monotonic version number per policy at the time of evaluate.
    attributed_cost_cents:
     type: integer
     minimum: 0
     description: >-
      Internal governance cost attributed to this evaluation at request time (integer cents).
      From workspace default or per-agent_id override; 0 when unset or legacy rows.

  EvaluateRequest:
   type: object
   required: [policy_name, context]
   properties:
    policy_name:
     type: string
    context:
     type: object
     required: [payload_scope]
     properties:
      agent_id:
       type: string
      payload_scope:
       type: object
       additionalProperties: true
      tool_calls:
       type: array
       description: Optional MCP-style tool trace for the audit run bundle (max 200 entries).
       items:
        type: object
        additionalProperties: true
      artifacts:
       type: array
       description: Content-addressed artifacts (SHA-256 hex, 64 chars) for integrity without storing blobs.
       maxItems: 100
       items:
        type: object
        required: [label, sha256]
        properties:
         label:
          type: string
         sha256:
          type: string
          pattern: "^[a-fA-F0-9]{64}$"
         mime_type:
          type: string
      mcp_trace:
       type: object
       description: >-
        Optional MCP or gateway correlation (session_id, client_name, request_id, parent_span_id, gateway_*).
        Omitted from Idempotency-Key fingerprint. Also stored on run_bundle when present.
       additionalProperties: true
    webhook_url:
     type: string
     format: uri
     description: >-
      Optional HTTPS URL for integrator callbacks: `approval_required` when an evaluation
      enters (or re-enters) a pending human approval stage, and `terminal_outcome` when the
      evaluation reaches a final state (auto-approved, approved, or rejected). Same signing
      and hostname controls as documented for outbound webhooks.

  IntegratorWebhookPayload:
   type: object
   description: JSON body POSTed to `webhook_url` (Content-Type application/json; optional HMAC header).
   required: [event, evaluation_id, resolution, policy_name, context]
   properties:
    event:
     type: string
     enum: [approval_required, terminal_outcome]
    evaluation_id:
     type: string
     format: uuid
    resolution:
     type: string
     enum: [pending_human, auto_approved, approved, rejected]
    policy_name:
     type: string
    context:
     type: object
     description: Mirrors evaluate request context (agent_id, payload_scope, optional tool_calls/artifacts).
    rejection_reason:
     type: string
     description: >
      Present for some terminal rejections only. Examples include no_matching_rule,
      policy_rejected, human_rejected, approval_timeout (time-boxed stage expired),
      approval_timeout_escalation_exhausted (escalate_next with no next stage).
    approval_stage_count:
     type: integer
     minimum: 1
     description: Number of approval stages still pending; present when event is approval_required.

  ReceiptRequest:
   type: object
   required: [evaluation_id, execution_status]
   properties:
    evaluation_id:
     type: string
     format: uuid
    execution_status:
     type: string
    execution_receipt:
     type: object
     additionalProperties: true
     default: {}
     description: Optional; omitted or empty object is treated as `{}`.
