#!/usr/bin/env bash # Fixture image only: bounded public package acquisition, no host apt/service use. # Requires a separate manager admission; this script is not a runtime test pass. set -euo pipefail [[ $# == 3 ]] || { echo 'usage: build-terminal-handler-runtime.sh EMPTY_OUTPUT BROWSER_IMAGE LOCKED_FRONTEND_DIRECTORY' >&2; exit 2; } out=$(realpath -e "$1"); image=$2; deps=$(realpath -e "$3") [[ "$image" =~ ^sha256:[0-9a-f]{64}$ && -d "$deps/node_modules" ]] || exit 2 [[ -d "$out" && -z $(find "$out" -mindepth 1 -maxdepth 1 -print -quit) ]] || exit 2 root=$(cd "$(dirname "$0")/../.." && pwd) # The image commit is a host control-plane operation, not a container RUN. # Refuse an unbounded parent rather than relying on container limits for it. python3 - <<'PY' from pathlib import Path group = next(line.split(':', 2)[2] for line in Path('/proc/self/cgroup').read_text().splitlines() if line.startswith('0::')) root = Path('/sys/fs/cgroup') / group.lstrip('/') assert 0 < int((root / 'memory.max').read_text()) <= 1073741824, 'Bounded parent memory required' assert int((root / 'memory.swap.max').read_text()) == 0, 'Parent swap forbidden' assert 0 < int((root / 'pids.max').read_text()) <= 256, 'Bounded parent process count required' quota, period = (root / 'cpu.max').read_text().split() assert 0 < int(quota) <= int(period), 'Parent CPU limit required' PY cmp "$root/neode-ui/package.json" "$deps/package.json" cmp "$root/neode-ui/package-lock.json" "$deps/package-lock.json" # These exact inputs must be committed; unrelated dirty upload fixtures are not copied. git -C "$root" diff --exit-code HEAD -- neode-ui tests/apps/terminal-browser tests/apps/terminal-handler tests/apps/build-terminal-handler-runtime.sh >/dev/null [[ -z $(git -C "$root" ls-files --others --exclude-standard -- tests/apps/terminal-handler tests/apps/build-terminal-handler-runtime.sh) ]] || exit 2 git -C "$root" rev-parse HEAD > "$out/source.commit" printf '%s\n' "$image" > "$out/base-image" podman ps -aq --no-trunc | sort > "$out/containers.before" cleanup() { local status=$? file cid exists trap - EXIT INT TERM set +e for file in "$out/ui.cid" "$out/packages.cid"; do [[ -s "$file" ]] || continue cid=$(<"$file") if [[ "$cid" =~ ^[a-f0-9]{64}$ ]]; then timeout --kill-after=2s 10s podman rm -f "$cid" >> "$out/cleanup.log" 2>&1 || status=1 podman container exists "$cid"; exists=$? [[ "$exists" == 1 ]] || status=1 else status=1; fi done podman ps -aq --no-trunc | sort > "$out/containers.after" || status=1 cmp "$out/containers.before" "$out/containers.after" || status=1 printf '%s\n' "$status" > "$out/outer.exit" exit "$status" } trap cleanup EXIT trap 'exit 143' TERM trap 'exit 130' INT mkdir -p "$out/input/src/components" "$out/input/src/stores" "$out/ui-output" cp "$root/neode-ui/src/components/CLIPopup.vue" "$out/input/src/components/" cp "$root/neode-ui/src/stores/cli.ts" "$out/input/src/stores/" cp "$root/neode-ui/src/style.css" "$out/input/src/" cp "$root/neode-ui/"{tailwind.config.js,postcss.config.js,package.json,package-lock.json} "$out/input/" cp -R "$root/tests/apps/terminal-browser" "$out/input/fixture" mkdir -p "$out/input/fixture/public/assets/fonts/Montserrat" "$out/input/fixture/public/assets/img" cp "$root/neode-ui/public/assets/fonts/Montserrat/"{Montserrat-Bold,Montserrat-ExtraBold}.ttf "$out/input/fixture/public/assets/fonts/Montserrat/" cp "$root/neode-ui/public/assets/img/bg.jpg" "$out/input/fixture/public/assets/img/" (cd "$out/input" && find . -type f -print0 | sort -z | xargs -0 sha256sum) > "$out/input.sha256" nonce="$(date +%s)-$$" podman create --cidfile "$out/ui.cid" --name "archy-terminal-assets-$nonce" --pull=never \ --network=none --pid=private --ipc=private --http-proxy=false --no-hosts \ --read-only --cap-drop=all --security-opt=no-new-privileges --memory=1g --memory-swap=1g \ --cpus=1 --pids-limit=256 --timeout=90 --log-driver=none \ --tmpfs /tmp:rw,size=256m --tmpfs /work:rw,size=128m \ --env HOME=/tmp --env XDG_CONFIG_HOME=/tmp/config --env NODE_PATH=/browser/node_modules \ --volume "$out/input:/input:ro" --volume "$out/ui-output:/out:rw" --volume "$deps/node_modules:/deps/node_modules:ro" \ --workdir /work --entrypoint /bin/sh "$image" -ec ' cp -R /input/. /work/ mkdir node_modules for dep in /deps/node_modules/*; do ln -s "$dep" node_modules/; done node /deps/node_modules/vite/bin/vite.js build --config fixture/vite.config.mjs cp -R terminal-dist /out/ ' > "$out/ui-create.log" 2>&1 timeout --kill-after=10s 100s podman start --attach "$(<"$out/ui.cid")" > "$out/ui-build.log" 2>&1 [[ -f "$out/ui-output/terminal-dist/index.html" && -z $(find "$out/ui-output" -type l -print -quit) ]] || exit 2 # No source, home, credentials, engine socket or service storage is mounted into # the public Debian package acquisition container. Its writable root is private. podman create --cidfile "$out/packages.cid" --name "archy-terminal-packages-$nonce" --pull=never \ --network=slirp4netns:allow_host_loopback=false --pid=private --ipc=private --http-proxy=false --no-hosts \ --security-opt=no-new-privileges --memory=1g --memory-swap=1g --cpus=1 --pids-limit=128 \ --timeout=180 --log-driver=none --entrypoint /bin/sh "$image" -ec ' apt-get -o Acquire::Retries=0 -o Acquire::http::Timeout=20 -o Acquire::https::Timeout=20 update DEBIAN_FRONTEND=noninteractive apt-get -o Acquire::Retries=0 -o Acquire::http::Timeout=20 -o Acquire::https::Timeout=20 install -y --no-install-recommends tmux test ! -e /etc/tmux.conf && test ! -e /root/.tmux.conf mkdir -p /terminal dpkg-query -W > /terminal/package-versions.txt tmux -V > /terminal/tmux-version.txt sha256sum /usr/bin/tmux > /terminal/tmux.sha256 ' > "$out/packages-create.log" 2>&1 timeout --kill-after=10s 190s podman start --attach "$(<"$out/packages.cid")" > "$out/packages.log" 2>&1 cid=$(<"$out/packages.cid") podman cp "$out/ui-output/terminal-dist" "$cid:/terminal/dist" podman cp "$root/tests/apps/terminal-handler/check.cjs" "$cid:/terminal/check.cjs" # Keep the resulting fixture image; do not delete retained artifacts/caches. # The admitted parent must bound this control-plane commit to1GiB/1CPU/60s. timeout --kill-after=10s 60s podman commit --pause=false \ --change 'ENTRYPOINT ["/bin/false"]' --change 'CMD []' \ --change 'LABEL archy.fixture=terminal-handler-only' "$cid" > "$out/runtime-image" podman cp "$cid:/terminal/package-versions.txt" "$out/package-versions.txt" podman cp "$cid:/terminal/tmux-version.txt" "$out/tmux-version.txt" podman cp "$cid:/terminal/tmux.sha256" "$out/tmux.sha256" sha256sum "$root/tests/apps/terminal-handler/check.cjs" > "$out/driver.sha256" (cd "$out/ui-output/terminal-dist" && find . -type f -print0 | sort -z | xargs -0 sha256sum) > "$out/assets.sha256"