Skip to content

Move the transaction policy engine to @tetherto/wdk-policies - #127

Merged
jonathunne merged 8 commits into
mainfrom
feat/move-policy-engine-to-wdk-policies
Nov 10, 2026
Merged

jonathunne merged 8 commits into
mainfrom
feat/move-policy-engine-to-wdk-policies

Conversation

@AlonzoRicardo

@AlonzoRicardo AlonzoRicardo commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Description

The transaction policy engine leaves core. It moves to tetherto/wdk-policies and is published as @tetherto/wdk-policies, an opt-in plugin that attaches to a WDK instance. Core keeps one small middleware hook, so the plugin can still govern every account the instance hands out.

Removed

  • src/policy/, types/src/policy/ and docs/policy-exclusions-audit.md.
  • WDK#registerPolicy, WDK#getPolicyExclusions, the constructor options maxConditionTimeoutMs and policyExclusions, _applyPolicies, and the engine calls in dispose.
  • From index.js: PolicyViolationError, PolicyConfigurationError, DEFAULT_POLICY_EXCLUSIONS, DENIAL_CODES, and the policy typedefs.
  • tests/wdk-policy.test.js, which moves with the engine.
  • The policy sections of README.md and AGENTS.md, replaced by a pointer to the new package.
  • zod from dependencies.

Changed: the middleware hook, in _runMiddlewares

  • A middleware receives a second argument, { blockchain, index, path }. index is absent for getAccountByPath; path is the account's full derivation path, or null if the account is not bound to a BIP-44 position.
  • A middleware's non-nullish return value replaces the account. The next middleware, and the caller of getAccount, get the replacement.
  • Middlewares run after _registerProtocols, where the engine used to wrap the account, so the account a middleware returns already has the protocol getters.

New typedef MiddlewareContext. MiddlewareFunction becomes (account, context) => Promise<account | void>.

Kept from the engine fixes that landed before the move

Motivation and Context

Every install of core ships the engine and its zod dependency, whether or not the app registers a single policy. The engine is in beta and opt-in by nature: nothing else in core uses it, and the files in src/policy/ import nothing from the rest of core, only zod.

A wrapper around the WDK instance would have needed no core change, but the code that created the instance could still get ungoverned accounts from it. The hook keeps today's property that every account the instance hands out goes through the engine.

What a developer changes

Before, in core After
new WDK(seed, { maxConditionTimeoutMs, policyExclusions }) new WDK(seed) and new WdkPolicies({ maxConditionTimeoutMs, policyExclusions })
wdk.registerPolicy(policies, options) new WdkPolicies().attach(wdk, 'ethereum').register(policies, options)
wdk.getPolicyExclusions() policies.getExclusions()
wdk.dispose(blockchains) wdk.dispose(blockchains), plus policies.dispose(blockchains)
errors and constants from @tetherto/wdk the same names from @tetherto/wdk-policies

Policies, rules, conditions, the evaluation context, simulation, denial codes and the error classes do not change.

Changelog note

  • The policy engine moved to @tetherto/wdk-policies; registerPolicy, getPolicyExclusions, the two constructor options and the policy exports are removed from core.
  • A middleware now receives { blockchain, index, path } as a second argument.
  • A middleware that returns a value by accident now replaces the account. The known middlewares, in wdk-examples/wdk/middleware.ts and the docs' middleware guide, return nothing.

Verification

  • npm test: 56 pass. The middleware tests expect the context argument and gain six cases: two replacement cases, the first-seen path, a null path, and how _buildProtocol treats a middleware's wrapper for account-registered and wallet-registered protocols. Each of the last four fails if the property it pins is reverted.
  • npm run lint: clean.
  • The engine's own suite, 157 tests, passes unchanged apart from its import line against this branch plus @tetherto/wdk-policies. See Receive the transaction policy engine from wdk-core wdk-policies#2.

Related Issue

Closes #122

Companion: tetherto/wdk-policies#2

Type of change

  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Release order

  1. @tetherto/wdk, the next beta, from this PR.
  2. @tetherto/wdk-policies 1.0.0-beta.1, right behind it. Its lockfile and CI need that core version on npm.
  3. wdk-docs the same week: the changelog, the core API reference, core configuration, the core index, the transactions guide, the transaction-policies guide, and the middleware guide's context argument and return value.

