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/web/server.py ADDED
@@ -0,0 +1,292 @@
1
+ import hashlib
2
+ import json
3
+ import os
4
+ import re
5
+ import sys
6
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
7
+ from urllib.parse import parse_qs, unquote, urlsplit
8
+
9
+ from raidlock.core import RaidlockError, db
10
+
11
+ from .. import __version__
12
+ from . import data, export
13
+
14
+ STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
15
+ STATIC = {
16
+ "/static/app.js": ("app.js", "text/javascript; charset=utf-8"),
17
+ "/static/app.css": ("app.css", "text/css; charset=utf-8"),
18
+ "/static/icon.svg": ("icon.svg", "image/svg+xml"),
19
+ "/static/icon-raid.svg": ("icon-raid.svg", "image/svg+xml"),
20
+ "/static/fonts/cinzel-600.woff2": ("fonts/cinzel-600.woff2", "font/woff2"),
21
+ "/static/fonts/cinzel-700.woff2": ("fonts/cinzel-700.woff2", "font/woff2"),
22
+ "/static/icon-32.png": ("icon-32.png", "image/png"),
23
+ }
24
+ WEB_DIR = os.path.dirname(os.path.abspath(__file__))
25
+ VIEW_FILES = ("raidmap", "replay", "graph", "frames", "achievements", "bosses")
26
+ for _view in VIEW_FILES:
27
+ STATIC[f"/static/views/{_view}.js"] = (os.path.join(WEB_DIR, _view, "view.js"), "text/javascript; charset=utf-8")
28
+ STATIC[f"/static/views/{_view}.css"] = (os.path.join(WEB_DIR, _view, "view.css"), "text/css; charset=utf-8")
29
+ INDEX = ("index.html", "text/html; charset=utf-8")
30
+ CSP = (
31
+ "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; connect-src 'self'; "
32
+ "font-src 'self'; manifest-src 'self'; media-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'; "
33
+ "frame-ancestors 'none'; require-trusted-types-for 'script'; trusted-types 'none'"
34
+ )
35
+ PERMISSIONS = "camera=(), display-capture=(), geolocation=(), microphone=(), payment=(), usb=()"
36
+ _STATIC_CACHE = {}
37
+ SEGMENT = re.compile(r"^[^/\\\x00-\x1f]{1,200}$")
38
+ TRANSCRIPT_PAGE = 500
39
+ FILE_EXTS = (
40
+ ".py",
41
+ ".pyc",
42
+ ".js",
43
+ ".mjs",
44
+ ".css",
45
+ ".json",
46
+ ".html",
47
+ ".htm",
48
+ ".map",
49
+ ".txt",
50
+ ".md",
51
+ ".svg",
52
+ ".png",
53
+ ".ico",
54
+ ".toml",
55
+ ".cfg",
56
+ ".ini",
57
+ ".env",
58
+ ".lock",
59
+ ".db",
60
+ ".jsonl",
61
+ ".yaml",
62
+ ".yml",
63
+ ".sh",
64
+ )
65
+
66
+
67
+ def allowed_hosts(port):
68
+ return {f"127.0.0.1:{port}", f"localhost:{port}", f"[::1]:{port}"}
69
+
70
+
71
+ class Handler(BaseHTTPRequestHandler):
72
+ server_version = "raidlock"
73
+ sys_version = ""
74
+ protocol_version = "HTTP/1.1"
75
+
76
+ def log_message(self, *args):
77
+ pass
78
+
79
+ def handle_one_request(self):
80
+ with db.released():
81
+ super().handle_one_request()
82
+
83
+ def _headers(self, code, ctype, length, extra=None):
84
+ self.send_response(code)
85
+ self.send_header("Content-Type", ctype)
86
+ self.send_header("Content-Length", str(length))
87
+ self.send_header("Content-Security-Policy", CSP)
88
+ self.send_header("X-Content-Type-Options", "nosniff")
89
+ self.send_header("X-Frame-Options", "DENY")
90
+ self.send_header("Referrer-Policy", "no-referrer")
91
+ self.send_header("Cross-Origin-Opener-Policy", "same-origin")
92
+ self.send_header("Cross-Origin-Resource-Policy", "same-origin")
93
+ self.send_header("Permissions-Policy", PERMISSIONS)
94
+ for k, v in (extra or {}).items():
95
+ self.send_header(k, v)
96
+ self.end_headers()
97
+
98
+ def _send(self, code, body, ctype, extra=None):
99
+ self._headers(code, ctype, len(body), extra)
100
+ self.wfile.write(body)
101
+
102
+ def _json(self, obj, code=200, etag=None):
103
+ extra = {"Cache-Control": "no-cache"}
104
+ if etag:
105
+ extra["ETag"] = etag
106
+ obj = data.redacted(data.scrub_private(obj, self.server.catalog.home))
107
+ self._send(
108
+ code,
109
+ json.dumps(obj, ensure_ascii=False, separators=(",", ":")).encode("utf-8"),
110
+ "application/json; charset=utf-8",
111
+ extra,
112
+ )
113
+
114
+ def _error(self, code, message):
115
+ self._json({"error": message}, code)
116
+
117
+ def _not_modified(self, etag):
118
+ if etag and self.headers.get("If-None-Match") == etag:
119
+ self._headers(304, "text/plain; charset=utf-8", 0, {"ETag": etag, "Cache-Control": "no-cache"})
120
+ return True
121
+ return False
122
+
123
+ def _host_ok(self):
124
+ hosts = self.headers.get_all("Host", [])
125
+ return len(hosts) == 1 and hosts[0] in allowed_hosts(self.server.server_address[1])
126
+
127
+ def _deny_method(self):
128
+ if not self._host_ok():
129
+ self._error(403, "forbidden host")
130
+ return
131
+ self._send(405, b'{"error":"method not allowed"}', "application/json", {"Allow": "GET"})
132
+
133
+ do_POST = do_PUT = do_DELETE = do_PATCH = do_OPTIONS = do_HEAD = do_TRACE = do_CONNECT = _deny_method
134
+
135
+ def do_GET(self):
136
+ if not self._host_ok():
137
+ self._error(403, "forbidden host")
138
+ return
139
+ url = urlsplit(self.path)
140
+ path = url.path
141
+ query = parse_qs(url.query)
142
+ try:
143
+ if path.startswith("/api/"):
144
+ self._api(path[len("/api/") :], query)
145
+ else:
146
+ self._static(path)
147
+ except RaidlockError as e:
148
+ if str(e) == "graph too large":
149
+ self._error(413, "graph too large")
150
+ return
151
+ self._error(404 if e.code == 3 else 500, e.message.split(" in /")[0])
152
+ except Exception:
153
+ self._error(500, "board could not be read")
154
+
155
+ def _static(self, path):
156
+ if path in STATIC:
157
+ name, ctype = STATIC[path]
158
+ elif path == "/" or path == "/index.html" or self._route(path):
159
+ name, ctype = INDEX
160
+ else:
161
+ self._error(404, "not found")
162
+ return
163
+ body, etag = _static_file(name)
164
+ if self._not_modified(etag):
165
+ return
166
+ self._send(200, body, ctype, {"Cache-Control": "no-cache", "ETag": etag})
167
+
168
+ def _route(self, path):
169
+ parts = path.strip("/").split("/")
170
+ if len(parts) != 1:
171
+ return False
172
+ seg = unquote(parts[0])
173
+ return (
174
+ bool(SEGMENT.match(seg))
175
+ and not seg.startswith(".")
176
+ and seg not in ("api", "static")
177
+ and not seg.lower().endswith(FILE_EXTS)
178
+ )
179
+
180
+ def _api(self, rest, query):
181
+ catalog = self.server.catalog
182
+ parts = [unquote(p) for p in rest.split("/") if p]
183
+ if parts == ["overview"]:
184
+ key = catalog.overview_key()
185
+ if self._not_modified(key):
186
+ return
187
+ payload = catalog.overview()
188
+ self._json(payload, etag=key)
189
+ return
190
+ if len(parts) == 2 and parts[0] in data.VIEWS:
191
+ self._view(parts[0], parts[1], query)
192
+ return
193
+ if not parts or parts[0] not in ("board", "transcript", "card", "snapshot") or len(parts) < 2:
194
+ self._error(404, "unknown endpoint")
195
+ return
196
+ ws = catalog.get(parts[1])
197
+ if ws is None or not os.path.isdir(ws.dir):
198
+ self._error(404, f"no workspace {parts[1][:80]}")
199
+ return
200
+ kind = parts[0]
201
+ if kind == "board" and len(parts) == 2:
202
+ key = ws.key_now()
203
+ if self._not_modified(key):
204
+ return
205
+ key, payload = ws.refresh()
206
+ self._json(data.public(payload), etag=key)
207
+ elif kind == "transcript" and len(parts) == 2:
208
+ self._transcript(ws, query)
209
+ elif kind == "card" and len(parts) == 3:
210
+ tag = data.etag_of(ws.key_now(), parts[2])
211
+ if self._not_modified(tag):
212
+ return
213
+ self._json(data.card_detail(ws, parts[2]), etag=tag)
214
+ elif kind == "snapshot" and len(parts) == 2:
215
+ _, payload = ws.refresh()
216
+ log = data.transcript_page(ws, 0, 200, "")["records"]
217
+ public = data.public(payload)
218
+ public["limits"] = catalog.provider_limits()
219
+ public = data.scrub_private(public, catalog.home)
220
+ log = data.scrub_private(log, catalog.home)
221
+ body = export.render(data.redacted(public), data.redacted(log)).encode("utf-8")
222
+ name = re.sub(r"[^A-Za-z0-9_.-]", "-", ws.name) or "board"
223
+ self._send(
224
+ 200,
225
+ body,
226
+ "text/html; charset=utf-8",
227
+ {"Cache-Control": "no-store", "Content-Disposition": f'attachment; filename="raidlock-{name}.html"'},
228
+ )
229
+ else:
230
+ self._error(404, "unknown endpoint")
231
+
232
+ def _view(self, kind, name, query):
233
+ ws = self.server.catalog.get(name)
234
+ if ws is None or not os.path.isdir(ws.dir):
235
+ self._error(404, f"no workspace {name[:80]}")
236
+ return
237
+ args = {k: v[:1] for k, v in sorted(query.items()) if k in data.VIEW_QUERY}
238
+ tag = data.etag_of(ws.key_now(), kind, args, data.view_clock(kind))
239
+ if self._not_modified(tag):
240
+ return
241
+ self._json(data.view(ws, kind, args), etag=tag)
242
+
243
+ def _transcript(self, ws, query):
244
+ try:
245
+ since = max(0, int((query.get("since") or ["0"])[0]))
246
+ limit = min(TRANSCRIPT_PAGE, max(1, int((query.get("limit") or [str(TRANSCRIPT_PAGE)])[0])))
247
+ except ValueError:
248
+ self._error(400, "since and limit must be integers")
249
+ return
250
+ task = (query.get("task") or [""])[0]
251
+ tag = data.etag_of(ws.key_now(), since, limit, task)
252
+ if self._not_modified(tag):
253
+ return
254
+ self._json(data.transcript_page(ws, since, limit, task), etag=tag)
255
+
256
+
257
+ def _static_file(name):
258
+ path = os.path.join(STATIC_DIR, name)
259
+ stat = os.stat(path)
260
+ key = (name, stat.st_mtime_ns, stat.st_size)
261
+ hit = _STATIC_CACHE.get(name)
262
+ if hit and hit[0] == key:
263
+ return hit[1], hit[2]
264
+ with open(path, "rb") as f:
265
+ body = f.read()
266
+ if name == INDEX[0]:
267
+ body = body.replace(b"{{version}}", __version__.encode())
268
+ etag = '"' + hashlib.sha256(body).hexdigest()[:20] + '"'
269
+ _STATIC_CACHE[name] = (key, body, etag)
270
+ return body, etag
271
+
272
+
273
+ class Server(ThreadingHTTPServer):
274
+ daemon_threads = True
275
+ allow_reuse_address = True
276
+
277
+ def __init__(self, address, catalog):
278
+ super().__init__(address, Handler)
279
+ self.catalog = catalog
280
+
281
+ def process_request_thread(self, request, client_address):
282
+ with db.released():
283
+ super().process_request_thread(request, client_address)
284
+
285
+ def handle_error(self, request, client_address):
286
+ if isinstance(sys.exc_info()[1], (ConnectionError, TimeoutError)):
287
+ return
288
+ super().handle_error(request, client_address)
289
+
290
+
291
+ def make_server(port=8765, single=None, home=None, extra=None, host="127.0.0.1"):
292
+ return Server((host, port), data.Catalog(single=single, home=home, extra=extra))