-
Notifications
You must be signed in to change notification settings - Fork 387
Settings as a modal with sections, and a local computer you can set up #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| // A computer your bots can use, running on this machine in a container. | ||
| // | ||
| // The cloud box needs a paid plan, and the "This Mac" path points an agent | ||
| // at the desktop the human is using. A container sits between them: free, | ||
| // disposable, and isolated from the user's files — and it runs the same | ||
| // X11 desktop our computer tools already speak (xdotool + scrot), so the | ||
| // tool layer doesn't change, only where the commands run. | ||
| // | ||
| // This module is deliberately read-only. It reports what is installed and | ||
| // never installs anything on someone's machine. | ||
| import { execFile } from "node:child_process"; | ||
| import { promisify } from "node:util"; | ||
|
|
||
| import { augmentedPath } from "./env-path.ts"; | ||
|
|
||
| const run = promisify(execFile); | ||
|
|
||
| /** The desktop image. Anthropic's computer-use demo image is the one whose | ||
| * toolchain matches our shell exactly — it ships xdotool, scrot and | ||
| * imagemagick — and it is MIT, with an arm64 build as well as x86. */ | ||
| export const IMAGE = "ghcr.io/anthropics/anthropic-quickstarts:computer-use-demo-latest"; | ||
| export const CONTAINER = "openmausbot-computer"; | ||
|
|
||
| /** Runtimes we can drive, best-known first. Docker is listed because most | ||
| * people already have it, NOT because we recommend installing it: its | ||
| * licence requires payment above 250 employees or $10M revenue, and for | ||
| * every government user. We adapt to whatever is already there. */ | ||
| const RUNTIMES = ["docker", "podman", "container"] as const; | ||
| export type Runtime = (typeof RUNTIMES)[number]; | ||
|
|
||
| async function sh(cmd: string, args: string[], timeout = 8000) { | ||
| return run(cmd, args, { timeout, env: { ...process.env, PATH: augmentedPath() } }); | ||
| } | ||
|
|
||
| async function installed(cmd: string): Promise<boolean> { | ||
| try { | ||
| await sh(process.platform === "win32" ? "where.exe" : "/usr/bin/which", [cmd], 4000); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| export interface ContainerComputerStatus { | ||
| /** the runtime we will use, or null when none is installed */ | ||
| runtime: Runtime | null; | ||
| /** everything we found, so the UI can say which one it picked */ | ||
| available: Runtime[]; | ||
| /** installed is not the same as running */ | ||
| daemonUp: boolean; | ||
| image: boolean; | ||
| container: "running" | "stopped" | "missing"; | ||
| image_ref: string; | ||
| container_name: string; | ||
| } | ||
|
|
||
| export async function containerComputerStatus(): Promise<ContainerComputerStatus> { | ||
| const available: Runtime[] = []; | ||
| for (const r of RUNTIMES) if (await installed(r)) available.push(r); | ||
| const runtime = available[0] ?? null; | ||
| const status: ContainerComputerStatus = { | ||
| runtime, | ||
| available, | ||
| daemonUp: false, | ||
| image: false, | ||
| container: "missing", | ||
| image_ref: IMAGE, | ||
| container_name: CONTAINER, | ||
| }; | ||
| if (!runtime) return status; | ||
|
|
||
| // Docker Desktop can be installed with its VM stopped, which fails every | ||
| // later command in a way that reads as "broken" rather than "not started" | ||
| try { | ||
| await sh(runtime, ["info", "--format", "{{.ServerVersion}}"], 10_000); | ||
| status.daemonUp = true; | ||
| } catch { | ||
| return status; | ||
| } | ||
|
|
||
| try { | ||
| await sh(runtime, ["image", "inspect", IMAGE]); | ||
| status.image = true; | ||
| } catch { | ||
| /* not pulled yet */ | ||
| } | ||
|
|
||
| try { | ||
| const { stdout } = await sh(runtime, ["inspect", "-f", "{{.State.Running}}", CONTAINER]); | ||
| status.container = stdout.trim() === "true" ? "running" : "stopped"; | ||
| } catch { | ||
| /* no such container */ | ||
| } | ||
| return status; | ||
| } | ||
|
|
||
| /** The commands the user is shown, built from the same constants the check | ||
| * above uses — so the instructions can't drift from what we look for. */ | ||
| export function setupCommands(runtime: Runtime | null) { | ||
| const rt = runtime ?? "docker"; | ||
| return { | ||
| pull: `${rt} pull ${IMAGE}`, | ||
| // 6080 is the desktop in a browser, 5900 for a native VNC viewer | ||
| run: `${rt} run -d --name ${CONTAINER} -p 6080:6080 -p 5900:5900 ${IMAGE}`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
for runtime in docker podman container; do
command -v "$runtime" >/dev/null || continue
printf '\n== %s ==\n' "$runtime"
"$runtime" run --help | grep -i -C 2 'publish\|port' || true
doneRepository: milind-soni/OpenMausBot Length of output: 161 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '== target file =='
cat -n server/container-computer.ts | sed -n '1,150p'
printf '%s\n' '== runtime references =='
rg -n --hidden -S 'docker|podman|container|setupCommands|6080|5900' \
server package.json README.md .github 2>/dev/null | head -250Repository: milind-soni/OpenMausBot Length of output: 6725 🌐 Web query:
💡 Result: To publish a container port to the local loopback interface (127.0.0.1) on the host, both Docker and Podman use the same syntax for the publish flag [1][2][3]. By including the IP address in the port mapping, you restrict access to the containerized service so that it is only reachable from the host machine itself, rather than from the wider network [1][4][3]. The syntax is: -p 127.0.0.1:HOST_PORT:CONTAINER_PORT For example: docker run -p 127.0.0.1:8080:80 nginx podman run -p 127.0.0.1:8080:80 nginx Key details: - By default, if you only specify the ports (e.g., -p 8080:80), the container is published on all network interfaces (0.0.0.0), making it accessible to any machine that can reach your host [5][6][3]. - Specifying 127.0.0.1 explicitly forces the binding to the loopback interface [1][7]. - You can also bind to specific IPv6 addresses using the same format by enclosing the address in brackets, for example: -p '[::1]:8080:80' [1]. - If you omit the host port but keep the IP (e.g., -p 127.0.0.1::80), the container port will be mapped to a randomly assigned ephemeral port on the host's loopback interface [2][8]. Always verify your port mappings after starting the container by running: docker port <container_name_or_id> podman port <container_name_or_id> Citations:
🌐 Web query:
💡 Result: The --publish (or -p) flag is used in container CLI tools (such as Apple's container utility) to forward traffic from a specific host port to a container port [1][2]. The supported format for this option is [host-ip:]host-port:container-port[/protocol] [1][3]. To publish a port specifically to the IPv4 loopback address (127.0.0.1), you use the following syntax: --publish 127.0.0.1:host-port:container-port For example, to forward traffic from port 8080 on your host's 127.0.0.1 interface to port 8000 inside the container, you would use: container run -d -p 127.0.0.1:8080:8000 <image_name> [1][4] Important Considerations: - Protocol: You can optionally specify the protocol (tcp or udp) by appending it to the end, for example: -p 127.0.0.1:8080:8000/tcp [1][2]. - IPv6 Loopback: To publish to the IPv6 loopback address (::1), you must enclose the address in brackets: -p '[::1]:8080:8000' [1][4]. - Connectivity Issues: If you encounter connection issues (e.g., connection reset or empty reply) when using 127.0.0.1, ensure that your container application is actually listening on 127.0.0.1 inside the container, or try binding to 0.0.0.0 (all interfaces) if necessary [4][5]. Additionally, on macOS, check that your container runtime has the required network permissions [6]. Citations:
🌐 Web query:
💡 Result: When using the Apple Citations:
Bind desktop ports to loopback addresses. Line 104 exposes the VNC and noVNC ports on all host interfaces. Use 🤖 Prompt for AI Agents |
||
| start: `${rt} start ${CONTAINER}`, | ||
| stop: `${rt} stop ${CONTAINER}`, | ||
| remove: `${rt} rm -f ${CONTAINER}`, | ||
| view: "http://localhost:6080/vnc.html", | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| // Setting up a computer your bots can use, on your own machine. | ||
| // | ||
| // Not a wall of instructions: it looks at what you actually have and only | ||
| // tells you the next thing to do. Each step reports its own state, so a | ||
| // half-finished setup is obvious instead of mysterious. | ||
| import { useCallback, useEffect, useState } from "react"; | ||
| import { AlertTriangle, Check, Circle, ExternalLink, Loader2, RefreshCw } from "lucide-react"; | ||
| import { Card, CommandLine } from "./SettingsModal"; | ||
| import { cn } from "@/lib/cn"; | ||
|
|
||
| interface Status { | ||
| runtime: string | null; | ||
| available: string[]; | ||
| daemonUp: boolean; | ||
| image: boolean; | ||
| container: "running" | "stopped" | "missing"; | ||
| image_ref: string; | ||
| container_name: string; | ||
| commands: Record<string, string>; | ||
| } | ||
|
|
||
| function Step({ | ||
| n, | ||
| title, | ||
| done, | ||
| children, | ||
| }: { | ||
| n: number; | ||
| title: string; | ||
| done: boolean; | ||
| children?: React.ReactNode; | ||
| }) { | ||
| return ( | ||
| <div className="flex gap-3"> | ||
| <div | ||
| className={cn( | ||
| "mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full text-[11px]", | ||
| done ? "bg-success/20 text-success" : "border border-hairline/50 text-ink-secondary", | ||
| )} | ||
| > | ||
| {done ? <Check size={12} /> : n} | ||
| </div> | ||
| <div className="min-w-0 flex-1"> | ||
| <div className={cn("text-[14px]", done ? "text-ink-secondary line-through" : "text-ink")}>{title}</div> | ||
| {!done && children && <div className="mt-2 flex flex-col gap-2">{children}</div>} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export function LocalComputerSection() { | ||
| const [status, setStatus] = useState<Status | null>(null); | ||
| const [loading, setLoading] = useState(true); | ||
|
|
||
| const refresh = useCallback(() => { | ||
| setLoading(true); | ||
| fetch("/api/local-computer") | ||
| .then((r) => r.json()) | ||
| .then(setStatus) | ||
| .catch(() => setStatus(null)) | ||
| .finally(() => setLoading(false)); | ||
| }, []); | ||
|
|
||
| useEffect(refresh, [refresh]); | ||
| // while the user is following the steps in a terminal, keep up with them | ||
| useEffect(() => { | ||
| const t = setInterval(refresh, 5000); | ||
| return () => clearInterval(t); | ||
| }, [refresh]); | ||
|
|
||
| const c = status?.commands ?? {}; | ||
| const ready = status?.container === "running"; | ||
|
|
||
| return ( | ||
| <> | ||
| <Card | ||
| title="A computer of its own" | ||
| subtitle="Your bots can drive a Linux desktop running on this Mac — free, disposable, and separate from your own desktop and files. It runs in a container, so nothing it does touches your machine." | ||
| > | ||
| <div className="flex items-center gap-2"> | ||
| <span | ||
| className={cn( | ||
| "flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[12.5px]", | ||
| ready ? "bg-success/15 text-success" : "bg-raised text-ink-secondary", | ||
| )} | ||
| > | ||
| {loading ? ( | ||
| <Loader2 size={12} className="animate-spin" /> | ||
| ) : ready ? ( | ||
| <Check size={12} /> | ||
| ) : ( | ||
| <Circle size={9} /> | ||
| )} | ||
| {loading ? "Checking…" : ready ? "Ready" : "Not set up yet"} | ||
| </span> | ||
| <button | ||
| onClick={refresh} | ||
| className="flex items-center gap-1.5 rounded-lg border border-hairline/40 px-2.5 py-1 text-[12.5px] text-ink-secondary hover:bg-raised hover:text-ink" | ||
| > | ||
| <RefreshCw size={12} /> Re-check | ||
| </button> | ||
| {ready && ( | ||
| <a | ||
| href={c.view} | ||
| target="_blank" | ||
| rel="noreferrer" | ||
| className="flex items-center gap-1.5 rounded-lg border border-hairline/40 px-2.5 py-1 text-[12.5px] text-ink hover:bg-raised" | ||
| > | ||
| <ExternalLink size={12} /> Watch the screen | ||
| </a> | ||
| )} | ||
| </div> | ||
| </Card> | ||
|
|
||
| <Card title="Setup" subtitle="Run these in Terminal. This panel notices when each step is done."> | ||
| <div className="flex flex-col gap-4"> | ||
| <Step n={1} title="Install a container runtime" done={Boolean(status?.runtime)}> | ||
| <div className="text-[13px] leading-relaxed text-ink-secondary"> | ||
| Any of these works — we use whichever you already have. <b className="text-ink">Colima</b> and{" "} | ||
| <b className="text-ink">Podman</b> are free for everyone; Docker Desktop needs a paid licence for | ||
| companies over 250 people or $10M revenue, and for government use. | ||
| </div> | ||
| <CommandLine command="brew install colima docker && colima start" /> | ||
| </Step> | ||
|
|
||
| <Step | ||
| n={2} | ||
| title={ | ||
| status?.runtime && !status.daemonUp | ||
| ? `Start ${status.runtime} — it's installed but not running` | ||
| : "Start the runtime" | ||
| } | ||
| done={Boolean(status?.daemonUp)} | ||
| > | ||
| <CommandLine command="colima start" /> | ||
| </Step> | ||
|
|
||
| <Step n={3} title="Download the desktop image (about 1 GB, once)" done={Boolean(status?.image)}> | ||
| <CommandLine command={c.pull ?? ""} /> | ||
| </Step> | ||
|
|
||
| <Step | ||
| n={4} | ||
| title={status?.container === "stopped" ? "Start the computer again" : "Start the computer"} | ||
| done={status?.container === "running"} | ||
| > | ||
| <CommandLine command={status?.container === "stopped" ? (c.start ?? "") : (c.run ?? "")} /> | ||
| </Step> | ||
| </div> | ||
| </Card> | ||
|
|
||
| {status && !status.runtime && ( | ||
| <Card title=""> | ||
| <div className="flex gap-2 text-[13px] text-ink-secondary"> | ||
| <AlertTriangle size={15} className="mt-0.5 shrink-0 text-warning" /> | ||
| <span> | ||
| No container runtime found on this machine yet. Step 1 installs one; everything after it is | ||
| automatic. | ||
| </span> | ||
| </div> | ||
| </Card> | ||
| )} | ||
|
|
||
| <Card | ||
| title="What this is" | ||
| subtitle={`The desktop image is ${status?.image_ref ?? "Anthropic's computer-use demo image"} — an MIT-licensed Linux desktop with a browser, a file manager and an editor, plus the tools a bot needs to see the screen and click. It runs as "${status?.container_name ?? "openmausbot-computer"}", and you can stop or delete it any time.`} | ||
| > | ||
| <div className="flex flex-col gap-2"> | ||
| <CommandLine command={c.stop ?? ""} /> | ||
| <CommandLine command={c.remove ?? ""} /> | ||
| </div> | ||
| </Card> | ||
| </> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Do not generate Docker commands when no runtime is selected.
Line 100 defaults to Docker when detection returns
null.src/components/LocalComputerSection.tsxrenderscommands.pullandcommands.run, so users without a runtime see Docker commands although the setup guidance recommends Colima or Podman. Keep runtime-specific commands unavailable until detection selects a runtime, and hide dependent setup commands until then.🤖 Prompt for AI Agents