# [ERC8257] AIエージェントが使うツールをオンチェーンで発見・検証するAgent Tool Registryの仕組みを理解しよう！

> ERC8257は、AIエージェントが利用できるツールを登録するオンチェーンレジストリです。登録情報は ToolConfig として保存され、ツールの説明本体はHTTPSで取得できるmanifestに置かれます。

- URL: https://cardene.dev/articles/f398eb8af12a4b28607e
- 公開日: 2026-06-09
- 更新日: 2026-06-09
- トピック: Blockchain
- タグ: blockchain, ethereum, smartcontract, erc

## はじめに

初めまして。
『DApps開発入門』という本や色々記事を書いているかるでねです。

https://amzn.asia/d/gxvJ0Pw

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください！

https://cardene.notion.site/EIP-2a03fa3ea33d43baa9ed82288f98d4a9

https://twitter.com/cardene777

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

今回は「**AIエージェントが使うツールをオンチェーンで発見・検証するAgent Tool Registryを定義するERC8257**」についてまとめていきます。

**ERC8257**は、AIエージェント向けツールをオンチェーンのレジストリへ登録し、ツールのmanifest、アクセス条件、価格ヒント、実行環境の検証情報を発見できるようにする提案です。
ツールの実行や支払いはレジストリが扱わず、ツールを見つけて、正しいmanifestかを検証し、利用できるかを判定するための最小限の土台を定義します。

:::message
以下にまとめられているものを解説しながらまとめていきます。
:::

https://eips.ethereum.org/EIPS/eip-8257

## 概要

**ERC8257**は、AIエージェントが利用できるツールを登録するオンチェーンレジストリです。
登録情報は `ToolConfig` として保存され、ツールの説明本体はHTTPSで取得できるmanifestに置かれます。

レジストリがオンチェーンで持つ情報は、誰が登録したか、manifestをどこから取得するか、そのmanifestの `keccak256` ハッシュ、アクセス判定を任せる外部コントラクトです。
ツールの入出力スキーマ、価格、アクセス条件、実行環境の説明はmanifestに入り、manifest全体の正しさをオンチェーンの `manifestHash` で検証します。

**ERC8257**の責務分担は以下です。

![responsibility.png](/images/qiita-62b45d51-ffbf-4fd8-8995-ce5fdc1831c1.webp)

ツール提供者はmanifestを自分のドメイン配下で公開し、そのURIとハッシュを `ToolRegistry` へ登録します。
エージェントやウォレットは `getToolConfig` でオンチェーン設定を読み、manifestを取得してハッシュ検証とcreator検証を行ったうえで、必要なら `hasAccess` で利用可否を確認します。

この構成では、レジストリは支払いを処理しません。
価格情報はmanifestのヒントとして公開され、実際の支払い確認はツールのendpointや支払いプロトコルが担当します。
そのため、レジストリは「検索と検証の土台」に集中できます。

## 動機

AIエージェント向けツールは、各サービスや各プラットフォームのカタログに分散しがちです。
エージェントが自律的にツールを選ぶには、ツールの場所、入出力、価格、アクセス条件、実行環境を機械的に読める必要があります。

しかし、単なるWebカタログだけでは、どのツール情報が正しいかをオンチェーンの主体と結びつけにくくなります。
そこで **ERC8257** は、manifestのハッシュをオンチェーンへ記録し、ツール情報が改ざんされていないことを確認できるようにします。

#### アクセス制御を外部コントラクトへ分ける理由

ツールごとのアクセス条件は、NFT保有、サブスクリプション、allowlist、DAO投票、評判スコアなどに広がります。
これらをレジストリ本体へ列挙すると、新しい条件を追加するたびにレジストリを変える必要があります。

**ERC8257**は `accessPredicate` という1つのアドレスにアクセス判定を委譲します。
`accessPredicate` が `address(0)` なら誰でも利用でき、外部コントラクトが設定されていれば、そのコントラクトへ `staticcall` して利用可否を判定します。

この「外部コントラクトに判定を任せる」設計は、**ERC4337** のPaymasterや、Uniswap v4のhook、Seaportのzoneに近い考え方です。

:::message
**ERC4337**については以下の記事を参考にしてください。
:::

[\[ERC4337\] プロトコルの変更をせずにAAを実装する仕組みを理解しよう！](/articles/8c97ca7c93c4557ebfc2)

#### 価格情報をmanifestへ置く理由

エージェントが同じ目的のツールを比較する時、実行前に価格を知る必要があります。
**ERC8257**では、価格情報をmanifestの `pricing` 配列に置きます。

