Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
06d5214
feat: 모델 선택 계약과 조합 검증 추가
kargnas Aug 16, 2026
cc73906
feat: Registry 동적 모델 조회 추가
kargnas Aug 16, 2026
4f2e8ed
feat: ACP 네이티브 모델 조회 추가
kargnas Aug 16, 2026
e081601
feat: Antigravity 모델 메타데이터 연동
kargnas Aug 16, 2026
b938148
feat: Claude 설정 기반 모델 조회 추가
kargnas Aug 16, 2026
2a2f5b8
feat: Codex 모델과 처리 등급 연동
kargnas Aug 16, 2026
683be15
feat: xAI API 모델 목록 연동
kargnas Aug 16, 2026
9adbf8b
refactor: Box 모델 provider 추론 제거
kargnas Aug 16, 2026
5144270
feat: 서버 모델 선택 검증과 실행 전파
kargnas Aug 16, 2026
9ead019
test: ACP 통신 모델 fixture 갱신
kargnas Aug 16, 2026
57f39be
test: 동적 기본 모델 기반 E2E 갱신
kargnas Aug 16, 2026
9c46382
feat: 프런트 모델 선택 타입 확장
kargnas Aug 16, 2026
ffae928
feat: 모델별 effort와 처리 등급 UI 추가
kargnas Aug 16, 2026
3b2da42
fix: 엔진 새로고침 오류 처리
kargnas Aug 16, 2026
a097bf5
docs: 동적 모델 선택 계약 반영
kargnas Aug 16, 2026
86eb077
fix: Codex Fast 처리 등급 중복 제거
kargnas Aug 16, 2026
c10b2ce
feat: 모델 목록 백그라운드 갱신 추가
kargnas Aug 16, 2026
b4c7e88
docs: 모델 목록 갱신 계약 반영
kargnas Aug 16, 2026
a32f3da
test: 통신 E2E 모델 선택값 갱신
kargnas Aug 16, 2026
aef5064
feat: Droid 동적 모델과 effort 조회 추가
kargnas Aug 16, 2026
2c72acb
fix: ACP 세션 설정 안정성 보완
kargnas Aug 16, 2026
ae4ca61
fix: Claude CLI 기본 모델의 클라우드 경로 차단
kargnas Aug 16, 2026
204667f
fix: Codex 모델 catalog 페이지 처리 보완
kargnas Aug 16, 2026
ea930a7
fix: 모델 처리 등급 기본값 표시 수정
kargnas Aug 16, 2026
a58c5ef
test: 거부된 모델 변경의 비영속성 검증
kargnas Aug 16, 2026
368aeac
docs: 모델 catalog 출처 설명 보완
kargnas Aug 16, 2026
bf2df34
fix: Codex fake switch 범위 제한
kargnas Aug 16, 2026
f13a25a
Stop handing clients the provider session cursors (#141)
mnthr7 Aug 16, 2026
26d0c7a
A webhook turn does not inherit auto mode (#137)
milind-soni Aug 16, 2026
ca79e4c
Refine webhook setup and chat experience (#148)
milind-soni Aug 16, 2026
1f67401
Pick a reasoning-effort level per bot (#144)
NuCl34R Aug 16, 2026
56a4452
Clean up webhook chat card (#149)
milind-soni Aug 16, 2026
a80c61c
Show webhook runs live in MAUS chat (#150)
milind-soni Aug 16, 2026
c65b5e2
Run a new webhook first event (#152)
milind-soni Aug 16, 2026
ed4519f
Bump version to 0.1.20
milind-soni Aug 16, 2026
b08e166
Persist Local VM workspaces across recreation (#151)
milind-soni Aug 16, 2026
a82697e
Harden Local VM lifecycle and readiness (#153)
milind-soni Aug 16, 2026
e5b9b02
dist-server: rebuild for v0.1.20
milind-soni Aug 16, 2026
2e87128
Harden browser sessions in computer sandboxes (#154)
milind-soni Aug 16, 2026
026a179
Roll back failed cloud computer provisioning (#155)
milind-soni Aug 16, 2026
f008177
Harden local and remote CUA computer use (#158)
milind-soni Aug 16, 2026
d579795
Bump version to 0.1.21
milind-soni Aug 16, 2026
81a9e54
Replace Composio Connect with project sessions (#165)
milind-soni Aug 17, 2026
1f659f9
Mirror delegated turn terminal state into bot channels (#164)
stephenlzc Aug 17, 2026
037d066
Merge main into feat/dynamic-model-catalog
kargnas Aug 17, 2026
49b376f
test: helperNoCatalog PATCH 성공 검증 추가
kargnas Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ already have:
- **Local first.** One small harness server on `127.0.0.1` owns every agent process. Transcripts, keys, and
events live in `~/.openmausbot`, not a cloud.
- **Agents with hands.** Each bot can get a real computer — a cloud Linux desktop it drives while you watch
live, or your own Mac — plus 500+ apps through Composio Connect.
live, or your own Mac — plus 500+ apps through Composio.

## Features

Expand All @@ -61,8 +61,10 @@ already have:

### 🧠 Pick a brain per bot

A model picker with a provider rail — Claude and Codex models side by side, defaults marked, unavailable
providers dimmed with the reason. Switch a bot's model mid-conversation.
A model picker with a provider rail — models reported by installed CLIs or supported provider APIs, with their
configured defaults, effort levels, and processing tiers such as Codex Fast. The cached list stays visible while
catalogs refresh manually or every five minutes. The server rejects unsupported model, effort, or tier selections
and only attaches tools to capable models.

<img src="docs/screenshots/model-picker.png" alt="Model picker with provider rail" width="100%">

Expand Down Expand Up @@ -93,7 +95,7 @@ permission broker turns every risky action into a decision you make, for cloud a

### 🔌 Connected apps

A one-click marketplace over Composio Connect: Gmail, Slack, GitHub, Notion, Linear and hundreds more.
A one-click marketplace over Composio Sessions: Gmail, Slack, GitHub, Notion, Linear and hundreds more.
OAuth once, and every bot can use them as tools.

<img src="docs/screenshots/marketplace.png" alt="Connected apps marketplace" width="100%">
Expand Down Expand Up @@ -162,7 +164,7 @@ flowchart LR
REG --> CL & CX & GR
CL & CX & GR -- "permission requests" --> BROKER
server -- "Box API" --> BOX[("Cloud computer<br/>box.ascii.dev")]
server -- "Composio Connect" --> APPS[("Gmail · Slack · GitHub · …")]
server -- "Composio Session" --> APPS[("Gmail · Slack · GitHub · …")]
```

| Layer | Where | What it does |
Expand Down Expand Up @@ -228,8 +230,7 @@ in the sidebar footer) when you want to enable its integration:

| Credential | What it enables | Where to get it |
|---|---|---|
| Composio Connect key (`ck_…`) | Connect Gmail, GitHub, Slack, Notion, and other apps to your bots | [Composio Connect setup guide](https://docs.composio.dev/docs/composio-connect) |
| Composio API key (`ak_…`) | Browse the full app catalog with official names and logos | [Composio project API key guide](https://docs.composio.dev/reference/authenticating-to-composio/project-api-key-permissions) |
| Composio project key (`ak_…`) | Connect Gmail, GitHub, Slack, Notion, and other apps to your bots | [OpenMausBot Composio setup](docs/composio.md) |
| Box API key | Give bots an isolated remote Linux computer with a desktop and terminal | [Box API key guide](https://docs.ascii.dev/box/api-keys) |
| ElevenLabs key | Read replies aloud, and call your bots | [ElevenLabs API keys](https://elevenlabs.io/app/settings/api-keys) |

Expand Down
9 changes: 8 additions & 1 deletion dist-server/auto-approve.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,14 @@ export function approvalKey(tool, summary) {
/** Why this request may be answered without the human, or null to ask.
* The returned string becomes the chip in the transcript, so an
* auto-approved action is never invisible. */
export function autoDecision(bot, tool, summary) {
export function autoDecision(bot, tool, summary, context) {
// Auto mode is something a person switched on for turns they are present
// for. A webhook turn begins with nobody watching, on a payload someone
// else wrote, so it does not inherit that decision — the guard below is a
// pattern list its own comment calls "not a security boundary", and it
// must not stand in for a human at 3am.
if (context?.unattended)
return null;
// the guards come first, so an "always allow" can never widen into them
if (looksDestructive(summary) || looksDestructive(tool))
return null;
Expand Down
119 changes: 67 additions & 52 deletions dist-server/box.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ensureRemoteCuaCommand, remoteComputerBootstrapCommand } from "./remote-computer.js";
// overridable so tests can point at a stub instead of the live provider
const BOX_API = process.env.OMB_BOX_API || "https://ascii.dev/api/box/v1";
const READY = new Set(["idle", "ready", "running"]);
Expand Down Expand Up @@ -176,61 +177,64 @@ export async function provisionBox(cfg, botId, botName) {
const vmName = await boxNameFor(botId);
let box = await findBox(cfg, botId);
let created = false;
if (!box) {
const createRes = await boxJson(cfg, "/boxes", {
method: "POST",
// substrate-side backstop: archives itself (billing pauses, disk
// survives) if every stop path dies
body: JSON.stringify({ ttlSeconds: 8 * 60 * 60 }),
});
if (!createRes.ok || !createRes.body?.box?.id) {
throw new Error(boxErrorMessage(createRes.status, "box create", createRes.body));
try {
if (!box) {
const createRes = await boxJson(cfg, "/boxes", {
method: "POST",
// substrate-side backstop: archives itself (billing pauses, disk
// survives) if every stop path dies
// The computer needs the user's desktop session, not the account
// owner's host credentials. Keep provider-side env injection off so
// API keys cannot silently appear inside the guest.
body: JSON.stringify({ ttlSeconds: 8 * 60 * 60, noEnv: true }),
});
if (!createRes.ok || !createRes.body?.box?.id) {
throw new Error(boxErrorMessage(createRes.status, "box create", createRes.body));
}
box = createRes.body.box;
created = true;
const rename = await boxJson(cfg, `/boxes/${box.id}`, {
method: "PATCH",
body: JSON.stringify({ name: vmName }),
});
if (!rename.ok)
throw new Error(boxErrorMessage(rename.status, "box naming", rename.body));
}
const ready = await waitReady(cfg, box.id);
if (!ready)
throw new Error("box did not become ready within 90s — retry in a minute");
// Install the exact Cua Driver executable in the background, keep its
// daemon private to the VM, and retain X11 tooling as a degraded fallback.
const bootstrap = remoteComputerBootstrapCommand(botName);
let boot;
for (let attempt = 0; attempt < 5; attempt++) {
boot = await runCommand(cfg, box.id, bootstrap);
if (boot.ok || boot.exitCode !== null)
break;
await new Promise((r) => setTimeout(r, 3000));
}
box = createRes.body.box;
created = true;
await boxJson(cfg, `/boxes/${box.id}`, { method: "PATCH", body: JSON.stringify({ name: vmName }) });
if (!boot?.ok) {
const detail = boot?.stderr?.slice(0, 200) || (boot?.exitCode != null ? `exit ${boot.exitCode}` : "no response");
throw new Error(`box setup failed: ${detail}`);
}
const joinUrl = await mintDesktopUrl(cfg, box.id);
if (!joinUrl)
throw new Error("box desktop link could not be created");
return { boxId: box.id, machineName: vmName, reused: !created, state: ready.state, joinUrl };
}
const ready = await waitReady(cfg, box.id);
if (!ready)
throw new Error("box did not become ready within 90s — retry in a minute");
// Idempotent bootstrap. Three layers:
// 1. X11 action + capture tools (xdotool/scrot/imagemagick) — the
// always-works fallback for the computer tools.
// 2. CUA (cua-computer-server, trycua) installed into /opt/ogb/venv in
// the BACKGROUND (first install takes minutes; nohup'd children
// survive the commands endpoint returning — probed by agentcal).
// 3. computer-server started loopback-only on :8000 when installed —
// driven from outside via the box's run-command endpoint, so no
// inbound port and no tunnel is ever needed.
const cuaInstall = [
"sudo apt-get update -qq || true",
"sudo apt-get install -y -qq gnome-screenshot xclip wmctrl xdotool imagemagick scrot >/dev/null 2>&1 || true",
'curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null 2>&1 || true',
'export PATH="$HOME/.local/bin:$PATH"',
'sudo mkdir -p /opt/ogb && sudo chown "$(whoami)" /opt/ogb',
"uv venv /opt/ogb/venv --python 3.13 >/dev/null 2>&1 || uv venv /opt/ogb/venv >/dev/null 2>&1 || true",
"[ -x /opt/ogb/venv/bin/python ] && uv pip install --python /opt/ogb/venv/bin/python cua-computer-server >/dev/null 2>&1 || true",
"[ -x /opt/ogb/venv/bin/python ] && /opt/ogb/venv/bin/python -c 'import computer_server' 2>/dev/null && touch /opt/ogb/cua-ready || true",
].join("; ");
const bootstrap = [
"command -v xdotool >/dev/null || sudo apt-get install -y -qq xdotool scrot imagemagick >/dev/null 2>&1 || true",
`[ -f /opt/ogb/cua-ready ] || [ -f /tmp/ogb-cua-installing ] || { touch /tmp/ogb-cua-installing; nohup bash -c '${cuaInstall.replace(/'/g, "'\\''")}; rm -f /tmp/ogb-cua-installing' > /tmp/ogb-cua-install.log 2>&1 & }`,
// start CUA computer-server (loopback only) once installed; pidfile-free
// guard on the module name is safe here — the pattern cannot match this
// bootstrap's own shell (agentcal's pgrep self-match trap)
'if [ -f /opt/ogb/cua-ready ] && ! pgrep -f "computer_server" >/dev/null 2>&1; then DISPLAY=${DISPLAY:-:0} nohup /opt/ogb/venv/bin/python -m computer_server --host 127.0.0.1 --port 8000 --width 1280 --height 800 > /tmp/ogb-cua-server.log 2>&1 & fi',
`tmux has-session -t work 2>/dev/null || tmux new-session -d -s work 'echo; echo " ▦ ${botName.replace(/["'\\\\]/g, "")}'"'"'s computer — OpenMausBot"; echo; exec bash -i'`,
"echo bootstrapped",
].join("\n");
let boot;
for (let attempt = 0; attempt < 5; attempt++) {
boot = await runCommand(cfg, box.id, bootstrap);
if (boot.ok || boot.exitCode !== null)
break;
await new Promise((r) => setTimeout(r, 3000));
catch (error) {
if (!created || !box?.id)
throw error;
const cleanup = await boxJson(cfg, `/boxes/${box.id}`, {
method: "DELETE",
headers: { "X-Ascii-Confirm-Delete": box.id },
}).catch(() => null);
boxIdCache.delete(botId);
if (cleanup?.ok)
throw error;
const message = error instanceof Error ? error.message : String(error);
throw new Error(`${message}. The new computer could not be removed automatically; delete box ${box.id} in ascii.dev.`);
}
const joinUrl = await mintDesktopUrl(cfg, box.id);
return { boxId: box.id, machineName: vmName, reused: !created, state: ready.state, joinUrl };
}
/** Wake the bot's box and return a FRESH desktop URL. */
export async function joinBox(cfg, botId) {
Expand All @@ -240,13 +244,24 @@ export async function joinBox(cfg, botId) {
const ready = await waitReady(cfg, box.id);
if (!ready)
throw new Error("the box did not wake in time — try again");
// Provider archive/resume preserves disk but not processes. Reattach the
// driver daemon before handing the desktop back to the user.
await runCommand(cfg, box.id, ensureRemoteCuaCommand(), { timeoutMs: 15_000 }).catch(() => null);
return { joinUrl: await mintDesktopUrl(cfg, box.id), state: ready.state ?? null };
}
/** Archive the bot's box now (billing pauses, disk survives). */
export async function sleepBox(cfg, botId) {
const box = await findBox(cfg, botId);
if (!box)
throw new Error("no computer for this bot");
// Ask the browser's oldest (main) process to exit before the provider
// snapshots the disk. This gives Chrome a chance to flush cookies and
// session state instead of restoring a crash-marked profile next wake.
const quiesceBrowser = [
'for name in chrome google-chrome chromium chromium-browser; do pid=$(pgrep -o -x "$name" 2>/dev/null || true); [ -z "$pid" ] || kill -TERM "$pid" 2>/dev/null || true; done',
'for i in 1 2 3 4 5 6 7 8; do if ! pgrep -x chrome >/dev/null 2>&1 && ! pgrep -x google-chrome >/dev/null 2>&1 && ! pgrep -x chromium >/dev/null 2>&1 && ! pgrep -x chromium-browser >/dev/null 2>&1; then break; fi; sleep 0.25; done',
].join("; ");
await runCommand(cfg, box.id, quiesceBrowser, { timeoutMs: 5_000 }).catch(() => null);
await boxJson(cfg, `/boxes/${box.id}/stop`, { method: "POST" }).catch(() => { });
return { ok: true };
}
Expand Down
18 changes: 18 additions & 0 deletions dist-server/comms-visibility.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,21 @@ export function mirrorReply(bus, target, reply, channel) {
bus.store.patchGroup(channel.id, { unread: true });
bus.broadcastGroup(channel.id);
}
/** Mirror a terminal activity note into the channel — for async handoffs
* whose terminal state is not a reply (turn failed, was stopped, or never
* started). Prior art (A2A, MCP Tasks) is unanimous that every terminal
* state of an async handoff should be visible where the human is looking,
* and the channel is that place. */
export function mirrorActivity(bus, from, channel, name, ok) {
if (!channel)
return;
const message = bus.store.appendMessage(channel.threadId, {
role: "bot",
kind: "activity",
tool: { name, ok },
from: { botId: from.id, name: from.name, color: from.color },
});
bus.broadcast({ kind: "message", threadId: channel.threadId, message });
bus.store.patchGroup(channel.id, { unread: true });
bus.broadcastGroup(channel.id);
}
Loading
Loading