diff --git a/containers/barc-stock-headless/Containerfile b/containers/barc-stock-headless/Containerfile new file mode 100644 index 0000000..fb470f4 --- /dev/null +++ b/containers/barc-stock-headless/Containerfile @@ -0,0 +1,28 @@ +# Official Ubuntu 24.04 amd64 manifest; builder identity is an OUTPUT. +FROM docker.io/library/ubuntu@sha256:f610ab94648195aa356059f5b41d6085c9d4d903c072430cdd1af7bdb646106b AS toolchain +USER 0:0 +ENV DEBIAN_FRONTEND=noninteractive CC=gcc-13 CXX=g++-13 +COPY containers/barc-stock-headless/ /recipe/ +# Preserve Ubuntu archive signature validation; never use trusted=yes. +RUN sed -i '/^Suites:/a Snapshot: 20261001T000000Z' /etc/apt/sources.list.d/ubuntu.sources && \ + timeout --kill-after=5s 1200s apt-get update --snapshot 20261001T000000Z && \ + timeout --kill-after=5s 1200s apt-get install --snapshot 20261001T000000Z -y --no-install-recommends \ + ca-certificates git curl python3 cmake=3.28.3-1build7 g++-13 ninja-build \ + pkg-config binutils p7zip-full zip unzip tar autoconf automake libtool \ + libgl1-mesa-dev libglu1-mesa-dev libsdl2-dev libdevil-dev libopenal-dev \ + libogg-dev libvorbis-dev libxcursor-dev libx11-dev libxrandr-dev \ + libfreetype-dev libfontconfig1-dev libcurl4-openssl-dev zlib1g-dev libssl-dev && \ + timeout --kill-after=5s 3600s python3 /recipe/build-image.py --mode toolchain +ENV PATH=/opt/vcpkg/installed/x64-linux/tools/protobuf:/opt/vcpkg/installed/x64-linux/tools/grpc:${PATH} +ENV CMAKE_PREFIX_PATH=/opt/vcpkg/installed/x64-linux PKG_CONFIG_PATH=/opt/vcpkg/installed/x64-linux/lib/pkgconfig +FROM toolchain AS development +RUN timeout --kill-after=5s 3600s python3 /recipe/build-image.py --mode development +WORKDIR /work +CMD ["bash"] +# Default final target: the complete selected runtime is acquired at image build. +FROM development AS headless-integration +RUN timeout --kill-after=5s 3600s python3 /recipe/build-image.py --mode headless-integration +COPY containers/barc-stock-headless/entrypoint.sh /opt/barc/entrypoint.sh +USER 10001:10001 +WORKDIR /work +ENTRYPOINT ["bash", "/opt/barc/entrypoint.sh"] diff --git a/containers/barc-stock-headless/README.md b/containers/barc-stock-headless/README.md new file mode 100644 index 0000000..8a8972e --- /dev/null +++ b/containers/barc-stock-headless/README.md @@ -0,0 +1,46 @@ +# Pinned BAR headless runtime image + +The default `headless-integration` image acquires the full official Recoil engine payload, the selected BAR game dependency graph, Avalanche3.4 and a HighBar plugin built against matching engine headers during image creation. It needs no pre-existing host engine/game installation. The `development` stage remains available for source/protocol work. The default image is the full runtime selected by the user; its entrypoint launches only spring-headless. + +This source preparation has not built or run an image. Image creation and native gameplay acceptance are separate outcomes. Existing stopped runtimes, consumed attempts, settings/data and unknown operations remain unchanged. + +## Exact inputs + +[resources.json](resources.json) records the acquisition routes and byte/source pins from the existing owners: + +| Input | Exact selection | Owning evidence | +|---|---|---| +| Recoil binary | Official2026.07.04 amd64 Linux archive SHA2569824c2c3…; extracted headless651d6dca… | FSBar `docs/roadmaps/barc-01-useful-play.md`, selected stock engine update | +| Recoil headers | Commit de693612… and tree081bfe95…; recursive submodules use that tree's gitlinks | HighBar `scripts/build-barc-stock-plugin.sh` | +| HighBar plugin/source | Protected master b57f11f2…, tree7095b687…; stock contract ac0c9439… | Current HighBar source and existing stock plugin build | +| FSBar source/fixture | Main0f113726…, tree7feb453a… | Current owning source; fixture retained as source, not silently activated | +| Map | Official Maps2.0 release `avalanche_3.4.sd7`, SHA2563873260c… | HighBar `docs/roadmaps/barc-01-native-asset-proof.md:87` | +| Game | `Beyond All Reason test-29926-0571aa8`, source0571aa8e…, Rapid package7fd33d9c….sdp, SHA25658ca71d2… | Same owning proof:88–90 | + +The old `hydrate-bar-assets.sh` downloads an unhashed moving byar-chobby.sdz. This recipe uses the owned exact Rapid game selection instead. The selected downloader comes from the hash-verified official engine archive. Its pinned source09a1f37c… confirms filesystem-writepath, rapid-download, validate-sdp and rapid-validate. The exact .sdp is authenticated by SHA256; its complete pool is acquired and validated by the upstream downloader. SDP record count/declared expanded size and actual downloaded population are bounded. Rapid pool validation is its existing MD5/CRC contract, not a stronger signed content provenance claim. + +The existing HighBar build script generates the plugin with HIGHBAR_STOCK_RECOIL=ON and three compiled source controls. Generated protobuf/gRPC C++ uses the qualified local protoc/plugin, avoiding moving remote Buf plugins. The complete official engine payload and game graph are retained. No engine/game startup or useful-play proof runs during image creation. Exact-header compilation and source controls do not establish actual loaded ABI compatibility or gameplay. + +## Repository-owned toolchain bootstrap + +The `toolchain` stage starts from the official Ubuntu24.04 amd64 manifest f610ab94… and installs through Ubuntu's signed `20261001T000000Z` snapshot. The existing Ubuntu archive keyring and Signed-By checks remain active; no moving snapshot, trusted=yes or pre-existing toolchain image is accepted. CMake is pinned3.28.3-1build7; the produced CMake3.28.3 and GCC13.3.0 are verified before plugin compilation. The signed snapshot fixes the remaining apt package resolution; its actual complete package versions are recorded as build output, rather than invented from unobserved indexes. + +The builder fetches the existing HighBar vcpkg baseline256acc640… and verifies its exact commit/treee382dac5…. Its own bootstrap metadata pins the2026-04-08 glibc executable SHA512c5a43929…; the resulting downloaded executable must match before installation. The manifest retains HighBar's grpc/protobuf/abseil dependencies and adds GTest for the existing compiled stock controls. Baseline-selected gRPC1.76.0#1, Protobuf6.33.4#1, Abseil20260107.1#2 and GTest1.17.0#2 use the source archive hashes/patches owned by that immutable vcpkg tree. Protoc33.4 and gRPC1.76.0 are checked before generation. Vcpkg may acquire its own baseline-pinned supporting tools and dependency ports; their installed status is retained. This is a fresh source build, not reuse of historical GCC16/protobuf36 qualification. + +`HighBar/scripts/build-barc-stock-plugin.sh` permits BARC_SYSROOT; `/usr` supplies Ubuntu SDL2/DevIL/OpenAL paths. The vcpkg prefix is supplied through the existing CMake environment search path while preserving every script engine commit/tree, contract and source-control check. No retained old sysroot byte identity is a required source input. Actual loader ABI compatibility against the official engine remains an output requiring qualification. Recoil requests DevIL1.8.0, while its own FindDevIL module has no version support and Noble supplies1.7.8; that honest API/header compatibility gap needs the first real stock build, not a false1.8 claim. The same vcpkg baseline owns DevIL1.8.0#13 if a source-owned sysroot correction proves necessary. + +The builder records `/opt/barc/toolchain/ubuntu-packages.txt`, `vcpkg-status.txt`, generated manifest, tool-byte SHA256SUMS and MANIFEST.SHA256. Subsequent stages verify those produced bytes. The maintained platform runner separately retains the actual resulting immutable image ID and logs. These records are qualification OUTPUTS; they neither require a previously qualified image nor establish a successful build in advance. Official parent metadata is verified, but raw layer/config verification and the full resulting toolchain/build/ELF closure remain first-build obligations. + +## Build and runtime boundaries + +`python3 containers/barc-stock-headless/build-image.py --mode preflight` performs inert data-only pin/shape checks before any image pull. After existing machine admission, use the repository root as the container context and this Containerfile. No toolchain build arguments are needed. Its default final stage acquires the full selected engine/game/map runtime. `--target development` is available for source/plugin work. Image creation does not run spring-headless or invent a native acceptance attempt. + +Use the maintained execution runner and its platform profile for process custody, logs, whole-build deadline and cleanup. Each recipe stage has a3600second cap, apt operations1200seconds and plugin/game acquisition1800seconds; an admitted outer whole-build deadline may end the build earlier. One compile job is selected. The actual proposed platform profile records4CPU/12GiB memory and a3600+120second build/cleanup boundary; storage80GiB is a sampled logical ceiling, not an asserted hard quota. These are proposed limits and no measured adequacy is claimed. + +Each binary/map acquisition is limited512MiB/300seconds. Engine expansion is2GiB/4096entries, listing metadata2MiB, path depth16. Git operations have600second deadlines inside their stage. Game census limits32768files/512directories/depth8/4GiB compressed and8GiB declared expansion. A final census does not bound peak downloader storage; the runner monitors scoped disk usage and preserves first failures. Runtime proposes one fresh UID10001 directory,1CPU/512MiB/128PIDs/120seconds. The actual platform supplies/enforces supported limits and measures signal propagation, teardown and evidence collection before native qualification; no hypothetical disk quota is an additional permanent bootstrap gate. + +Run as UID10001 with read-only root, all capabilities dropped, no-new-privileges and private network/PID namespaces. No GUI, privileged mode, host networking or existing host runtime/data mount is needed. The platform runner supplies a genuinely new writable `/work`, readonly `/input/startscript.txt`, its exact SHA256 and any specifically admitted owned UDS transport. No runtime or credential data is baked into the image. + +The entrypoint refuses a nonempty/linked runtime, verifies the startscript, confines token cleanup to the new directory and reuses the maintained HighBar `_launch.sh`. Sourcing it preserves a direct engine child that the entrypoint waits for and forwards termination to. Engine logs, PID, replays and settings remain in `/work` for the runner's independent terminal evidence/cleanup collection. Source setup copies small configuration/plugin data; immutable map/game resources are read through SPRING_DATADIR. No old engine packet is replaced, resumed or retried. + +The complete source-defined stock observer/passive gameplay fixture and native coordinator/broker journeys have their existing owner/admission joins. This image retains FSBar fixture source but does not invent modinfo, a startscript, actual game/map checksums, actor grants, platform trust or native acceptance. Full runtime acquisition does not close Count1, six useful-play journeys, receiver adoption or publication. diff --git a/containers/barc-stock-headless/build-image.py b/containers/barc-stock-headless/build-image.py new file mode 100644 index 0000000..8dbedbe --- /dev/null +++ b/containers/barc-stock-headless/build-image.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +"""Acquire exact BAR image inputs and reuse the existing stock plugin build.""" +import argparse +import hashlib +import gzip +import struct +import json +import os +import pathlib +import re +import shutil +import subprocess + +ROOT = pathlib.Path(__file__).resolve().parent +SHA = re.compile(r'[0-9a-f]{64}') +COMMIT = re.compile(r'[0-9a-f]{40}') +IMAGE = re.compile(r'(?:[a-z0-9][a-z0-9./:_-]*@sha256:[0-9a-f]{64}|sha256:[0-9a-f]{64})') + +def require(condition, reason): + if not condition: + raise ValueError(reason) + +def check_inputs(pins): + tool = pins['toolchain']; boot = tool['bootstrap'] + require(bool(IMAGE.fullmatch(tool['parentOsCandidate'])), 'digest-pinned Ubuntu parent required') + require(bool(re.fullmatch(r'20[0-9]{6}T[0-9]{6}Z', boot['ubuntuSnapshot'])), 'fixed signed snapshot required') + require(boot['vcpkgCommit'] == tool['vcpkgBaseline'] and bool(COMMIT.fullmatch(boot['vcpkgTree'])), 'exact canonical vcpkg commit/tree required') + require(bool(re.fullmatch(r'[0-9a-f]{128}', boot['vcpkgToolGlibcSha512'])), 'vcpkg tool hash missing') + for row in pins['sources'].values(): + require(row['url'].startswith('https://github.com/'), 'source must use the declared HTTPS repository') + require(bool(COMMIT.fullmatch(row['commit'])) and bool(COMMIT.fullmatch(row['tree'])), 'exact source commit/tree required') + require(pins['engine']['release'] == '2026.07.04', 'stock engine release differs') + for name in ('imageBuildSeconds', 'downloadSeconds', 'compileSeconds', 'gitFetchSeconds'): + require(type(pins['budgets'][name]) is int and 0 < pins['budgets'][name] <= 3600, 'invalid finite stage deadline') + for value in (pins['engine']['archiveSha256'], pins['engine']['headlessSha256'], pins['map']['sha256'], pins['game']['sdpSha256'], pins['stockContractSha256']): + require(bool(SHA.fullmatch(value)), 'resource SHA256 is missing') + require(pins['game']['name'] == 'Beyond All Reason test-29926-0571aa8', 'moving/unselected game name refused') + require(pins['game']['sdp'] == '7fd33d9ceb79c052065b8d3c2f6f2687.sdp', 'game package identity differs') + +def sha(path, maximum): + require(path.is_file() and not path.is_symlink(), 'missing/linked input: ' + str(path)) + require(path.stat().st_size <= maximum, 'input exceeds byte budget: ' + str(path)) + result = hashlib.sha256() + with path.open('rb') as stream: + while block := stream.read(1024 * 1024): + result.update(block) + return result.hexdigest() + +def run(command, seconds, *, env=None, cwd=None): + subprocess.run(['timeout', '--kill-after=5s', str(seconds) + 's', *command], env=env, cwd=cwd, check=True) + +def acquire_file(url, expected, target, seconds, maximum): + require(not target.exists(), 'acquisition destination already exists') + target.parent.mkdir(parents=True, exist_ok=True) + run(['curl', '--fail', '--location', '--proto', '=https', '--proto-redir', '=https', + '--retry', '0', '--connect-timeout', '20', '--max-time', str(seconds), + '--max-filesize', str(maximum), '--output', str(target), url], seconds + 5) + require(sha(target, maximum) == expected, 'downloaded artifact hash differs: ' + str(target)) + +def source(row, target, seconds): + require(not target.exists(), 'source destination already exists') + target.mkdir(parents=True) + run(['git', 'init', '-q', str(target)], 30) + run(['git', '-C', str(target), 'remote', 'add', 'origin', row['url']], 30) + env = dict(os.environ, GIT_TERMINAL_PROMPT='0') + run(['git', '-C', str(target), 'fetch', '--depth=1', '--no-tags', 'origin', row['commit']], seconds, env=env) + run(['git', '-C', str(target), 'checkout', '--detach', 'FETCH_HEAD'], 30) + for expression, expected in [('HEAD', row['commit']), ('HEAD^{tree}', row['tree'])]: + actual = subprocess.check_output(['git', '-C', str(target), 'rev-parse', expression], text=True).strip() + require(actual == expected, 'fetched source identity differs') + +def check_listing(records, max_files, max_bytes): + seen = set(); total = 0 + for record in records: + name = record.get('Path', '') + path = pathlib.PurePosixPath(name) + require(name and path.as_posix() == name and not path.is_absolute() and '..' not in path.parts and len(path.parts) <= 16, 'unsafe archive path') + require(name not in seen and len(seen) < max_files, 'duplicate/excessive archive population') + require(not any('Link' in key for key in record), 'archive links require separate qualification') + seen.add(name) + count = int(record.get('Size', '0')) + require(count >= 0, 'negative archive size') + total += count + require(total <= max_bytes, 'archive expanded-byte budget exceeded') + require(bool(seen), 'empty engine archive') + +def engine_listing(archive, pins): + # The outer digest is verified before native archive inspection. Bound textual + # metadata while streaming, before accumulating the file population. + records = []; current = {}; active = False; metadata_bytes = 0 + child = subprocess.Popen(['timeout', '--kill-after=5s', '120s', '7z', 'l', '-slt', str(archive)], stdout=subprocess.PIPE) + try: + for line in child.stdout: + metadata_bytes += len(line) + require(metadata_bytes <= 2 * 1024 * 1024 and len(line) <= 4096, 'archive listing exceeds metadata budget') + text = line.decode('utf-8').rstrip('\r\n') + if text == '----------': + active = True; continue + if not active: + continue + if not text: + if current: + records.append(current); current = {} + require(len(records) <= pins['budgets']['engineEntriesMax'], 'archive population exceeded') + continue + key, value = text.split(' = ', 1) + require(key not in current, 'duplicate archive metadata field') + current[key] = value + if current: records.append(current) + require(child.wait(timeout=5) == 0, '7z engine inspection failed') + finally: + if child.poll() is None: + child.kill(); child.wait(timeout=5) + child.stdout.close() + check_listing(records, pins['budgets']['engineEntriesMax'], pins['budgets']['engineExpandedMaxBytes']) + +def data_population(root, files_max, bytes_max): + files = directories = total = 0; stack = [(root, 0)] + while stack: + directory, depth = stack.pop() + require(depth <= 8, 'game directory depth exceeded') + with os.scandir(directory) as entries: + for entry in entries: + require(not entry.is_symlink(), 'game data contains a symlink') + if entry.is_dir(follow_symlinks=False): + directories += 1 + require(directories <= 512, 'game directory population exceeded') + stack.append((pathlib.Path(entry.path), depth + 1)) + else: + require(entry.is_file(follow_symlinks=False), 'nonregular game data') + files += 1; total += entry.stat(follow_symlinks=False).st_size + require(files <= files_max and total <= bytes_max, 'game data population/bytes exceeded') + +def sdp_population(path, files_max, bytes_max): + # The authenticated .sdp uses the upstream pool format: filename length, + # filename, MD5 digest, CRC32 and little-endian uint32 uncompressed size. + files = expanded = 0; seen = set() + with gzip.open(path, 'rb') as stream: + while length := stream.read(1): + record = stream.read(length[0] + 24) + require(len(record) == length[0] + 24 and length[0] > 0, 'truncated SDP record') + name = record[:length[0]].decode('utf-8') + relative = pathlib.PurePosixPath(name) + require(relative.as_posix() == name and not relative.is_absolute() and '..' not in relative.parts, 'unsafe SDP member path') + require(name not in seen and files < files_max, 'duplicate/excessive SDP file population') + seen.add(name); files += 1 + expanded += struct.unpack(' 0, 'empty SDP') + +def toolchain(pins): + check_inputs(pins) + boot = pins['toolchain']['bootstrap']; target = pathlib.Path('/opt/vcpkg') + source({'url': boot['vcpkgRepository'], 'commit': boot['vcpkgCommit'], 'tree': boot['vcpkgTree']}, target, pins['budgets']['gitFetchSeconds']) + metadata = (target / 'scripts/vcpkg-tool-metadata.txt').read_text() + require(boot['vcpkgToolRelease'] in metadata and boot['vcpkgToolGlibcSha512'] in metadata, 'vcpkg bootstrap metadata differs') + run(['bash', str(target / 'bootstrap-vcpkg.sh'), '-disableMetrics'], 600) + executable = target / 'vcpkg' + require(executable.is_file() and executable.stat().st_size <= 64 * 1024 * 1024, 'vcpkg executable exceeds budget') + require(hashlib.sha512(executable.read_bytes()).hexdigest() == boot['vcpkgToolGlibcSha512'], 'bootstrapped vcpkg binary differs') + manifest_root = pathlib.Path('/opt/barc/toolchain'); manifest_root.mkdir(parents=True) + manifest = {'name':'barc-stock-toolchain', 'version-string':'0.1.0', + 'builtin-baseline':boot['vcpkgCommit'], 'dependencies':['grpc','protobuf','abseil','gtest']} + # The first three are the existing HighBar manifest; GTest is required by + # its stock source controls. Resolve all sources from the same baseline. + (manifest_root / 'vcpkg.json').write_text(json.dumps(manifest, indent=2) + '\n') + env = dict(os.environ, VCPKG_MAX_CONCURRENCY='1', VCPKG_DISABLE_METRICS='1') + run([str(target / 'vcpkg'), 'install', '--triplet=x64-linux', '--x-manifest-root=' + str(manifest_root), + '--x-install-root=/opt/vcpkg/installed', '--disable-metrics'], 3600, env=env) + packages = subprocess.check_output(['dpkg-query','-W','-f=${Package} ${Version} ${Architecture}\n'], text=True) + (manifest_root / 'ubuntu-packages.txt').write_text(packages) + status = target / 'installed/vcpkg/status' + require(status.is_file(), 'vcpkg installed package status missing') + shutil.copy2(status, manifest_root / 'vcpkg-status.txt') + # Record actual package identities and tool bytes as OUTPUTS, not fake + # prequalified inputs. The platform also records the resulting image ID. + files = [manifest_root / name for name in ('vcpkg.json','ubuntu-packages.txt','vcpkg-status.txt')] + files += [target / 'vcpkg', pathlib.Path('/usr/bin/g++-13'), pathlib.Path('/usr/bin/cmake')] + for relative in ('tools/protobuf/protoc','tools/grpc/grpc_cpp_plugin'): + files.append(target / 'installed/x64-linux' / relative) + lines = [] + for path in files: + actual = path.resolve(strict=True) + lines.append(sha(actual, 512 * 1024 * 1024) + ' ' + str(actual)) + (manifest_root / 'SHA256SUMS').write_text('\n'.join(lines) + '\n') + (manifest_root / 'MANIFEST.SHA256').write_text(sha(manifest_root / 'SHA256SUMS', 1024 * 1024) + '\n') + +def verify_toolchain(): + manifest = pathlib.Path('/opt/barc/toolchain/SHA256SUMS') + expected = (manifest.parent / 'MANIFEST.SHA256').read_text().strip() + require(bool(SHA.fullmatch(expected)) and sha(manifest, 1024 * 1024) == expected, 'produced toolchain manifest differs') + run(['sha256sum', '--check', '--strict', str(manifest)], 120) + +def build(pins): + check_inputs(pins) + verify_toolchain() + cmake = subprocess.check_output(['cmake', '--version'], text=True).splitlines()[0] + require(cmake == 'cmake version ' + pins['toolchain']['cmakeVersion'], 'unselected CMake version') + require(subprocess.check_output(['g++-13', '-dumpfullversion'], text=True).strip() == pins['toolchain']['gccVersion'], 'unselected C++ compiler') + require(subprocess.check_output(['protoc', '--version'], text=True).strip() == 'libprotoc ' + pins['toolchain']['protobufVersion'], 'unselected protobuf generator') + require(subprocess.check_output(['pkg-config', '--modversion', 'grpc++'], text=True).strip() == pins['toolchain']['grpcVersion'], 'unselected grpc closure') + for command in ('git','curl','7z','ninja','timeout','grpc_cpp_plugin','readelf'): + require(shutil.which(command) is not None, 'missing qualified tool: ' + command) + budget = pins['budgets']; output = pathlib.Path('/opt/barc'); output.mkdir(exist_ok=True) + shutil.copy2(ROOT / 'resources.json', output / 'resources.json') + recoil = pathlib.Path('/src/recoil'); highbar = recoil / 'AI/Skirmish/BARb'; fsbar = pathlib.Path('/src/fsbar') + source(pins['sources']['recoil'], recoil, budget['gitFetchSeconds']) + # Gitlink revisions are bound by the verified engine tree. HighBar overrides + # only the explicitly owned BARb gitlink, not the engine's tracked files. + paths = subprocess.check_output(['git', '-C', str(recoil), 'config', '--file', '.gitmodules', '--get-regexp', 'path'], text=True) + for line in paths.splitlines(): + path = line.split(maxsplit=1)[1] + if path != 'AI/Skirmish/BARb': + run(['git', '-C', str(recoil), 'submodule', 'update', '--init', '--recursive', '--', path], budget['gitFetchSeconds']) + source(pins['sources']['highbar'], highbar, budget['gitFetchSeconds']) + source(pins['sources']['fsbar'], fsbar, budget['gitFetchSeconds']) + require(sha(highbar / 'contracts/barc-stock-queue-v1/contract.json', 1024 * 1024) == pins['stockContractSha256'], 'stock ABI/wire contract differs') + # Protobuf stubs are generated by the exact qualified tools; no moving Buf plugins. + generated = highbar / 'build/gen'; generated.mkdir(parents=True) + protos = sorted((highbar / 'proto/highbar').glob('*.proto')) + run(['protoc', '-I', str(highbar / 'proto'), '--cpp_out=' + str(generated), '--grpc_out=' + str(generated), + '--plugin=protoc-gen-grpc=' + shutil.which('grpc_cpp_plugin'), *map(str, protos)], 120) + env = dict(os.environ, BARC_BUILD_JOBS='1', BARC_SYSROOT=pins['toolchain']['sysroot']) + run(['bash', str(highbar / 'scripts/build-barc-stock-plugin.sh'), str(recoil), str(highbar), '/tmp/barc-plugin-build'], budget['compileSeconds'], env=env) + plugin = pathlib.Path('/tmp/barc-plugin-build/AI/Skirmish/BARb/data/libSkirmishAI.so') + require(plugin.is_file(), 'built plugin location differs; inspect actual build output') + run(['readelf', '--file-header', str(plugin)], 30) + # Runtime ABI/load qualification is separate; these are actual produced bytes. + (output / 'plugin').mkdir(); shutil.copy2(plugin, output / 'plugin/libSkirmishAI.so') + # Retain only source/setup needed by the maintained launcher; no old runtime. + installed = pathlib.Path('/opt/highbar') + for relative in ('data', 'tests/headless/LuaRules', 'tests/headless/LuaUI'): + if (highbar / relative).exists(): shutil.copytree(highbar / relative, installed / relative) + target = installed / 'tests/headless/_launch.sh'; target.parent.mkdir(parents=True, exist_ok=True); shutil.copy2(highbar / 'tests/headless/_launch.sh', target) + shutil.copytree(fsbar / 'tests/Broker.NativeProof/RuntimeEvidence/fixtures/seed-attribution-helper/fixture', output / 'source/seed-fixture') + (output / 'BUILD-SOURCES.txt').write_text('\n'.join(role + ' ' + row['commit'] + ' ' + row['tree'] for role,row in pins['sources'].items()) + '\n') + (output / 'PLUGIN.SHA256').write_text(sha(output / 'plugin/libSkirmishAI.so', budget['assetDownloadMaxBytes']) + ' /opt/barc/plugin/libSkirmishAI.so\n') + +def integrate(pins): + check_inputs(pins) + verify_toolchain() + output = pathlib.Path('/opt/barc'); budget = pins['budgets'] + require(not (output / 'engine').exists() and not (output / 'data').exists(), 'runtime acquisition requires a fresh image stage') + for role, row in pins['sources'].items(): + target = pathlib.Path('/src/recoil/AI/Skirmish/BARb') if role == 'highbar' else pathlib.Path('/src/' + role) + for expression, expected in [('HEAD', row['commit']), ('HEAD^{tree}', row['tree'])]: + require(subprocess.check_output(['git','-C',str(target),'rev-parse',expression], text=True).strip() == expected, 'development source identity differs') + run(['sha256sum','--check','--strict',str(output / 'PLUGIN.SHA256')], 30) + archive = pathlib.Path('/tmp/recoil.7z') + acquire_file(pins['engine']['url'], pins['engine']['archiveSha256'], archive, 300, budget['assetDownloadMaxBytes']) + engine_listing(archive, pins); (output / 'engine').mkdir() + run(['7z', 'x', '-y', '-o' + str(output / 'engine'), str(archive)], 300) + require(sha(output / 'engine/spring-headless', budget['engineExpandedMaxBytes']) == pins['engine']['headlessSha256'], 'headless binary differs') + # Retain the complete official engine payload; runtime entry is headless only. + data = output / 'data'; (data / 'maps').mkdir(parents=True) + acquire_file(pins['map']['url'], pins['map']['sha256'], data / 'maps' / pins['map']['file'], 300, budget['assetDownloadMaxBytes']) + prd = output / 'engine/pr-downloader'; require(prd.is_file(), 'pinned engine archive lacks its downloader') + env = dict(os.environ, PRD_RAPID_REPO_MASTER=pins['game']['rapidRepositoryMaster'], PRD_RAPID_USE_STREAMER='false') + run([str(prd), '--filesystem-writepath', str(data), '--rapid-download', pins['game']['name']], budget['downloadSeconds'], env=env) + sdp = data / 'packages' / pins['game']['sdp'] + require(sha(sdp, 8 * 1024 * 1024) == pins['game']['sdpSha256'], 'selected BAR manifest differs') + sdp_population(sdp, budget['gameFilesMax'], budget['gameExpandedMaxBytes']) + data_population(data, budget['gameFilesMax'], budget['gameCompressedMaxBytes']) + run([str(prd), '--filesystem-writepath', str(data), '--validate-sdp', str(sdp)], 300, env=env) + run([str(prd), '--filesystem-writepath', str(data), '--rapid-validate'], 300, env=env) + archive.unlink() + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--mode', choices=['preflight','toolchain','development','headless-integration'], default='development') + args = parser.parse_args() + try: + pins = json.loads((ROOT / 'resources.json').read_text()) + if args.mode == 'preflight': + check_inputs(pins) + elif args.mode == 'toolchain': + toolchain(pins) + elif args.mode == 'development': + build(pins) + else: + integrate(pins) + except (ValueError, OSError, subprocess.SubprocessError) as error: + parser.exit(2, 'BAR image preparation refused: ' + str(error) + '\n') diff --git a/containers/barc-stock-headless/entrypoint.sh b/containers/barc-stock-headless/entrypoint.sh new file mode 100755 index 0000000..07b4c55 --- /dev/null +++ b/containers/barc-stock-headless/entrypoint.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# The maintained HighBar launcher remains responsible for setup and engine launch. +set -euo pipefail +[[ $# -eq 1 && "$1" == /input/startscript.txt ]] || { + echo 'usage: image /input/startscript.txt (a separately admitted, hash-pinned input)' >&2; exit 64; +} +[[ "$(id -u)" == 10001 && -d /work && ! -L /work ]] || { + echo 'headless runtime requires UID10001 and its fresh /work directory' >&2; exit 2; +} +[[ -z "$(find /work -mindepth 1 -print -quit)" ]] || { + echo 'refusing retained/nonempty runtime: original attempts are never replayed' >&2; exit 2; +} +[[ -f "$1" && ! -L "$1" && "${BARC_STARTSCRIPT_SHA256:-}" =~ ^[0-9a-f]{64}$ ]] || { + echo 'missing exact startscript identity' >&2; exit 2; +} +[[ "$(sha256sum "$1" | cut -d' ' -f1)" == "$BARC_STARTSCRIPT_SHA256" ]] || { + echo 'startscript identity changed' >&2; exit 2; +} +# Clear paths stay inside this new attempt; no external token path is inherited. +export HIGHBAR_TOKEN_PATH=/work/highbar.token +export HIGHBAR_DATA_DIRS=/work:/opt/barc/data:/opt/barc/engine +export HIGHBAR_ENABLE_BUILTIN=false +unset DISPLAY WAYLAND_DISPLAY XAUTHORITY +mkdir -m 0700 /work/runtime /work/logs +SPRING_PID= +stop_child() { + if [[ -n "$SPRING_PID" ]]; then kill -TERM "$SPRING_PID" 2>/dev/null || true; fi +} +trap stop_child TERM INT +# Source the maintained launcher so its engine is a direct child that can be waited. +source /opt/highbar/tests/headless/_launch.sh \ + --start-script "$1" --engine /opt/barc/engine/spring-headless \ + --plugin-so /opt/barc/plugin/libSkirmishAI.so \ + --writedir /work --runtime-dir /work/runtime \ + --log /work/logs/engine.log --pid-file /work/engine.pid +status=0 +wait "$SPRING_PID" || status=$? +# A signal-interrupted wait must still collect the child's terminal status. +if kill -0 "$SPRING_PID" 2>/dev/null; then + stop_child + wait "$SPRING_PID" || status=$? +fi +exit "$status" diff --git a/containers/barc-stock-headless/resources.json b/containers/barc-stock-headless/resources.json new file mode 100644 index 0000000..6ade430 --- /dev/null +++ b/containers/barc-stock-headless/resources.json @@ -0,0 +1,80 @@ +{ + "engine": { + "release": "2026.07.04", + "url": "https://github.com/beyond-all-reason/RecoilEngine/releases/download/2026.07.04/recoil_2026.07.04_amd64-linux.7z", + "archiveSha256": "9824c2c38124e4b90a9b5f7c4e7200c3ea6503c0bd4bff0e0ff46212ec29dcab", + "headlessSha256": "651d6dca67ad99fde1a593d57eede0dfee167e200558988f154ad2bac1cfce21" + }, + "sources": { + "recoil": { + "url": "https://github.com/beyond-all-reason/RecoilEngine.git", + "commit": "de69361239d8c8b1012dba3f5aa3122954ea4da3", + "tree": "081bfe95b4e76e3212afa5805ab5a82e474c0b47" + }, + "highbar": { + "url": "https://github.com/FS-GG/HighBarV3.git", + "commit": "b57f11f290dc3239d894b0285ae272b6b21bc25d", + "tree": "7095b68768c962665718b0b9481bbbc8bbfd5fca" + }, + "fsbar": { + "url": "https://github.com/FS-GG/FSBarV2.git", + "commit": "0f113726dd632467464e5bd3125061069b5af456", + "tree": "7feb453a77c3c4b56332ae9362bd8a77739bed62" + } + }, + "map": { + "name": "Avalanche 3.4", + "file": "avalanche_3.4.sd7", + "url": "https://github.com/beyond-all-reason/Maps/releases/download/2.0/avalanche_3.4.sd7", + "sha256": "3873260c6b5e533490598488eab3ff0b6587c778aee60cbb71186938dfaee426" + }, + "game": { + "name": "Beyond All Reason test-29926-0571aa8", + "sourceCommit": "0571aa8ed85d53a80ca9396126212d98ebc3cf01", + "rapidRepositoryMaster": "https://repos.beyondallreason.dev/repos.gz", + "sdp": "7fd33d9ceb79c052065b8d3c2f6f2687.sdp", + "sdpSha256": "58ca71d252e89e844361293e3b6b0aa2fb29fd217094b83e2a8f51ed1541f250" + }, + "stockContractSha256": "ac0c94390a1b5da600e49e2d00d9a36d4782964a33603ddbc2c4bafbd3db8c28", + "toolchain": { + "sysroot": "/usr", + "cmakeVersion": "3.28.3", + "gccVersion": "13.3.0", + "protobufVersion": "33.4", + "grpcVersion": "1.76.0", + "vcpkgBaseline": "256acc64012b23a13041d8705805e1f23b43a024", + "parentOsCandidate": "docker.io/library/ubuntu@sha256:f610ab94648195aa356059f5b41d6085c9d4d903c072430cdd1af7bdb646106b", + "parentOsScope": "official Ubuntu24.04 linux/amd64 manifest metadata pin; raw layers/config and resulting builder remain qualification outputs", + "bootstrap": { + "ubuntuSnapshot": "20261001T000000Z", + "vcpkgRepository": "https://github.com/microsoft/vcpkg.git", + "vcpkgCommit": "256acc64012b23a13041d8705805e1f23b43a024", + "vcpkgTree": "e382dac5b60903d5051dff6ff899248a4c3a92e7", + "vcpkgToolRelease": "2026-04-08", + "vcpkgToolGlibcSha512": "c5a43929713980dc873fe1e1162d213880c4026cbae5c030544b3108964a85f5f8ec24b0eea0cd689e5caadee34c79dadf4982178f8c78b0fe857d267b4ce294", + "ports": { + "grpc": "1.76.0#1", + "protobuf": "6.33.4#1", + "abseil": "20260107.1#2", + "gtest": "1.17.0#2", + "c-ares": "1.34.6#1", + "re2": "2025-11-05#0" + } + } + }, + "budgets": { + "imageBuildSeconds": 3600, + "downloadSeconds": 1800, + "compileSeconds": 1800, + "gitFetchSeconds": 600, + "assetDownloadMaxBytes": 536870912, + "engineExpandedMaxBytes": 2147483648, + "engineEntriesMax": 4096, + "gameFilesMax": 32768, + "gameCompressedMaxBytes": 4294967296, + "gameExpandedMaxBytes": 8589934592, + "runtimeSeconds": 120, + "runtimeMemoryBytes": 536870912, + "runtimePids": 128 + } +} diff --git a/containers/barc-stock-headless/test_source.py b/containers/barc-stock-headless/test_source.py new file mode 100644 index 0000000..6210742 --- /dev/null +++ b/containers/barc-stock-headless/test_source.py @@ -0,0 +1,67 @@ +"""Inert controls: no container, network, compiler, engine or downloader runs.""" +import copy +import gzip +import hashlib +import importlib.util +import json +import pathlib +import struct +import tempfile +import unittest + +root = pathlib.Path(__file__).resolve().parent +spec = importlib.util.spec_from_file_location('bar_image_source', root / 'build-image.py') +module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module) +pins = json.loads((root / 'resources.json').read_text()) + +class ImageSourceTests(unittest.TestCase): + def test_moving_parent_tag_is_refused(self): + changed = copy.deepcopy(pins); changed['toolchain']['parentOsCandidate'] = 'ubuntu:latest' + with self.assertRaises(ValueError): module.check_inputs(changed) + def test_snapshot_is_required(self): + changed = copy.deepcopy(pins); changed['toolchain']['bootstrap']['ubuntuSnapshot'] = 'latest' + with self.assertRaises(ValueError): module.check_inputs(changed) + def test_bootstrap_hash_is_required(self): + changed = copy.deepcopy(pins); changed['toolchain']['bootstrap']['vcpkgToolGlibcSha512'] = '' + with self.assertRaises(ValueError): module.check_inputs(changed) + def test_declared_source_inputs_are_complete(self): + module.check_inputs(pins) + def test_wrong_game_selection_is_refused(self): + changed = copy.deepcopy(pins); changed['game']['name'] = 'Beyond All Reason stable' + with self.assertRaises(ValueError): module.check_inputs(changed) + def test_nonfinite_deadline_is_refused(self): + changed = copy.deepcopy(pins); changed['budgets']['downloadSeconds'] = 0 + with self.assertRaises(ValueError): module.check_inputs(changed) + def test_archive_paths_links_population_and_size(self): + module.check_listing([{'Path':'spring-headless','Size':'5'}], 2, 10) + for rows in ([{'Path':'../escape','Size':'5'}], [{'Path':'/escape','Size':'5'}], + [{'Path':'spring-headless','Size':'5','Symbolic Link':'escape'}], + [{'Path':'same','Size':'1'}, {'Path':'same','Size':'1'}], + [{'Path':'a','Size':'6'}, {'Path':'b','Size':'6'}], + [{'Path':'a','Size':'1'}, {'Path':'b','Size':'1'}, {'Path':'c','Size':'1'}]): + with self.subTest(rows=rows), self.assertRaises(ValueError): module.check_listing(rows, 2, 10) + def test_local_digest_and_symlink_refusal(self): + with tempfile.TemporaryDirectory() as directory: + path = pathlib.Path(directory) / 'data'; path.write_bytes(b'fixture') + self.assertEqual(module.sha(path, 7), hashlib.sha256(b'fixture').hexdigest()) + with self.assertRaises(ValueError): module.sha(path, 6) + link = pathlib.Path(directory) / 'link'; link.symlink_to(path) + with self.assertRaises(ValueError): module.sha(link, 7) + def test_game_population_is_bounded(self): + with tempfile.TemporaryDirectory() as directory: + path = pathlib.Path(directory); (path/'a').write_bytes(b'1234'); (path/'b').write_bytes(b'1234') + module.data_population(path, 2, 8) + with self.assertRaises(ValueError): module.data_population(path, 1, 8) + with self.assertRaises(ValueError): module.data_population(path, 2, 7) + def test_sdp_declared_population_and_expansion(self): + with tempfile.TemporaryDirectory() as directory: + path = pathlib.Path(directory) / 'fixture.sdp' + def write(name, size): + raw = bytes([len(name)]) + name + bytes(20) + struct.pack('