ただし、レジストリは資金を扱いません。
`pricing` は「いくら、どの資産で、誰へ、どのプロトコルで払うか」を示す発見用の情報で、支払いの強制はendpointや支払いプロトコル側で行います。

## 仕様

### `ToolConfig`

`ToolConfig` は、登録済みツールのオンチェーン設定です。

```js
struct ToolConfig {
    address creator;
    string metadataURI;
    bytes32 manifestHash;
    address accessPredicate;
}
```

各フィールドの役割は以下です。

| フィールド | 役割 |
|---|---|
| `creator` | ツールを登録したアドレス。登録後も同じ値として扱われます |
| `metadataURI` | Tool Manifest JSONを取得するURI |
| `manifestHash` | JCSで正規化したmanifest bytesの `keccak256` |
| `accessPredicate` | アクセス判定を任せるコントラクト。`address(0)` は公開ツール |

`metadataURI` は空ではない文字列で、2,048 bytes以内である必要があります。
`manifestHash` は `bytes32(0)` 以外である必要があります。
実在するコンテンツの `keccak256` がゼロになる前提は置きにくいため、ゼロハッシュは意味を持たないコミットメントとして拒否されます。

### `IToolRegistry`

`IToolRegistry` は、登録、更新、削除、参照、アクセス判定をまとめたインターフェースです。

```js
interface IToolRegistry {
    event ToolRegistered(
        uint256 indexed toolId,
        address indexed creator,
        address indexed accessPredicate,
        string metadataURI,
        bytes32 manifestHash
    );

    event ToolMetadataUpdated(
        uint256 indexed toolId,
        string newURI,
        bytes32 newHash
    );

    event AccessPredicateUpdated(
        uint256 indexed toolId,
        address indexed newPredicate
    );

    event ToolDeregistered(uint256 indexed toolId);

    error ToolNotFound(uint256 toolId);
    error NotToolCreator(uint256 toolId, address caller);
    error InvalidMetadataURI();
    error InvalidManifestHash();
    error InvalidAccessPredicate(address predicate);
    error ToolIsDeregistered(uint256 toolId);

    function registerTool(
        string calldata metadataURI,
        bytes32 manifestHash,
        address accessPredicate
    ) external returns (uint256 toolId);

    function deregisterTool(uint256 toolId) external;

    function updateToolMetadata(
        uint256 toolId,
        string calldata newURI,
        bytes32 newHash
    ) external;

    function setAccessPredicate(
        uint256 toolId,
        address newPredicate
    ) external;

    function getToolConfig(
        uint256 toolId
    ) external view returns (ToolConfig memory);

    function hasAccess(
        uint256 toolId,
        address account,
        bytes calldata data
    ) external view returns (bool);

    function tryHasAccess(
        uint256 toolId,
        address account,
        bytes calldata data
    ) external view returns (bool ok, bool granted);

    function toolCount() external view returns (uint256);
    function name() external view returns (string memory);
    function version() external view returns (string memory);
}
```

#### `registerTool`

```js
function registerTool(
    string calldata metadataURI,
    bytes32 manifestHash,
    address accessPredicate
) external returns (uint256 toolId);
```

ツールを新規登録する関数です。
呼び出し元が `creator` として記録され、連番の `toolId` が割り当てられます。

- **引数**

| 引数 | 型 | 説明 |
|---|---|---|
| `metadataURI` | `string` | Tool Manifest JSONを取得するURI |
| `manifestHash` | `bytes32` | JCSで正規化したmanifest bytesの `keccak256` |
| `accessPredicate` | `address` | アクセス判定コントラクト。公開ツールなら `address(0)` |

- **戻り値**

| 戻り値 | 型 | 説明 |
|---|---|---|
| `toolId` | `uint256` | 登録されたツールID |

登録時には `metadataURI` と `manifestHash` の制約を確認し、`accessPredicate` も検証します。
`accessPredicate` がERC165に対応している場合は、`IAccessPredicate` をサポートしている必要があります。
ERC165に対応していないコントラクトや、まだコードが存在しないアドレスはbest-effortとして受け入れられますが、実行時のアクセス判定で失敗すればアクセス拒否として扱われます。

#### `deregisterTool`

```js
function deregisterTool(uint256 toolId) external;
```

ツールを恒久的に削除する関数です。
呼び出せるのは `creator` だけです。

削除された `toolId` は再利用されません。
削除後に `getToolConfig`、`hasAccess`、`tryHasAccess`、`updateToolMetadata`、`setAccessPredicate`、`deregisterTool` を呼ぶと `ToolIsDeregistered` でrevertします。

一時停止したい場合は、削除ではなく常に拒否するpredicateへ `setAccessPredicate` で切り替える想定です。
削除は戻せない操作として扱われます。

