Agent Skillsの正典を公開リポジトリへ移し、index.json更新を自動化する

AIエージェント向けポータルのAgent Skillsは、workerが固定で埋め込んでいた文字列だった。これをAgent Plugins 1.0.0準拠の公開リポジトリへ切り出し、正典を移す。SKILL.mdを1本足してタグを切れば、配信面と /.well-known/agent-skills/index.json が自動で更新される仕組みを記録する。

自宅サーバーで運用しているAIエージェント向けニュースポータルには、エージェントに使い方を教えるAgent Skillsが9本ある。これまでその本文は、ポータルのworkerコンテナが固定で埋め込んでいた静的文字列だった。workerがそれをファイルとして書き出し、nginxが /skills/<name>.md として配信し、/.well-known/agent-skills/index.json がそれを索引する、という流れである。

この置き場所を Agent Plugins 1.0.0 準拠の公開GitHubリポジトリ portal4agents-plugin へ移した。狙いは単純で、Skillの正典(source of truth)をGitへ一本化し、リポジトリにSKILL.mdを1本足してタグを切るだけで、ポータルの配信面とindex.jsonが自動的に追随する状態にすることだ。2026-08-24時点で v0.2.1 が反映され、index.jsonには9件のSkillがsha256ダイジェスト付きで並んでいる。

背景:workerはSkillの置き場所として間違っていた

Agent Skillsは、SKILL.mdというMarkdownファイルを含むフォルダを1スキルとする軽量なオープン形式である。エージェントはタスクの内容を見て、使えそうなSkillのSKILL.md本文をそのままコンテキストへ読み込む。

問題は、その本文がworkerに固定で埋め込まれていたことだった。workerは要約生成・翻訳・グラフ照会といった不確実で遅い処理を担うコンテナで、そこにLLMもデータ補間も一切関与しない静的テキストが同居していた。workerは「生成」しておらず、静的コンテンツを不適切な場所から吐き出していただけである。

実害もあった。1文字直すたびにworkerイメージの再ビルドが要り、変更履歴はプライベートリポジトリに埋もれ、リリース単位という概念が存在しない。エージェントの手元に入るのはSkill本文なのに、そのバージョンを外から指し示す方法がなかった。リポジトリ化は方式変更ではなく、本来の姿への是正である。

Agent Plugins 1.0.0が固定したもの

Agent Plugins 1.0.0は、Agent SkillsとMCPサーバーをまとめて配布するためのベンダー中立なパッケージ形式である。仕様が固定しているのは配置とマニフェストで、以下の形から動かせない。

<plugin-root>/
├── plugin.json          # 必須
├── skills/              # 各直下ディレクトリの SKILL.md が1スキル
└── mcp.json             # MCPサーバー設定

plugin.json から配置を上書きすることも、コンポーネントをインライン宣言することもできない。マニフェストの必須項目は $schema と name の2つだけで、最上位に許可されるフィールドは10個に閉じている。未知のフィールドはクライアントが報告したうえで無視し、ロード自体は継続する。

失敗境界がコンポーネント単位でスコープされる点も実運用で効く。壊れたMCPサーバー項目はそのサーバーだけ、不正なSkillはそのSkillだけがスキップされ、1本の記述ミスがプラグイン全体を落とさない。一方でインストール・配布・権限・資格情報の保管は仕様が定義せず、クライアント側の管理に委ねられている。

リポジトリの中身

公開したのはSkill 9本とMCPエンドポイント情報だけで、ポータル本体はプライベートのままである。

portal4agents-plugin/
├── plugin.json
├── mcp.json
└── skills/
    ├── news-triage/SKILL.md
    ├── daily-tech-brief/SKILL.md
    ├── security-watch/SKILL.md
    ├── agent-readiness-compare/SKILL.md
    ├── company-news-monitor/SKILL.md
    ├── government-policy-watch/SKILL.md
    ├── citation-pack-builder/SKILL.md
    ├── vendor-risk-brief/SKILL.md
    └── portal-related-research/SKILL.md

mcp.json はポータルの読み取り専用MCPエンドポイント(streamable HTTP)を1つ宣言するだけで、資格情報も環境固有のヘッダーも埋め込まない。

書き方の方針として、SKILL.mdにはほぼ不変のものだけを残した。発動条件、「まず一覧で絞ってから必要なものだけ取得する」という原則、レコードのライセンス表示を守るという約束などである。逆に、ツールの引数リストやカテゴリ一覧、フィールド名といった可変情報は書かない。それらはMCPサーバーが initialize のinstructionsと各ツールのdescriptionとして実行時に配る。ポータルを更新するたびにSKILL.mdが腐る、という二重管理をここで断ち切っている。

却下した案:リモート参照スタブ

