
ReputAI
Verifiable ENSv2 subnames, identities, and role-based access control for autonomous AI agents.
Demo
About
Give an agent its own name and a compromised agent walks off with it — repoints it, redirects the address people pay, sells it. Keep the name yourself and co-sign every update, and the agent isn't autonomous; in practice nobody hand-approves a restart at 3am. ENSv2's Enhanced Access Control makes a third option possible, because permissions are resource-scoped down to the individual record. An agent gets a subname under the operator's domain. On its own, with nobody in the loop, it can change its service endpoint and **rotate its own signing key**. It cannot transfer the name, repoint its resolver, change the `addr()` counterparties pay, write any text key outside its allowlist, widen its permissions, or unregister itself. The operator keeps a one-transaction kill switch: `unregister()` force-expires the name and bumps its `eacVersionId`, so the agent's roles don't merely stop working — they cease to exist. > Compromising an agent's key lets an attacker impersonate its messaging until revocation — > never receive its funds, take its name, or outlive the kill switch. The agent can move around inside the box. It cannot move the box. **Three contracts, deployed and verified on Sepolia** beneath a real `.eth` name registered live through the canonical `ETHRegistrar`: - **AgentSandbox** — one `provision()` call mints the subname, grants a ceiling-checked role bitmap, and allowlists the exact text keys the agent may write. It holds no per-name roles itself, so it is provably not a backdoor into the names it mints. - **CounterpartyVerifier** — walks from the ENS root, verifies the agent's signature against the key the name publishes *right now*, and pays the resolved `addr()`, never what the agent claimed. Resolves, verifies and pays in one transaction, or reverts with the reason it refused. - **SubAgentRegistrar** — workers whose permissions are a strict subset of the orchestrator's; killing the orchestrator severs the whole fleet in one transaction. 101 tests against the real `ensdomains/contracts-v2` contracts, no mocks. One finding worth flagging: ENS resolver lookup walks up the tree, and a revoked name's records survive in the parent's resolver because they're keyed by namehash, which unregister()` doesn't rotate. A verifier that accepts an inherited resolver keeps paying a revoked agent. Resolution must be exact, inheritance is not identity. Containment here is unreachability, not deletion, which is exactly why the verifier is load-bearing.
How it's made
Solidity + Foundry, with `ensdomains/contracts-v2` as a git submodule compiled from source — so the tests run against the real `PermissionedRegistry`, `PermissionedResolver` and `ETHRegistrar`, not my idea of them. Deployment is `forge script` to Sepolia, verified on Etherscan; a fork test runs the deploy scripts themselves so wiring mistakes fail in `forge test`. The demo front end is Node + viem driving a four-pane tmux control room that polls the chain and holds nothing between frames — when the operator revokes in one pane, the verifier's verdict in another goes red on its own. The security model is two ENSv2 tiers pulling in opposite directions on purpose. At the registry the agent's authority is defined by subtraction - a role ceiling with `ROLE_SET_RESOLVER` and `ROLE_UNREGISTER` withheld. At the resolver it's defined by addition `authorizeTextRoles()` per key, so the agent holds `ROLE_SET_TEXT` at `resource(namehash, keccak256(key))` and nowhere else. `addr()` is unreachable from either direction. The sandbox contract holds only admin roles, so it can grant the right to write a key but cannot write a record itself. That record-level scoping is what ENSv2 bought: hand-rolling tiered, resource-scoped permissions securely in a hackathon window isn't realistic. Hierarchical registries then give subtree revocation as protocol behaviour. One transaction, no enumeration and being inside ENS means the agent resolves in every wallet and contract that already speaks ENS. The nitty-gritty: - `pubkey` was unusable, and it forced the design. `setPubkey` checks the name-wide resource and there's no `authorizePubkeyRoles` to narrow it, so granting it hands the agent a permission the operator can't scope. The rotatable key lives in a text key instead — which every ENS client already reads. - Registration is commit-reveal and the commitment binds the subregistry and resolver addresses, so both proxies must exist before committing: two scripts and a `sleep 60`, with phase 2 re-deriving the commitment before spending gas. - A `.eth` name is an ERC-1155, so an EIP-7702-delegated EOA has code and reverts `ERC1155InvalidReceiver`. Cost real debugging time; now pinned by a test. - Nothing is hard-coded. The whole run takes one variable, `DEMO_AGENT_LABEL`, and works unchanged against `anvil --fork-url` , which is how it was rehearsed.