openapi: 3.1.0
info:
  title: NoData Fabric API
  version: "1.0.0"
  description: >
    Access Compute for AI agents — one primitive exposed through a few endpoints.
    Govern data given to agents (Flow A: create agent → grant → plan → read → prove)
    and connect agents to each other, cross-org, with every hop proven (Flow B:
    enroll → grant → send → fetch). The server is content-blind: it stores ciphertext
    and wrapped keys, never plaintext or private keys. Deny-by-default; every access —
    and every non-access — is a signed receipt.
  license:
    name: Proprietary — NoData
servers:
  - url: https://nodatacapsule.com
    description: Production

tags:
  - name: Access Compute
    description: The single front door — plan (may I?) and execute (authorized read + proof).
  - name: Agents
    description: Create agents and mint their identity tokens (grants).
  - name: Fabric
    description: Agent-to-agent identity, connections, and content-blind exchange.

components:
  securitySchemes:
    OrgKey:
      type: http
      scheme: bearer
      description: Organization API key (ndp_…). Scope `governance`/`*` for grants; org-scoped for connections.
    AgentToken:
      type: http
      scheme: bearer
      description: Agent grant token (ndca-…), returned by POST /api/v1/governance/grant. Shown once.
  schemas:
    AccessDecision:
      type: object
      properties:
        decision: { type: string, enum: [allow, degrade, deny] }
        allowed:
          type: object
          properties:
            columns: { type: array, items: { type: string } }
        withheld:
          type: object
          properties:
            columns: { type: array, items: { type: string } }
        cost:
          type: object
          properties:
            distance: { type: [number, "null"], description: Authorization cost — the JEPA distance analogue. }
        proof_id: { type: [string, "null"] }
    Receipt:
      type: object
      properties:
        signature_hex: { type: string }
        signing_pubkey_hex: { type: string }
        signing_kid: { type: string }
        signed_at: { type: string, format: date-time }

