diff --git a/.github/workflows/actiond-prototype.yaml b/.github/workflows/actiond-prototype.yaml new file mode 100644 index 0000000..ea1aac4 --- /dev/null +++ b/.github/workflows/actiond-prototype.yaml @@ -0,0 +1,77 @@ +name: actiond prototype + +on: + pull_request: + paths: + - 'experiments/actiond/**' + - '.github/workflows/actiond-prototype.yaml' + +permissions: + contents: read + +jobs: + chromium: + runs-on: ubuntu-24.04 + timeout-minutes: 30 + env: + USE_BAZEL_VERSION: '9.2.0' + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: '24' + - uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0 + with: + bazelisk-cache: true + repository-cache: true + - name: Prepare declared browser runtime + run: | + corepack enable pnpm + pnpm install --frozen-lockfile + docker pull --platform linux/amd64 mcr.microsoft.com/playwright:v1.63.0-noble@sha256:bc6ab0d6d44ff4826e4cb8c1e6d801e185bfc42bb0753f8e2a30efc70db054c7 + bash experiments/actiond/prepare.sh "$RUNNER_TEMP/actiond-prototype" + - name: Build kernel with memory-advice syscalls + run: | + work="$RUNNER_TEMP/actiond-prototype" + git -C "$work/actiond" worktree add --detach "$work/kernel-source" c99b0cafc824faa59c2b04498eb1a6e5245b53e7 + git -C "$work/kernel-source" apply "$GITHUB_WORKSPACE/experiments/actiond/actiond-advice.patch" + cd "$work/kernel-source" + bazelisk --output_base="$work/kernel-output" build --bes_backend= --remote_executor= --remote_cache= --spawn_strategy=local --jobs=2 --platforms=//platforms:linux_x86_64_musl //vm:linux_kernel.image > "$work/kernel-build.log" 2>&1 + kernel=$(bazelisk --output_base="$work/kernel-output" cquery --bes_backend= --platforms=//platforms:linux_x86_64_musl //vm:linux_kernel.image --output=files) + cp "$kernel" "$work/patched-kernel" + - name: Enable KVM and vhost-vsock + run: | + test -c /dev/kvm + sudo chmod a+rw /dev/kvm + if [[ ! -e /dev/vhost-vsock ]]; then sudo modprobe vhost_vsock; fi + test -c /dev/vhost-vsock + sudo chmod a+rw /dev/vhost-vsock + - name: Capture inside the actiond VM + env: + ACTIOND_BAZEL: bazelisk + run: | + work="$RUNNER_TEMP/actiond-prototype" + curl -fsSL https://github.com/hermeticbuild/actiond/releases/download/v0.0.6/linux-actiond_linux_x86_64 -o "$work/actiond-worker" + printf '006dc798d4363596fe8ab997606fc93766a0cc427c2d005cf4fc1765fa4c2052 %s\n' "$work/actiond-worker" | sha256sum -c - + chmod +x "$work/actiond-worker" + "$work/actiond-worker" serve-vm --kernel="$work/patched-kernel" --root="$work/vm" --listen=127.0.0.1:8980 --memory-mib=3072 --cpus=2 --cas-image-size-mib=4096 > "$work/vm.log" 2>&1 & + worker_pid=$! + trap 'kill "$worker_pid" 2>/dev/null || true' EXIT + ready=false + for attempt in $(seq 1 90); do + kill -0 "$worker_pid" + if (echo > /dev/tcp/127.0.0.1/8980) 2>/dev/null; then ready=true; break; fi + sleep 1 + done + "$ready" + bash experiments/actiond/run-actiond.sh "$work" grpc://127.0.0.1:8980 + sha256sum "$work/results/first.png" "$work/results/second.png" + cmp "$work/results/first.png" "$work/results/second.png" + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + if: always() + with: + name: actiond-chromium-prototype + path: | + ${{ runner.temp }}/actiond-prototype/results/ + ${{ runner.temp }}/actiond-prototype/vm.log + ${{ runner.temp }}/actiond-prototype/kernel-build.log diff --git a/docs/actiond-evaluation.md b/docs/actiond-evaluation.md new file mode 100644 index 0000000..5d1a50a --- /dev/null +++ b/docs/actiond-evaluation.md @@ -0,0 +1,74 @@ +# actiond evaluation for VRT + +Source review on 2026-09-13 at upstream commit +[`8a42c3d`](https://github.com/hermeticbuild/actiond/tree/8a42c3d481df3a1bf1b80e95a9bb991a207fc035). +The initial review below is followed by a [runnable prototype](../experiments/actiond/README.md). +The prototype subsequently rendered under actiond's unmodified process runner. +Full VM/REAPI execution also passed on a GitHub KVM runner after enabling +`CONFIG_ADVISE_SYSCALLS` in the guest kernel; see the prototype's linked CI evidence. + +## Assessment + +Promising optional execution backend; not a drop-in replacement for the current +Testcontainers VRT implementation. Keep the current backend for AGI and FormatJS +while evaluating a general remote-execution-compatible VRT path. + +## What improves + +The entire test action could execute inside Linux: Node, fixture server, browser, +and comparison. actiond provides a Bazel REAPI executor/cache, immutable declared +inputs, a VM boundary, and per-action loopback-only networking. The VM has no +external network device. This would constrain consumer Node code as well as the +browser and remove Docker/Ryuk from that execution path. These are architectural +benefits, not a tested Chromium integration. +[Architecture](https://github.com/hermeticbuild/actiond/blob/8a42c3d481df3a1bf1b80e95a9bb991a207fc035/ARCHITECTURE.md) + +## Integration gaps + +- **Architecture:** Apple Silicon gets an ARM64 Linux guest; Windows/Linux guests + match their host architecture. Our current VRT browser uses Linux amd64 on all + hosts. Linux alone does not establish pixel equivalence between ARM64 and amd64; + a common architecture or verified separate baseline policy remains necessary. + [VM topology](https://github.com/hermeticbuild/actiond/blob/8a42c3d481df3a1bf1b80e95a9bb991a207fc035/ARCHITECTURE.md#topology) +- **Browser provisioning:** packaged runtimes contain selected glibc versions and + Bash. The executor selects those runtimes, rather than a caller-owned OCI image. + Chromium, additional shared libraries, fonts, and font configuration would need + declared packaging and a suitable launcher, or new runtime support. + [Runtime definitions](https://github.com/hermeticbuild/actiond/blob/8a42c3d481df3a1bf1b80e95a9bb991a207fc035/runtimes/BUILD.bazel), + [executor](https://github.com/hermeticbuild/actiond/blob/8a42c3d481df3a1bf1b80e95a9bb991a207fc035/src/action_executor.zig#L305-L331) +- **Rule changes:** our browser tests carry `no-remote` and `no-cache`; VRT starts + Testcontainers unconditionally. An executor backend must launch a declared + browser within the action and run the fixture server in that same action. + [Current rules](../internal/browser.bzl), [runner](../runtime/runner.ts) +- **Baseline updates:** `.update` currently copies into `BUILD_WORKSPACE_DIRECTORY`. + Remote capture must instead produce downloadable outputs, with a small local + wrapper applying them to the source workspace. Merely enabling remote execution + does not move the final `bazel run` executable into the executor. + [Baseline handling](../runtime/baselines.ts), [runner](../runtime/runner.ts) +- **Chromium compatibility:** the action sandbox rejects installing additional + seccomp filters. Chromium sandbox behavior needs a smoke test; do not assume its + normal sandbox works unchanged. Time and randomness still require test control. + [Sandbox implementation](https://github.com/hermeticbuild/actiond/blob/8a42c3d481df3a1bf1b80e95a9bb991a207fc035/src/action_runner.zig#L817-L854) +- **Provisioning:** actiond replaces the Docker prerequisite with its own VM worker. + Linux requires KVM/vhost-vsock access and io_uring; Windows requires Hyper-V. + Linux-compatible Bazel toolchains must also be configured. + [Setup](https://github.com/hermeticbuild/actiond/blob/8a42c3d481df3a1bf1b80e95a9bb991a207fc035/README.md#start-the-worker) + +## Prototype follow-up + +The prototype supplies its own ELF loader alongside Chromium and its libraries, +so no embedded glibc selection or OCI runtime support is needed for the smoke +action. It renders with the existing seccomp filter and no `/dev/shm` changes, +using `chromiumSandbox: false` and `--no-zygote`. The real VM initially failed +because `madvise` was absent (`ENOSYS`). Enabling `CONFIG_ADVISE_SYSCALLS=y` +resolved the syscall probe and screenshot failure; downloaded VM PNGs matched +the local result byte-for-byte. See its README for exact limits. + +## Next experiment + +Build one existing editor screenshot case as an ordinary Linux action with a +fully declared Chromium runtime. Run capture and comparison inside actiond, +return a PNG through Bazel outputs, and test local baseline application. Verify +Chromium startup, fonts, isolation, cancellation, and repeated captures. Compare +ARM64 and amd64 outputs explicitly before considering shared baselines. Keep the +interface REAPI-compatible so callers can choose actiond or another executor. diff --git a/experiments/actiond/BUILD.bazel.template b/experiments/actiond/BUILD.bazel.template new file mode 100644 index 0000000..9f6771f --- /dev/null +++ b/experiments/actiond/BUILD.bazel.template @@ -0,0 +1,19 @@ +load(":capture.bzl", "capture", "kernel_probe") + +platform( + name = "linux_amd64", + constraint_values = ["@platforms//os:linux", "@platforms//cpu:x86_64"], +) + +capture( + name = "capture", + node = "runtime/bin/node", + script = "capture.mjs", + runtime = ["ld.so"] + glob(["runtime/**"]), + playwright = glob(["playwright-core/**"]), +) + +kernel_probe( + name = "kernel_probe", + binary = "kernel-probe", +) diff --git a/experiments/actiond/README.md b/experiments/actiond/README.md new file mode 100644 index 0000000..42b1569 --- /dev/null +++ b/experiments/actiond/README.md @@ -0,0 +1,81 @@ +# actiond Chromium prototype + +Throwaway experiment, not a replacement for the production VRT backend. + +The action runs a Node HTTP fixture, Chromium, and screenshot comparison together. +Node, Playwright, Chromium, shared libraries, fonts, and even the ELF loader are +ordinary declared inputs. No OCI API, injected glibc runtime, Testcontainers, +Docker socket, or Ryuk is used inside the action. + +## Reproduce + +Setup requires Linux amd64, Python 3, curl, Git, Docker, and the repository's +installed pnpm dependencies. Preload the Playwright image pinned in `prepare.sh`. +Docker only extracts the image during preparation; the action consumes files. + +```sh +bash experiments/actiond/prepare.sh /tmp/actiond-prototype +# With an existing amd64 actiond worker: +bash experiments/actiond/run-actiond.sh /tmp/actiond-prototype grpc://127.0.0.1:8980 +``` + +`run-actiond.sh` invokes a standalone Bazel action with local fallback disabled. +It downloads declared PNG outputs into `/tmp/actiond-prototype/results`; source +baselines are untouched. `ACTIOND_BAZEL` may select the Bazel executable. + +Without KVM, test the actual actiond process runner separately: + +```sh +# Also preload the Ubuntu image pinned in run-sandbox.sh. +bash experiments/actiond/run-sandbox.sh /tmp/actiond-prototype +``` + +That diagnostic uses a disposable container to grant namespace/mount privileges. +Inside it, the unmodified actiond runner applies its own chroot, namespaces, +uid/gid drop, and seccomp filter. This does **not** exercise the VM, REAPI, CAS +input filesystem, or Bazel output collection. The Docker helper is a diagnostic, +not the proposed deployment architecture. + +## Local results + +- Unmodified actiond runner source at `8a42c3d481df3a1bf1b80e95a9bb991a207fc035`. +- Chromium `153.0.8010.12` / Playwright `1.63.0`, Linux amd64. +- `chromiumSandbox: false` and `--no-zygote` produced matching 640x360 PNGs across + fresh invocations. The default zygote path failed with child/GPU startup errors; + its root cause is not established. `--zygote` reproduces that diagnostic. +- `--sandbox` failed with “No usable sandbox” in the nested container harness; + this does not establish which restriction a real VM would hit first. +- Fixture HTTP and font loading succeeded; a direct external TCP connection + failed with `ENETUNREACH`. No `/dev/shm`, Docker socket, or extra device mounts. +- PNG: 11,389 bytes, SHA-256 + `498f5f17cc8af0437eeabf78db5c6079fd1d3f3009012537d5d0dc9ee9244362`. +- Bazel aquery resolves Linux amd64 and lists the loader, browser, libraries, + fonts, and Playwright files as inputs, with PNGs in a declared TreeArtifact. + +## VM kernel finding + +The released actiond `v0.0.6` VM boots and accepts the Bazel action, but Node aborts +in V8's `DiscardSystemPages`. The minimal `kernel-probe.c` action independently +returns `madvise(MADV_DONTNEED): errno=38 (Function not implemented)`. +[Failing VM run](https://github.com/perplexityai/rules_web_e2e/actions/runs/34775725126). + +Both actiond kernel configs use `allnoconfig` and omit `CONFIG_ADVISE_SYSCALLS`. +`actiond-advice.patch` enables it for ARM64 and amd64. The prototype workflow +rebuilds the exact `v0.0.6` kernel source with this patch and passes it to the +released worker through `--kernel`. It performs all compilation locally on the +GitHub runner; no BuildBuddy upload or remote build service is used. + +The patched main-branch kernel also builds locally. The patched release kernel +passed the syscall probe and screenshot action through real Bazel REAPI execution +with local fallback disabled. Both downloaded PNGs match the local hash above. +[Passing VM run](https://github.com/perplexityai/rules_web_e2e/actions/runs/34775967325). +The local host has no `/dev/kvm`; KVM validation ran on GitHub's Ubuntu runner. + +## Decision + +The process-level proof requires **no actiond userspace changes**. The VM proof +requires the memory-advice kernel fix above. Before adding OCI runtime support +or relaxing seccomp, validate an existing editor fixture on the patched VM. Production integration still needs an optional executor +backend, reviewed baseline-update handling, amd64 worker selection on Apple +Silicon, and cleanup/isolation coverage. One stable fixture is not evidence of +cross-architecture pixel equivalence or full Chromium compatibility. diff --git a/experiments/actiond/actiond-advice.patch b/experiments/actiond/actiond-advice.patch new file mode 100644 index 0000000..48c833c --- /dev/null +++ b/experiments/actiond/actiond-advice.patch @@ -0,0 +1,22 @@ +diff --git a/vm/linux.config b/vm/linux.config +index 69abcd9..1b6bffe 100644 +--- a/vm/linux.config ++++ b/vm/linux.config +@@ -1,5 +1,6 @@ + CONFIG_64BIT=y + CONFIG_ACTIONDFS_FS=y ++CONFIG_ADVISE_SYSCALLS=y + CONFIG_ACPI=y + CONFIG_ARM64=y + CONFIG_SMP=y +diff --git a/vm/linux_x86_64.config b/vm/linux_x86_64.config +index 278a71f..8bcd5a7 100644 +--- a/vm/linux_x86_64.config ++++ b/vm/linux_x86_64.config +@@ -1,5 +1,6 @@ + CONFIG_64BIT=y + CONFIG_ACTIONDFS_FS=y ++CONFIG_ADVISE_SYSCALLS=y + CONFIG_ACPI=y + CONFIG_HPET_TIMER=y + CONFIG_X86_PM_TIMER=y diff --git a/experiments/actiond/capture.bzl b/experiments/actiond/capture.bzl new file mode 100644 index 0000000..8c82d7d --- /dev/null +++ b/experiments/actiond/capture.bzl @@ -0,0 +1,46 @@ +"""Throwaway REAPI-compatible screenshot action; no Docker or Testcontainers.""" + +def _capture_impl(ctx): + out = ctx.actions.declare_directory("screenshots") + ctx.actions.run( + executable = ctx.file.node, + arguments = [ctx.file.script.path], + inputs = depset(ctx.files.runtime + ctx.files.playwright + [ctx.file.script]), + outputs = [out], + env = { + "HOME": "/tmp", + "TMPDIR": "/tmp", + "LANG": "C.UTF-8", + "TZ": "UTC", + "FONTCONFIG_PATH": "/workspace/runtime/etc/fonts", + "LD_LIBRARY_PATH": "/workspace/runtime/lib", + "OUTPUT_DIR": out.path, + }, + mnemonic = "ActiondChromiumSmoke", + ) + return [DefaultInfo(files = depset([out]))] + +capture = rule( + implementation = _capture_impl, + attrs = { + "node": attr.label(allow_single_file = True), + "script": attr.label(allow_single_file = True), + "runtime": attr.label_list(allow_files = True), + "playwright": attr.label_list(allow_files = True), + }, +) + +def _kernel_probe_impl(ctx): + out = ctx.actions.declare_file("kernel-probe.txt") + ctx.actions.run( + executable = ctx.file.binary, + arguments = [out.path], + outputs = [out], + mnemonic = "ActiondKernelProbe", + ) + return [DefaultInfo(files = depset([out]))] + +kernel_probe = rule( + implementation = _kernel_probe_impl, + attrs = {"binary": attr.label(allow_single_file = True)}, +) diff --git a/experiments/actiond/capture.mjs b/experiments/actiond/capture.mjs new file mode 100644 index 0000000..60b5b70 --- /dev/null +++ b/experiments/actiond/capture.mjs @@ -0,0 +1,48 @@ +// Prototype: all HTTP traffic, browser work and screenshot outputs stay in one action. +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import http from 'node:http'; +import path from 'node:path'; +import net from 'node:net'; +import { chromium } from './playwright-core/index.mjs'; + +assert.equal(process.platform, 'linux'); +assert.equal(process.arch, 'x64'); +assert.equal(fs.existsSync('/dev/shm'), false); +assert.equal(fs.existsSync('/var/run/docker.sock'), false); +const output = path.resolve(process.env.OUTPUT_DIR || '/workspace/outputs'); +fs.mkdirSync(output, { recursive: true }); +const html = `

