v1_policy.py python
113 lines 5.3 KB
Raw
sha256:106ef9e12cca7f3be3ffb2a6bd3abfa76f032dce453c9e5010d732a8d86f2e14 Reconcile reviewed credential compatibility 32254cd939885d9… Human patch 8 days ago
1 """Additive checkout-local exclusions; never untrack or rewrite user rules."""
2 from __future__ import annotations
3
4 import json
5 import re
6 import tomllib
7
8 from cli.v1_io import Refusal, digest, read
9
10 LOCAL_PATHS = (".overseer/config.yaml", ".overseer/bin/", ".overseer/local/",
11 "docs/NEXT.md", ".cursor/")
12
13
14 def local_path(path):
15 return any(path == p or (p.endswith("/") and path.startswith(p)) for p in LOCAL_PATHS)
16
17
18 def muse_ignore(raw):
19 text = (raw or b"").decode("utf-8")
20 try:
21 parsed = tomllib.loads(text)
22 forced = parsed.get("force_track", {}).get("paths", [])
23 if not isinstance(forced, list) or any(not isinstance(p, str) for p in forced):
24 raise ValueError()
25 if any(local_path(p.lstrip("/")) for p in forced):
26 raise Refusal("muse_force_tracks_local_binding")
27 sections = [("global", parsed.get("global", {}))]
28 domains = parsed.get("domain", {})
29 if not isinstance(domains, dict):
30 raise ValueError()
31 sections.extend(("domain." + name, value) for name, value in domains.items())
32 for name, value in sections:
33 if not isinstance(value, dict) or not isinstance(value.get("patterns", []), list):
34 raise ValueError()
35 patterns = value.get("patterns", [])
36 if any(not isinstance(p, str) for p in patterns):
37 raise ValueError()
38 additions = ["/" + p for p in LOCAL_PATHS] + ["/.git/"]
39 # A final suffix makes last-rule-wins negations harmless. Preserve
40 # all prior text, comments and rule order, even duplicate old rules.
41 if patterns[-len(additions):] == additions:
42 continue
43 table = re.search(r"(?m)^\[" + re.escape(name) + r"\][ \t]*(?:#[^\n]*)?$", text)
44 suffix = "\n# Overseer checkout-local bindings (never distribute).\npatterns = [\n" + "".join(
45 " " + json.dumps(p) + ",\n" for p in additions) + "]\n"
46 if not table:
47 if value:
48 raise Refusal("museignore_layout_unsupported")
49 text += ("\n" if text and not text.endswith("\n") else "") + "\n[" + name + "]" + suffix
50 continue
51 end_table = re.search(r"(?m)^\[", text[table.end():])
52 end = table.end() + end_table.start() if end_table else len(text)
53 body = text[table.end():end]
54 array = re.search(r"(?m)^patterns\s*=\s*\[", body)
55 if not array:
56 if "patterns" in value:
57 raise Refusal("museignore_layout_unsupported")
58 text = text[:end] + suffix + text[end:]
59 continue
60 # Locate the TOML array end, ignoring comments and quoted strings.
61 start = table.end() + array.end()
62 quote = None
63 comment = False
64 escaped = False
65 for pos in range(start, end):
66 char = text[pos]
67 if comment:
68 comment = char != "\n"
69 elif quote:
70 if escaped:
71 escaped = False
72 elif char == "\\" and quote == '"':
73 escaped = True
74 elif char == quote:
75 quote = None
76 elif char == "#":
77 comment = True
78 elif char in "\"'":
79 quote = char
80 elif char == "]":
81 # Newline puts any missing comma beyond a trailing comment.
82 insert = ("\n," if patterns else "") + "\n # Overseer checkout-local bindings.\n"
83 # TOML permits a trailing comma, but not two commas.
84 cleaned = re.sub(r"#[^\n]*", "", text[start:pos]).rstrip()
85 if cleaned.endswith(","):
86 insert = insert.removeprefix("\n,")
87 insert += "".join(" " + json.dumps(p) + ",\n" for p in additions)
88 text = text[:pos] + insert + text[pos:]
89 break
90 else:
91 raise Refusal("museignore_layout_unsupported")
92 updated = tomllib.loads(text)
93 # The parser checks our insertion; no existing value may be discarded.
94 for name, old in sections:
95 new = updated["global"] if name == "global" else updated["domain"][name[7:]]
96 if new["patterns"][:len(old.get("patterns", []))] != old.get("patterns", []):
97 raise ValueError()
98 except (tomllib.TOMLDecodeError, ValueError, TypeError, AttributeError):
99 raise Refusal("museignore_invalid_or_unsupported") from None
100 return text.encode()
101
102
103 def ignore_assets(root, *, expected=None):
104 git_raw = read(root, ".gitignore", missing=True)
105 muse_raw = read(root, ".museignore", missing=True)
106 if expected is not None:
107 expected.update({p: "absent" if data is None else digest(data)
108 for p, data in ((".gitignore", git_raw), (".museignore", muse_raw))})
109 old = git_raw or b""
110 suffix = b"\n# Overseer checkout-local bindings (never distribute).\n" + "".join(
111 "/" + p + "\n" for p in (*LOCAL_PATHS, ".muse/")).encode()
112 gitignore = old if old.endswith(suffix) else old + (b"\n" if old and not old.endswith(b"\n") else b"") + suffix
113 return {".gitignore": gitignore, ".museignore": muse_ignore(muse_raw)}
File History 2 commits
sha256:106ef9e12cca7f3be3ffb2a6bd3abfa76f032dce453c9e5010d732a8d86f2e14 Reconcile reviewed credential compatibility 32254cd939885d9… Human patch 8 days ago
sha256:c87cf2c2f03da507f66f93922cb339a2cbebb33ad6372a6039032fb09a20da12 Reconcile reviewed Overseer recovery d47291d5d9030de5ebef71… Human minor ⚠ 8 days ago