mirror of
https://github.com/openai/codex.git
synced 2026-11-09 20:50:33 +00:00
Add shared development execpolicy rules
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
# Shared rules only allow commands that are generally safe for every project
|
||||
# developer. Prompt and denial policies belong in user config so the repository
|
||||
# does not impose those flows globally.
|
||||
|
||||
prefix_rule(
|
||||
pattern = [
|
||||
"just",
|
||||
[
|
||||
"fmt",
|
||||
"fmt-check",
|
||||
"test",
|
||||
"clippy",
|
||||
"fix",
|
||||
"bench",
|
||||
"bench-smoke",
|
||||
"bazel-test",
|
||||
"bazel-clippy",
|
||||
"bazel-lock-check",
|
||||
"bazel-lock-update",
|
||||
"bazel-argument-comment-lint",
|
||||
"argument-comment-lint",
|
||||
"build-for-release",
|
||||
],
|
||||
],
|
||||
decision = "allow",
|
||||
justification = """
|
||||
Needed: host caches or nested sandbox capabilities.
|
||||
Safe: fixed non-publishing recipes only operate on the trusted checkout.
|
||||
""",
|
||||
)
|
||||
|
||||
prefix_rule(
|
||||
pattern = ["cargo", ["build", "check", "clippy", "test"]],
|
||||
decision = "allow",
|
||||
justification = """
|
||||
Needed: Cargo uses host registry, Git, and cache state.
|
||||
Safe: these commands only build, check, lint, or test the trusted workspace.
|
||||
""",
|
||||
)
|
||||
|
||||
prefix_rule(
|
||||
pattern = ["cargo", "nextest", "run"],
|
||||
decision = "allow",
|
||||
justification = """
|
||||
Needed: Nextest needs host runtime and nested sandboxes.
|
||||
Safe: it only runs tests built from the trusted workspace.
|
||||
""",
|
||||
)
|
||||
|
||||
prefix_rule(
|
||||
pattern = ["bazel", ["build", "test", "query", "aquery", "cquery"]],
|
||||
decision = "allow",
|
||||
justification = """
|
||||
Needed: Bazel uses its host output base and caches.
|
||||
Safe: these commands only build, test, or inspect trusted checkout targets.
|
||||
""",
|
||||
)
|
||||
|
||||
prefix_rule(
|
||||
pattern = ["pnpm", ["format", "test"]],
|
||||
decision = "allow",
|
||||
justification = """
|
||||
Needed: pnpm uses its host store and metadata cache.
|
||||
Safe: checked-in scripts exclude install, exec, and publish operations.
|
||||
""",
|
||||
)
|
||||
|
||||
prefix_rule(
|
||||
pattern = ["pnpm", "run", ["format", "test"]],
|
||||
decision = "allow",
|
||||
justification = """
|
||||
Needed: pnpm uses its host store and metadata cache.
|
||||
Safe: checked-in scripts exclude install, exec, and publish operations.
|
||||
""",
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
# Pin Codex development tool paths
|
||||
|
||||
Pinning prevents trusted rules from matching an unexpected executable with the same name.
|
||||
|
||||
From the development shell's repository root:
|
||||
|
||||
```sh
|
||||
mkdir -p ~/.codex/rules
|
||||
python3 scripts/setup_dev_execpolicy.py \
|
||||
> ~/.codex/rules/codex_dev_tool_paths.rules
|
||||
```
|
||||
|
||||
This pins binaries for the repository's
|
||||
[development rules](../../.codex/rules/development.rules). Rules load when a
|
||||
Codex session starts.
|
||||
@@ -72,6 +72,7 @@ def buildifier_formatter_group(*, check: bool) -> FormatterGroup:
|
||||
or name.endswith((".BUILD.bazel", ".MODULE.bazel", ".bzl", ".sky"))
|
||||
or ".bzl." in name
|
||||
or ".sky." in name
|
||||
or (path.parent == Path(".codex/rules") and path.suffix == ".rules")
|
||||
):
|
||||
buildifier_files.append(path.as_posix())
|
||||
buildifier_files.sort()
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Print host executable pins for Codex development commands."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Sequence
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
|
||||
# Keep this explicit: justfile recipes can delegate to scripts that invoke other tools.
|
||||
HOST_EXECUTABLES = ("cargo", "just", "bazel", "pnpm")
|
||||
GENERATED_HEADER = "# Generated by scripts/setup_dev_execpolicy.py. Do not edit."
|
||||
WINDOWS_EXECUTABLE_SUFFIXES = (".exe", ".cmd", ".bat", ".com")
|
||||
|
||||
|
||||
def executable_lookup_name(path: Path, *, windows: bool = os.name == "nt") -> str:
|
||||
name = path.name
|
||||
if not windows:
|
||||
return name
|
||||
|
||||
name = name.lower()
|
||||
for suffix in WINDOWS_EXECUTABLE_SUFFIXES:
|
||||
if name.endswith(suffix):
|
||||
return name[: -len(suffix)]
|
||||
return name
|
||||
|
||||
|
||||
def absolute_without_resolving(path: str) -> Path:
|
||||
return Path(os.path.abspath(os.path.expanduser(path)))
|
||||
|
||||
|
||||
def discover_host_executables(
|
||||
names: Sequence[str] = HOST_EXECUTABLES,
|
||||
*,
|
||||
which: Callable[[str], str | None] = shutil.which,
|
||||
) -> dict[str, Path]:
|
||||
executables: dict[str, Path] = {}
|
||||
missing: list[str] = []
|
||||
|
||||
for name in names:
|
||||
raw_path = which(name)
|
||||
if raw_path is None:
|
||||
missing.append(name)
|
||||
continue
|
||||
|
||||
path = absolute_without_resolving(raw_path)
|
||||
if executable_lookup_name(path) != executable_lookup_name(Path(name)):
|
||||
raise RuntimeError(
|
||||
f"resolved path for {name!r} must retain that executable name: {path}"
|
||||
)
|
||||
executables[name] = path
|
||||
|
||||
if missing:
|
||||
raise RuntimeError(
|
||||
"missing required host executables on PATH: " + ", ".join(missing)
|
||||
)
|
||||
return executables
|
||||
|
||||
|
||||
def render_host_executable_rules(executables: Mapping[str, Path]) -> str:
|
||||
blocks = []
|
||||
for name, path in executables.items():
|
||||
blocks.append(
|
||||
"\n".join(
|
||||
[
|
||||
"host_executable(",
|
||||
f" name = {json.dumps(name)},",
|
||||
f" paths = [{json.dumps(str(path))}],",
|
||||
")",
|
||||
]
|
||||
)
|
||||
)
|
||||
return f"{GENERATED_HEADER}\n\n" + "\n\n".join(blocks) + "\n"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
executables = discover_host_executables()
|
||||
except RuntimeError as error:
|
||||
print(f"error: {error}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
sys.stdout.write(render_host_executable_rules(executables))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
FORMAT_SCRIPT_PATH = Path(__file__).with_name("format.py")
|
||||
FORMAT_SCRIPT_SPEC = importlib.util.spec_from_file_location(
|
||||
"codex_format_script",
|
||||
FORMAT_SCRIPT_PATH,
|
||||
)
|
||||
if FORMAT_SCRIPT_SPEC is None or FORMAT_SCRIPT_SPEC.loader is None:
|
||||
raise RuntimeError(f"could not load {FORMAT_SCRIPT_PATH}")
|
||||
format_script = importlib.util.module_from_spec(FORMAT_SCRIPT_SPEC)
|
||||
sys.modules[FORMAT_SCRIPT_SPEC.name] = format_script
|
||||
FORMAT_SCRIPT_SPEC.loader.exec_module(format_script)
|
||||
|
||||
|
||||
class BuildifierFormatterGroupTest(unittest.TestCase):
|
||||
def test_includes_top_level_codex_rule_files(self) -> None:
|
||||
repository_files = b"\0".join(
|
||||
[
|
||||
b".codex/rules/z.rules",
|
||||
b".codex/rules/a.rules",
|
||||
b".codex/rules/nested/ignored.rules",
|
||||
b"elsewhere.rules",
|
||||
]
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
format_script.subprocess,
|
||||
"check_output",
|
||||
return_value=repository_files,
|
||||
):
|
||||
group = format_script.buildifier_formatter_group(check=True)
|
||||
|
||||
self.assertEqual(
|
||||
group.commands[0].args[4:],
|
||||
(".codex/rules/a.rules", ".codex/rules/z.rules"),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from contextlib import redirect_stdout
|
||||
import io
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
import setup_dev_execpolicy
|
||||
|
||||
|
||||
class SetupDevExecpolicyTest(unittest.TestCase):
|
||||
def test_discovers_absolute_paths_without_resolving_them(self) -> None:
|
||||
paths = {
|
||||
"cargo": "/tools/bin/cargo",
|
||||
"just": "/tools/bin/just",
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
setup_dev_execpolicy.discover_host_executables(
|
||||
tuple(paths),
|
||||
which=paths.get,
|
||||
),
|
||||
{
|
||||
"cargo": Path("/tools/bin/cargo"),
|
||||
"just": Path("/tools/bin/just"),
|
||||
},
|
||||
)
|
||||
|
||||
def test_reports_all_missing_executables(self) -> None:
|
||||
with self.assertRaisesRegex(
|
||||
RuntimeError,
|
||||
"missing required host executables on PATH: just, pnpm",
|
||||
):
|
||||
setup_dev_execpolicy.discover_host_executables(
|
||||
("cargo", "just", "pnpm"),
|
||||
which=lambda name: "/tools/bin/cargo" if name == "cargo" else None,
|
||||
)
|
||||
|
||||
def test_renders_host_executable_rules(self) -> None:
|
||||
self.assertEqual(
|
||||
setup_dev_execpolicy.render_host_executable_rules(
|
||||
{
|
||||
"cargo": Path("/tools/bin/cargo"),
|
||||
"bazel": Path("/tools/bin/bazel"),
|
||||
}
|
||||
),
|
||||
"""# Generated by scripts/setup_dev_execpolicy.py. Do not edit.
|
||||
|
||||
host_executable(
|
||||
name = "cargo",
|
||||
paths = ["/tools/bin/cargo"],
|
||||
)
|
||||
|
||||
host_executable(
|
||||
name = "bazel",
|
||||
paths = ["/tools/bin/bazel"],
|
||||
)
|
||||
""",
|
||||
)
|
||||
|
||||
def test_main_prints_rules(self) -> None:
|
||||
executables = {"cargo": Path("/tools/bin/cargo")}
|
||||
stdout = io.StringIO()
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
setup_dev_execpolicy,
|
||||
"discover_host_executables",
|
||||
return_value=executables,
|
||||
),
|
||||
redirect_stdout(stdout),
|
||||
):
|
||||
result = setup_dev_execpolicy.main()
|
||||
|
||||
self.assertEqual(result, 0)
|
||||
self.assertEqual(
|
||||
stdout.getvalue(),
|
||||
setup_dev_execpolicy.render_host_executable_rules(executables),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user