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.
- raidlock/__init__.py +11 -0
- raidlock/__main__.py +5 -0
- raidlock/_python.py +58 -0
- raidlock/achievements.py +156 -0
- raidlock/approvals.py +237 -0
- raidlock/bench.py +286 -0
- raidlock/bridge/__init__.py +186 -0
- raidlock/budgets.py +178 -0
- raidlock/cli.py +2153 -0
- raidlock/commands_install.py +114 -0
- raidlock/core/__init__.py +26 -0
- raidlock/core/advisories.py +16 -0
- raidlock/core/billing.py +54 -0
- raidlock/core/board.py +2251 -0
- raidlock/core/brief.py +111 -0
- raidlock/core/clock.py +40 -0
- raidlock/core/cost.py +198 -0
- raidlock/core/db.py +608 -0
- raidlock/core/errors.py +52 -0
- raidlock/core/exe.py +6 -0
- raidlock/core/hints.py +32 -0
- raidlock/core/identity.py +285 -0
- raidlock/core/lead.py +758 -0
- raidlock/core/lead_env.py +23 -0
- raidlock/core/lead_hook.py +42 -0
- raidlock/core/leases.py +19 -0
- raidlock/core/lock.py +99 -0
- raidlock/core/messages.py +98 -0
- raidlock/core/migrate.py +579 -0
- raidlock/core/model.py +126 -0
- raidlock/core/models.py +109 -0
- raidlock/core/paths.py +61 -0
- raidlock/core/portable.py +53 -0
- raidlock/core/receipts.py +73 -0
- raidlock/core/registry.py +39 -0
- raidlock/core/repos.py +250 -0
- raidlock/core/safeio.py +201 -0
- raidlock/core/status.py +52 -0
- raidlock/core/store.py +107 -0
- raidlock/core/tokens.py +77 -0
- raidlock/core/transcript.py +208 -0
- raidlock/duel.py +371 -0
- raidlock/foresight.py +191 -0
- raidlock/frames.py +162 -0
- raidlock/gitguard.py +753 -0
- raidlock/guard.py +239 -0
- raidlock/hooks.py +1526 -0
- raidlock/infer.py +205 -0
- raidlock/limits.py +685 -0
- raidlock/lore/__init__.py +22 -0
- raidlock/lore/approval.py +145 -0
- raidlock/lore/capture.py +146 -0
- raidlock/lore/cli.py +109 -0
- raidlock/lore/distill.py +17 -0
- raidlock/lore/frontmatter.py +56 -0
- raidlock/lore/inject.py +127 -0
- raidlock/lore/vault.py +264 -0
- raidlock/mcp/__init__.py +34 -0
- raidlock/mcp/resources.py +72 -0
- raidlock/mcp/server.py +184 -0
- raidlock/mcp/tools.py +178 -0
- raidlock/notify.py +144 -0
- raidlock/party.py +239 -0
- raidlock/pr.py +575 -0
- raidlock/prsync.py +318 -0
- raidlock/recipes/README.md +34 -0
- raidlock/recipes/__init__.py +105 -0
- raidlock/recipes/docs-refresh.md +15 -0
- raidlock/recipes/multi-repo-refactor.md +15 -0
- raidlock/recipes/security-audit.md +27 -0
- raidlock/recipes/upgrade-deps.md +15 -0
- raidlock/reputation.py +194 -0
- raidlock/resume.py +329 -0
- raidlock/resurrect.py +419 -0
- raidlock/routing.py +419 -0
- raidlock/runners/__init__.py +16 -0
- raidlock/runners/base.py +341 -0
- raidlock/runners/brief.py +176 -0
- raidlock/runners/claude_code.py +121 -0
- raidlock/runners/cli.py +110 -0
- raidlock/runners/codex.py +114 -0
- raidlock/runners/gemini.py +64 -0
- raidlock/runners/isolation.py +391 -0
- raidlock/runners/policy.py +123 -0
- raidlock/runners/runner.py +921 -0
- raidlock/runners/shell.py +35 -0
- raidlock/runners/verifycache.py +93 -0
- raidlock/runners/workspace.py +376 -0
- raidlock/scan.py +357 -0
- raidlock/snapshot.py +151 -0
- raidlock/treasury.py +210 -0
- raidlock/tui.py +223 -0
- raidlock/web/__init__.py +5 -0
- raidlock/web/achievements/view.css +19 -0
- raidlock/web/achievements/view.js +58 -0
- raidlock/web/bosses/view.css +19 -0
- raidlock/web/bosses/view.js +46 -0
- raidlock/web/cli.py +90 -0
- raidlock/web/data.py +687 -0
- raidlock/web/export.py +153 -0
- raidlock/web/frames/view.css +25 -0
- raidlock/web/frames/view.js +63 -0
- raidlock/web/graph/__init__.py +142 -0
- raidlock/web/graph/view.css +33 -0
- raidlock/web/graph/view.js +136 -0
- raidlock/web/raidmap/__init__.py +205 -0
- raidlock/web/raidmap/view.css +15 -0
- raidlock/web/raidmap/view.js +47 -0
- raidlock/web/replay/__init__.py +258 -0
- raidlock/web/replay/view.css +13 -0
- raidlock/web/replay/view.js +127 -0
- raidlock/web/server.py +292 -0
- raidlock/web/static/app.css +851 -0
- raidlock/web/static/app.js +2102 -0
- raidlock/web/static/fonts/OFL.txt +93 -0
- raidlock/web/static/fonts/cinzel-600.woff2 +0 -0
- raidlock/web/static/fonts/cinzel-700.woff2 +0 -0
- raidlock/web/static/icon-32.png +0 -0
- raidlock/web/static/icon-raid.svg +1 -0
- raidlock/web/static/icon.svg +1 -0
- raidlock/web/static/index.html +122 -0
- raidlock-0.1.0.dist-info/METADATA +186 -0
- raidlock-0.1.0.dist-info/RECORD +126 -0
- raidlock-0.1.0.dist-info/WHEEL +4 -0
- raidlock-0.1.0.dist-info/entry_points.txt +34 -0
- 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())
|