feat(evm-wallet-experiment): Add delegator exos#882
Merged
Conversation
Contributor
9d0cbd8 to
0224318
Compare
6ef18c5 to
eb307c5
Compare
Ensure workspace dist files are current via Turbo cache before image assembly, so stale build artifacts don't end up in the Docker image.
…JSON requests JSON.stringify drops BigInt fields silently; replace with a replacer that coerces BigInt to string so numeric values survive the wire. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…llowedMethods, and ERC20TransferAmount These enforcers expect tightly-packed bytes, not ABI head/tail encoding. Switch encodeAllowedTargets/encodeAllowedMethods to encodePacked and encodeErc20TransferAmount likewise. Update explainDelegationMatch to decode packed terms the same way, and update the caveat encoding tests to assert packed layout directly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…and enforcer key map Add registerChainContracts() / customChainContracts so devnet chains (e.g. Anvil at 31337) can be registered at runtime without touching the static CHAIN_CONTRACTS table. Add ENFORCER_CONTRACT_KEY_MAP to translate PascalCase enforcer keys in contracts.json to camelCase CaveatType names. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Represent delegations as discoverable exo objects ("twins") so an agent
can call E(twin).transfer(to, amount) instead of manually building
Execution structs. Adds a method catalog, grant builder, twin factory
with cumulative spend tracking, and wires them into the delegation and
coordinator vats.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…wins Build typed method guards from METHOD_CATALOG entries and pass them to makeDiscoverableExo, enabling arg-count/type validation at the exo boundary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…win method args Add valueLte to CaveatSpecStruct so call twins can carry a per-call ETH value limit that is checked locally before submission. In makeDelegationTwin, normalize args[1] to BigInt on entry (required when values arrive as hex strings over the daemon JSON-RPC boundary), apply the valueLte check, and pass normalizedArgs to buildExecution. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… generation generateSalt accepts an optional entropy hex string that is mixed into the counter hash when crypto.getRandomValues is unavailable. Thread the same entropy option through makeDelegation and all three buildDelegationGrant overloads (transfer, approve, call) so callers can supply per-run entropy to avoid salt collisions across fresh vat instances. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… caveat Integrate with the delegation framework's AllowedCalldataEnforcer to pin the first argument (recipient/spender) of transfer/approve at both the on-chain enforcer level and the local exo interface guard. - Add `allowedCalldata` to CaveatTypeValues and CaveatSpec - Add `encodeAllowedCalldata` helper and deployed enforcer address - Wire optional `recipient`/`spender` through grant builders - Twin derives address restriction from caveatSpecs, not a standalone field Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Accepts a full DelegationGrant and persists only the delegation to baggage. Used by coordinator-vat's provisionTwin, where redeemFn/readFn closures must stay in the coordinator scope and cannot cross the CapTP vat boundary. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ransaction - setup-wallets: provision delegation twin via provisionTwin (not receiveDelegation) so the away coordinator holds an active twin with a cumulativeSpend caveat spec (1000 ETH, native token) - coordinator-vat: store twins created by provisionTwin in a local coordinatorTwins map keyed by delegation ID - coordinator-vat: sendTransaction routes through the twin when one is registered for the matched delegation, so local caveat checks (e.g. cumulativeSpend budget) fire before the UserOp hits the chain Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-20 grant builders - Export FIRST_ARG_OFFSET from erc20.ts; remove local copies in delegation-grant.ts and delegation-twin.ts - Extract buildErc20Grant helper from the near-identical buildTransferGrant/buildApproveGrant (~90 lines removed) - Strengthen delegation twin method guards: M.any() -> M.string() for Hex returns, M.bigint() for getBalance - Clarify cast: as keyof typeof METHOD_CATALOG -> as CatalogMethodName Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…enerator factory
Replace the module-level saltCounter + entropy threading with a proper
factory: makeSaltGenerator() returns a closure with its own counter, so
each vat instance gets an independent salt sequence rather than sharing
module-level state. The entropy parameter is removed from
TransferOptions, ApproveOptions, CallOptions, and makeDelegation;
callers that need per-instance isolation use makeDelegationGrantBuilder
with a makeSaltGenerator() instance.
- Add SaltGenerator type and makeSaltGenerator() to delegation.ts
- generateSalt is now makeSaltGenerator() called once at module load
(backward-compatible export)
- makeDelegation accepts saltGenerator? instead of entropy?
- Add makeDelegationGrantBuilder({ saltGenerator }) factory to
delegation-grant.ts; extract dispatchGrant to share the switch
- coordinator-vat creates one saltGenerator + grantBuilder per vat
instance inside buildRootObject
- Add --build to docker:up so stale images are detected automatically
- Export makeSaltGenerator, SaltGenerator, makeDelegationGrantBuilder
from index.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… message directly With the queueMessage RPC fix in @MetaMask/ocap-kernel, vat rejections now surface as response.error with the actual message rather than a generic 'Internal error'. Update callVatExpectError in both the docker e2e helper and the integration test runner to return response.error.message directly and throw on unexpected success, replacing the dead response.result.body path.
Script invoked by the docker e2e suite to test local cumulativeSpend enforcement and chain-side rejection of an expired timestamp caveat. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Runs run-delegation-twin-e2e.mjs inside the away container as part of the Docker E2E suite. Covers local cumulativeSpend enforcement and chain-side rejection of an expired timestamp delegation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ages Include log file path in assertion failure messages so the relevant service log is immediately identifiable without digging through output.
Explains the grant → twin → discoverable capability layering: - Delegation grants as serializable describable delegations (redeemable bytestring + readable caveat specs) - Delegation twins as local capabilities that mirror on-chain stateful caveats (latently; on-chain is authoritative) - M.* patterns as the mechanism for discoverability and pre-validation Documents the enforcer mapping table and M.*/Gator overlap.
Transfer and approve twins only expose their own method name, not .call(). sendTransaction hardcodes E(twin).call() for all twin types, so routing through a provisioned transfer twin throws 'call is not a function on target'. Add two tests: one for the transfer twin (fails, demonstrating the bug) and one for the call twin (passes, control case). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sendTransaction was hardcoding E(twin).call() for all provisioned twins. Transfer and approve twins only expose their own method name, so this threw 'call is not a function on target' at runtime. Track each twin's CatalogMethodName in a parallel coordinatorTwinMethods map. In sendTransaction, decode the ABI-encoded (address, uint256) calldata args and dispatch to E(twin).transfer() or E(twin).approve() for ERC-20 twins; fall through to E(twin).call() for call twins. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n from delegation vat Twins cannot live in the delegation vat because redeemFn/readFn closures cannot cross the CapTP vat boundary. The coordinator builds twins locally via provisionTwin() and uses storeDelegation() for persistence. The addDelegation and getTwin methods were never called anywhere and represent an abandoned design. Remove them along with the now-unused makeDelegationTwin import and the twins map. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r twin
A delegation with only allowedTargets (no erc20TransferAmount) matches a
no-calldata tx because delegationMatchesAction skips allowedMethods when
action.data is falsy. sendTransaction then tries to decode the twin's
calldata args with data.slice() and BigInt('0x'), which throws a
SyntaxError with no useful context.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…transfer twin args
data.slice(74, 138) returns an empty string when tx.data is missing or
short, causing BigInt('0x') to throw a SyntaxError with no useful context.
Guard the decode path with an explicit length check and throw a clear error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ldCallGrant buildErc20Grant pushes both the timestamp caveat and a blockWindow caveatSpec when validUntil is provided. buildCallGrant pushed only the timestamp caveat, so call grants with a time restriction had incomplete caveatSpecs and consumers couldn't discover the time window from the grant's structured description. Mirror the same caveatSpecs.push call. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two concurrent transfer calls both pass the budget check before either commits, so spent never exceeds max during the check and both succeed even when their combined amount exceeds the limit. The rollback method exists for exactly this pattern but is never used. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ck on failure Committing after redeemFn resolved left a window where two concurrent calls could both pass the remaining() check and together exceed the budget. Commit before the await to atomically reserve the budget, then rollback if redeemFn throws. This is the pattern rollback() was designed for but was never called. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ead of delegator The test was asserting encodeBalanceOf(BOB) where BOB is the delegate, masking the bug. The delegator (ALICE) holds the tokens; querying the delegate's balance is irrelevant to how much can be transferred via the delegation. Assert ALICE so the test catches the wrong address. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… delegate The delegator holds the tokens; the delegate only has permission to transfer them. encodeBalanceOf(delegation.delegate) returned the delegate's own balance, which is irrelevant to the delegation's spending capacity. Query delegation.delegator instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4d41291 to
0c1be9b
Compare
…nator vat The previous comment claimed the per-vat generator prevents collisions even in the SES fallback, which overstates the guarantee. When crypto.getRandomValues is available (all real environments) salts are random regardless. The counter fallback is only reached in strict SES compartments that do not endow crypto. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…TransferAmount offsets 56bbb64 switched encodeErc20TransferAmount from ABI encoding to packed encoding to match the on-chain ERC20TransferAmountEnforcer, but describeCaveat was never updated. It checked for 130-char ABI-encoded terms, always found 106-char packed terms instead, and silently fell back to the generic 'ERC-20 transfer limit' string. Update the length check and slice offsets to match packed layout. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TransferOptions and CallOptions no longer accept an entropy field since the refactor moved salt isolation into makeSaltGenerator() at the vat level. The field was silently discarded, so the per-run uniqueness guarantee described in the comment never held. Remove the unused entropy constant, its import, and the two call sites. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit fd1fecf. Configure here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Introduces delegation grants and delegation twins — the bridge between
the MetaMask Delegation Framework ("Gator") and the ocap kernel's capability
model.
A delegation grant is a serializable, describable representation of a
delegation. It pairs the raw redeemable bytestring with
caveatSpecs: astructured, human-readable description of the active caveats (spend limits,
allowed targets, calldata constraints, etc.) that the application can reason
about without decoding opaque on-chain calldata.
A delegation twin is a local discoverable exo wrapping a grant. It exposes
the delegation's permitted operations (
transfer,approve,call,getBalance) as typed ocap capability methods, derivesM.*interface guardsdirectly from the grant's
caveatSpecs, and maintains a local mirror ofstateful caveats (cumulative spend, value limits) for fast pre-rejection before
a call ever reaches the network.
Changes
New: delegation grants and twins (
src/lib/)delegation-grant.ts—buildDelegationGrant()builders fortransfer,approve, andcallgrants; encodes all Gator caveats and returns aDelegationGrantwith a matchingcaveatSpecsarraydelegation-twin.ts—makeDelegationTwin()constructs a discoverable exofrom a grant; interface guards are narrowed by the grant's
caveatSpecs(e.g.an
allowedCalldatacaveat at offset 4 locks the first argument to a literaladdress in the
M.*pattern)method-catalog.ts— registry of catalog method names, ABI selectors, andmethod schemas; used by both grant builders and twins
New: coordinator vat (
src/vats/coordinator-vat.ts)Wires delegation twins into the vat layer — builds grants, instantiates twins,
and exposes them as kernel capabilities.
Design note
The local spend tracker and value pre-checks are advisory, not
authoritative. On-chain state is the truth. The twin's job is fast
pre-rejection and a structured capability interface — not to replace the
on-chain enforcer. If spend is tracked externally (e.g., a redemption outside
this twin), the local tracker may optimistically allow a call that the chain
will reject.
Test plan
yarn workspace @ocap/evm-wallet-experiment test:dev:quiet— unit testsfor
delegation-grant,delegation-twin,method-catalogyarn workspace @ocap/evm-wallet-experiment test:e2e:docker— docker e2esuite including the delegation twin flow
transfertwin with arecipientrestriction rejects a call toa different address locally (before hitting the network)
exceed the grant's
maxNote
Medium Risk
Adds a new delegation-grant/twin execution path and routes signed delegated transactions through it, changing how transactions are validated and dispatched. Also changes caveat term encodings and chain contract resolution, which could cause mismatches with deployed enforcers if incorrect.
Overview
Introduces a new delegation grant and delegation twin layer to bridge Gator delegations into kernel-discoverable ocap capabilities, including a
METHOD_CATALOG,DelegationGrant/CaveatSpectypes, and amakeDelegationTwin()factory that derives@endo/patternsinterface guards and performs local pre-checks (e.g., cumulative spend tracking,valueLte, optionalgetBalance).Updates delegation caveat encoding/decoding to match on-chain enforcers (packed
allowedTargets/allowedMethods/erc20TransferAmount, addsallowedCalldatasupport) and adds runtime chain contract registration/mapping for custom environments.Wires twins into
coordinator-vat: adds APIs to build+sign grants, provision twins with localredeemFn/readFnclosures, and routes matchingsendTransactioncalls through a provisioned twin (with safer calldata decoding). Adds unit and docker e2e coverage and improves daemon client error/bigint serialization; docker scripts now build monorepo first and bring up containers with--build.Reviewed by Cursor Bugbot for commit fd1fecf. Bugbot is set up for automated code reviews on this repo. Configure here.