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: 9 additions & 9 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -208,15 +208,15 @@ jobs:
# the tag meant.
#
# zstd, not gzip. A pull is already a compressed transfer, so this is not about compressing
# something that was not; it is a better algorithm for the same job. Measured on
# `agent-computer`, the image that matters: 962 MB becomes 886 MB, and it inflates several
# times faster, which is worth more than the 8% on 2 GB of Chromium. `force-compression`
# is what reaches the layers that came from somebody else's registry, which is where almost
# all of the bytes are; without it only our own thin layers change and the saving rounds to
# nothing. The cost is that these images no longer share layers with a gzip pull of the same
# base, and that a client which cannot read zstd cannot read them, which is why this is here
# and not on the `openbot` image above: that one is pulled by servers with whatever they have,
# and these are pulled by an installer that ships Podman.
# something that was not; it is a better algorithm for the same job. `force-compression`
# reaches layers that came from somebody else's registry; without it only our own thin layers
# change and the saving rounds to nothing. The cost is that these images no longer share layers
# with a gzip pull of the same base, and that a client which cannot read zstd cannot read them,
# which is why this is here and not on the `openbot` image above: that one is pulled by servers
# with whatever they have, and these are pulled by an installer that ships Podman. After the
# Chromium-only `agent-computer` Dockerfile change, while keeping Node/npm/npx from the
# official Node 24.18.1 image, local zstd OCI layer descriptors measured 535.0 MiB on arm64
# and 518.1 MiB on amd64.
- id: push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
Expand Down
36 changes: 21 additions & 15 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,29 @@
# 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.
#
# 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
# `playwright` dependency in `agent-computer/package.json`. Bump both or neither.

FROM mcr.microsoft.com/playwright:v1.62.1-noble AS base

# unzip is not in the Playwright image and bun's installer needs it.
# Bun is pinned. The installer takes whatever is newest otherwise, so the runtime drifts from the
# one the lockfile was resolved against and an image built next month is not the image built today.
ARG BUN_VERSION=1.3.14
# Into /usr/local rather than /root/.bun, because the runtime stage runs as `pwuser` and cannot read
# root's home. Set before the install, or the installer has already chosen the wrong directory.
# Chromium comes from Playwright's own installer, but the final image is not Playwright's all-browser
# image. Keep this version matched to `agent-computer/package.json`: bump both or neither.

FROM node:24.18.1-bookworm-slim AS node-toolchain
FROM oven/bun:1.3.14@sha256:e10577f0db68676a7024391c6e5cb4b879ebd17188ab750cf10024a6d700e5c4 AS bun-toolchain

FROM ubuntu:24.04 AS base

