raidlock 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. raidlock/__init__.py +11 -0
  2. raidlock/__main__.py +5 -0
  3. raidlock/_python.py +58 -0
  4. raidlock/achievements.py +156 -0
  5. raidlock/approvals.py +237 -0
  6. raidlock/bench.py +286 -0
  7. raidlock/bridge/__init__.py +186 -0
  8. raidlock/budgets.py +178 -0
  9. raidlock/cli.py +2153 -0
  10. raidlock/commands_install.py +114 -0
  11. raidlock/core/__init__.py +26 -0
  12. raidlock/core/advisories.py +16 -0
  13. raidlock/core/billing.py +54 -0
  14. raidlock/core/board.py +2251 -0
  15. raidlock/core/brief.py +111 -0
  16. raidlock/core/clock.py +40 -0
  17. raidlock/core/cost.py +198 -0
  18. raidlock/core/db.py +608 -0
  19. raidlock/core/errors.py +52 -0
  20. raidlock/core/exe.py +6 -0
  21. raidlock/core/hints.py +32 -0
  22. raidlock/core/identity.py +285 -0
  23. raidlock/core/lead.py +758 -0
  24. raidlock/core/lead_env.py +23 -0
  25. raidlock/core/lead_hook.py +42 -0
  26. raidlock/core/leases.py +19 -0
  27. raidlock/core/lock.py +99 -0
  28. raidlock/core/messages.py +98 -0
  29. raidlock/core/migrate.py +579 -0
  30. raidlock/core/model.py +126 -0
  31. raidlock/core/models.py +109 -0
  32. raidlock/core/paths.py +61 -0
  33. raidlock/core/portable.py +53 -0
  34. raidlock/core/receipts.py +73 -0
  35. raidlock/core/registry.py +39 -0
  36. raidlock/core/repos.py +250 -0
  37. raidlock/core/safeio.py +201 -0
  38. raidlock/core/status.py +52 -0
  39. raidlock/core/store.py +107 -0
  40. raidlock/core/tokens.py +77 -0
  41. raidlock/core/transcript.py +208 -0
  42. raidlock/duel.py +371 -0
  43. raidlock/foresight.py +191 -0
  44. raidlock/frames.py +162 -0
  45. raidlock/gitguard.py +753 -0
  46. raidlock/guard.py +239 -0
  47. raidlock/hooks.py +1526 -0
  48. raidlock/infer.py +205 -0
  49. raidlock/limits.py +685 -0
  50. raidlock/lore/__init__.py +22 -0
  51. raidlock/lore/approval.py +145 -0
  52. raidlock/lore/capture.py +146 -0
  53. raidlock/lore/cli.py +109 -0
  54. raidlock/lore/distill.py +17 -0
  55. raidlock/lore/frontmatter.py +56 -0
  56. raidlock/lore/inject.py +127 -0
  57. raidlock/lore/vault.py +264 -0
  58. raidlock/mcp/__init__.py +34 -0
  59. raidlock/mcp/resources.py +72 -0
  60. raidlock/mcp/server.py +184 -0
  61. raidlock/mcp/tools.py +178 -0
  62. raidlock/notify.py +144 -0
  63. raidlock/party.py +239 -0
  64. raidlock/pr.py +575 -0
  65. raidlock/prsync.py +318 -0
  66. raidlock/recipes/README.md +34 -0
  67. raidlock/recipes/__init__.py +105 -0
  68. raidlock/recipes/docs-refresh.md +15 -0
  69. raidlock/recipes/multi-repo-refactor.md +15 -0
  70. raidlock/recipes/security-audit.md +27 -0
  71. raidlock/recipes/upgrade-deps.md +15 -0
  72. raidlock/reputation.py +194 -0
  73. raidlock/resume.py +329 -0
  74. raidlock/resurrect.py +419 -0
  75. raidlock/routing.py +419 -0
  76. raidlock/runners/__init__.py +16 -0
  77. raidlock/runners/base.py +341 -0
  78. raidlock/runners/brief.py +176 -0
  79. raidlock/runners/claude_code.py +121 -0
  80. raidlock/runners/cli.py +110 -0
  81. raidlock/runners/codex.py +114 -0
  82. raidlock/runners/gemini.py +64 -0
  83. raidlock/runners/isolation.py +391 -0
  84. raidlock/runners/policy.py +123 -0
  85. raidlock/runners/runner.py +921 -0
  86. raidlock/runners/shell.py +35 -0
  87. raidlock/runners/verifycache.py +93 -0
  88. raidlock/runners/workspace.py +376 -0
  89. raidlock/scan.py +357 -0
  90. raidlock/snapshot.py +151 -0
  91. raidlock/treasury.py +210 -0
  92. raidlock/tui.py +223 -0
  93. raidlock/web/__init__.py +5 -0
  94. raidlock/web/achievements/view.css +19 -0
  95. raidlock/web/achievements/view.js +58 -0
  96. raidlock/web/bosses/view.css +19 -0
  97. raidlock/web/bosses/view.js +46 -0
  98. raidlock/web/cli.py +90 -0
  99. raidlock/web/data.py +687 -0
  100. raidlock/web/export.py +153 -0
  101. raidlock/web/frames/view.css +25 -0
  102. raidlock/web/frames/view.js +63 -0
  103. raidlock/web/graph/__init__.py +142 -0
  104. raidlock/web/graph/view.css +33 -0
  105. raidlock/web/graph/view.js +136 -0
  106. raidlock/web/raidmap/__init__.py +205 -0
  107. raidlock/web/raidmap/view.css +15 -0
  108. raidlock/web/raidmap/view.js +47 -0
  109. raidlock/web/replay/__init__.py +258 -0
  110. raidlock/web/replay/view.css +13 -0
  111. raidlock/web/replay/view.js +127 -0
  112. raidlock/web/server.py +292 -0
  113. raidlock/web/static/app.css +851 -0
  114. raidlock/web/static/app.js +2102 -0
  115. raidlock/web/static/fonts/OFL.txt +93 -0
  116. raidlock/web/static/fonts/cinzel-600.woff2 +0 -0
  117. raidlock/web/static/fonts/cinzel-700.woff2 +0 -0
  118. raidlock/web/static/icon-32.png +0 -0
  119. raidlock/web/static/icon-raid.svg +1 -0
  120. raidlock/web/static/icon.svg +1 -0
  121. raidlock/web/static/index.html +122 -0
  122. raidlock-0.1.0.dist-info/METADATA +186 -0
  123. raidlock-0.1.0.dist-info/RECORD +126 -0
  124. raidlock-0.1.0.dist-info/WHEEL +4 -0
  125. raidlock-0.1.0.dist-info/entry_points.txt +34 -0
  126. raidlock-0.1.0.dist-info/licenses/LICENSE +21 -0
