Open Dots

Anil-matcha/open-dots — self-hosted AI workspace with a governed action gateway · MIT

1 — 概要

Open Dots は、OpenAI Dots のオープンソース代替として作られたセルフホスト型の AI ワークスペース。アシスタント(bot)ごとのチャット、ツール実行、承認プロンプト、外部アプリ連携(Composio)、隔離されたブラウザ環境を 1 つのローカルアプリにまとめている。OpenAI や各モデル提供元とは無関係の独立プロジェクト。

設計の中心は Action Gateway。ファイル読み書き・GitHub 操作・ブラウザ遷移などの副作用はすべてここを通り、登録されていない操作は拒否される。リスクの高い操作はユーザーの承認を待ち、各段階が監査イベントとして SQLite に残る。

Next.jsclient/ — チャット UI
FastAPIserver/ — API + SSE
SQLite状態 + 暗号化設定
Playwrightruntime/ — Docker 内ブラウザ
ステータス: README 上は「Prototype / active development」。ローカルでの試用が前提で、マルチユーザー運用と悪意あるサイトに対する隔離は本番水準ではないと明記されている。

bot ごとに名前・役割・システムプロンプト・モデル ID・アバター色を持つ。会話は bot 単位のスレッドとして保存される。

/workspace と /connector の明示コマンドで操作を依頼する。書き込みや外部送信は承認カードで止まる。

bot 専用の Docker コンテナで Chromium を動かし、遷移・スクリーンショット・入力・シェル実行を行う。既定は疑似実装の fake。

2 — 構成

クライアントは HTTP で API を呼び、応答トークンとツールイベントを SSE(Server-Sent Events)で受け取る。サーバー側はルーター層とサービス層に分かれ、副作用を持つ処理はすべて Action Gateway の下にぶら下がる。

