Embedded Services (日本語)
なぜ組み込みサービスなのか?
Section titled “なぜ組み込みサービスなのか?”6つのサービスが組み込まれています。
| サービス | npmパッケージ | デフォルトポート | 用途 |
|---|---|---|---|
| 9Router | 9router |
20130 | OmniRouteがサブプロバイダーとして使用できるAIルーター。モデルは9router/{sub}/{model}として公開されます |
| CLIProxyAPI | GitHubリリースバイナリ(cliproxy) |
8317 | Anthropic CLI認証フロー用のローカルプロキシアダプター。OAuthトークンの有効期限が切れた場合にフォールバックルーティングを提供します |
| Mux | mux(ヘッドレスmux server) |
8322 | ローカルのエージェントオーケストレーションデーモン(coder/mux)。ライフサイクル管理のみを行い、ルーティング先ではありません(LLMプロキシ機能なし)。 |
| Bifrost | @maximhq/bifrost |
8080 | Go製AIゲートウェイのリレーバックエンド。実行中は、リレールート(/v1/relay/)によって自動的に選択されます |
| Dario | @askalf/dario |
3456 | Claudeサブスクリプションプロキシ。Claude Code形式のトラフィックに対するCLIProxyAPIの代替/フェイルオーバーとして機能します。注入されたキーはDARIO_ADMIN_TOKENとなり、その/admin/* OAuthコントロールプレーンへのアクセスを制御します |
| open-wa | @open-wa/wa-automate |
8323 | WhatsApp Web自動化(Puppeteer経由のヘッドレスChromium)。ライフサイクル管理のみを行い、ルーティング先ではありません。 |
6つすべてが同じ監視モデルに従います。
- OmniRouteはこれらを
DATA_DIR/services/{name}/配下にインストールします(OmniRoute自身のpackage.jsonから分離) - OmniRouteはこれらを子プロセスとして起動し、監視します
- OmniRouteは一時的なAPIキーを子プロセスの環境に注入し、(該当する場合)ダウンタイムなしでローテーションします
- すべての管理ルート(
/api/services/*)はLOCAL_ONLYです。ループバックからのみアクセスできます(厳格ルール#17)
主要な決定事項(設計計画より)
Section titled “主要な決定事項(設計計画より)”| 決定事項 | 値 |
|---|---|
| 9RouterネイティブUIへのダッシュボードアクセス | /dashboard/providers/services/9router/embed/*でのリバースプロキシ |
| インストール方式 | execFileによるnpm install {package}(シェル展開なし) |
| 利用モード | ルーティングエンジンに9router/{sub}/{model}として登録されたプロバイダー |
| APIキー管理 | OmniRouteが生成し、保存時に暗号化(AES-256-GCM)して、環境変数経由で注入 |
| ダッシュボードの場所 | /dashboard/providers/services(3つのタブ) |
| 自動起動 | サービスごとのトグル、デフォルトはOFF |
2. アーキテクチャ — 4層
Section titled “2. アーキテクチャ — 4層”┌────────────────────────────────────────────────────────────────────┐│ レイヤー1 — UI ││ /dashboard/providers/services (タブ: CLIProxyAPI | 9Router | Mux)││ ライブログ (SSE)、開始/停止/再起動/更新、設定、インストール ││ ││ src/app/(dashboard)/dashboard/providers/services/ ││ ├── page.tsx シェル + ?tab= によるタブルーティング││ ├── tabs/ CliproxyServiceTab, NinerouterServiceTab,││ │ MuxServiceTab ││ └── components/ ServiceStatusCard, ServiceLifecycleButtons,││ ServiceLogsPanel, ApiKeyCard, ... │└──────────────────────┬─────────────────────────────────────────────┘ │ HTTP (Next.js fetch)┌──────────────────────▼─────────────────────────────────────────────┐│ レイヤー2 — API (LOCAL_ONLY — ループバックのみ) ││ ││ /api/services/9router/{install|start|stop|restart|update| ││ rotate-key|status|auto-start|logs} ││ /api/services/cliproxy/{install|start|stop|restart|update| ││ status|auto-start|logs} ││ /api/services/mux/{install|start|stop|restart|update| ││ status|auto-start|logs} ││ /dashboard/providers/services/9router/embed/[...path] ││ (HTTP + WebSocket リバースプロキシ → 9Router アップストリーム) ││ ││ ゲート: LOCAL_ONLY_API_PREFIXES に "/api/services/" および ││ "/dashboard/providers/services/*/embed/" を含める │└──────────────────────┬─────────────────────────────────────────────┘ │ プロセス内呼び出し┌──────────────────────▼─────────────────────────────────────────────┐│ レイヤー3 — ServiceSupervisor (src/lib/services/) ││ ││ ServiceSupervisor.ts 汎用スーパーバイザー (child_process.spawn) ││ ├── インストール: execFile('npm', ['install', pkg, '--prefix']) ││ ├── 開始: spawn(node, [entrypoint], {env, cwd}) ││ ├── APIキー: crypto.randomBytes(32) → env NINEROUTER_API_KEY││ ├── ポート: 9Router は 20130 (設定可能) ││ ├── ログ: stdio リングバッファ 5 MB → SSE イベント ││ ├── ヘルス: 2~5秒ごとに HTTP GET /health、遅延リカバリー ││ └── ライフサイクル: SIGTERM 15秒 → SIGKILL ││ ││ registry.ts getSupervisor(name) / registerSupervisor() ││ bootstrap.ts プロセス開始時にすべての SERVICES[] を起動 ││ apiKey.ts getOrCreateApiKey(), generateServiceApiKey() ││ modelSync.ts 定期的に GET /v1/models → service_models テーブル││ ringBuffer.ts 循環ログバッファ (サービスごとに 5 MB) ││ healthCheck.ts ポーリングによる HTTP ヘルスプローブ ││ installers/ ninerouter.ts, cliproxy.ts, mux.ts, openwa.ts ││ (インストーラーアダプター) │└──────────────────────┬─────────────────────────────────────────────┘ │ OpenAI互換 HTTP (ループバック)┌──────────────────────▼─────────────────────────────────────────────┐│ レイヤー4 — プロバイダー / ルーティング ││ ││ open-sse/executors/ninerouter.ts ││ リクエストごとにポートとAPIキーを再取得 (キャッシュなし)。 ││ プロキシ前にモデルIDから "9router/" プレフィックスを削除。 ││ スーパーバイザーが "running" でなければ 503 service_not_running を返す。││ ││ src/shared/constants/providers.ts ││ "9router" のエントリ: isEmbeddedService: true ││ ││ open-sse/config/providerRegistry.ts ││ モデルは "9router/{sub}/{model}" (プレフィックス付き) として保存。││ modelSync.ts により5分ごとに同期。 ││ ││ Mux はライフサイクル管理のみ (レイヤー1~3) — LLMプロキシではなく││ エージェントオーケストレーションデーモンであるため、レイヤー4の ││ executor/provider エントリはなく、ルーティング対象にもならない。 │└────────────────────────────────────────────────────────────────────┘主要なソースファイル
Section titled “主要なソースファイル”| ファイル | 役割 |
|---|---|
src/lib/services/ServiceSupervisor.ts |
コアクラス:ライフサイクル、ロック、ヘルス、リングバッファ |
src/lib/services/bootstrap.ts |
プロセスレベルの登録と自動起動 |
src/lib/services/registry.ts |
シングルトンマップ tool → supervisor |
src/lib/services/apiKey.ts |
キー生成、保存時の AES-256-GCM 暗号化 |
src/lib/services/modelSync.ts |
定期的なモデル同期(5 分ごと)+ オンデマンド |
src/lib/services/ringBuffer.ts |
SSE サブスクライブ機能付き 5 MB 循環ログバッファ |
src/lib/services/healthCheck.ts |
HTTP ヘルスプローブ(間隔を設定可能) |
src/lib/services/installers/ninerouter.ts |
9Router の npm インストール/更新/アンインストール |
src/lib/services/installers/cliproxy.ts |
CLIProxyAPI の npm インストール/更新/アンインストール |
src/lib/services/installers/mux.ts |
Mux の npm インストール/更新/アンインストール |
src/lib/services/installers/openwa.ts |
open-wa の npm インストール/更新/アンインストール |
src/app/api/services/9router/_lib.ts |
getOrInitSupervisor() ヘルパー |
src/app/api/services/[name]/logs/route.ts |
共有 SSE ログエンドポイント |
open-sse/executors/ninerouter.ts |
プロバイダーエグゼキューター(レイヤー 4) |
3. ライフサイクルステートマシン
Section titled “3. ライフサイクルステートマシン” install() ┌─────────────┐ ──────────► ┌─────────────┐ │ not_installed│ │ stopped │◄──────────────────┐ └─────────────┘ └──────┬──────┘ │ │ start() │ ▼ │ stop() ┌──────────┐ │ │ starting │ │ └────┬─────┘ │ ヘルスプローブ成功 │ クラッシュ / SIGTERM │ ┌────▼─────┐ (5秒以内に終了) │ │ running │─ クラッシュ ────────►┤ └────┬─────┘ ┌─▼────┐ stop() │ │error │ ▼ └──────┘ ┌──────────┐ │ stopping │ └──────────┘状態は version_manager DBテーブル(status カラム)に保存され、
ServiceSupervisor のインメモリ状態にも反映されます。実行中のプロセスについては
インメモリ状態が信頼できる情報源であり、DB状態は起動時の永続的なフォールバックです。
| 遷移元 | イベント | 遷移先 |
|---|---|---|
not_installed |
install() が成功 |
stopped |
stopped |
start() が呼び出される |
starting |
starting |
ヘルスプローブが200を返す | running |
starting |
正常になる前にプロセスが終了 | error |
running |
stop() が呼び出される |
stopping → stopped |
running |
プロセスが予期せず終了(< 5秒) | error(高速クラッシュ) |
running |
プロセスが予期せず終了(> 5秒) | error |
error |
start() が呼び出される |
starting |
| 任意 | stopping 中に stop() |
何もしない |
ServiceSupervisor は、非同期操作ロック(withLock())を介してライフサイクル操作を
直列化します。同じスーパーバイザーに対して start() を同時に呼び出しても、生成される
プロセスは正確に1つだけです。2番目の呼び出し元は待機し、既存のステータスを返します。
これにより、たとえば自動起動とUIボタンが同時に実行された場合の競合状態を防ぎます。
4. API リファレンス
Section titled “4. API リファレンス”/api/services/ 配下のすべてのルートは LOCAL_ONLY です(ループバックのみ、厳格なルール #17)。
ループバック以外からのリクエストには、認証トークンに関係なく 403 LOCAL_ONLY が返されます。
4.1 9Router エンドポイント(11 ルート)
Section titled “4.1 9Router エンドポイント(11 ルート)”POST /api/services/9router/install
Section titled “POST /api/services/9router/install”npm から 9Router をインストールします。独自の package.json と node_modules/ を含む
DATA_DIR/services/9router/ を作成します。OmniRoute 自体の依存関係とは競合しません。
リクエストボディ(すべて任意):
{ "version": "latest" }| フィールド | 型 | デフォルト | 説明 |
|---|---|---|---|
version |
string |
"latest" |
インストールする npm バージョンタグまたは semver |
レスポンス:
| ステータス | 説明 |
|---|---|
200 |
{ ok: true, installedVersion: "x.y.z", path: "..." } |
400 |
無効なリクエストボディ(Zod 検証失敗) |
409 |
インストール中(ロック取得済み) |
500 |
npm install に失敗 — 分かりやすいエラーについては message を参照 |
注: execFile('npm', [...]) を使用します — シェルも補間も使用しません(厳格なルール #13)。
EACCES エラーは、分かりやすいメッセージとして提示されます。
POST /api/services/9router/start
Section titled “POST /api/services/9router/start”9Router を起動します。まだ登録されていない場合は supervisor を登録してから、
supervisor.start() を呼び出します。すでに実行中の場合は冪等です。
リクエストボディ: なし
レスポンス:
| ステータス | 説明 |
|---|---|
200 |
ServiceStatus オブジェクト(以下のスキーマを参照) |
409 |
9Router がインストールされていない(status: "not_installed") |
503 |
起動に失敗(プロセスエラー — lastError を参照) |
ServiceStatus スキーマ:
{ "tool": "9router", "state": "running", "pid": 12345, "port": 20130, "health": "healthy", "startedAt": "2026-05-25T10:00:00.000Z", "lastError": null}POST /api/services/9router/stop
Section titled “POST /api/services/9router/stop”9Router を正常に停止します。SIGTERM を送信して 15 秒待機し、まだ稼働している場合は SIGKILL を送信します。 すでに停止している場合は冪等です。
リクエストボディ: なし
レスポンス:
| ステータス | 説明 |
|---|---|
200 |
ServiceStatus(state: “stopped”) |
503 |
予期せず停止に失敗 |
POST /api/services/9router/restart
Section titled “POST /api/services/9router/restart”操作ロック下で stop()、start() の順に実行するのと同等です。
リクエストボディ: なし
レスポンス: start と同じです(最終的な ServiceStatus を返します)。
POST /api/services/9router/update
Section titled “POST /api/services/9router/update”9Router を新しい npm バージョンに更新します。サービスが実行中の場合は、最初に停止し、 npm install を実行して新しいバージョンをその場でインストールした後、 サービスを再起動します。
リクエストボディ(すべて任意):
{ "version": "latest" }レスポンス:
| ステータス | 説明 |
|---|---|
200 |
{ ok: true, previousVersion: "...", installedVersion: "..." } |
400 |
無効なボディ |
500 |
npm update に失敗 |
POST /api/services/9router/rotate-key
Section titled “POST /api/services/9router/rotate-key”9Router 用の新しい API キーを生成し、保存時に暗号化します。また、サービスが実行中の場合は、 環境から新しいキーを読み込むようにサービスを再起動します。古いキーは 直ちに無効化されます。
リクエストボディ: なし
レスポンス:
| ステータス | 説明 |
|---|---|
200 |
{ keyRotated: true, restarted: boolean } |
500 |
ローテーションに失敗 |
セキュリティ: 新しいキーがレスポンスで返されることはありません(認証情報の漏洩防止)。
キーは version_manager テーブルに暗号化(AES-256-GCM)して保存されます。
GET /api/services/9router/status
Section titled “GET /api/services/9router/status”バージョンメタデータと API キーのプレビューを含む、ライブ状態と DB 状態を統合したステータスを返します。
レスポンス:
| ステータス | 説明 |
|---|---|
200 |
以下のスキーマを参照 |
500 |
ステータスの読み取りに失敗 |
レスポンススキーマ:
{ "tool": "9router", "state": "running", "pid": 12345, "port": 20130, "health": "healthy", "startedAt": "2026-05-25T10:00:00.000Z", "lastError": null, "installedVersion": "1.2.3", "latestVersion": "1.2.4", "updateAvailable": true, "apiKeyMasked": "nr_****abcd", "autoStart": false, "providerExpose": false}POST /api/services/9router/auto-start
Section titled “POST /api/services/9router/auto-start”自動起動フラグを切り替えます。enabled: true の場合、次回 OmniRoute が起動するときに
サービスが自動的に起動します(サービスがインストールされている場合)。
リクエストボディ:
{ "enabled": true }レスポンス:
| ステータス | 説明 |
|---|---|
200 |
{ autoStart: true } |
400 |
無効なボディ |
GET /api/services/9router/logs
Section titled “GET /api/services/9router/logs”9Router の stdout/stderr リングバッファからライブログを配信する SSE ストリームです。
クエリパラメータ:
| パラメータ | 型 | デフォルト | 説明 |
|---|---|---|---|
tail |
integer |
200 | 最初に送信する過去のログ行数(最大 1000) |
filter |
string |
なし | 大文字と小文字を区別しない部分文字列フィルター(正規表現は不使用 — ReDoS 対策済み) |
SSE イベント:
| イベント | データ | 説明 |
|---|---|---|
snapshot |
LogLine[] |
過去ログの初期末尾部分 |
log |
LogLine |
ライブのログ行 |
heartbeat |
{} |
15 秒ごとのキープアライブ |
LogLine スキーマ:
{ "ts": 1716633600000, "stream": "stdout", "line": "[9router] Listening on :20130"}レスポンス:
| ステータス | 説明 |
|---|---|
200 |
text/event-stream |
400 |
filter パラメーターが長すぎる(200文字超) |
404 |
サービスが見つからない(supervisorに未登録) |
4.2 CLIProxyAPIエンドポイント(10ルート)
Section titled “4.2 CLIProxyAPIエンドポイント(10ルート)”CLIProxyAPIは、9Routerからrotate-keyを除き、accounts、provider-expose、
auto-restart-adoptedを加えたものと同じエンドポイント構成です。現在は起動時に
専用のデータプレーンAPIキーが注入されます(bootstrap.tsのneedsApiKey: true。
モデル同期に使用)。statusに含まれるフィールドは少なくなっています。
| メソッド | パス | 説明 |
|---|---|---|
POST |
/api/services/cliproxy/install |
npmからCLIProxyAPIをインストール |
POST |
/api/services/cliproxy/start |
CLIProxyAPIを起動 |
POST |
/api/services/cliproxy/stop |
CLIProxyAPIを停止 |
POST |
/api/services/cliproxy/restart |
CLIProxyAPIを再起動 |
POST |
/api/services/cliproxy/update |
新しいバージョンへ更新 |
GET |
/api/services/cliproxy/status |
ライブ状態 + DB状態(apiKeyMaskedなし) |
POST |
/api/services/cliproxy/auto-start |
自動起動を切り替え |
共有のGET /api/services/{name}/logsエンドポイント(§4.1を参照)は、
[name]動的セグメントを使用して4つすべてのサービスで機能します。
4.3 Muxエンドポイント(8ルート)
Section titled “4.3 Muxエンドポイント(8ルート)”MuxはCLIProxyAPIと同じエンドポイント構成で、APIサーフェスにrotate-keyルートは
ありません(ベアラートークンは9Routerと同様にgetOrCreateApiKey("mux")を介して
生成され、MUX_SERVER_AUTH_TOKEN環境変数を介して注入されますが、専用のローテーション
エンドポイントはまだありません)。Muxはライフサイクル管理のみの対象です。9Routerとは異なり、
Layer 4 executorを持たず、ルーティングプロバイダーとして登録されることもありません。
| メソッド | パス | 説明 |
|---|---|---|
POST |
/api/services/mux/install |
npmからMuxをインストール(npm i mux) |
POST |
/api/services/mux/start |
Muxを起動(mux server) |
POST |
/api/services/mux/stop |
Muxを停止 |
POST |
/api/services/mux/restart |
Muxを再起動 |
POST |
/api/services/mux/update |
新しいnpmバージョンへ更新 |
GET |
/api/services/mux/status |
ライブ状態 + DB状態 |
POST |
/api/services/mux/auto-start |
自動起動を切り替え |
4.4 Bifrostエンドポイント(8ルート)
Section titled “4.4 Bifrostエンドポイント(8ルート)”BifrostはGo製のAIゲートウェイ・リレーバックエンド(@maximhq/bifrost)です。
CLIProxyAPIと同じエンドポイント構成を使用します(rotate-keyはありません。
Bifrostは、-app-dir配下のconfig.jsonで独自のプロバイダーキーを管理します)。
| メソッド | パス | 説明 |
|---|---|---|
POST |
/api/services/bifrost/install |
npmからBifrostをインストール(@maximhq/bifrost) |
POST |
/api/services/bifrost/start |
ポート8080(デフォルト)でBifrostを起動 |
POST |
/api/services/bifrost/stop |
Bifrostを停止 |
POST |
/api/services/bifrost/restart |
Bifrostを再起動 |
POST |
/api/services/bifrost/update |
新しいバージョンへ更新 |
GET |
/api/services/bifrost/status |
ライブ状態 + DB状態 |
POST |
/api/services/bifrost/auto-start |
自動起動を切り替え |
GET |
/api/services/bifrost/logs |
SSEログ末尾(共有の[name]/logs動的ルートを介して) |
ルーティング接続: BIFROST_BASE_URLが未設定で、監視対象のBifrostインスタンスが
実行中の場合、getBifrostRoutingConfig()(routingBackend.ts内)は自動的に
http://127.0.0.1:{port}をリレーのベースURLとして使用します。明示的に設定された
BIFROST_BASE_URL環境変数が常に優先されます。
4.5 Darioエンドポイント(12ルート)
Section titled “4.5 Darioエンドポイント(12ルート)”他のサービスと同じライフサイクル構成(install、start、stop、restart、
update、status、auto-start、auto-restart-adopted)に加え、admin/配下に
トークンで保護されたOAuthコントロールプレーンがあります。admin/accounts、
admin/import-from-omniroute、admin/login-start、admin/login-complete
(すべてDARIO_ADMIN_TOKENによって保護されています)。
4.6 open-waエンドポイント(7ルート)
Section titled “4.6 open-waエンドポイント(7ルート)”open-wa(@open-wa/wa-automate)は、WhatsApp Webを自動化するために
ヘッドレスChromiumインスタンスを(Puppeteer経由で)操作します。Muxと同じ
エンドポイント構成を使用します(rotate-keyルートはまだありません)。
ライフサイクル管理のみの対象であり、ルーティング先ではなく、
Layer 4 executor/providerエントリもありません。
| メソッド | パス | 説明 |
|---|---|---|
POST |
/api/services/openwa/install |
npm から open-wa(@open-wa/wa-automate)をインストール |
POST |
/api/services/openwa/start |
ポート 8323(デフォルト)で open-wa を起動 |
POST |
/api/services/openwa/stop |
open-wa を停止 |
POST |
/api/services/openwa/restart |
open-wa を再起動 |
POST |
/api/services/openwa/update |
より新しいバージョンに更新 |
GET |
/api/services/openwa/status |
ライブ + DB ステータス |
POST |
/api/services/openwa/auto-start |
自動起動を切り替え |
GET |
/api/services/openwa/logs |
SSE ログ末尾(共有の [name]/logs 動的ルート経由) |
API キー: WA_KEY として注入されます。open-wa の汎用 WA_* プレフィックス付き環境変数オーバーライドにより、--key/-k CLI オプションへマッピングされます(dist/cli/setup.js::envArgs()、インストール済みの 4.76.0 パッケージで検証済み)。generateServiceApiKey() による生成時には ow_ がプレフィックスとして付与されます。open-wa は key/api_key HTTP ヘッダーからキーを読み取ります(Authorization: Bearer ではありません)。/api-docs* はチェックから明示的に除外されているため(dist/cli/server.js の setupAuthenticationLayer)、ヘルスプローブに認証ヘッダーは不要です。
ペアリング: open-wa は非公式であり、WhatsApp とは提携していません。そのため、接続された番号には WhatsApp 独自の自動化検出によって利用停止となるリスクがあります。初回起動時、ペアリング用 QR コードが stdout に出力され、既存のログパネル/SSE ストリームを通じて表示されます。この統合には、専用の QR 画像エンドポイントはまだありません。
4.7 リバースプロキシ(9Router ダッシュボードの埋め込み)
Section titled “4.7 リバースプロキシ(9Router ダッシュボードの埋め込み)”ダッシュボードは、次の内部リバースプロキシを介して iframe 内に 9Router Web UI を埋め込みます:
GET|POST|... /dashboard/providers/services/9router/embed/[...path]このプロキシは:
- リクエストを
http://127.0.0.1:{port}/{path}に転送します(ループバックのみ) - 受信した
cookieおよびauthorizationヘッダーを削除します(OmniRoute セッションの漏洩を防止) - 9Router 認証用に
Authorization: Bearer {apiKey}を注入します - レスポンスから
set-cookie、content-security-policy、x-frame-options、cross-origin-*を削除します - HTML レスポンスを書き換えて
<base href>を注入し、絶対パスを正規化します(/foo→/dashboard/.../embed/foo)
埋め込みダッシュボードの WebSocket アップグレードは、専用ポート上の補助サーバーによって処理されます(src/lib/services/embedWsProxy.ts を参照)。
セキュリティ: 埋め込みプロキシルートは LOCAL_ONLY_API_PREFIXES に分類され、ループバックからのみアクセスできます。Cloudflare/Ngrok トンネル経由で JWT を取得した攻撃者であっても、埋め込みサービスへのプロキシアクセスはできません。
5. セキュリティ
Section titled “5. セキュリティ”LOCAL_ONLY の強制(厳格ルール #17)
Section titled “LOCAL_ONLY の強制(厳格ルール #17)”/api/services/ および /dashboard/providers/services/*/embed/ 配下のすべてのルートは、
src/server/authz/routeGuard.ts で LOCAL_ONLY に分類されています。ループバックチェックは、
認証分岐より前に無条件で実行されます。
リクエストを受信 → isLocalOnlyPath(path)? → 非ループバック → 403 LOCAL_ONLY(常に認証チェックより前) → ループバック → 通常の認証へ進むこれにより、(トンネル経由などで)漏洩した JWT が npm install や
プロセス生成を引き起こすのを防ぎます。完全なティアマトリクスについては、
docs/security/ROUTE_GUARD_TIERS.md を参照してください。
API キーの注入
Section titled “API キーの注入”9Router と Mux は、それぞれの HTTP エンドポイント用に API キー/Bearer トークンを必要とします。 OmniRoute は次の処理を行います。
crypto.randomBytes(32).toString("base64url")を使用し、 サービス固有のプレフィックス(9Router はnr_、Mux はmx_)を付けてキーを生成します。- AES-256-GCM(プロバイダー認証情報と同じ暗号方式)を使用して、保存時に暗号化します。
- プロセス生成時に復号し、環境変数として注入します —
9Router では
NINEROUTER_API_KEY、Mux ではMUX_SERVER_AUTH_TOKENを使用します (CLI フラグにはしないため、トークンがps/プロセス一覧に表示されることはありません)。 - 平文のキーを HTTP レスポンスで返すことはありません。
CLIProxyAPI には、プロセス生成時に専用のデータプレーンキーが注入されます
(needsApiKey: true — アダプターに対するモデル同期に使用されます)。
SSRF 対策
Section titled “SSRF 対策”リバース HTTP プロキシ(/dashboard/.../embed/[...path])の転送先は、
http://127.0.0.1:{port} のみにハードコードされています。非ループバックの宛先への
リダイレクトには決して従いません。ssrf-req-filter ライブラリを使用し、
ループバック範囲外に名前解決されるアップストリーム URL をすべて拒否します。
シェルの安全性(厳格ルール #13)
Section titled “シェルの安全性(厳格ルール #13)”npm install は execFile('npm', ['install', pkg, '--prefix', dir]) で呼び出されます —
テンプレートリテラルやシェルを使用せず、外部パスをコマンド文字列に補間することもありません。
実行時の値(ポート、API キー)は、子プロセスの env オブジェクトを介して渡されます。
エラーのサニタイズ(厳格ルール #12)
Section titled “エラーのサニタイズ(厳格ルール #12)”/api/services/* からのすべてのエラーレスポンスは、buildErrorBody() または
sanitizeErrorMessage() を経由します。生の err.stack および err.message が、
呼び出し元へそのまま返されることはありません。
6. 新しい組み込みサービスの追加
Section titled “6. 新しい組み込みサービスの追加”次の 8 ステップに従ってください。正規のリファレンスとして、
src/lib/services/installers/ および src/app/api/services/ にある既存の実装を参照してください。
ステップ 1 — インストーラーの作成
Section titled “ステップ 1 — インストーラーの作成”ninerouter.ts をモデルとして、src/lib/services/installers/{name}.ts を作成します。
export const NAME_PACKAGE = "your-npm-package";export const NAME_DEFAULT_PORT = 20132; // 空いているポートを選択
export async function install(version = "latest"): Promise<InstallResult> { ... }export async function update(version = "latest"): Promise<InstallResult> { ... }export async function uninstall(): Promise<void> { ... }export function resolveSpawnArgs(apiKey: string, port: number): SpawnArgs { ... }export async function getInstalledVersion(): Promise<string | null> { ... }export async function getLatestVersion(): Promise<string | null> { ... }installers/utils.ts の runNpm(['install', NAME_PACKAGE, '--prefix', dir]) を使用してください。
execSync やシェル補間は決して使用しないでください。
ステップ 2 — ブートストラップへの登録
Section titled “ステップ 2 — ブートストラップへの登録”src/lib/services/bootstrap.ts の SERVICES 配列に ServiceEntry を追加します。
{ tool: "myservice", port: NAME_DEFAULT_PORT, healthPath: "/health", healthIntervalMs: 5_000, stopTimeoutMs: 15_000, logsBufferBytes: 5_242_880, needsApiKey: true, // API キーが不要な場合は false}cfg.tool === "myservice" を処理するように buildSpawnArgsFactory() を拡張します。
プラガブルなプロバイダープラグイン契約(フェーズ 1、#7333)
Section titled “プラガブルなプロバイダープラグイン契約(フェーズ 1、#7333)”src/lib/services/providerPlugins/ では ServiceProviderPlugin 契約が導入されています。
これは、同じバックエンドの構成を無関係な 2 つのファイルで個別に表現する代わりに、
バックエンドの bootstrap.ts にある ServiceEntry フィールドと、
serviceBackends.ts のマニフェストテンプレートフィールドを 1 つのオブジェクトにまとめるものです。
現時点では、移行済みなのは 9router のみです — bootstrap.ts は
getServiceProviderPlugin("9router")(src/lib/services/providerPlugins/registry.ts)から
SERVICES[] エントリを生成し、プラグインが存在しない場合は起動エラーをスローします。
cliproxy、mux、bifrost は、既存のインライン SERVICES[] リテラルを変更せず引き続き使用します。
open-sse/config/providerPluginManifest.ts には、
SERVICE_BACKEND_MANIFEST_TEMPLATE エントリから適切な形式の
ProviderPluginManifestEntry を構築する追加的な
createServiceBackendManifestEntry(pluginId, template) ヘルパーも追加されました —
これはまだ実際のリクエストパス(generateProviderPluginManifestFromRegistry() にも
/v1/providers/[provider]/models にも)接続されていません。契約が 2 つ目のバックエンドで
実証された後のフォローアップ課題として残されています。
issue #7333 で追跡され、フォローアップ PR に延期されている項目は次のとおりです。
同じレジストリを介した cliproxyapi の移行、mux/bifrost の
ServiceBackendPluginId ユニオンへの一般化、エグゼキューターのルーティングに関する
特殊処理(open-sse/executors/index.ts、
open-sse/handlers/chatCore/executorProxy.ts)のプラグイン契約への統合、および
createServiceBackendManifestEntry() の実際のマニフェスト/モデルのコードパスへの接続です。
ステップ 3 — マイグレーションと DB シードの追加
Section titled “ステップ 3 — マイグレーションと DB シードの追加”src/lib/db/migrations/ のマイグレーションを介して、サービスに対応する行が
version_manager に存在することを確認します。その行は次の内容にしてください。
INSERT OR IGNORE INTO version_manager (tool, status, auto_start, provider_expose)VALUES ('myservice', 'not_installed', 0, 0);ステップ 4 — 7 つの API エンドポイントの作成
Section titled “ステップ 4 — 7 つの API エンドポイントの作成”src/app/api/services/{name}/ 配下に作成します。
_lib.ts getOrInitSupervisor() ヘルパーinstall/route.ts POST — installer.install() を呼び出すstart/route.ts POST — supervisor.start() を呼び出すstop/route.ts POST — supervisor.stop() を呼び出すrestart/route.ts POST — supervisor.restart() を呼び出すupdate/route.ts POST — installer.update() を呼び出すstatus/route.ts GET — ライブステータスと DB ステータスをマージするauto-start/route.ts POST — auto_start フラグを切り替える共有の GET /api/services/[name]/logs ルートはすでに接続済みです。そこへの変更は
不要です。
すべてのエラーレスポンスを createErrorResponse() / buildErrorBody() に委譲してください。
ステップ 5 — LOCAL_ONLY_API_PREFIXES への追加
Section titled “ステップ 5 — LOCAL_ONLY_API_PREFIXES への追加”src/server/authz/routeGuard.ts で、/api/services/ がすでにリストに含まれていることを確認してください。
新しいプレフィックス(例: /api/tools/)を導入する場合は、LOCAL_ONLY_API_PREFIXES と、
プロセスを起動する場合は SPAWN_CAPABLE_PREFIXES の両方に追加してください。
tests/unit/authz/routeGuard.test.ts にテストを追加してください。
ステップ 6 — UI タブの追加
Section titled “ステップ 6 — UI タブの追加”src/app/(dashboard)/dashboard/providers/services/tabs/{Name}ServiceTab.tsx を作成してください。
共有コンポーネントを再利用してください。
ServiceStatusCard— ライブ状態 + ヘルスバッジServiceLifecycleButtons— 開始 / 停止 / 再起動 / 更新ServiceLogsPanel— SSE ログ末尾表示(/api/services/{name}/logsに接続)ApiKeyCard— キーの表示 + ローテーション(needsApiKey: trueの場合)
タブを ServicesPageShell.tsx に登録してください。
ステップ 7 — プロバイダーエントリの追加(サービスがルーティング先の場合)
Section titled “ステップ 7 — プロバイダーエントリの追加(サービスがルーティング先の場合)”組み込みサービスが OpenAI 互換の /v1/chat/completions エンドポイントを公開する場合:
src/shared/constants/providers.tsに、isEmbeddedService: trueを指定したプロバイダーエントリを追加してください。BaseExecutorを拡張するopen-sse/executors/{name}.tsを作成してください。リクエストごとにポートと API キーを再取得してください(コンストラクター内では決してキャッシュしないでください)。スーパーバイザーの状態が"running"でない場合は、503 service_not_runningレスポンスを返してください。open-sse/config/providerRegistry.tsに、サービスプレフィックス付き (例:myservice/sub/model)でモデルを登録してください。modelSync.tsによって最新状態が維持されます。
ステップ 8 — ドキュメント化とテスト
Section titled “ステップ 8 — ドキュメント化とテスト”docs/frameworks/EMBEDDED-SERVICES.md(このファイル)を更新し、§1 の テーブルにサービスを、§4 に新しいエンドポイントを追加してください。tests/unit/services/に単体テスト(ライフサイクル、インストーラー、API 形式)を追加してください。tests/integration/services/に統合テストを追加してください(RUN_SERVICES_INT=1の場合のみ)。- 新しいエンドポイントを
docs/openapi.yamlに追加してください。
7. トラブルシューティング
Section titled “7. トラブルシューティング”サービスが起動しない
Section titled “サービスが起動しない”症状: Startボタンを押すと503が返され、状態が"error"または"starting"のままになります。
チェックリスト:
GET /api/services/{name}/logs(またはダッシュボードのLogsパネル)を確認します。Error: ENOENT、address already in use、Cannot find moduleなどの行を探します。npmがPATHに含まれていることを確認します。OmniRouteを実行しているユーザーアカウントでwhich npmを実行してください。- サービスがインストールされていることを確認します。
GET /api/services/{name}/statusのinstalledVersionを確認してください。nullの場合は、先にインストールを実行します。 DATA_DIR/services/{name}/node_modules/が存在し、空でないことを確認します。- ステータスレスポンスの
lastErrorフィールドで、サニタイズされた終了理由を確認します。
コールドスタートが遅い(runningになるまで10秒超)
Section titled “コールドスタートが遅い(runningになるまで10秒超)”症状: "running"または"error"に移行するまで、長時間にわたって状態が"starting"のままになります。
説明: 9Routerのコールドスタートでは、大規模な依存関係ツリー(DNS、トンネル、MITMモジュール)のインポートが行われます。デフォルトのヘルスチェック間隔は2秒で、supervisorがタイムアウトと判断するまでに3回試行します(ただし、ポーリングは継続します)。
修正方法: healthIntervalMsとwaitForHealthyのタイムアウト(healthIntervalMs * 3)は、bootstrap.tsで設定できます。起動に時間がかかるサービスでは、healthIntervalMsを5000に、stopTimeoutMsを30 000に増やしてください。
ポートの競合(EADDRINUSE)
Section titled “ポートの競合(EADDRINUSE)”症状: ログにaddress already in use :::20130と表示されます。
原因:
- 別のプロセスがすでにポート20130を使用しています。
- 以前の9Routerプロセスが完全に停止していません(ゾンビPID)。
修正方法:
.envのNINEROUTER_PORT環境変数を使用して、デフォルトポートを変更します。- 競合しているプロセスを特定して終了します:
lsof -ti :20130 | xargs kill -9 - ポートは、
bootstrap.tsのportフィールドを使用してサービスごとに設定できます。
注: 9Routerは、OmniRouteのデフォルトポート20128との競合を避けるため、デフォルトでポート20130を使用します。
インストール時にPermission denied(EACCES)が発生する
Section titled “インストール時にPermission denied(EACCES)が発生する”症状: インストールで500が返され、ログにEACCESまたはpermission deniedと表示されます。
原因:
DATA_DIRまたはその親ディレクトリに、OmniRouteプロセスから書き込む権限がありません。- マウントされたボリュームへの書き込み権限がない状態で、rootless Docker内で実行しています。
修正方法:
DATA_DIR(デフォルト:~/.omniroute/)を確認します:ls -la ~/.omniroute/- OmniRouteプロセスの実行ユーザーがディレクトリを所有していることを確認します:
chown -R $USER ~/.omniroute/ - Dockerでは、ボリュームマウントにコンテナユーザー用の適切な権限が設定されていることを確認します。
更新に失敗する(npm installのタイムアウトまたはネットワークエラー)
Section titled “更新に失敗する(npm installのタイムアウトまたはネットワークエラー)”症状: 更新でInstallErrorを伴う500が返され、ログにネットワークタイムアウトが表示されます。
チェックリスト:
- npmレジストリに接続できることを確認します:
npm ping - 企業プロキシの設定を確認します:
npm config get proxy、npm config get https-proxy - インストールを手動で試します:
npm install {package}@latest --prefix ~/.omniroute/services/{name}/ - エアギャップ環境では、tarballを事前にダウンロードし、
npm install /path/to/tarball.tgzを使用します。
起動直後にサービスが"error"状態になる(即時クラッシュ)
Section titled “起動直後にサービスが"error"状態になる(即時クラッシュ)”症状: 状態が5秒未満で"starting"から"error"に移行します。lastErrorには"Fast crash (exited with code 1)"と表示されます。
チェックリスト:
- ログ末尾を十分な行数で確認します:
GET /api/services/{name}/logs?tail=500 - よくある原因は、サービスが必要とする環境変数が設定されていないことです。
- 9Routerの場合は、spawn時に渡されるenvに
NINEROUTER_DISABLE_MITM=trueとNINEROUTER_DISABLE_TUNNEL=trueが含まれていることを確認します(installers/ninerouter.tsのresolveSpawnArgsを参照)。
8. FAQ
Section titled “8. FAQ”Q: 組み込みサービスのエンドポイントを非ループバッククライアントに公開できますか?
いいえ。LOCAL_ONLY 階層は意図的に設定されています(厳格ルール #17)。npm install を実行したり、node プロセスを生成したりできるルートは、非ループバック通信からアクセスできてはなりません。そうしないと、トンネル(Cloudflare、Ngrok、Tailscale)経由で JWT が漏洩した場合、任意のプロセス生成が可能になるためです。/api/services/ には適用除外の特例はありません。/api/mcp/ とは異なり、manage スコープのバイパスリストから除外されています。docs/security/ROUTE_GUARD_TIERS.md を参照してください。
Q: 9Router と CLIProxyAPI は、本番環境やクラウド環境へのデプロイで利用できますか?
はい。どちらのサービスも OmniRoute 自体と同じローカルファーストモデルに従います。これらは同じマシン上で実行され、ループバック経由で通信します。ここでいう「本番環境」とは、リモートのクラウドプロバイダーではなく、OmniRoute がデプロイされている VPS またはローカルサーバーを指します。
Q: スーパーバイザーをデバッグするにはどうすればよいですか?
- SSE ログストリームを追跡します:
curl -N http://localhost:20128/api/services/9router/logs。 - OmniRoute の pino 出力で、
service:supervisor名前空間によりフィルタリングされた構造化ログを確認します。 - DB の行を調査します:
sqlite3 ~/.omniroute/omniroute.db "SELECT * FROM version_manager WHERE tool='9router'"。 GET /api/services/9router/statusを使用すると、現在のライブ状態、PID、ヘルス状態、lastErrorを1回の呼び出しで確認できます。
Q: スーパーバイザーで状態が "running" なのに、health: "degraded" または health: "unknown" と表示されます。問題がありますか?
"degraded" は、ヘルスプローブが 200 以外のレスポンスを返したことを意味します。"unknown" は、まだプローブが完了していないことを意味します(最初のポーリングとの競合)。どちらも起動中は一時的に発生します。"running" になってから healthIntervalMs * 3 ms を超えてもヘルス状態が "degraded" のままの場合、組み込みサービスは実行されていますが、その HTTP API が応答していません。ステータスレスポンスに記載されているポートが正しいか、またサービスが実際にそのポートでリッスンしているかを確認してください。
Q: 完全に再起動せずに 9Router の API キーを変更できますか?
いいえ。API キーは生成時に環境変数を介して 9Router に渡されます。実行中のプロセスでは環境変数を変更できません。POST .../rotate-key は、新しいキーを適用するためにサービスを自動的に停止して再起動します。キーのローテーションは、サービスの stopTimeoutMs(デフォルトは 15 秒)と起動時間を合わせた時間内に反映されます。
Q: リングバッファの上限はどのくらいで、満杯になるとどうなりますか?
各サービスには専用の 5 MB リングバッファがあります。バッファが満杯になると、新しいログ行のための領域を確保するために、最も古いログ行から削除されます。SSE の snapshot イベントは、tail の上限内で最新のログ行を返します。DB の行に logsBufferPath が設定されていない限り、ログはディスクに永続化されません。
docs/security/ROUTE_GUARD_TIERS.md— LOCAL_ONLY 階層の詳細docs/architecture/CODEBASE_DOCUMENTATION.md— §3.2 組み込みサービスのモジュールマッピングdocs/architecture/ARCHITECTURE.md— システムレベルのコンテキストdocs/openapi.yaml— 機械可読なエンドポイント定義CLAUDE.md§「新しい組み込みサービスの追加」— クイックリファレンス用チェックリスト
HagiCode
HagiCode は構造化ワークフロー、マルチエージェント実行、Hero Dungeon ビューを備えたエージェント型コーディングワークスペースです。
よりスマートで速く、楽しいエージェント型ワークフローで、使いやすいソフトウェアを形にします。

- Smart構造化ワークフローは意図をアイデアから変更のリリースまで実行可能な道筋にします。
- Efficientマルチエージェントのワークフローで調査、実装、レビューを並行して進めます。
- FunHero Dungeon により長時間のコーディングを視覚的で協力的な体験にします。