Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: CI

on:
push:
branches: [master]
pull_request:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
test:
name: Typecheck and test (Node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [20, 22]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm run typecheck
- run: npm test

contracts:
name: Contracts (forge)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- uses: foundry-rs/foundry-toolchain@v1
- run: forge build --sizes
working-directory: contracts
- run: forge test -vv
working-directory: contracts
39 changes: 32 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ What gets kept:
What you can count on:

- What comes back has the same sha256 as what went in.
- Reading history never changes your vault, and only `fetch` and `status --verify` go online.
- Reading history never changes your vault. `fetch` and `status --verify` go online, and so do `show`, `diff`, and `history --stat` when an operator key is set, to fill in a snapshot that isn't cached yet.
- A rollback previews first, checkpoints before it writes, and never touches the chain.
- `forget` removes local copies only. The encrypted snapshots on IPFS don't change.

Expand Down Expand Up @@ -156,6 +156,12 @@ The foundation is built on open standards, so your agent is never tied to one ap
| Remove leaked content | Edit it out, `ethagent save`, then `ethagent forget <ref> --file <path> --yes` for each version that held it |
| Connect another tool | `ethagent --add "<path to the instructions file it loads every session>"` |
| Find the vault | `ethagent --vault-dir` |
| See the Vault, its build, and who may do what | `ethagent custody`, or `ethagent custody --verify` to simulate each permission |
| Check the agent's ENS name | `ethagent ens` |
| Point the agent at another ENS name | `ethagent ens <name>`, then again with `--yes` |
| Change the name's text records | `ethagent ens --set <key>=<value> --clear <key>`, then again with `--yes` |
| Unlink the ENS name | `ethagent ens --unlink`, then again with `--yes` |
| Sign ENS changes with the operator key | `keychain exec ethagent -- ethagent ens <args> --operator` |

### Commands

Expand All @@ -178,7 +184,13 @@ Run any of these with `npx ethagent`. Commands marked interactive need a termina
| `checkpoint [label] [--json]` | Record the vault as it is right now. | Writes history |
| `rollback <ref> [--yes] [--json]` | Put past bytes back. Previews until `--yes`. | Writes the vault |
| `forget <ref> [--yes] [--json]` | Erase versions from local history. Previews until `--yes`. | Deletes history |
| `--version`, `--help` | Version, or the full command list. Every history command also takes `--help`. | Read-only |
| `custody [--json]` | Custody mode, the Vault and its build, whether it holds the token, the Vault-level owner, and the approved operators. | Goes online |
| `custody --verify [--json]` | Also simulates, sending nothing: the owner withdrawing and changing an operator, the operator rotating the agent URI, and the operator and a stranger being refused. Exits 4 on a mismatch. | Goes online |
| `ens [--json]` | The linked name, its records, the two-way check, the resolver, who controls the name, and whether the operator key could sign for it. | Goes online |
| `ens <name> [--operator] [--yes] [--json]` | Point the agent at a name: create it under a parent the signer controls, write the agent records, clear them on the old name, then publish the name. Previews until `--yes`. | Opens your wallet |
| `ens --unlink [--operator] [--yes] [--json]` | Clear the agent records on the linked name, then publish it unlinked. Previews until `--yes`. | Opens your wallet |
| `ens --set <key>=<value> --clear <key> [--operator] [--yes] [--json]` | Write every record change in one transaction. No save needed. Previews until `--yes`. | Opens your wallet, or none with `--operator` |
| `--version`, `--help` | Version, or the full command list. Every history and onchain command also takes `--help`. | Read-only |

### Saving

Expand All @@ -203,6 +215,17 @@ Run any of these with `npx ethagent`. Commands marked interactive need a termina
| `3` | No working storage credential, no operator key, or the wallet was cancelled or timed out. |
| `4` | Pinned, but the owner still needs to publish it. |

### Onchain identity

`ethagent custody` and `ethagent ens` work without the manager.

- **Previews.** ENS changes preview by default. They list every transaction, simulate the ones that can run now from the signer, and send nothing. Rerun with `--yes` to send.
- **Signers.** ENS transactions are signed in your browser wallet, in one tab for the whole change. With `--operator`, the operator key signs them with no popup instead. Run it as `keychain exec ethagent -- ethagent ens <args> --operator`.
- **What the operator key may do.** It only writes text records and creates subnames under a parent it controls. It never sets `addr` on an existing name and never changes ownership.
- **Publishing.** Changing or unlinking the name always ends with one owner-signed save. A stolen operator key therefore can't point your agent at a name it registered. Local state takes the new name only after that save lands.
- **Control check.** Before anything is sent, the command refuses unless the signer controls the name: as its owner, its NameWrapper owner, or a delegate approved on its resolver.
- **Custody writes.** Withdrawing, depositing, and changing operators stay in the manager.

### History commands

Every history command takes a ref wherever it names a version:
Expand Down Expand Up @@ -365,7 +388,7 @@ A manifest:

### Output and exit codes

With `--json`, every history command prints one line of ASCII-only JSON with `"schema": 1`. Fields are only ever added. A failure looks like this, and the hint names the fix:
With `--json`, every history and onchain command prints one line of ASCII-only JSON with `"schema": 1`. Fields are only ever added. A failure looks like this, and the hint names the fix:

```json
{ "schema": 1, "ok": false, "code": 3, "error": "snapshot bafybei... is not cached locally", "hint": "fetch it first: ..." }
Expand All @@ -376,19 +399,21 @@ With `--json`, every history command prints one line of ASCII-only JSON with `"s
| `0` | Done. `diff` returns 0 whether or not anything differs and reports `identical`. |
| `1` | Failed: unknown ref, missing path, or a store error. |
| `2` | Usage error, or a ref that matches more than one thing. |
| `3` | The snapshot isn't cached here, or there's no key to open it. |
| `4` | Partly done, or `status --verify` found a mismatch. |
| `3` | The snapshot isn't cached here or there's no key to open it, `--operator` ran without an injected operator key, or the wallet approval was cancelled. |
| `4` | Partly done, or `status --verify` or `custody --verify` found a mismatch. |

### Environment variables

| Variable | Effect |
| --- | --- |
| `ETHAGENT_OPERATOR_KEY` | The operator key for `save --operator` and `fetch`. Inject it from your OS keychain; never type it into a command. |
| `ETHAGENT_OPERATOR_KEY` | The operator key for `save --operator`, `fetch`, and `ens --operator`. Inject it from your OS keychain; never type it into a command. |
| `ETHAGENT_RPC_URL` | The RPC endpoint asked first for the agent's network. Use one that keeps history when the public endpoints can't reach back far enough for a wallet search. |
| `ETHAGENT_IPFS_API_URL` | Where snapshots are uploaded, in place of Pinata. |
| `PINATA_JWT` | IPFS storage credential, if you haven't set one up in the manager. |
| `PINATA_GATEWAY_URL` | Your own IPFS gateway. Downloads try it before any other source. |
| `ETHAGENT_IPFS_GATEWAYS` | Gateways to download from, separated by commas, in place of the built-in ones. |
| `ETHAGENT_IPFS_ROUTERS` | Content routers that find more sources for a file, separated by commas. Leave it empty to turn that off. |
| `ETHAGENT_HOSTS_FILE` | Where ethagent remembers how quickly each host answers, so the fastest is asked first. Defaults to `~/.ethagent/hosts.json`. Leave it empty to remember nothing. |
| `ETHAGENT_HOSTS_FILE` | Where ethagent remembers how quickly each host answers, so the fastest is asked first, and how many blocks each serves per log query. Defaults to `~/.ethagent/hosts.json`. Leave it empty to remember nothing. |
| `ETHAGENT_HARNESS_FILES` | Extra instruction files to keep in sync, separated by commas. |
| `ETHAGENT_NO_DAEMON` | Set to `1` to turn off background sync. |

Expand Down
10 changes: 10 additions & 0 deletions commands/ethagent.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Most of these run in a separate terminal because they need a wallet or a TTY, bu
- `npx ethagent --vault-dir` prints this agent's vault directory (where soul, memory, and the `skills/` folder live). Read-only and safe to run yourself.
- `npx ethagent reset` deletes the local identity, vault, history, and saved secrets (the IPFS storage credential is kept), and disconnects every tool. It asks to confirm in a terminal, so it hangs inside a session unless you pass `--yes`. Run it only when the user explicitly asks.
- `npx ethagent status`, `history`, `show`, `diff`, `fetch`, `checkpoint`, `rollback`, and `forget` manage continuity history headlessly. See "Continuity history" below.
- `npx ethagent custody` and `npx ethagent ens` inspect and manage the onchain identity headlessly. See "Onchain identity" below.

To rebuild the agent on a new machine, the user runs `npx ethagent`; it restores the identity from an ENS name or ERC-8004 token id, then asks the wallet to sign.

Expand Down Expand Up @@ -52,6 +53,15 @@ Every save, restore, and refetch also keeps the exact bytes of soul, memory, ski
- `forget <ref> [--file PATH] | --all` erases local plaintext history (leak repair). Run it only when the user asks; it previews by default and needs `--yes`. With `--file`, that exact content is removed from every snapshot and checkpoint and is never cached again. The encrypted copies on IPFS are not affected.
- Exit codes: 0 ok, 1 failure, 2 usage or ambiguous ref, 3 not cached or no key (the JSON `hint` names the fix), 4 partial or inconsistent. `diff` returns 0 either way and reports `identical`.

Onchain identity:

Both commands take `--json` (same `schema: 1` envelope) and `--help`.

- `custody` (read-only, safe anytime) shows the custody mode, the Vault address and its build, whether it holds the agent token, the Vault-level owner, and the approved operators. `custody --verify` also simulates with `eth_call`, sending nothing: the owner withdrawing, the owner changing an operator, the operator rotating the agent URI, and the operator and a stranger being refused. It exits 4 when a result differs from what the Vault should do. Withdrawing, depositing, and changing operators stay in `npx ethagent`.
- `ens` (read-only, safe anytime) shows the linked name, its live records, the two-way check, the resolver, who controls the name, and whether the operator key could sign for it.
- `ens <name>`, `ens --unlink`, and `ens --set <key>=<value> --clear <key>` change ENS. Without `--yes` they only preview: they list every transaction, simulate the ones that can run now, and send nothing (JSON `applied: false`). You may run previews yourself. Anything with `--yes` sends transactions and costs gas, so it stays the user's to run; show them the preview and the command.
- ENS transactions are signed in the browser wallet by default, or by the operator key with `--operator` (run through `keychain exec ethagent -- ethagent ens <args> --operator`; exit 3 without a key, 2 for an invalid one). The operator key only writes text records and creates subnames under a parent it controls. Publishing a name change (`ens <name>`, `ens --unlink`) always ends with one owner-signed save in the browser.

Recipes: "what changed in my memory since a date" is `diff at:YYYY-MM-DD working --file MEMORY.md --sections`; "when did this rule appear" is `history --file MEMORY.md --sections --json`; "undo that memory edit" is `rollback latest --file MEMORY.md`, then `--yes` once the user confirms; "backfill history" is `fetch --all` with a key.

If they ask "what's my agent" or "list my skills" without an identity yet, point them at `npx ethagent` to set one up first.
4 changes: 3 additions & 1 deletion src/cli/harnessGuidance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export function buildPortableInstruction(opts: PortableInstructionOptions): stri
"inside that skill's folder there. Do not hand-edit them (they are regenerated from the vault). To add or change a skill, " +
'put its folder in the vault skills dir (NOT this read-only mirror): run `npx ethagent --vault-dir` to print the vault ' +
"path, then create or edit the `<name>/SKILL.md` folder inside its `skills/` subdir. Past snapshots are kept locally " +
'byte for byte; `npx ethagent status --json` and `npx ethagent diff --json` show what changed.'
'byte for byte; `npx ethagent status --json` and `npx ethagent diff --json` show what changed. ' +
'`npx ethagent custody --json` and `npx ethagent ens --json` show the onchain identity; ENS changes preview ' +
"without `--yes`, and anything with `--yes` is the user's to run."
)
}
15 changes: 15 additions & 0 deletions src/cli/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ensureDaemon } from './daemon.js'
import { runWatch } from './watch.js'
import { runAddTool, runPause, runResume } from './prefs.js'
import { HISTORY_COMMANDS } from './history/commands.js'
import { ONCHAIN_COMMANDS } from './onchain/commands.js'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

