diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d793a81e..23d98cb7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -68,3 +68,58 @@ jobs: annotations: ${{ steps.meta.outputs.annotations }} cache-from: type=gha cache-to: type=gha,mode=max + + sandbox-image: + name: Publish the sandbox image to GHCR and Docker Hub + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v5 + + - uses: docker/setup-qemu-action@v4 + + - uses: docker/setup-buildx-action@v4 + + - uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # Tagged as the app is: latest is the newest release's, and a push to + # main is edge and sha-. Sandbox providers pull it: OpenSandbox + # for each sandbox, E2B to build a workspace's template. The package must + # be public for them to. Cloudflare can't pull from GHCR, so it pulls the + # same image from Docker Hub. + - id: meta + uses: docker/metadata-action@v6 + env: + DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index + with: + images: | + ghcr.io/${{ github.repository_owner }}/sugabots-sandbox + docker.io/nitrictech/sugabots-sandbox + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=edge,branch=main + type=sha,enable=${{ github.ref == 'refs/heads/main' }} + + - uses: docker/build-push-action@v7 + with: + context: docker/sandbox + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + annotations: ${{ steps.meta.outputs.annotations }} diff --git a/.github/workflows/sandbox-preview.yml b/.github/workflows/sandbox-preview.yml new file mode 100644 index 00000000..02153d22 --- /dev/null +++ b/.github/workflows/sandbox-preview.yml @@ -0,0 +1,115 @@ +name: Sandbox image preview + +# A pull request that changes the sandbox image gets it built and published as +# ghcr.io//sugabots-sandbox:pr-, to try before it's merged, and +# says how on the pull request. Closing the pull request deletes it. Pull +# requests from forks get none: their workflows can't publish packages. + +on: + pull_request: + types: [opened, synchronize, reopened, closed] + paths: + - "docker/sandbox/**" + - ".github/workflows/sandbox-preview.yml" + +concurrency: + group: sandbox-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +env: + IMAGE: ghcr.io/${{ github.repository_owner }}/sugabots-sandbox + TAG: pr-${{ github.event.pull_request.number }} + +jobs: + publish: + name: Publish the pull request's sandbox image + if: | + github.event.action != 'closed' && + github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + pull-requests: write + + steps: + - uses: actions/checkout@v5 + + - uses: docker/setup-qemu-action@v4 + + - uses: docker/setup-buildx-action@v4 + + - uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Both platforms, as a release has, so it runs on Apple silicon too. + - id: build + uses: docker/build-push-action@v7 + with: + context: docker/sandbox + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ env.IMAGE }}:${{ env.TAG }} + cache-from: type=gha,scope=sandbox-image + cache-to: type=gha,scope=sandbox-image,mode=max + + # One comment per pull request, updated on each push, found by its marker. + - name: Say how to try it + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR: ${{ github.event.pull_request.number }} + DIGEST: ${{ steps.build.outputs.digest }} + SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + marker="" + body="$(cat </dev/null + else + gh api --method POST "repos/$GITHUB_REPOSITORY/issues/$PR/comments" -f body="$body" >/dev/null + fi + + cleanup: + name: Delete the pull request's sandbox image + if: | + github.event.action == 'closed' && + github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + + permissions: + packages: write + + steps: + - name: Delete the image version tagged for the pull request + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PACKAGE: orgs/${{ github.repository_owner }}/packages/container/sugabots-sandbox + run: | + set -euo pipefail + version_id=$(gh api "/$PACKAGE/versions?per_page=100" --paginate \ + --jq ".[] | select(.metadata.container.tags[]? == \"$TAG\") | .id" | head -n1) + if [ -z "$version_id" ]; then + echo "No image version is tagged $TAG; nothing to delete." + exit 0 + fi + echo "Deleting package version $version_id ($TAG)" + gh api --method DELETE "/$PACKAGE/versions/$version_id" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f4e1368..64ca1b27 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,6 +12,7 @@ bun run check # lint, typecheck, and test bun run build # build all packages bun run format # apply Biome formatting fixes bun run db:studio # inspect the database with Drizzle Studio +bun run build:sandbox # build the sandbox image, ghcr.io/nitrictech/sugabots-sandbox:latest ``` The API exports traces and logs to any OTLP/HTTP collector set by the tracing diff --git a/docker/sandbox/Dockerfile b/docker/sandbox/Dockerfile new file mode 100644 index 00000000..fb60bd5b --- /dev/null +++ b/docker/sandbox/Dockerfile @@ -0,0 +1,90 @@ +# The default sandbox image: Debian with what agents reach for when they work +# on code, and desktops with a browser they drive through Playwright MCP. +# Commands run without a screen; `sugabots-desktop` starts a desktop, each on +# its own display. +# +# bun run build:sandbox +# +# Builds ghcr.io/nitrictech/sugabots-sandbox:latest, where releases publish it +# too, and to docker.io/nitrictech/sugabots-sandbox: the image OpenSandbox +# providers use unless a workspace sets another, and the one E2B templates are +# built from. The agents' user is the provider's to +# make: OpenSandbox's when it makes a sandbox, and E2B's own `user`, uid 1000, +# so this image has none of its own to clash with them. + +ARG PLAYWRIGHT_MCP_VERSION=0.0.83 + +# The desktop's wallpaper, rendered at the display's size, so the renderer +# doesn't ship in the image. +FROM debian:trixie-slim AS wallpaper +RUN apt-get update \ + && apt-get install -y --no-install-recommends librsvg2-bin \ + && rm -rf /var/lib/apt/lists/* +COPY wallpaper.svg /wallpaper.svg +RUN rsvg-convert --width 1280 --height 800 /wallpaper.svg --output /wallpaper.png + +# The dock's icons, from Papirus, without the rest of its 380 MB. +FROM debian:trixie-slim AS dock-icons +RUN apt-get update \ + && apt-get install -y --no-install-recommends papirus-icon-theme \ + && rm -rf /var/lib/apt/lists/* +RUN mkdir /icons \ + && cd /usr/share/icons/Papirus/64x64/apps \ + && cp -L chromium.svg /icons/browser.svg \ + && cp -L utilities-terminal.svg /icons/terminal.svg \ + && cp -L system-file-manager.svg /icons/files.svg + +FROM debian:trixie-slim + +ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8 + +# The tools agents use on a repository, then the desktop: a virtual display, +# a window manager and dock, a browser, a file manager, and what computer-use +# tools drive them with. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash ca-certificates curl wget git openssh-client less file procps \ + unzip zip xz-utils jq ripgrep fd-find tree \ + build-essential pkg-config \ + python3 python3-pip python3-venv \ + nodejs npm \ + xvfb x11vnc x11-utils openbox plank dconf-gsettings-backend dconf-cli xcompmgr xterm \ + xfonts-base dbus-x11 hsetroot \ + lxterminal pcmanfm adwaita-icon-theme librsvg2-common \ + nix-bin \ + chromium xdotool scrot xclip fonts-dejavu fonts-noto-color-emoji \ + && ln -s /usr/bin/fdfind /usr/local/bin/fd \ + && rm -rf /var/lib/apt/lists/* + +# The browser agents drive: Playwright MCP, on Debian's Chromium rather than +# a browser Playwright downloads. +ARG PLAYWRIGHT_MCP_VERSION +RUN npm install --global --omit=dev "@playwright/mcp@${PLAYWRIGHT_MCP_VERSION}" \ + && npm cache clean --force + +COPY --from=wallpaper /wallpaper.png /usr/share/sugabots-desktop/wallpaper.png +COPY --from=dock-icons /icons /usr/share/sugabots-desktop/icons +# The dock's launchers stand in for the apps' own, so the dock matches their +# windows to them, and its settings are the system's dconf defaults. +COPY dock/*.desktop /usr/share/applications/ +COPY dock/plank/ /usr/share/sugabots-desktop/plank/ +COPY dock/plank-theme/dock.theme /usr/share/plank/themes/Sugabots/dock.theme +COPY dock/plank.dconf /etc/dconf/db/local.d/00-plank +RUN mkdir -p /etc/dconf/profile \ + && printf 'user-db:user\nsystem-db:local\n' >/etc/dconf/profile/user \ + && dconf update +# Nix, for software beyond this image: `nix profile install nixpkgs#ffmpeg`, +# or `nix shell nixpkgs#ffmpeg` for one command. The agents' user owns the +# store, so it installs without root. `nixpkgs` follows a NixOS release and +# `nixpkgs-unstable` its newer versions, by branch, so the image never needs +# moving forward; whatever pins exact versions records the commit it used. +COPY nix/nix.conf nix/registry.json /etc/nix/ +# Nix looks for its state here, and without it falls back to a store in the +# user's home that needs namespaces the agents' user can't make. +RUN mkdir -p /nix/store /nix/var/nix && chown -R 1000:1000 /nix + +COPY sugabots-desktop /usr/local/bin/sugabots-desktop + +RUN mkdir -p /workspace && chmod 755 /usr/local/bin/sugabots-desktop + +WORKDIR /workspace diff --git a/docker/sandbox/dock/chromium.desktop b/docker/sandbox/dock/chromium.desktop new file mode 100644 index 00000000..39d2db6a --- /dev/null +++ b/docker/sandbox/dock/chromium.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Type=Application +Name=Browser +Comment=The agent's browser +Exec=sugabots-desktop open browser +Icon=/usr/share/sugabots-desktop/icons/browser.svg +StartupWMClass=sugabots-browser diff --git a/docker/sandbox/dock/lxterminal.desktop b/docker/sandbox/dock/lxterminal.desktop new file mode 100644 index 00000000..e346fb94 --- /dev/null +++ b/docker/sandbox/dock/lxterminal.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Type=Application +Name=Terminal +Comment=A shell in the workspace +Exec=sugabots-desktop open terminal +Icon=/usr/share/sugabots-desktop/icons/terminal.svg +StartupWMClass=lxterminal diff --git a/docker/sandbox/dock/pcmanfm.desktop b/docker/sandbox/dock/pcmanfm.desktop new file mode 100644 index 00000000..d182cb7f --- /dev/null +++ b/docker/sandbox/dock/pcmanfm.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Type=Application +Name=Files +Comment=The workspace's files +Exec=sugabots-desktop open files +Icon=/usr/share/sugabots-desktop/icons/files.svg +StartupWMClass=pcmanfm diff --git a/docker/sandbox/dock/plank-theme/dock.theme b/docker/sandbox/dock/plank-theme/dock.theme new file mode 100644 index 00000000..6957e323 --- /dev/null +++ b/docker/sandbox/dock/plank-theme/dock.theme @@ -0,0 +1,35 @@ +# The dock's look: a rounded, light, see-through bar the icons sit inside, +# in the style of macOS's. Sizes are tenths of the icon size. +[PlankTheme] +TopRoundness=4 +BottomRoundness=4 +LineWidth=1 +OuterStrokeColor=255;;255;;255;;110 +FillStartColor=245;;245;;247;;150 +FillEndColor=235;;235;;240;;150 +InnerStrokeColor=255;;255;;255;;0 + +[PlankDockTheme] +HorizPadding=1.5 +TopPadding=1.2 +BottomPadding=1.2 +ItemPadding=2.5 +IndicatorSize=4 +IconShadowSize=1 +UrgentBounceHeight=1.6666666666666667 +LaunchBounceHeight=0.625 +FadeOpacity=1 +ClickTime=300 +UrgentBounceTime=600 +LaunchBounceTime=600 +ActiveTime=300 +SlideTime=300 +FadeTime=250 +HideTime=250 +GlowSize=30 +GlowTime=10000 +GlowPulseTime=2000 +UrgentHueShift=150 +ItemMoveTime=450 +CascadeHide=true +BadgeColor=0;;0;;0;;0 diff --git a/docker/sandbox/dock/plank.dconf b/docker/sandbox/dock/plank.dconf new file mode 100644 index 00000000..1d3ed833 --- /dev/null +++ b/docker/sandbox/dock/plank.dconf @@ -0,0 +1,13 @@ +# The dock's settings, as the system's defaults: Plank reads them from dconf, +# by path, so they need no write at runtime. +[net/launchpad/plank/docks/dock1] +dock-items=['chromium.dockitem', 'lxterminal.dockitem', 'pcmanfm.dockitem'] +position='bottom' +alignment='center' +hide-mode='none' +icon-size=48 +zoom-enabled=true +zoom-percent=140 +lock-items=true +show-dock-item=false +theme='Sugabots' diff --git a/docker/sandbox/dock/plank/chromium.dockitem b/docker/sandbox/dock/plank/chromium.dockitem new file mode 100644 index 00000000..2884b731 --- /dev/null +++ b/docker/sandbox/dock/plank/chromium.dockitem @@ -0,0 +1,2 @@ +[PlankDockItemPreferences] +Launcher=file:///usr/share/applications/chromium.desktop diff --git a/docker/sandbox/dock/plank/lxterminal.dockitem b/docker/sandbox/dock/plank/lxterminal.dockitem new file mode 100644 index 00000000..e9644667 --- /dev/null +++ b/docker/sandbox/dock/plank/lxterminal.dockitem @@ -0,0 +1,2 @@ +[PlankDockItemPreferences] +Launcher=file:///usr/share/applications/lxterminal.desktop diff --git a/docker/sandbox/dock/plank/pcmanfm.dockitem b/docker/sandbox/dock/plank/pcmanfm.dockitem new file mode 100644 index 00000000..bc3af9d3 --- /dev/null +++ b/docker/sandbox/dock/plank/pcmanfm.dockitem @@ -0,0 +1,2 @@ +[PlankDockItemPreferences] +Launcher=file:///usr/share/applications/pcmanfm.desktop diff --git a/docker/sandbox/nix/nix.conf b/docker/sandbox/nix/nix.conf new file mode 100644 index 00000000..027ffb30 --- /dev/null +++ b/docker/sandbox/nix/nix.conf @@ -0,0 +1,12 @@ +# Nix for the agents' user alone: no daemon and no build users, since that +# user (uid 1000 on every provider) owns /nix and has no root. +build-users-group = +experimental-features = nix-command flakes +# Nix's build sandbox needs namespaces the agents' user can't make. Packages +# come built from the binary cache, so it only matters for one built here. +sandbox = false +substituters = https://cache.nixos.org +trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= +# Only the nixpkgs entries in /etc/nix/registry.json, not the global +# registry, which Nix would fetch from a host sandboxes don't reach. +flake-registry = diff --git a/docker/sandbox/nix/registry.json b/docker/sandbox/nix/registry.json new file mode 100644 index 00000000..8e73871a --- /dev/null +++ b/docker/sandbox/nix/registry.json @@ -0,0 +1,13 @@ +{ + "version": 2, + "flakes": [ + { + "from": { "type": "indirect", "id": "nixpkgs" }, + "to": { "type": "github", "owner": "NixOS", "repo": "nixpkgs", "ref": "nixos-26.05" } + }, + { + "from": { "type": "indirect", "id": "nixpkgs-unstable" }, + "to": { "type": "github", "owner": "NixOS", "repo": "nixpkgs", "ref": "nixos-unstable" } + } + ] +} diff --git a/docker/sandbox/sugabots-desktop b/docker/sandbox/sugabots-desktop new file mode 100755 index 00000000..3f096825 --- /dev/null +++ b/docker/sandbox/sugabots-desktop @@ -0,0 +1,207 @@ +#!/bin/bash +# Desktops in this sandbox, each on its own display, so agents working at the +# same time never click on each other's windows. +# +# sugabots-desktop start [display] start a bare desktop, print its DISPLAY +# sugabots-desktop stop display stop it, and its browser +# sugabots-desktop desktop name profile output the desktop for `name`, without a browser +# sugabots-desktop session name profile output the desktop for `name`, with its browser +# sugabots-desktop viewer name the viewer port of `name`'s desktop, if running +# sugabots-desktop open browser|terminal|files what the dock opens, on $DISPLAY +# +# `desktop` is what Sugabots runs when a person opens an agent's desktop, and +# `session` when the agent uses its browser. Both start the named desktop, or +# find it running: a display with a window manager and a dock, served to VNC +# viewers. `session` also starts the browser on it, or finds it running, and +# prints `display=N viewer=PORT browser=PORT started=yes|no`, where `started` +# says whether the browser had to start; `desktop` prints +# `display=N viewer=PORT started=yes|no` for the desktop. +# +# The browser is Chromium driven by Playwright MCP, shown on the desktop so +# people watching see what the agent does, with its profile under `profile` +# and its downloads and screenshots in `output`. It lives until the desktop +# stops, across the MCP sessions that drive it, and the dock's Browser starts +# it if the agent hasn't. Programs that stopped, as they do when a paused +# sandbox comes back, start again on the same display. +# +# Display N is served on port 5900+N to a VNC viewer, over plain VNC or a +# WebSocket, which x11vnc accepts on the same port. Its browser's MCP server +# is on port 8930+N, and the browser's DevTools on port 9230+N. +set -euo pipefail + +# Set here rather than relied on from the image: E2B runs commands without +# the image's ENV. +export LANG="${LANG:-C.UTF-8}" + +state="${TMPDIR:-/tmp}/sugabots-desktop" +mkdir -p "$state/sessions" + +free_display() { + local n=1 + while [ -e "/tmp/.X11-unix/X$n" ] || [ -e "/tmp/.X$n-lock" ] || [ -n "$(grep -lx "$n" "$state"/sessions/* 2>/dev/null)" ]; do + n=$((n + 1)) + done + echo "$n" +} + +# Whether every program of display N's `part`, desktop or browser, is running. +running() { + local pids="$state/$1.$2.pids" + [ -f "$pids" ] || return 1 + while read -r pid; do kill -0 "$pid" 2>/dev/null || return 1; done <"$pids" +} + +desktop() { + local n="$1" log="$state/$1.log" pids="$state/$1.desktop.pids" + export DISPLAY=":$n" + rm -f "/tmp/.X$n-lock" "/tmp/.X11-unix/X$n" + # -ac: any program in this sandbox may draw on it. -s 0: no screensaver, + # which would blank the screen after ten minutes without keyboard or mouse + # input, and agents drive the browser through Playwright, not the X server. + Xvfb "$DISPLAY" -screen 0 1280x800x24 -ac -s 0 -nolisten tcp >>"$log" 2>&1 & + echo $! >"$pids" + for _ in $(seq 50); do [ -e "/tmp/.X11-unix/X$n" ] && break; sleep 0.1; done + # The dock matches windows to its apps through a service on the session bus. + eval "$(dbus-launch --sh-syntax)" + echo "$DBUS_SESSION_BUS_PID" >>"$pids" + hsetroot -fill /usr/share/sugabots-desktop/wallpaper.png >>"$log" 2>&1 + openbox >>"$log" 2>&1 & + echo $! >>"$pids" + # For the dock's shadows and see-through background. + xcompmgr -n >>"$log" 2>&1 & + echo $! >>"$pids" + # The dock's items are read from the home it runs with. + mkdir -p "$HOME/.config/plank/dock1/launchers" + cp -n /usr/share/sugabots-desktop/plank/*.dockitem "$HOME/.config/plank/dock1/launchers/" + plank >>"$log" 2>&1 & + echo $! >>"$pids" + # No password: the ports are reached only through Sugabots. + x11vnc -display "$DISPLAY" -nopw -forever -shared -quiet -rfbport $((5900 + n)) >>"$log" 2>&1 & + echo $! >>"$pids" +} + +browser() { + local n="$1" profile="$2" output="$3" log="$state/$1.log" pids="$state/$1.browser.pids" + local devtools=$((9230 + n)) + mkdir -p "$profile" "$output" + # Started here rather than by Playwright MCP, which closes a browser it + # launched when its last client disconnects: at the end of every turn. + # Maximized, so it stops at the dock. --test-type leaves out the bar + # warning that --no-sandbox is on, which a container needs. The profile is + # found through XDG_CONFIG_HOME rather than --user-data-dir, which would + # put its path in the windows' names, and the dock would no longer know + # them as the Browser's. + DISPLAY=":$n" XDG_CONFIG_HOME="$profile" chromium --no-sandbox --test-type --start-maximized \ + --class=sugabots-browser \ + --remote-debugging-address=127.0.0.1 --remote-debugging-port="$devtools" \ + --no-first-run --no-default-browser-check about:blank >>"$log" 2>&1 & + echo $! >"$pids" + for _ in $(seq 100); do + curl -sf -o /dev/null "http://127.0.0.1:$devtools/json/version" && break + sleep 0.1 + done + playwright-mcp --host 0.0.0.0 --port $((8930 + n)) --allowed-hosts '*' \ + --cdp-endpoint "http://127.0.0.1:$devtools" \ + --output-dir "$output" --file-paths absolute >>"$log" 2>&1 & + echo $! >>"$pids" + for _ in $(seq 100); do + curl -s -o /dev/null "http://localhost:$((8930 + n))/mcp" && break + sleep 0.1 + done +} + +stop_part() { + local pids="$state/$1.$2.pids" + [ -f "$pids" ] || return 0 + xargs -r kill <"$pids" 2>/dev/null || true + rm -f "$pids" +} + +stop() { + local n="${1:?which display}" + stop_part "$n" browser + stop_part "$n" desktop +} + +# The display of the desktop named `name`, running, with where its browser +# keeps its profile and output recorded for the dock. Sets `desktop_started`. +named_desktop() { + local name="$1" profile="$2" output="$3" + local file="$state/sessions/$name" + if [ -f "$file" ]; then n="$(cat "$file")"; else n="$(free_display)"; echo "$n" >"$file"; fi + printf '%s\n%s\n' "$profile" "$output" >"$state/$n.browser-dirs" + desktop_started=no + if ! running "$n" desktop; then + # The browser can't outlive the display it was on. + stop "$n" + desktop "$n" + desktop_started=yes + fi +} + +desktop_only() { + named_desktop "${1:?name}" "${2:?profile directory}" "${3:?output directory}" + echo "display=$n viewer=$((5900 + n)) started=$desktop_started" +} + +session() { + named_desktop "${1:?name}" "${2:?profile directory}" "${3:?output directory}" + local started=no + if ! running "$n" browser; then + stop_part "$n" browser + browser "$n" "$2" "$3" + started=yes + fi + echo "display=$n viewer=$((5900 + n)) browser=$((8930 + n)) started=$started" +} + +viewer() { + local file="$state/sessions/${1:?name}" + [ -f "$file" ] || exit 3 + local n + n="$(cat "$file")" + running "$n" desktop || exit 3 + echo $((5900 + n)) +} + +# What the dock opens on the desktop it runs on. +open() { + local n="${DISPLAY#:}" + case "${1:-}" in + browser) + local profile output + { read -r profile; read -r output; } <"$state/$n.browser-dirs" + if running "$n" browser; then + # The same profile reaches the running browser, which opens the window. + XDG_CONFIG_HOME="$profile" exec chromium --no-sandbox --test-type --new-window + fi + stop_part "$n" browser + browser "$n" "$profile" "$output" + ;; + terminal) + exec lxterminal --working-directory=/workspace + ;; + files) exec pcmanfm /workspace ;; + *) + echo "usage: sugabots-desktop open browser|terminal|files" >&2 + exit 2 + ;; + esac +} + +case "${1:-}" in +start) + n="${2:-$(free_display)}" + desktop "$n" + echo "DISPLAY=:$n" + ;; +stop) stop "${2:-}" ;; +desktop) desktop_only "${2:-}" "${3:-}" "${4:-}" ;; +session) session "${2:-}" "${3:-}" "${4:-}" ;; +viewer) viewer "${2:-}" ;; +open) open "${2:-}" ;; +*) + echo "usage: sugabots-desktop start [display] | stop display | desktop name profile output | session name profile output | viewer name | open browser|terminal|files" >&2 + exit 2 + ;; +esac diff --git a/docker/sandbox/wallpaper.svg b/docker/sandbox/wallpaper.svg new file mode 100644 index 00000000..790e031f --- /dev/null +++ b/docker/sandbox/wallpaper.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/package.json b/package.json index 270f62f8..37bc4c20 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "db:push": "bun run --cwd packages/core db:push", "db:migrate": "bun run --cwd packages/core db:migrate", "db:studio": "bun run --cwd packages/core db:studio", - "db:seed": "node --env-file-if-exists=.env packages/server/src/seed.ts" + "db:seed": "node --env-file-if-exists=.env packages/server/src/seed.ts", + "build:sandbox": "docker build -t ghcr.io/nitrictech/sugabots-sandbox:latest docker/sandbox" }, "devDependencies": { "@biomejs/biome": "2.5.12",