#### `updateToolMetadata`

```js
function updateToolMetadata(
    uint256 toolId,
    string calldata newURI,
    bytes32 newHash
) external;
```

manifestのURIとハッシュを同時に更新する関数です。
呼び出せるのは `creator` だけです。

- **引数**

| 引数 | 型 | 説明 |
|---|---|---|
| `toolId` | `uint256` | 更新するツールID |
| `newURI` | `string` | 新しいmanifest URI |
| `newHash` | `bytes32` | 新しいmanifestのJCS正規化後ハッシュ |

manifestの入出力スキーマ、価格、アクセスヒント、検証情報を変えた時は、新しいmanifestを公開し、そのJCS正規化後ハッシュをオンチェーンへ反映します。
URIとハッシュのどちらかが変われば `ToolMetadataUpdated` イベントが発行されます。

#### `setAccessPredicate`

```js
function setAccessPredicate(
    uint256 toolId,
    address newPredicate
) external;
```

アクセス判定コントラクトを更新する関数です。
呼び出せるのは `creator` だけです。

- **引数**

| 引数 | 型 | 説明 |
|---|---|---|
| `toolId` | `uint256` | 更新するツールID |
| `newPredicate` | `address` | 新しいアクセス判定コントラクト。公開ツールなら `address(0)` |

新しいpredicateへ変える時は、登録時と同じ検証ルールが適用されます。
同じpredicateを設定する呼び出しは状態が変わらないため、実装は検証やイベント発行を省略できます。

#### `getToolConfig`

```js
function getToolConfig(
    uint256 toolId
) external view returns (ToolConfig memory);
```

登録済みツールの設定を返す参照関数です。

- **引数**

| 引数 | 型 | 説明 |
|---|---|---|
| `toolId` | `uint256` | 参照するツールID |

- **戻り値**

| 戻り値 | 型 | 説明 |
|---|---|---|
| `ToolConfig` | `ToolConfig` | 登録済みツールのオンチェーン設定 |

未登録のIDなら `ToolNotFound`、削除済みなら `ToolIsDeregistered` でrevertします。
未登録、削除済み、アクセス不可は別々の状態として扱われます。

#### `hasAccess`

```js
function hasAccess(
    uint256 toolId,
    address account,
    bytes calldata data
) external view returns (bool);
```

指定したアカウントがツールへアクセスできるかを返す参照関数です。

- **引数**

| 引数 | 型 | 説明 |
|---|---|---|
| `toolId` | `uint256` | 判定するツールID |
| `account` | `address` | アクセス可否を確認するアカウント |
| `data` | `bytes` | predicateへ渡す文脈データ |

- **戻り値**

| 戻り値 | 型 | 説明 |
|---|---|---|
| `bool` | `bool` | アクセスできるなら `true` |

`accessPredicate` が `address(0)` なら、predicateを呼ばずに `true` を返します。
predicateが設定されている場合は `staticcall` で呼び出し、正しいABIエンコードの `true` だけをアクセス許可として扱います。

predicateのrevert、ガス不足、戻り値の長さ不一致、`0` と `1` 以外のbool値、コードがないpredicateは、すべて `false` になります。
安全側へ倒すため、壊れたpredicateでアクセスが許可されることはありません。

#### `tryHasAccess`

```js
function tryHasAccess(
    uint256 toolId,
    address account,
    bytes calldata data
) external view returns (bool ok, bool granted);
```

アクセス可否に加えて、predicate呼び出しが正常に解釈できたかを返す参照関数です。

- **引数**

| 引数 | 型 | 説明 |
|---|---|---|
| `toolId` | `uint256` | 判定するツールID |
| `account` | `address` | アクセス可否を確認するアカウント |
| `data` | `bytes` | predicateへ渡す文脈データ |

- **戻り値**

| 戻り値 | 型 | 説明 |
|---|---|---|
| `ok` | `bool` | predicate呼び出しを正しく解釈できたか |
| `granted` | `bool` | アクセスが許可されたか |

戻り値の意味は以下です。

| 戻り値 | 意味 |
|---|---|
| `(true, true)` | 公開ツール、またはpredicateが正しい `true` を返した |
| `(true, false)` | predicateが正しい `false` を返した |
| `(false, false)` | predicate呼び出しが失敗、または戻り値を解釈できない |

`(false, true)` は無効な組み合わせとして扱う必要があります。
ウォレットやエージェントは、`tryHasAccess` を使うことで「条件を満たしていない」のか「predicateが壊れている」のかを分けて表示できます。

アクセス判定の流れは以下です。

