OpenWOP openwop.dev

Status: Stable · v1.1 (2026-05-22). Normative spec for conformance-only host-sample test seams under /v1/host/sample/*. Keywords MUST, SHOULD, MAY follow RFC 2119. See auth.md for the status legend.

OpenWOP's conformance suite verifies behavioral contracts that v1 cannot probe through the production wire surface alone. Examples:

  • "the prompt resolution chain layered correctly" can be observed end-to-end via prompt.composed event payloads, but isolating layer-by-layer precedence requires a synchronous resolver endpoint
  • "the LLM cache-key recipe produced byte-identical output across hosts" can only be asserted if hosts expose their canonicalize → SHA-256 → hex computation
  • "OTel span attributes don't carry BYOK canaries" requires an introspection endpoint scoped to a run

These contracts ship as conformance-only test seams under the host-extensions.md §"Canonical prefixes" namespace /v1/host/sample/*. They are NOT part of the v1 wire surface — production hosts SHOULD return 404 or 403 from these seams unless an env-gate (named per-seam below) is set.

This doc is the canonical reference for the test-seam contracts. Per-seam normative content also appears in the RFC + spec doc that introduces the seam; this doc is the consolidated index hosts implement against.

Capability advertisement (normative)

Hosts that expose any test seam MUST advertise it under /.well-known/openwop per capabilities.md. The advertising flags are tabulated below per seam. Conformance scenarios capability-gate on the matching flag; hosts that don't advertise skip cleanly.

Test seams

1. POST /v1/host/sample/prompt/resolve — Prompt resolution chain (RFC 0029)

FieldValue
Method + pathPOST /v1/host/sample/prompt/resolve
Capability gatecapabilities.prompts.supported: true
Env gate (reference impl)seam registered when capabilities.prompts.supported is asserted
IntroducedRFC 0029 §C

Request body:

{
  kind: 'system' | 'user' | 'few-shot' | 'schema-hint',
  node: {
    nodeId: string,
    config?: {
      systemPromptRef?: string | PromptRef,
      userPromptRef?: string | PromptRef,
      schemaHintPromptRef?: string | PromptRef,
      fewShotPromptRefs?: Array<string | PromptRef>,
      agentId?: string,
    },
  },
  agentManifest?: {
    agentId: string,
    systemPrompt?: string,
    systemPromptRef?: string,
    promptOverrides?: Partial<Record<PromptKind, string | PromptRef>>,
    promptLibraryRef?: string,
  },
  workflowDefaults?: { promptRefs?: Partial<Record<PromptKind, string | PromptRef>> },
  hostDefaults?: Partial<Record<PromptKind, string | PromptRef>>,
  agentBindingsSupported?: boolean,    // overrides capabilities.prompts.agentBindings for this probe
}

Response body:

{
  resolved: string | null,                                                      // rendered prompt text after variable substitution, or null if all 4 layers yielded null
  resolvedAt: 'node' | 'agent-intrinsic' | 'workflow' | 'host' | null,          // which layer won
  chain: Array<{                                                                 // every layer attempted, in priority order
    layer: 'node' | 'agent-intrinsic' | 'workflow' | 'host',
    ref: string | null,
    resolved: string | null,
  }>,
}

Hosts that advertise capabilities.prompts.supported: true MUST serve this seam with the documented shape. The chain[] array MUST list every layer attempted even when an earlier layer wins — conformance scenarios assert the full traversal record.

Conformance: prompt-resolution-chain-{node-wins,agent-intrinsic,fallback-cascade}.test.ts.

Production-path equivalent (preferred). The same layer-by-layer precedence record is carried by the durable agent.promptResolved event (schemas/run-event-payloads.schema.json agentPromptResolved — a REQUIRED chain[] with one applied: true entry + the full-traversal MUST). A host that emits the event is provable black-box via prompt-resolution-chain-event.test.ts, which creates a run and reads chain[] from the NORMATIVE GET /v1/runs/{runId}/events/poll endpoint — no seam. This synchronous seam remains the convenience for hosts that have not yet wired event emission (RFC 0029 staging); the production-path event is what graduates RFC 0029 prompt-chain precedence into the openwop-core-standard floor.

2. GET /v1/host/sample/test/otel/spans?runId=<id> — OTel span scrape (RFC 0034)

FieldValue
Method + pathGET /v1/host/sample/test/otel/spans?runId=<id>
Capability gatecapabilities.observability.testSeams.otelScrape: true
Env gate (reference impl)OPENWOP_TEST_OTEL_SCRAPE=true
IntroducedRFC 0034 §B

Returns recorded OTel spans for the named run. When otelScrape: true, the host MUST return 200 OK with body:

{
  spans: Array<{
    name: string,                                  // span name, e.g., "openwop.run", "openwop.dispatch"
    attributes: Record<string, unknown>,           // span attributes including any openwop.*-prefixed keys
    events: Array<{ name: string, attributes?: Record<string, unknown> }>,
  }>,
}

The spans[] array MUST include every span produced by the host's instrumentation for the named run, including any openwop.*-prefixed attributes added to span context. Hosts MAY redact span content using the canonical [REDACTED:<secretId>] marker per agent-memory.md §"SR-1 secret-redaction invariant" — that's the contract conformance tests.

The seam graduates two SECURITY invariants from reference-impl to protocol tier:

  • secret-leakage-otel-attribute — BYOK plaintexts MUST NOT appear as values on any openwop.* OTel attribute
  • (paired) secret-leakage-debug-bundle-otel — same invariant on debug-bundle exports

Conformance: envelope-reasoning-secret-redaction.test.ts (capability-gated on the seam).

3. POST /v1/host/sample/test/debug-bundle/export — Debug-bundle export probe (RFC 0034)

FieldValue
Method + pathPOST /v1/host/sample/test/debug-bundle/export
Capability gatecapabilities.observability.testSeams.debugBundleExport: true
Env gate (reference impl)OPENWOP_TEST_DEBUG_BUNDLE_EXPORT=true
IntroducedRFC 0034 §B

Synchronous debug-bundle export for conformance scenarios that need to assert canary redaction without first triggering an interrupt → debug bundle workflow.

Request body:

{
  runId: string,
}

Response body: same shape as GET /v1/runs/{runId}/debug-bundle per spec/v1/debug-bundle.mdDebugBundle with bundleVersion, host, run, events, redactionMode, redactionApplied, truncated, truncatedReason.

When advertised, the host MUST serve a 200 OK with the documented shape.

Conformance: gates on capabilities.observability.testSeams.debugBundleExport: true.

4. POST /v1/host/sample/test/llm-cache-key — LLM cache-key recipe (RFC 0041; recipe v2 per RFC 0150 §C)

FieldValue
Method + pathPOST /v1/host/sample/test/llm-cache-key
Capability gatecapabilities.multiAgent.executionModel.replayDeterminism.supported: true (RFC 0041 Phase 4 hosts); MAY be implemented earlier without advertising
Env gate (reference impl)implicit — seam registered alongside the cache-key implementation
IntroducedRFC 0041 §A. Re-pointed 2026-08-16 to the RFC 0150 §C v2 recipe (suite 1.109.0) — this section and both driving scenarios had kept the retired v1 field set for three days after replay.md replaced it in place, so the suite contradicted itself and a host that computed the current recipe went red for being right.

Computes the canonical LLM cache key per replay.md §"LLM cache-key recipe" §A + §B — the v2 semantic request (recipe: "openwop-semantic-request-v2", RFC 0150 §C). Conformance scenarios drive the seam to assert (a) intra-host reproducibility, (b) transport-only-field invariance, (b′) outcome-affecting-field sensitivity (seed, stop, maxOutputTokens DO change the key; providerOptions are carried), and (c) cross-host parity when two hosts both expose the seam.

Request body — an LLMCacheKeyInput-shaped object per replay.md §A (v2). Transport-only fields are accepted and ignored; outcome-affecting fields are part of the recipe (the tests exercise both directions):

{
  // Recipe fields (per replay.md §A, v2 — these influence the key):
  provider: string,                                  // canonical provider id, lowercase ASCII
  model: string,                                     // provider-stamped model id
  messages: Array<{ role, content, name?, toolCallId? }>,
  tools?: Array<{ name, description?, parameters }>,
  temperature?: number,
  topP?: number,
  topK?: number,
  responseFormat?: { type: 'text' | 'json' | 'tool_call', schema? },
  maxOutputTokens?: number,                          // RFC 0150 §C — decides truncation
  stop?: string[],                                   // RFC 0150 §C — decides where generation halts
  seed?: number,                                     // RFC 0150 §C — exists to change the output
  safetySettings?: Record<string, unknown>,          // RFC 0150 §C — any policy that can alter output
  providerOptions?: Record<string, unknown>,         // RFC 0150 §C — carried, never dropped (`vendor.<provider>.<option>`)

  // Transport-only fields (host MUST ignore for key computation):
  stream?: boolean,
  metadata?: Record<string, unknown>,
  user?: string,
  'x-request-id'?: string,
  traceparent?: string,
  tenantId?: string,
  runId?: string,
  timeoutMs?: number,
  // ... any other transport/bookkeeping field
}

Response body:

{
  cacheKey: string,    // 64 lowercase-hex chars (SHA-256 of JCS(projectSemanticRequestV2(input)))
}

Hosts MUST:

1. Build the v2 canonical object { recipe: "openwop-semantic-request-v2", provider, model, request: { … }, providerOptions? } — drop transport-only fields, keep every outcome-affecting one, place unknown provider options under providerOptions (replay.md §B step 1) 2. Canonicalize per replay.md §B (RFC 8785 JCS: sorted keys recursively, no whitespace, preserve array order; no Unicode normalization outside JCS — an earlier revision of this section said "UTF-8 NFC strings", which replay.md §B now explicitly forbids) 3. Return SHA-256 over the canonical bytes as lowercase hex

A missing or malformed provider/model/messages field MUST return 400 invalid_argument.

Conformance: replay-llm-cache-key.test.ts, replay-llm-cache-key-portable.test.ts.

5. Staged-refusal seam — POST /v1/host/sample/test/mock-ai/program mode refusal (RFC 0041 §B)

FieldValue
Method + pathPOST /v1/host/sample/test/mock-ai/program
Capability gatecapabilities.multiAgent.executionModel.replayDeterminism.refusalDivergenceEmission: true (RFC 0041 Phase 4)
Env gate (reference impl)OPENWOP_TEST_SEAM_ENABLED=true
IntroducedRFC 0041 §B; reuses the existing mock-AI program seam introduced by RFC 0032 §C

The replay.divergedAtRefusal behavioral assertion requires staging the mock-AI provider to return a valid envelope on the original run and a refusal on the replay (or vice-versa). Phase 4 hosts that advertise refusalDivergenceEmission: true MUST honor the following program shape on POST /v1/host/sample/test/mock-ai/program:

{
  nodeId: string,
  program: [
    { mode: 'envelope', envelope: { /* valid LLM envelope */ } },     // original run gets this
    { mode: 'refusal', refusalReason: string },                        // replay gets this
  ],
}

The host's mock-AI provider MUST honor the program deterministically by attempt index: the first call (original run) returns the first entry; the second call (replay) returns the second entry. The seam is callable BEFORE the run is created. The program is stored against the bare nodeId — there is no run, workflow, or tenant in the key — so a node id is the only unit of isolation this seam has. Two fixtures that declare the same node id therefore share one program, and a suite that runs scenario files in parallel will let them overwrite each other mid-run; the scenario that loses fails an assertion about its own subject rather than about the collision. Every fixture node that dispatches to the mock provider MUST be uniquely named across conformance/fixtures/. conformance/scripts/check-mock-ai-node-ids-unique.mjs enforces this.

When the replay's mock-AI call hits the refusal entry, the host MUST:

1. Emit a replay.divergedAtRefusal event with payload per schemas/run-event-payloads.schema.json §replayDivergedAtRefusal 2. Fail the replay with HTTP 422 + error.code: "replay_diverged_at_refusal"

Conformance: replay-divergence-at-refusal.test.ts (advertisement-shape probe lives now; the 2 behavioral it.todo assertions light up when this seam is wired).

6. Multi-region idempotency simulator — POST /v1/host/sample/test/multi-region/simulate-partition (RFC 0036 §C)

FieldValue
Method + pathPOST /v1/host/sample/test/multi-region/simulate-partition
Capability gatecapabilities.idempotency.multiRegion.supported: true OR capabilities.idempotency.crossRegion ∈ {reconciled-records, fenced-effects} (RFC 0036, revised by RFC 0150 §D)
Env gate (reference impl)OPENWOP_TEST_MULTI_REGION_SIMULATOR=true
IntroducedRFC 0036 §C — closes the CF-12 / OPS-5 multi-region simulation gap named in docs/KNOWN-LIMITS.md

The convergence rule in spec/v1/idempotency.md §"Multi-region idempotency annex" §"Convergence rule" is a pure-function MUST: given ≥2 conflicting ConflictClaim records sharing (tenantId, endpoint, key), the resolver MUST return the lex-min runId as the winner deterministically without coordination. This seam exposes that algorithm directly so conformance can mechanically verify the property against synthetic partitions (no actual multi-region replication required).

Request:

{
  claims: Array<{
    runId: string,       // engine-assigned id; lex-sort determines winner
    tenantId: string,    // claims with different tenantId MUST be rejected (400)
    endpoint: string,    // claims with different endpoint MUST be rejected (400)
    key: string,         // claims with different key MUST be rejected (400)
    region: string,      // identifies which region produced this claim
  }>  // length ≥ 2; length < 2 MUST be rejected (400)
}

Response (200 OK):

{
  winner: ConflictClaim,                                     // lex-min runId
  losers: ConflictClaim[],                                    // N-1 entries
  cacheRedirects: Array<{                                     // N entries (one per region)
    region: string,
    cacheKey: string,                                         // `${endpoint}:${key}`
    redirectToRunId: string,                                  // winner.runId
  }>,
  loserCancelReason: 'cross_region_dedup_loss',               // canonical literal
}

Idempotency: the resolver is a pure function with no side effects. Same inputs → same outputs across calls. Hosts MAY cache results but the seam itself doesn't persist state.

