📦 AIエージェントのスキルを社内で審査して配るレジストリ - SkillHub
目次

⚠️ 非公開(zenn 未公開)

📦 AIエージェントのスキルを社内で審査して配るレジストリ - SkillHub

AIエージェントに手順書を持たせる「スキル」が増えてくると、配布が急に面倒になります。Gitリポジトリに置いて各自がcloneする運用は、バージョンの取り違えと、誰が何を入れているのか分からない状態を生みます。かといって公開レジストリに置けば、社内の業務手順がそのまま外に出ます。

SkillHubは、この間を埋めるセルフホスト型のスキルレジストリです。iFLYTEKがApache-2.0で公開しており、ファイアウォール内に立てたまま、npmに近い公開・検索・インストール体験を提供します。

この記事では、SkillHubの構造とデータモデル、そして中核である「公開フロー」を、実装コードで確認しながら整理します。読み終えると、自社にスキルレジストリを置くべきか、置くなら何を運用として決める必要があるかを判断できます。

記事の全体像
この記事の全体像。以下、順に解説します。

SkillHubが引き受ける課題

スキル配布を自前で回すと、だいたい次の3つで詰まります。

課題 素朴な運用で起きること SkillHubの答え
配布 各自がcloneし、バージョンがばらける セマンティックバージョニング + タグでの配布
統制 誰でも共有ディレクトリに置けてしまう 名前空間ごとのロールと審査タスク
安全性 中身を読まずにエージェントへ渡してしまう 公開前の自動セキュリティスキャン

特徴的なのは、3番目です。スキルは実質的に「エージェントに実行させる指示とスクリプト」なので、レジストリはパッケージマネージャであると同時に、サプライチェーンの検査点になります。SkillHubはこれをデータモデルの中心に据えています。

構造

コンテナ構成

デプロイ単位は、アプリケーション3つとデータサービス3つの計6つです。