The policy engine is opt-in and imports nothing from the rest of core,
only zod, yet every install of @tetherto/wdk ships it. It moves to
tetherto/wdk-policies and is published as @tetherto/wdk-policies, a
plugin that attaches to a WDK instance. Core keeps one middleware hook
so the plugin can still govern every account the instance hands out.

Removed:

- src/policy/, types/src/policy/ and docs/policy-exclusions-audit.md.
- WDK#registerPolicy, WDK#getPolicyExclusions, the constructor options
  maxConditionTimeoutMs and policyExclusions, _applyPolicies, and the
  engine calls in dispose.
- From index.js: PolicyViolationError, PolicyConfigurationError,
  DEFAULT_POLICY_EXCLUSIONS, DENIAL_CODES and the policy typedefs.
- tests/wdk-policy.test.js, which moves with the engine.
- The policy sections of README.md and AGENTS.md, replaced by a
  pointer to the new package.
- zod from dependencies.

Changed, in _runMiddlewares:

- A middleware receives a second argument, { blockchain, index, path }.
  index is absent for getAccountByPath; path is the account's full
  derivation path.
- A middleware's non-nullish return value replaces the account. The
  next middleware, and the caller of getAccount, get the replacement.
- Middlewares run after _registerProtocols, where the engine used to
  wrap the account, so the account a middleware returns already has
  the protocol getters.

Kept from the engine fixes that landed before the move:

- #117: protocols are still built when a getter is called, through
  account._buildProtocol(category, label, subject). The plugin passes
  its governed account as the subject, so a protocol registered on the
  account is built with it. A protocol registered with
  wdk.registerProtocol is still built with the original account.
- #120: path is the one recorded the first time WDK saw the account,
  not a fresh read of account.path, so a later change to the account
  cannot change the path middlewares receive.

New typedef MiddlewareContext. MiddlewareFunction becomes
(account, context) => Promise<account | void>. The middleware tests
expect the context argument and gain two replacement cases, a
first-seen path case, and two cases for how _buildProtocol treats a
middleware's wrapper.

A middleware that returns a value by accident now replaces the
account.

Version 1.0.0-beta.21.

Refs #122
The inline comments in _registerProtocols still described the policy
proxy, which no longer lives in core. Their content now sits in the
method's JSDoc, written in terms of a middleware's wrapper. Also rewraps
the documentation lines this PR added that ran past 120 columns.
- _getAccountPath and _registerProtocols carry only @Private, in the
  source and the declarations.
- Stub values returned by mocks use the DUMMY_ prefix and dummy-
  placeholder strings.
- The account-protocol test asserts the exact protocol instance instead
  of its class, and the two new protocol tests have shorter names.
@darkhorse-core

Copy link
Copy Markdown

One compatibility note while reading this alongside the plugin: since a middleware's return value now replaces the account, middlewares written as concise arrows for beta.20 change behaviour silently, e.g. async (acc) => seen.push(await acc.getAddress()) makes getAccount() return 1, and async (acc) => cache.set(acc.path, acc) returns the Map. Rejecting a replacement that doesn't look like an account (no getAddress, say) with a clear error would make this fail loudly. Details on the versioning side are in tetherto/wdk-policies#2.

1.0.0-beta.21 was released from main with the engine still in core, so the move ships in the next version.
@AlonzoRicardo

Copy link
Copy Markdown
Contributor Author

Thanks, that's a real sharp edge. We'd rather not add a runtime check for it, though. MiddlewareFunction is typed to return the account or nothing, and WDK avoids runtime checks for what the types already rule out. Instead, the changelog and the middleware guide will call the change out, so a middleware written as a concise arrow gets braces or an explicit return.

Comment thread src/wdk.js Resolved
Comment thread src/wdk.js Resolved
Take main's bare-node-runtime ^1.5.2. zod stays removed.
path can be null, as in wdk-wallet, and a middleware may return any
IWalletAccount.
@jonathunne
jonathunne dismissed quocle108’s stale review November 10, 2026 11:35

requested changes were made

@jonathunne
jonathunne merged commit ac1a837 into main Nov 10, 2026
7 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.

Move the transaction policy engine to @tetherto/wdk-policies

4 participants