Status: Stable · v1.1 (2026-04-27; hygiene pass 2026-05-10). Formalized as
schemas/capabilities.schema.json. The public network handshake atGET /.well-known/openwopis the canonical v1 capability declaration. Fields marked required v1 are required for conformance; fields marked optional v1 have stable wire shapes but MAY be omitted by hosts that do not support the capability. Conformance suite scenarios verify the required surface end-to-end and gate optional profile scenarios from this document. Keywords MUST, SHOULD, MAY follow RFC 2119. Seeauth.mdfor the status legend.
Why this exists
External clients (CLIs, SDKs, agents from other ecosystems) need a deterministic way to discover what an OpenWOP-compliant server can do _before_ they issue requests. Specifically:
- Which protocol version they're talking to (and whether their client is too old)
- Which envelope types and node types are registered
- What hard limits apply (recursion, run duration, request body size)
- Which transports are exposed (REST, MCP, A2A, gRPC)
- Which OTel attribute taxonomy traces will use
- Which
configurablekeys are accepted on per-run overrides
This document specifies the public surface. Implementations MAY also maintain richer internal capability objects for prompt construction, node dispatch, or product UX; those internal objects are not normative unless projected through /.well-known/openwop.
Endpoint
An OpenWOP-compliant server MUST expose:
| Method | Path | Auth | Cache |
|---|---|---|---|
GET | /.well-known/openwop | None (public) | Cache-Control: public, max-age=300 recommended |
The path follows RFC 8615 .well-known URI conventions. The response MUST be JSON with Content-Type: application/json.
A server MAY expose this at additional paths for backward compatibility but MUST treat /.well-known/openwop as canonical.
Two surfaces
OpenWOP distinguishes two capability surfaces:
- In-package
Capabilities— what the engine tells the LLM in the system prompt. 5 fields. (stable) — see "In-package shape" below. - Network-handshake
Capabilities— whatGET /.well-known/openwopreturns to external clients. Superset of the in-package shape plus discovery, transport, observability, profile, testing, and optional feature advertisement. This is the public v1 handshake.
The in-package shape remains useful for engines and LLM prompts. The network-handshake shape is what clients and conformance suites consume.
In-package shape (internal, non-normative)
What the engine actually has today, used to format the system prompt:
interface Capabilities {
protocolVersion: string; // engine ↔ LLM contract version
supportedEnvelopes: readonly string[]; // ['prd.create', 'theme.create', ...]
schemaVersions: Readonly<Record<string, number>>; // { 'prd.create': 2, 'theme.create': 1 }
limits: CapabilityLimits; // hard caps on LLM behavior
extensions?: Readonly<Record<string, unknown>>; // per-canvas-type additions
}
interface CapabilityLimits {
clarificationRounds: number; // default 3
schemaRounds: number; // default 2
envelopesPerTurn: number; // default 5
}
Default limits (DEFAULT_CAPABILITY_LIMITS in the reference app — openwop/openwop-app repo, backend Capabilities.ts; historical, non-normative):
{ clarificationRounds: 3, schemaRounds: 2, envelopesPerTurn: 5 }
Helper functions (reference-app patterns, same Capabilities.ts):
buildCapabilities(opts)— construct from envelope catalog + limits. ValidatesschemaVersionsare non-negative integers.formatCapabilitiesForPrompt(caps)— render as system-prompt text block. Sorts envelope types + extension keys deterministically for prompt-cache stability.mergeCapabilities(base, extension)— per-canvas-type merge. Union of envelopes, extension wins on schema-version + limit conflicts.
Enforcement: Implementations typically track counters per (run, task, turn) and emit a capability-limit error on breach. The public wire consequence is the cap.breached event described below.
Network-handshake shape
The full GET /.well-known/openwop response. Only protocolVersion, supportedEnvelopes, schemaVersions, and the three base limits are required by schemas/capabilities.schema.json. Additional fields below are optional v1: their shapes are stable when present, and clients MUST tolerate absence.
Document-root layout (normative — RFC 0073)
Every capability family — the required protocolVersion / supportedEnvelopes / schemaVersions / limits, and every optional family (agents, secrets, aiProviders, auth, memory, multiAgent, authorization, and all others defined in schemas/capabilities.schema.json) — MUST appear as a property of the document root of the GET /.well-known/openwop response. capabilities.schema.json validates this full response: its properties are the root properties, and it declares no capabilities wrapper property. A host MUST NOT require a capabilities wrapper object to convey them.
A top-level capabilities wrapper object is a deprecated legacy shape, tolerated only by the schema's additionalProperties: true. Through the v1.x migration window a host MAY additionally mirror the families under a capabilities object for backward compatibility; clients SHOULD read the document root first and MAY fall back to a capabilities.* wrapper. The conformance suite reads the root only — root is the MUST above, so a host that serves families exclusively under the wrapper is non-conformant and is graded as such (RFC 0073 Phase 4). Hosts MUST serve families at the root and SHOULD NOT emit the wrapper. The host-side mirror affordance and the schema's additionalProperties tolerance are scheduled to retire together at the next major version (v2.0), at which point capabilities.schema.json tightens to forbid the wrapper — see RFC 0073.
{
"protocolVersion": "1.0",
"implementation": { "name": "...", "version": "...", "vendor": "..." },
"engineVersion": 1,
"eventLogSchemaVersion": 2,
"supportedTransports": ["rest", "mcp", "a2a"],
"supportedEnvelopes": ["prd.create", "theme.create", "..."],
"schemaVersions": { "prd.create": 2, "theme.create": 1 },
"limits": {
"clarificationRounds": 3,
"schemaRounds": 2,
"envelopesPerTurn": 5,
"maxNodeExecutions": 1000,
"maxRunDurationMs": 86400000
},
"configurable": {
"model": { "type": "string" },
"temperature": { "type": "number", "min": 0, "max": 2 },
"recursionLimit": { "type": "number", "max": 1000 }
},
"observability": {
"namespace": "openwop",
"spanAttributes": ["openwop.run_id", "openwop.node_id", "openwop.node_type", "openwop.event_seq", "openwop.workflow_id", "openwop.protocol_version"]
},
"runtimeCapabilities": ["chat.sendPrompt", "canvas.write"],
"secrets": {
"supported": true,
"scopes": ["tenant", "user", "run"],
"resolution": "host-managed"
},
"aiProviders": {
"supported": ["anthropic", "openai", "gemini"],
"byok": ["anthropic", "openai"]
},
"minClientVersion": "1.0",
"fixtures": ["conformance-noop"]
}
The example's maxNodeExecutions: 1000 is a deliberately non-default value (the documented default is 100) illustrating a host that raises the ceiling.
Field reference
| Field | Type | Status | Notes |
|---|---|---|---|
protocolVersion | string | required v1 | Protocol version the server speaks, e.g. "1.0". |
supportedEnvelopes | string[] | required v1 | Envelope type strings the engine recognizes. |
schemaVersions | Record<string, number> | required v1 | Active schema version per envelope type. Per-envelope-type integer, not per-spec-type semver. |
limits.clarificationRounds | number | required v1 | Default 3. |
limits.schemaRounds | number | required v1 | Default 2. |
limits.envelopesPerTurn | number | required v1 | Default 5. |
extensions | Record<string, unknown> | optional v1 | Per-host or per-workflow extension data. Clients treat as opaque. |
implementation.{name,version,vendor} | object | optional v1 | Identifies the server. |
engineVersion | number | optional v1 | See version-negotiation.md. |
eventLogSchemaVersion | number | optional v1 | See version-negotiation.md. |
supportedTransports | string[] | optional v1 | Subset of ["rest", "mcp", "a2a", "grpc"]. REST is required regardless of whether this field is present. |
limits.maxNodeExecutions | number | optional v1 | Default 100. Hosts that advertise it MUST enforce it. Engine-side ceiling clamping RunOptions.configurable.recursionLimit. Exceedance emits cap.breached with kind: "node-executions" and transitions the run to failed per §"Engine-enforced limits + cap.breached" below. |
limits.maxRunDurationMs | number | optional v1 | Maximum run duration (milliseconds) the host intends to allow. Upper bound for RunOptions.configurable.runTimeoutMs (RFC 0058). |
limits.maxRequestBodyBytes | number | optional v1 | Maximum REST request body accepted by the host, in bytes. Added to capabilities.schema.json by RFC 0094 (limits is otherwise closed — additionalProperties: false). |
configurable | object | optional v1 | Per-run parameter overlay schema. |
observability | object | optional v1 | OTel attribute taxonomy hints. See observability.md. |
minClientVersion | string | optional v1 | Client-side version floor for 426 Upgrade Required-style UX. |
runtimeCapabilities | string[] | optional v1 | Host-advertised opaque capability ids that NodeModules may require via NodeModule.requires. See §"Runtime capabilities" below. |
secrets.supported | boolean | optional v1 | Host advertises secret/credential resolution. Clients gate BYOK flows on this. See §"Secrets" below. |
secrets.scopes | string[] | optional v1 | Subset of ["tenant", "user", "run"]. |
secrets.resolution | string | optional v1 | Currently "host-managed". Reserved for future modes. |
aiProviders.supported | string[] | optional v1 | Providers the host's AI proxy can route to (anthropic, openai, gemini, etc.). |
aiProviders.byok | string[] | optional v1 | Subset of aiProviders.supported for which BYOK is permitted. |
aiProviders.policies | object | optional v1 | Host-side policy enforcement modes (disabled / optional / required / restricted). |
fixtures | string[] | optional v1 | Fixture workflow IDs the host has seeded. Conformance-suite-only contract. |
configurable
Schema for per-run parameter overrides accepted by POST /v1/runs configurable field. See run-options.md for full semantics. The capability declaration enumerates the keys the server accepts:
"configurable": {
"model": { "type": "string", "description": "AI model override" },
"temperature": { "type": "number", "min": 0, "max": 2 },
"maxTokens": { "type": "number", "min": 1, "max": 8192 },
"promptOverrides": { "type": "object" },
"recursionLimit": { "type": "number", "min": 1, "max": 1000 }
}
A client MUST consult this capability before sending configurable values and MUST omit keys not listed. An unknown key on the wire MAY be rejected with validation_error or silently ignored — implementations differ; the spec recommends rejection so misconfiguration is loud.
Runtime capabilities
Lets a host advertise opaque host facilities that NodeModules can require via NodeModule.requires?: readonly string[]. The protocol owns the _check_; provider value shapes are documented per-capability alongside their consumers, NOT here.
"runtimeCapabilities": ["chat.sendPrompt", "canvas.write", "secrets.byok"]
Field shape: array of unique non-empty strings. Capability ids are dotted, domain-scoped (conventional namespaces: chat., canvas., secrets., media.).
Client semantics. A client that submits a workflow whose nodes declare requires: ['chat.sendPrompt'] SHOULD first verify the host advertises that capability. A host that lacks a capability MUST refuse to dispatch nodes that declare it in requires, terminating the run with RunSnapshot.error.code = 'capability_not_provided' and the missing capability id in the error message.
Backward compat. Clients MUST tolerate the field's absence — only hosts that opt into runtime-capability advertisement expose it. NodeModules with no requires are unaffected.
Conformance coverage lives in conformance/src/scenarios/runtime-capabilities.test.ts.
secrets
Lets a host advertise that it supports secret-resolution + BYOK (Bring-Your-Own-Key) flows for AI provider credentials and other host-managed secrets.
"secrets": {
"supported": true,
"scopes": ["tenant", "user", "run"],
"resolution": "host-managed"
}
Field shape:
supported(boolean) — host has any secret-resolution at all. Hosts that don't store credentials (e.g., test deployments) returnfalseand clients MUST NOT attempt BYOK flows.scopes(string array, subset of["tenant", "user", "run"]) — declares which secret-storage scopes the host implements. Atenant-scoped secret is shared across the workspace;user-scoped is per-end-user;run-scoped is ephemeral per-run. Hosts that support multiple scopes return all of them. Naming alias: hosts that store tenant-scoped secrets at a workspace-keyed path (e.g., a host that usesworkspaces/{wsId}/secrets/{id}) advertisetenanthere regardless of internal field naming — the wire term istenant.runscope is reserved in v1.x; future hosts MAY advertise it without a spec bump (additive in thisscopesarray). Clients MUST tolerate any subset including unfamiliar future scopes.resolution(string, currently always"host-managed") — the resolution mode. Reserved for forward-compat: future versions may add"client-attached"for clients that pass credentials inline (out of scope for v1.x — clients MUST use opaque references viaRunOptions.configurable.ai.credentialRef).
Client semantics. Clients gate BYOK UX on secrets.supported === true. Without it, the BYOK flow is unavailable and the host serves all callers from platform-managed credentials.
Server semantics. Hosts that advertise secrets MUST implement a secret-resolution adapter. The adapter returns opaque resolved-secret references that downstream provider adapters dereference internally — raw key material NEVER appears in the protocol surface (no events, logs, traces, prompts, errors, exports, screenshots).
Hard rule (NFR-7): any code path that emits a RunEvent, OTel span, log line, error message, or exported artifact MUST NOT contain raw key material. Hosts MUST add lint + redaction unit tests verifying this invariant before exposing the BYOK surface.
aiProviders
Companion to secrets. Advertises which AI providers the host's AI-proxy can route to and which permit BYOK.
"aiProviders": {
"supported": ["anthropic", "openai", "gemini"],
"byok": ["anthropic", "openai"]
}
Field shape:
supported(string array) — provider ids the host's AI-proxy can route to. Conventional ids:anthropic,openai,gemini,mistral,cohere,vertex,bedrock. Hosts MAY add vendor-prefixed extensions.byok(string array, subset ofsupported) — providers for which the host permits BYOK. Empty array → all calls use platform-managed keys; non-empty → clients MAY pass an opaqueai.credentialRefinRunOptions.configurablefor matching providers.
Client semantics.
RunOptions.configurable.ai.provider— selects the provider (must be insupported).RunOptions.configurable.ai.model— selects the model.RunOptions.configurable.ai.credentialRef— opaque host-issued reference to a stored secret (must reference a credential of a provider inbyok).
Server semantics. Servers reject ai.credentialRef for providers NOT in byok with credential_forbidden. Servers reject unknown provider ids with validation_error.
aiProviders.authModes — BYOK auth-mode contract (RFC 0067, Active)
supported and byok say _which_ providers the host routes to and _which_ permit BYOK, but not _how_ a client is expected to supply a provider's credential. As the catalog grows beyond API-key providers (OAuth-backed providers, local Ollama/vLLM endpoints, platform-managed providers) the supply mechanism diverges. The optional authModes map advertises it so a client can pre-flight the credential UX without trial-and-error.
"aiProviders": {
"supported": ["anthropic", "openai", "vertex", "ollama"],
"byok": ["anthropic", "openai"],
"authModes": {
"anthropic": ["apiKey"],
"openai": ["apiKey"],
"vertex": ["oauth-pkce"],
"ollama": ["none"]
}
}
Field shape: authModes is an OPTIONAL object whose keys are provider ids (each MUST appear in supported) and whose values are non-empty, unique arrays of auth modes drawn from the closed enum ["apiKey", "oauth-pkce", "oauth-device", "none"].
| Mode | Meaning | Credential supply |
|---|---|---|
apiKey | Host accepts a stored API-key credential. | Client passes RunOptions.configurable.ai.credentialRef (today's BYOK path). Provider MUST appear in byok. |
oauth-pkce | Host acquires a token via an OAuth 2.0 authorization-code + PKCE flow it owns (RFC 0047 host.oauth). | Client references the host-stored credential by ref (RFC 0046); the PKCE flow is host-driven; key material is NEVER passed on ai.credentialRef. |
oauth-device | Host acquires a token via an OAuth 2.0 device-authorization flow it owns. | Same as oauth-pkce. |
none | Provider needs no caller-supplied credential. | A local provider (Ollama / vLLM at a host-configured endpoint) or one served entirely from platform-managed keys. Provider MUST NOT appear in byok. |
Auth-mode contract (normative when authModes is advertised):
1. Every key in authModes MUST also appear in aiProviders.supported. 2. A provider whose mode array includes apiKey MUST also appear in aiProviders.byok (apiKey _is_ the BYOK path; the two advertisements MUST agree). 3. A provider whose mode array is exactly ["none"] MUST NOT appear in aiProviders.byok. A provider MAY advertise ["apiKey", "none"] (BYOK permitted with a platform-managed fallback) and MUST then appear in byok. 4. A provider advertising oauth-pkce or oauth-device SHOULD also advertise capabilities.oauth (RFC 0047) with a matching provider id; the OAuth flow itself is governed by RFC 0047. The credential is referenced by ref (RFC 0046), never passed as key material. 5. Absent authModes, the default contract is unchanged: a provider in byok behaves as apiKey; a provider in supported but not in byok behaves as none. Clients MUST tolerate the field's absence and apply this default. 6. authModes advertises _capability_, not a per-run _requirement_ — per-provider policy enforcement remains aiProviders.policies. A client MUST NOT infer a policy mode from an auth mode, and MUST ignore an auth mode it does not recognize rather than reject the discovery document.
Provider-name vocabulary (non-normative). supported stays an open string[]. To reduce cross-host id drift, hosts SHOULD use the recommended ids in schemas/capabilities.schema.json §aiProviders.supported when routing to a known provider (anthropic, openai, gemini, vertex, bedrock, mistral, cohere, openrouter, litellm, together, huggingface, qwen, ollama, vllm). The list is advisory; a host MAY advertise any vendor-prefixed extension id, and clients MUST tolerate unknown ids. Aggregators (openrouter, litellm) front many upstream models disambiguated by RunOptions.configurable.ai.model (host-interpreted); this spec does not normate a model-id grammar.
aiProviders.policies
Additive companion to aiProviders. Lets a host advertise which policy modes it implements for per-provider gating. Hosts that omit this field implement no enforcement (clients see only optional semantics).
"aiProviders": {
"supported": ["anthropic", "openai", "gemini"],
"byok": ["anthropic", "openai"],
"policies": {
"modes": ["disabled", "optional", "required", "restricted"],
"scopes": ["workspace", "project", "canvas-type"],
"errorCode": "provider_policy_denied"
}
}
Field shape:
modes(string array, subset of["disabled", "optional", "required", "restricted"]) — declares the policy modes this host can enforce. A host MAY support a subset (e.g.,["optional", "required"]) — clients MUST tolerate any subset.scopes(string array, optional) — declares the resolution layers the host evaluates when computing the effective policy for a request. Conventional ids:workspace,project,canvas-type. Order is host-defined; the host MUST document its precedence rules.errorCode(string, optional, defaults toprovider_policy_denied) — the wire-format error code returned when policy enforcement denies a request. Reserved for hosts that need a vendor-prefixed alias.
The four modes (host-side enforcement, opaque to the engine):
| Mode | Meaning | Pre-dispatch behavior |
|---|---|---|
disabled | Provider MUST NOT be used at all. | Reject before LLM call with provider_policy_denied (reason: "provider_disabled"). |
optional | No restriction. Default behavior; equivalent to no policy. | Permit. |
required | Provider MAY only be used when the caller supplies BYOK credentials. | Two reject paths: pre-resolve, when RunOptions.configurable.ai.credentialRef is absent (reason: "byok_required"); post-resolve, when the credential reference was supplied but the resolver returned no usable secret (reason: "byok_required_but_unresolved"). |
restricted | Provider use is limited to an allowlist of model patterns. | Reject when the requested model does not match any wildcard in allowedModels (reason: "model_not_allowed"). The same reason covers the case where the resolved restricted policy has an empty/missing allowedModels — a misconfigured policy fails closed via the same wire shape, with allowed: [] in the error context. |
allowedModels is the per-policy companion field for restricted mode — a list of glob patterns matched against RunOptions.configurable.ai.model. Hosts MUST treat a restricted policy with no allowedModels as fail-closed; the rejection surfaces via reason: "model_not_allowed" (with an empty allowed array in the error context to disambiguate from the "model unmatched" subcase). The shape of stored policy documents (per-workspace / per-project / per-canvas-type) is host-internal and not part of the wire protocol.
Wire-format error. When policy enforcement denies a request, the host MUST respond with the errorCode advertised above (default provider_policy_denied) and SHOULD include a machine-readable reason field with one of ["provider_disabled", "byok_required", "byok_required_but_unresolved", "model_not_allowed"]. The error MUST NOT echo the resolved policy document — only the _decision_. This shape applies whether the denial surfaces as an HTTP error (REST), a JSON-RPC error (MCP), or a stream chunk's errorCode (streaming AI responses).
Resolver behavior.
- A host MAY layer policy resolution across multiple scopes (workspace → project → canvas-type). The effective policy is the host's deterministic merge of layer outputs; precedence is host-defined and SHOULD be documented per-deployment.
- If the resolver itself is unavailable (network outage, storage failure), hosts SHOULD fail-open to
optionalrather than fail-closed — denying ALL requests during resolver outage breaks the runbook unrecoverably. - The single exception is a
restrictedpolicy that resolved successfully but contains an empty/missingallowedModels— that's a misconfigured policy, not an outage, and MUST fail-closed (surfacing asreason: "model_not_allowed"withallowed: []).
Audit emission. Hosts SHOULD emit a per-decision audit event (host-internal taxonomy; conventional name policy.decision) carrying the resolved policy + which scope-layer supplied each field. The exact payload shape is host-internal and NOT part of the wire protocol — clients learn the _outcome_ through the provider_policy_denied error, not by subscribing to audit events.
Backward compat. Clients MUST tolerate the field's absence. A host that omits policies is equivalent to one that advertises {"modes": ["optional"]} and never returns provider_policy_denied.
fixtures
Lets a host advertise the set of conformance fixture workflows it has seeded so the conformance suite can decide which fixture-dependent scenarios run vs. skip.
"fixtures": ["conformance-noop", "conformance-delay", "conformance-cancellable"]
Field shape:
- OPTIONAL
string[]of fixture-workflow IDs the host has seeded. Each ID matches the correspondingidof a fixture stub innode_modules/@openwop/openwop-conformance/fixtures/{id}.json. - Hosts MAY advertise vendor-prefixed IDs (e.g.,
openwop.smoke.byok); the suite ignores IDs it doesn't recognize. - Order is not significant. Duplicates SHOULD NOT appear; consumers MUST tolerate them by treating the set as deduplicated.
- Absent or empty array means the host advertises no fixtures.
Client semantics. The v1.0 conformance baseline reads the field at suite init and gates fixture-dependent scenarios with it.skipIf / describe.skipIf. A scenario whose fixture isn't advertised is reported as skipped rather than failed. SDK clients SHOULD ignore the field; it's a conformance-suite contract, not a client-facing capability.
Server semantics. Hosts that seed conformance fixtures SHOULD advertise them under fixtures. Hosts that don't ship the fixture surface MAY omit the field entirely; pre-RFC hosts that omit it are interpreted as "advertises no fixtures." The advertisement is a claim that the host has the workflow doc resolvable by POST /v1/runs workflowId; the suite verifies the runtime behavior end-to-end.
openwop-fixtures profile. Hosts that advertise at least one fixture satisfy the openwop-fixtures profile per profiles.md.
Backward compat. Clients MUST tolerate the field's absence — hosts that predate this profile omit it. The v1.0 conformance baseline reads the field when present and treats absence as a compatible default.
agents
Multi-Agent Shift capability block (v1+). Hosts that implement any multi-agent surface declare it here; hosts that do not omit the block entirely. Each field gates a specific conformance scenario class; scenarios skip honestly when the relevant flag is absent.
"agents": {
"supported": true,
"profile": "wop-agents-full",
"modelClasses": ["reasoning", "tool-using", "chat"],
"orchestratorPattern": "delegate.smart",
"memoryBackends": ["long-term"],
"orchestrator": true,
"dispatch": true,
"reasoning": { "verbosity": "summary", "tokenLimit": 512 }
}
Phase semantics:
- Phase 1 — agent identity (
supported: true+ optionalprofile+ optionalreasoning). Whentrue, host accepts run-levelRunSnapshot.agent/runOrchestratorfields, emitsagent.reasoned/agent.toolCalled/agent.toolReturned/agent.handoff/agent.decidedevents, and honors the confidence-escalation contract (agent.decided.confidencebelow the resolved escalation threshold → suspend withnode.suspended { reason: 'low-confidence' }). - Phase 2 — agent packs (
modelClasses+orchestratorPattern).modelClassesfilters whichAgentManifestdistributions install on this host; manifests carrying an unsupportedmodelClassMUST refuse install withunsupported_model_class.orchestratorPatternadvertises the host's supervisor strategy — canonicalsingle/delegate/delegate.smart; vendor extensions undervendor.<host>.<pattern>. - Phase 3 — memory layer (
memoryBackends).long-termmeans the host implementsExecutionHost.memoryagainst a durable store with the SR-1 redaction invariant intact end-to-end (BYOK plaintext substituted for[REDACTED:<secretId>]before any persisted write). Hosts that don't wireMemoryAdapteromit the field. - Phase 5 — orchestrator role (
orchestrator: true). Host advertises thecore.orchestrator.supervisornode typeId AND honors the conservative-path suspend semantics (CP-1). - Phase 6 — dispatch loop (
dispatch: true). Host advertises thecore.dispatchCore typeId AND honors the conservative-path commitment CP-2 (no mid-run DAG mutation). Implies (but does NOT require)orchestrator: true.
Reasoning verbosity:
verbosity: 'summary'— host SHOULD bound eachagent.reasoned.reasoningpayload toreasoning.tokenLimittokens (default 512). Recommended for production.verbosity: 'full'— host MAY emit complete model traces. Useful for debug-bundle inspection; not recommended for general production.verbosity: 'off'— host suppressesagent.reasonedevents entirely. Conformance scenarioagentReasoningEvents.test.tsskips when this mode is in effect.
Runs MAY override the host default via RunOptions.configurable.reasoningVerbosity (see run-options.md).
Streaming reasoning (RFC 0024). Hosts MAY advertise agents.reasoning.streaming: true to declare they emit agent.reasoning.delta events incrementally while a reasoning block is still open, in addition to the final agent.reasoned. Defaults to false. Consumers MUST tolerate both modes: a streaming host emits zero-or-more agent.reasoning.delta events followed by exactly one closing agent.reasoned; a non-streaming host emits only the closing event. The closing agent.reasoned event remains authoritative for reasoning content — if a host's truncation / redaction pipeline transforms the trace at finalize time, consumers reading only the closing event see the canonical result. See RFC 0024 §Proposal for the full normative contract.
Backward compat. Clients MUST tolerate the entire agents block's absence — pre-MAS hosts omit it. Within the block, every field is optional; pattern is "declare what you support, omit what you don't."
conversationPrimitive
Multi-Agent Shift Phase 4 capability. When true, host advertises that it implements the core.conversationGate typeId AND honors the conversation.start / conversation.exchange / conversation.close suspend variants per interrupt.md. Hosts that don't claim this fall back to clarification.requested interrupts for multi-turn user interjections.
"conversationPrimitive": true
Field shape: OPTIONAL boolean. Absent or false means the host does NOT implement the conversation primitive; multi-turn user interjections route through the legacy clarification.requested interrupt path.
Conformance. conversationLifecycle.test.ts / conversationVsLegacySuspend.test.ts / conversationReplayDeterminism.test.ts / conversationCapabilityNegotiation.test.ts gate on this flag.
Refusal contract (normative). A workflow whose nodes[].typeId references core.conversationGate and is submitted to a host whose /.well-known/openwop does NOT advertise conversationPrimitive: true MUST be refused. Hosts MAY refuse at workflow registration time OR at run-create time; the wire-shape semantics are otherwise identical (see §"Unsupported capability" below). The same refusal contract applies symmetrically to other capability-gated typeIds — see §"Unsupported capability" for the canonical envelope and the typeId → capability map.
Backward compat. Pre-MAS hosts omit the field. v1.0 conformance baseline reads the field when present and skips conversation scenarios when absent.
workflowChainPacks
RFC 0013 (Accepted 2026-05-18; the matching spec doc workflow-chain-packs.md remains at DRAFT v1.x pending Phase B/C closure). When supported: true, the host's workflow editor implements workflow-chain pack expansion — the author drops a chain tile, the host resolves the pack (verifying signature), prompts for parameters, substitutes {{params.<name>}} placeholders throughout the chain's DAG, rewrites node ids for collision avoidance, and splices the resulting nodes into the parent workflow. Dispatching runtimes see only the expanded core.*/published-vendor typeIds — the chain reference is NOT preserved at runtime, so this capability is a workflow-edit-time concern only.
"workflowChainPacks": { "supported": true }
Field shape: OPTIONAL object. When present, supported: boolean is REQUIRED. Hosts that don't implement chain expansion omit the block entirely (or set supported: false).
Conformance. workflow-chain-pack-manifest-validation.test.ts gates on this flag for the registry-validation path; future workflow-chain-expansion.test.ts (Phase 2/3) will gate on it for end-to-end expansion. Hosts that don't advertise the capability are skipped, not failed.
Runtime invariance. The workflowChainPacks capability advertises editor support only — the runtime engine never sees chain-pack-specific surface (workflows reach the runtime fully expanded into concrete core.*/published-vendor typeIds). Hosts that omit the capability still execute workflows containing post-expansion DAGs cleanly; what they can't do is implement the author-time drag-tile flow. There is no runtime refusal contract for this capability — the chain reference is workflow-edit-time only and leaves no surface for the runtime to gate on.
connections
RFC 0095 (Draft). When packsSupported: true, the host installs kind: "connection" registry packs — portable provider definitions (connection-packs.md) — and MUST implement the connection-packs.md §Manifest clause 6 resolution contract: an RFC 0045 connector's auth.provider (or an RFC 0047 host.oauth provider string) resolves against the installed connection pack whose provider.id matches, with installed-vs-built-in precedence per SemVer §11 and connection_provider_unresolved / connection_provider_conflict diagnostics.
"connections": { "packsSupported": true }
Field shape: OPTIONAL object. When present, packsSupported: boolean is REQUIRED. Hosts that don't install connection packs omit the block entirely (provider resolution stays implementation-defined / host-built-in, exactly as before RFC 0095). An optional supported: boolean MAY accompany it for family-shape uniformity; behavior keys on packsSupported only.
Composition. Connection packs are only useful alongside oauth.supported (RFC 0047) or credentials.supported (RFC 0046); a host SHOULD NOT advertise connections.packsSupported without at least one of those.
Conformance. The connection-pack-manifest-valid / connection-pack-no-credential-material / connection-pack-reach-exclusive schema probes are always-on (server-free); the behavioral connection-provider-resolution / connection-pack-write-reconsent scenarios gate on connections.packsSupported and soft-skip when unadvertised (hard-fail under OPENWOP_REQUIRE_BEHAVIOR=true).
observability
Optional v1 observability advertisement. See observability.md.
"observability": {
"namespace": "openwop",
"spanAttributes": [
"openwop.run_id",
"openwop.node_id",
"openwop.node_type",
"openwop.event_seq",
"openwop.workflow_id",
"openwop.protocol_version"
],
"spanNames": ["openwop.run", "openwop.node.<typeId>", "openwop.interrupt"]
}
A server that exports OTel traces MUST use the openwop. namespace. Aliasing to vendor-specific taxonomies (e.g., langgraph., datadog.*) is per-deployment configuration, NOT spec'd.
Post-launch v1.0 capability additions
The following capability blocks landed after the initial v1.0 freeze as additive normative shapes. All are optional; hosts that omit them remain v1-conformant.
orchestrator (RFC 0006)
Distinct from the umbrella agents.orchestrator: boolean flag — this block carries the richer shape RFC 0006 §G requires.
"orchestrator": {
"supported": true,
"workerIdInterpretation": "node",
"fanOutSupported": false
}
supported— whentrue, host implementsrunOrchestratorsemantics (CO-1/CO-2/CO-3 ordering invariants).workerIdInterpretation— closed enum"node" | "agent" | "either". Tells clients whetherOrchestratorDecision.nextWorkerIdsentries are node IDs (resolved against the run's DAG) or agent IDs (resolved via the workflow's agent-to-node binding).fanOutSupported— whentrue, host honorsnextWorkerIds.length > 1per RFC 0007'sfanOutPolicy. Hosts that always treat length > 1 as a workflow-authoring error setfalse.
When orchestrator.supported: true, hosts MUST also advertise dispatch.supported: true (orchestrator decisions need a dispatch translator).
dispatch (RFC 0007)
"dispatch": {
"supported": true,
"models": ["child-run"],
"fanOutSupported": false,
"askUserRoutings": ["conversation", "clarification", "auto"]
}
models— supportedworkerDispatchModelvalues. v1.x normates only"child-run"; hosts MAY add vendor extensions undervendor.<host>.<model>.askUserRoutings— supportedaskUserRoutingvalues fromDispatchConfig. Hosts that omit"conversation"MUST also omitconversationPrimitive: true.
memory (RFC 0004)
Distinct from the umbrella agents.memoryBackends: string[] array — this block carries MemoryAdapter operational shape.
"memory": {
"supported": true,
"maxEntrySizeBytes": 65536,
"ttlSupported": true
}
supported— whentrue, host implements the four-operationMemoryAdaptercontract (list,get,put,delete) per RFC 0004 §A.maxEntrySizeBytes— upper bound onMemoryEntry.contentsize. Hosts SHOULD rejectputrequests exceeding this withvalidation_error.ttlSupported— whentrue, host honorsexpiresAtper RFC 0004 §E.
memory.compaction (RFC 0012, Accepted)
Why this exists. Long-running agents accumulate many small MemoryEntry rows over time. Hosts that periodically distill those into fewer, longer-lived summaries face two cross-host concerns: (1) observability — operators monitoring multi-host fleets need a canonical event vocabulary so dashboards don't have to learn each host's vendor extension; (2) security — summarization models can introduce secret-shaped substrings (hallucinated tokens, format-leaks from in-context examples) that were NOT present in any source entry, so the BYOK redaction pass MUST be reapplied at the compaction boundary. This sub-block standardizes both the wire shape and the SR-1 carry-forward invariant; the actual algorithm (summarization model, embedding scheme, scheduling) remains a host choice.
Optional sub-block. Hosts that distill many short-lived MemoryEntry rows into fewer long-lived ones MAY advertise it; hosts that don't are assumed not to compact (clients MUST NOT infer compaction from entry counts).
"memory": {
"supported": true,
"maxEntrySizeBytes": 65536,
"ttlSupported": true,
"compaction": {
"supported": true,
"trigger": "host-managed",
"maxInputEntries": 1000,
"maxOutputBytes": 65536
}
}
supported(boolean, REQUIRED when the sub-block is present) — whentrue, host performs compaction overlongTermmemory and emits thememory.compactedevent perobservability.md§"Canonical event vocabulary".trigger(closed enum"host-managed" | "client-requested" | "both", REQUIRED whensupported: true) —host-managedruns on a host-internal schedule clients do not control.client-requestedandbothare reserved enum values; v1.x normates onlyhost-managed.maxInputEntries(integer, OPTIONAL) — informational ceiling on how many source entries one compaction collapses. Not wire-enforced.maxOutputBytes(integer, OPTIONAL) — informational ceiling on the distilled entry size. SHOULD be≤ memory.maxEntrySizeBytes.
SR-1 carry-forward (normative). Hosts advertising memory.compaction.supported: true MUST route compacted entry content through the same BYOK redaction harness applied to a fresh put. Per RFC 0012 §D, the fact that source entries were SR-1-compliant at original put time is NOT evidence to skip redaction on derived content — summarization models can introduce secret-shaped substrings not present in any source. See SECURITY/invariants.yaml row memory-compaction-sr-1-carry-forward.
memory.distillation (RFC 0062, Active)
Why this exists. A "dream" is a periodic background run that distills recent transactional memory into long-term artifacts under an explicit token budget, then refreshes a retrieval index the next session loads at startup. openwop already had the halves — memory.compaction (RFC 0012) defines host-managed distillation + the memory.compacted event, and scheduling (RFC 0052) defines scheduled run initiation — but nothing bound them, pinned a _token budget_, or defined the _index_ that closes the loop back to startup. Distillation composes them; it reuses the memory.compacted event (extended with an additive optional distillation sub-object) rather than minting a parallel memory.distilled event.
Optional sub-block. Hosts that omit it keep plain on-demand compaction (memory.compaction) or no memory; clients MUST NOT infer distillation from entry counts. The run contract — read snapshot → mandatory token budget → RFC 0012 distill with SR-1 carry-forward → byte-stable archive → MEMORY-INDEX.json workspace file → extended memory.compacted — is normative in agent-memory.md §"Scheduled distillation".
"memory": {
"supported": true,
"maxEntrySizeBytes": 65536,
"ttlSupported": true,
"distillation": {
"supported": true,
"maxTokenBudget": 8000,
"scheduled": true,
"indexEmitted": true,
"tokenizerName": "claude",
"archiveRetention": "P30D"
}
}
supported(boolean, REQUIRED when the sub-block is present) — whentrue, host honors thedistillation.tokenBudgetreserved run-option key (run-options.md), runs budgeted distillation overlongTermmemory, writes a stable archive, and emitsmemory.compactedwith thedistillationsub-object.maxTokenBudget(integer, OPTIONAL) — largest per-run distillation token budget the host honors. A supplieddistillation.tokenBudgetis clamped to this; absent ⇒ the host defaults to this.scheduled(boolean, OPTIONAL) — whentrue, host can initiate distillation on a schedule (requirescapabilities.scheduling, RFC 0052). Distillation MAY also run on-demand without scheduling.indexEmitted(boolean, OPTIONAL) — whentrue, host writes a retrievable memory-index manifest (MEMORY-INDEX.json, a workspace file per RFC 0059) after distillation; updating it emitsworkspace.updated.tokenizerName(string, OPTIONAL) — identifier of the tokenizer the budget is counted against (e.g.claude,gpt-4). The budget is best-effort-honest per this tokenizer (±10% conformance tolerance), not byte-exact.archiveRetention(string, OPTIONAL) — ISO-8601 duration (e.g.P30D) the distilled archives persist before GC. Recursive distillation (distilling prior archives) is allowed; each level re-checks SR-1.
Token budget + SR-1 (normative). A distillation run MUST stay within its effective tokenBudget (min(distillation.tokenBudget, maxTokenBudget), defaulting to maxTokenBudget when absent); a source set that cannot be distilled within the budget MUST fail with token_budget_exceeded (rest-endpoints.md) and write no partial archive (atomic). SR-1 carry-forward (RFC 0012 §D) holds through distillation — a distilled archive MUST NOT re-expose a secret the sources had redacted; this reuses the memory-compaction-sr-1-carry-forward invariant, so distillation adds no new invariant.
runs.pauseResume (Track 13)
"runs": {
"pauseResume": {
"supported": true,
"drainPolicies": ["immediate", "drain-current-node"]
}
}
When supported: true, host implements POST /v1/runs/{runId}:pause and :resume per rest-endpoints.md §pause/resume.
idempotency (Track 13 multi-region annex)
"idempotency": {
"supported": true,
"layer1RetentionSeconds": 86400,
"layer2RetentionSeconds": 1209600,
"crossRegion": "best-effort"
}
crossRegion is a closed enum: "single-region" | "best-effort" | "strict". Default value when the block is advertised but the field is omitted MUST be treated as "single-region" per idempotency.md §"Multi-region idempotency".
webhooks.signatureAlgorithms (Track 13)
Extension of the existing webhooks block:
"webhooks": {
"supported": true,
"signatureAlgorithms": ["v1"]
}
When signatureAlgorithms is surfaced, it MUST include "v1" (the canonical baseline). Hosts that omit the field continue to honor the absence-equals-v1 rule per webhooks.md §"Signature algorithm versioning".
auth.profiles and auth.auditLogIntegrity (Track 13)
Extension of the existing auth advertisement:
"auth": {
"profiles": ["openwop-auth-api-key-rotation", "openwop-auth-oidc-user-bearer", "openwop-audit-log-integrity"],
"auditLogIntegrity": {
"hashChain": true,
"checkpointSignatureAlgorithm": "ed25519",
"checkpointPublicKey": "MCowBQYDK2VwAyEA...",
"checkpointIntervalEntries": 1000,
"checkpointIntervalSeconds": 300
},
"oidc": {
"issuers": ["https://accounts.example.com/"],
"audience": "https://openwop.example.com",
"supportedScopeMapping": "group-claim",
"introspectionIntervalSeconds": 300
}
}
Profile-string canonicalization follows auth-profiles.md §"Profile catalog". When openwop-audit-log-integrity appears in auth.profiles, the auditLogIntegrity block is REQUIRED.
Capability stability tier — tier / experimentalUntil (RFC 0042)
Every object-valued capability sub-block under the network-handshake capabilities. MAY carry an optional tier field, plus a paired experimentalUntil date. This makes a host's _stability claim_ for a capability machine-readable on the wire, so a consumer can tell a stable contract apart from an Active-RFC preview without fetching RFCS/.md.
| Field | Type | Meaning |
|---|---|---|
tier | `"stable" \ | "experimental"` |
experimentalUntil | string (YYYY-MM-DD) | REQUIRED when tier: "experimental" (schema-enforced via if/then). ISO-8601 date no more than 12 months past the discovery-response date. |
Normative rules.
- A host MUST omit
tierfor a capability whose underlying RFC is alreadyAccepted(the advertisement is then unconditionally stable). A capability whose RFC is stillActiveSHOULD advertisetier: "experimental"until the RFC promotes. - When
tier: "experimental", the host MUST advertiseexperimentalUntil≤ 12 months out. This is enforced at the schema layer:tier: "experimental"withoutexperimentalUntilMUST fail discovery validation. - Sunset rule. Reaching
experimentalUntilwithout the underlying RFC graduating toAcceptedobliges the host to take one of three publicly-visible actions: (1) flip tostable(the wire shape held — commit to it); (2) extend with a newexperimentalUntil≤ 12 months out (a _second_ extension REQUIRES an open deprecation RFC justifying the continued flux); or (3) retract the capability advertisement (clients then receivecapability_not_providedper §"Unsupported capability — refusal contract"). A host advertising anexperimentalUntilin the past is non-conformant (validation_error,details.field: "capabilities.<path>.experimentalUntil",details.reason: "experimentalUntil_in_past"). tieris per-capability, not per-host: a host MAY advertise some capabilitiesstableand othersexperimentalin the same discovery payload.
Conformance routing. Scenarios gated on a capability consult its tier via the experimentalGate(profileName, advertised, tier, experimentalUntil?) helper (conformance/src/lib/behavior-gate.ts). Under default mode an experimental capability soft-skips (a dedicated Skipped (experimental) tally, distinct from the capability-gated skip bucket); it runs as a hard assertion only when OPENWOP_REQUIRE_EXPERIMENTAL=true is set (in addition to OPENWOP_REQUIRE_BEHAVIOR=true). A host that omits tier is evaluated as stable — the prior behavior, unchanged. The shape probes live in conformance/src/scenarios/experimental-tier-shape.test.ts.
A host advertising any tier: "experimental" capability derives the openwop-experimental profile (profiles.md §openwop-experimental); clients requiring stable-only contracts filter on its negation. This is additive (COMPATIBILITY.md §2.1): tier is optional with default "stable", no existing wire surface changes, and existing v1 conformance passes are unaffected.
Unsupported capability — refusal contract
Workflows MAY reference typeIds that are gated on optional capability advertisement (e.g., core.conversationGate is gated on conversationPrimitive: true). A host that does not advertise the gating capability MUST refuse such workflows. Refusal may occur at either of two boundaries:
1. Workflow registration (e.g., on POST /v1/workflows or equivalent): the host refuses the workflow document before it can be referenced by POST /v1/runs. 2. Run creation (POST /v1/runs): the host accepts the workflow document but refuses to create a run from it.
The protocol does NOT prescribe which boundary to use; hosts MAY choose either. What hosts MUST NOT do is silently fall back to a substitute behavior (e.g., demote core.conversationGate to core.clarificationGate) — the refusal is observable.
Wire envelope
The refusal MUST use the canonical error envelope (error-envelope.schema.json) with:
- HTTP status code one of
400 Bad Request,404 Not Found, or422 Unprocessable Entity.400is recommended when the host validates capability fitness eagerly;422is recommended when the host accepts the request shape but rejects on capability resolution;404is acceptable when the host treats the unregisterable workflow as not-found. error.codefrom the closed set:
- validation_error (broadest — when capability gating is part of request validation), - capability_required (specific — preferred when the host wants to be unambiguous), - not_found (when registration was refused and the workflow is consequently unresolvable).
details.requiredCapabilitySHOULD name the capability key whose absence triggered the refusal (e.g.,"conversationPrimitive").details.offendingTypeIdSHOULD name the typeId in the workflow that triggered the gating (e.g.,"core.conversationGate").
{
"error": "capability_required",
"message": "Workflow \"conformance-conversation-capability-negotiation\" references core.conversationGate, but this host does not advertise capabilities.conversationPrimitive: true.",
"details": {
"requiredCapability": "conversationPrimitive",
"offendingTypeId": "core.conversationGate",
"nodeId": "convo"
}
}
Capability-gated typeId map (normative)
| typeId | Gating capability | Reference |
|---|---|---|
core.conversationGate | conversationPrimitive: true | §conversationPrimitive above |
core.orchestrator.supervisor | orchestrator.supported: true | §orchestrator (RFC 0006) |
core.dispatch | dispatch.supported: true | §dispatch (RFC 0007) |
Future RFCs adding capability-gated reserved typeIds MUST extend this table and follow the same refusal contract.
Conformance
conversationCapabilityNegotiation.test.ts exercises the refusal contract for core.conversationGate against hosts that do not advertise conversationPrimitive: true. Analogous scenarios for the other gated typeIds ship as their gating capabilities migrate to general advertisement.
Engine-enforced limits and the cap.breached event (closes CC-1 spec-side)
The Capabilities.limits fields (clarificationRounds, schemaRounds, envelopesPerTurn, maxNodeExecutions, and — per RFC 0058 — maxRunDurationMs, maxLoopIterations) are engine-enforced — the server MUST emit a cap.breached event AND fail the run / node when an attempted operation would exceed the configured ceiling. All kinds share the same event surface (run-event-payloads.schema.json#$defs.capBreached) so consumers handle one event with a kind discriminator instead of N parallel surfaces.
cap.breached payload
| Field | Type | Notes |
|---|---|---|
kind | string | One of clarification, schema, envelopes, node-executions; wasm-memory / wasm-fuel / wasm-execution-time (RFC 0008 §K); run-duration / loop-iterations (RFC 0058). |
limit | integer | The ceiling that was tripped (server-resolved value — see §Resolution below). For run-duration this is the resolved timeout in milliseconds; for loop-iterations the resolved iteration ceiling. |
observed | integer | The observed value at the moment of trip. Always strictly greater than limit. For run-duration the elapsed milliseconds; for loop-iterations the iteration count. MUST be recorded in the event and reused on replay / :fork — never recomputed from a live clock or counter (replay.md). |
nodeId | string (optional) | Set for node-scoped limits (clarification, schema). Absent for run-scoped limits (envelopes, node-executions, run-duration, loop-iterations). |
Resolution: recursionLimit + maxNodeExecutions
For the node-executions kind specifically (which is the runtime invariant for recursionLimit):
1. The server resolves the effective limit as min(RunOptions.configurable.recursionLimit, Capabilities.limits.maxNodeExecutions). If the caller didn't supply configurable.recursionLimit, the server uses maxNodeExecutions directly. 2. The server validates the caller's supplied value at run-create time via the validateRecursionLimit() helper documented in run-options.md. Out-of-range values return 400 validation_error BEFORE the run starts — never at runtime. 3. The server maintains a per-run nodeExecutionCount counter, incremented on every node-state transition into started. 4. When nodeExecutionCount > resolvedLimit, the server: - Emits cap.breached with kind: 'node-executions', limit: resolvedLimit, observed: nodeExecutionCount. - Transitions the run to failed. - Sets RunSnapshot.error.code = 'recursion_limit_exceeded' and RunSnapshot.error.message to a human-readable description. - Stops scheduling further nodes.
The other three kinds follow analogous patterns (per clarification / schema / envelopes semantics in §In-package shape above), differing only in _what_ gets counted and _which counter_ resets when.
Resolution: run-duration + loop-iterations (RFC 0058)
Two run-scoped bounds follow the same resolve-at-create, enforce-at-runtime, emit-cap.breached pattern:
run-duration— the server resolvesmin(RunOptions.configurable.runTimeoutMs, Capabilities.limits.maxRunDurationMs)(measured fromrun.started), validates the caller's value at run-create (400 validation_errorif out of range), and on expiry emitscap.breached { kind: 'run-duration', limit: <resolvedMs>, observed: <elapsedMs> }, transitions the run tofailedwitherror.code = 'run_timeout', and stops scheduling.loop-iterations— for hosts advertisingmultiAgent.executionModel.supported(the execution loop, RFC 0037; orchestrator turns are the counted iterations), the server resolvesmin(RunOptions.configurable.maxLoopIterations, Capabilities.limits.maxLoopIterations), counts one increment per orchestrator turn, and on exceedance emitscap.breached { kind: 'loop-iterations', limit: <resolvedMax>, observed: <iterationCount> }, transitions tofailedwitherror.code = 'loop_limit_exceeded', and stops scheduling.
Both are run-scoped (no nodeId). Adding these two kinds to the capBreached.kind enum is additive — no eventLogSchemaVersion bump — exactly as RFC 0008 §K added the wasm-* kinds.
What this closes
- CC-1: the
recursionLimitruntime invariant. Validation and runtime enforcement are expressed as a unifiedcap.breachedemission rather than a separate event class. NoeventLogSchemaVersionbump required —cap.breachedalready exists withnode-executionsin itskindenum (perrun-event-payloads.schema.jsonand theopenwop.cap_kindOTel attribute inobservability.md). - CC-4:
Capabilities.limits.maxNodeExecutionsis optional in v1 (only the three base limits are schema-required); hosts that advertise it MUST enforce it. Default100. The clamp ceiling forrecursionLimitoverrides.
Industry-standard alignment
Modern workflow engines unify limit-related failures under a small set of event types:
- LangGraph:
GraphRecursionError(single error class). - Temporal / Cadence: cap exceedance folds under
WorkflowExecutionTimedOut/ActivityTaskFailedwith reason discriminator. - AWS Step Functions:
ExecutionFailedwitherror: "States.Runtime"covers all runtime caps.
openwop follows the same pattern: cap.breached with a kind discriminator covers all engine-enforced caps — the four core kinds, the RFC 0008 wasm-* runtime caps, and the RFC 0058 run-duration / loop-iterations bounds.
Conformance fixture
conformance-cap-breach (specced in conformance/fixtures.md) exercises the path end-to-end: 10 sequential noop nodes + configurable.recursionLimit: 5 → terminal failed + cap.breached event with kind: 'node-executions'.
Status legend
- required v1 — required by
schemas/capabilities.schema.json; every conforming host MUST include it. - optional v1 — shape is stable when present; hosts MAY omit when unsupported, and clients MUST tolerate absence.
- future — not part of the v1 wire shape; use only in roadmap/RFC text until it is added to the schema.
Capability negotiation flow
A typical client startup:
1. Client → GET /.well-known/openwop
2. Server → 200 OK, Capabilities JSON
3. Client checks:
- protocolVersion satisfies my pinned floor? → if not, abort with version-mismatch UX
- implementation.version known? → log advisory if mismatch
- minClientVersion ≤ my version, if present? → if not, abort with upgrade-required UX
- supportedEnvelopes includes envelopes I emit? → if not, narrow my behavior
- advertised profiles cover my workflow needs? → if not, choose another host
- limits compatible with my workload? → if not, surface to user
4. Client → first protocol request
The server MUST NOT change capability response shape mid-session in a way that invalidates a client's prior negotiation. If the server's capabilities change (e.g., new node pack registered), it MAY surface this via a Capabilities-Etag response header that clients can probe periodically. See capabilities-change-detection.md for validator semantics, scoped capability views, and non-HTTP discovery handoff guidance.
Backward compatibility
Adding new fields to the Capabilities shape is non-breaking — clients ignore unknown fields. Removing or renaming fields is breaking and MUST be accompanied by a protocolVersion bump.
The required/optional split protects implementers from over-pinning: a host can be conformant with only the required base fields, while richer hosts can advertise optional profiles and capabilities without changing the protocol version.
Open spec gaps
| # | Gap | Owner |
|---|---|---|
| C2 | ✅ Closed by capabilities-change-detection.md: Capabilities-Etag semantics for mid-session capability change detection. | v1.x annex |
| C3 | ✅ Closed by capabilities-change-detection.md: non-HTTP discovery handoff guidance for MCP/A2A composition. | v1.x annex |
| C5 | ✅ Closed by capabilities-change-detection.md: scoped capability view rules without leaking private tenant features. | v1.x annex |
References
version-negotiation.md—engineVersion+eventLogSchemaVersiondeploy-skew safetycapabilities-change-detection.md—Capabilities-Etag, scoped views, and non-HTTP discovery handoffauth.md—/.well-known/openwopis unauthenticated by designrun-options.md—configurablefield semanticsobservability.md—openwop.*OTel taxonomy