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/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))
|