fix: relocate long-path runtime console launchers
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e387-534d-70e3-ad53-4ea05676db8c
This commit is contained in:
parent
505e82db74
commit
81b5fcff8e
3 changed files with 113 additions and 10 deletions
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue