OpenWOP openwop.dev
FieldValue
RFC0028
TitlePrompt Library Endpoints (/v1/prompts/*) and Prompt Pack Kind (kind: "prompt")
StatusAccepted
Author(s)OpenWOP Working Group
Created2026-05-19
Updated2026-05-25 (Active → Accepted: MyndHyve workflow-runtime advertises capabilities.prompts.{supported: true, packsSupported: true, mutableLibrary: true, library: {id: "myndhyve-system", renderEndpoint: "/v1/prompts:render", maxRenderRequestBytes: 65536}} live on https://myndhyve.ai/.well-known/openwop — verified 2026-05-25 via direct curl. A parallel session on the MyndHyve side shipped the host-side kind: "prompt" pack ingest + packsSupported: true advertise + the real library block end-to-end during the same conformance window that closed RFC 0041. Per the bootstrap-phase rule (advertisement + scenarios pass-modulo-honest-skip), the Tier-2 path-to-Accepted criterion — "MyndHyve Tier-1 advertises packsSupported: false, mutableLibrary: false; path-to-Accepted requires both true" per the README RFC index — is satisfied at the wire. Prior 2026-05-20 (Draft → Active — see Status history below).
Affectsspec/v1/prompts.md (extends §"Discovery & distribution") · spec/v1/registry-operations.md (adds prompt-pack flow) · api/openapi.yaml (adds 6 operations under /v1/prompts*) · schemas/prompt-pack-manifest.schema.json (NEW) · schemas/capabilities.schema.json (extends prompts block with packsSupported, mutableLibrary, library) · 5 new conformance scenarios · CHANGELOG
Compatibilityadditive
Supersedes

Summary

Builds on RFC 0027 by adding (a) a REST surface for listing, fetching, and rendering PromptTemplates — GET /v1/prompts, GET /v1/prompts/{templateId}, POST /v1/prompts:render, plus optional mutating endpoints POST /v1/prompts / PUT /v1/prompts/{templateId} / DELETE /v1/prompts/{templateId} — and (b) a new registry pack kind kind: "prompt" parallel to kind: "node" (RFC 0003) and kind: "workflow-chain" (RFC 0013), so prompt libraries can be authored once and distributed across hosts via the same signed-tarball + Ed25519 + SRI pipeline that already serves node and chain packs. The endpoints close the discovery + dispatch gap left open in RFC 0027 (which shipped the wire shape but no surface to fetch one); the pack kind closes the distribution gap (hosts today have no portable way to ship a curated prompt library).

Motivation

RFC 0027 normates PromptTemplate + PromptRef + capabilities.prompts + prompt.composed, but a host advertising prompts.supported: true has no spec'd way to expose its library to a client. Three concrete consequences:

1. Editor authoring blocked. A workflow-builder UI that wants to populate a "Prompt picker" dropdown (per the React reference app's planned kind: 'prompt-picker' ConfigField) has nowhere to call. The list endpoint is the gating piece. 2. Cross-host portability blocked. A prompt library curated against the myndhyve reference impl can't be exported to the openwop reference workflow-engine (or vice versa) without re-authoring each template. The same problem RFC 0013 solved for editor presets exists here for prompts: the in-the-wild assets live in proprietary stores. 3. Render reproducibility opaque. A host's prompt composition pipeline (the 10-step assembly in RFC 0027 §"Implementation notes") is internal. Clients that want to preview what a node will send to the LLM — before dispatch, for debugging or human-review surfaces — have no path. The prompt.composed event surfaces the _post-execution_ result; a :render preview surfaces the _pre-execution_ one.

The motivating use case: a multi-agent workflow author wants to drop three core.ai.callPrompt nodes (writer, critic, editor), pick a system prompt for each from a vendor-published vendor.acme.editorial-prompts prompt pack, preview each composed body in the inspector with the current node-input bindings, and dispatch — all without leaving the editor. Phase A gave the editor a PromptRef to store. Phase B gives the editor a list of refs to choose from and a :render endpoint to preview them.

This RFC takes the same posture as RFC 0021 (shape first, then surface): RFC 0027 landed the wire shape; this RFC lands the surface. RFC 0029 (Phase C) layers the agent-scoped override hierarchy on top.

Proposal

§A — REST surface

api/openapi.yaml gains six operations under a new /v1/prompts group. All six are gated on capabilities.prompts.supported: true. The mutating three (POST, PUT, DELETE) are additionally gated on capabilities.prompts.mutableLibrary: true.

paths:
  /v1/prompts:
    get:
      operationId: listPromptTemplates
      summary: List prompt templates available to the caller.
      parameters:
        - name: kind
          in: query
          schema: { type: string, enum: [system, user, few-shot, schema-hint] }
          description: Filter by PromptTemplate.kind.
        - name: tag
          in: query
          schema: { type: string }
          description: Filter to templates whose `tags[]` array contains this exact tag. Repeatable.
        - name: modelClass
          in: query
          schema: { type: string }
          description: Filter to templates whose `modelHints.modelClass` matches.
        - name: source
          in: query
          schema: { type: string, enum: [host, pack, user] }
          description: Filter by provenance.
        - name: cursor
          in: query
          schema: { type: string }
          description: Opaque pagination cursor.
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
      responses:
        '200':
          description: Paginated list of templates.
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/PromptTemplate'
                  nextCursor: { type: string }
        '501':
          description: Host does not advertise `capabilities.prompts.supported: true`.
    post:
      operationId: createPromptTemplate
      summary: Create a new prompt template (mutable libraries only).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PromptTemplate' }
      responses:
        '201':
          description: Created. `Location` header carries the canonical URI.
        '409':
          description: A template with this `templateId@version` already exists.
        '501':
          description: Host does not advertise `capabilities.prompts.mutableLibrary: true`.

  /v1/prompts/{templateId}:
    parameters:
      - name: templateId
        in: path
        required: true
        schema: { type: string, pattern: '^[a-z0-9][a-z0-9._-]{0,127}$' }
      - name: version
        in: query
        schema: { type: string, pattern: '^\\d+\\.\\d+\\.\\d+$' }
        description: Pin to a specific version. When omitted, returns the latest version.
    get:
      operationId: getPromptTemplate
      responses:
        '200':
          description: The template.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PromptTemplate' }
        '404':
          description: No such template (or version).
    put:
      operationId: updatePromptTemplate
      summary: Replace template body / metadata (mutable libraries only). MUST snapshot the prior version.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PromptTemplate' }
      responses:
        '200': { description: Updated. }
        '409':
          description: Submitted `version` does not exceed the stored version (SemVer comparison).
        '501':
          description: Host does not advertise `capabilities.prompts.mutableLibrary: true`.
    delete:
      operationId: deletePromptTemplate
      responses:
        '204': { description: Deleted. }
        '403':
          description: Template is host built-in or pack-sourced (`meta.source !== "user"`) and MUST NOT be deleted.
        '501':
          description: Host does not advertise `capabilities.prompts.mutableLibrary: true`.

  /v1/prompts:render:
    post:
      operationId: renderPromptTemplate
      summary: Render a template with supplied variable bindings and return the composed body + hash. Does NOT dispatch an LLM call.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ref, variables]
              properties:
                ref: { $ref: '#/components/schemas/PromptRef' }
                variables:
                  type: object
                  description: Variable bindings keyed by PromptVariable.name. Secret-source values MUST be supplied as `[REDACTED:<secretId>]` markers; the host resolves them via `capabilities.secrets`.
                contentTrust:
                  type: string
                  enum: [trusted, untrusted]
                  description: Aggregate trust marker for the supplied variable values, propagated through composition per RFC 0027 §E.
      responses:
        '200':
          description: Composed result.
          content:
            application/json:
              schema:
                type: object
                required: [hash, refs, variableHashes]
                properties:
                  composed: { type: string, description: "Full composed body. Present only when `capabilities.prompts.observability` is `full`." }
                  hash: { type: string, pattern: '^sha256:[0-9a-f]{64}$' }
                  refs: { type: array, items: { type: string } }
                  variableHashes:
                    type: object
                    additionalProperties: { type: string, pattern: '^sha256:[0-9a-f]{64}$' }
                  contentTrust: { type: string, enum: [trusted, untrusted] }
        '400':
          description: |
            Required variable unresolved (`prompt_variable_unresolved`) OR variable type mismatch (`prompt_variable_type_mismatch`) OR
            invalid PromptRef (`prompt_ref_invalid`).
        '404':
          description: Referenced template does not exist.
        '501':
          description: Host does not advertise `capabilities.prompts.supported: true`.

Deterministic-render invariant. Two calls to POST /v1/prompts:render with identical ref + variables + contentTrust MUST produce identical hash and variableHashes values. This is the same determinism contract prompt.composed carries at dispatch time (RFC 0027 §F).

Cache semantics. GET /v1/prompts/{templateId} responses SHOULD set ETag: "<hash-of-body>" and Cache-Control: max-age=60 (immutable when version is pinned: Cache-Control: public, max-age=31536000, immutable). Clients MAY use conditional requests (If-None-Match) to revalidate cheaply. This mirrors node-packs.md §"Immutable artifact" semantics.

§B — Pack kind: prompt

schemas/prompt-pack-manifest.schema.json (NEW). Peer to node-pack-manifest.schema.json (RFC 0003) and workflow-chain-pack-manifest.schema.json (RFC 0013). Pack manifest top-level kind field carries the literal "prompt". Diff against the existing pack-kind discriminator pattern:

 {
   "name": "vendor.acme.editorial-prompts",
   "version": "1.0.0",
+  "kind": "prompt",
   "engines": { "openwop": ">=1.1.0 <2.0.0" },
+  "prompts": [
+    {
+      "templateId": "writer-system",
+      "version": "1.0.0",
+      "kind": "system",
+      "text": "You are a careful editorial writer. {{styleGuide}}",
+      "variables": [
+        { "name": "styleGuide", "type": "string", "required": false, "source": "input" }
+      ],
+      "tags": ["editorial", "writing"]
+    }
+  ]
 }
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://openwop.dev/spec/v1/prompt-pack-manifest.schema.json",
  "title": "PromptPackManifest",
  "type": "object",
  "additionalProperties": false,
  "required": ["name", "version", "kind", "engines", "prompts"],
  "properties": {
    "name": {
      "type": "string",
      "pattern": "^(core|vendor|community|private)\\.[a-z][a-z0-9_-]*(\\.[a-z][a-zA-Z0-9_-]*)+$",
      "minLength": 1,
      "maxLength": 256
    },
    "version": {
      "type": "string",
      "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$"
    },
    "kind": { "type": "string", "const": "prompt" },
    "description": { "type": "string", "maxLength": 1024 },
    "author": { "type": "string" },
    "license": { "type": "string" },
    "homepage": { "type": "string", "format": "uri" },
    "repository": { "type": "string", "format": "uri" },
    "keywords": { "type": "array", "items": { "type": "string", "maxLength": 64 }, "maxItems": 50 },
    "engines": {
      "type": "object",
      "required": ["openwop"],
      "properties": { "openwop": { "type": "string" } },
      "additionalProperties": true
    },
    "dependencies": {
      "type": "object",
      "additionalProperties": { "type": "string" },
      "description": "Other prompt packs (or node/workflow-chain packs) whose templates this pack's templates reference (e.g., via `extends:` semantics introduced in a follow-up RFC, or via cross-pack `PromptRef.libraryId` lookups). Map of pack name → SemVer range. Install-time validation MUST resolve every entry before accepting the install; an unresolvable entry rejects with `prompt_pack_dependency_unresolvable`. Shape mirrors `workflow-chain-pack-manifest.schema.json#/properties/dependencies` (RFC 0013)."
    },
    "prompts": {
      "type": "array",
      "minItems": 1,
      "items": { "$ref": "https://openwop.dev/spec/v1/prompt-template.schema.json" },
      "description": "PromptTemplate entries (per RFC 0027 §A). Each MUST have a unique (templateId, version) pair within the pack."
    },
    "signing": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "publicKeyRef": { "type": "string" },
        "signatureRef": { "type": "string" },
        "method": { "type": "string", "enum": ["manual", "sigstore"] }
      }
    }
  }
}

Pack-kind discriminator invariant. A manifest with both prompts[] and any other top-level kind-specific array (nodes[], chains[], agents[]) MUST be rejected with pack_kind_invalid. Same posture as RFC 0013 §"Negative example (rejected at manifest validation)".

Install-time validation. When a host installs a prompt pack:

1. Verify Ed25519 signature per registry-operations.md §"Signature verification" (same flow as node/chain packs — no new crypto surface). 2. Verify SRI integrity per registry-operations.md §"Subresource Integrity" (unchanged). 3. Compile each prompts[].text against prompt-template.schema.json and assert variable-reference closure (every {{varName}} in text MUST appear in variables[] OR be a documented context key — TBD per RFC 0027 Q3). 4. Reject install with prompt_template_invalid on any of the above.

Conflict resolution. When two installed prompt packs ship the same templateId, the host MUST surface both in GET /v1/prompts with distinct meta.source.packName (extension to PromptTemplate.meta introduced in §C below). Clients disambiguate by passing the full PromptRef object form ({ libraryId, templateId, version }) — where libraryId resolves to the pack name. The stringy "prompt:templateId@version" form is rejected with prompt_ref_ambiguous when multiple packs match.

§C — Capability extension

schemas/capabilities.schema.json extends the prompts block introduced in RFC 0027 §D with three new optional fields:

   "prompts": {
     "type": "object",
     "required": ["supported"],
     "properties": {
       "supported": { "type": "boolean" },
       "templateKinds": { ... },
       "variableSources": { ... },
       "maxTemplateBytes": { ... },
       "observability": { ... },
+      "packsSupported": {
+        "type": "boolean",
+        "description": "Host installs `kind: \"prompt\"` registry packs per RFC 0028 §B. When false or absent, packs are not loaded; only host built-ins and (if `mutableLibrary`) user-created templates are visible at `GET /v1/prompts`."
+      },
+      "mutableLibrary": {
+        "type": "boolean",
+        "description": "Host honors the mutating endpoints (POST/PUT/DELETE /v1/prompts). When false or absent, those endpoints return 501. Pack-sourced and host-builtin templates are still read-only even when this is true; deletion of those is forbidden per RFC 0028 §A."
+      },
+      "library": {
+        "type": "object",
+        "additionalProperties": false,
+        "properties": {
+          "id": { "type": "string", "description": "Host's library identifier exposed at `/v1/prompts`. Single-library hosts MAY omit this." },
+          "renderEndpoint": { "type": "string", "format": "uri-reference", "description": "Absolute or relative URI of `:render` endpoint. Defaults to `/v1/prompts:render`. Useful for hosts mounting under a prefix." },
+          "maxRenderRequestBytes": { "type": "integer", "minimum": 1, "description": "Hard cap on `POST /v1/prompts:render` request body size." }
+        }
+      },
       "...": "..."
     }
   }

PromptTemplate.meta (RFC 0027 §A) gains one optional field:

   "meta": {
     "properties": {
       "author": { "type": "string" },
       "createdAt": { "type": "string", "format": "date-time" },
       "updatedAt": { "type": "string", "format": "date-time" },
-      "source": { "enum": ["host", "pack", "user"] }
+      "source": { "enum": ["host", "pack", "user"] },
+      "packName": {
+        "type": "string",
+        "description": "When `source: \"pack\"`, the installed pack's `name` (e.g., `vendor.acme.editorial-prompts`). Required when `source: \"pack\"`; absent otherwise."
+      },
+      "packVersion": {
+        "type": "string",
+        "description": "When `source: \"pack\"`, the installed pack's `version`. Required when `source: \"pack\"`; absent otherwise."
+      }
     }
   }

§D — Registry index entry

registry-operations.md §"Index format" already lists per-pack kind (per RFC 0013 §"Registry distinguishes by kind in index"). Prompt packs surface in the same /v1/index.json with:

{
  "packs": [
    {
      "name": "vendor.acme.editorial-prompts",
      "kind": "prompt",
      "latest": "1.0.0",
      "typeIds": ["writer-system", "critic-system", "editor-system"]
    }
  ]
}

For prompt packs, typeIds[] holds the pack's contributed templateIds — parallel to chain chainIds and node typeIds.

§E — Security carry-forward

  • SR-1 redaction. :render requests MUST carry secret-sourced variable values as [REDACTED:<secretId>] markers (matching RFC 0027 §E). The host resolves real values via capabilities.secrets before composition and never echoes them back in the composed response field. Conformance scenario prompt-render-secret-redaction.test.ts covers this.
  • Trust propagation. When :render request carries contentTrust: "untrusted", the response's contentTrust MUST also be "untrusted" and the composed body (if returned) MUST wrap the relevant segments in <UNTRUSTED>...</UNTRUSTED> markers. Same posture as RFC 0027 §E and RFC 0021 §E.
  • Pack signing. Reuses Ed25519 + SRI from registry-operations.md unchanged. No new signing surface. SECURITY/invariants.yaml gains no new entries — the pack-signing invariants from RFC 0003 already cover prompt packs by construction (same signature flow, same SRI digest, same trust chain).
  • Mutating-endpoint authorization. The POST / PUT / DELETE operations MUST require authentication per spec/v1/auth.md. Hosts SHOULD scope by writer role; the spec defers role-mapping to host policy.

Compatibility

Additive per COMPATIBILITY.md §2.1. All claims:

  • Existing endpoints: unchanged. The six new operations live under a new path group.
  • Existing pack kinds: unchanged. The pack-kind discriminator (kind: "node" default, kind: "workflow-chain" per RFC 0013) gains a third literal "prompt"; manifests without kind continue to validate as node packs.
  • Existing capabilities.prompts block (from RFC 0027): unchanged. Three new optional fields added.
  • Existing PromptTemplate.meta: unchanged. Two new optional fields (packName, packVersion) only populated when source: "pack".
  • Existing client SDKs: unchanged. Clients that don't call /v1/prompts* operations see no behavioral change.
  • Existing MUST requirements: not relaxed.

Hosts that don't advertise capabilities.prompts.supported: true see no behavioral change (the endpoints return 501 across the board). Hosts that advertise supported: true without packsSupported continue to serve host built-ins only.

Conformance

Five new scenarios under conformance/src/scenarios/. All gated on capabilities.prompts.supported: true; the mutating-endpoint scenario is additionally gated on mutableLibrary: true; the pack scenarios are additionally gated on packsSupported: true.

  • prompt-list-and-fetch.test.tsGET /v1/prompts returns a paginated list; GET /v1/prompts/{templateId} round-trips a known template; 404 on a fabricated templateId; ETag headers present; conditional If-None-Match returns 304.
  • prompt-render-deterministic.test.ts — Two POST /v1/prompts:render calls with identical inputs produce identical hash and variableHashes. Different variables produce different variableHashes for the relevant keys. Different contentTrust produces a different hash even with identical variables.
  • prompt-render-secret-redaction.test.ts:render with a secret-source variable bound to [REDACTED:<id>] returns a response where the composed body (under observability: "full") shows the marker preserved verbatim and never the plaintext secret value.
  • prompt-pack-install.test.ts — Install a fixture prompt pack; verify Ed25519 signature; assert listed templates appear in GET /v1/prompts with meta.source: "pack", meta.packName, meta.packVersion; assert DELETE /v1/prompts/{templateId} returns 403 for pack-sourced templates.
  • prompt-mutable-lifecycle.test.ts — Create a template via POST; fetch via GET; update via PUT and observe version bumped + prior version still accessible at the pinned path; delete via DELETE; assert 404 thereafter.

The behaviorGate helper from RFC 0023 / RFC 0027 gains two predicates: requirePromptPacksSupport() and requirePromptMutableLibrary().

A new in-tree example pack examples/packs/prompt-sample/ ships two templates (one system, one user) and proves the manifest format is implementable end-to-end. Parallel to the workflow-chain-sample pack from RFC 0013.

Alternatives considered

1. Single endpoint group, no pack kind. Ship only the REST surface; prompt distribution stays host-internal. Rejected — leaves the cross-host portability gap open. The whole rationale for separating Phase A (shape) from Phase B (surface) was to keep this RFC able to land both halves of the distribution story together; punting packs to a hypothetical RFC 0030 means hosts that adopt 0028 still can't share libraries.

2. Pack kind, no endpoint group. Ship only the pack format; clients fetch packs directly from the registry and resolve refs locally. Rejected — workflow-editor UX needs a list endpoint against the _host_ (so the picker reflects what _this_ host actually has installed), and :render is structurally a server-side operation (the host owns the composition pipeline + the secrets backend). The endpoints aren't optional for the picker UX.

3. **Reuse core.* registry namespace for prompt packs (e.g., core.prompts.writer).** Rejected — the core. namespace is reserved per node-packs.md §"Naming" for spec-authoritative artifacts. Vendor and community packs use their own prefixes; the spec ships _zero_ core. prompt packs at this RFC's acceptance time. A future RFC MAY introduce core.prompts.<name> for a canonical baseline library (e.g., a core.prompts.openwop-default-system template), but that's out of scope here.

4. Make :render a GET with query params. Rejected — variable bindings are arbitrary JSON values that don't safely encode into URLs; trust markers and secret-source variables need request-body handling. POST matches the operation-style verb used elsewhere in api/openapi.yaml (e.g., :fork, :pause, :resume).

5. Defer mutating endpoints to a separate RFC. Rejected — the wire shape is symmetric (POST mirrors GET, PUT mirrors a fresh POST, DELETE is trivial), and gating them behind mutableLibrary: false means hosts opt in selectively. Splitting them into a third RFC creates ceremony without spec-text benefit.

Unresolved questions

1. Bulk operations. Should the RFC ship POST /v1/prompts:bulkCreate and POST /v1/prompts:bulkDelete operations parallel to :bulk-cancel (existing /v1/runs:bulk-cancel)? Reference workflow-editor UX could collapse pack-import flows into one round-trip. Recommendation: defer; the loop-over-singletons approach is fine for v1.1 and ergonomics can be added in a follow-up.

2. Server-Sent Events for prompts.changed. When a host's library changes (pack installed, user template created), a long-lived editor session benefits from push notification. The existing /v1/runs/{runId}/events SSE is per-run; a global library-changes stream would be parallel. Recommendation: defer to a Phase D RFC; clients can poll GET /v1/prompts until then.

3. Pack-to-pack dependencies semantics. A prompt pack MAY declare other packs via the dependencies map (see §B schema, which mirrors the chain-pack precedent). Open question: what does it mean for a prompt pack to depend on another prompt pack — does it imply install-order, transitive resolution of cross-pack PromptRef.libraryId lookups, or eventual extends: template inheritance? This RFC ships the _field_ and the install-time MUST that every declared dependency must resolve; the _semantics_ of cross-pack composition (template inheritance, transitive dependencies closure) defer to a follow-up RFC. Reviewers may push to defer the field as well.

4. renderEndpoint URI normalization. The capabilities.prompts.library.renderEndpoint field accepts relative URIs. Should the spec require canonical absolute form, or honor the host's advertised prefix verbatim? Recommendation: honor verbatim; clients resolve relative against the discovery base URL same as elsewhere in REST endpoints.

5. Quota / rate-limit advertisement. Should capabilities.prompts.library carry per-tenant :render rate limits? The existing limits block in capabilities is for run-level quotas. Recommendation: defer; the global rate-limit middleware already exists per spec/v1/auth.md, and per-endpoint rates are operator-config, not protocol surface.

Implementation notes (non-normative)

  • Reference host apps/workflow-engine/backend/typescript MUST add a routes/prompts.ts route module mounting the six operations. Storage backend SHOULD reuse the host's existing key-value adapter (per capabilities.kv if present, or in-memory for the sample host). The pack-install path goes through the existing registry-resolver module (same as workflow-chain packs).
  • The reference host's :render implementation reuses the same Mustache substitution + variable validation utility introduced for RFC 0027's prompt.composed emission. Single codepath; less drift risk.
  • A starter example pack examples/packs/prompt-sample/ should ship with: two templates (writer-system + critic-user), one prompts/ source file per template, a signed manifest, and a README documenting how to install it locally for conformance runs.
  • Estimated effort: schemas + spec text ~1 day; reference-host endpoints ~2 days; pack install path ~1 day; example pack ~0.5 day; five conformance scenarios ~1.5 days; CHANGELOG ~30 min. Total ~6 days plus the standard Active window (or bootstrap waiver).
  • The React example app (per Part 5 of the accompanying analysis) consumes GET /v1/prompts from the new usePromptsClient.ts hook and surfaces POST /v1/prompts:render from a new "Preview composed prompt" button in src/builder/inspector/Inspector.tsx. These are non-normative implementation steps and ship separately from this RFC.

Acceptance criteria

Promotion from ActiveAccepted:

  • [x] spec/v1/prompts.md extended with §"Discovery & distribution" describing the endpoints + pack kind (cross-references this RFC).
  • [x] Prompt-pack install flow documented. (Original criterion named spec/v1/registry-operations.md §"Prompt-pack install flow" but the install flow lives canonically in spec/v1/prompts.md §"Discovery & distribution" — the registry-operations.md flow already covers the generic pack lifecycle (submission/validation/deprecation/yank/signing-key rotation) and is kind-agnostic. The prompt-pack specifics — kind: "prompt" manifest, boot-time install, meta.source: "pack" + meta.packName + meta.packVersion stamping, the ?source=pack filter on GET /v1/prompts — are in prompts.md where they're load-bearing for prompt consumers.)
  • [x] api/openapi.yaml adds the six operations under /v1/prompts* per §A — listPromptTemplates, createPromptTemplate, getPromptTemplate, updatePromptTemplate, deletePromptTemplate, renderPromptTemplate.
  • [x] schemas/prompt-pack-manifest.schema.json ships.
  • [x] schemas/capabilities.schema.json prompts block extended per §C with packsSupported, mutableLibrary, library.
  • [x] schemas/prompt-template.schema.json meta block extended per §C with packName, packVersion.
  • [x] Five new conformance scenarios per §"Conformance" land in @openwop/openwop-conformanceprompt-list-and-fetch.test.ts, prompt-mutable-lifecycle.test.ts, prompt-render-deterministic.test.ts, prompt-pack-install.test.ts, prompt-end-to-end-events.test.ts. Conformance suite minor bumped to @openwop/openwop-conformance@1.4.0 (2026-05-22).
  • [x] In-tree example pack examples/packs/prompt-sample/ ships, install-verifies under the reference host's registry resolver.
  • [x] CHANGELOG entry under [Unreleased].
  • [ ] INTEROP-MATRIX.md extended with rows for capabilities.prompts.packsSupported and capabilities.prompts.mutableLibrary, alongside the RFC 0027 base row. (Will land alongside the first non-steward advertisement.)
  • [x] Reference host (apps/workflow-engine/backend/typescript) advertises capabilities.prompts.packsSupported: true and mutableLibrary: true, passes all five new conformance scenarios.
  • [ ] First non-steward host advertises capabilities.prompts.supported: true AND packsSupported: true (third-party validation gate per RFC 0001). MAY be waived under bootstrap-phase waiver. (Path-to-Accepted.)

References

  • RFCS/0027-prompt-templates.md — Phase A wire shape this RFC's endpoints and pack kind serve.
  • RFCS/0003-agent-packs.mdkind: "node" pack baseline + Ed25519 signing + SRI integrity.
  • RFCS/0013-workflow-chain-packs.mdkind: "workflow-chain" precedent for adding a new pack kind alongside node packs.
  • RFCS/0021-ai-envelope-primitive.md — shape-first-then-surface precedent.
  • spec/v1/registry-operations.md — pack-install signature + SRI flow, reused unchanged.
  • spec/v1/rest-endpoints.md — REST URL conventions (:render, :bulk-* operation-style verbs).
  • api/openapi.yaml — operation definitions this RFC extends.
  • RFCS/0029-prompt-override-hierarchy.md (forthcoming) — Phase C agent-scoped override hierarchy that consumes this RFC's endpoints.
  • RFCS/0030-envelope-reasoning-and-tier-one-subset.md (forthcoming, parallel track) — Tier-1 structured-output compatibility subset (informative) — non-binding portability guidance authors of PromptTemplate.modelHints.envelopeType MAY cross-check when picking envelope kinds for cross-vendor portability.

Status history

Draft → Active (2026-05-20)

Promoted under the bootstrap-phase steward waiver per CONTRIBUTING.md §"Bootstrap-phase notes" + MAINTAINERS.md §"Bootstrap-phase RFC waivers". Same posture RFCs 0021–0027 and 0030 used in this release.

Evidence at promotion (spec text + wire shape + reference-host endpoints all locked; remaining acceptance criterion — first non-steward host advertisement — defined as the path to Accepted):

  • RFC text: §A (REST surface — 6 operations), §B (prompt-pack kind + signing + install-time validation), §C (mutating endpoints + 403/501 semantics).
  • Spec text: spec/v1/prompts.md §"Discovery & distribution" extended with the routing table; spec/v1/registry-operations.md extended with the prompt-pack install flow.
  • OpenAPI / schemas: 6 new operations under /v1/prompts* (listPromptTemplates, createPromptTemplate, getPromptTemplate, updatePromptTemplate, deletePromptTemplate, renderPromptTemplate) in api/openapi.yaml. schemas/prompt-pack-manifest.schema.json (NEW). schemas/capabilities.schema.json prompts block extended with endpointsSupported, mutableLibrary, library. All under additionalProperties: false.
  • Reference host: routes/prompts.ts serves all 6 routes against the in-memory PromptStore (host/promptStore.ts). host/promptPackLoader.ts (commit 4c2fe40, hardened in f70cfb6) scans examples/packs/* + OPENWOP_PROMPT_PACKS_DIR, validates manifests against prompt-pack-manifest.schema.json via Ajv2020, verifies Ed25519 signatures when the manifest carries a signing block, and installs templates via promptStore.installPackTemplates() with the canonical meta.source: "pack" + meta.packName + meta.packVersion provenance stamps. In-tree vendor.openwop.prompt-sample reference pack auto-installs at boot.
  • Conformance: prompt-list-and-fetch.test.ts, prompt-render-deterministic.test.ts, prompt-mutable-lifecycle.test.ts, prompt-pack-install.test.ts cover the REST surface + pack-source provenance + CRUD lifecycle. All capability-gated via behaviorGate('prompts-endpoints', ...) / behaviorGate('prompts-mutable', ...).
  • CHANGELOG.md: [1.1.2 — unreleased] entries cover the REST surface, pack-install flow, conformance scenarios.

Path to Active → Accepted: first non-steward host advertises capabilities.prompts.endpointsSupported: true AND mutableLibrary: true AND serves the six routes. MAY be waived under the bootstrap-phase waiver if the steward provides a public conformance run pointing at the advertised endpoint.

Active → Accepted (2026-05-25)

MyndHyve workflow-runtime advertises capabilities.prompts.{packsSupported: true, mutableLibrary: true, library: {id: "myndhyve-system", renderEndpoint: "/v1/prompts:render", maxRenderRequestBytes: 65536}} live on https://myndhyve.ai/.well-known/openwop (verified 2026-05-25 via direct curl). Tier-2 path-to-Accepted criterion met at the wire under the bootstrap-phase rule (advertisement + scenarios pass-modulo-honest-skip). A parallel session on the openwop side shipped the host-side kind: "prompt" pack ingest + packsSupported: true advertise + the real library block end-to-end during the same conformance window that closed RFC 0041.

Post-promotion notes (2026-05-25)

Within hours of the Tier-2 promotion, MyndHyve's own /code-review of their handler implementation surfaced a CRITICAL: their POST/PUT/DELETE /v1/prompts* handlers accepted an arbitrary workspaceId from the request body and wrote to workspaces/{wsId}/prompts/{templateId} via the Firebase Admin SDK — which bypasses Firestore security rules. Any authenticated user with a valid Firebase ID token could have inserted a user-template into a workspace they don't belong to. Live window of vulnerability: ~2 hours (12:31 UTC → 14:27 UTC) on revision workflow-runtime-00207-vzq; hotfix shipped as commit 26aa0a191, deployed as revision workflow-runtime-00208-km5. Wire shape unchanged before/after the hotfix; RFC 0028 promotion stood.

The disclosure surfaced a corresponding openwop spec gap: this RFC + spec/v1/prompts.md §"Discovery & distribution" §"Authorization" originally said only "Hosts SHOULD scope by writer role; the spec defers role-mapping to host policy" — implicit on the workspace-membership-on-writes contract that the MyndHyve hotfix added. The spec gap was closed by a follow-up docs-sync commit that:

  • Strengthened spec/v1/prompts.md §"Discovery & distribution" §"REST endpoints" with a normative §"Workspace membership on workspace-scoped writes" paragraph requiring hosts to verify the authenticated principal's workspace membership BEFORE honoring any mutating write — and explicitly calling out that hosts authenticating against an identity provider while persisting via a database vendor's privileged admin client (Firebase Admin SDK, Supabase service-role key, equivalent) MUST replicate the membership check at the application tier rather than relying solely on the database vendor's row-level security rules, since privileged admin clients bypass those rules.
  • Added a new protocol-tier SECURITY invariant prompt-mutation-workspace-membership-enforced in SECURITY/invariants.yaml pointing at the canonical conformance scenario.
  • Added the conformance scenario prompt-mutation-workspace-membership-enforced.test.ts, gated on capabilities.prompts.mutableLibrary: true. The probe drives POST /v1/prompts with a random (cryptographically-unique) non-member workspaceId and asserts the host refuses (any 4xx/5xx is acceptable; a 2xx silent write is the failure mode this invariant guards against). Operator may override the workspace via OPENWOP_TEST_NONMEMBER_WORKSPACE_ID for hosts that need a specific synthetic workspace shape.

The MyndHyve adopter pattern (Admin-SDK writes bypass DB-tier auth while caller-supplied workspaceId is trusted) is common across Firebase / Supabase / RLS-style stacks; the application-tier MUST is now spec-enforced rather than implementation-assumed.

Post-promotion tightening (2026-05-25, T1 + T2)

Two same-day tightenings landed in response to MyndHyve's green-light relay confirming the spec sharpening — combined into one commit since they share the threat model, the normative paragraph location, and the conformance-gate pattern:

  • T1 — canonical envelope on 403 for both the mutation scenario and the new read scenario. spec/v1/rest-endpoints.md §"Common error codes" gains a new workspace_membership_required error code pointing back at the prompts.md normative paragraph. The conformance scenarios assert error === "workspace_membership_required" ONLY when the host's refusal status is 403; other refusal codes (401 if the host treats the failure as authentication-level, 404 for existence-disclosure avoidance, 5xx on infra failure) have their envelope shape unconstrained. Canonical-on-403 is a strengthening, not a forced upgrade — hosts that prefer other status codes remain conformant.
  • T2 — read-side sister scenario. The normative paragraph in prompts.md was extended from §"Workspace membership on workspace-scoped writes" to §"Workspace membership on workspace-scoped reads and writes". Read paths are NOT exempt just because they don't write — a GET /v1/prompts?workspaceId=<not-mine> that returns another workspace's templates is a cross-tenant data leak with the same blast radius as a cross-tenant write. New conformance scenario prompt-read-workspace-membership-enforced.test.ts gates on capabilities.prompts.supported: true (broader than mutableLibrary per MyndHyve's preferred Option B gating — read-only hosts that expose ?workspaceId= reads aren't exempt from the symmetric authz invariant). The scenario self-skips hosts that don't recognize ?workspaceId= via response-shape detection (a 200 without the documented templates[] field is interpreted as "no workspace-scoped read surface, out of scope"). On 200 WITH templates: [] it PASSes (correct null result for a random nonexistent workspace); on 200 WITH non-empty templates[] it FAILs as a cross-tenant leak. Sister SECURITY invariant prompt-read-workspace-membership-enforced added.

MyndHyve adoption note (the "why this MUST exists" rationale, paraphrased from their disclosed corrected comment block): mutating + workspace-scoped read prompt endpoints write via the Firebase Admin SDK, which BYPASSES Firestore security rules. A requireWorkspaceMember middleware MUST run before any write or workspace-scoped read — it reads workspaces/{wsId}/members/{uid} directly as the authorization check, NOT as a rules-evaluated query. A previous version trusted the workspaceId field in the request body without an explicit membership check, on the (false) assumption that Firestore rules would gate the access. Admin SDK bypasses rules. The middleware closes that gap. The same anti-pattern applies to Supabase service-role keys, Convex-equivalent admin clients, and raw Postgres with a server-side connection that doesn't go through SET ROLE. The application-tier check is mandatory; the DB-tier rules are defense-in-depth below it.

MyndHyve confirms workflow-runtime-00208-km5 passes both probes (write + read) with no code changes — both paths already return 403 with error: "workspace_membership_required". The canonical envelope on 403 is the de-facto contract; the spec ratifies it.

Honest correction (2026-05-25, post-T1+T2). MyndHyve's reference-artifact relay that drove the T1 canonicalization claimed the host already emitted the workspace_membership_required envelope on 403. That claim was incorrect: pre-fix, the host's AUTH_CODE_TO_WIRE mapping translated the internal WORKSPACE_MEMBERSHIP_DENIED enum to the generic 'forbidden' wire code (shared with several other "kinda-forbidden" failure modes), so the actual 403 wire envelope was {"error": "forbidden", "message": "You are not a member of this workspace", ...}. The mismatch surfaced via MyndHyve's own subsequent /code-review — the same self-review discipline that surfaced the original CRITICAL — and was fixed in their commit 61993c85b by narrowing WORKSPACE_MEMBERSHIP_DENIED → 'workspace_membership_required' while leaving the narrower WORKSPACE_MEMBERSHIP_CHECK_FAILED (Firestore read of the members doc throws — infrastructure error, not a membership-denied semantic) mapped to 'forbidden' for fail-closed posture. Same commit also closed three sibling findings: a real memory leak in the GET-side resolveQueryWorkspaceId helper (the previous Promise-wrapping pattern leaked the request closure per failed-auth call; replaced with a standard Express conditional middleware), a misleading JSDoc that still claimed "Workspace membership verified via Firestore rules on write" (the same false reassurance pattern that drove the original CRITICAL), and the LIST_MAX_TOTAL_ENTRIES cap enforced during accumulation rather than only post-pass. Crucially: an integration test that mounts the full middleware chain on an ephemeral-port Express app and asserts the 403 envelope shape directly was added in the same commit — without that test, the regression that hid the original wire-code mismatch wouldn't have been caught at unit-test time.

This correction is recorded here for the same reason the INTEROP-MATRIX records the registerHostSampleRoutes honest-correction note (where MyndHyve disclosed that a hostnamed test seam had been 404'ing in production for prior conformance runs): protocol-credibility is built on adopters surfacing their own mistakes alongside the spec contract that's correct on first principles. The T1 canonicalization (workspace_membership_required as the canonical 403 envelope per rest-endpoints.md §"Common error codes") was the right move regardless of the adopter's incorrect reference artifact — it's more specific than the generic 'forbidden', gives operators and adopters a clear discriminator, and matches the threat-model framing extended to Supabase / Convex / RLS-bypass stacks in the normative paragraph. MyndHyve aligned to the spec rather than the other way around — the spec sharpened first, the adopter corrected to it after. That's the right direction for a spec contract that an ecosystem of implementations will pull from.