![access-predicate.png](/images/qiita-246bfc94-2170-4d17-a1ff-11cbe05aef24.webp)

`hasAccess` は単純なboolを返す便利な関数です。
一方で、`tryHasAccess` はpredicateの正常応答とアクセス可否を分けるため、UIやエージェントが原因を表示しやすくなります。

#### `toolCount`

```js
function toolCount() external view returns (uint256);
```

登録されたツールIDの最大値を返す参照関数です。

- **戻り値**

| 戻り値 | 型 | 説明 |
|---|---|---|
| `uint256` | `uint256` | これまでに割り当てられた最大 `toolId` |

`toolId` は1から始まる連番です。
削除済みIDは再利用されないため、`toolCount()` は現在利用可能な件数ではなく、割り当て済みIDの上限を表します。

#### `name()` / `version()`
```js
function name() external view returns (string memory);
function version() external view returns (string memory);
```

レジストリ実装を識別するための参照関数です。

どちらも空文字列以外を返す必要があります。
参照実装では `name()` が `"ToolRegistry"` を返し、`version()` は `"0.1"` や `"1.0"` のような文字列を返します。
この値は厳密なsemverではなく、デプロイ済みレジストリの粗い識別子として使われます。

### `IAccessPredicate`

`IAccessPredicate` は、アクセス判定を外部化するためのインターフェースです。

```js
struct AccessRequirement {
    bytes4 kind;
    bytes data;
    string label;
}

enum RequirementLogic {
    AND,
    OR
}

interface IAccessPredicate {
    function hasAccess(
        uint256 toolId,
        address account,
        bytes calldata data
    ) external view returns (bool);

    function name() external view returns (string memory);

    function getRequirements(
        uint256 toolId
    )
        external
        view
        returns (
            AccessRequirement[] memory requirements,
            RequirementLogic logic
        );
}
```

#### `hasAccess`

```js
function hasAccess(
    uint256 toolId,
    address account,
    bytes calldata data
) external view returns (bool);
```

predicateが、指定された `account` が `toolId` のツールを利用できるかを返す関数です。

- **引数**

| 引数 | 型 | 説明 |
|---|---|---|
| `toolId` | `uint256` | 判定対象のツールID |
| `account` | `address` | 利用可否を確認するアカウント |
| `data` | `bytes` | predicate実装へ渡す文脈データ |

- **戻り値**

| 戻り値 | 型 | 説明 |
|---|---|---|
| `bool` | `bool` | 利用できるなら `true` |

`ToolRegistry` はこの関数を `staticcall` で呼び出します。
戻り値は、正しいboolとして解釈できる必要があります。
解釈に失敗した場合、アクセスは拒否として扱われます。

#### `name`

```js
function name() external view returns (string memory);
```

predicate実装の表示名を返す関数です。

- **戻り値**

| 戻り値 | 型 | 説明 |
|---|---|---|
| `string` | `string` | predicate実装の表示名 |

ウォレットやエージェントは、この値をアクセス条件の説明に使えます。

#### `getRequirements`

```js
function getRequirements(
    uint256 toolId
)
    external
    view
    returns (
        AccessRequirement[] memory requirements,
        RequirementLogic logic
    );
```

エージェントが事前に必要条件を把握するための機械可読なヒントを返す関数です。

- **引数**

| 引数 | 型 | 説明 |
|---|---|---|
| `toolId` | `uint256` | 条件を確認するツールID |

- **戻り値**

| 戻り値 | 型 | 説明 |
|---|---|---|
| `requirements` | `AccessRequirement[]` | 必要条件の一覧 |
| `logic` | `RequirementLogic` | 条件をANDで見るかORで見るか |


`AccessRequirement.kind` はERC165風の4 bytes識別子です。
既知の種類は以下です。

| 種類 | `kind` | `data` |
|---|---|---|
| `IERC721Holding` | `0xbdf8c428` | `abi.encode(address collection)` |
| `IERC1155Holding` | `0xcb429230` | `abi.encode(address collection, uint256 tokenId)` |
| `ISubscription` | `0x44387cc2` | `abi.encode(address collection, uint8 minTier)` |

**ERC165**は、コントラクトが特定のインターフェースに対応しているかを確認するための規格です。
**ERC8257**では、`IToolRegistry` と `IAccessPredicate` のinterface IDも固定されています。

:::message
**ERC165**については以下の記事を参考にしてください。
:::

[\[ERC165\] コントラクトが特定のインターフェースをサポートしているか確認する仕組みを理解しよう！](/articles/541184977a5cba51b759)

