mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
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:
committed by
copyberry
parent
f59905647a
commit
dc0dc4f15d
@@ -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.
|
||||
|
||||
1
third_party/voice/BUILD.bazel
vendored
1
third_party/voice/BUILD.bazel
vendored
@@ -35,6 +35,7 @@ filegroup(
|
||||
"build_native.py",
|
||||
"linux_runtime.py",
|
||||
"macos_runtime.py",
|
||||
"package_runtime.py",
|
||||
"runtime.py",
|
||||
"windows_runtime.py",
|
||||
":source_inputs",
|
||||
|
||||
48
third_party/voice/assemble_package.py
vendored
48
third_party/voice/assemble_package.py
vendored
@@ -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
75
third_party/voice/package_runtime.py
vendored
Normal 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
third_party/voice/test_assemble_package.py
vendored
229
third_party/voice/test_assemble_package.py
vendored
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user