リファレンス

Dialup が提供するモジュール・コンパイル時オプション・HTML 属性・MCP ツール・認証などを一覧します。 モジュール単位の詳細は HexDocs も参照してください。

use Dialup オプション

defmodule MyApp do
  use Application

  use Dialup,
    app_dir: __DIR__ <> "/app",   # 必須
    title: "My App",              # <title> のデフォルト値
    lang: "en",                   # html lang 属性
    check_origin: :conn,          # WebSocket オリジン検証
    auth_accounts: MyApp.Accounts, # 認証 Accounts コンテキスト(省略可)
    plugs: [{Dialup.Auth.Plug, accounts: MyApp.Accounts}],  # 認証 Plug
    agent_session: false,         # headless MCP(本番は auth で保護)
    session_store: :memory        # :memory / :ets

  @impl Application
  def start(_type, _args) do
    children = [{Dialup, app: __MODULE__, port: 4000}]
    Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
  end
end
オプション説明
app_dir page.ex / layout.ex を配置するディレクトリ(必須)
title <title> のデフォルト値(デフォルト: "Dialup App")
lang html lang 属性の値(デフォルト: "en")
check_origin WebSocket オリジン検証。:conn(デフォルト)/ false / ホストのリスト
auth_accounts 認証 Accounts コンテキスト(省略可)。設定時は MCP guest grant が最小 capability に制限される
plugs カスタム Plug パイプライン(認証・CORS など)
agent_session POST /_dialup/agent-session を有効化(デフォルト: false。本番は auth plug で保護)
session_store セッション状態の保存先。:memory(デフォルト)または :ets

use Dialup.Layout

defmodule Dialup.App.Layout do
  use Dialup.Layout

  # 接続時に一度だけ呼ばれる(省略可)
  def mount(session) do
    user = Repo.get(User, get_user_id_from_cookie())
    {:ok, Map.put(session, :current_user, user)}
  end

  def render(assigns) do
    ~H"""
    <nav>{@current_user.name}</nav>
    <main>{raw(@inner_content)}</main>
    """
  end
end

use Dialup.Page

defmodule Dialup.App.Users.Id.Page do
  use Dialup.Page

  def page_title(assigns), do: "#{assigns.user.name} | MyApp"

  def mount(%{"id" => id}, assigns) do
    {:ok, %{user: Users.get!(id)}}
  end

  def handle_event("follow", _, assigns) do
    {:update, Map.update!(assigns, :user, &Users.follow/1)}
  end

  def render(assigns) do
    ~H"""
    <h1>{@user.name}</h1>
    <button ws-event="follow">フォロー</button>
    """
  end
end
page_title/1 は省略可能。assigns を受け取りタイトル文字列を返します。 nil を返すと use Dialuptitle 設定値が使われます。

handle_event / handle_info の返り値

# 状態のみ更新、再描画なし(高頻度イベント向け)
def handle_event("draft", value, assigns) do
  {:noreply, Map.put(assigns, :draft, value)}
end

# 全体を再描画
def handle_event("submit", _value, assigns) do
  {:update, Map.put(assigns, :submitted, true)}
end

# 指定した id の要素のみ更新(効率的)
defp render_counter(assigns) do
  ~H\"<span id=\"counter\">{@count}</span>\"
end

def handle_event("inc", _value, assigns) do
  new = Map.update!(assigns, :count, &(&1 + 1))
  {:patch, "counter", render_counter(new), new}
end

# 別ページへリダイレクト(handle_event)
def handle_event("logout", _value, assigns) do
  {:redirect, "/login", assigns}
end

# mount/2 からも {:redirect, path} 可能(認証ガード)
def mount(_params, session) do
  if session[:current_user], do: {:ok, session}, else: {:redirect, "/log_in"}
end

# JS フックを呼び出す(全体再描画も行う)
def handle_event("save", params, assigns) do
  {:ok, item} = Items.create(params)
  {:push_event, "show_toast", %{message: "保存しました"}, Map.put(assigns, :item, item)}
end
返り値説明
{:noreply, assigns}再描画なし。状態のみ更新
{:update, assigns}全体を再描画
{:patch, id, html, assigns}指定 id の要素のみ更新
{:redirect, path, assigns}別ページへ遷移(session 保持)
{:push_event, name, payload, assigns}JS フック関数を呼び出す(全体再描画も行う)

handle_info / subscribe

def mount(_params, assigns) do
  subscribe(MyApp.PubSub, "room:lobby")  # 自動 unsubscribe 付き
  Process.send_after(self(), :tick, 1_000)
  {:ok, %{messages: [], count: 0}}
end

def handle_info({:new_message, msg}, assigns) do
  {:update, Map.update!(assigns, :messages, &[msg | &1])}
end

def handle_info(:tick, assigns) do
  Process.send_after(self(), :tick, 1_000)
  {:update, Map.update!(assigns, :count, &(&1 + 1))}
end

ヘルパー関数

