From 2f07e4e85953629c6d5976a6d825a52e080e8a17 Mon Sep 17 00:00:00 2001 From: "erik.hauan" Date: Wed, 14 Oct 2026 14:20:50 -0700 Subject: [PATCH 1/2] Add fork-surface check (incl. local-dependency isolation) and tests check_fork_surface.py diffs HEAD against the upstream tag and fails on non-allowlisted upstream paths, budget overruns, or any upstream crate depending on codex-local-* (via cargo metadata, no crate lists). Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Qp4Gg5UQcjWVPZz3y58bWs --- local/fork-surface.toml | 27 +++- local/scripts/check_fork_surface.py | 151 +++++++++++++++++++++++ local/scripts/test_check_fork_surface.py | 67 ++++++++++ 3 files changed, 243 insertions(+), 2 deletions(-) create mode 100755 local/scripts/check_fork_surface.py create mode 100644 local/scripts/test_check_fork_surface.py diff --git a/local/fork-surface.toml b/local/fork-surface.toml index c3d49f48e8a5..87bbe2142d8f 100644 --- a/local/fork-surface.toml +++ b/local/fork-surface.toml @@ -1,5 +1,28 @@ -# Upstream paths the fork is allowed to modify. -allowlist = [ +# Fork-surface policy, enforced by local/scripts/check_fork_surface.py. +# +# The check diffs HEAD against the upstream tag in local/UPSTREAM_VERSION +# (`git diff --numstat ...HEAD`) and fails when a touched path is neither +# fork-owned nor allowed, or when the upstream edits exceed the budget. +# +# Schema: +# allow list of globs Upstream paths the fork may edit (the hook files). +# `allowlist` is accepted as an alias. +# generated list of globs Upstream paths that are allowed but excluded from +# the budget (e.g. Cargo.lock; verify by regeneration). +# ignore list of globs OPTIONAL extra fork-owned globs, appended to the +# built-in set, which is always ignored: local/**, +# codex-rs/local/**, boundaries/**, .claude/**, +# .codex/skills/**, .github/workflows/local-*.yml, +# .github/workflows/upstream-sync.yml +# [budget] +# max_lines integer Max added+removed lines over non-ignored, +# non-generated files. +# max_files integer Max count of those files. +# +# Globs use `*` (within one path segment), `**` (across segments), `?`. +# Paths are repo-relative with forward slashes. + +allow = [ "codex-rs/Cargo.toml", "codex-rs/cli/src/main.rs", "codex-rs/cli/build.rs", diff --git a/local/scripts/check_fork_surface.py b/local/scripts/check_fork_surface.py new file mode 100755 index 000000000000..838f6a11325f --- /dev/null +++ b/local/scripts/check_fork_surface.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Enforce the codex-local fork surface (plan Phase 6). Stdlib only. + +Prints one JSON object {ok, upstream, files[], total_lines, violations[]} and +exits 0 when ok, 1 on violations, 2 on a tool error (bad config, missing tag). +The policy schema is documented at the top of local/fork-surface.toml. +""" +import argparse +import json +import re +import subprocess +import sys +import tomllib +from pathlib import Path + +# Fork-owned paths: never counted, never need an allowlist entry. +BUILTIN_IGNORE = [ + "codex-rs/local/**", + "local/**", + "boundaries/**", + ".claude/**", + ".codex/skills/**", + ".github/workflows/local-*.yml", + ".github/workflows/upstream-sync.yml", +] + + +def glob_re(pattern): + out, i = [], 0 + while i < len(pattern): + c = pattern[i] + if pattern.startswith("**/", i): + out.append("(?:.*/)?") + i += 3 + continue + if pattern.startswith("**", i): + out.append(".*") + i += 2 + continue + out.append("[^/]*" if c == "*" else "[^/]" if c == "?" else re.escape(c)) + i += 1 + return re.compile("".join(out) + r"\Z") + + +def matches(path, patterns): + return any(glob_re(p).match(path) for p in patterns) + + +def git(root, *args): + r = subprocess.run(["git", "-C", str(root), *args], capture_output=True, text=True) + if r.returncode: + raise RuntimeError(f"git {' '.join(args)}: {r.stderr.strip()}") + return r.stdout + + +def resolve_ref(root, tag): + for ref in (tag, f"upstream/{tag}"): + try: + git(root, "rev-parse", "--verify", "--quiet", ref + "^{commit}") + return ref + except RuntimeError: + pass + raise RuntimeError(f"upstream ref not found: {tag} (also tried upstream/{tag})") + + +def local_dependency_violations(root): + """No crate outside codex-local-* may depend on codex-local-*, and only + codex-local-cli may depend on another local crate (it is the only root). + + sc-lint has no wildcard/default-allow mode (allowed_dependencies is + mandatory, so local crates could not depend on arbitrary upstream crates), + so this rule is enforced here, generically, with no crate lists. + """ + ws = Path(root) / "codex-rs" + if not (ws / "Cargo.toml").exists(): + return [] + r = subprocess.run(["cargo", "metadata", "--no-deps", "--format-version", "1"], + cwd=ws, capture_output=True, text=True) + if r.returncode: + raise RuntimeError("cargo metadata: " + r.stderr.strip()[-300:]) + out = [] + for pkg in json.loads(r.stdout)["packages"]: + name = pkg["name"] + for dep in pkg["dependencies"]: + if not dep["name"].startswith("codex-local-"): + continue + if not name.startswith("codex-local-"): + out.append({"kind": "upstream_depends_on_local", "crate": name, + "dependency": dep["name"]}) + elif name != "codex-local-cli" and dep["name"] != name: + out.append({"kind": "local_not_callable_from_cli_only", "crate": name, + "dependency": dep["name"]}) + elif name == "codex-local-cli" and dep["name"] == name: + continue + return out + + +def check(root): + root = Path(root) + tag = (root / "local/UPSTREAM_VERSION").read_text().strip() + cfg = tomllib.loads((root / "local/fork-surface.toml").read_text()) + allow = cfg.get("allow", cfg.get("allowlist", [])) + generated = cfg.get("generated", []) + ignore = BUILTIN_IGNORE + cfg.get("ignore", []) + budget = cfg.get("budget", {}) + ref = resolve_ref(root, tag) + + files, violations, total = [], [], 0 + for line in git(root, "diff", "--numstat", "--no-renames", f"{ref}...HEAD").splitlines(): + added, removed, path = line.split("\t", 2) + if matches(path, ignore) or matches(path, generated): + continue + # Binary files report "-"; they count as a file but add no lines. + n = (int(added) + int(removed)) if added != "-" else 0 + files.append({"path": path, "lines": n}) + total += n + if not matches(path, allow): + violations.append({"kind": "not_allowlisted", "path": path}) + files.sort(key=lambda f: f["path"]) + + max_lines, max_files = budget.get("max_lines"), budget.get("max_files") + if max_lines is not None and total > max_lines: + violations.append({"kind": "over_line_budget", "lines": total, "max": max_lines}) + if max_files is not None and len(files) > max_files: + violations.append({"kind": "over_file_budget", "files": len(files), "max": max_files}) + violations += local_dependency_violations(root) + return { + "ok": not violations, + "upstream": tag, + "files": files, + "total_lines": total, + "violations": violations, + } + + +def main(): + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap.add_argument("--root", default=None, help="repo root (default: parent of local/)") + args = ap.parse_args() + root = args.root or Path(__file__).resolve().parents[2] + try: + result = check(root) + except (RuntimeError, OSError, tomllib.TOMLDecodeError) as e: + print(json.dumps({"ok": False, "error": str(e), "violations": []})) + return 2 + print(json.dumps(result)) + return 0 if result["ok"] else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/local/scripts/test_check_fork_surface.py b/local/scripts/test_check_fork_surface.py new file mode 100644 index 000000000000..257018e15aae --- /dev/null +++ b/local/scripts/test_check_fork_surface.py @@ -0,0 +1,67 @@ +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +SCRIPT = Path(__file__).with_name("check_fork_surface.py") +TOML = 'allow = ["up/hook.rs"]\ngenerated = ["up/Cargo.lock"]\n[budget]\nmax_lines = 5\nmax_files = 2\n' + + +def git(root, *a): + subprocess.run(["git", "-C", str(root), "-c", "user.name=t", "-c", "user.email=t@t", + "-c", "commit.gpgsign=false", *a], check=True, capture_output=True) + + +class CheckForkSurface(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.root = Path(self.tmp.name) + git(self.root, "init", "-q") + self.write("local/UPSTREAM_VERSION", "v1\n") + self.write("local/fork-surface.toml", TOML) + self.write("up/hook.rs", "a\n") + self.write("up/other.rs", "a\n") + git(self.root, "add", "-A") + git(self.root, "commit", "-qm", "base") + git(self.root, "tag", "v1") + + def tearDown(self): + self.tmp.cleanup() + + def write(self, rel, text): + p = self.root / rel + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(text) + + def run_check(self): + git(self.root, "add", "-A") + git(self.root, "commit", "-qm", "change") + r = subprocess.run([sys.executable, str(SCRIPT), "--root", str(self.root)], + capture_output=True, text=True) + return r.returncode, json.loads(r.stdout) + + def test_ok(self): + self.write("up/hook.rs", "a\nb\n") + self.write("up/Cargo.lock", "x\n" * 100) # generated: not budgeted + self.write("codex-rs/local/x/lib.rs", "x\n" * 100) # fork-owned + code, out = self.run_check() + self.assertEqual((code, out["ok"], out["total_lines"]), (0, True, 1)) + self.assertEqual([f["path"] for f in out["files"]], ["up/hook.rs"]) + + def test_non_allowlisted_path(self): + self.write("up/other.rs", "a\nb\n") + code, out = self.run_check() + self.assertEqual(code, 1) + self.assertEqual(out["violations"], [{"kind": "not_allowlisted", "path": "up/other.rs"}]) + + def test_over_budget(self): + self.write("up/hook.rs", "a\n" + "b\n" * 9) + code, out = self.run_check() + self.assertEqual(code, 1) + self.assertEqual([v["kind"] for v in out["violations"]], ["over_line_budget"]) + + +if __name__ == "__main__": + unittest.main() From 3830b0930473ed02d601223ad8bbe2cf1bcc2235 Mon Sep 17 00:00:00 2001 From: "erik.hauan" Date: Wed, 14 Oct 2026 15:11:36 -0700 Subject: [PATCH 2/2] local: set fork-surface budget to measured totals plus headroom Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Qp4Gg5UQcjWVPZz3y58bWs --- local/fork-surface.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/local/fork-surface.toml b/local/fork-surface.toml index 87bbe2142d8f..7e20af78ca0b 100644 --- a/local/fork-surface.toml +++ b/local/fork-surface.toml @@ -38,6 +38,7 @@ allow = [ generated = ["codex-rs/Cargo.lock"] [budget] -# Spike-pass budget plus H8. -max_lines = 91 -max_files = 8 +# Measured 122 lines / 11 files across H1-H4 and H8 (Cargo.toml 1, H1 14, H2 36, H3 19, H4 18, +# H8 34 incl. tests) plus ~10% headroom. +max_lines = 135 +max_files = 12