paths:
  /api/v1/governance/grant:
    post:
      tags: [Agents]
      summary: Create an agent + scoped grant in one call (returns its identity token)
      security: [{ OrgKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agent, columns]
              properties:
                agent: { type: string, description: Human label; becomes the agent handle. }
                table: { type: string, default: agent_demo_records }
                columns: { type: array, items: { type: string }, description: Columns the agent may read (must be registered). }
                classifications: { type: array, items: { type: string } }
                where: { type: object, additionalProperties: true, description: Equality filter on plaintext columns only. }
                ttl_seconds: { type: integer, minimum: 60 }
      responses:
        "201":
          description: Agent created and grant attached. grant_token is shown once.
          content:
            application/json:
              schema:
                type: object
                properties:
                  grant_token: { type: string, description: The agent identity/bearer (ndca-…). Shown once. }
                  handle: { type: string }
                  agent_id: { type: string }
                  jti: { type: string }
                  expires_at: { type: string, format: date-time }
                  read_url: { type: string }
        "400": { description: Missing/invalid fields or columns out of scope. }
        "403": { description: Missing scope or key not bound to an org. }

  /api/v1/agents:
    get:
      tags: [Agents]
      summary: List this org's agents and their Fabric addresses (read-only)
      security: [{ OrgKey: [] }]
      responses:
        "200":
          description: Roster.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  count: { type: integer }
                  agents:
                    type: array
                    items:
                      type: object
                      properties:
                        agent_id: { type: string }
                        handle: { type: string }
                        name: { type: string }
                        status: { type: string }
                        fabric_enrolled: { type: boolean }
                        fabric_address: { type: [string, "null"] }

  /api/access/compute:
    post:
      tags: [Access Compute]
      summary: Plan (dry-run) — may I? No data released, no keys derived.
      description: The single front door for the PLAN side of every verb, dispatched by `action` (classify | read | send).
      security: [{ AgentToken: [] }]
      parameters:
        - in: query
          name: receipt
          schema: { type: string, enum: ["true"] }
          description: When "true", mint a verifiable proof of the decision.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                action: { type: string, enum: [classify, read, send], default: read }
                table: { type: string }
                resource: { type: string }
                columns: { type: array, items: { type: string } }
                where: { type: object, additionalProperties: true }
                purpose: { type: string }
                destination:
                  type: object
                  properties:
                    channel: { type: string }
                    recipientCount: { type: integer }
                    external: { type: boolean }
      responses:
        "200":
          description: A decision — not data.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AccessDecision' }
        "401": { description: Missing/invalid bearer. }
        "404": { description: Unknown table for this tenant. }

  /api/agents/{handle}/read:
    post:
      tags: [Access Compute]
      summary: Execute — release the authorized columns (+ audit chain)
      description: Denied columns never decrypt; every read chains an audit row, success or not.
      security: [{ AgentToken: [] }]
      parameters:
        - in: path
          name: handle
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [table, columns]
              properties:
                table: { type: string }
                columns: { type: array, items: { type: string } }
                where: { type: object, additionalProperties: true }
                limit: { type: integer, default: 50, maximum: 200 }
                purpose: { type: string }
      responses:
        "200":
          description: Authorized rows + audit.
          content:
            application/json:
              schema:
                type: object
                properties:
                  rows: { type: array, items: { type: object, additionalProperties: true } }
                  count: { type: integer }
                  audit:
                    type: object
                    properties:
                      id: { type: string }
                      chain_index: { type: integer }
        "403":
          description: scope_violation — includes denied_columns and an audit row.

  /api/agents/{handle}/fabric/enroll:
    post:
      tags: [Fabric]
      summary: Mint a Fabric address for an agent (register the public key only)
      security: [{ AgentToken: [] }]
      parameters:
        - { in: path, name: handle, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ecdh_pubkey_hex]
              properties:
                ecdh_pubkey_hex: { type: string, pattern: '^[0-9a-f]{64}$', description: X25519 public key, 32 raw bytes hex. }
      responses:
        "200":
          description: Enrolled.
          content:
            application/json:
              schema: { type: object, properties: { kid: { type: string, description: 16-hex Fabric address. } } }
        "409": { description: kid_collision — address already enrolled. }

  /api/v1/fabric-grants:
    get:
      tags: [Fabric]
      summary: List this org's connections (outbound issued + inbound to your agents)
      security: [{ OrgKey: [] }]
      responses:
        "200": { description: Grants and inbound list. }
    post:
      tags: [Fabric]
      summary: Authorize a recipient address (the connection) — deny-by-default; supports federation
      security: [{ OrgKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [subject_kid]
              properties:
                subject_kid: { type: string, pattern: '^[0-9a-f]{16}$', description: The recipient agent's Fabric address. }
                label: { type: string }
                allowed_sender_kids: { type: array, items: { type: string, pattern: '^[0-9a-f]{16}$' } }
                ttl_days: { type: integer, minimum: 1, maximum: 3650, description: Convenience — server derives expires_at. }
                expires_at: { type: string, format: date-time }
      responses:
        "201": { description: Grant created (receipted). }
        "409": { description: grant_exists. }
    patch:
      tags: [Fabric]
      summary: Revoke a connection
      security: [{ OrgKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [grant_id]
              properties:
                grant_id: { type: string }
                reason: { type: string }
      responses:
        "200": { description: Revoked (receipted). }

  /api/agents/{handle}/fabric/send:
    post:
      tags: [Fabric]
      summary: Send a content-blind envelope to a Fabric address
      description: Seal in your runtime; the server stores ciphertext + a wrapped key it cannot open, and signs a metadata-only receipt.
      security: [{ AgentToken: [] }]
      parameters:
        - { in: path, name: handle, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [recipient_kid, ciphertext_b64, wrapped_key_b64, payload_iv_b64]
              properties:
                recipient_kid: { type: string, pattern: '^[0-9a-f]{16}$' }
                ciphertext_b64: { type: string }
                wrapped_key_b64: { type: string }
                payload_iv_b64: { type: string }
                content_type_hint: { type: string }
                filename_hint: { type: string }
                ttl_hours: { type: integer, minimum: 1, maximum: 720 }
                max_fetches: { type: integer, minimum: 1, maximum: 20 }
                purpose: { type: string }
      responses:
        "200":
          description: Stored + signed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  sender_kid: { type: string }
                  recipient_kid: { type: string }
                  ttl_at: { type: string, format: date-time }
                  receipt: { $ref: '#/components/schemas/Receipt' }
        "412": { description: not_enrolled — POST /fabric/enroll first. }

  /api/agents/{handle}/fabric/inbox:
    get:
      tags: [Fabric]
      summary: List envelopes addressed to this agent (metadata only)
      security: [{ AgentToken: [] }]
      parameters:
        - { in: path, name: handle, required: true, schema: { type: string } }
      responses:
        "200": { description: Inbox items (no ciphertext). }

  /api/agents/{handle}/fabric/fetch:
    post:
      tags: [Fabric]
      summary: Fetch one envelope — through the gate (recomputed every time)
      description: >
        computeFabricFetchDecision runs from scratch: agent active · addressed to it · sender attributable ·
        active grant from the sender's org · not expired/revoked · sender on the allowlist (if set). Allow and
        deny are both signed onto the proof chain; the wrapped key is released only after an allow receipt.
      security: [{ AgentToken: [] }]
      parameters:
        - { in: path, name: handle, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [blob_id]
              properties:
                blob_id: { type: string, format: uuid }
      responses:
        "200":
          description: Allowed — envelope released with the decision receipt.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ciphertext_b64: { type: string }
                  wrapped_key_b64: { type: string }
                  payload_iv_b64: { type: string }
                  decision:
                    type: object
                    properties:
                      outcome: { type: string, enum: [allow] }
                      reasons: { type: array, items: { type: string } }
                      receipt_id: { type: string }
        "403":
          description: fabric_denied — the gate refused; the denial is receipted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error: { type: string, enum: [fabric_denied] }
                  reasons: { type: array, items: { type: string } }
        "410": { description: expired or fully fetched. }
