コンテンツにスキップ

mod パッケージ

mod は ubichill の配布・信頼の単位。1 つの mod が複数の Component(それぞれ 1 Worker)を束ねる。 ユーザーは mod 単位で信頼を判断し、権限も mod 単位で記憶される。ワールドは複数の mod を URL から読み込んで構成する(→ WORLD_AS_CODE.md)。

関連: ARCHITECTURE.md(実行モデル)/ CAPABILITIES.md(権限)/ API.md(Ubi.* SDK)


mod のソースは mods/<name>/mod.json で定義する。形式は Zod で検証される(唯一の真実源は packages/shared/src/schemas/mod.schema.ts の ModManifestSchema)。ubi mod build(build-workers.mjs)がこれを読み、Worker を esbuild で バンドルして runtime 用の versioned manifest(manifest.json)を出力する。

{
"id": "video-player", // mod ID(配布単位・権限記憶のキー)
"name": "VideoPlayer", // 表示名(任意)
"version": "2.1.0", // SemVer
"components": {
"screen": {
"src": "./src/screen.worker.tsx", // Worker エントリ(ビルド時。runtime では workerUrl に変換)
"watchEntityTypes": [], // 同期監視する Component 型
"watchScope": "entity", // entity | subtree | parent | world
"mediaTargets": ["main"], // メディア描画ターゲット(任意)
"defaultTransform": { "x": 0, "y": 0, "z": 2, "w": 640, "h": 360 }
}
}
}
フィールド 必須 説明
id ✓ mod ID。Component は id:componentKey(例 video-player:screen)で参照される
name 表示名
version ✓ SemVer
components Component 名 → 定義のマップ

Component 定義(ComponentManifestEntry)の主なフィールド:

フィールド 説明
src Worker エントリ(ビルド入力)。runtime manifest では workerUrl に変換される。無ければデータ専用 Component
watchScope 同期の可視範囲。entity(自身のみ)/ subtree(既定・自身+子孫)/ parent(自身+祖先)/ world(全体)
watchEntityTypes 起動時に同期反映する Component 型
mediaTargets / canvasTargets メディア/キャンバス描画ターゲット
defaultTransform 配置の既定値(x/y/z/w/h/rotation)
dataFields エディタ Inspector 用の編集可能フィールド定義
displayName / thumbnail エディタでの表示名・プレビュー画像
capabilities 通常は不要(自動生成)。手書きすると自動生成結果への補完になる

形式は JSON で確定。 URL で動的ロードされ、ゼロトラストの信頼境界で JSON.parse される。 依存ゼロ・YAML 特有の地雷(アンカー爆弾・暗黙の型強制)を境界に持ち込まないため。

mods/<id>/assets/ に置いたファイル(WASM・データ・zip など)は、ビルドで versioned ディレクトリへコピーされ、 各ファイルの sha256 が manifest の assetIntegrity に載る。manifest は lock で固定されるので、アセットの中身も lock に連なって固定される。Worker からは Ubi.asset.bytes(path) / Ubi.asset.wasm(path) で読み、Host が integrity と照合してから渡す(→ MOD_RUNTIME.md)。サンプルは mods/wasm-demo。


mod 開発者は権限を宣言しない。使用している Ubi.* API からビルド時に自動生成され、実際の許可は ユーザーが mod 読み込み時に一括承認する。全一覧・危険度・同意モデルは CAPABILITIES.md を参照。


mod は URL で動的に読み込まれるため、バージョン番号を固定するだけでは不十分。 同じ version でも、URL のドメインが乗っ取られたり CDN 上の内容が差し替えられれば 「同じバージョンの別物(悪意あるコード)」が配信され得る。

