Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 55 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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-<commit>. 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
Comment thread
tjholm marked this conversation as resolved.
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 }}
115 changes: 115 additions & 0 deletions .github/workflows/sandbox-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
name: Sandbox image preview

# A pull request that changes the sandbox image gets it built and published as
# ghcr.io/<owner>/sugabots-sandbox:pr-<number>, 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="<!-- sandbox-image-preview -->"
body="$(cat <<EOF
$marker
**Sandbox image preview**, built from ${SHA:0:7}:

\`\`\`
$IMAGE:$TAG
\`\`\`

To try it, set a sandbox provider's image to that under Sandboxes in the workspace's settings, then Upgrade the pod's sandbox; on E2B, prepare the template again first. The tag moves with each push to this pull request, so a machine that pulled it before may need \`docker pull\` again; for this exact build, use \`$IMAGE@$DIGEST\`.

It's deleted when the pull request closes.
EOF
)"
comment_id=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR/comments" --paginate \
--jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -n1)
if [ -n "$comment_id" ]; then
gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$comment_id" -f body="$body" >/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"
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
90 changes: 90 additions & 0 deletions docker/sandbox/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions docker/sandbox/dock/chromium.desktop
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions docker/sandbox/dock/lxterminal.desktop
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions docker/sandbox/dock/pcmanfm.desktop
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions docker/sandbox/dock/plank-theme/dock.theme
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions docker/sandbox/dock/plank.dconf
Original file line number Diff line number Diff line change
@@ -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'
2 changes: 2 additions & 0 deletions docker/sandbox/dock/plank/chromium.dockitem
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[PlankDockItemPreferences]
Launcher=file:///usr/share/applications/chromium.desktop
2 changes: 2 additions & 0 deletions docker/sandbox/dock/plank/lxterminal.dockitem
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[PlankDockItemPreferences]
Launcher=file:///usr/share/applications/lxterminal.desktop
2 changes: 2 additions & 0 deletions docker/sandbox/dock/plank/pcmanfm.dockitem
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[PlankDockItemPreferences]
Launcher=file:///usr/share/applications/pcmanfm.desktop
12 changes: 12 additions & 0 deletions docker/sandbox/nix/nix.conf
Original file line number Diff line number Diff line change
@@ -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 =
13 changes: 13 additions & 0 deletions docker/sandbox/nix/registry.json
Original file line number Diff line number Diff line change
@@ -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" }
}
]
}
Loading
Loading