HTTPS /api/* JDBC S3 API scan要求を投入 ScanTaskConsumerが取得 HTTP (検査要求と結果) 利用者ブラウザ / CLI Nginxリバースプロキシ Spring Bootバックエンド Skill Scannerセキュリティ検査 (Python) PostgreSQL 16メタデータ / 全文検索 Redis 7セッション / Stream S3 または MinIOパッケージ本体

注目すべきは検索基盤です。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-appREST Controller / 起動・配線 skillhub-domainドメインモデル / サービス skillhub-authOAuth2 / RBAC skillhub-search検索SPI / PostgreSQL実装 skillhub-storageオブジェクトストレージ抽象 skillhub-notification通知 skillhub-infraJPA / スキャナ連携

skillhub-storageskillhub-searchが抽象として切られているのが要点です。ストレージはObjectStorageServiceインターフェースに対してLocalFileStorageServiceS3StorageServiceの2実装があり、開発時はローカルファイル、本番はS3互換を設定だけで切り替えられます。

データモデル

主要エンティティ

Namespace id: bigint slug: varchar display_name: varchar type: varchar status: varchar NamespaceMember id: bigint namespace_id: bigint user_id: varchar role: varchar Skill id: bigint namespace_id: bigint slug: varchar visibility: varchar status: varchar latest_version_id: bigint download_count: bigint star_count: int SkillVersion id: bigint skill_id: bigint version: varchar status: varchar manifest_json: jsonb parsed_metadata_json: jsonb file_count: int total_size: bigint SkillTag id: bigint skill_id: bigint tag_name: varchar version_id: bigint SkillFile id: bigint version_id: bigint file_path: varchar sha256: varchar storage_key: varchar SecurityAudit id: bigint skill_version_id: bigint verdict: varchar is_safe: boolean max_severity: varchar findings: jsonb 1 many 1 many 1 many 1 many 1 many 1 many
エンティティ 役割
namespace スコープの単位。typeGLOBALまたはTEAM
namespace_member 所属ユーザーとロール。OWNER / ADMIN / MEMBER
skill スキル本体。visibilityPUBLIC / NAMESPACE_ONLY / PRIVATE
skill_version バージョン単位。審査状態とmanifest_jsonを保持
skill_tag betastableなど、特定バージョンを指す別名
skill_file パッケージ内の1ファイル。sha256storage_keyで実体を追跡
security_audit スキャン結果。論理削除で履歴を残す

skill_tagが指す先はversion_idカラムです。タグは「スキルに付ける分類ラベル」ではなく、「特定バージョンへのポインタ」である点に注意してください。npmのdist-tagと同じ発想です。

security_auditは論理削除(deleted_at)を採用しており、skill_versionが消えても検査履歴が残ります。監査目的のテーブルとして筋が通っています。

バージョンの状態機械

SkillHubの中核はこの状態遷移です。SkillVersionStatusは8つの値を持ちますが、現行のpublish処理が実際に通るのは次の7つです(DRAFTは列挙値として定義されているものの、publish経路では設定されません)。

publish (通常) publish (SUPER_ADMIN) スキャン失敗 requestedVisibility = PRIVATE requestedVisibility = 公開系 承認 却下 取り下げ / 新版公開による差し戻し yank SCANNING PUBLISHED SCAN_FAILED UPLOADED PENDING_REVIEW REJECTED YANKED

遷移の主導権はSkillPublishServiceSecurityScanServiceが分け合います。publishはまず可視性と権限で状態を決め、その直後にスキャンが起動して状態をSCANNINGへ移します。

条件 publish直後 スキャン 最終的な落ち着き先
SUPER_ADMINによる公開、または自動公開指定 PUBLISHED 実行され記録されるが状態は変えない PUBLISHED
visibility = PRIVATE UPLOADED SCANNINGへ移す UPLOADED
visibility = PUBLIC / NAMESPACE_ONLY PENDING_REVIEW SCANNINGへ移す PENDING_REVIEW

つまり人手の審査に乗るのは公開系のスキルだけで、PRIVATEUPLOADEDのまま使えます。摩擦のかけ方が可視性に連動しているのが、この設計の効きどころです。管理者による自動公開だけはスキャン結果を待たずにPUBLISHEDになりますが、security_auditへの記録は同じように行われます。

ここでPRIVATEを「自分専用」と読まないでください。VisibilityChecker.canAccess()は、PRIVATEスキルへのアクセスを所有者に加えて、そのnamespaceのADMIN / OWNERSUPER_ADMINにも許可します。スキップされるのは人手の審査だけで、スキャナが有効ならPRIVATEも検査対象です(triggerScan()は自動公開分も含む全バージョンで呼ばれます)。「レビューを通したくない機密スキルの置き場」ではない、という理解が必要です。

スキャン結果を反映するprocessScanResult()は、状態がSCANNINGのときだけ遷移させます。すでにPUBLISHEDYANKEDになったバージョンを、遅れて届いたスキャン結果が巻き戻すことはありません。スキャンがタイムアウトした場合はScanTaskConsumerSCAN_FAILEDへ落とします。

もう1つ、運用で効く挙動があります。同じスキルの新バージョンを公開すると、既存のPENDING_REVIEWバージョンはUPLOADEDへ差し戻されます。審査待ちが積み上がらないよう、最新版だけがレビュー対象になる仕組みです。審査者を待たせずに引っ込めたいときは、withdrawPendingVersion()でも同じUPLOADEDへ戻せます。なお、既存バージョンと同じバージョン番号での再公開は、そのバージョンがPUBLISHEDのときだけ拒否されます。SCAN_FAILEDREJECTEDで終わったバージョンは、同じ番号のまま公開しなおせます。

公開済みバージョンの取り下げはyankVersion()で、PUBLISHEDのときだけYANKEDへ遷移します。

公開からインストールまで

skillhub publish ./my-skill ZIP アップロード パッケージ検証 (SKILL.md / 拡張子 / サイズ) skillhub:scan:requests へ投入 スキャン要求を取得 HTTP で検査を依頼 verdict / findings security_audit へ記録 審査タスク (PENDING_REVIEW) 承認 PUBLISHED へ遷移 skillhub install my-skill --agent claude-code 開発者 skillhub CLI Spring Boot Redis Stream ScanTaskConsumer Skill Scanner 審査者

スキャンはRedis Stream(skillhub:scan:requests)経由の非同期処理です。ただしRedisをconsumeするのはスキャナではなく、バックエンド内のScanTaskConsumerです。SecurityScanService.triggerScan()が要求を積み、ScanTaskConsumerが拾ってSkillScannerAdapterからスキャナのHTTP APIを呼び、結果をHTTPレスポンスとして受け取ります。スキャナ側はSkillHubのDBにもRedisにも触りません。

重要なのは、このスキャナが公開系の可視性では必須である点です。SkillPublishService.requiresSecurityScanner()PUBLICNAMESPACE_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文書が既定で並びますが、リスト外の拡張子はSkillPackageValidatorDisallowed 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が持つのはskillIdversionIdだけで、実行者もリクエストIDも含みません。skill.download_countを集計するためのイベントであり、「誰がいつこのスキルを取得したか」を追える監査証跡ではありません。利用者単位の追跡が要件なら、Nginxのアクセスログ側で確保してください。

ストレージ

本番ではローカルファイルシステムではなく、S3互換ストレージを使ってください。バックエンドを複数レプリカにした時点で、ローカルファイル実装は成立しません。設定はSKILLHUB_STORAGE_S3_*系の環境変数で行います。

導入前に確認したい点

実装を読んだうえで、判断材料になりそうな点を挙げます。

「セマンティック検索」の中身
skill_search_document.semantic_vectorという列があり、セマンティック検索を掲げています。ただし実装のHashingSearchEmbeddingServiceは、語をハッシュして64次元ベクトルに落とし、それをカンマ区切りのTEXTとして保存する方式です。pgvectorも外部の埋め込みモデルも使っていません。表記ゆれには効きますが、意味的に離れた語をつなぐ挙動は期待しないほうが安全です。

成熟度
HelmチャートのappVersion0.2.14です。マイグレーションは35本を超えており、timestamptzへの型移行が繰り返し入っています。スキーマはまだ動いている段階と見るべきで、アップグレード時はマイグレーションの差分を確認する前提で計画してください。

スキャナは省略できない
PUBLICNAMESPACE_ONLYのスキルを公開するには、スキャナコンテナが有効である必要があります。無効なら公開が拒否されるため、「まずレジストリだけ立てて、検査は後から足す」という段階導入はPRIVATEスキルに限られます。最小構成でもスキャナを含める前提で計画してください。逆に、LLM分析は既定で無効なので、外部LLMへの依存を避けたまま導入することは可能です。

まとめ

  • SkillHubは、AIエージェントのスキルをファイアウォール内で配布するためのセルフホスト型レジストリです。
  • 中核はSkillVersionStatusの状態機械で、可視性に応じて審査の要否が変わります。PRIVATEは人手審査を経ずにUPLOADEDとなり、PUBLICNAMESPACE_ONLYPENDING_REVIEWを経由します。
  • 検索はPostgreSQLのtsvectorで完結し、運用するミドルウェアが少なく済みます。ただしセマンティック検索は軽量なハッシュ方式です。
  • スキャナはRedis Stream経由の非同期処理で、公開系スキルでは必須です。ただしLLM分析は既定で無効なので、外部LLMなしでも導入できます。
  • 検査結果は記録されるだけで、公開を自動で止めません。拡張子リスト外のファイルも警告止まりです。強制力を期待する箇所は、運用ルールで補ってください。
  • 導入時は、既知のデフォルトパスワード、外部ホストからのインストールスクリプト、appVersion 0.2.xという成熟度の3点を先に確認してください。

この記事が少しでも参考になった、あるいは改善点などがあれば、ぜひリアクションやコメント、SNSでのシェアをいただけると励みになります!

参考リンク