そこで package-lock / SRI 相当の内容ハッシュで固定する(構想):

  • ワールドが依存する各 mod を { id, version, url, integrity: "sha384-…" } としてロックファイルに記録する。
  • 読み込み時、Host は取得した mod の内容ハッシュを計算し、ロック値と照合する。 不一致なら実行しない(改竄・乗っ取りを検出)。
  • version は SemVer。runtime アセットはバージョン付きパス(/mods/<id>/v<version>/…)で 固定配信されるが、その内容もハッシュで縛ることで「バージョンは同じだが中身が変わった」を弾く。
  • lock の更新は明示的な再ロック時のみ。それ以外は承認済みの内容と bit 単位で同一であることが保証される。

lock が守るもの / 守らないもの(役割の分離):

担当
内容整合性(integrity): 承認したものと同一のコードか ← lock(本節)
信頼性(trust): そもそも承認してよい mod か ← capability 同意(CAPABILITIES.md)

lock は「最初から悪意ある mod」は防げない(それは capability 同意とレビューの領分)。 lock が防ぐのは「一度承認した mod が、後から URL 乗っ取り・CDN 改竄ですり替わる」こと。 ロックファイルはワールドが依存する mod 群を固定するもので、world owner が所有する (→ WORLD_AS_CODE.md)。

現状 mod は自己完結で、mod → mod の依存は持たない。Component 間の連携は Ubi.event の疎結合で行い、 相手が居なければ優雅に劣化する(ハードな依存解決はしない)。相手は参照フィールド(dataFields の entityRef に component: '<型>')で宣言し、ワールド作者が Inspector で配線する(→ API.md の 「Component の参照」)。階層の形(同じ親の下に並べる、など)を作者に暗黙に求めない。 将来 mod → mod 依存を入れる場合も、ゼロトラストのため capability は依存間で継承しない (各 mod は独立ロード+各自の承認+各自の integrity 照合)。

mod は SDK 経由で Host と通信する。SDK と Host は独立更新されるため、初期化時に PROTOCOL_VERSION を突き合わせて非互換を検出する (→ CAPABILITIES.md)。


mod の配布物には作者アカウントの署名が必須。Host は、署名で作者を確認できた mod だけを実行する。 鍵・作者アカウント・公開環境・取り消しはワールドの署名と同じ仕組みを使う(→ author-publishing.md)。

担当
承認したものと同一のコードか lock(ワールドが固定)
そのコードをだれが出したか 作者署名(本節)
何をしてよいか capability の同意
  • 署名するもの: v<version>/lock.json のうち実行内容を決める部分(id・version・manifest の hash・各 worker の hash と 権限の上限)。置き場所(URL)は含めないので、同じ配布物をどこに置いても同じ署名で確かめられる。
  • 置き場所: lock.json の兄弟の v<version>/lock.sig.json。
  • 署名の方法: ubichill build のあとに ubichill publish <ビルド出力>(ubichill login したアカウント。CI は Secret UBICHILL_CREDENTIALS)。mod はサーバーへ送らない。署名済みの出力をそのまま GitHub Pages などへ置く。
  • Host の確認(ブラウザで行う): ワールドの lock と一致した mod について lock.sig.json を取得し、ブラウザで署名を照合して、 作者のサーバーの WebFinger から鍵の一覧を直接取って作者を確かめる。Host のオリジンのサーバーには頼らないので、 SNS backend の無い単体モードでも同じ規則で動く。規則(鍵一覧の期限・取り消し・取得の上限・作者のサーバーが止まったときの猶予)は サーバーがサービストークンを発行するときと同じコード(@ubichill/shared の authorKeyDirectory)で、ブラウザは結果を localStorage に持つ。 署名が無い・lock と合わない・作者の鍵ではない・鍵が取り消されている・作者をいま確認できない mod は実行しない。
  • 表示: 権限の確認画面と World Editor の mod 一覧(選んでいる版)に、確認できた作者(@ID@ドメイン)を出す。名乗っているだけの作者は出さない。 実行しなかった mod は、理由つきで画面に出す(いまは確認できない場合は再試行できる)。
  • 権限の許可は「作者+mod の ID」に付く: 利用者が許可・拒否した記録(capability・外部通信のドメイン)は、確認できた作者と mod の ID の組に 結び付ける。同じ作者の新しい版は許可を引き継ぎ、同じ ID を名乗る別の作者の mod は引き継がない。 作者署名が必須になる前の記録(ID だけ)は、どの mod にも使われない(もう一度確認する)。
  • 取り消しが届くまで: 作者が公開環境を取り消すと、作者のサーバーが動いていれば 1 時間以内(鍵一覧を取り直す間隔)に各ブラウザへ届き、 さらに確認できた結果を 5 分だけ使い回すので、それを過ぎた読み込み(ワールドの移動・入り直し)から実行されなくなる。実行中の mod は止めない。
  • 作者のサーバーへの問い合わせ: 利用者のブラウザが作者のサーバー(WebFinger・鍵一覧)に直接問い合わせる。mod の配布物を作者の置き場所から 取るのと同じく、作者のサーバーには利用者の IP が見える。作者のサーバーは Access-Control-Allow-Origin: * で返す必要がある (Ubichill のサーバーは返している)。