raidlock/guard.py ADDED
@@ -0,0 +1,239 @@
1
+ """Git lease guards; run with python -m raidlock.guard.
2
+
3
+ register_cli is available to CLI extension loaders. The built-in loader does
4
+ not discover this module without a raidlock.commands entry point.
5
+ """
6
+
7
+ import argparse
8
+ import os
9
+ import shlex
10
+ import subprocess
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ from .core import Board, canonical, exe
15
+ from .core.errors import Denied, IdentityError, StaleLead
16
+ from .runners.workspace import git_run
17
+
18
+ HOOKS = ("pre-commit", "pre-push")
19
+ MARKER = ": 'raidlock guard hook v1'"
20
+
21
+
22
+ def git(*args):
23
+ proc = git_run(os.getcwd(), *args, binary=True)
24
+ proc.check_returncode()
25
+ return proc.stdout
26
+
27
+
28
+ def root():
29
+ return Path(os.fsdecode(git("rev-parse", "--show-toplevel")).strip())
30
+
31
+
32
+ def hook_dir():
33
+ from .core.repos import common_dir, git_dir
34
+
35
+ repo = root()
36
+ common = common_dir(git_dir(str(repo)))
37
+ configured = git_run(
38
+ repo, "config", "--file", os.path.join(common, "config"), "--no-includes", "--get", "core.hooksPath"
39
+ ).stdout.strip()
40
+ return repo / configured if configured else Path(common, "hooks")
41
+
42
+
43
+ def backup(path):
44
+ return path.with_name(path.name + ".raidlock-original")
45
+
46
+
47
+ def managed(path):
48
+ return path.is_file() and MARKER in path.read_text(errors="replace").splitlines()[:3]
49
+
50
+
51
+ def hook_text(name):
52
+ source = str(Path(__file__).resolve().parent.parent)
53
+ code = f"import sys; sys.path.insert(0, {source!r}); from raidlock.guard import main; sys.exit(main())"
54
+ return f'#!/bin/sh\n{MARKER}\nexec {shlex.quote(sys.executable)} -c {shlex.quote(code)} _hook {name} "$0" "$@"\n'
55
+
56
+
57
+ def install():
58
+ folder = hook_dir()
59
+ folder.mkdir(parents=True, exist_ok=True)
60
+ for name in HOOKS:
61
+ path = folder / name
62
+ if not managed(path) and os.path.lexists(backup(path)):
63
+ raise ValueError(f"backup already exists: {backup(path)}")
64
+ for name in HOOKS:
65
+ path = folder / name
66
+ if managed(path):
67
+ continue
68
+ if os.path.lexists(path):
69
+ path.rename(backup(path))
70
+ try:
71
+ path.write_text(hook_text(name))
72
+ path.chmod(0o755)
73
+ except OSError:
74
+ path.unlink(missing_ok=True)
75
+ if os.path.lexists(backup(path)):
76
+ backup(path).rename(path)
77
+ raise
78
+
79
+
80
+ def uninstall():
81
+ for name in HOOKS:
82
+ path = hook_dir() / name
83
+ if managed(path):
84
+ path.unlink()
85
+ if os.path.lexists(backup(path)):
86
+ backup(path).rename(path)
87
+
88
+
89
+ def staged_paths():
90
+ return split_paths(git("diff", "--cached", "--name-only", "--no-renames", "-z"))
91
+
92
+
93
+ def split_paths(data):
94
+ return {os.fsdecode(p) for p in data.split(b"\0") if p}
95
+
96
+
97
+ def pushed_paths(data, caller=None):
98
+ paths = set()
99
+ for line in data.splitlines():
100
+ fields = line.split()
101
+ if len(fields) != 4:
102
+ raise ValueError("invalid pre-push input")
103
+ _, local, _, remote = fields
104
+ if (
105
+ len(local) not in (40, 64)
106
+ or len(remote) != len(local)
107
+ or any(c not in "0123456789abcdef" for c in local + remote)
108
+ ):
109
+ raise ValueError("invalid pre-push object id")
110
+ if set(local) == {"0"}:
111
+ if caller is None or not caller.is_lead:
112
+ raise IdentityError("ref deletion requires explicit lead authorization")
113
+ continue
114
+ if set(remote) != {"0"} and (caller is None or not caller.is_lead):
115
+ try:
116
+ ancestor = git("merge-base", local, remote).decode().strip()
117
+ except subprocess.CalledProcessError:
118
+ raise IdentityError("cannot establish fast-forward; explicit lead authorization required") from None
119
+ if ancestor != remote:
120
+ raise IdentityError("force push requires explicit lead authorization")
121
+ revisions = [local] if set(remote) == {"0"} else [local, "^" + remote]
122
+ for commit in git("rev-list", *revisions, "--").decode().splitlines():
123
+ paths.update(
124
+ split_paths(
125
+ git(
126
+ "diff-tree",
127
+ "--root",
128
+ "-m",
129
+ "--no-commit-id",
130
+ "--name-only",
131
+ "--no-renames",
132
+ "-r",
133
+ "-z",
134
+ commit,
135
+ "--",
136
+ )
137
+ )
138
+ )
139
+ return paths
140
+
141
+
142
+ def refusal(board, caller, full):
143
+ if not caller.authenticated:
144
+ return {"allowed": False, "reason": "no authenticated agent; present your own token"}
145
+ decision = board.check_write(str(full), caller)
146
+ card_id = decision.get("card")
147
+ if (
148
+ decision["allowed"]
149
+ and card_id
150
+ and not caller.is_lead
151
+ and (not caller.fence or caller.fence != board.card(card_id)["fence"])
152
+ ):
153
+ decision = dict(decision, allowed=False, reason="stale fence")
154
+ return None if decision["allowed"] else decision
155
+
156
+
157
+ def check(hook="pre-commit", data="", board_path=None):
158
+ try:
159
+ board = Board.open(board_path)
160
+ caller = board.caller()
161
+ if not caller.authenticated:
162
+ raise IdentityError("no authenticated agent; present your own token")
163
+ board._auth(caller, scoped_ok=True)
164
+ repo = root()
165
+ paths = staged_paths() if hook == "pre-commit" else pushed_paths(data, caller)
166
+ denied = []
167
+ for path in sorted(paths):
168
+ canonical(repo.name + "/" + path)
169
+ decision = refusal(board, caller, repo / path)
170
+ if decision:
171
+ card = decision.get("card")
172
+ fix = f"raidlock claim {shlex.quote(card)}" if card else "raidlock claim <card>"
173
+ denied.append(
174
+ f"raidlock guard: {path!r}: holder={decision.get('holder') or 'none'}; "
175
+ f"{decision['reason']}; fix: {fix}"
176
+ )
177
+ except (Denied, IdentityError, StaleLead) as exc:
178
+ print(f"raidlock guard: refused: {exc.message}", file=sys.stderr)
179
+ return 2
180
+ except Exception:
181
+ warn = hook != "pre-push" and os.environ.get("RAIDLOCK_GUARD") == "warn"
182
+ print(
183
+ f"raidlock guard: {'warning' if warn else 'refused'}: board or git check unavailable; "
184
+ "fix: raidlock show --board <workspace>",
185
+ file=sys.stderr,
186
+ )
187
+ return 0 if warn else 2
188
+ for line in denied:
189
+ print(line, file=sys.stderr)
190
+ return 2 if denied else 0
191
+
192
+
193
+ def run_hook(args):
194
+ name, path, *forward = args
195
+ data = sys.stdin.buffer.read() if name == "pre-push" else None
196
+ code = check(name, os.fsdecode(data or b""))
197
+ original = backup(Path(path).absolute())
198
+ if code or not original.exists() or not os.access(original, os.X_OK):
199
+ return code
200
+ shell = [] if os.name == "posix" else [exe.find("sh")]
201
+ return subprocess.run([*shell, str(original), *forward], input=data).returncode
202
+
203
+
204
+ def setup(parser):
205
+ parser.add_argument("action", choices=("install", "uninstall", "check"))
206
+ parser.add_argument("--hook", choices=HOOKS, default="pre-commit")
207
+
208
+
209
+ def dispatch(args):
210
+ if args.action == "check":
211
+ data = sys.stdin.read() if args.hook == "pre-push" else ""
212
+ return check(args.hook, data, getattr(args, "board", None))
213
+ {"install": install, "uninstall": uninstall}[args.action]()
214
+ return 0
215
+
216
+
217
+ def register_cli(add_command):
218
+ def handler(ctx, args):
219
+ return {"_exit": dispatch(args)}, ""
220
+
221
+ add_command("guard", "install, uninstall or check Git lease guards", setup, handler)
222
+
223
+
224
+ def main(argv=None):
225
+ argv = list(sys.argv[1:] if argv is None else argv)
226
+ try:
227
+ if argv and argv[0] == "_hook":
228
+ return run_hook(argv[1:])
229
+ parser = argparse.ArgumentParser(prog="python -m raidlock.guard")
230
+ setup(parser)
231
+ parser.add_argument("--board")
232
+ return dispatch(parser.parse_args(argv))
233
+ except Exception:
234
+ print("raidlock guard: operation failed; fix: python -m raidlock.guard --help", file=sys.stderr)
235
+ return 2
236
+
237
+
238
+ if __name__ == "__main__":
239
+ sys.exit(main())