Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license

This commit is contained in:
2026-09-05 13:26:46 +03:00
commit 03634b1ca3
11340 changed files with 3442369 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
# Minimal openssl config for the dev sandbox.
#
# The sandbox replaces /etc wholesale, and on Debian/Ubuntu
# /usr/lib/ssl/openssl.cnf (openssl's compiled-in OPENSSLDIR) is a symlink into
# /etc/ssl -- so the config openssl insists on reading disappears and every
# `openssl req` fails with:
#
# Can't open "/usr/lib/ssl/openssl.cnf" for reading
#
# which surfaces to the payload as a bare `curl: (35) Recv failure`. Rather than
# reconstruct each distro's /etc/ssl, point OPENSSL_CONF at this file: the proxy
# only needs enough config for `req -addext` and `x509 -copy_extensions`.
[ req ]
distinguished_name = req_distinguished_name
[ req_distinguished_name ]
# Used by `req -x509` for the sandbox's own CA. Without an explicit
# basicConstraints the generated certificate is not a CA, and every leaf it
# signs is rejected by the client with "invalid CA certificate (79)".
[ sandbox_ca_ext ]
basicConstraints = critical,CA:true
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
[ ca ]
default_ca = sandbox_ca
[ sandbox_ca ]
default_md = sha256
policy = policy_anything
email_in_dn = no
preserve = no
[ policy_anything ]
commonName = optional
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
emailAddress = optional
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env bash
# Pick the release tags the install/update E2E should update FROM.
#
# Emits a JSON array of tag names on stdout, suitable for a GitHub Actions
# matrix (`fromJSON`). Choosing at runtime rather than hardcoding keeps the
# matrix honest as releases land: a pinned list silently stops covering the
# newest release the day after it ships, and pins the "oldest" forever even
# after it stops being a version anyone still runs.
#
# Selection: the newest tag, the oldest tag, and evenly spaced tags in between.
# Newest catches "did the last release break updating?", oldest is the longest
# upgrade jump anyone can still make, and the spread samples the migrations in
# between (config-schema bumps, venv layout changes, dependency floors).
#
# Usage:
# scripts/sandbox/pick-release-tags.sh [--count N] [--repo DIR]
#
# --count how many tags to emit (default 5, minimum 1). Fewer tags than
# requested emits all of them.
# --repo repository to read tags from (default: this checkout).
#
# Reads tags from the local checkout, so it needs one fetched with tags
# (actions/checkout with fetch-depth: 0, or `fetch-tags: true`). A shallow
# checkout has no tags and this exits non-zero rather than silently emitting an
# empty matrix.
#
# Only vYYYY.M.D[.N] release tags are considered; the repo also carries
# backup/* and one-off tags that are not releases.
set -euo pipefail
COUNT=5
# Default to the repository containing this script, resolved through its real
# path so a symlinked or copied script still reads the checkout it lives in
# rather than whatever repo the caller happens to be standing in.
REPO=""
while [ "$#" -gt 0 ]; do
case "$1" in
--count)
[ "$#" -ge 2 ] || { echo 'error: --count needs a value' >&2; exit 1; }
COUNT="$2"; shift 2 ;;
--repo)
[ "$#" -ge 2 ] || { echo 'error: --repo needs a value' >&2; exit 1; }
REPO="$2"; shift 2 ;;
-h|--help) sed -n '2,30p' "$0"; exit 0 ;;
*) echo "error: unknown argument: $1" >&2; exit 1 ;;
esac
done
case "$COUNT" in
''|*[!0-9]*) echo "error: --count must be a positive integer: $COUNT" >&2; exit 1 ;;
esac
[ "$COUNT" -ge 1 ] || { echo 'error: --count must be at least 1' >&2; exit 1; }
# Resolve the script's own location through symlinks, then ask git which
# worktree that path belongs to. Deriving the repo from the script rather than
# from $PWD means a copied script cannot silently report a different checkout's
# tags, and --show-toplevel keeps it correct when invoked from a subdirectory.
if [ -z "$REPO" ]; then
script_path="${BASH_SOURCE[0]}"
if command -v readlink >/dev/null 2>&1; then
script_path="$(readlink -f "$script_path" 2>/dev/null || printf '%s' "$script_path")"
fi
script_dir="$(cd "$(dirname "$script_path")" && pwd)"
REPO="$(git -C "$script_dir" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$script_dir")"
fi
# sort -V orders v2026.4.8 before v2026.4.13 (numeric), which a plain
# lexicographic sort gets wrong.
mapfile -t tags < <(
git -C "$REPO" tag --list 'v*' \
| grep -E '^v[0-9]{4}\.[0-9]+\.[0-9]+(\.[0-9]+)?$' \
| sort -V
)
total="${#tags[@]}"
if [ "$total" -eq 0 ]; then
echo "error: no release tags found in $REPO" >&2
echo ' A shallow clone has no tags: fetch with tags (actions/checkout' >&2
echo ' with fetch-depth: 0, or fetch-tags: true).' >&2
exit 1
fi
if [ "$total" -le "$COUNT" ]; then
picked=("${tags[@]}")
elif [ "$COUNT" -eq 1 ]; then
# One slot means the newest release; there is no span to spread across.
picked=("${tags[$((total - 1))]}")
else
# Evenly spaced indices across [0, total-1], endpoints included, so the
# oldest and newest are always present and the rest are spread between them.
picked=()
for slot in $(seq 0 $((COUNT - 1))); do
# Round to nearest rather than truncate, so the spacing does not bunch
# toward the oldest end.
index=$(( (slot * (total - 1) * 2 + (COUNT - 1)) / ((COUNT - 1) * 2) ))
candidate="${tags[$index]}"
# Guard against a duplicate if rounding lands twice on the same tag.
case " ${picked[*]-} " in
*" $candidate "*) continue ;;
esac
picked+=("$candidate")
done
fi
printf '['
for i in "${!picked[@]}"; do
[ "$i" -eq 0 ] || printf ','
printf '"%s"' "${picked[$i]}"
done
printf ']\n'
+237
View File
@@ -0,0 +1,237 @@
"""MITM proxy backing the dev sandbox's fake Internet.
Listens on 127.0.0.1:8080 and is pointed at by http_proxy/https_proxy inside
the sandbox. For each request it either serves a fixture from the filesystem or
forwards to the real host:
* ``<root>/<host>/<path>`` exists -> serve it. This is how the sandbox answers
the canonical install URL with the installer under test, so the payload can
run the true ``curl -fsSL https://…/install.sh | bash`` one-liner.
* otherwise -> forward upstream, verifying against the real CA bundle. The
sandbox is isolated from the *host*, not from the internet: a real install
still has to reach PyPI and npm.
HTTPS is intercepted by minting a per-host certificate from the sandbox's own
throwaway CA, which the payload trusts via CURL_CA_BUNDLE / SSL_CERT_FILE.
Usage: proxy.py <fixture-root> <certs-dir> <real-ca-bundle>
"""
import os
import pathlib
import socket
import ssl
import subprocess
import sys
import threading
from urllib.parse import unquote, urlsplit
ROOT, CERTS, REAL_CA = map(pathlib.Path, sys.argv[1:])
LISTEN_ADDRESS = ('127.0.0.1', 8080)
MAX_REQUEST_BYTES = 65536
UPSTREAM_TIMEOUT_SECONDS = 30
CERT_VALIDITY_DAYS = 2
def read_request(conn):
data = b""
while b"\r\n\r\n" not in data and len(data) < MAX_REQUEST_BYTES:
part = conn.recv(4096)
if not part:
return b""
data += part
return data
def run_openssl(args):
"""Run openssl, raising with its stderr when it fails.
Discarding stderr here costs real debugging time: the caller sees only a
dropped connection (``curl: (35) Recv failure``) and the log holds nothing
but the argv, so an unwritable directory, a missing CA key, and an option
the host's openssl rejects all look identical.
"""
done = subprocess.run(
['openssl', *args], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE
)
if done.returncode != 0:
detail = done.stderr.decode('utf-8', 'replace').strip()
raise RuntimeError(
f'openssl {args[0]} failed (exit {done.returncode}): {detail}'
)
_CERT_LOCK = threading.Lock()
def cert_for(host):
"""Return a (cert, key) pair for host, minting it from the sandbox CA.
Minting is serialized and published atomically. The proxy is threaded, so
two concurrent requests for the same host would otherwise both run openssl
into the same paths, and a reader could pick up a finished certificate
beside a key from the other writer -- which TLS rejects as
``[X509: KEY_VALUES_MISMATCH] key values mismatch``.
"""
safe = ''.join(char if char.isalnum() or char in '.-' else '_' for char in host)
cert, key = CERTS / f'{safe}.pem', CERTS / f'{safe}.key'
if cert.exists() and key.exists():
return cert, key
with _CERT_LOCK:
# Re-check: another thread may have finished while we waited.
if cert.exists() and key.exists():
return cert, key
# Build under unique temp names, then rename into place. os.replace is
# atomic, so a reader sees either the old pair or the new one, never a
# half-written mix. The key lands first: the certificate's existence is
# what everything else keys off.
stamp = f'{os.getpid()}.{threading.get_ident()}'
tmp_key = CERTS / f'{safe}.key.{stamp}'
tmp_cert = CERTS / f'{safe}.pem.{stamp}'
csr = CERTS / f'{safe}.csr.{stamp}'
run_openssl([
'req', '-newkey', 'rsa:2048', '-nodes',
'-subj', f'/CN={host}',
'-addext', f'subjectAltName=DNS:{host}',
'-keyout', str(tmp_key), '-out', str(csr),
])
run_openssl([
'x509', '-req', '-days', str(CERT_VALIDITY_DAYS), '-in', str(csr),
'-CA', str(CERTS / 'ca.pem'), '-CAkey', str(CERTS / 'ca.key'),
'-CAcreateserial', '-copy_extensions', 'copy', '-out', str(tmp_cert),
])
csr.unlink(missing_ok=True)
os.replace(tmp_key, key)
os.replace(tmp_cert, cert)
return cert, key
def file_for(host, target):
"""Resolve a request to a fixture file, or None to forward upstream."""
path = urlsplit(target).path or '/'
parts = pathlib.PurePosixPath(unquote(path)).parts
if '..' in parts:
return None
candidate = ROOT / host / pathlib.PurePosixPath(*[p for p in parts if p != '/'])
if candidate.is_dir():
candidate /= 'index.html'
return candidate if candidate.is_file() else None
def respond_fixture(conn, found):
body = found.read_bytes()
headers = (
f'Content-Length: {len(body)}\r\nConnection: close\r\n\r\n'.encode()
)
conn.sendall(b'HTTP/1.1 200 OK\r\n' + headers + body)
def close_request(request, target=None):
"""Rewrite a proxied request for a direct upstream connection."""
headers, separator, body = request.partition(b'\r\n\r\n')
lines = headers.split(b'\r\n')
if target is not None:
method, _, version = lines[0].split(b' ', 2)
lines[0] = b' '.join((method, target.encode(), version))
lines = [
line for line in lines
if not line.lower().startswith(b'proxy-connection:')
]
lines.append(b'Connection: close')
return b'\r\n'.join(lines) + separator + body
def relay(source, destination):
while True:
chunk = source.recv(MAX_REQUEST_BYTES)
if not chunk:
return
destination.sendall(chunk)
def forward_https(conn, host, port, request):
context = ssl.create_default_context(cafile=str(REAL_CA))
with socket.create_connection((host, port), timeout=UPSTREAM_TIMEOUT_SECONDS) as raw:
with context.wrap_socket(raw, server_hostname=host) as upstream:
upstream.sendall(close_request(request))
relay(upstream, conn)
def forward_http(conn, host, port, request, target):
parsed = urlsplit(target)
path = parsed.path or '/'
if parsed.query:
path += f'?{parsed.query}'
with socket.create_connection((host, port), timeout=UPSTREAM_TIMEOUT_SECONDS) as upstream:
upstream.sendall(close_request(request, path))
relay(upstream, conn)
def handle_connect(conn, target):
"""Intercept a CONNECT tunnel, terminating TLS with a minted cert."""
host, _, port_text = target.rpartition(':')
port = int(port_text or '443')
conn.sendall(b'HTTP/1.1 200 Connection Established\r\n\r\n')
cert, key = cert_for(host)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(cert, key)
with context.wrap_socket(conn, server_side=True) as tls:
nested = read_request(tls)
if not nested:
return
line = nested.split(b'\r\n', 1)[0].decode('iso-8859-1')
nested_target = line.split(' ', 2)[1]
found = file_for(host, nested_target)
if found is not None:
respond_fixture(tls, found)
else:
forward_https(tls, host, port, nested)
def host_from_headers(request):
for header in request.split(b'\r\n')[1:]:
if header.lower().startswith(b'host:'):
value = header.split(b':', 1)[1].strip().decode()
return value.split(':', 1)[0]
return None
def handle_request(conn):
with conn:
request = read_request(conn)
if not request:
return
line = request.split(b'\r\n', 1)[0].decode('iso-8859-1')
method, target, _ = line.split(' ', 2)
if method.upper() == 'CONNECT':
handle_connect(conn, target)
return
parsed = urlsplit(target)
host = parsed.hostname or host_from_headers(request) or 'unknown'
found = file_for(host, target)
if found is not None:
respond_fixture(conn, found)
else:
forward_http(conn, host, parsed.port or 80, request, target)
def handle(conn):
try:
handle_request(conn)
except Exception as error:
print(f'proxy request failed: {error!r}', file=sys.stderr, flush=True)
def main():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(LISTEN_ADDRESS)
server.listen()
while True:
conn, _ = server.accept()
threading.Thread(target=handle, args=(conn,), daemon=True).start()
if __name__ == '__main__':
main()
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Stand-in for ssh inside the dev sandbox.
#
# install.sh and `hermes update` clone over ssh first (git@github.com:...), so
# the sandbox needs an `ssh` that answers. Rather than run a real sshd, this
# ignores the host, user, and command git asked for and speaks the
# upload-pack protocol directly against the sandbox's bare repo -- which is
# what makes the ssh-first code path exercisable with no keys, no known_hosts,
# and no network.
#
# GIT_UPLOAD_PACK is substituted by dev-sandbox.sh when it installs this shim,
# because the host's git-upload-pack is not necessarily on the sandbox PATH.
exec @GIT_UPLOAD_PACK@ /work/repos/hermes-agent.git
+251
View File
@@ -0,0 +1,251 @@
#!/usr/bin/env bash
# Stage 2 of the dev sandbox: build the mounts and run the payload.
#
# Not called directly. scripts/dev-sandbox.sh (stage 1) creates the user and
# network namespaces with `unshare` and re-execs into this script inside them,
# so by the time this runs we are already at the target uid with a private
# netns. bwrap therefore does NOT create a userns here -- it only adds the
# mount and pid namespaces. (`unshare --user` grants its creator full
# capabilities in the new userns regardless of which uid it maps, which is what
# lets bwrap mount as a non-root uid.)
#
# The whole interface with stage 1 is the DEV_SANDBOX_* environment, asserted
# below: there are no shared functions or variables between the two stages.
# Stage 1 locates this script alongside the other sandbox assets (see
# DEV_SANDBOX_ASSETS in dev-sandbox.sh), so the Nix wrapper's store copy and a
# plain repo checkout both work.
set -euo pipefail
: "${DEV_SANDBOX_ROOT:?missing DEV_SANDBOX_ROOT}"
: "${DEV_SANDBOX_BASH:?missing DEV_SANDBOX_BASH}"
: "${DEV_SANDBOX_INTERACTIVE:?missing DEV_SANDBOX_INTERACTIVE}"
: "${DEV_SANDBOX_USER:?missing DEV_SANDBOX_USER}"
: "${DEV_SANDBOX_HOME:?missing DEV_SANDBOX_HOME}"
# Announce our pid so stage 1 can point slirp4netns at these namespaces,
# then hold until it reports the network is up.
slirp_ready="$DEV_SANDBOX_ROOT/root/logs/slirp.ready"
printf '%s\n' "$$" > "$DEV_SANDBOX_ROOT/root/logs/sandbox.pid"
for _ in $(seq 1 200); do
[ -s "$slirp_ready" ] && break
sleep 0.05
done
if [ ! -s "$slirp_ready" ]; then
echo 'error: timed out waiting for sandbox network setup' >&2
cat "$DEV_SANDBOX_ROOT/root/logs/slirp.log" >&2 || true
exit 1
fi
# The sandbox HOME is /root for a root install and /home/<user> for a
# user-level one. Only the latter needs its parent created first; --dir /
# is not a thing bwrap accepts.
home_mounts=()
home_parent="$(dirname "$DEV_SANDBOX_HOME")"
if [ "$home_parent" != / ]; then
home_mounts+=(--dir "$home_parent")
fi
home_mounts+=(--bind "$DEV_SANDBOX_ROOT/home" "$DEV_SANDBOX_HOME")
node_env=()
if [ -n "${DEV_SANDBOX_NODE_DIR:-}" ]; then
node_env+=(--setenv npm_config_nodedir "$DEV_SANDBOX_NODE_DIR")
fi
electron_env=()
if [ -n "${DEV_SANDBOX_ELECTRON_LD_LIBRARY_PATH:-}" ]; then
electron_env+=(
--setenv LD_LIBRARY_PATH "$DEV_SANDBOX_ELECTRON_LD_LIBRARY_PATH"
--setenv HERMES_DESKTOP_DISABLE_GPU 1
)
fi
gui_mounts=()
if [ -n "${DEV_SANDBOX_WAYLAND_SOCKET:-}" ]; then
runtime_dir="${DEV_SANDBOX_XDG_RUNTIME_DIR:?missing DEV_SANDBOX_XDG_RUNTIME_DIR}"
runtime_parent="$(dirname "$runtime_dir")"
runtime_grandparent="$(dirname "$runtime_parent")"
gui_mounts+=(
--dir "$runtime_grandparent"
--dir "$runtime_parent"
--dir "$runtime_dir"
--bind "$DEV_SANDBOX_WAYLAND_SOCKET" "$DEV_SANDBOX_WAYLAND_SOCKET"
--setenv XDG_RUNTIME_DIR "$runtime_dir"
--setenv WAYLAND_DISPLAY "${DEV_SANDBOX_WAYLAND_DISPLAY:?missing DEV_SANDBOX_WAYLAND_DISPLAY}"
)
fi
# How the sandbox gets a usable runtime, and where its own shims go.
#
# On Nix, every binary lives under /nix/store, so the sandbox can own /bin,
# /lib64 and /usr/bin outright and fill them with symlinks into the store.
#
# Elsewhere the runtime IS /usr, /bin, /lib, /lib64 -- so binding the
# sandbox's near-empty versions over them hides the real thing, and bwrap
# dies with `execvp /usr/bin/bash: No such file or directory`. Keep the host
# directories read-only and override only the individual files we shim.
#
# The same answer decides how /etc is handled further down.
if [ -d /nix ] && [[ "$(readlink -f "$DEV_SANDBOX_BASH")" == /nix/* ]]; then
USE_HOST_RUNTIME=false
else
USE_HOST_RUNTIME=true
fi
runtime_mounts=()
shim_mounts=()
if [ "$USE_HOST_RUNTIME" = false ]; then
runtime_mounts+=(--ro-bind /nix /nix)
shim_mounts+=(
--dir /usr
--dir /bin
--dir /lib64
--bind "$DEV_SANDBOX_ROOT/root/bin" /bin
--bind "$DEV_SANDBOX_ROOT/root/lib64" /lib64
--bind "$DEV_SANDBOX_ROOT/root/usr/bin" /usr/bin
)
else
for path in /usr /bin /sbin /lib /lib64; do
[ -e "$path" ] && runtime_mounts+=(--ro-bind "$path" "$path")
done
# The git-upload-pack shim standing in for github.com is the only file that
# must beat the host's copy; sh/ls/env are already there for real.
shim_mounts+=(--bind "$DEV_SANDBOX_ROOT/root/usr/bin/ssh" /usr/bin/ssh)
fi
# /etc: start from a copy of the host's and overwrite only the files we fake.
#
# Replacing the whole directory with a five-file one is the tempting shortcut
# and it is wrong: a distro puts things under /etc that binaries outside /etc
# depend on, so hiding all of it breaks tools that look fine on PATH. Two real
# examples, both Debian/Ubuntu: openssl's compiled-in openssl.cnf is a symlink
# into /etc/ssl, and /usr/bin/awk is a symlink to /etc/alternatives/awk -- with
# /etc replaced, openssl cannot mint a certificate and awk reports "not found".
# Those are two symptoms of one cause, and nothing says there are only two.
#
# Copying rather than mount-overlaying the individual files, because several of
# these are symlinks in the wild (resolv.conf -> ../run/systemd/... on Ubuntu,
# hosts and nsswitch.conf -> /etc/static/... on NixOS) and bwrap cannot bind a
# file onto a symlink whose target does not exist inside the sandbox.
#
# Symlinks are copied as symlinks, never dereferenced: on NixOS /etc/static
# points into the store and following it would copy gigabytes per sandbox. The
# store is already mounted at /nix on that path, and the host runtime dirs are
# mounted at their own paths, so absolute symlinks still resolve.
#
# The five we override, and why each must differ from the host's:
# passwd, group the sandbox identity, which does not exist on the host
# resolv.conf slirp4netns's DNS, not the host resolver
# nsswitch.conf files+dns only, so nothing consults host NSS modules
# hosts minimal, so no host entry leaks in
#
# os-release is removed rather than replaced. Installers branch on it to reach
# for a package manager -- `install.sh` reads ID from it and, on debian/ubuntu,
# offers to apt-get build tools, prompting on /dev/tty when sudo exists but is
# not passwordless. That prompt cannot be satisfied here (no terminal) and it is
# fatal under `set -e`. Inheriting the host's file would make the sandbox claim
# to be a distro whose package manager it cannot actually use; absent means
# DISTRO="unknown" and the apt path is skipped, which is the truth.
etc_mounts=()
if [ "$USE_HOST_RUNTIME" = true ] && [ -d /etc ]; then
sandbox_etc="$DEV_SANDBOX_ROOT/etc-merged"
rm -rf -- "$sandbox_etc"
mkdir -p "$sandbox_etc"
# -a keeps symlinks as symlinks; unreadable entries (shadow, sudoers) are
# skipped rather than failing the run.
cp -a /etc/. "$sandbox_etc/" 2>/dev/null || true
for etc_file in passwd group resolv.conf nsswitch.conf hosts; do
[ -f "$DEV_SANDBOX_ROOT/etc/$etc_file" ] || continue
rm -f "$sandbox_etc/$etc_file"
cp "$DEV_SANDBOX_ROOT/etc/$etc_file" "$sandbox_etc/$etc_file"
done
rm -f "$sandbox_etc/os-release" "$sandbox_etc/lsb-release"
etc_mounts+=(--ro-bind "$sandbox_etc" /etc)
else
etc_mounts+=(--bind "$DEV_SANDBOX_ROOT/etc" /etc)
fi
# /dev without a tty, so a script guarding on `[ -e /dev/tty ]` takes its
# no-terminal path.
#
# bwrap's --dev creates a /dev/tty NODE, but nothing in here has a controlling
# terminal, so opening it fails with "No such device or address". That is the
# worst of both: the guard passes and the read then fails. Under `set -e` --
# which install.sh uses -- a failed read inside a function aborts the whole
# installer, which is exactly how older releases died here while prompting for
# sudo to install ripgrep/ffmpeg.
#
# Making the tty real is not the fix: with an openable terminal that prompt
# blocks forever waiting for input nobody will type. Absent is what a headless
# machine looks like, and what every prompt in here should assume.
#
# --dev cannot be used with the node removed afterwards (bwrap refuses to mount
# a directory over a device node), so /dev is assembled explicitly.
dev_mounts=(
--tmpfs /dev
--dev-bind /dev/null /dev/null
--dev-bind /dev/zero /dev/zero
--dev-bind /dev/full /dev/full
--dev-bind /dev/random /dev/random
--dev-bind /dev/urandom /dev/urandom
--symlink /proc/self/fd /dev/fd
--symlink /proc/self/fd/0 /dev/stdin
--symlink /proc/self/fd/1 /dev/stdout
--symlink /proc/self/fd/2 /dev/stderr
)
if [ "$DEV_SANDBOX_INTERACTIVE" = true ]; then
# An interactive shell is deliberately given a terminal; keep bwrap's /dev.
dev_mounts=(--dev /dev)
fi
exec bwrap \
--unshare-pid \
--die-with-parent --proc /proc --tmpfs /tmp \
"${dev_mounts[@]}" \
"${gui_mounts[@]}" \
"${runtime_mounts[@]}" \
--bind "$DEV_SANDBOX_ROOT/root" /work \
"${shim_mounts[@]}" \
--bind "$DEV_SANDBOX_ROOT/root/usr/local" /usr/local \
"${home_mounts[@]}" \
"${etc_mounts[@]}" \
--chdir /work/repo \
--clearenv \
--setenv PATH "$DEV_SANDBOX_HOME/.local/bin:/usr/local/bin:/usr/bin:$PATH" \
--setenv HOME "$DEV_SANDBOX_HOME" \
--setenv USER "$DEV_SANDBOX_USER" \
--setenv LOGNAME "$DEV_SANDBOX_USER" \
--setenv CURL_CA_BUNDLE /work/certs/ca.pem \
--setenv SSL_CERT_FILE /work/certs/ca.pem \
--setenv GIT_SSL_CAINFO /work/certs/ca.pem \
--setenv NODE_EXTRA_CA_CERTS /work/certs/real-ca.pem \
--setenv OPENSSL_CONF /work/certs/openssl.cnf \
--setenv HTTP_PROXY http://127.0.0.1:8080 \
--setenv HTTPS_PROXY http://127.0.0.1:8080 \
--setenv ALL_PROXY http://127.0.0.1:8080 \
--setenv NO_PROXY '' \
--setenv DEV_SANDBOX_INTERACTIVE "$DEV_SANDBOX_INTERACTIVE" \
--setenv ELECTRON_DISABLE_SANDBOX 1 \
"${node_env[@]}" \
"${electron_env[@]}" \
-- "$DEV_SANDBOX_BASH" -ceu '
python3 /work/proxy.py /work/http /work/certs /work/certs/real-ca.pem >/work/logs/proxy.log 2>&1 &
proxy_pid=$!
cleanup() {
kill "$proxy_pid" 2>/dev/null || true
wait "$proxy_pid" 2>/dev/null || true
}
trap cleanup EXIT INT TERM
# Bash opens /dev/tcp itself, so the readiness probe needs no netcat --
# one less binary the sandbox has to find on the host (GitHub runners
# ship no `nc`).
proxy_up() { (exec 3<>/dev/tcp/127.0.0.1/8080) 2>/dev/null; }
for _ in $(seq 1 100); do
proxy_up && break
sleep 0.05
done
if ! proxy_up; then
echo "error: the sandbox fake-internet proxy never came up" >&2
cat /work/logs/proxy.log >&2 || true
exit 1
fi
"$@"
' sandbox-command "$@"