# The Bun image digest pins the amd64/arm64 release bytes, including if its tag changes.
ARG PLAYWRIGHT_VERSION=1.62.1
# Keep Bun and global installs readable by the runtime's unprivileged user.
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 \
&& rm -rf /var/lib/apt/lists/* \
&& curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}"
ENV DEBIAN_FRONTEND=noninteractive
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
COPY --from=node-toolchain /usr/local /usr/local
COPY --from=bun-toolchain /usr/local/bin/bun /usr/local/bin/bun
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl unzip xz-utils \
&& ln -s bun /usr/local/bin/bunx \
&& bunx --bun "playwright@${PLAYWRIGHT_VERSION}" install --with-deps chromium \
&& rm -rf /root/.cache /tmp/* /var/lib/apt/lists/* \
&& useradd --create-home --shell /bin/bash pwuser


FROM base AS deps
Expand Down
25 changes: 18 additions & 7 deletions agent-computer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
# The Bot's computer uses Playwright's image so Chromium and its system libraries stay matched.
# The Bot's computer installs only the Playwright browser it launches.
#
# The image tag and Playwright dependency must be pinned to the same exact version. Bump both or
# neither.
FROM mcr.microsoft.com/playwright:v1.62.1-noble
FROM node:24.18.1-bookworm-slim AS node-toolchain
FROM oven/bun:1.3.14@sha256:e10577f0db68676a7024391c6e5cb4b879ebd17188ab750cf10024a6d700e5c4 AS bun-toolchain

# unzip is not in the Playwright image and bun's installer needs it.
RUN apt-get update && apt-get install -y --no-install-recommends unzip \
&& rm -rf /var/lib/apt/lists/* \
&& curl -fsSL https://bun.sh/install | bash
ENV PATH="/root/.bun/bin:${PATH}"
FROM ubuntu:24.04

# The Bun image digest pins the amd64/arm64 release bytes, including if its tag changes.
ARG PLAYWRIGHT_VERSION=1.62.1
ENV BUN_INSTALL=/usr/local
ENV PATH="/usr/local/bin:${PATH}"
ENV DEBIAN_FRONTEND=noninteractive
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
COPY --from=node-toolchain /usr/local /usr/local
COPY --from=bun-toolchain /usr/local/bin/bun /usr/local/bin/bun
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl unzip xz-utils \
&& ln -s bun /usr/local/bin/bunx \
&& bunx --bun "playwright@${PLAYWRIGHT_VERSION}" install --with-deps chromium \
&& rm -rf /root/.cache /tmp/* /var/lib/apt/lists/*

WORKDIR /app
# The lockfile as well as the manifest: `--frozen-lockfile` with no lockfile present resolves afresh
Expand Down
103 changes: 103 additions & 0 deletions app/src/lib/computers/host-access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {
mutationOptions,
queryOptions,
type QueryClient,
} from "@tanstack/react-query";
import { client } from "@/lib/client";

export type HostFolderGrant = {
id: string;
botId: string;
actorId: string;
displayName: string;
revoked?: boolean;
ownerName?: string;
ownerEmail?: string;
};

export type HostAccessPendingOperation = {
operationId: string;
kind?:
| "choose_folder"
| "list_files"
| "read_file"
| "write_file"
| "run_command"
| "cancel"
| "stop";
botId: string;
actorId?: string;
displayName?: string;
writable?: boolean;
ownerName?: string;
ownerEmail?: string;
status?: "pending" | "approved" | "refused";
};

export type HostAccessStatus = {
connected: boolean;
grants: HostFolderGrant[];
pending: HostAccessPendingOperation[];
};

export const hostAccessKeys = {
all: ["host-access"] as const,
status: () => ["host-access", "status"] as const,
};

function invalidateHostAccess(queryClient: QueryClient) {
return queryClient.invalidateQueries({ queryKey: hostAccessKeys.all });
}

export function hostAccessQueryOptions() {
return queryOptions({
queryKey: hostAccessKeys.status(),
refetchInterval: 2_000,
queryFn: (): Promise<HostAccessStatus> =>
client("/api/host-access", {
fallback: "Folder access could not be loaded.",
}).then((response) => response.json()),
});
}

export function requestHostFolderGrantMutationOptions(
queryClient: QueryClient,
) {
return mutationOptions({
mutationFn: (variables: { botId: string }): Promise<unknown> =>
client("/api/host-access/grants", {
method: "POST",
body: { botId: variables.botId },
fallback: "The desktop app could not open the folder chooser.",
}).then((response) => response.json()),
onSuccess: () => invalidateHostAccess(queryClient),
onError: () => invalidateHostAccess(queryClient),
});
}

export function revokeHostFolderGrantMutationOptions(queryClient: QueryClient) {
return mutationOptions({
mutationFn: async (variables: { grantId: string }) => {
await client(
`/api/host-access/grants/${encodeURIComponent(variables.grantId)}`,
{
method: "DELETE",
fallback: "The host folder grant could not be revoked.",
},
);
},
onSuccess: () => invalidateHostAccess(queryClient),
});
}

export function stopHostAccessMutationOptions(queryClient: QueryClient) {
return mutationOptions({
mutationFn: async () => {
await client("/api/host-access/stop", {
method: "POST",
fallback: "Host folder access could not be stopped.",
});
},
onSuccess: () => invalidateHostAccess(queryClient),
});
}
Loading
Loading