use Dialup.Page で自動的に import されます。

def mount(params, assigns) do
  {:ok, assigns
  |> set_default(%{errors: [], page: 1})   # 未設定キーのみ初期値を設定
  |> overwrite(%{user_id: params["id"]})}  # 既存キーも含めて上書き
end
関数既存キー用途
overwrite(assigns, map)上書き新データで状態を更新
set_default(assigns, map)保持初期値の設定(初回のみ)

HTML 属性

属性対象説明
ws-href="/path" 任意 クリック時に SPA ナビゲーション
ws-event="name" 任意 クリック時に handle_event/3 を呼ぶ
ws-value="val" ws-event と組み合わせ handle_event の第二引数として渡す値
ws-submit="name" form 送信時にフォームデータをオブジェクトとして渡す
ws-change="name" input / textarea / select 入力のたびに現在の値を送信
ws-debounce="300" ws-change と組み合わせ 指定 ms 間入力がなかった場合のみ送信
ws-hook="HookName" 任意(id 必須) 外部 JS ライブラリのライフサイクル管理(mounted / updated / destroyed)

ws-hook と push_event

# HTML 側(id 属性は必須)
<div id="map" ws-hook="MapHook" data-lat={@lat} data-lng={@lng}></div>

# lib/root.html.heex で登録
Dialup.connect({
  hooks: {
    MapHook: {
      mounted(el) {
        const lat = parseFloat(el.dataset.lat);
        const lng = parseFloat(el.dataset.lng);
        el._map = L.map(el).setView([lat, lng], 13);
      },
      updated(el) {
        el._map.setView([parseFloat(el.dataset.lat), parseFloat(el.dataset.lng)]);
      },
      destroyed(el) {
        el._map?.remove();
      }
    },
    // push_event ハンドラは関数で登録
    show_toast: ({ message }) => {
      document.getElementById("toast").textContent = message;
    }
  }
});
push_eventws-hook
起点サーバーが能動的に呼ぶDOM の変化が自動で呼ぶ
登録形式関数 name: (payload) => {}オブジェクト name: { mounted, updated, destroyed }
用途トースト・モーダル・スクロールなど一発イベント地図・チャート・リッチエディタなど初期化が必要なもの

HTTP MCP tools

エージェントに公開する操作は、通常の ws-event ではなく <.dialup_action> または declare_action/1 で宣言します。 人間の WebSocket イベントとエージェントの tools/call は同じ UserSessionProcess に直列化されます。 ナビゲーションは <.dialup_action navigate="/path"> として宣言したリンクだけが tool になります。意味のある領域は <.dialup_region> / declare_region/1 で安定した名前を与えます。

Action モード

<.dialup_action> は次の 4 モードのいずれか 1 つだけを指定します。 tools/list の各 tool には _meta.mode が付きます。

モード属性サーバー側ルーティング
command command={{Context, :cmd}} Commanded コマンドを組み立て → Context.dispatch/1 → ページ remount
set set={%{key: value}} レンダリング時の map を assigns にマージ(UI 専用 state)
navigate navigate="/path" 宣言済みパスへ遷移
action(legacy) name={:event} handle_event/3 に委譲

Commanded / CQRS を使う場合は command モードを優先し、 use Dialup.CommandedContextmix dialup.gen.aggregate でスキャフォールドを生成します。 available={...} はコンパイル時に __available__/2 を導出し、 人間の UI とエージェントの tools/list で同じ条件を共有します。 詳細は Hex ガイド Building agent-native applications を参照してください。

defmodule Dialup.App.Invoice.Page do
  use Dialup.Page

  alias MyApp.Ordering

  def mount(_params, assigns) do
    {:ok, assigns |> set_default(%{items: [], order_id: "ord-1", details_open: false})}
  end

  def agent_state(assigns), do: %{items: assigns.items, details_open: assigns.details_open}

  def render(assigns) do
    ~H"""
    <.dialup_region name={:items} role="list" desc="請求書の明細" data={@items}>
      <ul><li :for={item <- @items}>{item.sku} x {item.qty}</li></ul>
    </.dialup_region>

    <.dialup_action
      command={{Ordering, :add_item}}
      desc="明細を追加する"
      params={%{sku: :string, qty: {:integer, default: 1}}}
      bind={%{order_id: @order_id}}
      errors={%{too_many: "明細が多すぎます"}}
      available={length(@items) < 20}
      sku="SKU-1"
      qty="1"
    >
      追加
    </.dialup_action>

    <.dialup_action
      name={:toggle_details}
      desc="詳細パネルを開閉"
      params={%{}}
      set={%{details_open: !@details_open}}
    >
      詳細
    </.dialup_action>
    """
  end
end

フォーム送信や段階的移行が必要な操作は legacy モードで handle_event/3 に残せます。

# legacy モード(handle_event/3 に委譲)
<.dialup_action
  name={:archive}
  desc="請求書をアーカイブする"
  params={%{}}
  available={@status == :draft}