| インターフェース | interface ID |
|---|---|
| `IToolRegistry` | `0xf1dc8075` |
| `IAccessPredicate` | `0xbdf9dc18` |

`getRequirements` は発見と準備のためのヒントです。
最終的なアクセス可否は `hasAccess` や `tryHasAccess` で確認します。
predicateの設定が変わると、manifestの `access` ヒントとオンチェーンpredicateの状態がずれることがあるためです。

### ツールIDの範囲

`toolId` は、単体ではグローバルな識別子ではありません。
同じ `toolId` が、別チェーンや別レジストリで異なるツールを指すことがあります。

そのため、ツール参照は `chainId` と `registryAddress` と `toolId` を合わせて扱います。
推奨される文字列表現は以下です。

```txt
eip155:<chainId>/erc8257:<registryAddress>/<toolId>
```

`erc8257` のCAIP namespaceは、提案時点ではまだCAIP側へ登録されていません。
ただし、将来の登録を見越して、相互運用しやすい表記としてこの文字列が推奨されています。

### Tool Manifest

`metadataURI` が指すmanifestはJSONです。
ツールの名前、説明、endpoint、入出力スキーマ、creatorアドレス、価格、アクセスヒント、実行環境の検証情報を含みます。

manifestに載る発見情報は以下です。

![manifest-discovery.png](/images/qiita-a56cd6c8-e2ab-4462-9a50-ae4f978f5d39.webp)

エージェントはmanifestを読むことで、ツールの目的、呼び出し方、価格、アクセス条件、信頼判断に必要な情報を実行前に確認できます。
レジストリはこれらの詳細をオンチェーンへ直接保存せず、manifestのハッシュを保存して改ざん検知に使います。

#### manifestの正規化bytes

`manifestHash` は、JSONをそのままハッシュした値ではありません。
**RFC8785** のJSON Canonicalization Scheme（JCS）で正規化したbytesに対する `keccak256` です。

JCSは、オブジェクトキー順、空白、数値表現、文字列エスケープを揃えます。
そのうえで **ERC8257** は、JCSの前に以下のルールも求めます。

| ルール | 内容 |
|---|---|
| Unicode | すべてのJSON文字列はNFCである必要があります |
| BOM | UTF-8 BOMを含まないbytesである必要があります |
| hex | manifest内のhex文字列は小文字である必要があります |

これらを満たさないmanifestは検証失敗です。
コンシューマは、勝手にNFCへ直したり、BOMを剥がしたり、hexを小文字化したりせず、失敗として扱います。
黙った補正をすると、オンチェーンにコミットされたbytesと別のbytesを検証したことになるためです。

#### 必須フィールド

manifestの主な必須フィールドは以下です。

| フィールド | 型 | 説明 |
|---|---|---|
| `type` | `string` | schema version identifier |
| `name` | `string` | ツール名。1から128 Unicode code points |
| `description` | `string` | ツール説明。1から500 Unicode code points |
| `endpoint` | `string` | ツールがホストされるHTTPS URL |
| `inputs` | `object` | 入力パラメータのJSON Schema |
| `outputs` | `object` | 出力パラメータのJSON Schema |
| `creatorAddress` | `string` | このmanifestを登録できるオンチェーンアドレス |

`endpoint` は `https://` で始まるURLである必要があります。
`creatorAddress` は `^0x[0-9a-f]{40}$` に一致する小文字hexアドレスで、オンチェーンの `creator` と一致する必要があります。

#### 任意フィールド

manifestの主な任意フィールドは以下です。

| フィールド | 役割 |
|---|---|
| `version` | ツールのsemver表記 |
| `image` | ツールアイコンURL |
| `tags` | 発見用タグ |
| `pricing` | 支払い方法の候補 |
| `access` | 事前確認用のアクセス条件 |
| `verifiability` | 実行環境やデータ保持の自己申告と検証情報 |

定義されていないトップレベルフィールドは無視します。
将来拡張のため、拡張フィールドにはreverse DNS表記の名前空間を付けることが推奨されています。

### 価格情報

`pricing` は、ツール実行前にエージェントが価格を知るための配列です。
レジストリは支払いを処理せず、支払いの検証も行いません。

`pricing` の各要素は以下です。

| フィールド | 必須 | 説明 |
|---|---|---|
| `amount` | はい | 最小単位での金額。10進文字列 |
| `asset` | はい | CAIP-19表記の資産識別子 |
| `recipient` | はい | CAIP-10表記の受取先 |
| `protocol` | はい | 支払いプロトコル識別子 |

`asset` と `recipient` は同じチェーンを参照する必要があります。
`pricing` がある場合は、1件以上の配列である必要があります。
`null` は拒否されます。
配列の順序はcreatorの優先順として扱われ、エージェントは対応できる最初の `protocol` を選びます。

