HyperGen

iframeブートストラップドキュメント

HyperGenのサンドボックス化されたiframeに読み込まれなければならない (MUST) 完全なHTMLドキュメント。HTMX、SSE拡張、postMessageブリッジ、基本スタイルを含む。

iframeブートストラップドキュメント

ブートストラップドキュメントは、サンドボックス化されたiframeに読み込まれる完全なHTMLページです。エージェント生成HTMLのランタイム環境を提供します: インタラクティビティのためのHTMX、ストリーミングのためのSSE拡張、ホスト通信のためのpostMessageブリッジ、CSS変数サポート付きの基本スタイル。

必須要素

適合するブートストラップドキュメントは以下のすべてを含まなければなりません (MUST):

要素目的
HTMXスクリプトインタラクティビティのためのhx-*属性を処理
HTMX SSE拡張スクリプトストリーミングのためのsse-connectsse-swapを有効化
SSEルート要素(#hg-rootSSEエンドポイントに接続し、ストリーミングされたフラグメントを受信するコンテナ
postMessageブリッジホストからのhg:themehg:destroyメッセージを処理
ResizeObserverhg:resizeメッセージ経由でホストにコンテンツサイズの変更を通知
接続状態スクリプト<html>hg-connected / hg-disconnected CSSクラスを切り替え
基本スタイルリセット(box-sizingmargin: 0)、CSS変数デフォルト、ローディングインジケータースタイル

最小限の適合ブートストラップ

以下はすべての要件を満たす最小限のHTMLドキュメントです。実装はこれに追加してもよいですが (MAY)、必須要素を削除してはなりません (MUST NOT)。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <script src="https://unpkg.com/htmx.org@2"></script>
  <script src="https://unpkg.com/htmx-ext-sse@2"></script>
  <style>
    :root {
      /* テーマ変数はホストからpostMessage経由で注入 */
    }
    *, *::before, *::after { box-sizing: border-box; }
    body {
      margin: 0;
      font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
      line-height: 1.5;
      color: var(--hg-text, #1a1a1a);
      background: var(--hg-background, transparent);
    }
    .hg-indicator { display: none; }
    .htmx-request .hg-indicator,
    .htmx-request.hg-indicator { display: inline-block; }
    .hg-disconnected body { opacity: 0.7; }
  </style>
</head>
<body>
  <div id="hg-root"
       hx-ext="sse"
       sse-connect="/api/agent/stream"
       sse-swap="message"
       hx-swap="innerHTML">
  </div>

  <!-- 接続状態 -->
  <script>
    (function() {
      var root = document.documentElement;
      function setConnected(v) {
        if (v) {
          root.classList.add("hg-connected");
          root.classList.remove("hg-disconnected");
        } else {
          root.classList.remove("hg-connected");
          root.classList.add("hg-disconnected");
        }
      }
      setConnected(false);
      document.body.addEventListener("htmx:sseOpen", function() {
        setConnected(true);
      });
      document.body.addEventListener("htmx:sseError", function() {
        setConnected(false);
      });
      document.body.addEventListener("htmx:sseClose", function() {
        setConnected(false);
      });
    })();
  </script>

  <!-- postMessageブリッジ -->
  <script>
    (function() {
      var hostOrigin = "*";

      window.addEventListener("message", function(e) {
        if (hostOrigin !== "*" && e.origin !== hostOrigin) return;
        var msg = e.data;
        if (!msg || typeof msg.type !== "string") return;

        switch (msg.type) {
          case "hg:theme":
            if (msg.vars && typeof msg.vars === "object") {
              Object.keys(msg.vars).forEach(function(key) {
                document.documentElement.style.setProperty(key, msg.vars[key]);
              });
            }
            break;
          case "hg:destroy":
            var root = document.getElementById("hg-root");
            if (root) {
              root.removeAttribute("sse-connect");
              root.removeAttribute("hx-ext");
              if (typeof htmx !== "undefined") {
                htmx.trigger(root, "htmx:sseClose");
              }
            }
            break;
        }
      });

      // ResizeObserver
      var ro = new ResizeObserver(function() {
        window.parent.postMessage({
          type: "hg:resize",
          height: document.documentElement.scrollHeight,
          width: document.documentElement.scrollWidth
        }, hostOrigin);
      });
      ro.observe(document.body);

      // ナビゲーションブリッジ
      document.addEventListener("click", function(e) {
        var target = e.target;
        while (target && target !== document.body) {
          if (target.getAttribute &&
              target.getAttribute("data-hg-navigate")) {
            e.preventDefault();
            window.parent.postMessage({
              type: "hg:navigate",
              url: target.getAttribute("data-hg-navigate")
            }, hostOrigin);
            return;
          }
          target = target.parentElement;
        }
      });

      // データブリッジ
      document.addEventListener("hg:data", function(e) {
        window.parent.postMessage({
          type: "hg:data",
          payload: e.detail
        }, hostOrigin);
      });
    })();
  </script>
</body>
</html>

Sandbox属性の要件

iframe要素は以下のパーミッションでsandbox属性を使用しなければなりません (MUST):

<iframe sandbox="allow-scripts allow-same-origin allow-forms"
        src="about:blank"
        style="width: 100%; border: none;">
</iframe>

必須パーミッション

パーミッション理由
allow-scriptsHTMXがhx-*属性を処理しSSE接続を管理するためにJavaScriptが必要。
allow-same-originHTMXがエージェントサーバーにHTTPリクエスト(GET、POST)を行う必要がある。same-originなしではXHRとfetchがブロックされる。
allow-formsHTMXのフォーム送信(<form>要素でのhx-post)にフォーム送信機能が必要。

明示的に拒否されるパーミッション

以下のパーミッションは、ホストアプリケーションに文書化された具体的な必要がない限り、付与してはなりません (MUST NOT):

パーミッション付与時のリスク
allow-top-navigationiframeがページ全体をリダイレクトできる。
allow-popupsiframeが新しいブラウザウィンドウを開ける。
allow-modalsalert()confirm()prompt()がホストアプリケーションをブロックできる。
allow-pointer-lockiframeがカーソルをキャプチャできる。
allow-downloadsiframeがファイルダウンロードをトリガーできる。

ホストアプリケーションは、必須の3つを超えるパーミッションをsandbox属性に追加してもよいです (MAY)。追加の各パーミッションはセキュリティ境界を弱め、文書化すべきです (SHOULD)。

Content Security Policy

ホストアプリケーションは、iframeの機能を制限するContent-Security-Policyでブートストラップドキュメントを提供すべきです (SHOULD)。

推奨CSP

default-src 'none';
script-src 'self' 'unsafe-inline' https://unpkg.com/htmx.org https://unpkg.com/htmx-ext-sse;
style-src 'self' 'unsafe-inline';
connect-src <agent-server-origin>;
img-src 'self' data: https:;
font-src 'self' data:;
ディレクティブ目的
default-src 'none'明示的に許可されていないものをすべて拒否。
script-srcインラインスクリプト(ブリッジに必要)と信頼されたCDNからのHTMXを許可。
style-srcインラインスタイル(テーマ注入とエージェント生成<style>ブロックに必要)を許可。
connect-srcHTTP/SSE接続を既知のエージェントサーバーオリジンに制限。データ漏洩に対する主要な防御。<agent-server-origin>を実際のオリジンに置換。
img-src同一オリジン、data URI、HTTPSからの画像を許可。
font-src同一オリジンとdata URIからのフォントを許可。

ブートストラップドキュメントがインラインスクリプトを使用し、エージェントがインラインスタイルを生成するため、script-srcstyle-srcの両方に'unsafe-inline'が必要です。実装がブートストラップスクリプトにnonce やハッシュを使用できる場合、'unsafe-inline'よりもそれらを推奨すべきです (SHOULD)。

HTMXバージョン要件

  • ブートストラップドキュメントはHTMX バージョン2.x(2.0.0以降)を読み込まなければなりません (MUST)。
  • ブートストラップドキュメントはHTMX SSE拡張 バージョン2.x を読み込まなければなりません (MUST)。
  • 両スクリプトはCDN(unpkg、cdnjs、jsdelivr)から読み込んでも、ローカルで提供してもよいです (MAY)。
  • HTMXスクリプトはSSE拡張スクリプトの前に読み込まなければなりません (MUST)(拡張はHTMXコアに依存)。

CDN URL

リファレンス実装はunpkgでメジャーバージョンピニングを使用:

<script src="https://unpkg.com/htmx.org@2"></script>
<script src="https://unpkg.com/htmx-ext-sse@2"></script>

本番デプロイメントでは特定のバージョンにピン留めすべきであり (SHOULD)(例: htmx.org@2.0.4)、追加のセキュリティのためにSubresource Integrity (SRI) ハッシュを使用してもよいです (MAY)。

接続状態CSSクラス

ブートストラップドキュメントは、SSE接続状態に基づいて<html>要素のCSSクラスを切り替えるスクリプトを含まなければなりません (MUST):

クラス適用タイミング
hg-connectedSSE接続がオープン(htmx:sseOpenイベント受信時)。
hg-disconnectedSSE接続がクローズまたはエラー(htmx:sseErrorまたはhtmx:sseClose受信時)。

初期状態はhg-disconnectedでなければなりません (MUST)(SSE接続が開く前に適用)。

エージェントはこれらのクラスを使用してコンテンツを条件付きでスタイルしてもよいです (MAY):

.hg-disconnected .status-indicator { color: red; }
.hg-connected .status-indicator { color: green; }

ブートストラップの読み込み

ブートストラップドキュメントは2つの方法でiframeに読み込めます:

1. srcdoc経由(推奨)

ホストがブートストラップHTMLをサーバーサイドで生成し、iframeのsrcdoc属性として設定します。追加のHTTPリクエストを回避できます。

iframe.srcdoc = bootstrapHtml;

2. src URL経由

ホストがiframeをブートストラップドキュメントを提供するURLに向けます。リクエストごとにブートストラップを動的に生成する必要がある場合に便利です。

iframe.src = "/api/agent/bootstrap";

srcを使用する場合、サーバーはContent-Type: text/htmlと完全なブートストラップドキュメントで応答しなければなりません (MUST)。

カスタマイズポイント

実装は以下の方法でブートストラップドキュメントをカスタマイズしてもよいです (MAY):

  • sseEndpointsse-connectのURL。エージェントのSSEエンドポイントに設定しなければなりません (MUST)。
  • sseEventNamesse-swapの値。デフォルトは"message"
  • swapStrategy — ルート要素のhx-swapの値。デフォルトは"innerHTML"
  • hostOrigin — postMessage検証に使用されるオリジン。デフォルトは"*"。本番デプロイメントでは実際のホストオリジンに設定すべきです (SHOULD)。
  • themeVars:rootスタイルブロックに設定される初期CSS変数値。
  • extraHead<head>に注入される追加コンテンツ(スタイルシート、メタタグ)。
  • HTMX CDN URL — 代替CDNまたはセルフホストHTMX URL。

ブートストラップドキュメントのパラメーター化されたバージョンについては、リファレンスサーバー実装(hypergen-server.tsbootstrapHtml())を参照してください。

目次