Skip to content

Headless hardening: fixes from the live test run - #9

Merged
baairon merged 8 commits into
masterfrom
claude/wonderful-wright-obszfp
Oct 7, 2026
Merged

baairon merged 8 commits into
masterfrom
claude/wonderful-wright-obszfp

Conversation

@baairon

@baairon baairon commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes from running the full test plan against the live agent on Base. Most of them are places that said "all fine" without having looked. One commit per fix.

  • restore --owner finds Vault-held agents.
    • A token in a Vault is owned by the Vault, so balanceOf(owner) is 0 and the lookup returned agents: [].
    • Discovery now also scans transfers out of the wallet and keeps tokens whose Vault records this wallet as owner. This works for every Vault build.
    • When the RPC can't serve the log history and nothing was found, it exits 1 with the RPC detail and an ETHAGENT_RPC_URL hint, instead of an empty list.
  • check and the manager read tokenURI.
    • agentURI(uint256) reverts on the registry, so the URI probe was always unknown and mid-flow custody never fired.
    • A different pointer is now classified: local-newer when a pinned snapshot is waiting, chain-newer otherwise. The manager's "Newer snapshot onchain" banner can appear again.
    • Mid-flow custody comes from local state (the token is in the Vault but Advanced was not saved here), not from URI drift.
  • save --operator matches save.
    • It pulls the tools' edits first, which it never did.
    • It skips when nothing changed, so it no longer pins and sends with no changes.
    • It reads the token back and reports verification.
    • Both saves share one helper and now print schema: 1.
  • Stale daemon.pid.
    • The daemon touches its pid file every 30 seconds.
    • Only a pid that is alive and beat recently counts as running or gets SIGTERM. A recycled pid no longer reads as the daemon.
  • No hosts.json.*.tmp leftovers. Host-stats writes are synchronous, so process.exit can't cut them off. Stale temp files are swept on load.
  • ens --delete --operator.
    • Allowed only when the operator key manages the parent; that key could already remove the name at the ENS contracts directly.
    • The preflight still refuses any other key, and the unlinked save still needs the owner.
    • The browser path's hint points to --operator when that is the parent's manager.
  • ens without a key in the shell reports whether the active operator could sign (keyAvailable: false), instead of operator: null.

Docs: README (restore --owner, save verification, the operator delete rule) and commands/ethagent.md.

Testing

  • npm run typecheck and npm test (775 pass) in a clean env.
  • New tests:
    • discovery: a Vault-held agent is listed, and an unscannable wallet throws with the RPC detail;
    • restore: the --owner error and hint;
    • reconciliation probes: tokenURI, local-newer vs chain-newer, mid-flow;
    • operator-save: pull, skip, verify, exit 3;
    • a stale pid is neither running nor signalled;
    • the temp-file sweep;
    • ens --delete --operator, and the keyless operator view.
  • Smoke test: restore --owner in a temp HOME with no reachable RPC exits 1 with the detail and the hint, and leaves no temp files.

To re-check after merge:

  • restore --owner <owner> --network base --json lists #45744;
  • check --json reports a real chain.agentUri;
  • ens --json shows the operator without keychain exec;
  • keychain operator-save with no edits reports skipped and sends nothing.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HG2miyE22qTuug1Quxj7iw


Generated by Claude Code

claude added 8 commits October 7, 2026 00:47
…it can't look

A Vault-held token belongs to the Vault, so balanceOf(owner) is 0 and the owner
lookup returned an empty list. Discovery now also scans transfers out of the
wallet and keeps tokens whose Vault records this wallet as the owner. When that
scan can't run and nothing was found, it fails with the RPC detail and an
ETHAGENT_RPC_URL hint instead of claiming the wallet holds nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HG2miyE22qTuug1Quxj7iw
agentURI(uint256) reverts on the Base registry, so the agent URI probe was
always unknown and the custody probe never saw a mid-flow switch. The probes
now read tokenURI. A differing pointer is this machine's when a pinned snapshot
waits to publish and the chain's otherwise, so the manager's "Newer snapshot
onchain" banner can finally appear. Mid-flow custody comes from local state
(token in the Vault, Advanced not saved here), not from URI drift.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HG2miyE22qTuug1Quxj7iw
The operator path never pulled the tools' edits and would pin and send a
transaction with nothing changed. It now pulls first, skips like save does,
and reads the token back after publishing to report verification. The
no-changes check, the read-back, and the JSON envelope live in one helper
both saves use, and both now print schema: 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HG2miyE22qTuug1Quxj7iw
Status and stopDaemon trusted any live pid, so a pid the system had reused read
as the sync daemon and could be sent SIGTERM. The daemon now touches its pid
file every 30 seconds, and only a pid that is alive and beat within two minutes
counts as running or gets signalled; a stale file is cleared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HG2miyE22qTuug1Quxj7iw
The deferred host-stats write was asynchronous, so a command ending with
process.exit could cut it off after the temp file was written and before the
rename. Every write is now synchronous (the file is small), and the first load
sweeps temp files older than a minute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HG2miyE22qTuug1Quxj7iw
Only the parent's manager can remove a subname, and when that is the agent's
operator key the delete had no headless signer. It may now sign with
--operator; the preflight still refuses any key that doesn't manage the
parent, and publishing the unlinked agent still takes the owner wallet. When
the browser wallet isn't the parent's manager but the operator key is, the hint
says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HG2miyE22qTuug1Quxj7iw
…ts key

