openapi: 3.1.0
info:
  title: ActPass Gateway API
  version: "1.0"
  description: |
    Signed action-authorization gateway for AI agents. Every risky action gets a
    deterministic allow / deny / require_approval decision outside the LLM,
    backed by EdDSA Action Passports and recorded in a tamper-evident evidence chain.

    **Fail-closed:** errors, missing keys, and unverifiable claims always deny.
    All endpoints are tenant-scoped by the authenticated API key.
  contact:
    email: security@actpass.org
servers:
  - url: https://actpass.org/api
  - url: http://localhost:3000/api
security:
  - bearerAuth: []
tags:
  - name: Actions
    description: Preflight decisions and gated execution
  - name: Passports
    description: Issue, verify, and revoke signed Action Passports
  - name: Approvals
    description: Human-in-the-loop approval queue
  - name: Policies
    description: Deterministic policy CRUD
  - name: Tools
    description: Tool-manifest registry and drift detection
  - name: Evidence
    description: Append-only, hash-chained audit ledger
  - name: Credentials
    description: Encrypted credential vault
  - name: Keys
    description: Gateway API-key management
  - name: Platform

paths:
  /v1/actions/preflight:
    post:
      tags: [Actions]
      summary: Decide whether an agent action may proceed
      description: |
        The core decision endpoint. Verifies the Action Passport (if supplied),
        checks tool-manifest drift, evaluates deterministic policy, and routes
        to human approval when required. A passport carrying a verified
        approval_hash satisfies a require_approval outcome.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreflightRequest'
      responses:
        '200':
          description: Decision rendered (allow / require_approval / warn / deny in monitor mode)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreflightResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/actions/execute:
    post:
      tags: [Actions]
      summary: Execute an action through the gateway
      description: |
        Runs preflight, then (on allow) forwards the call to the configured
        upstream with the bound credential injected, sealing request and result
        into the evidence chain. Requires a gateway-scope API key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreflightRequest'
      responses:
        '200':
          description: Execution result with decision and evidence linkage
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/passports/issue:
    post:
      tags: [Passports]
      summary: Issue a signed Action Passport
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IssuePassportRequest'
      responses:
        '200':
          description: EdDSA-signed passport (JWS) and its claims
        '401': { $ref: '#/components/responses/Unauthorized' }

  /v1/passports/verify:
    post:
      tags: [Passports]
      summary: Verify a passport without consuming its single-use jti
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [passport]
              properties:
                passport: { type: string, description: Compact JWS }
                audience: { type: string }
      responses:
        '200':
          description: Verification result with per-check errors and reason codes

  /v1/passports/revoke:
    post:
      tags: [Passports]
      summary: Revoke a passport by jti (durable, all replicas)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [jti]
              properties:
                jti: { type: string }
                reason: { type: string }
      responses:
        '200': { description: Revoked }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /v1/passports/jwks:
    get:
      tags: [Passports]
      summary: Public JWKS for passport verification (includes rotation-overlap keys)
      security: []
      responses:
        '200':
          description: JSON Web Key Set

  /v1/approvals:
    get:
      tags: [Approvals]
      summary: List approval requests for the tenant
      responses:
        '200': { description: Tenant-scoped approval queue }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /v1/approvals/{id}/decide:
    post:
      tags: [Approvals]
      summary: Apply a reviewer decision (FSM-gated, RBAC approval.decide)
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action]
              properties:
                action:
                  type: string
                  enum: [approve, deny, modify, request_context, require_reauth, require_tool_reapproval, escalate, expire]
                comment: { type: string }
                modified_args: { type: object }
      responses:
        '200':
          description: New status plus the immutable event_hash binding reviewer + action
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { description: Transition not allowed by the approval FSM }

  /v1/approvals/expire:
    post:
      tags: [Approvals]
      summary: Sweep pending approvals past the SLA window to expired
      responses:
        '200': { description: Count of expired approvals }

  /v1/policies:
    get:
      tags: [Policies]
      summary: List policies
      responses:
        '200': { description: Tenant policies }
    post:
      tags: [Policies]
      summary: Create a policy (JSON DSL, validated before persist)
      responses:
        '200': { description: Created policy with computed policy_hash }
        '403': { $ref: '#/components/responses/Forbidden' }

  /v1/policies/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: string } }
    get:
      tags: [Policies]
      summary: Fetch a policy
      responses:
        '200': { description: Policy document }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Policies]
      summary: Update a policy (bumps version, recomputes hash)
      responses:
        '200': { description: Updated policy }
    delete:
      tags: [Policies]
      summary: Delete a policy
      responses:
        '200': { description: Deleted }

  /v1/tools/ingest:
    post:
      tags: [Tools]
      summary: Register or update a tool manifest (canonicalized + hashed)
      description: Drift from the previously approved manifest is classified; material changes flip the tool to require re-approval at runtime.
      responses:
        '200': { description: Manifest hash and drift classification }

  /v1/tools/diff:
    post:
      tags: [Tools]
      summary: Classify drift between two tool manifests
      responses:
        '200': { description: Drift classes with reason codes (e.g. tool.read_to_write_conversion) }

  /v1/evidence/events:
    get:
      tags: [Evidence]
      summary: List evidence events (tenant-scoped)
      responses:
        '200': { description: Hash-chained evidence events }
    post:
      tags: [Evidence]
      summary: Append a custom evidence event to the chain
      responses:
        '200': { description: Sealed event with chain linkage }

  /v1/evidence/verify:
    get:
      tags: [Evidence]
      summary: Recompute and verify the evidence hash chain
      description: Recomputes hashes from sealed canonical events — content tampering and chain breaks are both detected.
      responses:
        '200': { description: Chain verification report }

  /v1/evidence/export:
    get:
      tags: [Evidence]
      summary: Export an evidence bundle (JSON / Markdown / CSV / SIEM-JSONL), Ed25519-signed
      parameters:
        - { name: format, in: query, schema: { type: string, enum: [json, md, csv, siem] } }
      responses:
        '200': { description: Signed evidence bundle }

  /v1/credentials:
    get:
      tags: [Credentials]
      summary: List vault items (metadata only — secrets never leave the vault)
      responses:
        '200': { description: Credential metadata }
    post:
      tags: [Credentials]
      summary: Store a credential (AES-256-GCM envelope encryption)
      responses:
        '200': { description: Stored; secret is write-only }
        '403': { $ref: '#/components/responses/Forbidden' }

  /v1/keys:
    get:
      tags: [Keys]
      summary: List API keys (hashes only)
      responses:
        '200': { description: Key metadata }
    post:
      tags: [Keys]
      summary: Create an API key (returned once, stored hashed)
      responses:
        '200': { description: The new key — shown exactly once }
        '403': { $ref: '#/components/responses/Forbidden' }
    delete:
      tags: [Keys]
      summary: Revoke an API key
      responses:
        '200': { description: Revoked }

  /v1/compliance:
    get:
      tags: [Platform]
      summary: Evaluate compliance controls (CC-1…CC-6) against live system state
      responses:
        '200': { description: Control-by-control pass/fail with evidence }

  /v1/integrations/siem:
    get:
      tags: [Platform]
      summary: SIEM-formatted event stream (Splunk / Datadog / CEF / webhook)
      responses:
        '200': { description: Exported events in the requested SIEM dialect }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Gateway API key (create via POST /v1/keys). Dev fallback keys via ACTPASS_API_KEYS.

  responses:
    Unauthorized:
      description: Missing/invalid API key or passport
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiError' }
    Forbidden:
      description: Authenticated but not permitted (RBAC or policy deny)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiError' }
    NotFound:
      description: Resource not found in this tenant
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiError' }
    RateLimited:
      description: Tenant request budget exceeded (fixed window per minute)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiError' }

  schemas:
    ApiError:
      type: object
      required: [error]
      properties:
        error: { type: string, description: Human-readable message }
        reason_code: { type: string, description: Typed reason code (e.g. passport.replay_detected, approval.invalid) }

    PreflightRequest:
      type: object
      required: [tool, resource, agent_id, user_id]
      properties:
        tool: { type: string, example: stripe.refund.create }
        resource: { type: string, example: stripe:charge:ch_123 }
        args: { type: object, additionalProperties: true }
        agent_id: { type: string }
        agent_name: { type: string }
        user_id: { type: string }
        goal: { type: string }
        mode: { type: string, enum: [monitor, warn, enforce, strict] }
        passport: { type: string, description: Compact JWS Action Passport }
        audience: { type: string }
        idempotency_key: { type: string }

    PreflightResponse:
      type: object
      properties:
        decision: { type: string, enum: [allow, deny, warn, require_approval, require_tool_reapproval] }
        reason_code: { type: string, example: approval.satisfied }
        summary: { type: string }
        matched_rules: { type: array, items: { type: string } }
        approval_request_id: { type: string }
        chain_id: { type: string, description: Evidence-chain id linking this decision to its audit trail }

    IssuePassportRequest:
      type: object
      required: [tenant_id, agent_id, user_id, goal, allowed_tools, risk_tier, policy_id, audience, ttl_seconds]
      properties:
        tenant_id: { type: string }
        agent_id: { type: string }
        user_id: { type: string }
        goal: { type: string }
        allowed_tools: { type: array, items: { type: string } }
        allowed_resources: { type: array, items: { type: string } }
        resource_constraints: { type: object, additionalProperties: true }
        risk_tier: { type: string, enum: [low, medium, high, critical] }
        policy_id: { type: string }
        approval_hash: { type: [string, "null"], description: event_hash of a granted approval — verified fail-closed at use time }
        audience: { type: string }
        ttl_seconds: { type: integer }
