mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Add installed voice host lifecycle support (#41902)
## What changed - Add `VoiceHost` to resolve the packaged voice helper, launch it with an allowlisted environment, perform the protocol handshake, and enforce bounded shutdown and process cleanup. - Preserve native executable path encoding in the pipe process APIs. - Add `third_party/voice/assemble_package.py` to create a fresh package copy containing a target-compatible helper and a provenance manifest with file hashes. ## Testing - Cover installed helper lifecycle, build matching, missing and symlinked helpers, non-UTF-8 package paths, environment filtering, package validation, target pairing, and failure cleanup. GitOrigin-RevId: f893074b36ae6bb9bcedc00fbe7af6bb72745f4c
This commit is contained in:
committed by
copyberry
parent
f88ff940c0
commit
13bc770eaf
Vendored
+1
@@ -31,6 +31,7 @@ filegroup(
|
||||
filegroup(
|
||||
name = "build_inputs",
|
||||
srcs = [
|
||||
"assemble_package.py",
|
||||
"build_native.py",
|
||||
":source_inputs",
|
||||
],
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
"""Add a lifecycle-only helper to a fresh private copy of a canonical Codex package."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
|
||||
|
||||
def assemble(package: Path, helper: Path, voice_target: str, commit: str, output: Path):
|
||||
package, helper = package.resolve(strict=True), helper.resolve(strict=True)
|
||||
output = output.absolute()
|
||||
if output.exists() or output.is_symlink() or output.resolve().is_relative_to(package):
|
||||
raise ValueError("output must be fresh and outside the input package")
|
||||
if not re.fullmatch(r"[0-9a-f]{40}", commit):
|
||||
raise ValueError("a full build commit is required; dev builds are not distributable")
|
||||
metadata = json.loads((package / "codex-package.json").read_text())
|
||||
app_target = metadata["target"]
|
||||
targets = {
|
||||
f"{arch}-{suffix}": f"{arch}-{suffix.replace('musl', 'gnu')}"
|
||||
for arch in ("aarch64", "x86_64")
|
||||
for suffix in (
|
||||
"apple-darwin", "unknown-linux-gnu", "unknown-linux-musl", "pc-windows-msvc"
|
||||
)
|
||||
}
|
||||
if targets.get(app_target) != voice_target:
|
||||
raise ValueError("incompatible app and helper targets")
|
||||
suffix = ".exe" if app_target.endswith("windows-msvc") else ""
|
||||
entrypoint = f"bin/codex{suffix}"
|
||||
expected = {
|
||||
"layoutVersion": 1, "variant": "codex", "entrypoint": entrypoint,
|
||||
"resourcesDir": "codex-resources", "pathDir": "codex-path",
|
||||
}
|
||||
if any(metadata.get(key) != value for key, value in expected.items()):
|
||||
raise ValueError("input is not a canonical Codex package")
|
||||
if not metadata["version"].endswith(f"+{commit}"):
|
||||
raise ValueError("package version does not match the declared build")
|
||||
if (package / "codex-resources/voice").exists():
|
||||
raise ValueError("input already contains voice resources")
|
||||
for path in package.rglob("*"):
|
||||
if path.is_symlink() or not (path.is_file() or path.is_dir()):
|
||||
raise ValueError("package inputs must be regular files or directories")
|
||||
if not helper.is_file() or not (package / entrypoint).is_file():
|
||||
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")
|
||||
output.mkdir() # Exclusive creation: never clean or overwrite a pre-existing output.
|
||||
try:
|
||||
shutil.copytree(package, output, dirs_exist_ok=True)
|
||||
relative_helper = f"codex-resources/voice/bin/codex-voice-host{suffix}"
|
||||
destination = output / relative_helper
|
||||
destination.parent.mkdir(parents=True)
|
||||
shutil.copy2(helper, destination)
|
||||
digests = {}
|
||||
for relative in (entrypoint, relative_helper):
|
||||
with (output / relative).open("rb") as source:
|
||||
digests[relative] = hashlib.file_digest(source, "sha256").hexdigest()
|
||||
manifest = {
|
||||
"schemaVersion": 1, "buildCommit": commit, "appTarget": app_target,
|
||||
"voiceTarget": voice_target, "appVersion": metadata["version"], "sha256": digests,
|
||||
}
|
||||
(destination.parent.parent / "manifest.json").write_text(
|
||||
json.dumps(manifest, indent=2) + "\n", encoding="utf-8"
|
||||
)
|
||||
except BaseException:
|
||||
shutil.rmtree(output)
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--package", type=Path, required=True)
|
||||
parser.add_argument("--helper", type=Path, required=True)
|
||||
parser.add_argument("--voice-target", required=True)
|
||||
parser.add_argument("--build-commit", required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
assemble(args.package, args.helper, args.voice_target, args.build_commit, args.output)
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
"""Exercise private package copies, target pairing, provenance, and failure cleanup."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from assemble_package import assemble
|
||||
|
||||
|
||||
class AssembleTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
temporary = tempfile.TemporaryDirectory(prefix="voice package ")
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self.root = Path(temporary.name)
|
||||
self.package = self.root / "app"
|
||||
(self.package / "bin").mkdir(parents=True)
|
||||
(self.package / "codex-resources").mkdir()
|
||||
(self.package / "codex-path").mkdir()
|
||||
self.commit = "a" * 40
|
||||
self.metadata = {
|
||||
"layoutVersion": 1, "version": f"0.0.0+{self.commit}",
|
||||
"target": "aarch64-unknown-linux-musl", "variant": "codex",
|
||||
"entrypoint": "bin/codex", "resourcesDir": "codex-resources", "pathDir": "codex-path",
|
||||
}
|
||||
(self.package / "codex-package.json").write_text(json.dumps(self.metadata))
|
||||
(self.package / "bin/codex").write_bytes(b"unchanged app")
|
||||
self.helper = self.root / "helper.exe"
|
||||
self.helper.write_bytes(b"private helper")
|
||||
self.helper.chmod(0o755)
|
||||
self.output = self.root / "installed copy"
|
||||
|
||||
def test_copies_app_unchanged_and_records_distinct_linux_targets(self):
|
||||
assemble(self.package, self.helper, "aarch64-unknown-linux-gnu", self.commit, self.output)
|
||||
self.assertEqual((self.output / "bin/codex").read_bytes(), b"unchanged app")
|
||||
self.assertEqual((self.package / "bin/codex").read_bytes(), b"unchanged app")
|
||||
self.assertFalse((self.package / "codex-resources/voice").exists())
|
||||
self.assertEqual(
|
||||
(self.output / "codex-package.json").read_bytes(),
|
||||
(self.package / "codex-package.json").read_bytes(),
|
||||
)
|
||||
self.assertEqual(
|
||||
json.loads((self.output / "codex-resources/voice/manifest.json").read_text()),
|
||||
{
|
||||
"schemaVersion": 1, "buildCommit": self.commit,
|
||||
"appTarget": self.metadata["target"], "voiceTarget": "aarch64-unknown-linux-gnu",
|
||||
"appVersion": self.metadata["version"], "sha256": {
|
||||
"bin/codex": hashlib.sha256(b"unchanged app").hexdigest(),
|
||||
"codex-resources/voice/bin/codex-voice-host": hashlib.sha256(b"private helper").hexdigest(),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def test_rejects_incompatible_targets_and_unstamped_or_mixed_builds(self):
|
||||
for target, commit in [
|
||||
("aarch64-unknown-linux-musl", self.commit),
|
||||
("x86_64-unknown-linux-gnu", self.commit),
|
||||
("aarch64-unknown-linux-gnu", "dev"),
|
||||
("aarch64-unknown-linux-gnu", "b" * 40),
|
||||
]:
|
||||
with self.subTest(target=target, commit=commit), self.assertRaises(ValueError):
|
||||
assemble(self.package, self.helper, target, commit, self.output)
|
||||
self.assertFalse(self.output.exists())
|
||||
|
||||
def test_assembles_matching_gnu_linux_app_and_helper_targets(self):
|
||||
for architecture in ("aarch64", "x86_64"):
|
||||
target = f"{architecture}-unknown-linux-gnu"
|
||||
with self.subTest(target=target):
|
||||
self.metadata["target"] = target
|
||||
(self.package / "codex-package.json").write_text(json.dumps(self.metadata))
|
||||
output = self.root / target
|
||||
assemble(self.package, self.helper, target, self.commit, output)
|
||||
manifest = json.loads((output / "codex-resources/voice/manifest.json").read_text())
|
||||
self.assertEqual(
|
||||
manifest,
|
||||
{
|
||||
"schemaVersion": 1, "buildCommit": self.commit,
|
||||
"appTarget": target, "voiceTarget": target,
|
||||
"appVersion": self.metadata["version"], "sha256": {
|
||||
"bin/codex": hashlib.sha256(b"unchanged app").hexdigest(),
|
||||
"codex-resources/voice/bin/codex-voice-host": hashlib.sha256(b"private helper").hexdigest(),
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertEqual((output / "bin/codex").read_bytes(), b"unchanged app")
|
||||
self.assertEqual(
|
||||
(output / "codex-resources/voice/bin/codex-voice-host").read_bytes(),
|
||||
self.helper.read_bytes(),
|
||||
)
|
||||
|
||||
def test_never_replaces_existing_or_nested_outputs(self):
|
||||
for output in (self.package, self.package / "nested", self.helper):
|
||||
with self.subTest(output=output), self.assertRaises(ValueError):
|
||||
assemble(self.package, self.helper, "aarch64-unknown-linux-gnu", self.commit, output)
|
||||
self.assertEqual(self.helper.read_bytes(), b"private helper")
|
||||
self.assertFalse((self.package / "nested").exists())
|
||||
|
||||
def test_cleans_only_its_new_copy_on_failure(self):
|
||||
with patch("assemble_package.shutil.copy2", side_effect=OSError("copy failed")):
|
||||
with self.assertRaises(OSError):
|
||||
assemble(self.package, self.helper, "aarch64-unknown-linux-gnu", self.commit, self.output)
|
||||
self.assertFalse(self.output.exists())
|
||||
self.assertEqual((self.package / "bin/codex").read_bytes(), b"unchanged app")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user