With no operator key in the shell, the read view reported operator: null, which
read as no operator at all. It now checks the identity's active operator
against the name's control and marks keyAvailable: false.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HG2miyE22qTuug1Quxj7iw
…or ENS delete

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HG2miyE22qTuug1Quxj7iw
@baairon
baairon merged commit dc54610 into master Oct 7, 2026
3 checks passed
baairon added a commit that referenced this pull request Oct 7, 2026
- The heartbeat from #9 read a watcher that never touches its pid file
  as gone. That is every watcher started before #9, including 5.3.2's:
  status deleted its pid file, ensureDaemon started a second watcher
  beside it, and pause could not stop it.
- A pid that is alive but not beating is the daemon when its process
  is an ethagent watcher by its command line (/proc on Linux, ps on
  macOS, PowerShell on Windows). Any other live pid is still treated
  as recycled and is never signalled.
- A starting watcher stops such a watcher and takes the pid file only
  once that process is gone. A beating watcher is left in place.
- The heartbeat makes a watcher exit when sync is paused or another
  live process holds the pid file, and a watcher removes the pid file
  only while it is still its own.
- daemonStatus no longer deletes the pid file, so status is read-only
  again.
baairon added a commit that referenced this pull request Oct 7, 2026
* wip: chain suite on local anvil chains

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HG2miyE22qTuug1Quxj7iw

* fix: transfer runs in the command's wallet tab

The signing step opened a second browser session of its own, the one command
that bypassed its wallet tab. It now takes the command's session when given
one, as create and the custody steps do; the manager still opens its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HG2miyE22qTuug1Quxj7iw

* test: chain suite walks an agent's whole life on local anvil chains

npm run test:chain starts a Mainnet and a Base anvil, places stand-in registry
and ENS contracts at the real addresses, serves IPFS from the runner, and runs
every --yes path in order with a test wallet in place of the browser: create,
profile, ENS link, records and delete, Advanced custody with the operator key,
an operator save, check, restore on a fresh machine and by owner lookup, Simple
custody, and a transfer the receiver restores. Every step is checked against
chain state, and nothing can reach a public host. CI runs it as its own job.

Along the way: ETHAGENT_ENS_RPC_URL for ENS reads and writes, and saves with
ETHAGENT_IPFS_API_URL no longer demand a Pinata JWT they never use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HG2miyE22qTuug1Quxj7iw

* fix(test): Base test chain genesis names every fork and a base fee

CI's Anvil read the bare genesis as pre-London and refused EIP-1559
transactions, so create failed and every later step had no agent. The genesis
now activates every fork through Cancun from block 0 with a base fee, and CI
pins Foundry to the version the suite was run with.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HG2miyE22qTuug1Quxj7iw

* fix: an older or stalled daemon is taken over instead of duplicated

- The heartbeat from #9 read a watcher that never touches its pid file
  as gone. That is every watcher started before #9, including 5.3.2's:
  status deleted its pid file, ensureDaemon started a second watcher
  beside it, and pause could not stop it.
- A pid that is alive but not beating is the daemon when its process
  is an ethagent watcher by its command line (/proc on Linux, ps on
  macOS, PowerShell on Windows). Any other live pid is still treated
  as recycled and is never signalled.
- A starting watcher stops such a watcher and takes the pid file only
  once that process is gone. A beating watcher is left in place.
- The heartbeat makes a watcher exit when sync is paused or another
  live process holds the pid file, and a watcher removes the pid file
  only while it is still its own.
- daemonStatus no longer deletes the pid file, so status is read-only
  again.

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants