Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .github/workflows/policy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ jobs:
TRAILER = re.compile(
r"^(Co-authored-by|Made-with|Generated-by|Generated-with|Assisted-by|Signed-off-by):"
r"|Generated with|cursor\.com|anthropic\.com|claude\.ai|openai\.com", re.I | re.M)
PRIVATE_PATH = re.compile(r"/(?:Users|home)/[^/\s]+/|\.(?:agent|workspace)/")
PLAN = re.compile(r"(?:\b[^\s/]+\.plan\.md\b|\bPLANS\.md\b)", re.I)
PRIVATE_PATH = re.compile(r"/(?:Users|home)/[^/\s]+/|[.]workspace/")
AGENT = re.compile(r"\b(ChatGPT|Codex|Cursor|Claude)\b", re.I)


Expand Down Expand Up @@ -83,8 +82,6 @@ jobs:
reasons.append("attribution trailer or tool attribution")
if PRIVATE_PATH.search(text):
reasons.append("private execution path")
if PLAN.search(text):
reasons.append("private plan file")
terms = sorted({match.group() for match in AGENT.finditer(text)
if match.group().lower() not in allow_terms})
if terms:
Expand Down Expand Up @@ -132,8 +129,12 @@ jobs:
comparison = git("hash-object", "-t", "tree", "--stdin").strip()
else:
comparison = git("merge-base", base, head).strip()
paths = git("diff", "--name-only", "--no-renames", "-z", comparison, head).split("\0")
paths = git("diff", "--name-only", "--no-renames", "--diff-filter=d", "-z", comparison, head).split("\0")
for path in filter(None, paths):
file = Path(path)
if (any(part in {".agent", ".workspace"} for part in file.parts)
or file.name == "PLANS.md" or file.name.endswith(".plan.md")):
findings.append(f"{path}: private execution file")
# Git can emit byte fixtures as text when they contain no NUL. Keep
# scanning their readable content without requiring valid UTF-8.
patch = git("diff", "--no-ext-diff", "--no-color", "--no-renames", "--unified=0",
Expand Down
16 changes: 11 additions & 5 deletions Documentation/Reference/WorkflowIntegration.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,23 @@ default-branch site builds publish through a separate non-cancelling workflow.

The required check is `policy / policy`. Pull requests check the title for
Conventional Commit syntax, attribution and unapproved terms, then scan only
added lines. Contributor identities and signatures do not block pull requests.
added lines for attribution, unapproved product terms, absolute personal paths
and paths into the local-only `.workspace` directory. Mentions of `.agent/`,
`PLANS.md` and `*.plan.md` are allowed in text. Changed file paths are checked
separately: added or modified files are rejected when any path segment is
`.agent` or `.workspace`, or the basename is `PLANS.md` or ends with `.plan.md`.
Deleting those files is allowed. Contributor identities and signatures do not
block pull requests.
Default-branch pushes require every new commit to be GitHub-verified with the
owner or GitHub as committer; external and bot authors are accepted. An initial
push checks the complete initial history. The embedded shared version validator
runs when `.github/release.json` exists.

The `allow-terms` input lists identifiers and product names required by the
repository, one per line. It does not exempt attribution, private paths or plan
files. Keep the list limited to the repository's actual public surface. Shared
policy sources and their tests name all supported tools, so this repository
declares those names in its own caller.
repository, one per line. It does not exempt attribution, absolute personal paths,
local-only directory paths or private execution files. Keep the list limited to
the repository's actual public surface. Shared policy sources and their tests
name all supported tools, so this repository declares those names in its own caller.

`Scripts/check-policy.py` owns the policy implementation. Run
`python3 Scripts/sync-policy-workflow.py` after editing it. The generated
Expand Down
4 changes: 3 additions & 1 deletion MAINTENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ file in the same pull request.
setting.

External pull requests and bot-authored changes are welcome. Pull-request
policy checks the title and added content. Default-branch push policy requires
policy checks the title, added content and added or modified file paths, as
defined in [Source policy](Documentation/Reference/WorkflowIntegration.md#source-policy).
Default-branch push policy requires
GitHub-verified commits committed by GitHub or the owner; it does not restrict
the original author. Dependabot, updater Apps and GitHub Actions may author
changes. Product names are allowed only through the caller's `allow-terms`.
Expand Down
11 changes: 6 additions & 5 deletions Scripts/check-policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
TRAILER = re.compile(
r"^(Co-authored-by|Made-with|Generated-by|Generated-with|Assisted-by|Signed-off-by):"
r"|Generated with|cursor\.com|anthropic\.com|claude\.ai|openai\.com", re.I | re.M)
PRIVATE_PATH = re.compile(r"/(?:Users|home)/[^/\s]+/|\.(?:agent|workspace)/")
PLAN = re.compile(r"(?:\b[^\s/]+\.plan\.md\b|\bPLANS\.md\b)", re.I)
PRIVATE_PATH = re.compile(r"/(?:Users|home)/[^/\s]+/|[.]workspace/")
AGENT = re.compile(r"\b(ChatGPT|Codex|Cursor|Claude)\b", re.I)


Expand Down Expand Up @@ -54,8 +53,6 @@ def content_findings(path, number, text, allow_terms):
reasons.append("attribution trailer or tool attribution")
if PRIVATE_PATH.search(text):
reasons.append("private execution path")
if PLAN.search(text):
reasons.append("private plan file")
terms = sorted({match.group() for match in AGENT.finditer(text)
if match.group().lower() not in allow_terms})
if terms:
Expand Down Expand Up @@ -103,8 +100,12 @@ def check_range(repository, base, head, allow_terms, *, verify_commits=False, fe
comparison = git("hash-object", "-t", "tree", "--stdin").strip()
else:
comparison = git("merge-base", base, head).strip()
paths = git("diff", "--name-only", "--no-renames", "-z", comparison, head).split("\0")
paths = git("diff", "--name-only", "--no-renames", "--diff-filter=d", "-z", comparison, head).split("\0")
for path in filter(None, paths):
file = Path(path)
if (any(part in {".agent", ".workspace"} for part in file.parts)
or file.name == "PLANS.md" or file.name.endswith(".plan.md")):
findings.append(f"{path}: private execution file")
# Git can emit byte fixtures as text when they contain no NUL. Keep
# scanning their readable content without requiring valid UTF-8.
patch = git("diff", "--no-ext-diff", "--no-color", "--no-renames", "--unified=0",
Expand Down
91 changes: 90 additions & 1 deletion Tests/test_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ def test_content_and_allowlist(self):
self.assertTrue(policy.content_findings("a", 3, "Co-authored-by: helper", set()))
workstation = "/" + "Users/fixture/project"
self.assertTrue(policy.content_findings("a", 3, workstation, set()))
self.assertTrue(policy.content_findings("a", 3, "/" + "home/fixture/project", set()))
self.assertTrue(policy.content_findings("a", 3, "." + "workspace/cache", set()))
self.assertTrue(policy.content_findings("a", 3, "example." + "plan.md", set()))
for text in (".agent/plans/x.md", ".agent/PLANS.md", "PLANS.md", "release.plan.md"):
with self.subTest(text=text):
self.assertEqual(policy.content_findings("a", 3, text, set()), [])
self.assertTrue(policy.content_findings("a", 3, "Codex", set()))
self.assertEqual(policy.content_findings("a", 3, "Codex API", {"codex"}), [])
self.assertTrue(policy.content_findings("a", 3, workstation + " Codex", {"codex"}))
Expand Down Expand Up @@ -86,6 +89,92 @@ def commit(text):
finally:
os.chdir(previous)

def test_real_git_private_file_paths(self):
output = ROOT / ".build/policy-tests"
output.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(dir=output) as directory:
previous = Path.cwd()
os.chdir(directory)
try:
subprocess.run(["git", "init", "-q"], check=True)
def commit():
policy.git("add", "--all", "--force")
policy.git("-c", "user.name=Fixture", "-c", "user.email=fixture@example.test",
"-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null", "commit", "-qm", "fix: fixture")
return policy.git("rev-parse", "HEAD").strip()
# Plan basenames and suffixes are matched case-sensitively.
public_paths = ("sample.txt", ".agent-notes/x.md", ".workspace-notes/x.md",
"docs/PLANS.md.example", "release.plan.md/guide.md", "plans.md", "docs/Release.PLAN.md")
for name in public_paths:
path = Path(name)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("Document .agent/plans/x.md, .agent/PLANS.md and release.plan.md.\n")
base = commit()
self.assertEqual(policy.check_range("example/repo", policy.ZERO_SHA, base, set()), [])
private_paths = (".agent/plans/x.md", ".agent/plans/draft notes.md", "docs/PLANS.md", "x.plan.md",
"." + "workspace/state.json", "docs/.agent/x.md", "docs/." + "workspace/state.json")
expected = sorted(f"{name}: private execution file" for name in private_paths)
for name in private_paths:
path = Path(name)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("initial\n")
added = commit()
self.assertEqual(sorted(policy.check_range("example/repo", base, added, set())), expected)
self.assertEqual(sorted(policy.check_range("example/repo", policy.ZERO_SHA, added, set())), expected)
self.assertEqual(sorted(policy.check_range("example/repo", base, added, set(),
verify_commits=True, fetch_commit=lambda *_: record())), expected)
self.assertEqual(sorted(policy.check_range("example/repo", base, added, set(),
verify_commits=True, fetch_commit=lambda *_: record(False))),
sorted([*expected, f"{added}: commit is not Verified by GitHub"]))
for name in private_paths:
Path(name).write_text("modified\n")
modified = commit()
self.assertEqual(sorted(policy.check_range("example/repo", added, modified, set())), expected)
self.assertEqual(sorted(policy.check_range("example/repo", added, modified, set(),
verify_commits=True, fetch_commit=lambda *_: record())), expected)
Path("sample.txt").write_text("updated documentation\n")
unchanged = commit()
self.assertEqual(policy.check_range("example/repo", modified, unchanged, set()), [])
for name in private_paths:
Path(name).unlink()
deleted = commit()
self.assertEqual(policy.check_range("example/repo", unchanged, deleted, set()), [])
self.assertEqual(policy.check_range("example/repo", unchanged, deleted, set(),
verify_commits=True, fetch_commit=lambda *_: record()), [])
finally:
os.chdir(previous)

def test_real_git_renames_and_standalone_agent_file(self):
output = ROOT / ".build/policy-tests"
output.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(dir=output) as directory:
previous = Path.cwd()
os.chdir(directory)
try:
subprocess.run(["git", "init", "-q"], check=True)
def commit():
policy.git("add", "--all", "--force")
policy.git("-c", "user.name=Fixture", "-c", "user.email=fixture@example.test",
"-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null", "commit", "-qm", "fix: fixture")
return policy.git("rev-parse", "HEAD").strip()
Path("sample.txt").write_text("clean\n")
base = commit()
Path(".agent").mkdir()
Path("sample.txt").rename(".agent/sample.txt")
moved_in = commit()
self.assertEqual(policy.check_range("example/repo", base, moved_in, set()),
[".agent/sample.txt: private execution file"])
Path(".agent/sample.txt").rename("sample.txt")
moved_out = commit()
self.assertEqual(policy.check_range("example/repo", moved_in, moved_out, set()), [])
Path(".agent").rmdir()
Path("sample.txt").rename(".agent")
standalone = commit()
self.assertEqual(policy.check_range("example/repo", moved_out, standalone, set()),
[".agent: private execution file"])
finally:
os.chdir(previous)


if __name__ == "__main__":
unittest.main()