Skip to content

REST/OpenAPI bridge

Status: target state, partial. The contract is fixed in ACTOR-COMMUNICATION-CONTRACT.md. The HTTP gateway today exposes only its own ops (gateway.http.*) and a curated set of platform routes (/api/auth/..., /api/identity/..., /api/apps). A general "any actor accepts becomes a REST POST" bridge is not in production code; this page documents the target.

The mapping

Source: WORKSTREAMS/core-and-packaging/ACTOR-COMMUNICATION-CONTRACT.md § "How they map to protocol gateways".

Matrix declarationOpenAPI primitive
static acceptsPOST /api/<root>/<mount>/<op> endpoints
static stateGET /api/<root>/<mount>/$state/<key> endpoints
static emitsGET /api/<root>/<mount>/$events SSE endpoints
static blackboardGET and PUT /api/<root>/<mount>/$blackboard/<factName>
static streams(not directly mapped — REST doesn't natively model durable streams)

The bridge:

  1. Walks system.catalog.search to enumerate live mounts and ops.
  2. Generates an OpenAPI 3.x document where each accepts entry becomes a POST operation with a typed request body and response schema.
  3. Serves the OpenAPI document at a well-known URL (e.g., /api/openapi.json or per-mount).
  4. Translates inbound POST /api/<root>/<mount>/<op> requests into actor invocations, returning the response as JSON.

Auto-generated docs

The vision (MASTER-PLAN.md Part 5):

"Forms from schemas. Live events from subscriptions. Live state from KV. Children as navigation. Export buttons for every protocol format. Generated entirely from \$introspect. The actor author wrote zero UI code."

"Resolution priority: 1. Actor has custom webapp → render it. 2. Actor has typed schemas → auto-generated CQRS view. 3. Actor has nothing → raw introspect JSON. No dead ends. Every actor is browsable."

The OpenAPI spec is one of the export formats. From an OpenAPI doc you can generate typed clients in any language, hit the actor with curl, or plug it into Postman.

Schema generation

accepts.<op>.schema and accepts.<op>.returns map to JSON Schema:

typescript
// Matrix declaration
static accepts = {
  'market.quote': {
    description: 'Get the current price...',
    schema: { symbol: { type: 'string', description: '...' } },
    returns: {
      price: { type: 'number', description: '...' },
      volume: { type: 'number', description: '...' },
    },
  },
};

Becomes:

yaml
# OpenAPI 3.x
paths:
  /api/COM.NIMBLETEC.RICHARD-SANTOMAURO/market.feed/market.quote:
    post:
      summary: Get the current price...
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                symbol:
                  type: string
                  description: ...
              required: [symbol]
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  price:    { type: number, description: ... }
                  volume:   { type: number, description: ... }

The translation is mechanical because FieldDescriptor is structurally close to JSON Schema.

Authentication

Inbound REST requests carry the standard auth artifacts:

MechanismHow it works
mx_session cookieSame-origin requests; cookie carries the session
Authorization: Bearer <jwt>Cross-origin or programmatic clients
Per-tenant API keyServer-to-server calls (target — not implemented)

The bridge validates the auth artifact through auth.session.validate or equivalent, derives the principal, and authorizes the actor call under that principal.

Error mapping

Matrix { ok: false, error, statusCode? } replies map to HTTP status codes:

Matrix replyHTTP status
{ ok: true, ... }200
{ ok: false, statusCode: 400, error }400
{ ok: false, statusCode: 404, error }404
{ ok: false, error } (no statusCode)500 (default for unspecified errors)
transport timeout504 Gateway Timeout
auth failure401
permission denied403

The body is the actor's error payload, JSON-stringified.

Why this is not the universal default today

The HTTP gateway exists and runs in production; it does not auto-expose every accepts op. Reasons it isn't yet the default:

  • Authorization model. Surfacing every internal op publicly without per-op auth review is unsafe. Each op needs an explicit "exposed via REST" declaration before it should be reachable from outside the bus.
  • Schema accuracy. Many actor accepts declarations today use the short string form (name: 'string') rather than the full FieldDescriptor form. Auto-generating a typed OpenAPI spec needs the long form everywhere.
  • Idempotency / rate-limiting. REST callers have different expectations from in-bus callers. Idempotency keys, rate limits, and retry policies need an HTTP-level layer above the actor call.

Per-op opt-in (target shape)

A future static rest declaration on actors:

typescript
static rest = {
  'market.quote': {
    method: 'POST',
    path: '/api/market/quote',
    auth: 'session',
    rateLimit: { requestsPerMinute: 60 },
  },
};

This makes opting an op into the REST surface explicit and reviewable.

See also