Conformance: multi-region-idempotency-behavior.test.ts (6 assertions covering lex-min winner, multi-region cache redirects, canonical cancel reason, order-invariance, and 400-on-tuple-mismatch).

7. Cross-engine append-ordering harness — POST /v1/host/sample/test/cross-engine/{append,read,reset} (RFC 0036 §B)

FieldValue
Method + path3 endpoints (see below)
Capability gatecapabilities.eventLog.crossEngineOrdering.supported: true (RFC 0036 §B)
Env gate (reference impl)OPENWOP_TEST_CROSS_ENGINE_HARNESS=true
IntroducedRFC 0036 §B — closes the CF-8 cross-engine append-ordering gap named in docs/KNOWN-LIMITS.md

The cross-engine ordering invariant in spec/v1/channels-and-reducers.md §"Cross-engine ordering" requires that two engine instances writing to the same shared channel converge to a single globally-ordered linearization on read. This seam exposes a synthetic two-engine harness so conformance can verify the property without standing up two real engine instances.

Endpoints:

POST /v1/host/sample/test/cross-engine/append
  Body: { engineId: string, channelId: string, value: unknown, lamport?: number }
  Returns: { engineId, value, lamport, seq } — the assigned timestamp + sequence

GET  /v1/host/sample/test/cross-engine/read?channelId=<id>
  Returns: { entries: AppendEntry[] } — linearized by (lamport, engineId, seq)

POST /v1/host/sample/test/cross-engine/reset
  Body: {}
  Returns: { ok: true } — clears the in-memory log

