openapi: 3.0.3
info:
  title: Molecule Memory Plugin v1
  version: 1.0.0
  description: |
    Contract between workspace-server and a memory backend plugin. The
    plugin owns its own storage; workspace-server is the security
    perimeter (secret redaction, namespace ACL, GLOBAL audit/wrap).

    Defined in RFC #2728. See docs/rfc/memory-v2-rationale.md for design
    rationale.

    Auth: none. Plugins MUST be reachable only on a private network or
    unix socket — workspace-server is the only sanctioned client.
servers:
  - url: http://localhost:9100
    description: Built-in postgres-backed plugin (default)

paths:
  /v1/health:
    get:
      summary: Liveness + capability probe
      operationId: getHealth
      responses:
        '200':
          description: Plugin healthy
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HealthResponse' }
        '503':
          description: Plugin unhealthy (e.g., backing store down)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /v1/namespaces/{name}:
    parameters:
      - $ref: '#/components/parameters/NamespaceName'
    put:
      summary: Upsert a namespace (idempotent)
      operationId: upsertNamespace
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/NamespaceUpsert' }
      responses:
        '200': { $ref: '#/components/responses/Namespace' }
        '400': { $ref: '#/components/responses/BadRequest' }
    patch:
      summary: Update namespace metadata or TTL
      operationId: patchNamespace
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/NamespacePatch' }
      responses:
        '200': { $ref: '#/components/responses/Namespace' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      summary: Delete namespace and all its memories (operator action)
      operationId: deleteNamespace
      responses:
        '204':
          description: Deleted
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/namespaces/{name}/memories:
    parameters:
      - $ref: '#/components/parameters/NamespaceName'
    post:
      summary: Write a memory to a namespace
      description: |
        `content` MUST already be secret-redacted by the workspace-server.
        Plugin does not run additional redaction.
      operationId: commitMemory
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MemoryWrite' }
      responses:
        '201':
          description: Memory persisted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MemoryWriteResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/search:
    post:
      summary: Search memories across one or more namespaces
      description: |
        workspace-server MUST intersect the requested `namespaces` with
        the caller's currently-readable set BEFORE invoking this
        endpoint. The plugin treats the list as authoritative.
      operationId: searchMemories
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SearchRequest' }
      responses:
        '200':
          description: Search results
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SearchResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }

  /v1/memories/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: string, format: uuid }
    delete:
      summary: Forget a memory by id
      description: |
        `requested_by_namespace` is the namespace the caller has write
        access to; the plugin SHOULD reject if the memory doesn't belong
        to that namespace.
      operationId: forgetMemory
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ForgetRequest' }
      responses:
        '204':
          description: Forgotten
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

components:
  parameters:
    NamespaceName:
      in: path
      name: name
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 256
        pattern: '^[a-z]+:[A-Za-z0-9_:.\-]+$'
        example: 'workspace:550e8400-e29b-41d4-a716-446655440000'

  responses:
    Namespace:
      description: Namespace state
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Namespace' }
    BadRequest:
      description: Invalid input
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: Caller lacks write access to the requested namespace
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    HealthResponse:
      type: object
      required: [status, version, capabilities]
      properties:
        status: { type: string, enum: [ok, degraded] }
        version: { type: string, example: "1.0.0" }
        capabilities:
          type: array
          items:
            type: string
            enum: [embedding, fts, ttl, pin, propagation]
          description: |
            Optional features this plugin supports. workspace-server
            adapts MCP responses based on this list (e.g., agents can
            request semantic search only when `embedding` is present).

    NamespaceKind:
      type: string
      enum: [workspace, team, org, custom]

    Namespace:
      type: object
      required: [name, kind, created_at]
      properties:
        name: { type: string }
        kind: { $ref: '#/components/schemas/NamespaceKind' }
        expires_at:
          type: string
          format: date-time
          nullable: true
        metadata:
          type: object
          additionalProperties: true
          nullable: true
        created_at: { type: string, format: date-time }

    NamespaceUpsert:
      type: object
      required: [kind]
      properties:
        kind: { $ref: '#/components/schemas/NamespaceKind' }
        expires_at: { type: string, format: date-time, nullable: true }
        metadata:
          type: object
          additionalProperties: true
          nullable: true

    NamespacePatch:
      type: object
      properties:
        expires_at: { type: string, format: date-time, nullable: true }
        metadata:
          type: object
          additionalProperties: true
          nullable: true

    MemoryKind:
      type: string
      enum: [fact, summary, checkpoint]

    MemorySource:
      type: string
      enum: [agent, runtime, user]

    MemoryWrite:
      type: object
      required: [content, kind, source]
      properties:
        id:
          type: string
          format: uuid
          nullable: true
          description: |
            Optional idempotency key. When supplied, the plugin MUST
            treat the write as upsert keyed on this id (re-running
            the same write does not duplicate). When omitted, the
            plugin generates a fresh UUID. Used by the backfill CLI.
        content:
          type: string
          minLength: 1
          description: Already secret-redacted by workspace-server.
        kind: { $ref: '#/components/schemas/MemoryKind' }
        source: { $ref: '#/components/schemas/MemorySource' }
        expires_at: { type: string, format: date-time, nullable: true }
        propagation:
          type: object
          additionalProperties: true
          nullable: true
          description: |
            Opaque metadata the plugin stores and returns. Reserved for
            future cross-namespace propagation semantics.
        pin: { type: boolean, default: false }
        embedding:
          type: array
          items: { type: number }
          nullable: true
          description: |
            Optional pre-computed embedding. Plugins reporting the
            `embedding` capability MAY ignore this and recompute.

    MemoryWriteResponse:
      type: object
      required: [id, namespace]
      properties:
        id: { type: string, format: uuid }
        namespace: { type: string }

    Memory:
      type: object
      required: [id, namespace, content, kind, source, created_at]
      properties:
        id: { type: string, format: uuid }
        namespace: { type: string }
        content: { type: string }
        kind: { $ref: '#/components/schemas/MemoryKind' }
        source: { $ref: '#/components/schemas/MemorySource' }
        expires_at: { type: string, format: date-time, nullable: true }
        propagation:
          type: object
          additionalProperties: true
          nullable: true
        pin: { type: boolean }
        created_at: { type: string, format: date-time }
        score:
          type: number
          nullable: true
          description: Relevance score from search (semantic + FTS).

    SearchRequest:
      type: object
      required: [namespaces]
      properties:
        namespaces:
          type: array
          items: { type: string }
          minItems: 1
          description: |
            Already intersected with the caller's readable set by
            workspace-server.
        query: { type: string }
        kinds:
          type: array
          items: { $ref: '#/components/schemas/MemoryKind' }
        limit:
          type: integer
          minimum: 1
          maximum: 100
          default: 20
        embedding:
          type: array
          items: { type: number }
          nullable: true

    SearchResponse:
      type: object
      required: [memories]
      properties:
        memories:
          type: array
          items: { $ref: '#/components/schemas/Memory' }

    ForgetRequest:
      type: object
      required: [requested_by_namespace]
      properties:
        requested_by_namespace:
          type: string
          description: Namespace the caller has write access to.

    Error:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          enum:
            - bad_request
            - not_found
            - forbidden
            - internal
            - unavailable
        message: { type: string }
        details:
          type: object
          additionalProperties: true
          nullable: true
