molecule-core/docs/api-protocol/memory-plugin-v1.yaml
Hongming Wang 53d823e719 Memory v2 PR-1: OpenAPI plugin contract + Go bindings
First of 11 PRs implementing the memory-system plugin refactor (RFC #2728).
This PR is pure additive scaffolding — no behavior change, no integration
yet. It defines the wire shape between workspace-server and a memory
plugin so PR-2 (HTTP client) and PR-3 (built-in postgres plugin) can be
built against a single source of truth.

What ships:
  - docs/api-protocol/memory-plugin-v1.yaml: OpenAPI 3.0.3 spec covering
    /v1/health, namespace upsert/patch/delete, memory commit, search,
    forget. Auth-free (private network only); workspace-server is the
    only sanctioned client and the security perimeter.
  - workspace-server/internal/memory/contract: typed Go bindings with
    Validate() methods on every wire object so both client (PR-2) and
    server (PR-3) self-check at the boundary.
  - Round-trip JSON tests for every type (catch asymmetric tag bugs).
  - 5 golden vector files under testdata/ pinning the exact wire shape;
    update via UPDATE_GOLDENS=1.

Coverage: 100% of statements in contract.go.

The validation rules encode design decisions worth flagging in review:
  - SearchRequest with empty Namespaces is REJECTED at plugin level —
    workspace-server is required to intersect the readable set
    server-side; an empty list reaching the plugin is a bug.
  - NamespacePatch with no fields is REJECTED — empty patches are
    pointless round-trips.
  - MemoryWrite with whitespace-only Content is REJECTED — zero-info
    memories pollute search results.

No code yet calls into this package; integration starts in PR-2.
2026-05-04 06:45:52 -07:00

350 lines
10 KiB
YAML

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:
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