Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/contracts/effects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Durable effects contract v1

Hermes Workflows identifies a logical external operation independently of an execution attempt. The operation ID is the SHA-256 of canonical JSON containing the workflow instance ID, logical effect key, adapter ID, and canonical input hash. Attempt numbers, claim tokens, worker IDs, and clocks are deliberately excluded. JSON object keys must be unique; mapping item streams with repeated keys are rejected rather than silently collapsed during normalization.

## Delivery semantics

This contract is **at least once**, not exactly once. A worker may start more than once. An idempotent adapter must accept the stable operation ID, expose `lookup_receipt(operation_id)`, and ensure repeated `perform(operation_id, input)` calls converge on one externally observable operation. The framework's durable receipt proves what the adapter reported; it cannot prove universal exactly-once delivery.

Policies are explicit:

- `pure`: no externally observable mutation.
- `idempotent`: repeats with the same operation ID converge on the same external result.
- `unsafe`: execution is refused unless a caller explicitly authorizes it. That authorization is persisted with the intent and rechecked by the coordinator immediately before any adapter lookup or external `perform` call. A pending intent can be claimed once, but an expired uncertain claim cannot be reclaimed automatically and must go to human reconciliation.
- `unclassified`: refused. Legacy effects are unclassified until an owner classifies and adapts them.

No policy is inferred from a function name, tool name, or implementation.

## Durable records and fencing

`effect_intents` stores canonical input, input hash, policy, explicit unsafe-authorization evidence, state, attempt count, and the active claim token. Before insertion, the store recomputes the complete operation identity from workflow ID, effect key, adapter ID, and the canonicalized input and rejects any mismatch. State moves from `pending` to `claimed`, then to `completed` or `failed`. Every successful claim receives a fresh, unpredictable token generated by the store; callers cannot choose ownership identity, and the durable `effect_claim_tokens` ledger prevents reuse of any token previously issued by that store, including across expiry and reclaim. An expired claim may be replaced. Completion and failure use compare-and-swap on the active opaque claim token; a stale worker cannot write a receipt or terminal state. Directly persisted unsafe intents without authorization remain inventoriable and claimable once, but the execution boundary refuses them before adapter lookup, external mutation, or durable completion.

`effect_receipts` stores the adapter receipt, its SHA-256, sensitivity flag, completion time, and the claim token that fenced the write. Intent completion and receipt insertion are one SQLite transaction.

The public receipt descriptor contains only operation ID, adapter receipt ID, receipt hash, presence, sensitivity, and completion time. It never includes the raw payload. Sensitive payloads remain available only through the durable effect store's privileged lookup path.

## Adapter lookup

Adapters are obtained through the FND-RT process-local service registry at service ID `effects.adapters`, contract version `1`. The resolver returns an adapter whose `adapter_id` exactly matches the request and which implements:

- `lookup_receipt(operation_id)`
- `perform(operation_id, canonical_input)`

The coordinator always queries the adapter receipt before performing the operation. This closes the recovery window where the external system accepted the operation but the local receipt transaction did not commit, provided the external adapter can look up the stable operation ID. A receipt may omit `operation_id`; if it includes one, it must exactly match the requested operation or the coordinator rejects it before durable completion.

## Crash windows

The probe covers the four approved windows: before the adapter call; during the adapter call; after external success before adapter-receipt commit; and after durable receipt commit before linked-command completion. Each scenario runs at least two worker attempts, produces one external effect, and ends with one completed local receipt. This is evidence for the supplied file-backed adapter, not a general exactly-once guarantee.
Loading