npm i ubichill && npm i -D @ubichill/cli # SDK と、ubichill コマンド(Go のインスタンスサーバー込み)
npx ubichill dev # 手元の mod を本番と同じ Go で動かす(http://localhost:3333/)
npx ubichill login # 1 回だけ(CI は ubichill ci create で作った UBICHILL_CREDENTIALS)
npx ubichill build
npx ubichill publish dist # 単体 mod の出力。複数 mod なら dist/mods
npx ubichill verify --dist-dir=dist --require-signatures

内容が変わるビルドをすると、以前の内容に付けた lock.sig.json は消える(署名し直す)。同じ内容の再ビルドでは残る。

publish に公開先の URL(Host がこの mod を取りに来る基点。ワールドの依存の source.url と同じ値)を渡すと、署名したときに 作者アカウントのサーバーへ登録され、ユーザーページの「公開している mod」に出る。--public-url を省くと package.json の homepage を使う。サーバーは公開先から lock.json と lock.sig.json を取り、本人の署名付きで配られていると確かめたものだけを出す (公開前・別の物に置き換えた・鍵を取り消した場合は出ない)。設定の「ワールド」→「外部に置いた作品」で、作品ごとの状態 (公開済み・公開待ち・取得失敗・署名を確認できない)を見て再確認でき、登録を外せる。API トークン(CLI・CI)で登録するときは、 そのトークンの公開環境の鍵で署名した作品に限る(ワールドの公開と同じ)。

npx ubichill publish dist --public-url=https://yourname.github.io/your-mod

開発用の Host(pnpm dev、または VITE_ENVIRONMENT=development でビルドしたプレビュー)は、自分のオリジンから配る mod に 限って、署名ファイルの無い mod を「署名なし(開発)」として動かす。署名ファイルがあれば開発用の Host でも必ず確かめる。 本番としてビルドした Host(VITE_ENVIRONMENT を指定しないビルドを含む)に例外は無い。

自分でビルドしたイメージに自分の mod を同梱して本番運用するときは、自分のサーバーのアカウントで署名する (ubichill login --server=<自分のサーバー> → ubichill publish packages/frontend/public/mods → イメージをビルド。 --build-arg REQUIRE_MOD_SIGNATURES=true で、署名の無い mod があればビルドを止められる)。 公式イメージに同梱の mod は公式アカウント([email protected])の署名なので、サーバーが ubichill.com に 問い合わせられる必要がある(確認できた結果はサーバーに保存される)。


mod は URL ベースで配布する(GitHub Pages / 任意 CDN)。ワールドは spec.dependencies[].source にその URL を書く。

mods:
- name: pen
src: https://yourname.github.io/pen-mod # GitHub Pages
- name: custom
src: https://cdn.example.com/my-mod # 任意ホスティング

src から versioned manifest と Worker JS を取得する。取得時に Content-Type 検証と、 ロックファイルの integrity ハッシュ照合を行う(→ バージョンと lock。実装予定)。 これにより、同一 URL・同一バージョンでも内容がすり替わっていれば検出できる。