fix: document + tool the macOS com.apple.provenance workaround (#3)
Root cause: macOS Sequoia+ blocks Python.app from executing files carrying the com.apple.provenance extended attribute. Files written by an agent/IDE terminal get it (shell scripts and pre-session files do not). This is a macOS security feature, not a bug in our code — so the fix is an operator workaround, not a code change to the tools. - scripts/clear-provenance: recursively removes ONLY com.apple.provenance under a path (default: repo root); tolerates files without it; leaves other xattrs intact; supports --dry-run. Advises running from a Full-Disk-Access terminal. - README Troubleshooting section documenting the symptom, the helper, manual xattr equivalents, and the Full Disk Access alternative. Narrow + macOS-specific; no auth/release/worktree/tracker/MCP behavior changed. Tests: tests/test_clear_provenance.py (6 cases) — dry-run default/explicit path, missing-path error, bad-flag/too-many-args exit 2, and that only com.apple.provenance is targeted (not a blanket xattr clear). Dry-run only; no real xattr mutation. bash -n clean; py_compile mcp_server.py clean; full suite 319 passed / 0 failures; git diff --check clean; no secrets. Closes #3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""Tests for scripts/clear-provenance (#3).
|
||||
|
||||
Exercises argument handling and the inert --dry-run path only — no real xattr
|
||||
mutation, no network. (Actually removing com.apple.provenance is macOS-only and
|
||||
has real side effects, so it is not exercised here.)
|
||||
"""
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
SCRIPT = REPO / "scripts" / "clear-provenance"
|
||||
|
||||
|
||||
def run(*args):
|
||||
proc = subprocess.run(["bash", str(SCRIPT), *args],
|
||||
capture_output=True, text=True, cwd=str(REPO))
|
||||
return proc.returncode, proc.stdout, proc.stderr
|
||||
|
||||
|
||||
class TestClearProvenance(unittest.TestCase):
|
||||
|
||||
def test_dry_run_defaults_to_repo_root(self):
|
||||
rc, out, _ = run("--dry-run")
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("would run: xattr -r -d com.apple.provenance", out)
|
||||
self.assertIn(str(REPO), out)
|
||||
|
||||
def test_dry_run_explicit_path(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
f = Path(d) / "x.py"
|
||||
f.write_text("print('hi')\n")
|
||||
rc, out, _ = run("--dry-run", str(f))
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn(str(f), out)
|
||||
|
||||
def test_missing_path_errors(self):
|
||||
rc, _, err = run("--dry-run", "/no/such/path-xyz")
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn("no such path", err)
|
||||
|
||||
def test_bad_flag_exit_2(self):
|
||||
rc, _, _ = run("--bogus")
|
||||
self.assertEqual(rc, 2)
|
||||
|
||||
def test_too_many_args_exit_2(self):
|
||||
rc, _, _ = run("a", "b")
|
||||
self.assertEqual(rc, 2)
|
||||
|
||||
def test_only_targets_provenance_attribute(self):
|
||||
# The command removes only com.apple.provenance, not all xattrs.
|
||||
rc, out, _ = run("--dry-run")
|
||||
self.assertIn("com.apple.provenance", out)
|
||||
self.assertNotIn("xattr -rc", out) # not a blanket "clear all"
|
||||
self.assertNotIn("-c ", out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user