`amount` が `"0"` の場合、ウォレットの支出制御では無料として扱います。
ただし、endpointが特定プロトコルの交渉を期待していることを示せるため、`pricing` が無い無料ツールとは意味が少し違います。

### アクセス条件

manifestの `access` は、predicateの条件を事前に読むためのヒントです。
オンチェーンの `IAccessPredicate.getRequirements` と役割が近いですが、最終的な判定はpredicateの `hasAccess` が担います。

`access` の主なフィールドは以下です。

| フィールド | 説明 |
|---|---|
| `logic` | `"AND"` または `"OR"` |
| `requirements` | 条件オブジェクトの配列 |

各条件オブジェクトは以下です。

| フィールド | 説明 |
|---|---|
| `kind` | 4 bytes selector。小文字hex |
| `data` | ABIエンコードされたpayload。小文字hex |
| `label` | 表示用の短い説明 |
| `links` | 関連URLのmap。値はHTTPS URLのみ |

`access` はmanifestに含まれるため、`manifestHash` によってオンチェーンへコミットされます。
ただし、predicateのオンチェーン設定がmanifest更新なしに変わることがあります。
そのため、`access` は準備用のヒントであり、実際の利用直前には `hasAccess` か `tryHasAccess` で確認します。

### 検証可能性

`verifiability` は、ツールの実行環境やデータの扱いについて、エージェントが事前に信頼判断するための情報です。
これもオンチェーンの `ToolConfig` ではなくmanifestに入ります。

主なフィールドは以下です。

| フィールド | 役割 |
|---|---|
| `tier` | 信頼段階の要約 |
| `execution` | 実行環境 |
| `description` | 説明 |
| `dataRetention` | データ保持方針 |
| `sourceVisibility` | ソースコードの公開状況 |
| `attestation` | TEEやE2EE向けの検証情報 |
| `reproducibleBuild` | 再現可能ビルドの情報 |

`tier` の値は以下です。

| 値 | 意味 |
|---|---|
| `self-attested` | すべて自己申告。ハードウェア隔離や再現可能ビルドはありません |
| `hardware-attested` | TEEのattestationを検証できます |
| `verifiable` | TEE attestationと再現可能ビルドを組み合わせて検証できます |

`execution` の値は以下です。

| 値 | 意味 |
|---|---|
| `standard` | 通常のサーバー実行 |
| `tee` | Trusted Execution Environment上で実行 |
| `e2ee` | 入力を端末側で暗号化し、検証済みTEE内で復号 |

`tier` は自己申告です。
エージェントは `tier` だけで高い信頼を与えず、`attestation` や `reproducibleBuild` の構造がその主張を支えているかを確認します。

例えば、`tier` が `verifiable` なのに `attestation` や `reproducibleBuild` が無いmanifestは一貫していません。
indexerや発見レイヤーは、一貫しないmanifestをフラグし、実際に確認できる低い信頼段階として扱うことが推奨されています。

### originとの結びつき

manifestは、ツールの `endpoint` と同じoriginのwell-known pathで取得できる必要があります。

```txt
<origin>/.well-known/ai-tool/<slug>.json
```

`slug` は以下に一致します。

```txt
[a-z0-9]([a-z0-9-]*[a-z0-9])?
```

長さは1から64文字です。
`metadataURI` はクエリ文字列やfragmentを含まないURIである必要があります。
`endpoint` はpathやqueryを含められますが、origin比較ではscheme、host、portだけを使います。

このルールにより、endpointのoriginを管理している主体だけが、そのendpointに対応するmanifestを配置できます。
攻撃者が他人のAPI endpointを勝手に登録しても、同じoriginのwell-known pathへmanifestを配置できる主体ではないため、検証で落とせます。

### creatorとの結びつき

Origin Bindingは、manifestがendpointのoriginから出ていることを確認します。
しかし、`registerTool` はpermissionlessなので、別のアドレスが正しいmanifest URLを読み取り、自分の `creator` として登録する攻撃が残ります。

そこでmanifestには `creatorAddress` を入れます。
コンシューマは、manifest内の `creatorAddress` とオンチェーンの `ToolConfig.creator` が一致するかを確認します。

```json
{
  "creatorAddress": "0xabcdefabcdef1234567890abcdefabcdef123456"
}
```

`creatorAddress` は小文字hexの20 bytesアドレスで、ゼロアドレス以外である必要があります。
ゼロアドレスは `msg.sender` になれないため、正当な登録者として一致しないからです。

### コンシューマ検証

エージェント、ウォレット、indexerは、ツールを解決する時に以下の順で検証します。

![manifest-verification.png](/images/qiita-1df36e02-609f-4fe3-8398-477e9a6d28b2.webp)

1. `metadataURI` からmanifestを取得する
    オンチェーンの `ToolConfig.metadataURI` を使ってmanifestを取得します。
    HTTPリダイレクトは追わず、3xxは検証失敗として扱います。
2. `metadataURI` が `endpoint` と同じoriginのwell-known pathにあることを確認する
    URLを正規化し、scheme、host、port、path、slugを確認します。
    `metadataURI` はqueryやfragmentを含まない必要があります。
3. JCSで正規化し、`keccak256` が `manifestHash` と一致することを確認する
    UTF-8 BOMなし、NFC文字列、小文字hexの条件を満たす必要があります。
    ハッシュが合わない時は、更新トランザクションとの競合を考慮して `getToolConfig` を1回だけ読み直せます。
4. `creatorAddress` がオンチェーンの `creator` と一致することを確認する
    一致しなければ、正しいoriginのmanifestを別アドレスが登録したものとして拒否します。

4つの検証をすべて通ったツールだけがcanonicalな登録として扱われます。
エージェントは、canonicalな登録だけをユーザーに提示し、実行対象として扱う必要があります。

## 補足

#### hashによるコミットメント

`manifestHash` は、mutableな `metadataURI` に対するオンチェーンのコミットメントです。
manifestの内容が変わる場合は、`updateToolMetadata` で新しいURIとハッシュを登録し、`ToolMetadataUpdated` イベントを発行します。

indexerはイベントを追うことでmanifestの変化を検出できます。
特定の `manifestHash` をpinしているコンシューマは、新しいmanifestを明示的に再承認するまで古いコミットメントに留まれます。

#### predicateの検証

`IToolRegistry.hasAccess(uint256,address,bytes)` と `IAccessPredicate.hasAccess(uint256,address,bytes)` は同じselectorを持ちます。
そのため、同名同引数の関数を持つ別コントラクトを誤ってpredicateに設定すると、意図しないアクセス判定が起きます。

**ERC8257**は、ERC165に対応しているpredicateについて `IAccessPredicate` 対応を確認します。
`supportsInterface(type(IERC165).interfaceId)` が `true` のコントラクトが `IAccessPredicate` をサポートしていない場合、登録や更新は `InvalidAccessPredicate` でrevertします。

一方で、ERC165に対応していないコントラクトはbest-effortで受け入れます。
この場合の最後の防御は、`staticcall` と厳密なbool decodeです。
正しく `true` を返せないpredicateはアクセス拒否として扱われます。

#### EIP712によるAccessProof

`account` 引数は、呼び出し元が「このアカウントについて確認したい」と申告する値です。
レジストリは `account` と `msg.sender` を結びつけません。
そのため、`hasAccess` が `true` を返しても、現在のリクエスト主体が本当に `account` を操作できるとは限りません。

この問題に対して、提案では **EIP712** のtyped dataを使った `AccessProof` パターンを示しています。
`toolId`、`account`、`deadline` を含むメッセージに署名し、predicateまたはendpointが署名者を検証します。

:::message
**EIP712**については以下の記事を参考にしてください。
:::

[\[EIP712\] 型付き構造化データのハッシュ化と署名の仕組みを理解しよう！](/articles/f54bc85cb66de5a54257)

`deadline` を入れることで再利用期間を制限し、`chainId` と `verifyingContract` をdomainに入れることで、別チェーンや別predicateへの再利用を防ぎます。

## 互換性

**ERC8257**は新しい `IToolRegistry` と `IAccessPredicate` を追加する提案で、既存のERCを変更しません。
インターフェース検出にはERC165を使いますが、ERC165自体の変更は不要です。

predicateは独立したコントラクトです。
既存のコントラクトでも、`IAccessPredicate` の関数シグネチャと挙動に合っていればpredicateとして使えます。

## 参考実装

Foundryベースの参照実装が、この提案と同じリポジトリで管理されています。
参照実装は `ToolRegistry`、`IToolRegistry`、`IAccessPredicate` を含み、ERC165 interface ID、manifest URI長制限、predicate連携、戻り値の厳密decode、ゼロコードpredicateの扱いをテストで固定しています。

実装者は、`IToolRegistry` のfunction setを変えてinterface IDが変わらないようにする必要があります。
function setを変える場合、それは別インターフェースとして新しい識別子を持つべきです。

## セキュリティ

#### predicateのガスと失敗時の扱い

レジストリはpredicateを `staticcall` で呼び出します。
predicateがrevertしたり、ガスを使い切ったり、不正な戻り値を返したりした場合、`hasAccess` は `false` を返し、`tryHasAccess` は `(false, false)` を返します。

predicate呼び出しの最大ガス値は、提案では固定しません。
SLOADや署名検証などのコストはハードフォークやチェーンごとの料金体系で変わるためです。
オンチェーンで `hasAccess` を組み込むコントラクトは、`staticcall{gas: budget}` のように明示的な予算を与え、外部predicateに払う最悪コストを制限することが推奨されています。

#### upgrade可能なpredicate

`accessPredicate` がERC1967 proxyやUUPS proxyの場合、predicateの管理者は `setAccessPredicate` を呼ばずにアクセスロジックを変えられます。
そのため、アクセス条件の変更を追跡するには、`AccessPredicateUpdated` イベントだけでなく、predicate側の状態やイベントも確認する必要があります。

コンシューマやindexerは、predicateアドレスにproxyパターンがないかを確認し、proxyなら高リスクとして扱うことが推奨されています。
ERC1967の `Upgraded(address)` イベントを監視し、predicateの実装変更をユーザーに見せることも重要です。

#### registryのデプロイ

レジストリ自体がproxyでデプロイされている場合、proxy管理者は実装を差し替えて、`hasAccess`、`getToolConfig`、`toolCount` の挙動を変えられます。
canonicalなレジストリとして扱うアドレスでは、bytecode hashやデプロイトランザクションをpinすることが推奨されています。

discovery layerは、レジストリアドレスだけでなく、そのbytecode hashも公開することで、下流のウォレットやエージェントが同じ実装を見ているか確認できます。

#### 読み取り専用のreentrancy

predicate呼び出しは `staticcall` なので、predicateは状態を書き換えられません。
ただし、predicateが他のコントラクトの状態を読むことはできます。

外部プロトコルの状態が途中まで更新されたタイミングで `hasAccess` を呼ぶと、predicateが一貫しない状態を読んでしまうことがあります。
`hasAccess` の結果で状態変更を制御するコントラクトは、外部view呼び出しとしてのread-only reentrancyを考慮する必要があります。

同じトランザクション内で同じ `(toolId, account)` の判定を複数回使う場合は、EIP1153の一時ストレージに最初の結果をキャッシュできます。
これはレジストリの挙動を変えるものではなく、組み込み側のコスト削減策です。

#### `data` に秘密情報を入れない

`hasAccess` と `tryHasAccess` の `data` は、predicateへそのまま渡されます。
これはオンチェーンview呼び出しの入力なので、RPC trace、ノードログ、監視基盤から見えることがあります。

`data` には、公開してよい文脈データだけを入れる必要があります。
秘密鍵、API token、パスワード、session cookie、bearer tokenのような秘密情報は、`data` ではなく別の安全な手段で扱います。
正当な用途は、Merkle proof、token ID、EIP712署名、公開challengeへの署名など、公開されても問題ない値です。

秘密情報が必要なアクセス方式では、オンチェーンpredicateへ渡さず、HTTPS endpoint側で検証します。

#### コードがないpredicate

登録時や更新時に、まだコードが存在しないpredicateアドレスは受け入れられます。
CREATE2で後からデプロイする設計を許すためです。

ただし、実行時にゼロコードアドレスへ `staticcall` した場合、戻り値を正しいboolとして解釈できる必要があります。
解釈に失敗した時はアクセス拒否になります。
counterfactual deploymentを使うcreatorは、CREATE2 saltとinit-code hashが意図したデプロイを指していることを確認し、利用者へ公開する前にpredicateをデプロイすることが推奨されています。

## 引用

https://eips.ethereum.org/EIPS/eip-8257

https://raw.githubusercontent.com/ethereum/ERCs/master/ERCS/erc-8257.md

## 最後に

今回は「**AIエージェントが使うツールをオンチェーンで発見・検証するAgent Tool Registryを定義するERC8257**」についてまとめてきました。

ERC8257は、AIエージェント向けツールを「オンチェーンで登録し、オフチェーンmanifestで詳しく説明し、hashとoriginとcreatorで検証する」ための提案です。
レジストリ本体は最小限に保ち、アクセス条件はpredicate、価格や実行環境の説明はmanifestへ分けることで、新しいツールや支払い方式を追加しやすくしています。

他でも色々記事を書いているのでぜひよろしければ読んでいってください！

https://amzn.asia/d/gxvJ0Pw

https://cardene.notion.site/EIP-2a03fa3ea33d43baa9ed82288f98d4a9

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://twitter.com/cardene777

https://cardene.substack.com/
