fix: relocate long-path runtime console launchers
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e387-534d-70e3-ad53-4ea05676db8c
This commit is contained in:
tegwick 2026-09-27 23:07:44 +02:00
parent 505e82db74
commit 81b5fcff8e
3 changed files with 113 additions and 10 deletions

View file

@ -11,6 +11,7 @@ import hashlib
import json import json
import os import os
import re import re
import shlex
import subprocess import subprocess
from pathlib import Path from pathlib import Path
@ -43,6 +44,49 @@ def install_claude(output: Path, source: Path, sha256: str, version: str) -> dic
"expected_version": version, "source": str(source)} "expected_version": version, "source": str(source)}
def relocate_entrypoints(output: Path) -> list[str]:
"""Rewrite direct shebangs and uv/distlib long-path shell launchers.
Long build paths (or spaces) produce a three-line shell/Python trampoline.
Its interpreter must also refer to the fixed in-sandbox runtime mount.
Recognize generated shapes, never arbitrary shell source or binary data.
"""
relocated = []
python_names = [p.name for p in (output / "bin").iterdir()
if re.fullmatch(r"python(?:[0-9]+(?:\.[0-9]+)*)?", p.name)]
interpreters = [str(output / "bin" / name) for name in python_names]
direct = {("#!" + name).encode() for name in interpreters}
trampolines = {
("'''exec' " + quoted + ' "$0" "$@"').encode()
for name in interpreters
for quoted in (shlex.quote(name), "'" + name.replace("'", "'\\''") + "'")
}
for path in sorted((output / "bin").iterdir()):
if path.is_symlink() or not path.is_file():
continue
with path.open("rb") as stream:
header = stream.read(8192)
lines = header.splitlines()
skip = 0
if lines and lines[0] in direct:
skip = 1
elif len(lines) >= 3 and lines[0] == b"#!/bin/sh":
if lines[1] in trampolines and lines[2] == b"' '''":
skip = 3
elif lines[1].startswith(b"'''exec'") and str(output).encode() in lines[1]:
raise ValueError("unsupported build-host Python trampoline")
if skip:
body = path.read_bytes().split(b"\n", skip)[skip]
path.write_bytes(f"#!{RUNTIME_MOUNT}/bin/python3\n".encode() + body)
relocated.append(path.name)
for required in ("rein-aharness", "glas-harness", "sandboxer"):
path = output / "bin" / required
expected = f"#!{RUNTIME_MOUNT}/bin/python3".encode()
if path.exists() and path.read_bytes().split(b"\n", 1)[0] != expected:
raise ValueError(f"unrelocated governed entrypoint: {required}")
return relocated
def verify_source_files(site: Path, mappings: list[tuple[Path, str, str]]) -> dict: def verify_source_files(site: Path, mappings: list[tuple[Path, str, str]]) -> dict:
"""Refuse stale/missing/extra package contents even when versions match.""" """Refuse stale/missing/extra package contents even when versions match."""
expected = {} expected = {}
@ -103,15 +147,7 @@ def build(output: Path, rein_source: Path, llm_source: Path,
else: else:
checked(["uv", "pip", "install", "--python", str(output / "bin/python3"), checked(["uv", "pip", "install", "--python", str(output / "bin/python3"),
str(rein_source), str(llm_source)]) str(rein_source), str(llm_source)])
# Console entrypoints must reference the in-sandbox mount, not the build host. entrypoints = relocate_entrypoints(output)
for path in (output / "bin").iterdir():
if not path.is_file() or path.is_symlink():
continue
with path.open("rb") as stream:
first_line = stream.readline(4096)
if first_line.startswith(f"#!{output}/bin/python".encode()):
body = path.read_bytes().partition(b"\n")[2]
path.write_bytes(f"#!{RUNTIME_MOUNT}/bin/python3\n".encode() + body)
metadata = json.loads(checked([ metadata = json.loads(checked([
str(output / "bin/python3"), "-c", str(output / "bin/python3"), "-c",
"import importlib.metadata,json,platform; " "import importlib.metadata,json,platform; "
@ -120,6 +156,7 @@ def build(output: Path, rein_source: Path, llm_source: Path,
])) ]))
if claude_binary is not None: if claude_binary is not None:
metadata["claude"] = install_claude(output, claude_binary, claude_sha256, claude_version) metadata["claude"] = install_claude(output, claude_binary, claude_sha256, claude_version)
metadata["relocated_entrypoints"] = entrypoints
metadata["source_revisions"] = revisions metadata["source_revisions"] = revisions
if owner_runtime: if owner_runtime:
lock = json.loads((rein_source / "deploy/runtime-contract-lock.json").read_text()) lock = json.loads((rein_source / "deploy/runtime-contract-lock.json").read_text())

View file

@ -96,3 +96,44 @@ def test_installed_package_contents_must_match_committed_source(tmp_path, change
else: else:
result = builder.verify_source_files(site, [(source, "fixture", "fixture")]) result = builder.verify_source_files(site, [(source, "fixture", "fixture")])
assert result["files_verified"] == 1 assert result["files_verified"] == 1
@pytest.mark.parametrize("launcher", ["direct", "long-path", "spaces"])
def test_console_entrypoint_runs_after_build_directory_disappears(tmp_path, monkeypatch, launcher):
import shutil
import subprocess
import sys
name = {"long-path": "long-" + "x" * 150, "spaces": "build with spaces"}.get(
launcher, "build"
)
output = tmp_path / name
(output / "bin").mkdir(parents=True)
shutil.copy2(sys.executable, output / "bin/python3")
interpreter = str(output / "bin/python3")
body = "print('entrypoint-ok')\n"
if launcher == "direct":
header = f"#!{interpreter}\n"
else:
header = "#!/bin/sh\n'''exec' '" + interpreter + "' \"$0\" \"$@\"\n' '''\n"
command = output / "bin/rein-aharness"
command.write_text(header + body)
command.chmod(0o755)
binary = output / "bin/native"
binary.write_bytes(b"\x7fELF\x00leave unchanged")
mount = tmp_path / "mounted"
monkeypatch.setattr(builder, "RUNTIME_MOUNT", str(mount))
assert builder.relocate_entrypoints(output) == ["rein-aharness"]
shutil.move(output, mount)
assert not output.exists()
result = subprocess.run([str(mount / "bin/rein-aharness")], capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert result.stdout.strip() == "entrypoint-ok"
assert (mount / "bin/native").read_bytes() == b"\x7fELF\x00leave unchanged"
def test_unknown_governed_launcher_fails_closed(tmp_path):
(tmp_path / "bin").mkdir()
(tmp_path / "bin/rein-aharness").write_text("#!/bin/sh\nexec /unreviewed/python3\n")
with pytest.raises(ValueError, match="unrelocated governed entrypoint"):
builder.relocate_entrypoints(tmp_path)

View file

@ -9,7 +9,7 @@ flavor: implementation
owner: codex owner: codex
topic_slug: bwrap-runtime-and-private-state topic_slug: bwrap-runtime-and-private-state
created: "2026-09-05" created: "2026-09-05"
updated: "2026-09-09" updated: "2026-09-27"
state_hub_workstream_id: "d3f12387-fd23-58f0-b979-9c811507614d" state_hub_workstream_id: "d3f12387-fd23-58f0-b979-9c811507614d"
--- ---
@ -282,3 +282,28 @@ The first standalone candidate exposed a stale cached local wheel despite curren
Git metadata. Owner builds now refresh all local distributions and compare every Git metadata. Owner builds now refresh all local distributions and compare every
installed package source/definition file with its tracked source; stale, missing or installed package source/definition file with its tracked source; stale, missing or
extra files refuse the build. Four content-conformance regressions cover that gap. extra files refuse the build. Four content-conformance regressions cover that gap.
## 2026-09-27 installed launcher defect and guarded correction
REINAH-WP-0003's single admitted attempt completed native provider/companion
apply, verify and exec, including scoped-token revocation. The claimed job
`6efa9436-6a91-47c0-94e5-b31b5a0e7e3a` failed before any provider request
reservation. The sandbox was destroyed and the source repository is unchanged.
The parent EUR 10 liability remains held for explicit reconciliation; no retry
or replacement authority is inferred.
Installed artifact `b6e4e8a4` contains uv-generated three-line shell launchers
that still reference a vanished temporary build interpreter. The builder had
only rewritten direct Python shebangs. Fixed both generated forms and added
actual relocation/execution tests for direct, long and space-containing build
paths, plus refusal of unknown governed launchers. Builder tests: 12 passed;
focused lint passes. Rein's real installed bwrap probe reproduces both broken
entrypoints at exit 127 with zero provider forwards; its new bootstrap checks
refuse the malformed artifact before claiming.
T04 remains `wait` and this plan stays `blocked`: replacement artifact build/
admission and a separately authorized successful model/tool/commit proof remain.
The existing SAND-WP-0015 / REINAH-WP-0003 / SECRETS-WP-0009 records retain the
work. No new task or workplan. Detailed non-secret receipts are in rein-aharness
`docs/evidence/2026-09-27-metered-*.json`.