Declared Linux browser

Same-action HTTP fixture · 12345

`; +const font = fs.readFileSync('/workspace/runtime/fonts/truetype/freefont/FreeSans.ttf'); +const server = http.createServer((req, res) => { + res.setHeader('Content-Type', req.url === '/font.ttf' ? 'font/ttf' : 'text/html'); + res.end(req.url === '/font.ttf' ? font : html); +}); +await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); +let browser; +try { + browser = await chromium.launch({ executablePath: '/workspace/runtime/chromium/chrome-headless-shell', chromiumSandbox: process.argv.includes('--sandbox'), headless: true, timeout: 15000, args: (process.argv.includes('--zygote') || process.argv.includes('--sandbox')) ? [] : ['--no-zygote'] }); + const page = await browser.newPage({ viewport: { width: 640, height: 360 }, deviceScaleFactor: 1, locale: 'en-US', timezoneId: 'UTC', reducedMotion: 'reduce' }); + await page.goto(`http://127.0.0.1:${server.address().port}`); + await page.evaluate(() => document.fonts.ready); + assert.equal(await page.evaluate(() => document.fonts.check('28px Fixture')), true); + assert.equal(await page.locator('h1').textContent(), 'Declared Linux browser'); + const first = await page.screenshot({ path: path.join(output, 'first.png'), animations: 'disabled' }); + const second = await page.screenshot({ path: path.join(output, 'second.png'), animations: 'disabled' }); + assert.deepEqual(first, second); + await assert.rejects(new Promise((resolve, reject) => { + const socket = net.connect({ host: '1.1.1.1', port: 443 }); + socket.setTimeout(1000, () => { socket.destroy(); reject(new Error('timeout')); }); + socket.once('error', reject); + socket.once('connect', () => { socket.destroy(); resolve(); }); + }), error => error.code === 'ENETUNREACH'); + console.log(JSON.stringify({ browser: browser.version(), platform: process.platform, arch: process.arch, identical: true, bytes: first.length, sandbox: process.argv.includes('--sandbox') })); +} finally { + await browser?.close(); + await new Promise(resolve => server.close(resolve)); +} diff --git a/experiments/actiond/kernel-probe.c b/experiments/actiond/kernel-probe.c new file mode 100644 index 0000000..2701a74 --- /dev/null +++ b/experiments/actiond/kernel-probe.c @@ -0,0 +1,25 @@ +// Minimal regression probe for V8's failing DiscardSystemPages call. +#define _GNU_SOURCE +#include +#include +#include +#include +#include + +int main(int argc, char **argv) { + if (argc != 2) return 2; + long size = sysconf(_SC_PAGESIZE); + void *memory = mmap(NULL, size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (memory == MAP_FAILED) { perror("mmap"); return 1; } + int result = madvise(memory, size, MADV_DONTNEED); + int failure = errno; + munmap(memory, size); + if (result != 0) { + fprintf(stderr, "madvise(MADV_DONTNEED): errno=%d (%s)\n", failure, strerror(failure)); + return 1; + } + FILE *output = fopen(argv[1], "w"); + if (!output) { perror("output"); return 1; } + fputs("madvise(MADV_DONTNEED) succeeded\n", output); + return fclose(output) != 0; +} diff --git a/experiments/actiond/package-runtime.sh b/experiments/actiond/package-runtime.sh new file mode 100755 index 0000000..138e0be --- /dev/null +++ b/experiments/actiond/package-runtime.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Setup-only prototype: run inside the pinned Playwright image. +set -euo pipefail +out=${1:-/output} +mkdir -p "$out"/{bin,lib,chromium,etc/fonts,fonts} +cp /usr/bin/node /bin/bash "$out/bin/" +cp -a /ms-playwright/chromium_headless_shell-1243/chrome-headless-shell-linux64/. "$out/chromium/" +for binary in /usr/bin/node /bin/bash /ms-playwright/chromium_headless_shell-1243/chrome-headless-shell-linux64/chrome-headless-shell; do + ldd "$binary" | awk '/=> \// {print $3} /^\s*\/lib/ {print $1}' | while read -r lib; do + cp -L "$lib" "$out/lib/" + done +done +cp -L /lib64/ld-linux-x86-64.so.2 "$out/lib/" +cp -a /usr/share/fonts/. "$out/fonts/" +cat > "$out/etc/fonts/fonts.conf" <<'XML' + + + + /workspace/runtime/fonts + /tmp/fontconfig + +XML +cat > "$out/chrome" <<'SH' +#!/workspace/runtime/bin/bash +exec /workspace/runtime/chromium/chrome-headless-shell "$@" +SH +chmod +x "$out/chrome" +tar -C "$out" -cf - . diff --git a/experiments/actiond/patch-interpreter.py b/experiments/actiond/patch-interpreter.py new file mode 100644 index 0000000..e19447b --- /dev/null +++ b/experiments/actiond/patch-interpreter.py @@ -0,0 +1,29 @@ +"""Point this amd64 prototype's ELF executables at a declared loader. + +The replacement fits inside the existing PT_INTERP segment; no offsets move. +Unlike invoking ld.so directly, this preserves Chromium's /proc/self/exe lookup. +""" +import pathlib +import struct +import sys + +interpreter = b"/workspace/ld.so\0" +for filename in sys.argv[1:]: + path = pathlib.Path(filename) + data = bytearray(path.read_bytes()) + assert data[:6] == b"\x7fELF\x02\x01", "expected ELF64 little endian" + assert struct.unpack_from("&2; exit 1; } +mkdir -p "$work"/{build,workspace} +actiond_commit=8a42c3d481df3a1bf1b80e95a9bb991a207fc035 +zig_sha=70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00 +if [[ ! -d $work/actiond/.git ]]; then + git clone https://github.com/hermeticbuild/actiond.git "$work/actiond" +fi +git -C "$work/actiond" checkout --detach "$actiond_commit" +if [[ ! -f $work/zig.tar.xz ]]; then + curl -fsSL https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz -o "$work/zig.tar.xz" +fi +printf '%s %s\n' "$zig_sha" "$work/zig.tar.xz" | sha256sum -c - +if [[ ! -d $work/zig-x86_64-linux-0.16.0 ]]; then tar -xf "$work/zig.tar.xz" -C "$work"; fi +for file in action_runner cas reapi protobuf_wire; do cp "$work/actiond/src/$file.zig" "$work/build/"; done +cp "$here/sandbox-smoke.zig" "$work/build/" +printf 'pub const executor_timing_logs = false;\npub const actiondfs_fstype: [:0]const u8 = "actiondfs";\n' > "$work/build/options.zig" +ZIG_GLOBAL_CACHE_DIR="$work/zig-cache" "$work/zig-x86_64-linux-0.16.0/zig" build-exe -O ReleaseFast -target x86_64-linux-musl \ + --dep actiond_build_options -Mroot="$work/build/sandbox-smoke.zig" \ + -Mactiond_build_options="$work/build/options.zig" -femit-bin="$work/build/sandbox-smoke" +if [[ ! -f $work/runtime.tar ]]; then + docker run --rm -i --network none --pull=never \ + mcr.microsoft.com/playwright:v1.63.0-noble@sha256:bc6ab0d6d44ff4826e4cb8c1e6d801e185bfc42bb0753f8e2a30efc70db054c7 \ + bash -s /output < "$here/package-runtime.sh" > "$work/runtime.tar.tmp" + mv "$work/runtime.tar.tmp" "$work/runtime.tar" +fi +mkdir -p "$work/workspace/runtime" +tar -xf "$work/runtime.tar" -C "$work/workspace/runtime" +cp "$work/workspace/runtime/lib/ld-linux-x86-64.so.2" "$work/workspace/ld.so" +python3 "$here/patch-interpreter.py" "$work/workspace/runtime/bin/node" "$work/workspace/runtime/chromium/chrome-headless-shell" +cp -RL "$here/../../node_modules/playwright-core" "$work/workspace/" +ZIG_GLOBAL_CACHE_DIR="$work/zig-cache" "$work/zig-x86_64-linux-0.16.0/zig" cc -target x86_64-linux-musl -O2 "$here/kernel-probe.c" -o "$work/workspace/kernel-probe" +cp "$here/capture.mjs" "$here/capture.bzl" "$work/workspace/" +cp "$here/BUILD.bazel.template" "$work/workspace/BUILD.bazel" +printf 'module(name = "actiond_chromium_prototype")\nbazel_dep(name = "platforms", version = "1.1.0")\n' > "$work/workspace/MODULE.bazel" +printf 'Prepared %s\n' "$work" diff --git a/experiments/actiond/run-actiond.sh b/experiments/actiond/run-actiond.sh new file mode 100755 index 0000000..ba4e104 --- /dev/null +++ b/experiments/actiond/run-actiond.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Requires an already running amd64 actiond worker. No local execution fallback. +set -euo pipefail +work=${1:?usage: run-actiond.sh ABSOLUTE_WORK_DIRECTORY GRPC_ENDPOINT} +endpoint=${2:?supply the actiond GRPC endpoint} +cd "$work/workspace" +"${ACTIOND_BAZEL:-bazel}" --output_base="$work/bazel-output" build //:kernel_probe //:capture --keep_going \ + --host_platform=//:linux_amd64 --platforms=//:linux_amd64 \ + --remote_executor="$endpoint" --remote_cache="$endpoint" \ + --spawn_strategy=remote --remote_local_fallback=false \ + --remote_upload_local_results=false --noremote_cache_compression \ + --remote_download_outputs=all +mkdir -p "$work/results" +cp bazel-bin/screenshots/*.png "$work/results/" diff --git a/experiments/actiond/run-sandbox.sh b/experiments/actiond/run-sandbox.sh new file mode 100755 index 0000000..f80e700 --- /dev/null +++ b/experiments/actiond/run-sandbox.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Tests the real actiond runner; DOES NOT test its VM, REAPI or actiondfs. +set -euo pipefail +work=${1:?usage: run-sandbox.sh ABSOLUTE_WORK_DIRECTORY [CHROMIUM_FLAGS]} +shift +root="$work/root" +mkdir -p "$root"/{dev,proc,tmp,var/tmp,workspace/outputs} "$work/results" +touch "$root/dev/null" +chmod 1777 "$root/tmp" "$root/var/tmp" "$root/workspace/outputs" +cp -a "$work/workspace/runtime" "$work/workspace/playwright-core" "$work/workspace/capture.mjs" "$work/workspace/ld.so" "$root/workspace/" +rm -f "$root/workspace/outputs/"*.png +container=$(docker create --network none --cap-add SYS_ADMIN --cap-add NET_ADMIN \ + --security-opt seccomp=unconfined --security-opt apparmor=unconfined \ + --pull=never ubuntu:24.04@sha256:224a1869083a311ef3f13648a154ba79832fbef6364d31493642ca03082da254 \ + /bin/bash -c 'mkdir /cas; exec /smoke /action-root /cas /workspace/runtime/bin/node /workspace/capture.mjs "$@"' prototype "$@") +trap 'docker rm -f "$container" >/dev/null' EXIT +docker cp "$root" "$container:/action-root" +docker cp "$work/build/sandbox-smoke" "$container:/smoke" +set +e +docker start -a "$container" 2>&1 | tee "$work/results/sandbox.log" +smoke_exit=${PIPESTATUS[0]} +set -e +docker cp "$container:/action-root/workspace/outputs/." "$work/results/" +exit "$smoke_exit" diff --git a/experiments/actiond/sandbox-smoke.zig b/experiments/actiond/sandbox-smoke.zig new file mode 100644 index 0000000..f86587c --- /dev/null +++ b/experiments/actiond/sandbox-smoke.zig @@ -0,0 +1,40 @@ +// Prototype: invokes actiond's real runner without its VM, REAPI or actiondfs. +const std = @import("std"); +const runner = @import("action_runner.zig"); +const cas = @import("cas.zig"); + +pub fn main(init: std.process.Init) !void { + const io = init.io; + const allocator = init.arena.allocator(); + const args = try init.minimal.args.toSlice(allocator); + if (args.len < 4) return error.ExpectedRootCasAndCommand; + var dir = try std.Io.Dir.openDirAbsolute(io, args[2], .{}); + defer dir.close(io); + const mounts = [_]runner.BindMount{.{ + .source = try allocator.dupeZ(u8, "/dev/null"), + .target = try std.fmt.allocPrintSentinel(allocator, "{s}/dev/null", .{args[1]}, 0), + .read_only = false, + }}; + var outcome = try runner.runCommandWithOptions(io, allocator, cas.Store.init(dir), .{ + .arguments = args[3..], + .environment_variables = &.{ + .{ .name = "HOME", .value = "/tmp" }, + .{ .name = "TMPDIR", .value = "/tmp" }, + .{ .name = "LANG", .value = "C.UTF-8" }, + .{ .name = "TZ", .value = "UTC" }, + .{ .name = "FONTCONFIG_PATH", .value = "/workspace/runtime/etc/fonts" }, + .{ .name = "LD_LIBRARY_PATH", .value = "/workspace/runtime/lib" }, + }, + }, .{ + .chroot_dir = args[1], + .chroot_cwd = "/workspace", + .bind_mounts = &mounts, + .timeout_ns = 90 * std.time.ns_per_s, + }); + defer outcome.deinit(allocator); + std.debug.print("stdout:\n{s}\nstderr:\n{s}\nstatus: {any}\n", .{ outcome.stdout, outcome.stderr, outcome.status }); + switch (outcome.status) { + .exited => |code| if (code != 0) std.process.exit(code), + else => return error.CommandFailed, + } +}