リファレンス
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
nil を返すと use Dialup の title 設定値が使われます。
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_event | ws-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.CommandedContext と
mix 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/list、tools/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 ガイドでも同じ内容を参照できます)。
-
Human login — HTTP
POST /log_in→broadcast_auth_user→dialup_sessionローテーション → WS 再接続でcurrent_userを layout session に注入 -
Agent-first — guest grant →
issue_browser_url→ 人間が/log_in→ live セッション昇格 → 旧 guest トークンは auth_fingerprint で昇格しない →POST /_dialup/agent-connect?t=<tab_id>で再接続 - Logout sync — pre-rotate id へ broadcast → DB トークン失効 → cookie ローテーション → MCP grant ダウングレード
Agent / エージェント
- Guest —
[:read_scene, :issue_browser_url](auth_accounts設定時) - Logged in —
agent_grant/1で拡張(通常は:allまたはカスタム) - Auth fingerprint — ログイン前に発行した guest トークンは、ログイン後に昇格しない
- Re-connect — 人間がログインしたら
POST /_dialup/agent-connect?t=<tab_id>を再実行(ブラウザで再承認) - Human approval —
dialup.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
guides/authentication.md に
シーケンス図、
CSRF remount、レビュー remediation、テスト一覧などの詳細があります。