Open Dots
Anil-matcha/open-dots — self-hosted AI workspace with a governed action gateway · MIT
Open Dots は、OpenAI Dots のオープンソース代替として作られたセルフホスト型の AI ワークスペース。アシスタント(bot)ごとのチャット、ツール実行、承認プロンプト、外部アプリ連携(Composio)、隔離されたブラウザ環境を 1 つのローカルアプリにまとめている。OpenAI や各モデル提供元とは無関係の独立プロジェクト。
設計の中心は Action Gateway。ファイル読み書き・GitHub 操作・ブラウザ遷移などの副作用はすべてここを通り、登録されていない操作は拒否される。リスクの高い操作はユーザーの承認を待ち、各段階が監査イベントとして SQLite に残る。
bot ごとに名前・役割・システムプロンプト・モデル ID・アバター色を持つ。会話は bot 単位のスレッドとして保存される。
/workspace と /connector の明示コマンドで操作を依頼する。書き込みや外部送信は承認カードで止まる。
bot 専用の Docker コンテナで Chromium を動かし、遷移・スクリーンショット・入力・シェル実行を行う。既定は疑似実装の fake。
クライアントは 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 本 |
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 |
ActionGateway(server/app/services/action_gateway.py)は、操作名から ActionDefinition を引き、登録・形式・プレビューを確認してから実行する。未登録の名前は action.denied を記録して即座に拒否する。新しい操作を足すには register_action(definition, executor) で定義と実行関数を登録する必要がある。
execute() は保留中リストに無いリクエストや、承認必須なのに allow を得ていないリクエストを ActionGatewayError で止める。承認をすり抜けて実行関数を直接呼ぶ経路をゲートウェイ側で塞いでいる。
登録済みの操作
| 操作名 | risk | 承認 | 内容 |
|---|---|---|---|
workspace.list | read | 必要 | ワークスペース内のディレクトリ一覧 |
workspace.read | read | 必要 | ファイル読み取り(既定 128 KiB 上限) |
workspace.write | write | 必要 | ファイル書き込み。親ディレクトリは既存である必要がある |
connector.github_list_issues | read | 不要 | Composio 経由で issue を最大 10 件取得 |
connector.github_create_issue | write | 必要 | Composio 経由で issue を作成 |
computer.create / start / stop / pause / reset | write | 不要 | bot 専用コンピュータのライフサイクル操作 |
computer.health / screenshot / files_list | read | 不要 | 状態確認・画面取得・/workspace の一覧 |
computer.browser_navigate | external | 必要 | HTTP(S) の絶対 URL へ遷移(2048 文字まで) |
computer.terminal_execute | write | 必要 | コンテナ内で /bin/sh -lc を実行 |
computer.send_input | write | 必要 | クリック・キー入力などのイベント送信 |
computer.cleanup | write | 必要 | コンテナとワークスペースの後始末 |
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を除き、リストは件数だけ残す
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 Gateway | action.requested → action.approved / action.denied / action.expired → action.started → action.completed / action.failed |
| ApprovalBroker | approval.opened → approval.allow / approval.deny / approval.expired |
各イベントには request_id・thread_id・bot_id・tool・action・intent・target・arguments・preview・risk が秘匿処理済みで入る。
コマンドはシェルとして実行されず、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 操作に限られ、任意のツール探索や書き込みは実装されていない。
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(既定) | メモリ内の決定的な疑似実装。プロセスを起動しない | ローカル開発とテスト |
docker | bot ごとに open-dots-computer イメージのコンテナを起動し、driver.mjs と HTTP で通信 | ローカルで実ブラウザを動かす |
remote | COMPUTER_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) | 書き込み先をメモリ上に限定 |
--mount | bot 専用ディレクトリ → /workspace | bot ごとに作業領域を分ける(DATA_DIR/computers 配下) |
--cap-drop ALL / no-new-privileges | — | Linux capability を全て外し、権限昇格を禁止 |
--security-opt seccomp | runtime/seccomp_profile.json | ファイルが存在する場合のみ適用 |
--cpus / --memory / --pids-limit | 2.0 / 2g / 512 | 資源上限(環境変数で変更可) |
--publish | 127.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/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とし、読み出しは「設定済みか」の真偽値だけ返す。
APP_AUTH_TOKEN の設定、クライアント側の NEXT_PUBLIC_API_TOKEN、HTTPS、狭い CORS_ORIGINS を求めている。NEXT_PUBLIC_ 変数はブラウザに配信されるバンドルへ埋め込まれる点に注意が必要。
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:3000API キーは環境変数の代わりに App Settings からも入力できる。初回起動時にセッション用トークンと暗号化鍵が DATA_DIR に作られる。
| 変数 | 既定値 | 用途 |
|---|---|---|
MODEL_API_KEY | 空 | 設定画面に保存が無いときの推論 API キー |
MODEL_API_BASE_URL | 空 | 推論 API のベース URL(必須) |
DEFAULT_MODEL | gpt-5-mini | 新規アシスタントの初期モデル |
COMPOSIO_API_KEY | 空 | コネクタ用(任意) |
DATA_DIR | ~/.open-dots | SQLite とローカル鍵の保存先 |
APP_ENCRYPTION_KEY | DATA_DIR に生成 | Fernet 鍵 |
APP_AUTH_TOKEN | DATA_DIR に生成 | API の Bearer トークン |
WORKSPACE_ROOT | リポジトリのルート | ワークスペース操作の境界 |
WORKSPACE_MAX_FILE_BYTES | 131072 | 読み書きの上限バイト数 |
APPROVAL_TIMEOUT_SECONDS | 120 | 承認待ちの期限 |
COMPUTER_PROVIDER | fake | fake / docker / remote |
CORS_ORIGINS | http://127.0.0.1:3000,http://localhost:3000 | 許可するオリジン |
HOST / PORT | 127.0.0.1 / 8000 | API の待ち受け |
同梱の 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(画像・動画生成系のエンドポイント名の対応表)は、サーバーとクライアントのコードから参照されていない。
- GitHub: Anil-matcha/open-dots — 本体リポジトリ(MIT)
- デモ動画(YouTube) — README に掲載されている紹介動画
- Composio ドキュメント — コネクタの OAuth と GitHub 操作に使うサービス
- Playwright Docker — コンピュータランタイムのベースイメージ