Lamport-clock semantics (the host's advertised orderingModel: 'lamport'):

  • Each append advances the engine's clock to max(local, incoming) + 1
  • The lamport? field on append is the engine's view of the OTHER engine's clock (incoming hint); honored per the lamport receive rule
  • read linearizes by (lamport ASC, engineId ASC, seq ASC) — a deterministic total order
  • Hosts advertising a different orderingModel (vector-clock, global-sequencer, or x-host-<host>-<key>) MAY substitute their own algorithm but MUST honor the same append/read/reset contract

Conformance: cross-engine-append-behavior.test.ts (4 assertions covering global linearization, lamport monotonicity, receive-rule advancement, and read-determinism).

8. Sandbox MVP — POST /v1/host/sample/test/sandbox-{load,invoke} (RFC 0035)

FieldValue
Method + path2 endpoints (see below)
Capability gatecapabilities.sandbox.supported: true (RFC 0035 §A)
Env gate (reference impl)OPENWOP_TEST_SANDBOX_MVP=true
IntroducedRFC 0035 §B — exercises the 8 sandbox failure-mode invariants against a synthetic misbehaving-pack registry

The sandbox seam exists so conformance can drive the §B failure-mode invariants without a real pack runtime + real misbehaving pack tarballs. Each sandbox-invoke request names a synthetic typeId from the host's pre-populated misbehaving-pack registry; the host executes the matching code body inside its sandbox and returns either the result or a typed error envelope per host-capabilities.md §"Error codes".

Endpoints:

POST /v1/host/sample/test/sandbox-load
  Body: { packId: string }
  Returns: 200 { ok: true, packId } | 400 validation_error | 404 sandbox_pack_not_found

POST /v1/host/sample/test/sandbox-invoke
  Body: {
    typeId: string,                       // e.g. 'misbehave.fs-escape-read'
    args?: Record<string, unknown>,       // available as `args` inside the sandboxed code
    packId?: string,                      // identifies the pack containing typeId
    allowedHostCalls?: string[],          // capability-gate whitelist for this invocation
  }
  Returns: 200 { result: unknown } | 200 { error: SandboxError }

SandboxError shape (canonical per host-capabilities.md §"Error codes"):

{
  code:
    | 'sandbox_escape_attempt'      // forbidden-syscall escape (fs/env/network/process)
    | 'sandbox_capability_denied'   // host call not in allowedHostCalls
    | 'sandbox_memory_exceeded'     // memoryLimitBytes overflow
    | 'sandbox_timeout'             // wallClockLimitMs overflow
    | 'sandbox_invocation_error',   // fallback for thrown errors not in the canonical catalog
  details: {
    escapeKind?:                     // SET when code === 'sandbox_escape_attempt'
      | 'host-fs-escape'
      | 'host-env-leak'
      | 'network-escape'
      | 'host-process-escape',
    requestedCapability?: string,    // REQUIRED when code === 'sandbox_capability_denied'
    requestedBytes?: number,         // MAY appear when code === 'sandbox_memory_exceeded'
    message: string,
  },
}

Synthetic misbehaving-pack typeIds the conformance suite exercises:

typeIdFailure mode it probes
misbehave.fs-escape-readsandbox_escape_attempt + escapeKind: host-fs-escape
misbehave.fs-escape-writesandbox_escape_attempt + escapeKind: host-fs-escape
misbehave.env-leaksandbox_escape_attempt + escapeKind: host-env-leak
misbehave.network-escapesandbox_escape_attempt + escapeKind: network-escape
misbehave.process-escapesandbox_escape_attempt + escapeKind: host-process-escape
misbehave.timeoutsandbox_timeout
misbehave.memory-bombsandbox_memory_exceeded
misbehave.cross-pack-mutate(no failure; result.shared MUST equal 1 on every invocation — cross-pack mutation MUST NOT leak across fresh contexts)
misbehave.capability-gate-violationsandbox_capability_denied + details.requestedCapability
well-behaved.echo(no failure; result.echoed === args.input)
well-behaved.host-fetch(no failure when allowedHostCalls includes 'fetch')

Conformance: sandbox-mvp-behavior.test.ts (10 assertions covering 5 escape kinds + timeout + memory + cross-pack isolation + capability-gate + 2 well-behaved baselines).

9. Workspace cross-owner driver — POST /v1/host/sample/workspace/op (RFC 0059)

FieldValue
Method + pathPOST /v1/host/sample/workspace/op
Capability gatecapabilities.workspace.supported: true (RFC 0059 §A)
Env gate (reference impl)none (the in-memory host enables it unconditionally; production hosts gate per the §"Production safety" rule below)
IntroducedRFC 0059 §E — drives host.workspace CRUD against an EXPLICIT {tenant, workspace} owner so the workspace-cross-tenant-isolation (WCT-1) invariant is exercisable on a single-credential host (mirrors the blob/kv/queue/table cross-tenant seams)

The production §C endpoints (/v1/host/workspace/files) bind every request to one authenticated owner, so a single-credential host cannot demonstrate cross-owner isolation through them. This seam takes the {tenant, workspace} owner in the body — letting a conformance scenario write as owner A and attempt a read as owner B — and routes through the SAME owner-scoped store the §C endpoints use. The host MUST still scope strictly by the supplied owner triple (WCT-1); the seam only supplies the triple that production resolves from the authenticated identity.

POST /v1/host/sample/workspace/op
  Body: {
    tenant: string,            // owner tenant (RFC 0048)
    workspace: string,         // owner workspace
    op: 'list' | 'get' | 'put' | 'delete',
    path?: string,             // required for get/put/delete
    content?: string,          // required for put
    contentType?: string,      // optional for put
    ifMatch?: string,          // optional optimistic-concurrency token for put
    prefix?: string,           // optional filter for list
    version?: number,          // optional historical read for get
  }
  Returns: the same body/status as the matching §C endpoint
           (200 WorkspaceFile | 200 { files } | 204 | 404 not_found
            | 409 workspace_conflict | 413 workspace_too_large)

Conformance: workspace-cross-tenant-isolation.test.ts (WCT-1 — write as owner A, then assert a different workspace AND a different tenant both fail closed on get/list, while the owner still reads its own file).

10. Connection-pack install/resolve/consent driver — POST /v1/host/sample/connection-packs/{install,resolve,consent-plan} (RFC 0095)

FieldValue
Method + pathPOST /v1/host/sample/connection-packs/install · POST /v1/host/sample/connection-packs/resolve · POST /v1/host/sample/connection-packs/consent-plan · POST /v1/host/sample/connection-packs/egress-check (RFC 0120)
Capability gatecapabilities.connections.packsSupported: true (RFC 0095 §C)
Env gate (reference impl)seam registered when connections.packsSupported is asserted; production hosts gate per §"Production safety"
IntroducedRFC 0095 §Conformance — drives connection-packs.md §Manifest clauses 2/4/6/8 black-box on hosts whose install path is otherwise boot-time or publish-time

Connection packs install through host-specific channels (a boot-time loader on the reference app; a registry publish path on other hosts), so the §Manifest clause 2/6/8 behaviors need a uniform driver for black-box conformance. The seams route through the SAME validation + resolution code paths the host's production install channel uses; they only supply the manifest (and, for resolve, an optional simulated built-in) that production sources elsewhere.

POST /v1/host/sample/connection-packs/install
  Body: { manifest: <connection-pack manifest JSON> }
  Returns: 200 {
    installed: boolean,
    errors?: Array<{ code: string, path?: string }>,
    //          code ∈ connection_pack_credential_material | pack_kind_invalid
    //                 | schema-validation identifiers (host-specific)
  }

POST /v1/host/sample/connection-packs/resolve
  Body: {
    provider: string,                 // the RFC 0045/0047 provider id
    simulateBuiltinVersion?: string,  // optional: behave as if a built-in
                                      // definition of `provider` at this
                                      // version existed (SemVer §11 probe)
  }
  Returns: 200 {
    resolved: boolean,
    source?: 'pack' | 'builtin',
    version?: string,
    code?: 'connection_provider_unresolved' | 'connection_provider_conflict',
  }

POST /v1/host/sample/connection-packs/consent-plan
  Body: { provider: string, requested: Array<'read' | 'write'> }
  Returns: 200 {
    steps: Array<{
      groups?: Array<{ key: string, access: 'read' | 'write' }>,
      includesWrite?: boolean,
    }>,
  }

POST /v1/host/sample/connection-packs/egress-check      // RFC 0120
  Body: {
    provider: string,        // an installed pack's provider id
    requestHost: string,     // the destination host a credential-bearing
                             // RFC 0045 connector egress would target
  }
  Returns: 200 {
    allowed: boolean,        // true IFF requestHost matches provider.apiHosts
    code?: 'host_not_allowed' | 'connection_provider_unresolved',
  }

The install seam MUST run the clause-2 credential-material scan BEFORE generic schema validation (the specific code wins); a rejected manifest is NOT installed and MUST NOT disturb other installed packs (clause 8). The resolve seam applies the clause-6 precedence rule (installed ≥ built-in per SemVer §11, else connection_provider_conflict). The consent-plan seam returns the host's planned consent sequence; write groups MUST occupy a separate step from the initial read authorization (clause 4). The egress-check seam (RFC 0120) reports the host's credential-egress allow-list decision for (provider, requestHost): allowed is true IFF requestHost matches an apiHosts entry under the §Manifest item-10 dot-anchored suffix-containment rule (equal OR ends with "." + entry) — it MUST fail closed (allowed:false) for any non-match (no substring/suffix escape) and for a provider with no apiHosts. It is a pure decision probe: no credential is sent and no outbound request is made.

Conformance: connection-pack-no-credential-material.test.ts (specific-code leg), connection-provider-resolution.test.ts (clauses 6 + 8), connection-pack-write-reconsent.test.ts (clause 4), connection-pack-apihosts.test.ts (RFC 0120 — the egress allow-list behavioral leg).

11. Reviewable-learning / goals / portability surfaces — /v1/host/sample/{proposals,goals,export,import} (RFCs 0096/0097/0098)

FieldValue
Method + path/v1/host/sample/proposals[...] (RFC 0096) · /v1/host/sample/goals[...] (RFC 0097) · GET /v1/host/sample/export · POST /v1/host/sample/import[?dryRun=] (RFC 0098)
Capability gatecapabilities.agents.proposals · capabilities.agents.goals · capabilities.portability
Env gate (reference impl)seam registered when the matching capability is asserted; production hosts gate per §"Production safety". These are the floor surfaces, promotable to the normative /v1/{proposals,goals,export,import} paths at graduation (RFC 0086 precedent).
IntroducedRFCs 0096/0097/0098 §Conformance — black-box drivers for the inertness / bounded-continuation / no-secret-values behavioral legs
# RFC 0096 — proposals
GET    /v1/host/sample/proposals[?state=&kind=]      → 200 { proposals: Proposal[] }
GET    /v1/host/sample/proposals/{id}                → 200 Proposal
PATCH  /v1/host/sample/proposals/{id}                → 200 Proposal   # revise; MUST NOT activate
POST   /v1/host/sample/proposals/{id}/apply          → 200 { installedArtifactRef } | 403 (no scope) | 422 (malformed-for-kind)
POST   /v1/host/sample/proposals/{id}/reject         → 200 Proposal
DELETE /v1/host/sample/proposals/{id}                → 200 Proposal   # archive (soft)

# RFC 0097 — goals  (no `complete`/`satisfy` write: completion is the judge's verdict)
GET    /v1/host/sample/goals[?state=]                → 200 { goals: Goal[] }
GET    /v1/host/sample/goals/{id}                    → 200 Goal
POST   /v1/host/sample/goals                         → 200 Goal | 422 (requiresBounds advertised + no bounds)
PATCH  /v1/host/sample/goals/{id}                    → 200 Goal | 4xx (client-set state:satisfied refused)
POST   /v1/host/sample/goals/{id}/{pause,resume,abandon} → 200 Goal

# RFC 0098 — portability
GET    /v1/host/sample/export[?kinds=]               → 200 ExportBundle              # refs only, no secret values
POST   /v1/host/sample/import?dryRun=true            → 200 ImportPlan                # no writes
POST   /v1/host/sample/import                        → 200 ImportResult | 422 (literal credential value | dependsOn cycle) | 403 (no import scope)

The proposals/{id}/apply seam MUST install the byte image last persisted on the proposal (no re-synthesis — proposal-no-resynthesis) and MUST route activation through the advertised agents.proposals.activation mode. The goals POST seam MUST reject a bounds-less goal 422 when requiresBounds is advertised, and a client-supplied state: satisfied is refused on PATCH (goal-completion-judge-only). The import seam MUST reject a bundle whose connection-ref payload carries a literal credential value 422 BEFORE applying (export-bundle-no-credential-material), and ?dryRun=true MUST make zero writes.

Conformance: proposal-reviewable-learning.test.ts, goal-standing-continuation.test.ts, export-bundle-portability.test.ts (each soft-skips on 404 when the seam is unwired).

12. Real-time voice seams — POST /v1/host/sample/ai/call-{transcriber,speech-synthesizer} + /v1/host/sample/voice/barge-in (RFC 0106)

Three seams let the gated RFC 0106 scenarios drive the realtimeVoice surface without a media-transport stack:

  • POST /v1/host/sample/ai/call-transcriber { audio: { streamRef? | url? }, languageCode?, interimResults? } — the ctx.callTranscriber path. A host whose live-stream transport is host-internal per §E MUST honestly reject a live audio.streamRef with transcription_unsupported (the deterministic arm, under OPENWOP_TEST_SEAM_ENABLED, forces a mock provider and returns the canonical voice.* turn). A produced turn resolves a non-empty finalText at voice.turn_commit; every emitted voice.transcript carries contentTrust: "untrusted" (§F INV-2).
  • POST /v1/host/sample/ai/call-speech-synthesizer with { …, stream: true } — the §C streaming arm. Returns the finalized RFC 0105 audio asset (exactly one of url/base64) AND an events[] array of voice.synthesis_chunk run-events carrying metadata only (seq/mimeType; bytes by url/streamRef, inline base64 only under the host cap — G8), terminated by final: true.
  • POST /v1/host/sample/voice/barge-in — emits a voice.* sequence demonstrating voice.barge_invoice.cancelled with NO voice.synthesis_chunk after the cancel (the §F INV-3 voice-bargein-no-partial-leak proof).

Conformance: voice-transcription-streaming.test.ts, voice-transcription-unadvertised.test.ts, voice-synthesis-streaming.test.ts, voice-bargein-no-partial-leak.test.ts, voice-interim-not-durable.test.ts, voice-streamref-tenant-bound.test.ts (each gated on aiProviders.realtimeVoice.* and soft-skipping on 404 / on the honest transcription_unsupported §E path). Reference host: openwop-app (rev 00293-89w, live).

Production safety (normative)

All seams under /v1/host/sample/* are conformance-only. Hosts deployed in production:

  • SHOULD return 404 Not Found from every seam unless an env-gate explicitly enables it
  • MUST NOT honor the seams under default deployment configuration
  • MUST document which env-gates were set for the conformance run in the host's conformance.md evidence file
  • MUST require an authenticated, NON-ANONYMOUS principal on an ENABLED seam. A host that

mints an anonymous identity for credential-less callers — a session, a synthetic anon:<id> tenant, any principal issued because no credential was presented — MUST NOT treat that identity as satisfying this requirement. The env-gate governs whether a seam exists; it is not a substitute for who may call it, and staging keys such as nodeId are not secrets: they ship inside chain packs.

This clause was weaker for its first twenty minutes and the host it was written for satisfied it while remaining wide open. It originally read "MUST apply the same authentication and tenant resolution as the canonical surface" — which RFC 0132 makes trivially satisfiable, because the canonical surface legitimately admits anonymous actors on public agent surfaces. Auth ran, succeeded, and issued tenantId: "anon:<sid>"; the seam applied exactly the canonical treatment; the hole survived the rule. test-seam-unauthenticated.test.ts reds that host regardless, because it asserts the observable property rather than the mechanism — so the prose and its own conformance leg disagreed, and the leg was right. Corrected on the reporter's wording. A rule that reads like it closes a gap while being satisfiable by construction is the same advertise-what-you-cannot-guarantee shape this section exists to catch, one turn further in.

  • MUST NOT count two controls that read the same switch as two layers. An env-gate on

registration and an in-code guard on the same variable are one control with two call sites: flipping the variable removes both at once. A defense-in-depth claim requires the layers to fail independently.

Why these two were added (2026-08-15). A tier-1 host's production deployment had the

seam env-gate enabled, and a seam answered an unauthenticated request from the public

internet with real seam JSON. The registration path logged *"test seam ENABLED — NEVER

enable in production"* while doing exactly that. Its second guard was commented as

defense-in-depth and read the same environment variable as the first.

Corrected within the hour, and the correction is what sharpened the rule. The first

report said the staging route performed no tenant resolution at all. It does: auth

runs and succeeds, minting an anonymous session with a synthetic anon:<id> tenant.

So the seam was applying exactly the canonical surface's treatment — which is why the

clause above had to move from "same as canonical" to non-anonymous, and why the

conformance leg asserts the observable 200 rather than the presence of a gate.

The corpus was complicit in the shape: it required the seams to be off by default and said

nothing about what an enabled seam owes its callers, which reads as an assurance it never

made. This is the same class as a capability advertised and not routed — **a control whose

stated purpose exceeds what it does.**

The host-extension namespace /v1/host/sample/* is per host-extensions.md §"Canonical prefixes" — it is host-private space and does not affect the v1 wire-shape stability contract.

Canonical-endpoint conformance hooks

A handful of conformance assertions exercise wire-surface contracts that ride the canonical OpenWOP REST endpoints rather than a dedicated /v1/host/sample/ seam. These hooks need an operator-provided seed runId (or equivalent) communicated via an OPENWOP_TEST_ environment variable so the conformance driver can target a known refusal-eligible state without smuggling a host-private endpoint.

10. POST /v1/runs/{runId}:fork mode:replay against a past-retention runId (RFC 0039 §B MAE-3)

The MAE-3 contract is: a fork from a past event-log index MUST either serve memory-as-of that index OR refuse with 422 replay_memory_snapshot_unavailable per rest-endpoints.md §"Common error codes" — silent substitution of current memory is non-conformant.

The conformance driver targets the canonical fork endpoint with mode: "replay". The host's pre-flight order is normative for distinguishing this refusal from neighboring 422s:

1. checkFromSeqBounds(fromSeq, maxSeq) runs FIRST and returns 422 invalid_from_seq for fromSeq > maxSeq + 1. An impossible-fromSeq driver hits this gate, NOT MAE-3. 2. checkReplayMemorySnapshotPreflight(...) runs AFTER bounds-check and returns 422 replay_memory_snapshot_unavailable ONLY when the memory snapshot for an in-bounds fromSeq cannot be served — details.reason MUST be one of {"retention_expired", "event_log_unavailable"}.

Driving MAE-3 from outside therefore requires an actually-realized refusal-eligible state. Conventions:

HookEnv varRealizes
Past-retention runOPENWOP_TEST_EXPIRED_REPLAY_RUN_IDA known runId whose event log has aged past the host's retention window; forking with mode: "replay" returns details.reason: "retention_expired". Operator provides the runId via env (parallel naming to the existing OPENWOP_TEST_EXPIRED_RUN_ID used by production-retention-expiry).
Event-log-unavailable run(host-side fault-injection seam)Not deterministically reproducible from outside — requires a host-side fault-injection seam to mark a run's event log unavailable. Documented here for completeness; no env-var convention yet.

Envelope shape (normative; covered behaviorally in multi-agent-memory-lifecycle.test.ts):

{
  "error": "replay_memory_snapshot_unavailable",
  "details": {
    "fromSeq": 0,
    "sourceRunId": "<runId from the URL>",
    "reason": "retention_expired"
  }
}

details.reason MUST be one of {"retention_expired", "event_log_unavailable"}. The host MAY add additional optional fields under details; fromSeq MUST echo the requested fromSeq and sourceRunId MUST echo the runId from the URL.

Conformance: multi-agent-memory-lifecycle.test.ts (the MAE-3 behavioral assertion soft-skips when OPENWOP_TEST_EXPIRED_REPLAY_RUN_ID is unset OR the host does not advertise multiAgent.executionModel.version >= 2 + memory.supported: true).

Open seams (light up when fixtures ship)

  • Memory cross-run TTL roundtrip seam (RFC 0039 MAE-2) — POST /v1/host/sample/test/memory/cross-run-ttl-roundtrip. Contract: drive a parent → child → parent memory write/read sequence with controlled wall-clock skew to assert child-write-time TTL anchoring. Behavioral assertion in multi-agent-memory-lifecycle.test.ts stays it.todo until a memory-advertising Phase 2 host wires the seam.
  • Credential resolution + redaction seam (RFC 0046) — POST /v1/host/sample/credentials/echo. Gated on capabilities.credentials.supported. Contract: resolve a seeded credential whose plaintext is a known canary, run an echo node, and return the run's observable surfaces (events + inputs + variables + channels + snapshot + debug bundle). The behavioral assertion in credential-payload-redaction.test.ts asserts the canary is absent from every returned surface (SECURITY invariant credential-payload-redaction); soft-skips on 404 until a credentials-advertising host wires the seam.
  • OAuth connector-echo seam (RFC 0047) — POST /v1/host/sample/oauth/connector-echo. Gated on capabilities.oauth.supported. Contract: a synthetic provider issues a token whose value is a known canary; a connector node runs; the run's observable surfaces (including the connector.authorized event) are returned. oauth-connector-redaction.test.ts asserts the token canary is absent from every surface and that connector.authorized carries the credential reference, not the token (reuses the credential-payload-redaction invariant); soft-skips on 404.
  • Run-ownership seam (RFC 0048) — GET /v1/host/sample/identity/owned-run. Contract: return a RunSnapshot that carries an owner triple. cross-workspace-isolation.test.ts asserts the owner echo carries a non-empty tenant; soft-skips on 404 (or when owner is omitted by a single-tenant host).
  • Cross-workspace isolation seam (RFC 0048 §D) — POST /v1/host/sample/identity/cross-workspace-read. Contract: a principal scoped to workspace A attempts to read a run owned by workspace B. cross-workspace-isolation.test.ts asserts the read fails closed with run_forbidden / not_found (no existence leak); soft-skips on 404 until a workspace-ownership host wires the seam.
  • Authorization-decision seam (RFC 0049 §C) — POST /v1/host/sample/authorization/decide. Gated on capabilities.authorization.supported. Contract: request a decision ({ principal, action, resource }) for a principal whose role is absent/unseeded; the host MUST return { allowed: false } (fail-closed). authorization-fail-closed.test.ts asserts the deny (SECURITY invariant authorization-fail-closed); soft-skips on 404 until an authorization-advertising host wires the seam.
  • SAML assertion-validation seam (RFC 0050) — POST /v1/host/sample/auth/saml/validate. Gated on capabilities.auth.profiles[] includes openwop-auth-saml + an operator-supplied synthetic IdP (OPENWOP_TEST_SAML_IDP_URL). Contract: present an assertion of a named variant (valid, alg-none, bad-signature, unsigned, expired, not-yet-valid, signature-wrapping); the host MUST accept valid and reject every negative with unauthenticated. The request MAY carry nameId (the persistent NameID the minted assertion asserts) and the idpUrl names the trust root, whose signed <saml:Issuer> the host MUST compare against the entityID bound to the SCIM connection before forming a link (RFC 0163 §B.1). The 2xx response body carries authenticated: boolean and, when a link-scoped deny fired, linkedDenied: true (RFC 0159 §A.3) so the subject-link scenarios can tell a cross-lane deny from an ordinary rejection. auth-saml-profile.test.ts drives the negatives — the 1-positive + 6-negative assertions are minted by the bundled synthetic IdP harness (conformance/src/lib/saml-idp.ts), which also runs the negative reference suite server-free; the host-ACS path soft-skips on 404 / absent env.
  • SCIM provisioning seam (RFC 0050) — POST /v1/host/sample/auth/scim/provision. Gated on capabilities.auth.profiles[] includes openwop-auth-scim + an operator-supplied SCIM endpoint (OPENWOP_TEST_SCIM_URL). Contract: drive a SCIM create-user / assign-group / deactivate-user op; the host MUST upsert an RFC 0048 principal / RFC 0049 role and deny a deactivated principal's subsequent decisions. auth-scim-profile.test.ts drives the roundtrip; soft-skips on 404 / absent env. RFC 0159 / RFC 0163 extensions: create-user MAY carry externalId (the opaque IdP-stable id the SCIM lane records) and idpUrl (the synthetic-IdP endpoint whose entityID the host binds to this SCIM connection — the trust root that feeds the SCIM lane, RFC 0163 §B.1; when absent the host uses its configured default); deactivate-user MAY address the user by externalId or email; a link op with linkKey (email | an attribute name) asks the host to form a cross-lane link on that key, and the host MUST reject a mutable/PII linkKey with 4xx or, if it accepts the request, MUST NOT apply any cross-lane effect from it (RFC 0159 §A.2). auth-subject-link.test.ts and auth-subject-link-key-class.test.ts drive these; soft-skip on 404 / absent env.
  • Approval-gate seam (RFC 0051) — POST /v1/host/sample/governance/approval-gate. Gated on capabilities.authorization.supported. Contract: drive a named scenario (unauthorized-grant, grant, reject, override, quorum) against a core.openwop.governance.approvalGate node; the host returns { released, event } reflecting the outcome (an unauthorized principal MUST NOT release; override MUST emit approval.overridden with a reason + an audit entry). approval-gate-flow.test.ts drives unauthorized + override-audited; soft-skips on 404 until a governance-advertising host wires the seam.
  • Scheduling tick seam (RFC 0052) — POST /v1/host/sample/scheduling/tick. Gated on capabilities.scheduling.supported + cron: true. Contract: advance a deterministic clock for a named scenario (single-tick, missed-window with missedTicks) and return { runsFired } — the count of runs a cron schedule produced. The host MUST report runsFired === 1 for a single tick (once-per-tick) and runsFired <= 1 for a missed window (no backlog flood). scheduling-cron-fires-once.test.ts drives both; soft-skips on 404 until a scheduling host wires the seam. (Delayed-execution horizon + calendar scenarios deferred.)
  • Heartbeat tick seam (RFC 0060) — POST /v1/host/sample/heartbeat/tick. Gated on capabilities.heartbeat.supported. Contract: evaluate a heartbeat predicate once for a request { heartbeatId, observedState, simulateSlowMs? } (simulateSlowMs asks the predicate to overrun maxRuntimeMs, exercising the §B.2 timeout path) and return { evaluated: HeartbeatEvaluated[], stateChanged: HeartbeatStateChanged[], enqueuedRuns: number } — exactly one evaluated per tick (§B.1); stateChanged + enqueuedRuns non-empty/non-zero ONLY when observedState differs from the prior tick's persisted state (§B.5, the anti-spam guarantee); evaluated[].status === "timeout" when simulateSlowMs exceeds the budget (§B.2). heartbeat-fires-once-per-tick.test.ts / heartbeat-idempotent-no-spam.test.ts / heartbeat-runtime-bound.test.ts drive these; soft-skip on 404 until a heartbeat host wires the seam.
  • Tool-hooks invoke seam (RFC 0064) — POST /v1/host/sample/toolhooks/invoke. Gated on capabilities.toolHooks.supported. Contract: evaluate the per-tool authorization + rate-limit gate for one call { principal, toolName, requiredScopes?, args?, simulateRateLimitExhausted?, simulateToolError? } and return the { toolCalled, toolReturned } payload pair the host would emit (the additive RFC 0064 fields on the existing agent.toolCalled / agent.toolReturned events). toolReturned.status MUST be forbidden when the principal lacks a requiredScopes entry (or authz is unevaluable — fail-closed, RFC 0049), rate_limited when simulateRateLimitExhausted, error with a populated error (_errorObject { code, message }, SR-1-redacted) and a non-negative durationMs when simulateToolError (the RFC 0064 §F tool-failure-honesty arm: the tool ran and failed), else ok with a non-negative durationMs; error and outcome are mutually exclusive; toolCalled.argsHash MUST be a secret-redacted (SR-1) JCS+SHA-256 hash carrying no raw secret material. tool-hooks-content-free.test.ts / tool-hooks-authorization-fail-closed.test.ts / tool-hooks-rate-limit.test.ts / tool-hooks-secret-redaction.test.ts / tool-hooks-failure-honesty.test.ts drive these; soft-skip on 404 (or, for the §F arm, until the host wires simulateToolError) until a tool-hooks host wires the seam.
  • Sub-run attestation seam (RFC 0063) — POST /v1/host/sample/subrun/attest. Gated on capabilities.agents.subRunAttestation. Contract: drive one sub-workflow harvest-then-merge for a request { childOutputs, outputAttestation: { checksum?, algorithm?, requireApproval?, principalScope? }, approvalAction? } and return { attestation, harvestedEvent, merged, mergedValues? } — the attestation { checksum, algorithm } the host would surface on core.workflowChain.event { phase: 'output.harvested' }, whether the merge proceeded, and the merged values. The checksum MUST be the RFC 8785 JCS + SHA-256 digest of childOutputs (byte-stable for identical inputs, host-independent). When requireApproval: true, merged MUST be true only for approvalAction accept/edit-accept and MUST be false (fail-closed) for reject or an absent/expired approval. subrun-checksum-stable.test.ts / subrun-approval-gate.test.ts / subrun-approval-fail-closed.test.ts drive these; soft-skip on 404 until a sub-run-attestation host wires the seam.
  • Memory-distillation seam (RFC 0062) — POST /v1/host/sample/memory/distill. Gated on capabilities.memory.distillation.supported. Contract: run one budgeted distillation for a request { memoryRef, tokenBudget?, sources?, indexEmitted?, includeSecretCanary? } and return { event, archiveChecksum, indexUpdated, indexFile? } — the memory.compacted event the host would emit (carrying the additive distillation { tokenBudget, tokensUsed, indexUpdated } sub-object) plus the stable archive's checksum. event.distillation.tokensUsed MUST be ≤ the resolved tokenBudget; an un-meetable budget MUST return token_budget_exceeded with no partial archive (atomic). The same sources + tokenBudget MUST yield an identical archiveChecksum (byte-stable). When indexEmitted, a MEMORY-INDEX.json workspace file MUST be retrievable and a workspace.updated event fired. When includeSecretCanary, a redacted secret in the sources MUST stay redacted in the archive (SR-1). distillation-token-budget.test.ts / distillation-stable-archive.test.ts / distillation-index-roundtrip.test.ts / distillation-secret-carryforward.test.ts drive these; soft-skip on 404 until a distillation host wires the seam.
  • Dead-letter exhaustion seam (RFC 0053) — POST /v1/host/sample/deadletter/exhaust. Gated on capabilities.deadLetter.supported. Contract: drive a node that deterministically exhausts a short retry policy for a named scenario (exhaust-retries, fork-after-dead-letter); the host returns { event, forkEligible } — the run.dead_lettered event (carrying attempts) and whether the dead-lettered run is forkable. deadletter-retry-exhaustion.test.ts drives both; soft-skips on 404 until a dead-letter host wires the seam. (Retention-purge scenario deferred — needs a clock seam.)
  • Agent-loop seam (RFC 0061) — POST /v1/host/sample/agentloop/run. Gated on capabilities.multiAgent.executionModel.version >= 5. Contract: drive a bounded stateful loop for a request { turns, workspaceWriteAtTurn?, suspendAtTurn?, resume? } and return { decisions, workspaceVisible?, resumedIteration? } — the ordered runOrchestrator.decided payloads the host would emit (each carrying the iteration counter). decisions[k].iteration MUST equal k+1 (1-based, monotonic, one per turn). When workspaceWriteAtTurn: i is set (requires host.workspace.supported), workspaceVisible MUST report the write invisible to turn _i_'s snapshot and visible to turn _i+1_ (§C input 2). When suspendAtTurn + resume are set (requires statefulResume: true), resumedIteration MUST equal the suspend iteration — the counter does not reset or skip (§D). agent-loop-iteration-monotonic.test.ts / agent-loop-workspace-snapshot.test.ts / agent-loop-stateful-resume.test.ts drive these; soft-skip on 404 until a version-5 host wires the seam.
  • Runtime-requirement install-gate seam (RFC 0076 §A) — POST /v1/host/sample/packs/install-gate. No capability flag (RFC 0076 §A adds a manifest field + host behavior, not an advertisement); soft-skips on 404. Contract: evaluate a candidate manifest's runtime.requires[] against a simulated host grant-set for a request { manifest, grantSet?, gating? } and return the install-time outcome. When gating !== false (sandbox host): if every runtime.requires entry is in grantSet the host MUST return 200 { outcome: "installed" }; if any entry is not granted the host MUST refuse at install with 400 { error: "pack_runtime_requirement_unmet", unmet: [...], manifest: "<name>@<version>", advice? } (the capability_not_provided envelope shape) — NOT install-and-fail-at-first-invocation. When gating: false (non-sandbox host) the host installs unconditionally and SHOULD return 200 { outcome: "installed", requiresProjected: [...] }, the declared requirements projected onto the inventory entry for operator visibility. runtime-requires-install-gate.test.ts drives install-grant / install-refuse / non-sandbox-projection; soft-skip on 404 until a runtime-requires-gating host (MyndHyve is the first adopter) wires the seam. The pure-schema vocabulary rejection (runtime.requires: ["node:dns/promises"]invalid_manifest) is covered server-free by runtime-requires-shape.test.ts.
  • Safe-fetch seam (RFC 0076 §B) — POST /v1/host/sample/http/safe-fetch. Gated on capabilities.httpClient.safeFetch.supported; soft-skips on 404. Contract: evaluate one ctx.http.safeFetch call for a request { url, init?, simulateRebindTo? } and return { outcome, status?, blocked?, toolCalled?, toolReturned? } — the host applies the §host.http SSRF guard (resolve→pin→connect). The host MUST return { outcome: "blocked", blocked: "ssrf" } for a loopback / RFC 1918 / link-local / cloud-metadata target AND for a simulateRebindTo that re-resolves a public name to a blocked address (DNS-rebinding); MUST return { outcome: "blocked", blocked: "upgrade" } when init.headers requests Connection: upgrade; else { outcome: "fetched", status }. When capabilities.toolHooks.prePostEvents is also advertised, a fetched call MUST include the { toolCalled, toolReturned } pair (transport: "http"). safefetch-behavior.test.ts drives SSRF-block / rebinding / upgrade-refusal / audit-when-both; soft-skip on 404 until a safeFetch host wires the seam.
  • Safe-fetch live-run audit seam (RFC 0076 §B / RFC 0064 §B) — POST /v1/host/sample/http/safe-fetch-run. Gated on capabilities.httpClient.safeFetch.supported + capabilities.toolHooks.prePostEvents (both); soft-skips on 404. Distinct from the inline safe-fetch seam above: this seam executes one ctx.http.safeFetch call inside a real run through the host's _production_ per-ctx injection path (the same ctx.http.safeFetch a node receives at dispatch), then returns { runId, outcome }. Contract: for a request { url, init? } the host MUST run one ctx.http.safeFetch in a real run and return 200 { runId, outcome } where outcome is "fetched" (public target the guard allowed) or "blocked" (link-local / RFC-1918 / cloud-metadata target the SSRF guard refused); the conformance driver then reads the run's durable event log via GET /v1/host/sample/test/runs/:runId/events and asserts a callId-paired agent.toolCalled (transport: "http") / agent.toolReturned was persisted. The audit pair MUST be persisted for _every_ invocation — blocked as well as fetched (per §host.http "for every safeFetch invocation"; a refused egress attempt is itself a security-relevant event the durable log must capture). safefetch-live-audit.test.ts exploits this: it drives a guaranteed-blocked metadata URL as an egress-independent floor (reachable on any host with no outbound connectivity, so the bar can never pass vacuously on an egress-blocked host) plus a best-effort public fetch for success-path coverage. This closes the seam-vs-production gap in safefetch-behavior.test.ts (whose audit assertion reads only the inline seam echo): a host can pass the inline seam yet ship a production createSafeFetch() with no audit hooks — the "quiet bypass" §host.http forbids. safefetch-live-audit.test.ts drives it via behaviorGate('openwop-safefetch-live-audit', …) so a host advertising both flags but not emitting to the durable log FAILS under OPENWOP_REQUIRE_BEHAVIOR=true; the seam itself soft-skips on 404 (host-pending) until a safeFetch host wires it. This is the RFC 0076 §B → Accepted bar. Load-bearing host note: the audit pair MUST be emitted through the host's _durable_ run-event-log append path (the same path production tool calls use — e.g. getEventLog().append(runId, 'agent.toolCalled'|'agent.toolReturned', …) with RFC 0002 §B callId pairing + causationId), not captured-and-echoed inline like the non-run safe-fetch seam above — otherwise the scenario reads the durable log, finds nothing, and correctly fails while the inline seam stays green.
  • Run event-log read seam (companion to the live-run seams above; used by event-log-query.tsqueryTestEvents) — GET /v1/host/sample/test/runs/:runId/events. Conformance-only, env-gated; soft-skips on 404 (isEventLogSeamAvailable()). Contract: return the run's persisted events as { events: TestEvent[] } (each { eventId, runId, type, payload, timestamp, sequence, causationId?, nodeId?, contentTrust? }), optionally filtered by ?type=&correlationId=&causationId=&nodeId=. The host MUST workspace-scope the read — refuse (or return empty for) a runId outside the caller's {tenant, workspace}, so the test seam is never a weaker cross-tenant disclosure path than production (matches the identity/ / credential-echo seam RBAC precedent + the WCT-1 posture). Enforcement scope: like every /v1/host/sample/test/ seam this is _reference-host-honored_, not protocol-tier — check-security-invariants.sh covers production surfaces, not conformance-only test seams, so no protocol-tier invariant gates this MUST; it inherits the same cross-tenant intent as the production workspace-cross-tenant-isolation (WCT-1) invariant and is the host operator's responsibility to uphold when wiring the seam. Read-only; no side effects. Already consumed by the RFC 0021 aiEnvelope engine-projection scenarios and now by safefetch-live-audit.test.ts; a host that wires it un-soft-skips that whole cohort.
  • Roster portfolio fire seam (RFC 0086 §C) — POST /v1/host/sample/roster/fire. Gated on capabilities.agents.roster.supported; soft-skips on 404. Contract: fire one workflow in a roster member's portfolio for a request { rosterId?, triggerSource?, asWorkItem? } (host picks a default member when rosterId is omitted) and return { runId, rosterId, triggerSubscriptionId? }. The fired run MUST emit roster.run.initiated as its FIRST attribution event — immediately after run.started, BEFORE any agent.invocation. / agent. event (§C ordering) — content-free per the roster-attribution-no-content invariant (ids + persona + trigger source ONLY; never the work-item body/prompt/credential). When asWorkItem: true the fire takes the RFC 0083 durable-work-item path and the event MUST carry triggerSubscriptionId (so trigger→run→roster is traceable via /ancestry, RFC 0040). The conformance driver reads the run's durable events via the run event-log read seam and asserts the ordering + content-free payload + work-item triggerSubscriptionId. agent-roster-attribution.test.ts drives it via behaviorGate('openwop-roster-attribution', …); the normative GET /v1/agents/roster read leg runs black-box on any roster host regardless of this seam. This is the RFC 0086 → Accepted bar (first adopter: MyndHyve agents.roster).
  • Live manifest-invocation seam (RFC 0077 §B/§E/§F) — POST /v1/host/sample/agents/live-invoke. Gated on capabilities.agents.liveRuntime.supported; soft-skips on 404. Contract: drive one live manifest invocation for a request { agentId?, source?, returnSchemaRef?, forceInvalidResult?, attemptTool? } (host picks a default agent when agentId is omitted) and return { runId, invocationId, outcome? }. The invocation MUST bracket its agent.* family with agent.invocation.started as the FIRST agent-scoped event and agent.invocation.completed as the LAST (§E), sharing one invocationId, with source ∈ {workflow-node,run-api,chat-mention} and outcome ∈ {completed,handed-off,escalated,refused,failed} — both events content-free (identifiers + selection/outcome metadata only, never prompt or result body). When returnSchemaRef + forceInvalidResult: true are set (requires liveRuntime.structuredOutput), the host MUST fail the invocation (completed.outcome === "failed", schemaValidated !== true) rather than ship a result that violates handoff.returnSchemaRef (§B step 6). When attemptTool names a tool OUTSIDE the agent's toolAllowlist, the host MUST NOT call it (no agent.toolCalled for that tool — the §F-1 / RFC 0002 §A14 allowlist floor). The conformance driver reads the durable run events via the run event-log read seam. agent-live-invocation-bracket.test.ts / agent-live-structured-output.test.ts / agent-live-allowlist-enforced.test.ts drive these via behaviorGate('openwop-live-invocation-bracket' | 'openwop-live-structured-output' | 'openwop-live-allowlist-enforced', …). This is the RFC 0077 → Accepted bar (first adopter: MyndHyve agents.liveRuntime).
  • Trigger-bridge delivery seam (RFC 0083 §C) — POST /v1/host/sample/trigger-bridge/deliver. Profile-gated on openwop-trigger-bridge (derived from discovery per §D — the bridge advertised + a dead-letter sink + a durable source); soft-skips on 404. Contract: drive one delivery through the durable bridge for a request { scenario, dedupKey?, source? } and return { runId?, subscriptionId?, outcome?, deliveredCount? }, persisting the trigger.delivery.attempted + trigger.subscription.state.changed events to the durable run-event log (read back via the run event-log read seam). scenario: "dedup" delivers the same dedupKey twice and MUST be effectively-once (≤1 trigger.delivery.attempted { outcome:"delivered" } for that key, §C-1); scenario: "exhaust" exhausts the retry policy and MUST terminate in trigger.delivery.attempted { outcome:"dead-lettered" } + trigger.subscription.state.changed { toState:"dead-lettered" } (§C-2 + RFC 0053); scenario: "deliver" performs one successful delivery whose resulting run's run.started MUST carry causationId == the delivery id (§C / RFC 0040, resolvable via /ancestry). Both trigger.* events MUST be content-free (SR-1: ids/states/counters only — never inbound body/headers/credentials). trigger-bridge-delivery.test.ts drives all three legs via behaviorGate('openwop-trigger-bridge', …); the normative GET /v1/trigger-subscriptions read runs black-box regardless of this seam. This is the RFC 0083 → Accepted bar.
  • Deferred chain-expansion seam (RFC 0124 / RFC 0136) — POST /v1/host/sample/chain/deferred-expand. Referenced from capabilities.md §workflowChainPacks.deferredParameters; gated on capabilities.workflowChainPacks.deferredParameters.supported; soft-skips on 404. Contract: expand a chain in deferred-parameter mode for a request { chain: { parameters, dag? }, values? }chain.parameters accepts a full { type, properties, required? } object OR a bare properties map; dag optional (defaults to a trivial core.identity node); values optional per-param seed values. Returns the minted top-level variables[] as { variables: [{ name, type?, format?, required, sensitive? }] }, where name is the bare parameter name (the internal deferred-parameter prefix de-aliased) and format is present iff the host minted it: a string parameter's declared format is copied verbatim — including an unrecognised value (RFC 0136 req 2) — while a non-string parameter omits format entirely (req 1). A sensitive parameter's flag composes with format on one variable (req 6). workflow-variable-format.test.ts leg B1 drives it (a chain declaring email/note/count → the minted email carries format:"email", note carries the unrecognised value verbatim, count has no format) and soft-skips on 404 or an absent variables[] (a host advertising deferredParameters but not yet serving the variables[] extension). The RFC 0124 value-resolution branches (chainId: conformance.deferred / -sensitive returning { resolved, composed }) are the same seam's other cases and are unaffected. This is (with B2's seam-free run round-trip) the RFC 0136 → Accepted witness.
  • Eval-run seam (RFC 0081 §B/§C) — POST /v1/host/sample/agents/eval-run. Gated on capabilities.agents.evalSuite.supported; soft-skips on 404. Contract: drive one mode:"eval" projection for a request { agentId?, modes?, taskCount? } (host picks a default manifest agent + a built-in golden suite when omitted) and return { runId, suiteId?, suiteVersion?, taskCount?, passed?, aggregateScore? }, persisting the eval.* family to the durable run-event log (read back via the run event-log read seam). The eval run MUST emit eval.started as the FIRST eval event, one eval.scored PER TASK (after that task's terminal agent.decided), and eval.completed ONCE before run.completed (§C ordering: eval.started.sequence < every eval.scored.sequence < eval.completed.sequence; the eval.scored count == eval.completed.taskCount). Every eval.scored MUST be content-free (score ∈ 0..1, passed boolean, ids/scalars ONLY — NEVER task output, rubric prose, or model completion; SR-1 / eval-summary-no-content-leak). The terminal run output MUST be a schema-valid EvalSummary (eval-summary.schema.json) readable via the NORMATIVE GET /v1/runs/{runId}/eval-summary, with passedCount &lt;= taskCount and no per-task output body. agent-eval-run.test.ts drives it via behaviorGate(&#39;openwop-eval-run&#39;, …); the normative eval-summary read runs black-box regardless of this seam. This is the RFC 0081 → Accepted bar (first adopter: MyndHyve agents.evalSuite).
  • Deployment-transition seam (RFC 0082 §B/§E) — POST /v1/host/sample/agents/deployment-transition. Gated on capabilities.agents.deployment.supported; soft-skips on 404. Contract: drive one deployment transition for a request { scenario, agentId?, version?, channel?, evalRunId? } and return { runId?, record?, allowed?, error?, resolvedAgentVersion? }, persisting the deployment. family (+ agent.invocation.started) to the durable run-event log (read back via the run event-log read seam). scenario: &quot;promote&quot; runs the §E contract (authorize RFC 0049 deploy:promote → RFC 0051 approvalGate → RFC 0081 eval-verify when evalRunId set) and MUST emit a content-free deployment.promoted whose toState is in the seven-state vocabulary + carries toVersion; the returned record MUST validate against agent-deployment.schema.json. scenario: &quot;unauthorized&quot; drives a principal lacking deploy:promote and MUST fail closed (allowed:false, NO deployment.promoted — the deployment-promotion-fail-closed invariant). scenario: &quot;eval-gate-unmet&quot; drives a promote whose evalRunId has EvalSummary.passed:false and MUST deny with error:&quot;eval_gate_unmet&quot; + NO deployment.promoted (§E-3). scenario: &quot;channel-pin&quot; starts a @channel-bound run whose resolved version is recorded as resolvedAgentVersion on agent.invocation.started (§B — the recorded fact a replay re-reads rather than re-resolving). All deployment. events MUST be content-free (SR-1: ids/state/scalars only — never a manifest body/prompt/credential). agent-deployment-lifecycle.test.ts drives all four legs via behaviorGate(&#39;openwop-deployment-lifecycle&#39;, …); the normative GET /v1/agents/{agentId}/deployments read runs black-box regardless of this seam. This is the RFC 0082 → Accepted bar (first adopter: MyndHyve agents.deployment).
  • Tool-session seam (RFC 0078 §D) — POST /v1/host/sample/tools/session-run. Gated on capabilities.toolCatalog.sessionLifecycle; soft-skips on 404/405. Contract: drive one tool-session interaction for a request { toolId? } (host picks a default catalog tool when omitted) and return { runId, sessionId?, toolId? }, persisting tool.session.opened → the RFC 0064 call events (agent.toolCalled/agent.toolReturned) → tool.session.closed to the durable run-event log (read back via the run event-log read seam). tool.session.opened MUST precede the FIRST call event and tool.session.closed MUST follow the LAST (§D bracket ordering), both sharing one sessionId, each carrying a toolId, with tool.session.closed.outcome ∈ {completed,failed,aborted,expired}. Both events MUST be content-free (SR-1: ids/outcome ONLY — never tool args/result/credential). tool-session-lifecycle.test.ts drives it via behaviorGate(&#39;openwop-tool-session-lifecycle&#39;, …); the normative GET /v1/tools catalog read runs black-box regardless of this seam. This is part of the RFC 0078 → Accepted bar (first adopter: MyndHyve toolCatalog).
  • Egress-decision seam (RFC 0079 §C) — POST /v1/host/sample/egress/decide. Gated on capabilities.httpClient.egressPolicy.supported; soft-skips on 404/405. Contract: drive one egress-policy decision for a request { scenario } and return { decision?, reason?, destination?, credentialAttached?, canaryLeaked? } — the host evaluates a host-issued credential's RFC 0079 §A audiences[] provenance against the egress destination. scenario: &quot;out-of-audience&quot; (credential bound to audience A, egress to B ∉ A) MUST return decision ∈ {denied,downgraded} + reason: &quot;out-of-audience&quot; and MUST NOT attach the credential (credentialAttached !== true — the §C confused-deputy MUST backing the egress-credential-audience-bound invariant). scenario: &quot;provenance-unevaluable&quot; MUST return decision: &quot;denied&quot; + reason: &quot;provenance-unevaluable&quot; (fail-closed). scenario: &quot;in-audience&quot; is the control (MAY allowed). scenario: &quot;canary&quot; seeds a credential whose value is a known sentinel and the host MUST NOT surface it (canaryLeaked !== true) nor spill the blocked URL/host/header into the decision (SR-1); decision ∈ the closed enum + reason ∈ the CLOSED vocabulary throughout. egress-audience-binding.test.ts (keystone) + egress-decision-content-free.test.ts drive these via behaviorGate(&#39;openwop-egress-audience-binding&#39; | &#39;openwop-egress-decision-content-free&#39;, …). This is the RFC 0079 → Accepted bar (first adopter: MyndHyve httpClient.egressPolicy). Egress policy layers over the RFC 0076 §B safeFetch SSRF guard — no new normative read endpoint.
  • Memory-consolidation seam (RFC 0068 §D) — POST /v1/host/sample/memory/consolidate. Gated on capabilities.agents.memoryConsolidation.supported; soft-skips on 404/501. Contract: run one background-consolidation pass for a request { memoryRef, includeSecretCanary? } and return { event: { inputCount, outputCount }, secretLeaked? }, emitting the agent.memory.consolidated event (durable-append, like the live-run seams). A merge/dedup pass MUST have outputCount &lt;= inputCount (§D.1); a second pass over the unchanged corpus MUST be a no-op (inputCount == outputCount — the §D.2 idempotence MUST that bounds runaway consolidation); when includeSecretCanary, a redacted secret in a source entry MUST stay redacted in the consolidated entry (secretLeaked: false — §D.3 / agent-memory.md §SR-1 carry-forward). memory-consolidation-idempotent.test.ts drives it via the capability gate. This is part of the RFC 0068 → Accepted bar (first adopter: MyndHyve agents.memoryConsolidation).
  • Commitment-fire seam (RFC 0068 §C) — POST /v1/host/sample/commitment/fire. Gated on capabilities.agents.commitments.supported; soft-skips on 404/501. Contract: fire one inferred standing commitment for a request { memoryRef, condition, includeIntentionCanary? } and return { event: { commitmentId, memoryRef, condition }, fireCount?, intentionCanary? }, emitting the commitment.fired event (durable-append). The event MUST carry commitmentId + the source memoryRef (§C.1 CTI-1 provenance) + condition; it MUST be content-free — the inferred intention text MUST NOT appear anywhere on the event payload (§C.3; the seam MAY echo the plaintext as the top-level intentionCanary ONLY so the driver can assert its absence from event); a commitment MUST fire at most once per satisfied condition (fireCount &lt;= 1, §C.2). commitment-fired.test.ts drives it via the capability gate. This is part of the RFC 0068 → Accepted bar (first adopter: MyndHyve agents.commitments).
  • Budget-run seam (RFC 0084 §C/§D) — POST /v1/host/sample/budget/run. Gated on capabilities.budget.supported; soft-skips on 404/501. Contract: drive one budgeted run for a request { scenario } and return { runId?, outcome?, error?, modelCalled? }, persisting the budget. + cap.breached + run.failed family to the durable run-event log (read back via the run event-log read seam). Budget consumption is tracked OFF the existing RFC 0026 provider.usage stream (no double-counting). scenario: &quot;hard-cost-exhaust&quot; (requires enforce:&quot;hard&quot;, dimensions:[&quot;cost&quot;]) MUST emit, in strict sequence, budget.reserved {effectiveBudget, scope}budget.consumed {dimension:&quot;cost&quot;, consumed, limit, remaining}budget.threshold.crossed {dimension:&quot;cost&quot;, percent}budget.exhausted {dimension:&quot;cost&quot;}cap.breached {kind:&quot;budget-cost&quot;, limit, observed}run.failed {error:&quot;budget_exhausted&quot;} (the §D hard-stop, reusing the unified cap.breached overflow event per the RFC 0058 precedent). scenario: &quot;model-denied&quot; drives a run whose resolved model violates budget.modelDeny/modelAllow; the host MUST refuse with budget_model_denied BEFORE the provider call (modelCalled !== true, modelDeny wins on conflict — fail-closed, composing RFC 0031 + RFC 0067 at the dispatch seam). scenario: &quot;advisory&quot; (requires enforce:&quot;advisory&quot;) MUST emit the budget. events but MUST NOT stop the run (no cap.breached{budget-}, no run.failed{budget_exhausted}). Every budget. payload MUST be content-free (SR-1 / budget-no-pricing-leak: dimension/limit/consumed/remaining/percent scalars only — NEVER provider pricing tables / per-token rates / cost-model internals). The §E orthogonality with RFC 0058 is normative — budget has no wall-time/iteration dimension. budget-enforcement.test.ts drives it via behaviorGate(&#39;openwop-budget-enforcement&#39;, …). This is the RFC 0084 → Accepted bar (first adopter: MyndHyve budget).
  • Multi-party conversation seam (RFC 0101 §Conformance) — POST /v1/host/sample/conversation/multi-party/open + POST /v1/host/sample/conversation/multi-party/exchange. Gated on capabilities.multiPartyConversation.supported (via isMultiPartyConversationSupported()); soft-skips on 404/405. The seam exists because RFC 0101 standardizes the multi-party shape (roster + speakerId + capability) but deliberately mints no normative client wire-route to open a conversation — opening, turn order, and round protocol are non-normative host product policy. The seam is the conformance-driver's host-agnostic way to initiate a council and submit turns, routing through the SAME roster-membership + attribution enforcement the host applies on its production conversation path. It is self-contained — it does NOT require the host to implement the full RFC 0005 conversation gate; a host MAY back it with a small in-memory council registry. Contract:

```http POST /v1/host/sample/conversation/multi-party/open Body: { conversationId: string, participants: AgentRef[], maxParticipants?: number } Returns: 200 { conversationId, accepted: true } | 400|422 { error: "validation_error", message, details? } # participants.length exceeds maxParticipants (request or advertised)

POST /v1/host/sample/conversation/multi-party/exchange Body: { conversationId: string, turn: ConversationTurn } # ConversationTurn per conversation-turn.schema.json (+ RFC 0101 speakerId) Returns: 200 { accepted: true } | 400|422 { error: "validation_error", message, details? } # see MUSTs below | 404 { error: "not_found", message } # unknown conversationId ```

A host advertising the capability MUST enforce, on exchange: (1) a role: &#39;agent&#39; turn that omits speakerId is rejected validation_error (the §Spec attribution MUST); (2) a turn whose speakerId is NOT in the conversation's declared participants roster is rejected validation_error (the §Spec membership MUST, RFC 0005 §E turn-validation path); and on open: (3) a participants array exceeding the request's or the host's advertised maxParticipants is rejected validation_error (the §Spec maxParticipants MUST). RFC 0005 §E pins the error code (validation_error), not the HTTP status — a host MAY use 400 or 422; the conformance leg asserts on error.code and tolerates either. A roster-valid, attributed agent turn MUST be accepted. multi-party-conversation-behavioral.test.ts drives all four (one positive + three rejections) via behaviorGate(&#39;openwop-multi-party-conversation&#39;, …); the always-on multi-party-conversation-shape.test.ts covers the schema-expressible facts server-free regardless of this seam. This is the RFC 0101 → behavioral-conformance bar (reference impl: the postgres example host). A host whose multi-party enforcement is bound to a product flow rather than a generic open (e.g. openwop-app ADR 0040's advisory-board council, which keys roster enforcement on a board group) MAY instead witness via its own host-side behavioral test + an INTEROP-MATRIX.md row — the RFC 0086 dual-staging — but the reference seam ensures the behavioral MUSTs have at least one suite-executable, host-agnostic witness.

Open spec gaps

> Absorbed into spec/v1/gaps.json (RFC 0174 §E.3, 2026-09-03). The 2 row(s) this table carried are now openwop.gap.spec.host-sample-test-seams.<local> entries with a disposition and a witness class, one namespace with every RFC register (RFC 0166 §B). The table is retired; do not add rows here.

Cross-references

  • host-extensions.md §"Canonical prefixes" — the /v1/host/sample/* namespace contract
  • capabilities.md §"Truthful advertisement" — the host's commitment when it advertises any of the above flags
  • host-capabilities.md §"capabilities.observability.testSeams" — the OTel scrape + debug-bundle export capability sub-block
  • observability.md §"OTel collector test seam (RFC 0034)" — the canonical RFC 0034 §B normative text the OTel + debug-bundle seams implement
  • replay.md §"LLM cache-key recipe" — the canonical recipe the §4 LLM cache-key seam computes
  • prompts.md §"Resolution chain (normative)" — the canonical RFC 0029 resolver semantics the §1 seam exposes

13. Channel-presence snapshot — POST /v1/host/sample/channel-presence/snapshot (RFC 0110)

OPTIONAL. Gated on capabilities.channelPresence.supported. RFC 0110 carries channel.presence over a host SSE — a held connection a conformance client cannot assert against — and mints no normative client route to open presence. This seam returns a live channel.presence snapshot after a TRANSIENT join (so present is non-vacuous: it includes the requesting member), routing through the SAME membership gate + closed payload the host applies in production.

  • Request: { "conversationId": string, "member": "user:<id>" }.
  • Response 200: the RFC 0110 channel-presence-payload.schema.json shape — { conversationId, present: string[], typing: string[] } — CLOSED (no field beyond these; the no-PII guard); every ref an opaque RFC 0041 user:/agent: subject; typing ⊆ present; the requesting member appears in present.
  • 404 / 405: seam not wired — the gated scenario (channel-presence-behavioral.test.ts) soft-skips. A host whose presence is bound to a product flow (e.g. a membership-gated channel SSE) witnesses via its own host-side route test + an INTEROP-MATRIX.md row (the RFC 0086 dual-staging).
  • Ephemerality (RFC 0110): presence is live state — the host MUST NOT persist channel.presence to the replayable event log; it does not appear on GET /v1/runs/{runId}/events.

14. Transcript-window accounting — GET /v1/host/sample/agent/transcript-window?runId=…&iteration=N (RFC 0111)

FieldValue
Method + pathGET /v1/host/sample/agent/transcript-window?runId=<id>&iteration=<N>
Capability gatecapabilities.multiAgent.executionModel.contextBudget.transcriptTokenBudget present (RFC 0111)
Env gate (reference impl)OPENWOP_TEST_SEAM_ENABLED=true
IntroducedRFC 0111 §"Conformance seam"

OPTIONAL. The orchestrator transcript the host assembles each iteration is host-internal and never crosses the wire; conformance cannot observe the per-turn token bound from discovery alone. This seam returns the host's OWN accounting of what it fed the model on orchestrator turn iteration of run runId, so the harness can cross-check it against the run event-log and the advertised contextBudget.

  • Request: query params runId (string) + iteration (1-based integer, the runOrchestrator.decided.iteration of the turn).
  • Response 200: { tokenCounter: string, tokenCount: integer, eventIds: string[], summarizedRanges: Array<{ summaryRef: string, replacedTurns: string[] }> }.

- tokenCounter MUST equal the advertised contextBudget.tokenCounter. - tokenCount is the host's reported total tokens of transcript fed that turn, in tokenCounter units; it MUST be ≤ contextBudget.transcriptTokenBudget. - eventIds are the event-log entries (most-recent tail) the host fed verbatim that turn — no older event included while a newer eligible one is dropped. The harness independently reads these from the run event-log (GET /v1/host/sample/test/runs/:runId/events) and re-computes their token sum in the advertised unit, confirming the host's tokenCount is internally consistent and ≤ budget. - summarizedRanges lists each in-window range the host replaced with a summary; every entry's summaryRef MUST have a matching context.summarized event in the run event-log.

  • 404 / 405: seam not wired — the gated scenario (context-budget-transcript-bound.test.ts) soft-skips.
  • Non-vacuity ceiling (RFC 0111): the seam proves the host's DECLARED accounting is internally consistent + within budget; it cannot black-box-prove the host feeds nothing additional off-seam. The capability is advertise-and-attest — a host that lies in the seam fails the cross-check; a host that lies past the seam is outside any wire/seam witness.

15. A2UI surface-emit driver — POST /v1/host/sample/a2ui/emit-surface (RFC 0114)

FieldValue
Method + pathPOST /v1/host/sample/a2ui/emit-surface
Capability gatecapabilities.a2uiSurface.deltaTransport: true (RFC 0114)
Env gate (reference impl)OPENWOP_TEST_SEAM_ENABLED=true
IntroducedRFC 0114 §"Delta transport"

OPTIONAL. A real ui.a2ui-surface envelope is one-shot per producing node, so a conformance harness cannot, from the canonical wire alone, drive the SECOND surface emission needed to observe a delta frame. This seam supplies that trigger ONLY — it MUST flow through the host's REAL surface-emit path, the REAL ?a2uiDelta=1 delta transport, and the REAL closed-catalog validator (mirroring RFC 0114 §"Delta transport"); it MUST NOT be a mock that synthesizes a delta or bypasses validation. The harness drives the trigger; the host produces the delta exactly as it would in production.

  • Request: { "runId": string, "surface": object }surface is a full ui.a2ui-surface payload ({ catalogVersion, surface, reasoning? }) the host records as the run's current full surface. The first call establishes the baseline (surface A); each subsequent call is a new full surface (surface B, …) the host both records full AND, for any ?a2uiDelta=1 subscriber, transports as a delta frame computed against the previously emitted full.
  • Response 200: { surfaceRef: string } — the recorded envelope id of the surface just emitted (the surfaceRef a delta frame references). The host MUST record the canonical envelope as the FULL surface (never a delta).
  • Out-of-catalog surface: a request whose surface carries a component outside the host's closed catalogVersion catalog MUST be rejected by the SAME real validator a full surface receives — the seam returns a fail-closed error (no delta is transported, no out-of-catalog component reaches render), exactly as RFC 0114 requires of any delta whose post-patch surface fails closed-catalog validation.
  • 404 / 405: seam not wired — the gated scenario (a2ui-surface-delta-transport.test.ts) soft-skips its live-host leg (the always-on schema + RFC 6902 reconstruction legs still run).
  • Non-vacuity (RFC 0114): the witness is non-vacuous because both the surface-emit and the ?a2uiDelta=1 transport are the host's production paths and the catalog gate is the host's real validator — the harness only supplies the surface-update trigger (same shape as RFC 0115's harness-driven re-poll). A ?a2uiDelta=1 subscriber's reconstructed surface MUST equal the full surface a non-negotiating subscriber materializes for the same update.

16. Prompt-prefix cache generate driver — POST /v1/host/sample/ai/generate (RFC 0116)

FieldValue
Method + pathPOST /v1/host/sample/ai/generate
Capability gatecapabilities.aiProviders.promptPrefixCache.supported: true (RFC 0116)
Env gate (reference impl)OPENWOP_TEST_SEAM_ENABLED=true
IntroducedRFC 0116 §"Conformance"

OPTIONAL. The ctx.aiEnvelope.generate cachePrefixId hint, the (tenant, cachePrefixId) cache key, and the provider.usage.cacheReadTokens/cacheWriteTokens witness are all host-internal; a conformance harness cannot drive a tenant-scoped provider generate from the canonical wire alone. This seam routes a generate through the host's REAL envelope/provider path (the same path ctx.aiEnvelope.generate uses) so the harness can observe outcome-invariance, the cache-hit witness, and cross-tenant non-share. It MUST NOT be a mock that fabricates cache tokens or bypasses the tenant cache key.

  • Request: { "tenantId": string, "envelopeType": string, "systemPrompt": string, "cachePrefixId"?: string }. The tenantId selects the resolved tenant the host scopes the cache by (in production this comes from the authenticated identity — the seam supplies it explicitly for the cross-tenant leg). Omitting cachePrefixId is the control case.
  • Response 200: { "envelope": { "envelopeType": string, "payload": object, "envelopeId": string }, "usage": { "inputTokens": integer, "outputTokens": integer, "cacheReadTokens"?: integer, "cacheWriteTokens"?: integer } }envelope + inputTokens/outputTokens MUST be identical whether cachePrefixId hit, missed, or was absent (cost-hint-only); cacheReadTokens shows > 0 only on a cache HIT for the requesting tenant; cacheWriteTokens shows > 0 on a PRIME. The response MUST NOT echo cachePrefixId where SR-1 would redact, and MUST NOT carry prompt/response substrings in the usage block.
  • 404 / 405: seam not wired — the gated scenario (prompt-prefix-cache.test.ts) soft-skips its live-host legs.
  • Non-vacuity (RFC 0116): the witness is non-vacuous because the cache-token fields are emitted by the host's real provider-usage path and the (tenant, cachePrefixId) key is the host's real cache key — tenant B's first use of tenant A's cachePrefixId MUST return cacheReadTokens == 0 (the prompt-prefix-cache-cross-tenant-isolation invariant), while the recorded envelope + input/output tokens stay invariant.

17. Front-end plugin host-RPC driver — POST /v1/host/sample/ui-plugin/rpc (RFC 0117)

FieldValue
Method + pathPOST /v1/host/sample/ui-plugin/rpc
Capability gatecapabilities.uiPlugins.supported: true (RFC 0117)
Env gate (reference impl)OPENWOP_TEST_SEAM_ENABLED=true
IntroducedRFC 0117 §Conformance / frontend-plugin-packs.md §Host-RPC

OPTIONAL. The ui-plugin/1 host-RPC boundary (frontend-plugin-packs.md §Host-RPC) normally runs over postMessage between a sandboxed cross-origin iframe and the host; a conformance harness cannot stand up a real sandboxed plugin frame from the canonical wire alone. This seam routes a single ui-plugin/1 request through the host's real plugin-RPC dispatcher — the same handler the sandbox boundary uses — so the harness can exercise the closed hostApi allowlist (frontend-plugin-rpc-allowlist) and the version-token optimistic concurrency (§Concurrency) without a live iframe. It MUST NOT be a mock that bypasses the allowlist or fabricates the version token.

  • Request: { "message": <ui-plugin/1 request envelope> } — the message validates against ui-plugin-message.schema.json ({ openwop: "ui-plugin/1", type: "request", id, method, params? }).
  • Response 200: the ui-plugin/1 response envelope { "openwop": "ui-plugin/1", "type": "response", "id": integer, "ok": boolean, "result"?: object, "error"?: { "code": string, "currentVersion"?: string } }. An undeclared/unknown method MUST return { ok: false, error: { code: "method_not_allowed" } } (never silent execution). A stale or unknown artifact.write params.version MUST return { ok: false, error: { code: "artifact_conflict", currentVersion } } and MUST NOT persist; an absent artifact returns artifact_not_found. A successful artifact.write MUST return the new opaque version in result. The error.message, if present, MUST NOT carry secret material (frontend-plugin-no-byok).
  • 404 / 405: seam not wired — the gated scenario (frontend-plugin-packs.test.ts) soft-skips its live-host behavioral legs.
  • Non-vacuity (RFC 0117): the witness is non-vacuous because the host dispatches through its real allowlist + its real optimistic-concurrency store — method_not_allowed is produced by real enforcement (not a hard-coded reply), and artifact_conflict requires the host to have minted a current version the stale token compares against (a fixed conformance-canary artifact id is provisioned under the seam so the conflict path is exercised against a real current version). The version token is opaque and host-minted; the plugin round-trips it verbatim.

18. Parallel sub-workflow fan-out driver — POST /v1/host/sample/dispatch/fanout (RFC 0118)

FieldValue
Method + pathPOST /v1/host/sample/dispatch/fanout
Capability gatecapabilities.dispatch.fanOutSupported: true AND "parallel" ∈ capabilities.dispatch.fanOutPolicies (RFC 0118)
Env gate (reference impl)OPENWOP_TEST_SEAM_ENABLED=true
IntroducedRFC 0118 §Conformance / node-packs.md §"core.dispatch parallel fan-out and join"

OPTIONAL. The fanOutPolicy: 'parallel' path on core.dispatch (node-packs.md §"core.dispatch parallel fan-out and join") dispatches every nextWorkerIds[i] as a concurrent child run and joins on their terminals per joinPolicy; a conformance harness cannot stand up a full registered orchestrator run with real concurrent children from the canonical wire alone. This seam drives a single parallel fan-out through the host's real bounded-concurrency coordinator and join fold — the same coordinator the registered core.dispatch parallel branch uses — so the harness can exercise the join semantics + the replay-deterministic mergeOrder without a live multi-agent run. It MUST NOT be a mock that fabricates joinOutcome or returns nextWorkerIds order as mergeOrder.

  • Request: { "nextWorkerIds": string[] (length ≥ 2), "config": <DispatchConfig> }config validates against dispatch-config.schema.json and MUST carry fanOutPolicy: "parallel"; it MAY carry joinPolicy ({ mode, quorum?, onChildFailure }) and maxConcurrency. A config.fanOutPolicy other than "parallel", or a mode: "quorum" with a missing / out-of-range quorum (MUST be 1 ≤ quorum ≤ nextWorkerIds.length), MUST be rejected 4xx (the cross-field MUSTs the host also enforces at POST /v1/workflows).
  • Response 200: { "joinOutcome": "satisfied" | "failed" | "partial", "children": [ { "workflowId": string, "childRunId": string, "childStatus": "completed" | "failed" | "cancelled", "error"?: string } ], "mergeOrder": string[] }. joinOutcome + mergeOrder mirror the core.dispatch.join event ($defs.dispatchJoin, run-event-payloads.schema.json); children[] is the core.dispatch node-output shape (node-packs.md §Output shape). joinOutcome is 'satisfied' when the mode is met and onChildFailure did not fail the node, 'partial' when the mode is met but ≥1 child is non-completed under collect/absorb (node succeeds), 'failed' when fail-fast tripped or the mode is unsatisfiable (node fails). mergeOrder is the childRunIds in the parent host's observed wall-clock terminal order — the replay-deterministic tiebreak for colliding outputMapping keys (re-applied verbatim on :fork, never recomputed). children.length equals nextWorkerIds.length under wait-all.
  • 404 / 403: seam not wired — the gated scenario (dispatch-fanout-parallel.test.ts) soft-skips its live-host behavioral legs.
  • Non-vacuity (RFC 0118): the witness is non-vacuous because the host runs its real bounded-concurrency dispatch + its real join fold — joinOutcome: 'satisfied' under wait-all requires every dispatched child to have actually reached a terminal state (not a hard-coded reply), children.length ≥ 2 (childCount > 1 by construction), and mergeOrder reflects the order the host actually observed its children terminate. A host that returns nextWorkerIds order (instead of observed terminal order) fails the replay-determinism gate: when two children write the same parent variable via colliding outputMapping, the value MUST resolve to the LAST child in mergeOrder, so a dispatch-order mergeOrder yields the wrong winner.

19. Self-hosted runner driver — POST /v1/host/sample/runner/{register,dispatch} (RFC 0122)

FieldValue
Method + pathPOST /v1/host/sample/runner/register · POST /v1/host/sample/runner/dispatch
Capability gatecapabilities.selfHostedRunner.supported: true (RFC 0122)
Env gate (reference impl)OPENWOP_TEST_SEAM_ENABLED=true
IntroducedRFC 0122 §Conformance / self-hosted-runner.md

OPTIONAL. The runner↔host channel (self-hosted-runner.md) routes a run's per-step dispatch to a user-controlled runner over an SSE-receive + POST-result channel; a conformance harness cannot stand up a real remote runner from the canonical wire alone. This seam drives the host's real runner registry + dispatch router — the same match/dedup/liveness path a live runner uses — so the harness can exercise subject-first isolation, at-most-once dedup, and runner_unavailable without a live runner process. It MUST NOT be a mock that fabricates a match; the router MUST consult the same subject-first match and the same {runId, stepId} result store the production path uses.

  • register request: a SelfHostedRunnerRegistration { runnerId, subject, capabilities }. Records a runner bound to subject (the run's owning RFC 0048 principal). Response 200/201: { "runnerId": string }.
  • dispatch request: { subject, ...<SelfHostedRunnerDispatchFrame> } — the owning subject plus a dispatch frame ({ runId, stepId, seq, kind, provider?/model?/tool?, inputs }). The router matches a runner owned by subject FIRST, then capability.
  • Response 200 (routed): { "result": <SelfHostedRunnerResultFrame.output>, "deduped": boolean }. deduped is true iff a result for this {runId, stepId} was already persisted (the redelivery was dropped, not re-executed — at-most-once).
  • Response 4xx (no owning-subject runner): the canonical flat envelope { "error": "runner_unavailable", "message": "…", "details": { "retriable": true } } (corrected 2026-08-16, S22 — this line read the nested { error: { code, retriable } } shape before; the suite tolerates the nested shape from a seam through the first minor after 2026-11-10 while hosts converge). A dispatch for a subject with no registered runner MUST fail this way and MUST NOT fall back to another subject's runner (subject-first isolation).
  • 404 / 403: seam not wired — the gated scenario (self-hosted-runner.test.ts) soft-skips its behavioral legs.
  • Non-vacuity (RFC 0122): the witness is non-vacuous because the router runs the host's real subject-first match (registering a runner for subject B and dispatching for subject A yields runner_unavailable, proving no cross-subject fallback) and the real {runId, stepId} result store (a second identical dispatch returns deduped: true only if the first result was actually persisted). A credential MUST NOT appear on the dispatch frame, the returned result, or any event/log (runner-credential-non-transit; the frame schemas are additionalProperties:false).

20. Workload-identity resolution driver — POST /v1/host/sample/test/workload-identity/resolve (RFC 0154)

FieldValue
Method + pathPOST /v1/host/sample/test/workload-identity/resolve
Capability gatecapabilities.auth.workloadIdentity.supported: true (RFC 0154 §A)
Env gate (reference impl)OPENWOP_TEST_SEAM_ENABLED=true
IntroducedRFC 0154 §A/§B — closes the gap that made §A's requirements unobservable

OPTIONAL. RFC 0154 §A's requirements are behavioral and, without this seam, unobservable from the wire: a host must cryptographically verify the presented identity, bind it to the request, resolve it to an OpenWOP principal before authorization, and fail closed when it cannot. None of that is visible in a normal request's response — a call either succeeds or 401s, and both outcomes look identical whether the host verified anything or simply trusted a header.

That invisibility is the reason the seam exists. RFC 0148 §A resolves an unobservable requirement to blocked, not to a pass, so without this endpoint RFC 0154 cannot be certified at all — which is the honest position and is what the gated scenario reports.

The seam drives the host's real verification and resolution path. It MUST NOT be a mock that returns a canned principal: the resolver must be the same one the production request path consults, or the witness proves nothing about production.

  • Request: { identity: <WorkloadIdentity>, expectedAudience?: string } — a

workload-identity object. Because that schema is closed and forbids credential material, the seam cannot be handed a raw token even by a caller trying to.

  • Response 200 (resolved): { "principalId": string, "resolved": true }. principalId

is the OpenWOP principal the identity mapped to — opaque, and never the presented subject verbatim unless the host genuinely uses it as its principal ID.

  • Response 4xx (fail closed): the canonical flat envelope { "error": <code>, "message": "…", "details": { "retriable": false } } (corrected 2026-08-16, S22 — read nested before; tolerated during the window) with

a closed reason code — identity_unverified, identity_unresolvable, audience_mismatch, delegation_expired, sender_constraint_missing, or (chain bounds, added 2026-08-16 — RFC 0154 §B / auth.md §"Bounds") delegation_chain_too_long, delegation_chain_cyclic, delegation_scope_amplified. retriable MUST be false: an identity that does not resolve will not resolve on retry, and marking it retriable invites a caller to hammer a failing authorization path.

  • 404 / 403: seam not wired — workload-identity-behavior.test.ts reports the

requirement as blocked rather than skipping it quietly.

  • Non-vacuity (RFC 0154 §A): the witness is non-vacuous because the negative cases

cannot be satisfied by a host that merely echoes its input. An identity whose audience names a different host MUST yield audience_mismatch; an expired delegation.expiresAt MUST yield delegation_expired; and an identity presented without the sender constraint the host advertises MUST yield sender_constraint_missing. A host that returns resolved: true for any of those has demonstrated it is not checking, which is precisely the confused-deputy failure RFC 0147 R12 names. Identity is not authorization: a 200 here means the identity resolved, never that the caller may act.

  • **Chain bounds (RFC 0154 §B, workload-identity-chain-bounds.test.ts, gated on

auth.workloadIdentity.delegation.supported):** a delegation.chain longer than the advertised delegation.maxChainDepth MUST yield delegation_chain_too_long; a chain in which a subject appears twice MUST yield delegation_chain_cyclic; and a chain in which a later hop's scopes (OPTIONAL per-hop verified scopes, workload-identity.schema.json) contain a scope the previous hop's do not MUST yield delegation_scope_amplified. All three are fail-closed refusals of a verified-looking chain — a host that resolves any of them has let authority accumulate through hops it trusts transitively (threat model A4).

21. Compensation unwind + replay drivers — POST /v1/host/sample/test/compensation/{unwind,replay} (RFC 0151)

FieldValue
Method + pathPOST /v1/host/sample/test/compensation/unwind · POST /v1/host/sample/test/compensation/replay
Capability gatecapabilities.compensation.supported: true (RFC 0151 §A)
Env gate (reference impl)OPENWOP_TEST_SEAM_ENABLED=true
IntroducedRFC 0151 §C–§F witness (compensation-behavior.test.ts, suite 1.94.0). Catalogued 2026-08-16 — the witness had driven these two paths for three suite minors with no contract in this document, which meant a willing host had assertions to satisfy and no shape to build against.

OPTIONAL. RFC 0151 §C's rules — plan persisted before the first inverse action, descending forward-completion order, replay does not re-fire — are host-internal and unobservable from a normal run's wire except as the relative order of compensation.* events, which no black-box scenario can provoke without an effect that fails on cue. RFC 0148 §A resolves an unobservable requirement to blocked, so without these seams RFC 0151 cannot be certified; the gated scenario reports exactly that.

Both seams drive the host's real unwind path against deterministic fake effects. They MUST NOT be a mock that returns a canned event list: the executor, plan persistence, and ordering must be the ones the production failure path uses, or the witness proves nothing.

unwind — run a workflow of nodes (default 2, 1..8) forward-committing fake effects, fail the last node, and let the host unwind.

  • Request: { nodes?: integer, fail?: boolean }.
  • fail (OPTIONAL, default true, SP-11a 2026-08-18) — when false, the seam runs the

same compensator-declaring workflow to successful completion: no node fails, no trigger fires, and the host MUST record no compensation.requested. The returned snapshot MUST therefore read compensationStatus: "none" (compensation.md §"Run rollup"). This is the healthy-run case, and it is the one nothing observed: every other leg of this seam drives a failure, so a host that derived the rollup from row existence rather than plan state reported pending on a run that had nothing to unwind and passed every existing witness. A host that does not honour fail: false — it fails the node anyway, or rejects the field — leaves compensation-behavior.test.ts to record that leg blocked, not to pass it.

  • Response 200: { runId, events, compensatedOrder }.

- runId — the run the seam created, so the scenario can GET /v1/runs/{runId} and assert the §D rollup: after a clean unwind the snapshot MUST carry compensationStatus: "completed" (see compensation.md §"Run rollup"). A host that advertises the family and omits the field on that snapshot fails the gating rule, not merely the fold. - events — the run's compensation.* events in emission order, each at least { type, payload }; compensation.requested MUST precede compensation.started. - compensatedOrder — the forward-completion ordinals (1-based) in the order their inverse actions were executed; for reverse-completion this MUST be strictly descending.

  • Non-vacuity: the fake effects MUST record their inverse execution so

compensatedOrder is what happened, not what was scheduled; and payloads MUST be content-free — the witness rejects a serialized event containing credential or provider-body markers (-----BEGIN, Bearer , sk-, authorization, providerResponse).

replay — replay a run whose recorded outcomes include a completed unwind.

  • Request: {}.
  • Response 200: { runId, refiredEffects }refiredEffects MUST be 0 (RFC 0151 §F:

replay defaults use recorded compensation outcomes and never re-fire inverse effects). The replayed run's snapshot MUST carry the recorded rollup unchanged.

  • 404 / 403: seam not wired — compensation-behavior.test.ts fails the plan-before-effect

leg with a message stating the requirement is unobservable and therefore blocked, and returns early on the remaining legs (covered by that assertion, not by silence).

Recovery extension (2026-08-16 — the witness for RFC 0151 §C retry-stability, §E operator authority, and §B/§F recorded-facts; compensation-recovery.test.ts). RFC 0151 §G names compensation-effect-id-retry-stable, compensation-tenant-authority-bound, and compensation-input-recorded-facts-only; none is observable through the two seams above, so SECURITY/threat-model-compensation.md §7 left them named-not-registered. This extension is the seam each one needs. All three sub-features are OPTIONAL and independently blocked (compensation-recovery.test.ts reports which is absent); a host that wires the base seams but not this extension keeps its RFC 0151 §C/§D/§F witness.

  • unwind request gains failFirstInverseAttempts?: integer 0..3 and hold?: boolean.

- failFirstInverseAttempts: n — the first inverse action to run (the highest forward ordinal under reverse-completion) fails transiently n times, then succeeds. The seam MUST run it under retry bounds of at least n + 1 attempts so the unwind still completes. This is the only way to observe §C's rule that a retry re-presents the same identity. - hold: true — the first inverse action fails permanently and the plan's disposition is manual-intervention: compensation.manual_intervention_required is emitted, the snapshot reads compensationStatus: "manual", and the returned runId is a held plan the operator seam below can act on.

  • unwind response gains `inverseActions: [{ ordinal, effectId, attempts, outcome,

downstreamKeys }] — one entry per plan entry, in execution order. effectId is the inverse-action identity (§C tuple, opaque); attempts the attempts made; outcome one of completed | failed | skipped | terminated | held | irreversible (irreversible = a committed node declaring irreversibleEffect: true, compensation.md §B — never runs, never completes); downstreamKeys the idempotency key the fake downstream received on each attempt (length === attempts). Non-vacuity: with failFirstInverseAttempts: 2 the witness expects attempts: 3, a single distinct value across downstreamKeys`, and exactly one plan entry per ordinal — one obligation, three attempts.

  • replay request gains runId?: string (replay that run; default: a fresh completed

unwind as before) and response gains source: [{ ordinal, effectId, input }] and replayed: [{ ordinal, effectId, input }] — the inverse actions of the source run and of the replay, with the input each was executed with. Non-vacuity: replayed MUST deep-equal source (same identities, same inputs — §B/§F: an inverse built from a re-derived value is not the inverse of what was done) while refiredEffects stays 0.

  • NEW POST /v1/host/sample/test/compensation/operator — drives the host's real §E

operator path against a held plan, as a presented actor. - Request: { runId, action: "retry" | "skip" | "substitute" | "terminate", justification?: string, nodeTypeId?: string, actor: { tenantId, principalId, operator: boolean } }. actor is the seam's stand-in for the caller's authenticated context; the host MUST evaluate it through the same RFC 0049 decision the production operator path uses (operator: true = holds operator authority in actor.tenantId; nothing else). skip requires a non-empty justification; substitute requires a registered nodeTypeId. - 200: { runId, action, compensationStatus, planVersion, audited: true }compensationStatus is the snapshot value after the action (§D fold; e.g. retry on a held plan whose retried inverse succeeds → completed; terminatepartial / failed), planVersion increments on substitute only, and audited: true means an authorization.decided record was written for the override. - 404 not_found: actor.tenantId is not the plan's tenant — the RFC 0132 §A.2 rule: neutralize to the actor's tenant, do not reveal that another tenant's plan exists. - 403 forbidden: same tenant, operator: false — and the refusal MUST also be audited (authorization.decided, reason: authority-denied on the plan). details.retriable: false. - 409 compensation_action_invalid: the action's precondition fails (e.g. retry on a completed action, skip without justification, substitute with an unregistered nodeTypeId). - Non-vacuity: the witness first presents a cross-tenant actor (expects 404), then a same-tenant non-operator (expects 403), then the operator (expects 200) — a seam that answers 200 to all three has demonstrated it consults nothing.

22. A2A negotiation + durable-task drivers — POST /v1/host/sample/a2a/{invoke,tasks/start,tasks/push-config} · GET /v1/host/sample/a2a/tasks/{taskId} (RFC 0152 §B, RFC 0100)

FieldValue
Method + pathPOST /v1/host/sample/a2a/invoke · POST /v1/host/sample/a2a/tasks/start · GET /v1/host/sample/a2a/tasks/{taskId} · POST /v1/host/sample/a2a/tasks/push-config
Capability gateinvoke: capabilities.a2a.supported && protocolVersions.length > 0 (RFC 0152 §A). tasks/*: capabilities.a2a.durableTasks / capabilities.a2a.pushNotifications (RFC 0100).
Env gate (reference impl)OPENWOP_TEST_SEAM_ENABLED=true
Introduceda2a-task-roundtrip.test.ts (RFC 0100, suite 1.34.0) and a2a-version-negotiation.test.ts (RFC 0152 §B, suite 1.96.0). Catalogued 2026-08-16 — both had driven these paths without a contract here. Corrected the same day: openwop-app has NOT wired invoke (its tasks/* seams exist); the §B legs are blocked against it, which is the honest reading of a seam nobody could build to a contract that did not exist.

OPTIONAL. RFC 0152 §B's rules are about what the host puts on the wire toward a peer — the A2A-Version header, and whether a downgrade was explicit — which no black-box request to the host's own API can observe. The invoke seam makes the host call a peer of the suite's choosing so the peer can capture the headers.

invoke — make the host's real A2A client path call peerUrl once (any operation; the witness only reads headers and the outcome).

  • Request: { peerUrl: string, authenticated?: boolean, peerOffersOnly?: "<Major.Minor>", requestVersion?: "<Major.Minor>", scenario?: "peer-asserts-authority" }. scenario (additive, 2026-08-16, RFC 0152 §E): with "peer-asserts-authority" the suite's peer answers with an agent message that asserts approval (metadata.openwop.approval: "accept"), scopes, an interrupt resolution, and a foreign referenceTaskIds[]; the host runs its normal client path and reports peerAuthority: { approvalAdvanced: boolean, scopesWidened: boolean, referencedTaskDereferenced: boolean } on the 200 body — all three MUST be false (a2a-peer-authority.test.ts; a host that does not yet report the block is recorded blocked, not passed). authenticated: true asks the host to make the call under an authenticated principal (§B's fail-closed default applies); peerOffersOnly is passed to the fake peer so it advertises/accepts only that version; requestVersion overrides the version the host asks for (used to force an unsupported one).
  • Response 200: { negotiatedVersion: "<Major.Minor>", ... } — the version the host actually used, which MUST equal the A2A-Version header on every non-GET call the peer captured. Reporting preferredVersion while having used a lower one is the silent downgrade §B forbids.
  • Response ≥ 400: the canonical (flat) error envelope — for an unsupported version, error: "interop_version_unsupported", retriable: false, details.protocol: "a2a", details.requested, details.supported[] (a2a-integration.md §B). A raw upstream body is a failure of the leg.
  • 404 / 403: seam not wired — the header leg fails with a message stating the requirement is unobservable and therefore blocked (RFC 0148 §A); the remaining legs return early, covered by that assertion.
  • Non-vacuity: the seam MUST drive the same A2A client the production a2a.invoke path uses (same negotiation code, same header construction); a seam that hand-writes A2A-Version proves nothing about production.

tasks/start{ scenario: "paused-at-approval" }200 { taskId }: drive a real backing run to a paused HITL state and return the A2A task id (which is the run id, RFC 0100). tasks/{taskId}200 { state, runId, metadata?: { openwop?: { interrupt?: { kind } } } }: the persisted A2ATaskState projection read without the original connection; state uses the stored (0.3-lowercase) vocabulary, input-required for the paused run. tasks/push-config{ taskId, url }≥ 400 for a private/loopback/link-local url before any push is attempted (a2a-push-egress-ssrf); the seam MUST run the same RFC 0093 egress guard as the production CreateTaskPushNotificationConfig path.

23. MCP revision-negotiation driver — POST /v1/host/sample/mcp/invoke (RFC 0153 §B)

FieldValue
Method + pathPOST /v1/host/sample/mcp/invoke
Capability gatecapabilities.mcp.supported && capabilities.mcp.protocolVersions.length > 0 (RFC 0153 §A)
Env gate (reference impl)OPENWOP_TEST_SEAM_ENABLED=true
Introducedmcp-version-negotiation.test.ts (RFC 0153 §A/§B, suite 1.97.0). Catalogued 2026-08-16 — driven without a contract here. Corrected the same day: openwop-app has NOT wired this seam; its §A legs pass and its §B legs are blocked (RFC 0148 §A).

OPTIONAL. Same shape and reason as §22's A2A invoke: RFC 0153 §B is about the MCP-Protocol-Version header (and _meta) the host puts on the wire toward a server, which no request to the host's own API can observe. The seam makes the host's real MCP client call serverUrl once so McpFakeServer can capture headers.

  • Request: { serverUrl: string, requestVersion?: "<YYYY-MM-DD>", tool?: string, arguments?: object, clientCapabilities?: object, elicitationAnswer?: object, scenario?: "extension-asserts-authority" }. requestVersion overrides the revision the host asks for (used to force an unsupported one). Additive fields (2026-08-16, RFC 0153 §C/§D): tool names the tool to call on the suite's server (default: whatever the host's client path calls; the suite's server offers echo and the MRTR tool needs_input), arguments its arguments, clientCapabilities the _meta.io.modelcontextprotocol/clientCapabilities the host declares for the call (the MRTR leg passes { elicitation: {} }), elicitationAnswer a programmatic answer the seam MAY use to resolve the clarification interrupt the input_required result raises (in production a human answers it), and scenario: "extension-asserts-authority" asks the host to run its normal client path against a result whose _meta asserts authority.
  • Response 200: { negotiatedVersion: "<YYYY-MM-DD>", ... } — the revision the host actually used, in MCP date form; it MUST equal the MCP-Protocol-Version header (and _meta revision) on every call the server captured and MUST be one the host lists in protocolVersions. Reporting preferredVersion while having sent a lower one is the silent downgrade §B forbids.

Additive response blocks (2026-08-16): for tool: "needs_input"mrtr: { inputRequiredSeen: boolean, retried: boolean, requestStateEchoed: boolean, result?: unknown } (mcp-mrtr-roundtrip.test.ts cross-checks retried/requestStateEchoed against what the suite's server actually received, so the report cannot outrun the wire); for scenario: "extension-asserts-authority"extensionAuthority: { scopesWidened: boolean, approvalAdvanced: boolean }, both MUST be false (mcp-extension-opacity.test.ts). A host that does not yet report a block is recorded blocked for that requirement, not passed.

  • Response ≥ 400: the canonical (flat) error envelope — for an unsupported revision, error: "interop_version_unsupported", retriable: false, details.protocol: "mcp", details.requested, details.supported[].
  • 404 / 403: seam not wired — the header leg reports the requirement as unobservable and therefore blocked (RFC 0148 §A).
  • Non-vacuity: the seam MUST drive the same MCP client the production ctx.mcp.* path uses (same _meta construction, same header construction); a seam that hand-writes MCP-Protocol-Version proves nothing about production. Under the current profile the call MUST be stateless — no initialize before it.

24. Sample-workflow registration — POST /v1/host/sample/workflows (used by the MCP server-mount legs, dispatch mapping legs, and the RFC 0153 §C MRTR server half)

FieldValue
Method + pathPOST /v1/host/sample/workflows
Capability gatenone of its own — each calling leg gates on the capability it exercises
Env gate (reference impl)OPENWOP_TEST_SEAM_ENABLED=true
Introduceddispatch-output-mapping.test.ts, mcp-server-*-roundtrip.test.ts, mcp-mrtr-roundtrip.test.ts. Catalogued 2026-08-16 (S17) — driven for months without a contract here.

OPTIONAL. Registers a workflow the host will serve for the duration of the conformance run (an exposed MCP tool/resource/prompt, a dispatch parent, an MRTR suspending tool). The seam accepts the host's registration shape{ workflowId, nodes: [{ nodeId, typeId, config, inputs? }], edges?: [{ edgeId, sourceNodeId, targetNodeId }] } — and answers 201 { workflowId, nodeCount }, 400 validation_error on a malformed body, 404/403 when the seam is not mounted.

Recorded divergence. This shape keys nodes and edges by nodeId / edgeId; the canonical workflow-definition.schema.json keys both by id. Every leg posting here has used nodeId since the seam existed, so the seam is documented as it behaves rather than as the canonical schema would have it. The RFC 0013 workflow-chain vocabulary (edges: [{ from, to }]) is NOT accepted here — mcp-mrtr-roundtrip.test.ts posted it until 2026-08-16 and the first 2026-07-28 host answered 400, which the leg then walked past into tools/call on a tool that was never registered (S17). A calling leg MUST treat a 4xx on this registration as a suite defect (assert < 400), never as seam absence. Aligning the seam to canonical id (accepting both during a window) is an open item for the reference host; the corpus does not change the canonical schema for it.

25. Concurrent duplicate-delivery driver — POST /v1/host/sample/test/idempotency/concurrent-claim (RFC 0150 §B)

FieldValue
Method + pathPOST /v1/host/sample/test/idempotency/concurrent-claim
Capability gatenone — the obligation is unconditional (idempotency.md §"Concurrent duplicates (Layer 2)")
Env gate (reference impl)OPENWOP_TEST_SEAM_ENABLED=true
IntroducedRFC 0150 §B witness, gap G17. The invariant layer2-invocation-claim-atomic shipped with an empty tests list because nothing could drive it; this seam is what closes that.

OPTIONAL. idempotency.md §"Concurrent duplicates (Layer 2)" requires the persist guarding a side effect to be an atomic claim — a compare-and-set / insert-if-absent that at most one executor can win — and says it MUST hold within a single-instance deployment, naming the orphan-sweep-races-a-live-owner case. Nothing in the corpus could witness it: driving the rule needs two executors of one run concurrently reaching one effect seam, which a black-box suite cannot cause. Per RFC 0148 §A that resolves to blocked, and a wire probe asserting it would pass on a host that has the defect.

The seam drives the host's real claim path against a deterministic fake effect. It MUST NOT be a mock that returns a canned count: the invocation log, the claim, and the effect chokepoint must be the ones the production path uses, or the witness proves nothing.

  • Request: { executors?: integer } — concurrent executors of one logical invocation

(default 2, range 2..8).

  • Response 200: { logicalInvocationId, mintedIds, attempted, delivered }.

- mintedIds — the logicalInvocationId each executor minted, in start order, length attempted. - attempted — executors that reached the chokepoint and tried to fire. - delivered — effects that actually escaped.

Both assertions are required, and the second is why this seam exists. A scenario MUST assert delivered === 1, and that every entry in mintedIds is identical. Without the identity assertion a host passes by minting different identities and never colliding — delivered === 1 because nothing raced. That is a vacuous pass wearing a green check, and it is exactly the condition idempotency.md §"Idempotency key composition" → "Across a recovery boundary" describes: the ordinal reproduces iff the resumed unit re-executes the node's logical activities from the start, and a host that resumes mid-node shifts every downstream identity and defeats Layer-2 dedup on the crash it most needs to survive. A seam reporting one id per executor makes that failure visible instead of green.

Reproducing the race is the hard part, and a true statement can stand in the way of it.

Reported 2026-09-02 by a tier-1 host whose emitter carried the note *"within one process the

shared ordinal counter hands the second emit a DIFFERENT identity, so it is not reproducible

single-instance."* True of two sequential emits — and it became a reason nobody tried. The

counter is module-level and monotonic, so two un-awaited calls both mint the same ordinal:

identity minting runs synchronously before the first await. That is not a trick; it is what a

real re-dispatch does, since each executor re-executes the node from the start. Measured on that

host: 2 effects delivered without the claim, 1 with it. A host implementing this seam that

cannot make attempted > delivered happen with the claim removed has not reproduced the race,

and its green result means nothing — that negative control is the seam's own acceptance test.

A host that does not mount this seam leaves the scenario recording blocked (unwitnessed), never inapplicable: the requirement applies to every host, so its absence is missing evidence rather than a requirement that does not bind.