flowchart TD
  C["Next.js client
ChatWindow / ApprovalCard / ComputerPanel"] -->|"HTTP + SSE"| API["FastAPI
認証ミドルウェア + routers"] API --> DB[("SQLite
bots / messages / approvals / audit_events")] API --> P["ModelProviderService
推論アダプタ"] API --> G{"Action Gateway
登録 → ポリシー → 承認 → 実行"} G --> AB["ApprovalBroker
asyncio.Future で待機"] G --> W["WorkspaceService
WORKSPACE_ROOT 内のみ"] G --> CN["Composio
GitHub issue 一覧 / 作成"] G --> CP["ComputerProvider"] CP --> F["fake
メモリ内"] CP --> D["docker
Playwright コンテナ"] CP --> R["remote
HTTP サービス"] G -.->|"監査イベント"| DB
ディレクトリ役割主なファイル
client/Next.js(App Router)の UI。チャット、承認カード、コンピュータパネル、監査パネル、設定ドロワーcomponents/ChatWindow.jsx, ApprovalCard.jsx, lib/api.js
server/app/routers/HTTP API。/api/v1/* 配下に chat・approvals・computers・connectors・audit など 10 ルーターchat.py, computers.py, approvals.py
server/app/services/ゲートウェイ、承認、永続化、推論アダプタ、コンピュータ実装、暗号化action_gateway.py, approval_broker.py, storage_service.py
runtime/コンテナ内で動く Node.js ドライバ。Playwright の Chromium をトークン付き HTTP で操作するdriver.mjs, Dockerfile, seccomp_profile.json
server/tests/pytest。ゲートウェイ、認証、ワークスペース境界、各コンピュータプロバイダ、コネクタのテストtest_action_gateway.py ほか 8 本
3 — 1 ターンの流れ

1 回の発話は 2 つのリクエストに分かれる。POST /api/v1/chat/send がユーザーメッセージを保存し、続く GET /api/v1/chat/stream/{thread_id} が SSE で応答を流す。ツール操作はモデルが決めるのではなく、最後のユーザー発話が /workspace または /connector で始まるかをサーバーが構文解析して決まる。

sequenceDiagram
  participant U as Client
  participant S as chat router
  participant G as Action Gateway
  participant A as ApprovalBroker
  participant M as 推論 API
  U->>S: POST /chat/send
  U->>S: GET /chat/stream (SSE)
  S-->>U: turn.started
  S->>S: /workspace・/connector を解析
  S->>G: open(action)
  alt 承認が必要
    G->>A: open → Future 作成
    S-->>U: request.opened
    U->>A: POST /approvals/respond (allow / deny)
    A-->>G: decision
  end
  G->>G: execute
  S-->>U: tool.started → tool.completed / failed
  S->>M: 履歴 + 結果を system prompt に追記して送信
  M-->>S: 応答
  S-->>U: content.delta ... turn.completed

ツールの結果はモデルへの function calling ではなく、Action result (workspace.read): {...} のような文字列としてシステムプロンプトの末尾に足される。拒否・期限切れ・失敗も同じ形で伝わるため、モデルはそれを踏まえて返答を書く。

SSE イベント送出タイミング主なフィールド
turn.startedストリーム開始直後botMsgId, model
request.opened承認が必要な操作を受け付けたrequestId, tool, summary, arguments
tool.started承認後(または承認不要)に実行を開始tool, requestId, action
tool.completed / tool.failed実行結果、または構文・ポリシー違反result / error
tool.denied / tool.expiredユーザーが拒否 / 承認待ちがタイムアウトtool, requestId
content.delta推論 API の応答テキストdelta
turn.completed応答完了。bot メッセージを保存した後ok, botMsgId
4 — Action Gateway

ActionGateway(server/app/services/action_gateway.py)は、操作名から ActionDefinition を引き、登録・形式・プレビューを確認してから実行する。未登録の名前は action.denied を記録して即座に拒否する。新しい操作を足すには register_action(definition, executor) で定義と実行関数を登録する必要がある。

normalizeWorkspaceToolCall / ActionInvocation に揃える。それ以外は拒否
policy check未登録・tool/action 名の不一致・引数が dict でない・preview なしを拒否
pending_approvalrequires_approval=True なら ApprovalBroker に承認を依頼
approvedallow の決定、または承認不要の操作
executeexecutor を呼ぶ。戻り値は dict 必須。例外も failed として記録

execute() は保留中リストに無いリクエストや、承認必須なのに allow を得ていないリクエストを ActionGatewayError で止める。承認をすり抜けて実行関数を直接呼ぶ経路をゲートウェイ側で塞いでいる。

登録済みの操作

操作名risk承認内容
workspace.listread必要ワークスペース内のディレクトリ一覧
workspace.readread必要ファイル読み取り(既定 128 KiB 上限)
workspace.writewrite必要ファイル書き込み。親ディレクトリは既存である必要がある
connector.github_list_issuesread不要Composio 経由で issue を最大 10 件取得
connector.github_create_issuewrite必要Composio 経由で issue を作成
computer.create / start / stop / pause / resetwrite不要bot 専用コンピュータのライフサイクル操作
computer.health / screenshot / files_listread不要状態確認・画面取得・/workspace の一覧
computer.browser_navigateexternal必要HTTP(S) の絶対 URL へ遷移(2048 文字まで)
computer.terminal_executewrite必要コンテナ内で /bin/sh -lc を実行
computer.send_inputwrite必要クリック・キー入力などのイベント送信
computer.cleanupwrite必要コンテナとワークスペースの後始末
読み取りでも承認が要る操作がある: ActionDefinition の requires_approval の既定値は True。ワークスペースの 3 操作はこの既定値のまま登録されているため、list と read も承認カードが出る。承認を省いているのはコネクタの一覧取得と、コンピュータの状態系操作だけ。

WorkspaceService._resolve() が次を拒否する。

  • 空文字・NUL 文字を含むパス
  • 絶対パス
  • 解決後に WORKSPACE_ROOT の外へ出るパス(../ やシンボリックリンク経由を含む)
  • WORKSPACE_MAX_FILE_BYTES(既定 131072)を超える読み書き

redact_sensitive() が監査ログとリクエスト表示の前に値を加工する。

  • キー名に token / secret / password / cookie / api_key 等を含む値を [REDACTED] に置換
  • 文字列中の api_key=... 形式も正規表現で伏せる
  • 完了時の result_summary からは content / body / data を除き、リストは件数だけ残す
5 — 承認と監査

ApprovalBroker はプロセス内の asyncio.Future で承認を待つ。SSE ストリームを開いたコルーチンが wait() で止まり、クライアントの POST /api/v1/approvals/respond({"request_id", "action": "allow" | "deny"})が resolve() を呼ぶと再開する。

stateDiagram-v2
  [*] --> pending_approval: open()
  [*] --> denied: ポリシー違反
  pending_approval --> approved: allow
  pending_approval --> denied: deny
  pending_approval --> expired: 120 秒経過
  approved --> running: execute()
  running --> completed
  running --> failed
  completed --> [*]
  failed --> [*]
  denied --> [*]
  expired --> [*]
  • タイムアウト: APPROVAL_TIMEOUT_SECONDS(既定 120 秒)を過ぎると expired になり、承認レコードと監査ログが更新される。
  • 単一プロセス前提: 待機中の承認はメモリ上の Future なので、サーバー再起動や複数ワーカー構成では引き継がれない。コード上のコメントも「local single-user server」向けと明記している。
  • コンピュータ操作の承認: POST /api/v1/computers/{bot_id}/actions で開いた要求は、承認後に /actions/{request_id}/execute を呼んで実行する 2 段階構成。

監査ログは audit_events テーブルに追記され、/api/v1/audit と UI の AuditPanel から参照できる。1 つの操作は次のイベント列を残す。

発行元イベント
Action Gatewayaction.requested → action.approved / action.denied / action.expired → action.started → action.completed / action.failed
ApprovalBrokerapproval.opened → approval.allow / approval.deny / approval.expired

各イベントには request_id・thread_id・bot_id・tool・action・intent・target・arguments・preview・risk が秘匿処理済みで入る。

6 — チャットコマンド

コマンドはシェルとして実行されず、shlex.split で 1 行目だけを分解する。書き込み内容や issue 本文は 2 行目以降に書く。構文が崩れているとゲートウェイに届く前に tool.failed を返し、その理由をモデルへのコンテキストに入れる。

# ワークスペース(WORKSPACE_ROOT からの相対パス)
/workspace list [path]
/workspace read <path>
/workspace write <path>
<ファイル内容(2 行目以降)>

# GitHub(Composio で OAuth 接続済みであること)
/connector github issues <owner>/<repo> [open|closed|all]
/connector github create-issue <owner>/<repo> "<title>"
/connector github create-issue <owner>/<repo>
<title>
[body]

owner と repo は英数字・ドット・ハイフン・アンダースコアのみ(各 100 文字まで)。コネクタは現在 GitHub の 2 操作に限られ、任意のツール探索や書き込みは実装されていない。

7 — コンピュータランタイム

ComputerProvider は Protocol で定義された共通インターフェイス。create / start / stop / pause / reset / health のライフサイクルと、browser_navigate / terminal_execute / files_list / screenshot / send_input / cleanup の操作を持つ。コンピュータ ID は bot ID から computer-<bot_id> として導出し、他の bot のコンピュータ ID を指定すると拒否する。

COMPUTER_PROVIDER実装用途
fake(既定)メモリ内の決定的な疑似実装。プロセスを起動しないローカル開発とテスト
dockerbot ごとに open-dots-computer イメージのコンテナを起動し、driver.mjs と HTTP で通信ローカルで実ブラウザを動かす
remoteCOMPUTER_REMOTE_BASE_URL の互換サービスへ HTTP で委譲(認証ヘッダ・解像度・fps を設定可能)外部のブラウザ実行基盤を使う

DockerComputerProvider._launch() が docker run に渡すオプション。イメージは mcr.microsoft.com/playwright:v1.62.1-noble をベースに pwuser で動く。

オプション値効果
--read-only—ルートファイルシステムを読み取り専用にする
--tmpfs/tmp 512m, pwuser のホームディレクトリ 1g(nosuid)書き込み先をメモリ上に限定
--mountbot 専用ディレクトリ → /workspacebot ごとに作業領域を分ける(DATA_DIR/computers 配下)
--cap-drop ALL / no-new-privileges—Linux capability を全て外し、権限昇格を禁止
--security-opt seccompruntime/seccomp_profile.jsonファイルが存在する場合のみ適用
--cpus / --memory / --pids-limit2.0 / 2g / 512資源上限(環境変数で変更可)
--publish127.0.0.1::3000ドライバのポートをループバックの空きポートにだけ公開
--env COMPUTER_TOKEN起動ごとの乱数ドライバは x-computer-token が一致しない要求を拒否
ネットワークの外向き通信は制限されていない: 起動オプションに --network の指定は無く、コンテナは既定のブリッジネットワークから外部へ到達できる。README も「悪意あるサイトに対する堅牢なサンドボックスではない」とし、外向き通信・イメージの出所・認証情報の露出を確認してから信頼できないコンテンツに使うよう求めている。
docker build -t open-dots-computer:1.62.1 ./runtime
export COMPUTER_PROVIDER=docker
export COMPUTER_DOCKER_IMAGE=open-dots-computer:1.62.1
8 — 認証と秘密情報
  • /api/v1/* は、health と auth 系の 5 パスを除き、HTTP ミドルウェアで認証を要求する(失敗時 401)。
  • トークンは APP_AUTH_TOKEN、未設定なら DATA_DIR/.auth-token に生成(権限 0600)。
  • APP_AUTH_TOKEN を設定していない場合に限り、ループバックからのアクセスはトークンを HttpOnly のセッションクッキー(SameSite=Lax)に交換できる。
  • それ以外は Authorization: Bearer が必要。比較は secrets.compare_digest。
  • 推論 API キーと Composio キーは SecretStore が Fernet で暗号化して SQLite に保存する。
  • 鍵は APP_ENCRYPTION_KEY、未設定なら DATA_DIR/.encryption.key に生成(権限 0600)。
  • 設定 API のスキーマではキーを writeOnly とし、読み出しは「設定済みか」の真偽値だけ返す。
ループバック以外に公開する場合: README は APP_AUTH_TOKEN の設定、クライアント側の NEXT_PUBLIC_API_TOKEN、HTTPS、狭い CORS_ORIGINS を求めている。NEXT_PUBLIC_ 変数はブラウザに配信されるバンドルへ埋め込まれる点に注意が必要。
9 — 起動と設定

Node.js と npm、Python 3.10 以上と pip、推論 API のキーとベース URL。

1. API サーバー(ターミナル 1)

git clone https://github.com/Anil-matcha/open-dots.git
cd open-dots/server
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
export MODEL_API_KEY="your_api_key"
export MODEL_API_BASE_URL="https://your-inference-host.example/api/v1"
python run.py        # http://127.0.0.1:8000 (/docs に OpenAPI UI)

2. Web クライアント(ターミナル 2)

cd open-dots/client
npm install
npm run dev          # http://localhost:3000

API キーは環境変数の代わりに App Settings からも入力できる。初回起動時にセッション用トークンと暗号化鍵が DATA_DIR に作られる。

変数既定値用途
MODEL_API_KEY空設定画面に保存が無いときの推論 API キー
MODEL_API_BASE_URL空推論 API のベース URL(必須)
DEFAULT_MODELgpt-5-mini新規アシスタントの初期モデル
COMPOSIO_API_KEY空コネクタ用(任意)
DATA_DIR~/.open-dotsSQLite とローカル鍵の保存先
APP_ENCRYPTION_KEYDATA_DIR に生成Fernet 鍵
APP_AUTH_TOKENDATA_DIR に生成API の Bearer トークン
WORKSPACE_ROOTリポジトリのルートワークスペース操作の境界
WORKSPACE_MAX_FILE_BYTES131072読み書きの上限バイト数
APPROVAL_TIMEOUT_SECONDS120承認待ちの期限
COMPUTER_PROVIDERfakefake / docker / remote
CORS_ORIGINShttp://127.0.0.1:3000,http://localhost:3000許可するオリジン
HOST / PORT127.0.0.1 / 8000API の待ち受け
10 — 制約と注意点

同梱の ModelProviderService は OpenAI 互換の chat completions を話さない。POST {MODEL_API_BASE_URL}/{model_id} に予測リクエストを送り、request_id が返れば GET {base}/predictions/{request_id}/result を最大 180 秒ポーリングする。画像は {base}/upload_file に上げる。この契約を実装したサービスが別途必要になる。

  • 推論 API はステートレス扱いで、直前 10 件までの履歴をテキストにしてシステムプロンプトへ埋め込む。
  • 応答はポーリング完了後にまとめて受け取り、サーバー側で空白ごとに区切り、20 ms 間隔で content.delta として流す。トークン単位のストリーミングではない。
  • モデル一覧(/api/v1/models)は routers/models.py にハードコードされた 31 件の ID。実際に使えるかは接続先サービス次第。
  • 利用者はローカルの 1 名のみ。ユーザー管理・ロール・マルチユーザーの権限付与は未実装。
  • SQLite はローカル状態。複数インスタンスでの共有やバックアップの仕組みは無い。
  • 汎用のプロバイダプラグイン機構は未実装。
  • コンピュータランタイムは任意機能で、任意の Web コンテンツに対するセキュリティ境界ではない。
  • コネクタ操作は意図的に絞られており、任意のツール探索や書き込みは無い。
  • モバイル・デスクトップクライアント、永続メモリ、定期実行エンジンは無い。
  • ツール実行はユーザーの明示コマンドが起点で、モデルが自律的にツールを選ぶループは無い。1 ターンで実行される操作は最大 1 件。
  • SQLite スキーマには tasks と memories テーブルがあるが、README 上はメモリサービスと定期実行は未実装扱い。
  • server/app/services/endpoint_mapping.json(画像・動画生成系のエンドポイント名の対応表)は、サーバーとクライアントのコードから参照されていない。