OpenWOP openwop.dev

OpenWOP is additive to tool calling. It defines no model-facing function-calling format and replaces neither MCP nor A2A; it specifies the things those protocols deliberately leave to the harness — who was allowed to call, what a failure looks like on the wire, and what happens the second time the same call arrives.

What tool calling standardizes, and what it leaves open

In the agent world, "tool calling" is a model-output convention. The model emits a name plus JSON arguments, a harness dispatches it, and the result comes back as another turn. Function calling, tool use, and the Model Context Protocol (MCP) are variations on that shape. They standardize how a tool is described — name, description, JSON Schema — and how a single invocation is framed. MCP goes further and makes the tool list a server you can connect to.

What none of them specify:

  • Who was allowed to make the call.
  • What happens if the same call arrives twice.
  • How a failure is distinguished from a success that returned nothing.
  • Whether replaying a conversation re-fires the side effect.

That is fine for a chat loop. It is not fine for an engine that resumes after a crash.

OpenWOP's position: a tool call is a durable, authorized, idempotent effect on a run's event log — not a turn in a transcript.

Four surfaces

Description is a projection, not a registry

OpenWOP tools already lived behind five unrelated surfaces — node-pack typeIds, workflow-as-tool (core.subWorkflow and chains), MCP servers, connectors, and host-extension scopes — each discoverable only through its own mechanism. An agent handed a toolAllowlist of opaque <scope>:<tool-id> strings had no way to resolve them.

The toolCatalog capability unifies all five behind one ToolDescriptor (tool-descriptor.schema.json):

FieldFieldField
toolIdsourcetitle
descriptioninputSchemaoutputSchema
authegressapproval
replayPolicysafetyTiercostHint
latencyHint

toolId, source, and safetyTier are required; source is exactly the five origins above (node-pack, workflow, mcp, connector, host-extension).

The catalog is read-only, authorization-scoped, and non-disclosing: an unauthorized tool id returns 404, not 403. The catalog will not confirm that a tool exists to someone who may not use it. Descriptors carry requirement flags such as auth.credentialRef, never credential material. Sub-keys: sources, sessionLifecycle, compactView.

Invocation is a pair of durable events

Not a transcript entry. agent.toolCalled and agent.toolReturned are both in the v2 event codemap, alongside tool.session.opened and tool.session.closed.

Permission is fail-closed

Under toolHooks.perToolAuthorization, the host checks the run principal's scopes before invoking — and if authorization cannot be evaluated, it does not invoke. It emits agent.toolReturned { status: 'forbidden' } and a 403. Sibling sub-keys: prePostEvents, perToolRateLimit.

Failure has to be legible on the wire

The discriminators on agent.toolReturned (error, outcome, status) are optional in the schema, so a bare {agentId, toolName, callId} return was schema-legal on failure — a failed tool was wire-indistinguishable from one that succeeded and returned nothing. A host MUST NOT represent a failure as a bare success; that shape is reserved for genuine success.

Idempotency: the part no function-calling spec addresses

OpenWOP gives the effect — not the call — an identity. This is written out in full normative prose in idempotency.md.

RuleWhat the specification requires
KeyingDerived from the business operation, stable across every entry point, containing no runId, nodeId, or ordinal.
AttemptsThe retry counter MUST NOT participate in the identity. Two retries of one logical invocation collide rather than diverge.
ClaimThe persist that guards the effect MUST be an atomic claim — compare-and-set or insert-if-absent — that at most one executor can win.
Provider keyWhere the provider accepts an idempotency key, the host MUST inject the effect identity or a documented deterministic derivative.
RetentionAn effect record MUST be retained for at least 14 days.

It is checkable rather than asserted. A host advertising idempotency MUST serve GET /runs/{runId}/effects, each record carrying effectId, nodeId, attempt, keying, and state.

Why the atomic claim exists

A chat-loop mental model breaks under recovery. An orphan sweeper re-dispatches a run whose previous owner is stalled but still alive. Both executors re-execute the node from the start, so both mint the same effect identity. If the invocation log is a read-then-write, both miss and both fire.

A host had exactly that shape — INSERT OR REPLACE, which always wins and never reports a conflict — and measured two charges where there should have been one.

The rule is worded to hold within a single-instance deployment. This is not a distributed-systems edge case; it is reachable on one machine.

Composing with MCP and A2A

OpenWOP does not compete on transport. REST and SSE are the wire, and a v2 host MUST NOT advertise a transport list — supportedTransports does not exist in the v2 capabilities schema, and a discovery document carrying it fails validation.

A2A and MCP are facets, not forks. A host that speaks either MUST advertise the corresponding facet with every required field, per interop.md:

Facet fieldA2AMCP
Offeredversions[] as major.minorrevisions[] as dates
DefaultpreferredVersion, served when the peer names nonepreferredVersion
FloorminimumVersionminimumRevision
FreshnessrefreshedAtrefreshedAt
Profile idsa2a-<major.minor>mcp-<date>

A negotiation that would land below the floor fails closed with interop_version_unsupported.

There is no legacy escape hatch: a2a-0.3-legacy and mcp-2025-06-18-legacy do not exist in v2, and the profiles[] item patterns admit no -legacy suffix. Version negotiation is a protocol, not a bag of compatibility shims.

Two more details worth keeping straight:

  • MCP round ceiling. mcp.mrtr.maxRounds (an integer, 1–16) is an advertised ceiling on multi-round tool-result rounds. Exceeding it fails with mcp_mrtr_rounds_exceeded.
  • mcp.serverMount.transports[] is the MCP server's own transport enum — explicitly not a host transport advertisement. The two get conflated easily.

The catalog composes rather than reinvents. auth.scopes declares what the per-call authorization enforces; egress: "safe-fetch" advertises that a tool routes through the host's SSRF-guarded fetch; approval surfaces the existing approval requirement; and safetyTier: "exec" is an honest carve-out that MUST declare source: "host-extension" and MUST NOT be dressed up as protocol-tier.

The agent world standardized the envelope. OpenWOP specifies who may call, what a failure looks like, and what happens the second time the same call arrives.

Where the normative text lives

The effect layer is written out directly in v2 core, at idempotency.md. The authorization and failure-honesty rules are owned by their RFCs rather than restated in the core tree — read RFC 0064 for tool invocation hooks and authorization, and RFC 0078 for the portable tool catalog and tool-session contract. Error codes referenced above are in the error vocabulary.