OpenWOP openwop.dev

Status: Stable · v2.1.0 (2026-09-11) · RFC 0170 §E.1, RFC 0171 §A.4, RFC 0173 §B.

Why this exists

interrupt is the one primitive by which a run waits for something outside itself: a human decision, an answer, an external event, a conversation turn. Every kind shares one payload shape, one pair of events, one resolve contract and one token scheme, so a client that can resolve an approval can resolve anything.

Payload

schemas/v2/suspend-request.schema.json (InterruptPayload) is closed and discriminated by kind; kind, key and data are REQUIRED.

Kinddata (required fields)Notes
approvalartifactId, artifactType, title, actions5-action vocabulary below; quorum and eligibility fields
clarificationquestions[] (id, question, optional schema)Snapshot status waiting-input
external-eventeventType, correlationSnapshot status MUST be waiting-external
customcustomKind, optional payloadA host MUST accept and persist it; rendering is best-effort
conversation.startconversationIdGated on the conversation capability (capabilities.md); conversationId MUST be tenant-unique and MUST NOT be assumed resolvable on another host
conversation.exchangeconversationId, promptThe resume value MUST validate against outcomeSchema when supplied
conversation.closeconversationIdGated as above
low-confidenceagentId, threshold, observedAn agent.decided with confidence below the threshold MUST be followed by node.suspended { reason: 'low-confidence' }; the per-run threshold is configurable.run.escalationThreshold (runs.md)

key is the deterministic re-entry key: a host MUST invoke an interrupt with key K at most once for the lifetime of the run. On recovery the engine MUST consult the event log, find the prior interrupt.resolved, and return the persisted resumeValue without emitting a second interrupt.requested; an in-memory cache MAY serve in-process replays but MUST NOT replace the event log for cross-process replays. A host MUST validate the resume value against resumeSchema when one is declared and MUST refuse a failing value with 400 validation_error. timeoutMs, when set, is the interrupt's own deadline.

Events

Every kind is recorded by two registered types (events.md): interrupt.requested, whose payload is the InterruptPayload verbatim, and interrupt.resolved, whose payload is { nodeId, interruptId, kind?, resumeValue? } (closed). The legacy approval. and clarification. types remain registered; their payload definitions in schemas/v2/run-event-payloads.schema.json are $ref aliases of interruptRequested and interruptResolved (RFC 0171 §A.4 E4), so there is one shape per direction. A host emitting interrupt.requested SHOULD also emit the legacy kind-specific type until its consumers migrate. Both events are durable and appear in the updates and debug stream modes. While suspended, RunSnapshot.currentNodeId names the node and status is waiting-approval, waiting-input or waiting-external.

Resolve surfaces

OperationPathAuthBody
resolveInterruptByRunPOST /runs/{runId}/interrupts/{nodeId}approvals:respond{ resumeValue } (closed)
inspectInterruptByTokenGET /interrupts/{token}the token— (returns the InterruptPayload)
resolveInterruptByTokenPOST /interrupts/{token}the token{ resumeValue } (closed)

A host MUST expose the run-scoped surface and SHOULD expose the signed-token surface for callers not authenticated to the protocol (a payment webhook, a mail link). Every resolve MUST honor Idempotency-Key (idempotency.md). Exactly one of two concurrent resolves MUST succeed; the other MUST receive 409 interrupt_already_resolved.

StatusCodeCondition
400validation_errorresumeValue fails resumeSchema or the approval action is not in actions
401interrupt_token_invalidMAC, alg or kid not accepted
404not_foundNo such run or node
409interrupt_already_resolvedAlready resolved; or a token invalidated by resolution, cancellation or completion
410interrupt_expiredToken past expiresAt (token surface only)

Tokens

The token grammar is ow2.<alg>.<kid>.<payload>.<mac>, defined in identity.md; alg MUST be one the host advertises in interrupt.tokenAlgs[] (hs256 at the cut) and kid MUST select a secret the host holds, otherwise 401 interrupt_token_invalid. A v1 two-segment token remains resolvable under kid: legacy until its expiresAt.

RuleRequirement
ExpiryEvery token MUST carry expiresAt. The default SHOULD be 30 minutes; a host MUST cap the lifetime at the interrupt's timeoutMs when one exists. A token MUST NOT outlive the interrupt it resolves; past expiresAt the host MUST answer 410 interrupt_expired.
InvalidationA token MUST be invalidated when its interrupt is resolved or its run is cancelled or completed; later use MUST answer 409 interrupt_already_resolved.
VerificationMAC comparison MUST be constant-time. kid selects the verification secret so secrets rotate without orphaning outstanding tokens.
IntentA token minted with intent: resolve authorizes both operations; a host MAY mint intent: inspect tokens, and a resolve with one MUST be refused with 403.

Approval

actions is a non-empty subset of accept, reject, refine, edit-accept, ask; a host MUST enforce it on resolve. ask does not exit the suspend.

actionRequired field
accept— (feedback?)
reject— (feedback?)
refinerefineFeedback { scope: whole | section | items, sectionPath?, itemIds?, tags?, text? }
edit-accepteditedArtifactData

Every resume carries decidedAt; decidedBy MAY be omitted by an authenticated caller, and every consumer MUST treat it as an opaque string. requiredApprovals sets the quorum (default 1) and rejectionPolicy is single-veto (default) or majority; when overrideBypassesQuorum is true a configured override principal MAY release the gate alone, otherwise its vote counts once.

Approver enforcement

Enforcement is an obligation of the fields, not a discovery flag (RFC 0173 §B). The facet spec/v2/facets/interrupt.schema.json carries tokenAlgs[] (REQUIRED) and refKinds[]principal, group, role.

FieldBinds
approversList (explicit principals)Everywhere: a host advertising interrupt MUST refuse a resolver not in the list
approverGroupRefsOnly where refKinds includes group: the host MUST surface the field unchanged and MUST resolve and enforce its members as eligible approvers
approverRoleRefsOnly where refKinds includes role: as for groups, with holders
audienceA notification hint, never eligibility; omitted ⇒ the host SHOULD notify the union of the eligibility refs

Refs are opaque to the engine; the host resolves them against its own identity model. Membership MUST be resolved at decision time and MUST NOT be re-resolved during replay or forkRun: the recorded eligibility decision is fixed history (replay.md). A host that does not advertise a ref kind MUST ignore that field. A relaxation of any obligation here is an operator setting recorded in the certification bundle, never a discovery field (security-defaults.md).