>
  アーカイブ
</.dialup_action>

def handle_event("archive", _value, assigns) do
  {:update, Map.put(assigns, :status, :archived)}
end

HTTP MCP セッション API

ライブセッションを操作する bearer token の取得、agent-first 起動、browser handoff の完了 エンドポイントです。join token は finalize-join 成功時に消費されます (URL を開いただけでは完了しません)。

# Chat agent entry(人間が AI に貼る URL)
GET /agent?t=TAB_ID

# 外部 AI が live タブへ接続(pending token → ブラウザ承認)
POST /_dialup/agent-connect?t=TAB_ID

# 既存タブから即 active token(same-browser JS のみ)
POST /_dialup/agent-handoff?tab_id=TAB_ID
Cookie: dialup_session=SESSION_ID

# 人間タブなしで agent-first 開始
POST /_dialup/agent-session
Content-Type: application/json
{"path":"/invoices"}

# browser handoff 完了(dialup.js が呼ぶ)
POST /_dialup/finalize-join?tab_id=TAB_ID&nonce=NONCE

# JSON-RPC(Bearer token)
POST /agent
Authorization: Bearer TOKEN
エンドポイント用途
GET /agent?t=TAB_ID Chat agent エントリ(人間が AI に貼る URL)
POST /_dialup/agent-connect 外部 AI が live タブへ接続(pending token → ブラウザ承認)
POST /_dialup/agent-handoff same-browser から即 active token を発行
POST /_dialup/agent-session headless セッションを起動(browser 未接続)
POST /_dialup/finalize-join browser handoff 完了(cookie 設定 + join token 消費)
POST /agent JSON-RPC(tools/listtools/call など)。Bearer token 必須

ページモジュールのオプション

モジュール属性説明
@layout false 全ての layout.ex を無効化(全画面ページ・ログイン画面など)
@static true WebSocket 接続なしで表示(純粋に静的なページ)

認証

Dialup は HTTP 認証(クッキー + CSRF)と MCP 権限を同一ライブセッションに結びつけます。 ユーザ DB はアプリ側(Ecto)に置き、フレームワークは bridge と generator を提供します。

2 種類のセッション

概念 Cookie 用途
Connection session dialup_session 1 タブ = 1 UserSessionProcess
Auth session _dialup_user_token DB 上の opaque トークン → current_user

Quick start

mix dialup.gen.auth Accounts User users

Application モジュールに Accounts と Auth Plug を配線します。

use Dialup,
  app_dir: __DIR__ <> "/app",
  auth_accounts: MyApp.Accounts,
  plugs: [{Dialup.Auth.Plug, accounts: MyApp.Accounts}]

ページ保護(mount redirect)

未ログイン時に別パスへ送るには mount/2 から {:redirect, "/log_in"} を返します。HTTP GET・WebSocket 遷移・ /.well-known/dialup-agent の discovery が同じ解決経路を使います。

def mount(_params, session) do
  if session[:current_user] do
    {:ok, session}
  else
    {:redirect, "/log_in"}
  end
end

シーケンス図

ログイン・エージェント先行・ログアウト同期のフローは、リポジトリの guides/authentication.md#sequence-diagrams に Mermaid 形式でまとめています (Hex ガイドでも同じ内容を参照できます)。

  1. Human login — HTTP POST /log_inbroadcast_auth_userdialup_session ローテーション → WS 再接続で current_user を layout session に注入
  2. Agent-first — guest grant → issue_browser_url → 人間が /log_in → live セッション昇格 → 旧 guest トークンは auth_fingerprint で昇格しないPOST /_dialup/agent-connect?t=<tab_id> で再接続
  3. Logout sync — pre-rotate id へ broadcast → DB トークン失効 → cookie ローテーション → MCP grant ダウングレード

Agent / エージェント

  • Guest[:read_scene, :issue_browser_url]auth_accounts 設定時)
  • Logged inagent_grant/1 で拡張(通常は :all またはカスタム)
  • Auth fingerprint — ログイン前に発行した guest トークンは、ログイン後に昇格しない
  • Re-connect — 人間がログインしたら POST /_dialup/agent-connect?t=<tab_id> を再実行(ブラウザで再承認)
  • Human approvaldialup.js が WS push で承認モーダルを表示(dialup:agent_approval
def agent_grant(assigns) do
  Dialup.Auth.Grants.for_user(assigns, capabilities: [:read_scene, :issue_browser_url])
end

def agent_state(assigns) do
  %{user: Dialup.Auth.public_user(assigns[:current_user])}
end

本番チェックリスト

  • Notifier を Swoosh 等に差し替え(reset URL をログしない)
  • オープン登録を無効化またはメール確認を追加
  • agent_session: true は auth plug で保護
  • HTTPS + secure_cookies
Hex ガイド: リポジトリの guides/authentication.mdシーケンス図、 CSRF remount、レビュー remediation、テスト一覧などの詳細があります。