AIエージェントに手順書を持たせる「スキル」が増えてくると、配布が急に面倒になります。Gitリポジトリに置いて各自がcloneする運用は、バージョンの取り違えと、誰が何を入れているのか分からない状態を生みます。かといって公開レジストリに置けば、社内の業務手順がそのまま外に出ます。
SkillHubは、この間を埋めるセルフホスト型のスキルレジストリです。iFLYTEKがApache-2.0で公開しており、ファイアウォール内に立てたまま、npmに近い公開・検索・インストール体験を提供します。
この記事では、SkillHubの構造とデータモデル、そして中核である「公開フロー」を、実装コードで確認しながら整理します。読み終えると、自社にスキルレジストリを置くべきか、置くなら何を運用として決める必要があるかを判断できます。

この記事の全体像。以下、順に解説します。
SkillHubが引き受ける課題
スキル配布を自前で回すと、だいたい次の3つで詰まります。
| 課題 | 素朴な運用で起きること | SkillHubの答え |
|---|---|---|
| 配布 | 各自がcloneし、バージョンがばらける | セマンティックバージョニング + タグでの配布 |
| 統制 | 誰でも共有ディレクトリに置けてしまう | 名前空間ごとのロールと審査タスク |
| 安全性 | 中身を読まずにエージェントへ渡してしまう | 公開前の自動セキュリティスキャン |
特徴的なのは、3番目です。スキルは実質的に「エージェントに実行させる指示とスクリプト」なので、レジストリはパッケージマネージャであると同時に、サプライチェーンの検査点になります。SkillHubはこれをデータモデルの中心に据えています。
構造
コンテナ構成
デプロイ単位は、アプリケーション3つとデータサービス3つの計6つです。
注目すべきは検索基盤です。SkillHubは全文検索にElasticsearchを使わず、PostgreSQLのtsvectorで完結させています。skill_search_documentテーブルに生成列としてsearch_vectorを持ち、GINインデックスを張る構成です。
-- V2__phase2_skill_tables.sql より
ALTER TABLE skill_search_document
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('simple', coalesce(title, '')), 'A') ||
setweight(to_tsvector('simple', coalesce(summary, '')), 'B') ||
setweight(to_tsvector('simple', coalesce(keywords, '')), 'B') ||
setweight(to_tsvector('simple', coalesce(search_text, '')), 'C')
) STORED;
運用するミドルウェアが1つ減るのは、社内ツールとしては素直な設計判断です。
バックエンドのモジュール
Mavenマルチモジュールで、依存が一方向に流れる構成です。
skillhub-storageとskillhub-searchが抽象として切られているのが要点です。ストレージはObjectStorageServiceインターフェースに対してLocalFileStorageServiceとS3StorageServiceの2実装があり、開発時はローカルファイル、本番はS3互換を設定だけで切り替えられます。
データモデル
主要エンティティ
| エンティティ | 役割 |
|---|---|
namespace |
スコープの単位。typeはGLOBALまたはTEAM |
namespace_member |
所属ユーザーとロール。OWNER / ADMIN / MEMBER |
skill |
スキル本体。visibilityはPUBLIC / NAMESPACE_ONLY / PRIVATE |
skill_version |
バージョン単位。審査状態とmanifest_jsonを保持 |
skill_tag |
betaやstableなど、特定バージョンを指す別名 |
skill_file |
パッケージ内の1ファイル。sha256とstorage_keyで実体を追跡 |
security_audit |
スキャン結果。論理削除で履歴を残す |
skill_tagが指す先はversion_idカラムです。タグは「スキルに付ける分類ラベル」ではなく、「特定バージョンへのポインタ」である点に注意してください。npmのdist-tagと同じ発想です。
security_auditは論理削除(deleted_at)を採用しており、skill_versionが消えても検査履歴が残ります。監査目的のテーブルとして筋が通っています。
バージョンの状態機械
SkillHubの中核はこの状態遷移です。SkillVersionStatusは8つの値を持ちますが、現行のpublish処理が実際に通るのは次の7つです(DRAFTは列挙値として定義されているものの、publish経路では設定されません)。
遷移の主導権はSkillPublishServiceとSecurityScanServiceが分け合います。publishはまず可視性と権限で状態を決め、その直後にスキャンが起動して状態をSCANNINGへ移します。
| 条件 | publish直後 | スキャン | 最終的な落ち着き先 |
|---|---|---|---|
SUPER_ADMINによる公開、または自動公開指定 |
PUBLISHED |
実行され記録されるが状態は変えない | PUBLISHED |
visibility = PRIVATE |
UPLOADED |
SCANNINGへ移す |
UPLOADED |
visibility = PUBLIC / NAMESPACE_ONLY |
PENDING_REVIEW |
SCANNINGへ移す |
PENDING_REVIEW |
つまり人手の審査に乗るのは公開系のスキルだけで、PRIVATEはUPLOADEDのまま使えます。摩擦のかけ方が可視性に連動しているのが、この設計の効きどころです。管理者による自動公開だけはスキャン結果を待たずにPUBLISHEDになりますが、security_auditへの記録は同じように行われます。
ここでPRIVATEを「自分専用」と読まないでください。VisibilityChecker.canAccess()は、PRIVATEスキルへのアクセスを所有者に加えて、そのnamespaceのADMIN / OWNERとSUPER_ADMINにも許可します。スキップされるのは人手の審査だけで、スキャナが有効ならPRIVATEも検査対象です(triggerScan()は自動公開分も含む全バージョンで呼ばれます)。「レビューを通したくない機密スキルの置き場」ではない、という理解が必要です。
スキャン結果を反映するprocessScanResult()は、状態がSCANNINGのときだけ遷移させます。すでにPUBLISHEDやYANKEDになったバージョンを、遅れて届いたスキャン結果が巻き戻すことはありません。スキャンがタイムアウトした場合はScanTaskConsumerがSCAN_FAILEDへ落とします。
もう1つ、運用で効く挙動があります。同じスキルの新バージョンを公開すると、既存のPENDING_REVIEWバージョンはUPLOADEDへ差し戻されます。審査待ちが積み上がらないよう、最新版だけがレビュー対象になる仕組みです。審査者を待たせずに引っ込めたいときは、withdrawPendingVersion()でも同じUPLOADEDへ戻せます。なお、既存バージョンと同じバージョン番号での再公開は、そのバージョンがPUBLISHEDのときだけ拒否されます。SCAN_FAILEDやREJECTEDで終わったバージョンは、同じ番号のまま公開しなおせます。
公開済みバージョンの取り下げはyankVersion()で、PUBLISHEDのときだけYANKEDへ遷移します。
公開からインストールまで
スキャンはRedis Stream(skillhub:scan:requests)経由の非同期処理です。ただしRedisをconsumeするのはスキャナではなく、バックエンド内のScanTaskConsumerです。SecurityScanService.triggerScan()が要求を積み、ScanTaskConsumerが拾ってSkillScannerAdapterからスキャナのHTTP APIを呼び、結果をHTTPレスポンスとして受け取ります。スキャナ側はSkillHubのDBにもRedisにも触りません。
重要なのは、このスキャナが公開系の可視性では必須である点です。SkillPublishService.requiresSecurityScanner()はPUBLICとNAMESPACE_ONLYでスキャナの有効化を要求し、無効ならerror.security.scanner.requiredで公開そのものを拒否します。「スキャナは後回しにして審査ワークフローだけ先に回す」という導入はできません(PRIVATEのみに限れば可能です)。
一方、スキャナ内部の分析エンジンは選択式です。既定で有効なのはmetaだけで、LLM分析は既定で無効です。
| アナライザ | 環境変数 | 既定 |
|---|---|---|
| meta | SKILLHUB_SCANNER_USE_META |
true |
| behavioral | SKILLHUB_SCANNER_USE_BEHAVIORAL |
false |
| llm | SKILLHUB_SCANNER_USE_LLM |
false |
| ai-defense | SKILLHUB_SCANNER_USE_AI_DEFENSE |
false |
| virus-total | SKILLHUB_SCANNER_USE_VIRUS_TOTAL |
false |
| trigger | SKILLHUB_SCANNER_USE_TRIGGER |
false |
つまり、外部LLMを一切使わない構成でもスキャナは動きます。LLM分析を有効にするときだけ、SKILLHUB_SCANNER_LLM_PROVIDER(既定anthropic)と対応するAPIキーが必要になります。合議で精度を上げるSKILLHUB_SCANNER_LLM_CONSENSUS_RUNSは既定3なので、有効化するとコストと所要時間がその分かかります。
判定の厳しさはSKILLHUB_SCANNER_POLICY_PRESET(既定balanced)とSKILLHUB_SCANNER_FAIL_ON_SEVERITY(既定high)で決まります。後者は「どの深刻度以上をunsafeと判定するか」の閾値です。
パッケージの制約
公開できるZIPには上限がありますが、固定値ではなくskillhub.publish.*の設定項目です。同梱のapplication.ymlが出荷時の既定を与えています。
| 項目 | 出荷時の既定 | 設定キー |
|---|---|---|
| 最大ファイル数 | 100 | skillhub.publish.max-file-count |
| 単一ファイル最大サイズ | 10 MB | skillhub.publish.max-single-file-size |
| パッケージ合計最大サイズ | 100 MB | skillhub.publish.max-package-size |
SkillPublishPropertiesクラスのフィールド初期値(SkillPackagePolicyの定数と同じ500ファイル)と、application.ymlが与える値(100ファイル)は異なります。実際に効くのは後者なので、上限を確認するときはクラス定数ではなく設定ファイルを見てください。
これらの上限超過とSKILL.mdの欠落は、検証時にエラーとして公開を止めます。
一方、拡張子リストの扱いは違います。.md .txt .json .yaml .tomlなどのドキュメント・設定、.js .ts .py .sh .go .rs .javaなどのスクリプト、画像、Office文書が既定で並びますが、リスト外の拡張子はSkillPackageValidatorがDisallowed file extensionという警告として返すだけです。公開前確認のフローで警告を了承すれば、そのまま公開できます。リスト自体はSKILLHUB_PUBLISH_ALLOWED_FILE_EXTENSIONSで上書きできます。
つまり拡張子リストは「弾く壁」ではなく「気づかせる仕組み」です。バイナリの持ち込みを本気で止めたいなら、警告を無視できない運用ルールを別途決める必要があります。
パス正規化だけは厳格で、絶対パス、..によるルート脱出、ドライブレター混入は、いずれもエラーとして弾かれます。ZIP展開時のパストラバーサル対策として妥当な作りです。
導入
Docker Composeで試す
READMEのクイックスタートは、リモートスクリプトを取得して実行する形です。
rm -rf /tmp/skillhub-runtime
curl -fsSL https://imageless.oss-cn-beijing.aliyuncs.com/runtime.sh | sh -s -- up
# 公開URLを指定する場合
curl -fsSL https://imageless.oss-cn-beijing.aliyuncs.com/runtime.sh | sh -s -- up \
--public-url https://skillhub.your-company.com
配布元がAliyun OSSの外部ホストである点は、社内導入時にレビュー対象になります。取得したスクリプトを一度保存して読んでから実行する運用を勧めます。
初期設定では、デフォルト管理者(ユーザー名admin / パスワードChangeMe!2026)でログインします。このパスワードはREADMEに公開されている既知の値なので、公開URLを割り当てる前に変更してください。
Kubernetesへ載せる
Helmチャートがcharts/skillhubに同梱されています。PostgreSQLとRedisはBitnamiのサブチャートを依存として持ち、postgresql.enabled / redis.enabledで外部マネージドサービスへ差し替えられます。
helm dependency update charts/skillhub
helm install skillhub charts/skillhub -f my-values.yaml
values.schema.jsonが用意されているので、値の誤りはインストール前に検出されます。
利用
CLI
CLIはnpmパッケージ@astron-team/skillhubとして配布されています。
npm install -g @astron-team/skillhub
skillhub login --token sk_xxx --registry https://skillhub.your-company.com
skillhub search pdf
skillhub install pdf-parser --agent claude-code
skillhub publish ./my-skill --namespace team-myteam
サブコマンドはlogin logout whoami search install list update remove publish doctor version helpが実装されています。
エージェントプロファイル
installの--agentは、スキルの配置先をエージェントごとに解決するためのオプションです。リポジトリには15のプロファイルが同梱されています。
| 系統 | 対応エージェント |
|---|---|
| CLI系 | claude-code / codex / gemini-cli / kiro-cli / opencode / openclaw / openhands |
| エディタ系 | cursor / windsurf / trae / trae-cn / kilo / roo |
| その他 | github-copilot / generic-fallback |
--agentは複数指定でき、1つのスキルを複数エージェントへ同時に配置できます。配置先は次の3つのオプションで決めます。
| オプション | 役割 |
|---|---|
--agent <name> |
エージェントのプロファイルに従って配置先を解決 |
--scope user|project |
ユーザー単位かプロジェクト単位かを選択 |
--dir <path> |
明示したディレクトリへ配置 |
--agentと--scopeは併用できます。skillhub install pdf-parser --scope project --agent codexのように、対象エージェントの配置先をプロジェクト単位に寄せる指定が可能です。
排他なのは--dirだけです。--dirを--scopeまたは--agentと同時に指定するとusage errorになるので、CI用スクリプトを書くときは--dir単独か、--agent / --scopeの組み合わせかに寄せてください。
運用
起動と停止
make validate-release-config
docker compose --env-file .env.release -f compose.release.yml up -d
docker compose --env-file .env.release -f compose.release.yml down
make validate-release-configを先に通すことで、.env.releaseの不備を起動前に検出できます。
監視
Prometheus・Grafanaのスタックがmonitoring/に同梱されています。
cd monitoring
docker compose -f docker-compose.monitoring.yml up -d
# Prometheus: http://localhost:9090
# Grafana: http://localhost:3001
加えてaudit_logテーブルに、実行者・アクション・対象・リクエストIDが記録されます。公開や審査といったガバナンス操作は、管理画面またはAdmin API経由でここから照会できます。
ダウンロードは別扱いです。SkillDownloadedEventが持つのはskillIdとversionIdだけで、実行者もリクエストIDも含みません。skill.download_countを集計するためのイベントであり、「誰がいつこのスキルを取得したか」を追える監査証跡ではありません。利用者単位の追跡が要件なら、Nginxのアクセスログ側で確保してください。
ストレージ
本番ではローカルファイルシステムではなく、S3互換ストレージを使ってください。バックエンドを複数レプリカにした時点で、ローカルファイル実装は成立しません。設定はSKILLHUB_STORAGE_S3_*系の環境変数で行います。
導入前に確認したい点
実装を読んだうえで、判断材料になりそうな点を挙げます。
「セマンティック検索」の中身
skill_search_document.semantic_vectorという列があり、セマンティック検索を掲げています。ただし実装のHashingSearchEmbeddingServiceは、語をハッシュして64次元ベクトルに落とし、それをカンマ区切りのTEXTとして保存する方式です。pgvectorも外部の埋め込みモデルも使っていません。表記ゆれには効きますが、意味的に離れた語をつなぐ挙動は期待しないほうが安全です。
成熟度
HelmチャートのappVersionは0.2.14です。マイグレーションは35本を超えており、timestamptzへの型移行が繰り返し入っています。スキーマはまだ動いている段階と見るべきで、アップグレード時はマイグレーションの差分を確認する前提で計画してください。
スキャナは省略できない
PUBLICとNAMESPACE_ONLYのスキルを公開するには、スキャナコンテナが有効である必要があります。無効なら公開が拒否されるため、「まずレジストリだけ立てて、検査は後から足す」という段階導入はPRIVATEスキルに限られます。最小構成でもスキャナを含める前提で計画してください。逆に、LLM分析は既定で無効なので、外部LLMへの依存を避けたまま導入することは可能です。
まとめ
- SkillHubは、AIエージェントのスキルをファイアウォール内で配布するためのセルフホスト型レジストリです。
- 中核は
SkillVersionStatusの状態機械で、可視性に応じて審査の要否が変わります。PRIVATEは人手審査を経ずにUPLOADEDとなり、PUBLICとNAMESPACE_ONLYはPENDING_REVIEWを経由します。 - 検索はPostgreSQLの
tsvectorで完結し、運用するミドルウェアが少なく済みます。ただしセマンティック検索は軽量なハッシュ方式です。 - スキャナはRedis Stream経由の非同期処理で、公開系スキルでは必須です。ただしLLM分析は既定で無効なので、外部LLMなしでも導入できます。
- 検査結果は記録されるだけで、公開を自動で止めません。拡張子リスト外のファイルも警告止まりです。強制力を期待する箇所は、運用ルールで補ってください。
- 導入時は、既知のデフォルトパスワード、外部ホストからのインストールスクリプト、
appVersion 0.2.xという成熟度の3点を先に確認してください。
この記事が少しでも参考になった、あるいは改善点などがあれば、ぜひリアクションやコメント、SNSでのシェアをいただけると励みになります!
参考リンク
- GitHub - iflytek/skillhub
SkillVersionStatus.java(本文で参照した状態列挙)SkillPublishService.java(公開時の状態分岐とスキャナ必須判定)SecurityScanService.java(スキャン結果の反映ロジック)SkillPackageValidator.java(エラーと警告の切り分け)application.yml(出荷時のパッケージ上限)VisibilityChecker.java(PRIVATEの可視範囲)cli/src/commands/install.ts(CLIフラグの排他条件)scanner/docs/configuration.md(アナライザとポリシーの既定値)- SkillHub Docs (Zread)
- DeepWiki - iflytek/skillhub