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
2026-09-02 18:38:37 +00:00
committed by copyberry
parent f59905647a
commit dc0dc4f15d
5 changed files with 363 additions and 5 deletions

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.
The package version must end in `+<build-commit>`. The manifest records declared
build provenance and file hashes, not authentication or binary architecture proof.
Native loading, media/privacy controls and actual audio proof remain subsequent
integration stages; this assembler does not add native runtime libraries.
Add `--runtime <prepared-runtime>` to include the platform preparer's selected
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.

View File

@@ -35,6 +35,7 @@ filegroup(
"build_native.py",
"linux_runtime.py",
"macos_runtime.py",
"package_runtime.py",
"runtime.py",
"windows_runtime.py",
":source_inputs",

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 hashlib
@@ -6,9 +6,23 @@ import json
from pathlib import Path
import re
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)
output = output.absolute()
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")
if not suffix and not helper.stat().st_mode & 0o111:
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.
try:
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.parent.mkdir(parents=True)
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 = {}
for relative in (entrypoint, relative_helper):
with (output / relative).open("rb") as source:
digests[relative] = hashlib.file_digest(source, "sha256").hexdigest()
digests.update(
{f"codex-resources/voice/{name}": value for name, value in inputs.items()}
)
manifest = {
"schemaVersion": 1,
"buildCommit": commit,
@@ -91,7 +125,15 @@ if __name__ == "__main__":
parser.add_argument("--voice-target", required=True)
parser.add_argument("--build-commit", 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()
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
third_party/voice/package_runtime.py vendored Normal file
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

View File

@@ -2,12 +2,17 @@
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch
from assemble_package import assemble
from runtime import PLUGINS, digest
class AssembleTests(unittest.TestCase):
@@ -36,6 +41,230 @@ class AssembleTests(unittest.TestCase):
self.helper.chmod(0o755)
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):
assemble(
self.package,