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
18 changes: 16 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,23 @@ COPILOTKIT_LICENSE_TOKEN=
# 0, or leaving this unset, switches the watchdog off. Nothing is watched and no turn is ever ended.
AGENT_STALL_TIMEOUT_MS=60000

# Local Codex compatibility mode. This runs the host-side AG-UI adapter on port 4202, reuses the
# account already authenticated by `codex login`, and skips the two API-key Bot containers. Its
# Codex threads survive adapter restarts. Every side-effecting tool call returns through OpenBot's
# signed gateway, where the current grant, policy and audit trail are applied. MCP, app, plugin and
# web capabilities are disabled; turns are sandboxed read-only without network access, and the
# adapter interrupts any native shell or file action Codex still attempts.
#
# CODEX_AGENT_ENABLED=true
# CODEX_AGENT_PORT=4202
# CODEX_AGENT_WORKSPACE=.openbot-codex/workspace
# CODEX_AGENT_STATE=.openbot-codex/threads.json
# OPENBOT_TOOL_URL=http://localhost:3001/api/agent-tools/call
# AGENT_ENDPOINT_ALLOWED_HOSTS=localhost:4202

# Model key. Required by the proof-of-concept Bot, which speaks OpenAI's API directly, and by the
# framework Bot unless you point it at another provider below.
# framework Bot unless you enable the local Codex compatibility mode above or point it at another
# provider below.
OPENAI_API_KEY=

# Where that key is spent. Unset, it is OpenAI. Set, it is any endpoint speaking the same
Expand Down Expand Up @@ -329,4 +344,3 @@ AGENT_TOOL_TOKEN=
# for a deployment that has not stood up a worker. Set for one that has: openssl rand -base64 32.
# Do not accept a default in production.
WORKER_SHARED_SECRET=

21 changes: 19 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,19 @@ jobs:
# question: every secret key a container demands has to be one the chart actually writes.
# Getting that wrong is invisible until a pod starts, and every shipped target had it wrong.
- run: bun scripts/check-rendered-chart.ts rendered.yaml
- name: Render VM computer mode
if: matrix.target == 'eks-sandbox'
run: |
helm template ci charts/openbot \
--values charts/openbot/ci/eks-sandbox-values.yaml \
--set computers.mode=vm \
--set computers.runtimeClassName=kata-qemu \
--set-string secrets.keyEncryptionKey="$(openssl rand -base64 32)" \
--api-versions agents.x-k8s.io/v1beta1/Sandbox \
--api-versions extensions.agents.x-k8s.io/v1beta1/SandboxTemplate \
> rendered-vm.yaml
grep -q 'runtimeClassName: kata-qemu' rendered-vm.yaml
bun scripts/check-rendered-chart.ts rendered-vm.yaml
# And that the refusals are load-bearing rather than decorative. A chart full of `fail`
# messages nothing ever triggers is a chart that has never been shown to refuse anything, and
# every one of these describes a state that shipped in a values file at some point.
Expand Down Expand Up @@ -152,6 +165,10 @@ jobs:
# A browser inside every replica of a replicated API.
refuses "an embedded browser across several replicas" \
--set server.embeddedComputer=true --set server.replicaCount=2
# A VM label that quietly launches an ordinary shared-kernel pod would be worse than not
# offering the mode. The runtime is the boundary and must be explicit.
refuses "VM computer mode without a VM-backed RuntimeClass" \
--set computers.mode=vm --set-string computers.runtimeClassName=
# A Bot's egress proxy on a port the computer's own network policy does not allow. The
# variables reach the computer through extraEnv, so nothing else notices that the policy
# then refuses to let it be reached.
Expand All @@ -161,11 +178,11 @@ jobs:
--set-string computers.extraEnv[0].value=http://proxy.internal:3128
# A warm pool nothing claims from. Only meaningful where the target asks for per-Bot
# computers; on the others the mode is not sandbox and the refusal is not armed.
if grep -qE '^ *mode: sandbox' charts/openbot/ci/${{ matrix.target }}-values.yaml; then
if grep -qE '^ *mode: (sandbox|vm)' charts/openbot/ci/${{ matrix.target }}-values.yaml; then
refuses "a warm pool no Bot can be handed a computer from" \
--set computers.sandbox.warmPool.enabled=true
else
echo "skipped: the warm-pool refusal is only armed for computers.mode: sandbox"
echo "skipped: the warm-pool refusal is only armed for per-Bot computer modes"
fi
# And that a values key this chart did not used to have still renders when it is absent.
#
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ jobs:
run: |
set -euo pipefail
test "v$(bun -e 'console.log(require("./package.json").version)')" = "$VERSION"
grep -qx "appVersion: \"${VERSION#v}\"" charts/openbot/Chart.yaml || {
echo "::error::The Helm chart does not point at ${VERSION}."
exit 1
}
grep -q "^## ${VERSION#v}$" CHANGELOG.md || {
echo "::error::CHANGELOG.md has no section for ${VERSION#v}."
exit 1
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ jobs:
' -- "$BUMP" > /tmp/version
version=$(cat /tmp/version)

# The chart's default image must be the image this release builds. Leaving appVersion on
# the previous release makes new templates execute against an older filesystem, which can
# turn a valid chart upgrade into a command-not-found CrashLoop.
sed -i -E "s/^appVersion: .*/appVersion: \"$version\"/" charts/openbot/Chart.yaml
grep -qx "appVersion: \"$version\"" charts/openbot/Chart.yaml

# Unreleased becomes the version, and a fresh empty Unreleased takes its place so the next
# change has somewhere to go without anyone hand-editing a heading.
bun -e '
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ node_modules/
app/src/lib/generated/application-config.ts
.logs/
.demo-logs/
.openbot-codex/
**/.impeccable
.wave-state.md

Expand All @@ -28,3 +29,4 @@ app/.tanstack/
# it is what makes that fetch reproducible, and ignoring it meant every build resolved the dependency
# afresh, so CI and a customer install could take different subchart versions with no diff to show it.
charts/*/charts/
.pstack/
49 changes: 49 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,55 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.

## Unreleased

### People and Bots can use their own avatars

The signed-in person can upload, replace, or remove a PNG, JPEG, or WebP avatar from Settings. Bot
owners can do the same from the coworker dialog, and deployment administrators can brand packaged
Bots without gaining permission to rewrite their executable profiles. Custom images now follow the
person or Bot through the sidebar, channel roster, recipients, handoffs, and profile surfaces, while
removal returns to the identity-provider image, initials, or generated Bot avatar.

Uploads are authenticated, limited to 2 MB and safe image dimensions, checked against their actual
file signature, and served from short private versioned URLs rather than copied into roster JSON.

### Computer use is a whole graphical computer, with a dock that survives closing the browser

A Bot with an isolated computer now exposes its full Linux desktop through the guarded OpenBot
screen: browser chrome, tabs, native dialogs, a terminal, and the desktop itself rather than only a
CDP canvas of one page. The small dock contains exactly the applications the image provides — the
managed Browser and a Terminal in the durable workspace. Closing Chromium with its own **X** leaves
the desktop running, records that the browser closed, and the Browser icon opens the same Bot profile
again instead of leaving the computer in a dead state. Non-default desktop sizes are letterboxed
rather than stretched.

The full framebuffer is offered only by a per-Bot provider. A shared computer keeps the Bot-scoped
page stream, because its process-wide desktop could expose another Bot's window. Kubernetes
`computers.mode: sandbox` gives each Bot its own container; `vm` requires the configured Kata runtime
class for the VM boundary. Older computer images also fall back to the page stream during a rolling
upgrade. The chart is now 0.2.0, and release automation keeps its default `appVersion` on the exact
OpenBot image being published.

Watching remains read-only. Taking control now returns a random, short-lived capability held only by
that browser tab; the computer checks it again on the WebSocket and on every mouse or keyboard event.
A second authorized viewer can watch but cannot inject input, replace the first person's takeover, or
reuse the public “human is driving” state as permission. Closing the tab lets the lease expire and
returns the wheel to the Bot.

### A local Codex coworker keeps its conversation and uses OpenBot's governed tools

OpenBot can now run a coworker through the Codex app already signed in on the host, without an API
key. Its conversation survives adapter and app-server restarts: OpenBot records the Codex thread it
owns, resumes that exact thread, and refuses to silently replace unreadable recovery state.

Connector calls do not go around the deployment. Codex sees only the tools OpenBot assigned to that
coworker, and every call returns with the deployment's signed run assertion through the existing
grant, policy and audit gateway. Native Codex shell, file, web, browser, computer, app, plugin and MCP
paths are disabled before a turn can begin, the child process receives none of OpenBot's tool,
computer, database or provider credentials, and the remaining turn is sandboxed read-only without
network access. A post-start interruption remains as an alarm if a disabled native path is ever
reported. Rejected, stale and duplicate callbacks are reported as failed tool results rather than
being carried out.

### Coworkers are made in a wizard and managed in a dialog

Creating a coworker is now a three-step wizard — who it is, who may see it, then where it runs,
Expand Down
32 changes: 24 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
# is meant to run on.
#
# The supervisor. It exists to give each Bot its own container, which needs a Docker socket, which
# no serverless container platform permits. Without it every Bot shares the browser below, exactly
# as they do on a laptop with no supervisor configured. Per-Bot isolation is A6.
# no serverless container platform permits. Without it every Bot shares the desktop below, exactly
# as they do on a laptop with no supervisor configured. Kubernetes deployments use
# `computers.mode: sandbox` or `vm` for one isolated computer per Bot.
#
# THE BASE IS PLAYWRIGHT'S, not Bun's, because Chromium and its system libraries have to stay
# matched and that image is the only place that is guaranteed. The tag must move with the
Expand All @@ -28,7 +29,17 @@ ARG BUN_VERSION=1.3.14
# root's home. Set before the install, or the installer has already chosen the wrong directory.
ENV BUN_INSTALL=/usr/local
ENV PATH="/usr/local/bin:${PATH}"
RUN apt-get update && apt-get install -y --no-install-recommends unzip xz-utils \
RUN apt-get update && apt-get install -y --no-install-recommends \
dbus-x11 \
openbox \
tint2 \
unzip \
websockify \
x11vnc \
x11-xserver-utils \
xterm \
xz-utils \
xvfb \
&& rm -rf /var/lib/apt/lists/* \
&& curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}"

Expand All @@ -41,6 +52,7 @@ WORKDIR /src
COPY package.json bun.lock ./
COPY tsconfig.base.json bunfig.toml ./
COPY app/package.json app/package.json
COPY agent-codex/package.json agent-codex/package.json
COPY server/package.json server/package.json
COPY worker/package.json worker/package.json
RUN bun install --frozen-lockfile
Expand All @@ -57,8 +69,9 @@ RUN cd agent-computer && bun install --frozen-lockfile
# biome and the test tooling are a gigabyte that nothing in a running container imports.
RUN mkdir -p /prod && cp package.json bun.lock /prod/ \
&& cp -r app/package.json /prod/app-package.json \
&& cd /prod && mkdir -p app server worker \
&& cd /prod && mkdir -p app agent-codex server worker \
&& cp /src/app/package.json app/package.json \
&& cp /src/agent-codex/package.json agent-codex/package.json \
&& cp /src/server/package.json server/package.json \
&& cp /src/worker/package.json worker/package.json \
&& bun install --frozen-lockfile --production
Expand Down Expand Up @@ -111,6 +124,8 @@ COPY shared shared
COPY examples examples
COPY agent-computer/src agent-computer/src
COPY agent-computer/package.json agent-computer/package.json
COPY agent-computer/entrypoint.sh agent-computer/entrypoint.sh
RUN chmod +x agent-computer/entrypoint.sh

# The built app, served by the API on the same origin. There is no CORS in this server, so this is
# not a convenience: two origins would simply fail.
Expand Down Expand Up @@ -147,10 +162,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# the tool description tells a model to run. `sudo cat /proc/1/environ` does not.
#
# WHAT THIS IS NOT. It is a floor, not a boundary. Root is one CVE away and a shared container is not
# an isolation story for code a model wrote: that needs a computer per Bot and a sandbox under it,
# which is why per-Bot computers and gVisor are not optional extras next to this feature. Run the
# image with `--security-opt no-new-privileges` where the platform allows, which turns setuid off
# entirely for anything not named here.
# an isolation story for code a model wrote: that needs a computer per Bot and a sandboxed runtime
# under it. Kubernetes deployments get that with `computers.mode: sandbox`, or `vm` plus a Kata
# runtime class for a hardware-virtualized boundary. Run the image with
# `--security-opt no-new-privileges` where the platform allows, which turns setuid off entirely for
# anything not named here.
RUN apt-get update && apt-get install -y --no-install-recommends sudo \
&& rm -rf /var/lib/apt/lists/* \
&& printf '%s\n' \
Expand Down
Loading