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. Seeauth.mdfor 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.composedevent 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 → hexcomputation - "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)
| Field | Value |
|---|---|
| Method + path | POST /v1/host/sample/prompt/resolve |
| Capability gate | capabilities.prompts.supported: true |
| Env gate (reference impl) | seam registered when capabilities.prompts.supported is asserted |
| Introduced | RFC 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)
| Field | Value |
|---|---|
| Method + path | GET /v1/host/sample/test/otel/spans?runId=<id> |
| Capability gate | capabilities.observability.testSeams.otelScrape: true |
| Env gate (reference impl) | OPENWOP_TEST_OTEL_SCRAPE=true |
| Introduced | RFC 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 anyopenwop.*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)
| Field | Value |
|---|---|
| Method + path | POST /v1/host/sample/test/debug-bundle/export |
| Capability gate | capabilities.observability.testSeams.debugBundleExport: true |
| Env gate (reference impl) | OPENWOP_TEST_DEBUG_BUNDLE_EXPORT=true |
| Introduced | RFC 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.md — DebugBundle 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)
| Field | Value |
|---|---|
| Method + path | POST /v1/host/sample/test/llm-cache-key |
| Capability gate | capabilities.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 |
| Introduced | RFC 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)
| Field | Value |
|---|---|
| Method + path | POST /v1/host/sample/test/mock-ai/program |
| Capability gate | capabilities.multiAgent.executionModel.replayDeterminism.refusalDivergenceEmission: true (RFC 0041 Phase 4) |
| Env gate (reference impl) | OPENWOP_TEST_SEAM_ENABLED=true |
| Introduced | RFC 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)
| Field | Value |
|---|---|
| Method + path | POST /v1/host/sample/test/multi-region/simulate-partition |
| Capability gate | capabilities.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 |
| Introduced | RFC 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)
| Field | Value |
|---|---|
| Method + path | 3 endpoints (see below) |
| Capability gate | capabilities.eventLog.crossEngineOrdering.supported: true (RFC 0036 §B) |
| Env gate (reference impl) | OPENWOP_TEST_CROSS_ENGINE_HARNESS=true |
| Introduced | RFC 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 onappendis the engine's view of the OTHER engine's clock (incoming hint); honored per the lamport receive rule readlinearizes by(lamport ASC, engineId ASC, seq ASC)— a deterministic total order- Hosts advertising a different
orderingModel(vector-clock,global-sequencer, orx-host-<host>-<key>) MAY substitute their own algorithm but MUST honor the sameappend/read/resetcontract
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)
| Field | Value |
|---|---|
| Method + path | 2 endpoints (see below) |
| Capability gate | capabilities.sandbox.supported: true (RFC 0035 §A) |
| Env gate (reference impl) | OPENWOP_TEST_SANDBOX_MVP=true |
| Introduced | RFC 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:
| typeId | Failure mode it probes |
|---|---|
misbehave.fs-escape-read | sandbox_escape_attempt + escapeKind: host-fs-escape |
misbehave.fs-escape-write | sandbox_escape_attempt + escapeKind: host-fs-escape |
misbehave.env-leak | sandbox_escape_attempt + escapeKind: host-env-leak |
misbehave.network-escape | sandbox_escape_attempt + escapeKind: network-escape |
misbehave.process-escape | sandbox_escape_attempt + escapeKind: host-process-escape |
misbehave.timeout | sandbox_timeout |
misbehave.memory-bomb | sandbox_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-violation | sandbox_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)
| Field | Value |
|---|---|
| Method + path | POST /v1/host/sample/workspace/op |
| Capability gate | capabilities.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) |
| Introduced | RFC 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)
| Field | Value |
|---|---|
| Method + path | POST /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 gate | capabilities.connections.packsSupported: true (RFC 0095 §C) |
| Env gate (reference impl) | seam registered when connections.packsSupported is asserted; production hosts gate per §"Production safety" |
| Introduced | RFC 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)
| Field | Value |
|---|---|
| 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 gate | capabilities.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). |
| Introduced | RFCs 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? }— thectx.callTranscriberpath. A host whose live-stream transport is host-internal per §E MUST honestly reject a liveaudio.streamRefwithtranscription_unsupported(the deterministic arm, underOPENWOP_TEST_SEAM_ENABLED, forces amockprovider and returns the canonicalvoice.*turn). A produced turn resolves a non-emptyfinalTextatvoice.turn_commit; every emittedvoice.transcriptcarriescontentTrust: "untrusted"(§F INV-2).POST /v1/host/sample/ai/call-speech-synthesizerwith{ …, stream: true }— the §C streaming arm. Returns the finalized RFC 0105audioasset (exactly one ofurl/base64) AND anevents[]array ofvoice.synthesis_chunkrun-events carrying metadata only (seq/mimeType; bytes byurl/streamRef, inlinebase64only under the host cap — G8), terminated byfinal: true.POST /v1/host/sample/voice/barge-in— emits avoice.*sequence demonstratingvoice.barge_in→voice.cancelledwith NOvoice.synthesis_chunkafter the cancel (the §F INV-3voice-bargein-no-partial-leakproof).
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 Foundfrom 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.mdevidence 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
200rather 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:
| Hook | Env var | Realizes |
|---|---|---|
| Past-retention run | OPENWOP_TEST_EXPIRED_REPLAY_RUN_ID | A 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 inmulti-agent-memory-lifecycle.test.tsstaysit.todountil a memory-advertising Phase 2 host wires the seam. - Credential resolution + redaction seam (RFC 0046) —
POST /v1/host/sample/credentials/echo. Gated oncapabilities.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 incredential-payload-redaction.test.tsasserts the canary is absent from every returned surface (SECURITY invariantcredential-payload-redaction); soft-skips on404until a credentials-advertising host wires the seam. - OAuth connector-echo seam (RFC 0047) —
POST /v1/host/sample/oauth/connector-echo. Gated oncapabilities.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 theconnector.authorizedevent) are returned.oauth-connector-redaction.test.tsasserts the token canary is absent from every surface and thatconnector.authorizedcarries the credential reference, not the token (reuses thecredential-payload-redactioninvariant); soft-skips on404. - Run-ownership seam (RFC 0048) —
GET /v1/host/sample/identity/owned-run. Contract: return aRunSnapshotthat carries anownertriple.cross-workspace-isolation.test.tsasserts the owner echo carries a non-emptytenant; soft-skips on404(or whenowneris omitted by a single-tenant host). - Cross-workspace isolation seam (RFC 0048 §D) —
POST /v1/host/sample/identity/cross-workspace-read. Contract: aprincipalscoped to workspace A attempts to read a run owned by workspace B.cross-workspace-isolation.test.tsasserts the read fails closed withrun_forbidden/not_found(no existence leak); soft-skips on404until a workspace-ownership host wires the seam. - Authorization-decision seam (RFC 0049 §C) —
POST /v1/host/sample/authorization/decide. Gated oncapabilities.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.tsasserts the deny (SECURITY invariantauthorization-fail-closed); soft-skips on404until an authorization-advertising host wires the seam. - SAML assertion-validation seam (RFC 0050) —
POST /v1/host/sample/auth/saml/validate. Gated oncapabilities.auth.profiles[]includesopenwop-auth-saml+ an operator-supplied synthetic IdP (OPENWOP_TEST_SAML_IDP_URL). Contract: present an assertion of a namedvariant(valid,alg-none,bad-signature,unsigned,expired,not-yet-valid,signature-wrapping); the host MUST acceptvalidand reject every negative withunauthenticated. The request MAY carrynameId(the persistentNameIDthe minted assertion asserts) and theidpUrlnames 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). The2xxresponse body carriesauthenticated: booleanand, 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.tsdrives 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 on404/ absent env. - SCIM provisioning seam (RFC 0050) —
POST /v1/host/sample/auth/scim/provision. Gated oncapabilities.auth.profiles[]includesopenwop-auth-scim+ an operator-supplied SCIM endpoint (OPENWOP_TEST_SCIM_URL). Contract: drive a SCIMcreate-user/assign-group/deactivate-userop; the host MUST upsert an RFC 0048 principal / RFC 0049 role and deny a deactivated principal's subsequent decisions.auth-scim-profile.test.tsdrives the roundtrip; soft-skips on404/ absent env. RFC 0159 / RFC 0163 extensions:create-userMAY carryexternalId(the opaque IdP-stable id the SCIM lane records) andidpUrl(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-userMAY address the user byexternalIdoremail; alinkopwithlinkKey(email| an attribute name) asks the host to form a cross-lane link on that key, and the host MUST reject a mutable/PIIlinkKeywith4xxor, if it accepts the request, MUST NOT apply any cross-lane effect from it (RFC 0159 §A.2).auth-subject-link.test.tsandauth-subject-link-key-class.test.tsdrive these; soft-skip on404/ absent env. - Approval-gate seam (RFC 0051) —
POST /v1/host/sample/governance/approval-gate. Gated oncapabilities.authorization.supported. Contract: drive a namedscenario(unauthorized-grant,grant,reject,override,quorum) against acore.openwop.governance.approvalGatenode; the host returns{ released, event }reflecting the outcome (an unauthorized principal MUST NOT release;overrideMUST emitapproval.overriddenwith areason+ an audit entry).approval-gate-flow.test.tsdrives unauthorized + override-audited; soft-skips on404until a governance-advertising host wires the seam. - Scheduling tick seam (RFC 0052) —
POST /v1/host/sample/scheduling/tick. Gated oncapabilities.scheduling.supported+cron: true. Contract: advance a deterministic clock for a namedscenario(single-tick,missed-windowwithmissedTicks) and return{ runsFired }— the count of runs a cron schedule produced. The host MUST reportrunsFired === 1for a single tick (once-per-tick) andrunsFired <= 1for a missed window (no backlog flood).scheduling-cron-fires-once.test.tsdrives both; soft-skips on404until a scheduling host wires the seam. (Delayed-execution horizon + calendar scenarios deferred.) - Heartbeat tick seam (RFC 0060) —
POST /v1/host/sample/heartbeat/tick. Gated oncapabilities.heartbeat.supported. Contract: evaluate a heartbeat predicate once for a request{ heartbeatId, observedState, simulateSlowMs? }(simulateSlowMsasks the predicate to overrunmaxRuntimeMs, exercising the §B.2 timeout path) and return{ evaluated: HeartbeatEvaluated[], stateChanged: HeartbeatStateChanged[], enqueuedRuns: number }— exactly oneevaluatedper tick (§B.1);stateChanged+enqueuedRunsnon-empty/non-zero ONLY whenobservedStatediffers from the prior tick's persisted state (§B.5, the anti-spam guarantee);evaluated[].status === "timeout"whensimulateSlowMsexceeds the budget (§B.2).heartbeat-fires-once-per-tick.test.ts/heartbeat-idempotent-no-spam.test.ts/heartbeat-runtime-bound.test.tsdrive these; soft-skip on404until a heartbeat host wires the seam. - Tool-hooks invoke seam (RFC 0064) —
POST /v1/host/sample/toolhooks/invoke. Gated oncapabilities.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 existingagent.toolCalled/agent.toolReturnedevents).toolReturned.statusMUST beforbiddenwhen the principal lacks arequiredScopesentry (or authz is unevaluable — fail-closed, RFC 0049),rate_limitedwhensimulateRateLimitExhausted,errorwith a populatederror(_errorObject { code, message }, SR-1-redacted) and a non-negativedurationMswhensimulateToolError(the RFC 0064 §F tool-failure-honesty arm: the tool ran and failed), elseokwith a non-negativedurationMs;errorandoutcomeare mutually exclusive;toolCalled.argsHashMUST 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.tsdrive these; soft-skip on404(or, for the §F arm, until the host wiressimulateToolError) until a tool-hooks host wires the seam. - Sub-run attestation seam (RFC 0063) —
POST /v1/host/sample/subrun/attest. Gated oncapabilities.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? }— theattestation { checksum, algorithm }the host would surface oncore.workflowChain.event { phase: 'output.harvested' }, whether the merge proceeded, and the merged values. ThechecksumMUST be the RFC 8785 JCS + SHA-256 digest ofchildOutputs(byte-stable for identical inputs, host-independent). WhenrequireApproval: true,mergedMUST betrueonly forapprovalActionaccept/edit-acceptand MUST befalse(fail-closed) forrejector an absent/expired approval.subrun-checksum-stable.test.ts/subrun-approval-gate.test.ts/subrun-approval-fail-closed.test.tsdrive these; soft-skip on404until a sub-run-attestation host wires the seam. - Memory-distillation seam (RFC 0062) —
POST /v1/host/sample/memory/distill. Gated oncapabilities.memory.distillation.supported. Contract: run one budgeted distillation for a request{ memoryRef, tokenBudget?, sources?, indexEmitted?, includeSecretCanary? }and return{ event, archiveChecksum, indexUpdated, indexFile? }— thememory.compactedevent the host would emit (carrying the additivedistillation { tokenBudget, tokensUsed, indexUpdated }sub-object) plus the stable archive's checksum.event.distillation.tokensUsedMUST be ≤ the resolvedtokenBudget; an un-meetable budget MUST returntoken_budget_exceededwith no partial archive (atomic). The samesources+tokenBudgetMUST yield an identicalarchiveChecksum(byte-stable). WhenindexEmitted, aMEMORY-INDEX.jsonworkspace file MUST be retrievable and aworkspace.updatedevent fired. WhenincludeSecretCanary, 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.tsdrive these; soft-skip on404until a distillation host wires the seam. - Dead-letter exhaustion seam (RFC 0053) —
POST /v1/host/sample/deadletter/exhaust. Gated oncapabilities.deadLetter.supported. Contract: drive a node that deterministically exhausts a short retry policy for a namedscenario(exhaust-retries,fork-after-dead-letter); the host returns{ event, forkEligible }— therun.dead_letteredevent (carryingattempts) and whether the dead-lettered run is forkable.deadletter-retry-exhaustion.test.tsdrives both; soft-skips on404until 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 oncapabilities.multiAgent.executionModel.version >= 5. Contract: drive a bounded stateful loop for a request{ turns, workspaceWriteAtTurn?, suspendAtTurn?, resume? }and return{ decisions, workspaceVisible?, resumedIteration? }— the orderedrunOrchestrator.decidedpayloads the host would emit (each carrying theiterationcounter).decisions[k].iterationMUST equalk+1(1-based, monotonic, one per turn). WhenworkspaceWriteAtTurn: iis set (requireshost.workspace.supported),workspaceVisibleMUST report the write invisible to turn _i_'s snapshot and visible to turn _i+1_ (§C input 2). WhensuspendAtTurn+resumeare set (requiresstatefulResume: true),resumedIterationMUST 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.tsdrive these; soft-skip on404until 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 on404. Contract: evaluate a candidate manifest'sruntime.requires[]against a simulated host grant-set for a request{ manifest, grantSet?, gating? }and return the install-time outcome. Whengating !== false(sandbox host): if everyruntime.requiresentry is ingrantSetthe host MUST return200 { outcome: "installed" }; if any entry is not granted the host MUST refuse at install with400 { error: "pack_runtime_requirement_unmet", unmet: [...], manifest: "<name>@<version>", advice? }(thecapability_not_providedenvelope shape) — NOT install-and-fail-at-first-invocation. Whengating: false(non-sandbox host) the host installs unconditionally and SHOULD return200 { outcome: "installed", requiresProjected: [...] }, the declared requirements projected onto the inventory entry for operator visibility.runtime-requires-install-gate.test.tsdrives install-grant / install-refuse / non-sandbox-projection; soft-skip on404until 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 byruntime-requires-shape.test.ts. - Safe-fetch seam (RFC 0076 §B) —
POST /v1/host/sample/http/safe-fetch. Gated oncapabilities.httpClient.safeFetch.supported; soft-skips on404. Contract: evaluate onectx.http.safeFetchcall 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 asimulateRebindTothat re-resolves a public name to a blocked address (DNS-rebinding); MUST return{ outcome: "blocked", blocked: "upgrade" }wheninit.headersrequestsConnection: upgrade; else{ outcome: "fetched", status }. Whencapabilities.toolHooks.prePostEventsis also advertised, a fetched call MUST include the{ toolCalled, toolReturned }pair (transport: "http").safefetch-behavior.test.tsdrives SSRF-block / rebinding / upgrade-refusal / audit-when-both; soft-skip on404until asafeFetchhost wires the seam. - Safe-fetch live-run audit seam (RFC 0076 §B / RFC 0064 §B) —
POST /v1/host/sample/http/safe-fetch-run. Gated oncapabilities.httpClient.safeFetch.supported+capabilities.toolHooks.prePostEvents(both); soft-skips on404. Distinct from the inlinesafe-fetchseam above: this seam executes onectx.http.safeFetchcall inside a real run through the host's _production_ per-ctx injection path (the samectx.http.safeFetcha node receives at dispatch), then returns{ runId, outcome }. Contract: for a request{ url, init? }the host MUST run onectx.http.safeFetchin a real run and return200 { runId, outcome }whereoutcomeis"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 viaGET /v1/host/sample/test/runs/:runId/eventsand asserts acallId-pairedagent.toolCalled(transport: "http") /agent.toolReturnedwas persisted. The audit pair MUST be persisted for _every_ invocation —blockedas well asfetched(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.tsexploits 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 insafefetch-behavior.test.ts(whose audit assertion reads only the inline seam echo): a host can pass the inline seam yet ship a productioncreateSafeFetch()with no audit hooks — the "quiet bypass" §host.http forbids.safefetch-live-audit.test.tsdrives it viabehaviorGate('openwop-safefetch-live-audit', …)so a host advertising both flags but not emitting to the durable log FAILS underOPENWOP_REQUIRE_BEHAVIOR=true; the seam itself soft-skips on404(host-pending) until asafeFetchhost 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 §BcallIdpairing +causationId), not captured-and-echoed inline like the non-runsafe-fetchseam 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.ts→queryTestEvents) —GET /v1/host/sample/test/runs/:runId/events. Conformance-only, env-gated; soft-skips on404(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) arunIdoutside the caller's{tenant, workspace}, so the test seam is never a weaker cross-tenant disclosure path than production (matches theidentity// 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.shcovers production surfaces, not conformance-only test seams, so no protocol-tier invariant gates this MUST; it inherits the same cross-tenant intent as the productionworkspace-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 bysafefetch-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 oncapabilities.agents.roster.supported; soft-skips on404. Contract: fire one workflow in a roster member's portfolio for a request{ rosterId?, triggerSource?, asWorkItem? }(host picks a default member whenrosterIdis omitted) and return{ runId, rosterId, triggerSubscriptionId? }. The fired run MUST emitroster.run.initiatedas its FIRST attribution event — immediately afterrun.started, BEFORE anyagent.invocation./agent.event (§C ordering) — content-free per theroster-attribution-no-contentinvariant (ids + persona + trigger source ONLY; never the work-item body/prompt/credential). WhenasWorkItem: truethe fire takes the RFC 0083 durable-work-item path and the event MUST carrytriggerSubscriptionId(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-itemtriggerSubscriptionId.agent-roster-attribution.test.tsdrives it viabehaviorGate('openwop-roster-attribution', …); the normativeGET /v1/agents/rosterread leg runs black-box on any roster host regardless of this seam. This is the RFC 0086 → Accepted bar (first adopter: MyndHyveagents.roster). - Live manifest-invocation seam (RFC 0077 §B/§E/§F) —
POST /v1/host/sample/agents/live-invoke. Gated oncapabilities.agents.liveRuntime.supported; soft-skips on404. Contract: drive one live manifest invocation for a request{ agentId?, source?, returnSchemaRef?, forceInvalidResult?, attemptTool? }(host picks a default agent whenagentIdis omitted) and return{ runId, invocationId, outcome? }. The invocation MUST bracket itsagent.*family withagent.invocation.startedas the FIRST agent-scoped event andagent.invocation.completedas the LAST (§E), sharing oneinvocationId, withsource∈ {workflow-node,run-api,chat-mention} andoutcome∈ {completed,handed-off,escalated,refused,failed} — both events content-free (identifiers + selection/outcome metadata only, never prompt or result body). WhenreturnSchemaRef+forceInvalidResult: trueare set (requiresliveRuntime.structuredOutput), the host MUST fail the invocation (completed.outcome === "failed",schemaValidated !== true) rather than ship a result that violateshandoff.returnSchemaRef(§B step 6). WhenattemptToolnames a tool OUTSIDE the agent'stoolAllowlist, the host MUST NOT call it (noagent.toolCalledfor 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.tsdrive these viabehaviorGate('openwop-live-invocation-bracket' | 'openwop-live-structured-output' | 'openwop-live-allowlist-enforced', …). This is the RFC 0077 → Accepted bar (first adopter: MyndHyveagents.liveRuntime). - Trigger-bridge delivery seam (RFC 0083 §C) —
POST /v1/host/sample/trigger-bridge/deliver. Profile-gated onopenwop-trigger-bridge(derived from discovery per §D — the bridge advertised + a dead-letter sink + a durable source); soft-skips on404. Contract: drive one delivery through the durable bridge for a request{ scenario, dedupKey?, source? }and return{ runId?, subscriptionId?, outcome?, deliveredCount? }, persisting thetrigger.delivery.attempted+trigger.subscription.state.changedevents to the durable run-event log (read back via the run event-log read seam).scenario: "dedup"delivers the samededupKeytwice and MUST be effectively-once (≤1trigger.delivery.attempted { outcome:"delivered" }for that key, §C-1);scenario: "exhaust"exhausts the retry policy and MUST terminate intrigger.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'srun.startedMUST carrycausationId== the delivery id (§C / RFC 0040, resolvable via/ancestry). Bothtrigger.*events MUST be content-free (SR-1: ids/states/counters only — never inbound body/headers/credentials).trigger-bridge-delivery.test.tsdrives all three legs viabehaviorGate('openwop-trigger-bridge', …); the normativeGET /v1/trigger-subscriptionsread 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 fromcapabilities.md§workflowChainPacks.deferredParameters; gated oncapabilities.workflowChainPacks.deferredParameters.supported; soft-skips on404. Contract: expand a chain in deferred-parameter mode for a request{ chain: { parameters, dag? }, values? }—chain.parametersaccepts a full{ type, properties, required? }object OR a bare properties map;dagoptional (defaults to a trivialcore.identitynode);valuesoptional per-param seed values. Returns the minted top-levelvariables[]as{ variables: [{ name, type?, format?, required, sensitive? }] }, wherenameis the bare parameter name (the internal deferred-parameter prefix de-aliased) andformatis present iff the host minted it: a string parameter's declaredformatis copied verbatim — including an unrecognised value (RFC 0136 req 2) — while a non-string parameter omitsformatentirely (req 1). Asensitiveparameter's flag composes withformaton one variable (req 6).workflow-variable-format.test.tsleg B1 drives it (a chain declaringemail/note/count→ the mintedemailcarriesformat:"email",notecarries the unrecognised value verbatim,counthas noformat) and soft-skips on404or an absentvariables[](a host advertisingdeferredParametersbut not yet serving thevariables[]extension). The RFC 0124 value-resolution branches (chainId: conformance.deferred/-sensitivereturning{ 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 oncapabilities.agents.evalSuite.supported; soft-skips on404. Contract: drive onemode:"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 theeval.*family to the durable run-event log (read back via the run event-log read seam). The eval run MUST emiteval.startedas the FIRST eval event, oneeval.scoredPER TASK (after that task's terminalagent.decided), andeval.completedONCE beforerun.completed(§C ordering:eval.started.sequence< everyeval.scored.sequence<eval.completed.sequence; theeval.scoredcount ==eval.completed.taskCount). Everyeval.scoredMUST be content-free (score∈ 0..1,passedboolean, 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-validEvalSummary(eval-summary.schema.json) readable via the NORMATIVEGET /v1/runs/{runId}/eval-summary, withpassedCount <= taskCountand no per-task output body.agent-eval-run.test.tsdrives it viabehaviorGate('openwop-eval-run', …); the normative eval-summary read runs black-box regardless of this seam. This is the RFC 0081 → Accepted bar (first adopter: MyndHyveagents.evalSuite). - Deployment-transition seam (RFC 0082 §B/§E) —
POST /v1/host/sample/agents/deployment-transition. Gated oncapabilities.agents.deployment.supported; soft-skips on404. Contract: drive one deployment transition for a request{ scenario, agentId?, version?, channel?, evalRunId? }and return{ runId?, record?, allowed?, error?, resolvedAgentVersion? }, persisting thedeployment.family (+agent.invocation.started) to the durable run-event log (read back via the run event-log read seam).scenario: "promote"runs the §E contract (authorize RFC 0049deploy:promote→ RFC 0051 approvalGate → RFC 0081 eval-verify whenevalRunIdset) and MUST emit a content-freedeployment.promotedwhosetoStateis in the seven-state vocabulary + carriestoVersion; the returnedrecordMUST validate againstagent-deployment.schema.json.scenario: "unauthorized"drives a principal lackingdeploy:promoteand MUST fail closed (allowed:false, NOdeployment.promoted— thedeployment-promotion-fail-closedinvariant).scenario: "eval-gate-unmet"drives a promote whoseevalRunIdhasEvalSummary.passed:falseand MUST deny witherror:"eval_gate_unmet"+ NOdeployment.promoted(§E-3).scenario: "channel-pin"starts a@channel-bound run whose resolved version is recorded asresolvedAgentVersiononagent.invocation.started(§B — the recorded fact a replay re-reads rather than re-resolving). Alldeployment.events MUST be content-free (SR-1: ids/state/scalars only — never a manifest body/prompt/credential).agent-deployment-lifecycle.test.tsdrives all four legs viabehaviorGate('openwop-deployment-lifecycle', …); the normativeGET /v1/agents/{agentId}/deploymentsread runs black-box regardless of this seam. This is the RFC 0082 → Accepted bar (first adopter: MyndHyveagents.deployment). - Tool-session seam (RFC 0078 §D) —
POST /v1/host/sample/tools/session-run. Gated oncapabilities.toolCatalog.sessionLifecycle; soft-skips on404/405. Contract: drive one tool-session interaction for a request{ toolId? }(host picks a default catalog tool when omitted) and return{ runId, sessionId?, toolId? }, persistingtool.session.opened→ the RFC 0064 call events (agent.toolCalled/agent.toolReturned) →tool.session.closedto the durable run-event log (read back via the run event-log read seam).tool.session.openedMUST precede the FIRST call event andtool.session.closedMUST follow the LAST (§D bracket ordering), both sharing onesessionId, each carrying atoolId, withtool.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.tsdrives it viabehaviorGate('openwop-tool-session-lifecycle', …); the normativeGET /v1/toolscatalog read runs black-box regardless of this seam. This is part of the RFC 0078 → Accepted bar (first adopter: MyndHyvetoolCatalog). - Egress-decision seam (RFC 0079 §C) —
POST /v1/host/sample/egress/decide. Gated oncapabilities.httpClient.egressPolicy.supported; soft-skips on404/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 §Aaudiences[]provenance against the egress destination.scenario: "out-of-audience"(credential bound to audience A, egress to B ∉ A) MUST returndecision∈ {denied,downgraded} +reason: "out-of-audience"and MUST NOT attach the credential (credentialAttached !== true— the §C confused-deputy MUST backing theegress-credential-audience-boundinvariant).scenario: "provenance-unevaluable"MUST returndecision: "denied"+reason: "provenance-unevaluable"(fail-closed).scenario: "in-audience"is the control (MAYallowed).scenario: "canary"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.tsdrive these viabehaviorGate('openwop-egress-audience-binding' | 'openwop-egress-decision-content-free', …). This is the RFC 0079 → Accepted bar (first adopter: MyndHyvehttpClient.egressPolicy). Egress policy layers over the RFC 0076 §BsafeFetchSSRF guard — no new normative read endpoint. - Memory-consolidation seam (RFC 0068 §D) —
POST /v1/host/sample/memory/consolidate. Gated oncapabilities.agents.memoryConsolidation.supported; soft-skips on404/501. Contract: run one background-consolidation pass for a request{ memoryRef, includeSecretCanary? }and return{ event: { inputCount, outputCount }, secretLeaked? }, emitting theagent.memory.consolidatedevent (durable-append, like the live-run seams). A merge/dedup pass MUST haveoutputCount <= 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); whenincludeSecretCanary, 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.tsdrives it via the capability gate. This is part of the RFC 0068 → Accepted bar (first adopter: MyndHyveagents.memoryConsolidation). - Commitment-fire seam (RFC 0068 §C) —
POST /v1/host/sample/commitment/fire. Gated oncapabilities.agents.commitments.supported; soft-skips on404/501. Contract: fire one inferred standing commitment for a request{ memoryRef, condition, includeIntentionCanary? }and return{ event: { commitmentId, memoryRef, condition }, fireCount?, intentionCanary? }, emitting thecommitment.firedevent (durable-append). The event MUST carrycommitmentId+ the sourcememoryRef(§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-levelintentionCanaryONLY so the driver can assert its absence fromevent); a commitment MUST fire at most once per satisfied condition (fireCount <= 1, §C.2).commitment-fired.test.tsdrives it via the capability gate. This is part of the RFC 0068 → Accepted bar (first adopter: MyndHyveagents.commitments). - Budget-run seam (RFC 0084 §C/§D) —
POST /v1/host/sample/budget/run. Gated oncapabilities.budget.supported; soft-skips on404/501. Contract: drive one budgeted run for a request{ scenario }and return{ runId?, outcome?, error?, modelCalled? }, persisting thebudget.+cap.breached+run.failedfamily to the durable run-event log (read back via the run event-log read seam). Budget consumption is tracked OFF the existing RFC 0026provider.usagestream (no double-counting).scenario: "hard-cost-exhaust"(requiresenforce:"hard",dimensions:["cost"]) MUST emit, in strict sequence,budget.reserved {effectiveBudget, scope}→budget.consumed {dimension:"cost", consumed, limit, remaining}→budget.threshold.crossed {dimension:"cost", percent}→budget.exhausted {dimension:"cost"}→cap.breached {kind:"budget-cost", limit, observed}→run.failed {error:"budget_exhausted"}(the §D hard-stop, reusing the unifiedcap.breachedoverflow event per the RFC 0058 precedent).scenario: "model-denied"drives a run whose resolved model violatesbudget.modelDeny/modelAllow; the host MUST refuse withbudget_model_deniedBEFORE the provider call (modelCalled !== true,modelDenywins on conflict — fail-closed, composing RFC 0031 + RFC 0067 at the dispatch seam).scenario: "advisory"(requiresenforce:"advisory") MUST emit thebudget.events but MUST NOT stop the run (nocap.breached{budget-}, norun.failed{budget_exhausted}). Everybudget.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 —budgethas no wall-time/iteration dimension.budget-enforcement.test.tsdrives it viabehaviorGate('openwop-budget-enforcement', …). This is the RFC 0084 → Accepted bar (first adopter: MyndHyvebudget). - Multi-party conversation seam (RFC 0101 §Conformance) —
POST /v1/host/sample/conversation/multi-party/open+POST /v1/host/sample/conversation/multi-party/exchange. Gated oncapabilities.multiPartyConversation.supported(viaisMultiPartyConversationSupported()); soft-skips on404/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: 'agent' 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('openwop-multi-party-conversation', …); 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 contractcapabilities.md§"Truthful advertisement" — the host's commitment when it advertises any of the above flagshost-capabilities.md§"capabilities.observability.testSeams" — the OTel scrape + debug-bundle export capability sub-blockobservability.md§"OTel collector test seam (RFC 0034)" — the canonical RFC 0034 §B normative text the OTel + debug-bundle seams implementreplay.md§"LLM cache-key recipe" — the canonical recipe the §4 LLM cache-key seam computesprompts.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.jsonshape —{ conversationId, present: string[], typing: string[] }— CLOSED (no field beyond these; the no-PII guard); every ref an opaque RFC 0041user:/agent:subject;typing ⊆ present; the requesting member appears inpresent. - 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 + anINTEROP-MATRIX.mdrow (the RFC 0086 dual-staging). - Ephemerality (RFC 0110): presence is live state — the host MUST NOT persist
channel.presenceto the replayable event log; it does not appear onGET /v1/runs/{runId}/events.
14. Transcript-window accounting — GET /v1/host/sample/agent/transcript-window?runId=…&iteration=N (RFC 0111)
| Field | Value |
|---|---|
| Method + path | GET /v1/host/sample/agent/transcript-window?runId=<id>&iteration=<N> |
| Capability gate | capabilities.multiAgent.executionModel.contextBudget.transcriptTokenBudget present (RFC 0111) |
| Env gate (reference impl) | OPENWOP_TEST_SEAM_ENABLED=true |
| Introduced | RFC 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, therunOrchestrator.decided.iterationof 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)
| Field | Value |
|---|---|
| Method + path | POST /v1/host/sample/a2ui/emit-surface |
| Capability gate | capabilities.a2uiSurface.deltaTransport: true (RFC 0114) |
| Env gate (reference impl) | OPENWOP_TEST_SEAM_ENABLED=true |
| Introduced | RFC 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 }—surfaceis a fullui.a2ui-surfacepayload ({ 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=1subscriber, transports as a delta frame computed against the previously emitted full. - Response 200:
{ surfaceRef: string }— the recorded envelope id of the surface just emitted (thesurfaceRefa delta frame references). The host MUST record the canonical envelope as the FULL surface (never a delta). - Out-of-catalog surface: a request whose
surfacecarries a component outside the host's closedcatalogVersioncatalog 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=1transport 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=1subscriber'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)
| Field | Value |
|---|---|
| Method + path | POST /v1/host/sample/ai/generate |
| Capability gate | capabilities.aiProviders.promptPrefixCache.supported: true (RFC 0116) |
| Env gate (reference impl) | OPENWOP_TEST_SEAM_ENABLED=true |
| Introduced | RFC 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 }. ThetenantIdselects 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). OmittingcachePrefixIdis the control case. - Response 200:
{ "envelope": { "envelopeType": string, "payload": object, "envelopeId": string }, "usage": { "inputTokens": integer, "outputTokens": integer, "cacheReadTokens"?: integer, "cacheWriteTokens"?: integer } }—envelope+inputTokens/outputTokensMUST be identical whethercachePrefixIdhit, missed, or was absent (cost-hint-only);cacheReadTokensshows> 0only on a cache HIT for the requesting tenant;cacheWriteTokensshows> 0on a PRIME. The response MUST NOT echocachePrefixIdwhere 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'scachePrefixIdMUST returncacheReadTokens == 0(theprompt-prefix-cache-cross-tenant-isolationinvariant), 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)
| Field | Value |
|---|---|
| Method + path | POST /v1/host/sample/ui-plugin/rpc |
| Capability gate | capabilities.uiPlugins.supported: true (RFC 0117) |
| Env gate (reference impl) | OPENWOP_TEST_SEAM_ENABLED=true |
| Introduced | RFC 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> }— themessagevalidates againstui-plugin-message.schema.json({ openwop: "ui-plugin/1", type: "request", id, method, params? }). - Response 200: the
ui-plugin/1responseenvelope{ "openwop": "ui-plugin/1", "type": "response", "id": integer, "ok": boolean, "result"?: object, "error"?: { "code": string, "currentVersion"?: string } }. An undeclared/unknownmethodMUST return{ ok: false, error: { code: "method_not_allowed" } }(never silent execution). A stale or unknownartifact.writeparams.versionMUST return{ ok: false, error: { code: "artifact_conflict", currentVersion } }and MUST NOT persist; an absent artifact returnsartifact_not_found. A successfulartifact.writeMUST return the new opaqueversioninresult. Theerror.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_allowedis produced by real enforcement (not a hard-coded reply), andartifact_conflictrequires the host to have minted a currentversionthe stale token compares against (a fixedconformance-canaryartifact id is provisioned under the seam so the conflict path is exercised against a real current version). Theversiontoken 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)
| Field | Value |
|---|---|
| Method + path | POST /v1/host/sample/dispatch/fanout |
| Capability gate | capabilities.dispatch.fanOutSupported: true AND "parallel" ∈ capabilities.dispatch.fanOutPolicies (RFC 0118) |
| Env gate (reference impl) | OPENWOP_TEST_SEAM_ENABLED=true |
| Introduced | RFC 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> }—configvalidates againstdispatch-config.schema.jsonand MUST carryfanOutPolicy: "parallel"; it MAY carryjoinPolicy({ mode, quorum?, onChildFailure }) andmaxConcurrency. Aconfig.fanOutPolicyother than"parallel", or amode: "quorum"with a missing / out-of-rangequorum(MUST be1 ≤ quorum ≤ nextWorkerIds.length), MUST be rejected4xx(the cross-field MUSTs the host also enforces atPOST /v1/workflows). - Response 200:
{ "joinOutcome": "satisfied" | "failed" | "partial", "children": [ { "workflowId": string, "childRunId": string, "childStatus": "completed" | "failed" | "cancelled", "error"?: string } ], "mergeOrder": string[] }.joinOutcome+mergeOrdermirror thecore.dispatch.joinevent ($defs.dispatchJoin,run-event-payloads.schema.json);children[]is thecore.dispatchnode-output shape (node-packs.md§Output shape).joinOutcomeis'satisfied'when themodeis met andonChildFailuredid not fail the node,'partial'when themodeis met but ≥1 child is non-completedundercollect/absorb(node succeeds),'failed'whenfail-fasttripped or themodeis unsatisfiable (node fails).mergeOrderis thechildRunIds in the parent host's observed wall-clock terminal order — the replay-deterministic tiebreak for collidingoutputMappingkeys (re-applied verbatim on:fork, never recomputed).children.lengthequalsnextWorkerIds.lengthunderwait-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'underwait-allrequires every dispatched child to have actually reached a terminal state (not a hard-coded reply),children.length ≥ 2(childCount> 1by construction), andmergeOrderreflects the order the host actually observed its children terminate. A host that returnsnextWorkerIdsorder (instead of observed terminal order) fails the replay-determinism gate: when two children write the same parent variable via collidingoutputMapping, the value MUST resolve to the LAST child inmergeOrder, so a dispatch-ordermergeOrderyields the wrong winner.
19. Self-hosted runner driver — POST /v1/host/sample/runner/{register,dispatch} (RFC 0122)
| Field | Value |
|---|---|
| Method + path | POST /v1/host/sample/runner/register · POST /v1/host/sample/runner/dispatch |
| Capability gate | capabilities.selfHostedRunner.supported: true (RFC 0122) |
| Env gate (reference impl) | OPENWOP_TEST_SEAM_ENABLED=true |
| Introduced | RFC 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.
registerrequest: aSelfHostedRunnerRegistration{ runnerId, subject, capabilities }. Records a runner bound tosubject(the run's owning RFC 0048 principal). Response 200/201:{ "runnerId": string }.dispatchrequest:{ subject, ...<SelfHostedRunnerDispatchFrame> }— the owningsubjectplus a dispatch frame ({ runId, stepId, seq, kind, provider?/model?/tool?, inputs }). The router matches a runner owned bysubjectFIRST, then capability.- Response 200 (routed):
{ "result": <SelfHostedRunnerResultFrame.output>, "deduped": boolean }.dedupedistrueiff 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 returnsdeduped: trueonly 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 areadditionalProperties:false).
20. Workload-identity resolution driver — POST /v1/host/sample/test/workload-identity/resolve (RFC 0154)
| Field | Value |
|---|---|
| Method + path | POST /v1/host/sample/test/workload-identity/resolve |
| Capability gate | capabilities.auth.workloadIdentity.supported: true (RFC 0154 §A) |
| Env gate (reference impl) | OPENWOP_TEST_SEAM_ENABLED=true |
| Introduced | RFC 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.tsreports 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)
| Field | Value |
|---|---|
| Method + path | POST /v1/host/sample/test/compensation/unwind · POST /v1/host/sample/test/compensation/replay |
| Capability gate | capabilities.compensation.supported: true (RFC 0151 §A) |
| Env gate (reference impl) | OPENWOP_TEST_SEAM_ENABLED=true |
| Introduced | RFC 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, defaulttrue, SP-11a 2026-08-18) — whenfalse, 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 }—refiredEffectsMUST be0(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.tsfails 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.
unwindrequest gainsfailFirstInverseAttempts?: integer 0..3andhold?: 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.
unwindresponse 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.
replayrequest gainsrunId?: 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; terminate → partial / 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)
| Field | Value |
|---|---|
| Method + path | POST /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 gate | invoke: 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 |
| Introduced | a2a-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 foreignreferenceTaskIds[]; the host runs its normal client path and reportspeerAuthority: { approvalAdvanced: boolean, scopesWidened: boolean, referencedTaskDereferenced: boolean }on the 200 body — all three MUST befalse(a2a-peer-authority.test.ts; a host that does not yet report the block is recordedblocked, not passed).authenticated: trueasks the host to make the call under an authenticated principal (§B's fail-closed default applies);peerOffersOnlyis passed to the fake peer so it advertises/accepts only that version;requestVersionoverrides 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 theA2A-Versionheader on every non-GET call the peer captured. ReportingpreferredVersionwhile 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.invokepath uses (same negotiation code, same header construction); a seam that hand-writesA2A-Versionproves 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)
| Field | Value |
|---|---|
| Method + path | POST /v1/host/sample/mcp/invoke |
| Capability gate | capabilities.mcp.supported && capabilities.mcp.protocolVersions.length > 0 (RFC 0153 §A) |
| Env gate (reference impl) | OPENWOP_TEST_SEAM_ENABLED=true |
| Introduced | mcp-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" }.requestVersionoverrides the revision the host asks for (used to force an unsupported one). Additive fields (2026-08-16, RFC 0153 §C/§D):toolnames the tool to call on the suite's server (default: whatever the host's client path calls; the suite's server offersechoand the MRTR toolneeds_input),argumentsits arguments,clientCapabilitiesthe_meta.io.modelcontextprotocol/clientCapabilitiesthe host declares for the call (the MRTR leg passes{ elicitation: {} }),elicitationAnswera programmatic answer the seam MAY use to resolve theclarificationinterrupt theinput_requiredresult raises (in production a human answers it), andscenario: "extension-asserts-authority"asks the host to run its normal client path against a result whose_metaasserts authority. - Response 200:
{ negotiatedVersion: "<YYYY-MM-DD>", ... }— the revision the host actually used, in MCP date form; it MUST equal theMCP-Protocol-Versionheader (and_metarevision) on every call the server captured and MUST be one the host lists inprotocolVersions. ReportingpreferredVersionwhile 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_metaconstruction, same header construction); a seam that hand-writesMCP-Protocol-Versionproves nothing about production. Under the current profile the call MUST be stateless — noinitializebefore 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)
| Field | Value |
|---|---|
| Method + path | POST /v1/host/sample/workflows |
| Capability gate | none of its own — each calling leg gates on the capability it exercises |
| Env gate (reference impl) | OPENWOP_TEST_SEAM_ENABLED=true |
| Introduced | dispatch-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 canonicalworkflow-definition.schema.jsonkeys both byid. Every leg posting here has usednodeIdsince 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.tsposted it until 2026-08-16 and the first 2026-07-28 host answered400, which the leg then walked past intotools/callon a tool that was never registered (S17). A calling leg MUST treat a4xxon this registration as a suite defect (assert< 400), never as seam absence. Aligning the seam to canonicalid(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)
| Field | Value |
|---|---|
| Method + path | POST /v1/host/sample/test/idempotency/concurrent-claim |
| Capability gate | none — the obligation is unconditional (idempotency.md §"Concurrent duplicates (Layer 2)") |
| Env gate (reference impl) | OPENWOP_TEST_SEAM_ENABLED=true |
| Introduced | RFC 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 areal 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 > deliveredhappen 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.