移行前に検討して捨てた案がある。SKILL.mdには名前だけ書き、中身はポータルの /skills/*.md を実行時に取りに行かせる、というものだ。

これは仕様上まず成立しない。Agent Plugins 1.0.0は、プラグイン由来のファイルを読むとき解決後のパスがプラグインルート内に留まることを要求し、ルート外へ解決されるパスはクライアントが拒否しなければならないと定めている。リモートURL参照の仕組みは1.0.0に存在しない。

仮に本文へURLを書いて実行時にfetchさせても、実務で破綻する。Agent Skillsは段階的開示の形式で、発動時にSKILL.md本文がそのままコンテキストへ入る。中身が「取りに行け」だけなら、入るのは指示ではなく指示への参照になる。実行環境にHTTP取得手段がある保証もなく、index.jsonのダイジェストと実際に読まれた内容の対応も取れなくなる。

同期パイプラインは検証ゲートで守る

公開リポジトリを正典にすると、ポータルの /skills/<name>.md は派生物になる。プラグイン側は skills/news-triage/SKILL.md というディレクトリ形式、配信側は /skills/news-triage.md というフラット形式なので、どのみち変換が必要だからだ。

取得は自前のHTTP fetchではなくgitチェックアウトにした。整合性検証はオブジェクトハッシュ、バージョン固定はタグ、ロールバックは前タグへのcheckout——必要なものはgitが標準で持っている。自前で書くのは変換部分だけで済む。

同期スクリプトは、コピーを始める前に次を全部検証し、1つでも外れたら止まる。

  • チェックアウトに .git / plugin.json / skills/ が揃っていること
  • working treeにローカル変更がないこと
  • HEADがブランチではなく厳密なタグを指し、それがSemVer形式であること
  • 環境変数でタグを固定している場合は一致すること
  • plugin.json がAgent Plugins 1.0.0 のスキーマを指し、name が期待どおりであること
  • タグ vX.Y.Z と plugin.json の version が一致すること
  • 各Skillのディレクトリ名がケバブケースであること
  • SKILL.md frontmatterの name がディレクトリ名と一致し、description が空でないこと
  • Skillが0件でないこと

反映は一時ディレクトリへ組み立ててから入れ替える。旧世代は退避しておき、index.json生成が失敗したら旧世代へ巻き戻して再生成し直す。排他ロックも取るので、cronとデプロイが重なっても二重に走らない。

index.json側は、配信ディレクトリのフラットコピーを読んで、各Skillの name / description / URLと、配信するバイト列そのものから計算したsha256ダイジェストを並べる。リポジトリと配信面が乖離すれば数値がずれて検知できる。このindex.jsonは /agents.txt、/agent-manifest.json、/ai.txt からも同じURLで参照される。

結果として、日々の作業はこうなった。リポジトリにSKILL.mdを1本足し、plugin.json のバージョンを上げ、同じ番号でタグを切る。あとは同期が走り、/skills/<name>.md とindex.jsonが揃って更新される。

影響・今後

一番大きい変化は、Skillにリリースという単位が生まれたことだ。実際、公開初日のうちに v0.1.0(Skill 5本)→ v0.2.0(4本追加して9本)→ v0.2.1(記述の修正)と3タグを切っている。v0.2.1 は、書いたSkillを実際にライブのポータルへ当てて判明した思い込みの修正だった。政府文書のレコードは本文テキストを持たないので「本文を読んで判断せよ」は実行できない、license_note が full なのは「再利用してよい」であって「ポータルが本文を持っている」ではない、といった類である。こうした修正が履歴として残り、番号で参照できるようになった。

運用上の制約も明確にした。ポータル側は main を追わない。main を追うとpushが即座に本番の配信内容を書き換え、インストール済みのプラグインにも影響してしまうからだ。取得はデプロイ時とcronのみで、リクエストのたびにGitHubへ取りに行くことはしない。配信経路に外部サービスの可用性を持ち込まないためである。

課題も残っている。現在の mcp.json はヘッダーなしの無認証前提だ。将来的に認証を導入するとき、無認証の読み取り層を黙って潰すとインストール済みプラグインが一斉に動かなくなる。無認証読み取りを恒久的に維持すると明言するか、認証必須化をメジャーバージョン扱いにするか、先に決めておく必要がある。

よくある質問

Skillを1本追加したいとき、何をすればよいのか

公開リポジトリの skills/ 直下に新しいディレクトリを作り、その中に SKILL.md を置く。frontmatterの name はディレクトリ名と完全に一致させ、description を空にしない。あとは plugin.json の version を上げ、同じ番号のタグ(vX.Y.Z)を切るだけでよい。ポータル側の同期がタグを検証してコピーし、/skills/<name>.md と /.well-known/agent-skills/index.json が自動で更新される。

SKILL.mdに何を書き、何を書かないべきか

書くのは、発動条件・「一覧で絞ってから必要なものだけ取得する」といった原則・MCPエンドポイントの存在など、ほとんど変化しない情報である。ツールの引数リスト、カテゴリ一覧、レスポンスのフィールド名といった可変情報は書かない。それらはMCPサーバーが実行時にinstructionsとtool descriptionとして必ず配るため、SKILL.mdに複製すると更新のたびに腐る。

index.jsonのダイジェストは何のためにあるのか

配信されているSKILL.md本文のバイト列から計算したsha256値で、sha256- プレフィックス付きで記録される。エージェント側は取得した本文が索引と同一かを確認でき、運用側はリポジトリと配信内容の乖離を検知できる。配信実体から同一処理内で計算しているのがポイントで、別々に作ると乖離を見逃す。

同期の途中で失敗したらどうなるのか

コピー前の検証で落ちた場合は、配信中のファイルに一切触れずに終了する。コピー後にindex.json生成が失敗した場合は、退避しておいた旧世代へ巻き戻して再生成し直す。どちらの経路でも、直前まで配信できていた内容が残る。

まとめ

やったことは、Agent Skillsの本文をworkerの固定埋め込みからAgent Plugins 1.0.0準拠の公開リポジトリへ移し、ポータル配信をその派生物にした、それだけである。仕様が配置を固定してくれているおかげで、変換処理はディレクトリ形式からフラット形式への写像とダイジェスト計算だけの確定的な数十行に収まった。

得られたのは、リリース単位・変更履歴・ロールバック・整合性検証という、Gitがもともと持っている性質そのものだ。裏返せば、これまではそれらを一つも持たずにエージェント向けの指示を配っていたということになる。手元の作業がSKILL.md 1本とタグ1本まで縮んだのは、新しく何かを作ったからではなく、置き場所を正しい側へ戻したからだった。

参考ソース

関連記事