Skip to content

feat(edge): mint an effect id for each module call - #58

Merged
markwang-obin merged 2 commits into
mainfrom
feat/edge-mints-effect-id
Oct 29, 2026
Merged

markwang-obin merged 2 commits into
mainfrom
feat/edge-mints-effect-id

Conversation

@markwang-obin

Copy link
Copy Markdown
Contributor

What

The edge mints a fresh x-codefly-effect-id (receipts.EffectIDHeaderName, a UUID) on every module call it forwards. It does this after stripping the browser's x-codefly-* headers, and sets none on frontend calls.

Why

A module operation that keeps receipts (Codefly sdk-go/receipts, RECOVERY_RECEIPT) requires an effect id on every call. Its guard refuses a call without one. The edge forwards no x-codefly-* header a browser sent, so today a browser can never reach a receipt-guarded operation.

Found in obin-ai/module-robin#633. To prove confirmed writes, Robin makes RenameConversation recoverable with the receipts SDK, and the chat UI's own rename would then be refused. Every receipt-guarded write a page calls hits the same wall.

Why the edge mints it, rather than the browser sending one

  • It keeps the edge's rule: "no x-codefly-* header leaves the edge unless the edge set it".
  • A browser can never name an effect. Receipts are keyed by tenant, effect id and method, not by person. A browser-chosen id, if known to someone else in the tenant, would let them replay that person's recorded answer without the handler running. Minting at the edge closes that off.
  • The cost: each call is its own effect, so a browser retry is a new attempt. That's what happens today, so nothing is lost.

Considered and not done: forwarding a browser-sent effect id. That would make UI retries safe, but it relaxes the edge's rule and needs receipts keyed by person.

Changes

  • edge.go: toModule mints the id, proxy sets it after stripping; the frontend proxy passes none.
  • effect_test.go (new): a forged X-Codefly-Effect-Id is replaced, two calls get different ids, and the Connect face gets one too. It failed before the change ("the module received effect id """).
  • environment_proxy_test.go: the new argument.
  • README: the edge section states the minted id.
  • go.mod / go.sum: sdk-go was already required; receipts brings one indirect dependency (santhosh-tekuri/jsonschema/v6).

Fix or hack

Fix at its owner: the edge owns which x-codefly-* headers reach a module. Interfaces and conformance don't state edge header behaviour, so this isn't a contract change.

Open: a production host's browser edge (Accounts / saas-starter auth-gateway) has to do the same, or a receipt-guarded write called from a page breaks there too. I didn't check that edge's header handling.

Checks

  • go vet . and go test . in modules/authority/services/edge/code: pass.
  • Not run: the playground end to end. module-robin picks this up when it moves its authority pin, as part of #633.

🤖 Generated with Claude Code

A module operation that keeps receipts (sdk-go/receipts, RECOVERY_RECEIPT)
requires an x-codefly-effect-id on every call, and the edge forwards no
x-codefly-* header a browser sent. So a browser could never reach a guarded
operation: obin-ai/module-robin#633 found chat's rename would break for the UI
the moment it is guarded.

The edge now sets a fresh effect id (uuid) on each module call, after
stripping the browser's x-codefly-* headers, and none on frontend calls. It
stays within the edge's rule ("no x-codefly-* header leaves the edge unless the
edge set it"), and since a browser never names an effect it can't replay
another person's receipt: receipts are keyed by tenant, effect and method, not
by person. Each call is its own effect, so a browser retry is a new attempt,
as it is today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The receipts SDK accepts Idempotency-Key as the HTTP spelling of the effect
id. The edge kept it, so a caller sending one put two effect ids on the call,
and both SDK adapters refuse that with InvalidArgument before the operation
runs. Strip it whenever the edge mints the effect id; the test now sends one on
REST and Connect and checks neither reaches the module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@markwang-obin

Copy link
Copy Markdown
Contributor Author

Review finding fixed in 7a2439a: the edge now drops a caller's Idempotency-Key (receipts.IdempotencyKeyHeaderName, the SDK's HTTP spelling of the effect id) whenever it mints the effect id, so a call carries exactly one. TestEachModuleCallCarriesAnEffectIDTheEdgeMinted now sends one on REST and Connect and checks neither reaches the module; it failed before the fix. go vet and go test -race pass in the edge. README updated.

@markwang-obin
markwang-obin merged commit d3dbee6 into main Oct 29, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant