Repository navigation
feat(discord-bot): MCP にインデクサーの読み取りツールを追加する - #582
Merged
Merged
Conversation
Goldsky が持っている情報のうち、MCP に出ていたのは ThanksToken アドレス・ クエストモジュール・Open クエストの id とタイトルだけだった。エージェントが 「いま何 THX 持ってる?」「先月誰が誰に送った?」「レビュー待ちは?」に答えられない。 読み取りツールを 3 本 → 7 本にする: - toban_workspace_info … hatId 7 種・モジュール 6 種・開設日を追加 - toban_member_status … THX 残高と送付累計を追加(枠とは別物) - toban_open_quests … status 引数、報酬シェア・説明・承認数・URL を追加 - toban_quest_detail … 新設。提出の試行履歴と承認者 - toban_thx_history … 新設。送付履歴(data を UTF-8 に復元) - toban_workspace_members… 新設。ロールシェアの保有とエスクロー - toban_reward_distributions … 新設。分配の予定と実績 書き込み系(*_propose)は変更なし。確認ボタン経由でしか署名しない不変条件と、 Turnkey のポリシーには一切触れていない。 実装上の判断: - 読み取りクエリは src/mcp/queries.ts に置いた。chain.ts は ABI と書き込み パスの resolver を持ち、turnkey/policy.json と並べて監査できる大きさに 保ちたいため。共有するのは postGraphQL だけ。 - 全クエリが treeId を filter に載せ、全リストが first を取る(上限 100)。 トークンがギルドを、ギルドがワークスペースを固定するのが読み取り側の 唯一の防壁なので、テストで「実際に投げた変数」を検証している。 - 「送受信の両方」は where.or ではなく 2 エイリアスで引く。graph-node の or は 兄弟フィルタと並ぶ形になり、その兄弟がワークスペーススコープなので優先順位を 推測で書きたくない。1 リクエストのままで曖昧さがない。 - 数量は 3 つの互換性のない単位(THX / ロールシェア / ERC-20 の生の値)が 混在する。サブグラフは ERC-20 の decimals を持っていないので換算できない。 フィールド名の接尾辞に加えて、応答自体に units ブロック(項目 → 単位と、 使った単位の注記)を載せた。事故が起きるのは読んだ瞬間であって、 ドキュメントを読む瞬間ではないため。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
`POST /mcp` を `@toban/discord-bot` から `pkgs/extensions/mcp` へ移し、 Discord を確認チャネルのアダプタという位置に下げる設計。 決定事項: - Write はエージェント自身が行わず、確認チャネルのアダプタに依頼するのみ。 またぐのは POST /internal/propose 1 本で、Turnkey 資格情報 / actor / performThx / ConfirmPayload を触るコードは全部 discord-bot に残る - read は他ワークスペースも読めてよい(オンチェーン由来で Goldsky が公開)。 トークンの treeId は境界ではなく既定値 - ただし identity の解決(Discord <-> ウォレット)だけは home に限る。 D1 にあり公開されていないので、越境させると他コミュニティの名簿が作れる - トークンは tbn2.<treeId>.<tokenId>.<mac>。個別失効のため登録簿を持つ - 発行は /$treeId/settings から。Privy のウォレットで認証し、 Worker 側が Hats subgraph で hat 保有を検証する - OAuth はやらない。OpenClaw は多数の代理で動くので per-user OAuth と モデルが合わず、セルフサービス発行に必要な部分は OAuth 無しで足りる - 確認ボタンは残る。認証が代替できるのは「誰が actor か」だけで、 「本人が今この取引を意図した」の証明は代替できない Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Privy の JWT 検証だと Worker に新しい secret と外部 JWKS 依存が増え、 スマートコントラクトウォレットを扱えない。リポジトリには既に pkgs/extensions/identity/src/eip712/ + verify.ts のオフチェーン EIP-712 の 作法があり(ERC-1271 を RPC 経由で通す)、そこに乗せる。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs/mcp-extraction.md の設計どおり、POST /mcp を Discord bot Worker から 独立した Worker へ移す。Discord は「確認チャネルのアダプタ」になり、 MCP を使うために Discord が必須ではなくなった。 - pkgs/extensions/mcp を新設。protocol / tools / queries / auth を移設し、 トークンは tbn2.<treeId>.<tokenId>.<mac> に変更。ステートレス MAC → 登録簿参照の 2 段検証で、個別失効ができるようになった - discord-bot に POST /internal/propose を新設。propose の中身はここに残り、 MCP 側は転送するだけ。ConfirmPayload を触るコード(confirm / button / discord-rest)と Turnkey 署名は discord-bot に残る - read は treeId 引数で越境可(subgraph は公開)。ただし identity の解決 (Discord <-> ウォレット)は home ワークスペースのみ - /$treeId/settings からトークンを発行・一覧・失効できる導線を追加。 認可は EIP-712 署名 + Hats subgraph の hat 保有を Worker 側で検証 実装中に見つけて塞いだもの: - MCP_INTERNAL_PROPOSE_SECRET を追加。service binding だけでは discord-bot の公開 URL への直接 POST を止められないので、これが無いと MCP トークンを持たない第三者が任意チャンネルに確認ボタンを投稿できた - list と revoke が同一の EIP-712 ペイロードを共有し、かつ list は nonce を 消費しない設計だったため、一覧表示用に管理者が作った署名を有効期限内に revoke へ流用できた。署名が操作にも対象にも紐づいていなかったのが原因で、 McpTokenListRequest と McpTokenRevokeRequest(tokenId を署名対象に含む)に 分割。回帰テストを追加してある - フロントが typed-data の envelope を message キーで送っていて、 ハンドラが読む typedData と食い違っていた(本番なら発行も失効も 400) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1. 一覧・失効 UI が死んでいた。フロントが GET /api/mcp-tokens を叩いて いたが、Worker の実装は POST /api/mcp-tokens/list + 署名。405 が返り、 一覧は常に空、失効ボタンは行の中にしか無いので到達不能だった。 署名はページ表示では行わず、明示操作(「署名して一覧を表示」)または 発行直後のウォレットセッション再利用のみ。list は nonce を消費しない ので 30 分キャッシュして使い回す 2. pkgs/openclaw が削除済みの discord-bot/mcp を指したままだった (guilds.example.json / setup.md / render-config.ts / skills/README.md、 加えて openclaw/CLAUDE.md にも 1 箇所) 3. lookupDiscordIds が identity のバッチ上限 200 を超えると 400 を握り潰し、 全員 null を返していた。エージェントはワークスペース全員を「未連携」と 報告する。200 件ずつ分割し、部分失敗は identityLookupDegraded で 「連携なし」と区別できるようにした 4. 発行の nonce をトークン挿入の後に burn していたため、1 つの署名から 複数の有効トークンが作れた。PRIMARY KEY を実際の排他にするため insertToken より前へ移動 5. SKILL.md が「read はトークンのワークスペースに限定される」と書いていたが 越境 read を許す実装と矛盾していた 6. verifyTypedData に渡す types / domain を定数に固定(悪用は不可能だが、 その推論を毎回する必要をなくす) 4 についてはレビュー修正が行き過ぎていたので調整した。署名検証より前に burn すると、このエンドポイントは公開(ブラウザから直接叩く)なので 第三者が署名なしで任意の nonce 行を挿入でき、剪定経路の無いテーブルが 無限に膨らむ。署名検証の後、insertToken の前が正しい位置。 両側の境界を固定する回帰テストを 2 本置いた。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4 観点(reuse / simplification / efficiency / altitude)のレビュー結果。 振る舞いを変える修正は入れていない。 @toban/mcp: - issue.ts が list.ts の認証エンベロープを約 100 行再実装していたので、 共有部分を handlers/auth.ts に出して 3 ハンドラで共有。issue.ts は nonce burn の位置を保つため結合済み関数ではなく部品を直接呼ぶ - json() / errorResponse() の 4 コピーを src/http.ts に統合(1 つだけ 引数順が逆でコピペ事故の温床だった) - 越境 identity のゲートを 5 箇所のコピーから 2 つの choke point へ。 guardDiscordUserIdArg が引数解決の唯一の入口、lookupDiscordIds が 逆引きの唯一の入口で、home 外なら内部で短絡する。identityLookupDegraded も同じ関数が整形するので呼び出し側が伝播を忘れられない - IDENTITY_LOOKUP_CHUNK_SIZE を @toban/identity の MAX_WALLETS_PER_BATCH の import に変更。200 の二重定義をやめ、identity 側の変更が typecheck で落ちるようにした - toban_member_status の Goldsky 3 本を 1 本のエイリアスクエリへ (subrequest 4 → 2、送付提案の直前に必ず通るパス) - verifyWalletTreeAuth / revoke.ts / worker.ts の直列・重複を解消 - orderBy(desc()) 化、treeId ガードの 6 重複を畳む、未使用の注入口を削除 frontend: - handleIssue / handleRevoke を mcp-tokens.ts の純関数へ(テスト 8 本追加)。 route 配下はユニットテストできず、それが list の形不一致を見逃した原因 だったのに、issue / revoke は同じ穴のまま残っていた - 手組みの domain を buildMcpTokenDomain に統一 discord-bot: - postGraphQL を private に戻す(切り出し後は外部利用者がいない) - src/mcp/ → src/confirm/ に改名。MCP サーバーは別パッケージになり、 残っているのは ConfirmPayload を触る 3 ファイルだけ docs: - §8 に、見送った約 240 行の cross-package 重複を決定と理由つきで記録 (chain.ts の resolver 群、IdentityFetchClient の transport、テスト fixtures、resolveChain)。5 つ目の共有パッケージか @toban/identity の subpath 追加が必要で、差分外の既存コードに手が入るため follow-up にした - propose の wire contract が Discord の形をしていることも既知のコストと して記録。2 つ目のアダプタで third-party 依存の改名が必要になる revoke.ts の nonce burn は早期化していない。失効は冪等なので issuance の 「1 署名から複数の有効トークン」に相当する害が無く、早期化すると失敗した 失効が nonce を無駄に消費する副作用だけが増える。実装と食い違っていた CLAUDE.md の記述を直した。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # pnpm-lock.yaml
This branch was successfully deployed
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.
なにをしたか
Goldsky が持っている情報のうち、MCP に出ていたのは ThanksToken アドレス・クエストモジュール・Open クエストの id とタイトルだけでした。エージェントが「いま何 THX 持ってる?」「先月誰が誰に送った?」「レビュー待ちは?」に答えられない状態だったので、読み取りツールを 3 本 → 7 本にします。
toban_workspace_infoindexed: falseを返して落とさないtoban_member_statusthxBalance(受け取った量)、thxSentTotal(送った累計)、roles[]を追加toban_open_questsstatus引数(Open / PendingReview / Completed / Cancelled)とlimit。報酬シェア数・説明・承認数・提出者・各タイムスタンプ・パーマリンクを追加。discordUserIdは任意化toban_quest_detailtoban_thx_historydataを UTF-8 に復元してメッセージも返す。direction/sinceDays/limittoban_workspace_memberstoban_reward_distributions署名まわりは触っていません
書き込み系(
toban_thx_propose/toban_quest_submit_propose)は変更なしです。「エージェントは提案しかできず、署名するのはクリック」「行為者は Discord 署名済み interaction からしか読まない」「performThx/performQuestSubmitがチェーンへの唯一の経路」という不変条件と、turnkey/policy.jsonには一切手を入れていません。追加したのは全部 GraphQL の読み取りです。レビューで見てほしいところ
1. 読み取りクエリを
chain.tsではなく新しいsrc/mcp/queries.tsに置きました。chain.tsは ABI と書き込みパスの resolver を持っていて、turnkey/policy.jsonと並べて監査できる大きさに保ちたいためです。共有するのはpostGraphQLだけ(error handling を 1 箇所に保つため export しました)。2. ワークスペーススコープをテストで担保しています。
全クエリが
treeIdを filter に載せ、全リストがfirstを取ります(上限 100)。トークンがギルドを、ギルドがワークスペースを固定するのが読み取り側の唯一の防壁なので、「実際にインデクサーに投げた変数」を assert するテストにしました(calls[0].variables.whereを検証)。3. 「送受信の両方」は
where.orではなく 2 エイリアスで引いています。graph-node の
orは兄弟フィルタと並ぶ形になり、その兄弟がワークスペーススコープなので優先順位を推測で書きたくありませんでした。1 リクエストのままで曖昧さがありません。4. 単位を応答自体に載せました。 ここが一番議論の余地があると思います。
数量は互換性のない 3 単位が混在します。サブグラフは ERC-20 の
decimalsを持っていないので、rawは原理的に換算できません。thxthxBalanceamountThxなどsharesamountSharessharesrawtotalDepositedRawなどフィールド名の接尾辞に加えて、応答に
unitsブロック(項目 → 単位、および使った単位の注記)を付けています:{ "distributions": [{ "balances": [{ "totalDepositedRaw": "1000000" }] }], "units": { "fields": { "totalDepositedRaw": "raw" }, "notes": { "raw": "ERC-20 の最小単位そのまま。小数桁をインデクサーが持っていないため未換算。…" } } }ドキュメントだけにしなかったのは、事故が起きるのは読んだ瞬間だからです。サードパーティのエージェントが読むのは JSON で、SKILL.md を配れるのは自分たちが運用する OpenClaw だけ。
totalDepositedRaw: "1000000"を「100 万円ぶん」と読むのが一番ありそうな失敗で、USDC なら 1 USDC、18 桁のトークンなら 0.000000000001 と桁が 6 つ違います。notesにはその応答で実際に使われている単位の分だけ入ります(クエスト一覧にthxの説明は付かない)。コンテキストを無駄に積まないためで、テストで担保しています。テスト
68 → 91全通過。新規 23 本は subgraph の fetch をスタブして、以下をカバーしています。mintFromは public なのでdataは任意のバイト列)*Thx/*Shares/*Rawで終わる全キーがunits.fieldsに存在すること(新フィールドの登録漏れ検出)デプロイ
手順に変更はありません。 新しい secret も binding も増えていないので、
deploy:sepolia/deploy:baseをそのまま実行できます。ただし
pkgs/openclaw/skills/toban/SKILL.mdを更新しているので、Fly 側に反映するにはpnpm openclaw push:configが必要です(イメージには焼き込まれていません)。🤖 Generated with Claude Code