Repository navigation
Expand file tree
/
Copy pathprovision.sh
More file actions
executable file
·140 lines (126 loc) · 6.85 KB
/
Copy pathprovision.sh
File metadata and controls
executable file
·140 lines (126 loc) · 6.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#!/usr/bin/env bash
# shellcheck disable=SC2016 # single-quoted scripts expand in the agent's shell
# Provisions an fctl agent sandbox: the tools agents need, Claude Code and
# fctl. Used by the Dockerfile (container sandboxes, Debian 13) and by `fctl
# host image build --driver lima`, which runs it in a Debian VM (fctl-base) as
# root with AGENT_USER set to the VM's user: Debian 13, or 12 with Lima before
# 2.0 and hosts of fctl 0.14 and earlier, so it works on both. Runs as root.
#
# FCTL_VERSION picks the fctl release and FCTL_DOWNLOAD where releases come
# from (`fctl host image build` passes its own version and download_base);
# empty means the latest, from https://fctl.sh/releases. CLAUDE_VERSION
# picks Claude Code's (empty: the latest); the image workflow passes the one
# it labels the image with, so that every architecture gets the same.
set -euo pipefail
AGENT_USER="${AGENT_USER:-agent}"
# Toolchain versions; README.md lists them too. Rust is the release fctl
# builds with (rust-toolchain.toml in hyperstructure/fctl), so sessions
# working on fctl have nothing to download: bump the two together.
NODE_MAJOR="${NODE_MAJOR:-26}"
COREPACK_VERSION="${COREPACK_VERSION:-0.36.0}"
TYPESCRIPT_VERSION="${TYPESCRIPT_VERSION:-7.0.2}"
BUN_VERSION="${BUN_VERSION:-1.4.4}"
UV_VERSION="${UV_VERSION:-0.13.4}"
PYTHON_VERSION="${PYTHON_VERSION:-3.15.0}"
RUST_VERSION="${RUST_VERSION:-1.99.1}"
# Playwright's version picks its browsers' builds and the system libraries
# they need, which come with it.
PLAYWRIGHT_VERSION="${PLAYWRIGHT_VERSION:-1.64.1}"
FCTL_VERSION="${FCTL_VERSION:-}"
FCTL_DOWNLOAD="${FCTL_DOWNLOAD:-}"
CLAUDE_VERSION="${CLAUDE_VERSION:-}"
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends \
ca-certificates curl wget git gnupg less openssh-client procps psmisc lsof sudo \
rsync unzip xz-utils zip zstd \
build-essential cmake pkg-config libssl-dev \
jq ripgrep fd-find tree file shellcheck sqlite3 \
ffmpeg imagemagick pngquant
# Debian calls fd `fdfind`.
ln -sf /usr/bin/fdfind /usr/local/bin/fd
# GitHub CLI
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list
# Node.js, corepack, TypeScript and Playwright. Node.js stopped shipping
# corepack in 25, so it comes from npm, which also puts its yarn and pnpm
# shims on the PATH. npm leaves out its funding and audit notices and its
# update check, which only lengthen agents' output.
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash -
apt-get install -y --no-install-recommends gh nodejs
npm config set --location=global fund=false audit=false update-notifier=false
npm install -g "corepack@$COREPACK_VERSION" "typescript@$TYPESCRIPT_VERSION" \
"playwright@$PLAYWRIGHT_VERSION"
npm cache clean --force
# The system libraries and fonts that Playwright's Chromium, Firefox and
# WebKit need, from its own list for this Debian release, and Xvfb for
# headed browsers. The agent's browsers come below.
playwright install-deps chromium firefox webkit
# uv, and Python for everyone: uv's build of CPython in /opt/uv/python, as
# python3 and python in /usr/local/bin.
curl -LsSf "https://astral.sh/uv/$UV_VERSION/install.sh" | env UV_INSTALL_DIR=/usr/local/bin UV_NO_MODIFY_PATH=1 sh
env UV_PYTHON_INSTALL_DIR=/opt/uv/python UV_PYTHON_BIN_DIR=/usr/local/bin UV_NO_CACHE=1 \
uv python install --default --preview-features python-install-default "$PYTHON_VERSION"
rm -rf /var/lib/apt/lists/*
# The agent user. The sandbox is the security boundary, so sudo needs no password.
if ! id "$AGENT_USER" >/dev/null 2>&1; then
useradd -m -s /bin/bash -u 1000 "$AGENT_USER"
fi
echo "$AGENT_USER ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/90-fctl-$AGENT_USER"
chmod 0440 "/etc/sudoers.d/90-fctl-$AGENT_USER"
mkdir -p /opt/fctl/bin
chown -R "$AGENT_USER" /opt/fctl
# fctl, for everyone on the machine: agents name and describe their session
# with `fctl edit`. A version that is not published (a development build of
# the host) gets the latest release instead. The host copies its own
# fctl-agent in at session start, so the one in the archive goes.
install_fctl() {
curl -fsSL https://fctl.sh/install |
env FCTL_VERSION="$1" FCTL_DOWNLOAD="$FCTL_DOWNLOAD" FCTL_INSTALL_DIR=/usr/local/bin \
FCTL_NO_COMPLETIONS=1 sh
}
if ! install_fctl "$FCTL_VERSION"; then
[ -n "$FCTL_VERSION" ] || exit 1
echo "fctl $FCTL_VERSION is not published; installing the latest release" >&2
install_fctl ""
fi
rm -f /usr/local/bin/fctl-agent-*-linux
# Claude Code, installed for the agent user (native build, ~/.local/bin/claude).
AGENT_HOME="$(getent passwd "$AGENT_USER" | cut -d: -f6)"
sudo -u "$AGENT_USER" -H env CLAUDE_VERSION="$CLAUDE_VERSION" \
bash -c 'curl -fsSL https://claude.ai/install.sh | bash -s -- ${CLAUDE_VERSION:+"$CLAUDE_VERSION"}'
sudo -u "$AGENT_USER" -H mkdir -p "$AGENT_HOME/workspace"
# The agent's PATH: Claude Code and uv's tools in ~/.local/bin, Rust's and
# Bun's tools, then fctl's supervisor. The Dockerfile sets the same; login
# shells (setup scripts, the Lima driver's) get it from here.
cat > /etc/profile.d/fctl.sh <<'EOF'
PATH="$HOME/.local/bin:$HOME/.cargo/bin:$HOME/.bun/bin:/opt/fctl/bin:$PATH"
EOF
# Runs a script as the agent user in a login shell; it gets the arguments
# after the script as $1….
as_agent() {
local script="$1"
shift
sudo -u "$AGENT_USER" -H bash -lc "set -euo pipefail; $script" bash "$@"
}
# Rust, with the components and targets that rust-toolchain.toml lists, and
# Bun, in the agent's home: they find their files through HOME alone
# (fctl-agent passes Claude no RUSTUP_HOME or the like), and agents add
# toolchains, targets and global tools without sudo.
as_agent 'curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs |
sh -s -- -y --no-modify-path --profile minimal --default-toolchain "$1" \
--component rustfmt,clippy --target wasm32-unknown-unknown' "$RUST_VERSION"
as_agent 'curl -fsSL https://bun.sh/install | bash -s "bun-v$1"' "$BUN_VERSION"
as_agent 'ln -sf bun "$HOME/.bun/bin/bunx"'
# Playwright's headless Chromium and its WebKit, in ~/.cache/ms-playwright,
# where Playwright looks for them with HOME alone (fctl leaves
# PLAYWRIGHT_BROWSERS_PATH unset for an image that has them there): agents
# add the other browsers and other versions' builds without sudo. They stay
# when other versions install theirs, for as long as the global Playwright
# that installed them is there.
as_agent 'playwright install --only-shell --no-progress chromium webkit'
# What sessions find on the PATH.
as_agent 'claude --version; fctl --version; python3 --version; uv --version
rustc --version; cargo --version; node --version; corepack --version; tsc --version
bun --version; playwright --version'