Package prepared runtimes with the voice host (#42332)

## What changed

- Add an optional `--runtime` input to `assemble_package.py`.
- Validate the runtime receipt, target, source manifest, plugin inventory, paths,
  and file hashes before copying only the declared runtime files.
- Preserve each platform's runtime layout and record the copied files and
  `runtime.json` in the package manifest.
- Recheck hashes after copying and remove the new output if assembly fails.

## Testing

Add package assembly tests for Linux, macOS, and Windows layouts, invalid
receipts and paths, modified inputs, symlinked directories, and copy-time
changes.

GitOrigin-RevId: 8c9609af8ced406926707b2b0e1dcd6d2251f94b
This commit is contained in:
Benjamin Carlsson authored and copyberry committed 2026-09-02 19:13:32 +00:00
1 parent f59905647a
commit dc0dc4f15d
5 files changed
+363 -5

No files matched your search

+13 -2
View File
@@ -24,5 +24,16 @@ an existing validated package into a fresh output and adds the helper. Supply
Linux MUSL apps require same-architecture GNU helpers; other targets must match. Linux MUSL apps require same-architecture GNU helpers; other targets must match.
The package version must end in `+<build-commit>`. The manifest records declared The package version must end in `+<build-commit>`. The manifest records declared
build provenance and file hashes, not authentication or binary architecture proof. build provenance and file hashes, not authentication or binary architecture proof.
Native loading, media/privacy controls and actual audio proof remain subsequent Add `--runtime <prepared-runtime>` to include the platform preparer's selected
integration stages; this assembler does not add native runtime libraries. libraries and `runtime.json` beside the helper. The assembler checks the target,
pinned source manifest, plugin list, relative paths and file hashes, then checks
the copied hashes again. It preserves `lib/` and `plugins/` on macOS, `lib/` and
`lib/gstreamer-1.0/` on Linux, and the shared `bin/` on Windows. Unlisted files are
not copied. The package manifest records every included runtime file and the
unchanged runtime receipt.
Omitting `--runtime` retains helper-only assembly.
This accepts a development runtime receipt, not an authenticated release. It
does not repeat native loader inspection or establish trust in the build inputs.
The helper still does not load these files; native loading, media/privacy controls,
linking against the prepared SDK and actual audio proof remain integration stages.
+1
View File
@@ -35,6 +35,7 @@ filegroup(
"build_native.py", "build_native.py",
"linux_runtime.py", "linux_runtime.py",
"macos_runtime.py", "macos_runtime.py",
"package_runtime.py",
"runtime.py", "runtime.py",
"windows_runtime.py", "windows_runtime.py",
":source_inputs", ":source_inputs",
+45 -3
View File
@@ -1,4 +1,4 @@
"""Add a lifecycle-only helper to a fresh private copy of a canonical Codex package.""" """Add a helper and optional prepared runtime to a fresh private Codex package."""
import argparse import argparse
import hashlib import hashlib
@@ -6,9 +6,23 @@ import json
from pathlib import Path from pathlib import Path
import re import re
import shutil import shutil
import sys
# Import only this script's siblings, including under PYTHONSAFEPATH.
sys.path.insert(0, str(Path(__file__).resolve().parent))
from package_runtime import runtime_files
from runtime import digest
def assemble(package: Path, helper: Path, voice_target: str, commit: str, output: Path): def assemble(
package: Path,
helper: Path,
voice_target: str,
commit: str,
output: Path,
*,
runtime: Path | None = None,
):
package, helper = package.resolve(strict=True), helper.resolve(strict=True) package, helper = package.resolve(strict=True), helper.resolve(strict=True)
output = output.absolute() output = output.absolute()
if ( if (
@@ -57,6 +71,17 @@ def assemble(package: Path, helper: Path, voice_target: str, commit: str, output
raise ValueError("helper and app entrypoint must be regular files") raise ValueError("helper and app entrypoint must be regular files")
if not suffix and not helper.stat().st_mode & 0o111: if not suffix and not helper.stat().st_mode & 0o111:
raise ValueError("helper is not executable") raise ValueError("helper is not executable")
inputs = {}
if runtime is not None:
runtime = runtime.resolve(strict=True)
if any(parent.samefile(package) for parent in (runtime, *runtime.parents)):
raise ValueError("runtime must be outside the input package")
if any(
parent.exists() and parent.samefile(runtime)
for parent in output.resolve().parents
):
raise ValueError("output must be outside the runtime input")
inputs = runtime_files(runtime, voice_target)
output.mkdir() # Exclusive creation: never clean or overwrite a pre-existing output. output.mkdir() # Exclusive creation: never clean or overwrite a pre-existing output.
try: try:
shutil.copytree(package, output, dirs_exist_ok=True) shutil.copytree(package, output, dirs_exist_ok=True)
@@ -64,10 +89,19 @@ def assemble(package: Path, helper: Path, voice_target: str, commit: str, output
destination = output / relative_helper destination = output / relative_helper
destination.parent.mkdir(parents=True) destination.parent.mkdir(parents=True)
shutil.copy2(helper, destination) shutil.copy2(helper, destination)
for relative, expected_digest in inputs.items():
copied = output / "codex-resources/voice" / relative
copied.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(runtime / relative, copied)
if digest(copied) != expected_digest:
raise ValueError("runtime file changed during copying")
digests = {} digests = {}
for relative in (entrypoint, relative_helper): for relative in (entrypoint, relative_helper):
with (output / relative).open("rb") as source: with (output / relative).open("rb") as source:
digests[relative] = hashlib.file_digest(source, "sha256").hexdigest() digests[relative] = hashlib.file_digest(source, "sha256").hexdigest()
digests.update(
{f"codex-resources/voice/{name}": value for name, value in inputs.items()}
)
manifest = { manifest = {
"schemaVersion": 1, "schemaVersion": 1,
"buildCommit": commit, "buildCommit": commit,
@@ -91,7 +125,15 @@ if __name__ == "__main__":
parser.add_argument("--voice-target", required=True) parser.add_argument("--voice-target", required=True)
parser.add_argument("--build-commit", required=True) parser.add_argument("--build-commit", required=True)
parser.add_argument("--output", type=Path, required=True) parser.add_argument("--output", type=Path, required=True)
parser.add_argument(
"--runtime", type=Path, help="prepared development runtime to include unchanged"
)
args = parser.parse_args() args = parser.parse_args()
assemble( assemble(
args.package, args.helper, args.voice_target, args.build_commit, args.output args.package,
args.helper,
args.voice_target,
args.build_commit,
args.output,
runtime=args.runtime,
) )
+75
View File
@@ -0,0 +1,75 @@
"""Validate prepared runtime files before copying them into a private package.
The runtime receipt records preparation, not authenticity. Preserve its bytes and
layout; native loader inspection remains the platform preparer's responsibility.
"""
import hashlib
import json
from pathlib import Path
import re
from runtime import PLUGINS, digest
def runtime_files(root: Path, target: str) -> dict[str, str]:
manifest_path = root / "runtime.json"
if manifest_path.is_symlink() or not manifest_path.is_file():
raise ValueError("runtime manifest must be a regular file")
with manifest_path.open("rb") as source:
data = source.read(1024 * 1024 + 1)
if len(data) > 1024 * 1024:
raise ValueError("runtime manifest exceeds limit")
manifest = json.loads(data)
if (
manifest.get("schemaVersion") != 1
or manifest.get("developmentOnly") is not True
or manifest.get("target") != target
or manifest.get("sourceManifestSha256")
!= digest(Path(__file__).with_name("sources.json"))
or not re.fullmatch(r"[0-9a-f]{40}", manifest.get("sourceCommit", ""))
):
raise ValueError(
"runtime receipt does not match the source inputs and helper target"
)
if target.endswith("-apple-darwin"):
pattern, plugin = (
r"(?:lib|plugins)/[A-Za-z0-9_+.-]+\.dylib",
"plugins/libgst{}.dylib",
)
elif target.endswith("-unknown-linux-gnu"):
pattern, plugin = (
r"lib/(?:gstreamer-1\.0/)?[A-Za-z0-9_+.-]+\.so(?:\.[0-9]+)*",
"lib/gstreamer-1.0/libgst{}.so",
)
elif target.endswith("-pc-windows-msvc"):
pattern, plugin = r"bin/[A-Za-z0-9_+.-]+\.[dD][lL][lL]", "bin/gst{}.dll"
else:
raise ValueError("unsupported native runtime target")
libraries = manifest.get("libraries", [])
if not 1 <= len(libraries) <= 128:
raise ValueError("unexpected runtime inventory size")
files, names = {}, set()
for record in libraries:
name, expected = record["path"], record["sha256"]
if not re.fullmatch(pattern, name) or name.casefold() in names:
raise ValueError("invalid or colliding runtime path")
path = root / name
if (
path.parent.is_symlink()
or path.is_symlink()
or not path.is_file()
or not path.resolve().is_relative_to(root)
):
raise ValueError("runtime entries must be regular files inside the input")
if not re.fullmatch(r"[0-9a-f]{64}", expected) or digest(path) != expected:
raise ValueError("runtime file digest mismatch")
names.add(name.casefold())
files[name] = expected
plugins = sorted(plugin.format(name) for name in PLUGINS)
if sorted(manifest.get("plugins", [])) != plugins or not set(plugins).issubset(
files
):
raise ValueError("runtime must include exactly the selected plugins")
files["runtime.json"] = hashlib.sha256(data).hexdigest()
return files
+229
View File
@@ -2,12 +2,17 @@
import hashlib import hashlib
import json import json
import os
from pathlib import Path from pathlib import Path
import shutil
import subprocess
import sys
import tempfile import tempfile
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
from assemble_package import assemble from assemble_package import assemble
from runtime import PLUGINS, digest
class AssembleTests(unittest.TestCase): class AssembleTests(unittest.TestCase):
@@ -36,6 +41,230 @@ class AssembleTests(unittest.TestCase):
self.helper.chmod(0o755) self.helper.chmod(0o755)
self.output = self.root / "installed copy" self.output = self.root / "installed copy"
def make_runtime(
self, target="aarch64-unknown-linux-gnu", plugin="lib/gstreamer-1.0/libgst{}.so"
):
root = self.root / target
root.mkdir()
libraries = []
for name in PLUGINS:
path = root / plugin.format(name)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(f"prepared {name}".encode())
libraries.append(
{"path": path.relative_to(root).as_posix(), "sha256": digest(path)}
)
manifest = {
"schemaVersion": 1,
"developmentOnly": True,
"target": target,
"sourceCommit": "b" * 40,
"sourceManifestSha256": digest(Path(__file__).with_name("sources.json")),
"plugins": [record["path"] for record in libraries],
"libraries": libraries,
}
(root / "runtime.json").write_text(json.dumps(manifest))
return root, manifest
def test_cli_packages_each_runtime_layout_without_changing_bytes(self):
for target, plugin in (
("aarch64-unknown-linux-gnu", "lib/gstreamer-1.0/libgst{}.so"),
("aarch64-apple-darwin", "plugins/libgst{}.dylib"),
("aarch64-pc-windows-msvc", "bin/gst{}.dll"),
):
with self.subTest(target=target):
runtime, receipt = self.make_runtime(target, plugin)
(runtime / "unlisted-file").write_bytes(b"must not ship")
windows = target.endswith("windows-msvc")
entrypoint = "bin/codex.exe" if windows else "bin/codex"
self.metadata.update(target=target, entrypoint=entrypoint)
(self.package / entrypoint).write_bytes(b"unchanged app")
(self.package / "codex-package.json").write_text(
json.dumps(self.metadata)
)
output = self.root / (target + " packaged")
subprocess.run(
[
sys.executable,
str(Path(__file__).with_name("assemble_package.py").resolve()),
"--package",
str(self.package),
"--helper",
str(self.helper),
"--voice-target",
target,
"--build-commit",
self.commit,
"--output",
str(output),
"--runtime",
str(runtime),
],
check=True,
capture_output=True,
cwd=self.root,
env={**os.environ, "PYTHONSAFEPATH": "1"},
)
voice = output / "codex-resources/voice"
self.assertFalse((voice / "unlisted-file").exists())
expected = {r["path"]: r["sha256"] for r in receipt["libraries"]}
expected["runtime.json"] = digest(runtime / "runtime.json")
self.assertEqual(
{name: digest(voice / name) for name in expected}, expected
)
self.assertEqual(
{name: digest(runtime / name) for name in expected}, expected
)
manifest = json.loads((voice / "manifest.json").read_text())
helper_name = "codex-voice-host.exe" if windows else "codex-voice-host"
self.assertEqual(
manifest["sha256"],
{
entrypoint: digest(self.package / entrypoint),
f"codex-resources/voice/bin/{helper_name}": digest(self.helper),
**{
f"codex-resources/voice/{name}": value
for name, value in expected.items()
},
},
)
def test_rejects_invalid_runtime_receipts_before_creating_package(self):
runtime, original = self.make_runtime()
changes = [
{"target": "x86_64-unknown-linux-gnu"},
{"developmentOnly": False},
{"sourceManifestSha256": "0" * 64},
{"sourceCommit": "dev"},
{"plugins": original["plugins"][:-1]},
{"libraries": []},
{"libraries": original["libraries"] * 20},
{"libraries": original["libraries"] + [original["libraries"][0]]},
]
for name in (
"../outside.so",
"lib/../outside.so",
"bin/codex-voice-host",
"lib/evil:stream.so",
):
changes.append({"libraries": [{**original["libraries"][0], "path": name}]})
for change in changes:
with self.subTest(change=change), self.assertRaises(ValueError):
(runtime / "runtime.json").write_text(
json.dumps({**original, **change})
)
assemble(
self.package,
self.helper,
original["target"],
self.commit,
self.output,
runtime=runtime,
)
self.assertFalse(self.output.exists())
def test_rejects_runtime_digest_changes_and_nested_output(self):
runtime, receipt = self.make_runtime()
nested = runtime / "new package"
with self.assertRaisesRegex(ValueError, "outside the runtime"):
assemble(
self.package,
self.helper,
receipt["target"],
self.commit,
nested,
runtime=runtime,
)
self.assertFalse(nested.exists())
(runtime / receipt["plugins"][0]).write_bytes(b"changed")
with self.assertRaisesRegex(ValueError, "digest mismatch"):
assemble(
self.package,
self.helper,
receipt["target"],
self.commit,
self.output,
runtime=runtime,
)
self.assertFalse(self.output.exists())
def test_rejects_runtime_inside_input_package(self):
runtime, receipt = self.make_runtime()
nested = self.package / "runtime-staging"
runtime.rename(nested)
(nested / "unlisted-file").write_bytes(b"must not ship")
sources = [nested, self.package]
alias = self.package.with_name("APP") / "runtime-staging"
if alias.exists():
sources.append(alias)
for source in sources:
with (
self.subTest(source=source),
self.assertRaisesRegex(ValueError, "outside the input package"),
):
assemble(
self.package,
self.helper,
receipt["target"],
self.commit,
self.output,
runtime=source,
)
self.assertFalse(self.output.exists())
def test_rejects_symlinked_runtime_library_directories(self):
runtime, receipt = self.make_runtime()
outside = self.root / "outside"
(runtime / "lib/gstreamer-1.0").rename(outside)
try:
(runtime / "lib/gstreamer-1.0").symlink_to(
outside, target_is_directory=True
)
except OSError as error:
self.skipTest(f"symlink creation unavailable: {error}")
with self.assertRaisesRegex(ValueError, "regular files"):
assemble(
self.package,
self.helper,
receipt["target"],
self.commit,
self.output,
runtime=runtime,
)
self.assertFalse(self.output.exists())
def test_copy_revalidation_removes_only_new_output(self):
runtime, receipt = self.make_runtime()
original_copy = shutil.copy2
for changed_name in (receipt["plugins"][0], "runtime.json"):
def changed_copy(source, destination, **kwargs):
result = original_copy(source, destination, **kwargs)
if source == runtime.resolve() / changed_name:
Path(destination).write_bytes(b"changed after validation")
return result
with (
self.subTest(changed_name=changed_name),
patch("assemble_package.shutil.copy2", changed_copy),
):
with self.assertRaisesRegex(ValueError, "changed during copying"):
assemble(
self.package,
self.helper,
receipt["target"],
self.commit,
self.output,
runtime=runtime,
)
self.assertFalse(self.output.exists())
self.assertEqual(
json.loads((runtime / "runtime.json").read_text()), receipt
)
self.assertEqual(
(self.package / "bin/codex").read_bytes(), b"unchanged app"
)
def test_copies_app_unchanged_and_records_distinct_linux_targets(self): def test_copies_app_unchanged_and_records_distinct_linux_targets(self):
assemble( assemble(
self.package, self.package,