Expand Down Expand Up @@ -61,6 +62,16 @@ function printHelp(): void {
' ethagent rollback <ref> restore past bytes (preview, then --yes)',
' ethagent forget <ref> erase local history (preview, then --yes)',
'',
'onchain identity (add --json for machine output, --help for details):',
' ethagent custody [--verify] Vault, build, holder, and operators; --verify simulates permissions',
' ethagent ens the linked ENS name, its records, and who controls it',
' ethagent ens <name> point the agent at a name (preview, then --yes)',
' ethagent ens --unlink clear the agent records and unlink (preview, then --yes)',
' ethagent ens --set <key>=<value> --clear <key>',
' change text records (preview, then --yes)',
' ethagent ens <args> --operator',
' sign the ENS transactions with the operator key (via keychain exec)',
'',
' ethagent --version print version',
' ethagent --help print this help',
]
Expand Down Expand Up @@ -183,6 +194,10 @@ async function main(): Promise<number> {
const { runHistoryCommand } = await import('./history/index.js')
return runHistoryCommand(argv[0], argv.slice(1))
}
if (argv[0] && ONCHAIN_COMMANDS.has(argv[0])) {
const { runOnchainCommand } = await import('./onchain/index.js')
return runOnchainCommand(argv[0], argv.slice(1))
}
if (flags.has('--version') || flags.has('-v')) {
process.stdout.write(`ethagent ${version}\n`)
return 0
Expand Down
3 changes: 3 additions & 0 deletions src/cli/onchain/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Onchain identity commands. They load lazily, next to the history verbs, before
// --help and bootstrap.
export const ONCHAIN_COMMANDS = new Set(['custody', 'ens'])
Loading
Loading