@mcptoolshop/style-dataset-lab 3.4.0 → 3.5.0
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.
- package/CHANGELOG.md +27 -0
- package/README.es.md +111 -59
- package/README.fr.md +117 -67
- package/README.hi.md +109 -70
- package/README.it.md +111 -60
- package/README.ja.md +116 -76
- package/README.md +58 -2
- package/README.pt-BR.md +119 -64
- package/README.zh.md +108 -67
- package/bin/sdlab.js +50 -1
- package/lib/adapters/ai-toolkit.js +187 -30
- package/lib/adapters/comfyui-workflows.js +84 -9
- package/lib/asset-ingest.js +575 -0
- package/lib/asset-masks.js +323 -0
- package/lib/asset-source.js +1176 -0
- package/lib/canon-author.js +350 -0
- package/lib/canon-review-gate.js +205 -0
- package/lib/colab-launcher-guards.js +285 -0
- package/lib/export.js +1 -1
- package/lib/freeze-gate.js +3 -0
- package/lib/generated-provenance.js +178 -5
- package/lib/lane1-render.js +668 -0
- package/lib/memory-store.js +663 -0
- package/lib/model-licences.js +305 -0
- package/lib/npy-meta.js +130 -0
- package/lib/png-meta.js +512 -0
- package/lib/records.js +54 -8
- package/lib/reingest-selected.js +15 -1
- package/lib/reingest.js +27 -3
- package/lib/run-manifest.js +59 -11
- package/lib/train-runs.js +214 -0
- package/lib/verifier.js +3 -0
- package/lib/verify-chain.js +367 -0
- package/package.json +2 -2
- package/scripts/asset-ingest.js +160 -0
- package/scripts/canon-draft.js +82 -0
- package/scripts/canon-freeze.js +4 -0
- package/scripts/colab/REAL_SERVICE_ASSUMPTIONS.md +14 -0
- package/scripts/colab/ai-toolkit-override-py315.txt +45 -0
- package/scripts/colab/colab_window.sh +587 -0
- package/scripts/colab/lane1_render.py +970 -0
- package/scripts/colab/launch_guards.py +564 -0
- package/scripts/colab/launch_lane1_stub.sh +309 -0
- package/scripts/colab/launch_lane3_stub.sh +366 -0
- package/scripts/colab/remote_runner.py +59 -0
- package/scripts/doctor.js +1 -0
- package/scripts/generate.js +38 -12
- package/scripts/verify.js +67 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""C2 Colab launcher guards (DESIGN-v3 section 9.2 item 6 — lessons v4k, v4p, v4q).
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
1. evaluate_wall_guard (v4k: WALL_S and GRACE_S honoured in stub and live modes)
|
|
6
|
+
2. evaluate_cap_guard (v4k: CAP and MARGIN honoured in stub and live modes)
|
|
7
|
+
3. parse_vitals_log & evaluate_vitals_and_stall_guard (v4p: DEAD_HARNESS_TRIPPED on
|
|
8
|
+
cgroup OOM or 2 consecutive phase: run samples with 0 worker procs, STALL_GUARD_TRIPPED on stall)
|
|
9
|
+
4. check_host_ram_projection & run_between_item_fetch (v4q: pre-load cgroup/host RAM
|
|
10
|
+
check against kernel SIGKILL rc=-9, plus between-item fetch before current.json reset)
|
|
11
|
+
5. verify_input_shas & scrub_colab_tokens (pre-open sha256 gate and log token scrubber)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
import shutil
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
WORKER_PROC_RE = re.compile(
|
|
26
|
+
r"\b(comfy|main\.py|ai-toolkit|run\.py|llama-server|ollama|lane1_render\.py)\b",
|
|
27
|
+
re.I,
|
|
28
|
+
)
|
|
29
|
+
OOM_KILL_RE = re.compile(r"\boom_kill\s+([1-9]\d*)\b|\bOut of memory: Killed process\b", re.I)
|
|
30
|
+
|
|
31
|
+
TOKEN_SCRUB_PATTERNS = [
|
|
32
|
+
(re.compile(r"(colab-runtime-proxy-token=)[^\s&\"']+"), r"\1[REDACTED]"),
|
|
33
|
+
(re.compile(r"(access_token=)[^\s&\"']+"), r"\1[REDACTED]"),
|
|
34
|
+
(re.compile(r"(Bearer\s+)[A-Za-z0-9._\-]+"), r"\1[REDACTED]"),
|
|
35
|
+
(re.compile(r"\bhf_[A-Za-z0-9]{16,}\b"), "hf_[REDACTED]"),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def sha256_file(path: Path) -> str:
|
|
40
|
+
h = hashlib.sha256()
|
|
41
|
+
with Path(path).open("rb") as f:
|
|
42
|
+
for chunk in iter(lambda: f.read(1 << 20), b""):
|
|
43
|
+
h.update(chunk)
|
|
44
|
+
return h.hexdigest()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
SELF_PIN_LINE_RE = re.compile(rb'^(PINNED_SELF_SHA=")[0-9a-fA-F]{64}(")', flags=re.M)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def sha256_self_normalized(path: Path) -> tuple[str | None, str]:
|
|
51
|
+
raw = Path(path).read_bytes()
|
|
52
|
+
m = re.search(rb'^PINNED_SELF_SHA="([0-9a-fA-F]{64})"', raw, flags=re.M)
|
|
53
|
+
embedded_pin = m.group(1).decode("ascii").lower() if m else None
|
|
54
|
+
norm_bytes = SELF_PIN_LINE_RE.sub(rb"\1\2", raw, count=1)
|
|
55
|
+
return embedded_pin, hashlib.sha256(norm_bytes).hexdigest()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def normalize_hex_sha(val: str) -> str:
|
|
59
|
+
s = str(val).strip().lower()
|
|
60
|
+
if s.startswith("sha256:"):
|
|
61
|
+
s = s[7:]
|
|
62
|
+
return s
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def verify_input_shas(expected_shas: dict[str, str]) -> dict[str, str]:
|
|
66
|
+
verified: dict[str, str] = {}
|
|
67
|
+
for file_str, exp_raw in expected_shas.items():
|
|
68
|
+
p = Path(file_str)
|
|
69
|
+
if not p.exists():
|
|
70
|
+
raise ValueError(f"REFUSED: INPUT_FILE_MISSING: {p} does not exist")
|
|
71
|
+
exp = normalize_hex_sha(exp_raw)
|
|
72
|
+
got = sha256_file(p)
|
|
73
|
+
if got != exp:
|
|
74
|
+
embedded_pin, norm_got = sha256_self_normalized(p)
|
|
75
|
+
if embedded_pin is not None and exp == norm_got and embedded_pin == norm_got:
|
|
76
|
+
got = norm_got
|
|
77
|
+
else:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"REFUSED: INPUT_SHA_MISMATCH for {p}: got {got}, expected {exp}"
|
|
80
|
+
)
|
|
81
|
+
verified[file_str] = got
|
|
82
|
+
return verified
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def scrub_colab_tokens(text: str) -> str:
|
|
86
|
+
out = text
|
|
87
|
+
for pattern, repl in TOKEN_SCRUB_PATTERNS:
|
|
88
|
+
out = pattern.sub(repl, out)
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def scrub_file_in_place(path: Path) -> None:
|
|
93
|
+
p = Path(path)
|
|
94
|
+
if not p.exists() or not p.is_file():
|
|
95
|
+
return
|
|
96
|
+
raw = p.read_text(encoding="utf-8", errors="replace")
|
|
97
|
+
scrubbed = scrub_colab_tokens(raw)
|
|
98
|
+
if scrubbed != raw:
|
|
99
|
+
p.write_text(scrubbed, encoding="utf-8", newline="\n")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def evaluate_wall_guard(
|
|
103
|
+
*,
|
|
104
|
+
mode: str = "live",
|
|
105
|
+
elapsed_s: float,
|
|
106
|
+
wall_s: float = 3000.0,
|
|
107
|
+
grace_s: float = 60.0,
|
|
108
|
+
) -> dict[str, Any]:
|
|
109
|
+
threshold_s = round(max(0.0, float(wall_s) - float(grace_s)), 6)
|
|
110
|
+
elapsed_val = round(float(elapsed_s), 6)
|
|
111
|
+
remaining_s = round(threshold_s - elapsed_val, 6)
|
|
112
|
+
tripped = elapsed_val >= threshold_s
|
|
113
|
+
return {
|
|
114
|
+
"mode": mode,
|
|
115
|
+
"tripped": tripped,
|
|
116
|
+
"code": "WALL_GUARD_TRIPPED" if tripped else None,
|
|
117
|
+
"remaining_s": remaining_s,
|
|
118
|
+
"threshold_s": threshold_s,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
CURRENT_BALANCE_RE = re.compile(
|
|
123
|
+
r"^\s*Current\s+balance:\s*([+-]?\d+(?:\.\d+)?)\s*(?:compute\s+units)?\s*$",
|
|
124
|
+
re.I | re.M,
|
|
125
|
+
)
|
|
126
|
+
USAGE_RATE_RE = re.compile(
|
|
127
|
+
r"^\s*Usage\s+rate:\s*([+-]?\d+(?:\.\d+)?)\s*/\s*hr?\s*$",
|
|
128
|
+
re.I | re.M,
|
|
129
|
+
)
|
|
130
|
+
ACTIVE_ASSIGNMENTS_RE = re.compile(
|
|
131
|
+
r"^\s*Active\s+assignments:\s*(\d+)\s*$",
|
|
132
|
+
re.I | re.M,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def parse_usage_text(
|
|
137
|
+
raw_usage: str,
|
|
138
|
+
*,
|
|
139
|
+
usage_rc: int = 0,
|
|
140
|
+
configured_rate: float = 0.26,
|
|
141
|
+
session_opened: bool = True,
|
|
142
|
+
post_open_rate: float | None = None,
|
|
143
|
+
pre_open_rate: float | None = None,
|
|
144
|
+
) -> dict[str, Any]:
|
|
145
|
+
cfg = float(configured_rate)
|
|
146
|
+
|
|
147
|
+
def _select_rate(
|
|
148
|
+
cur_measured: float | None,
|
|
149
|
+
) -> tuple[float | None, float | None, float, str]:
|
|
150
|
+
eff_pre = cur_measured if not session_opened else pre_open_rate
|
|
151
|
+
existing_pos_post = post_open_rate if (post_open_rate is not None and post_open_rate > 0.0) else None
|
|
152
|
+
if existing_pos_post is not None:
|
|
153
|
+
eff_post: float | None = existing_pos_post
|
|
154
|
+
elif session_opened and cur_measured is not None:
|
|
155
|
+
eff_post = cur_measured
|
|
156
|
+
elif post_open_rate is not None:
|
|
157
|
+
eff_post = post_open_rate
|
|
158
|
+
else:
|
|
159
|
+
eff_post = None
|
|
160
|
+
|
|
161
|
+
if eff_post is not None and eff_post > 0.0:
|
|
162
|
+
return eff_pre, eff_post, float(eff_post), "post_open_measured"
|
|
163
|
+
if eff_post is not None and eff_post == 0.0:
|
|
164
|
+
pos_pre = float(eff_pre) if (eff_pre is not None and eff_pre > 0.0) else 0.0
|
|
165
|
+
return eff_pre, eff_post, max(cfg, pos_pre), "configured_pending_post_open_rate"
|
|
166
|
+
if eff_pre is not None:
|
|
167
|
+
return eff_pre, None, max(float(eff_pre), cfg), "max_of_pre_open_and_configured"
|
|
168
|
+
return None, None, cfg, "configured"
|
|
169
|
+
|
|
170
|
+
if usage_rc != 0:
|
|
171
|
+
eff_pre, eff_post, rate_used, rate_src = _select_rate(None)
|
|
172
|
+
return {
|
|
173
|
+
"ok": False,
|
|
174
|
+
"code": "COLAB_USAGE_PARSE_FAILED",
|
|
175
|
+
"balance": None,
|
|
176
|
+
"rate_measured": None,
|
|
177
|
+
"rate_measured_pre_open": eff_pre,
|
|
178
|
+
"rate_measured_post_open": eff_post,
|
|
179
|
+
"rate_for_projection": rate_used,
|
|
180
|
+
"rate_used_for_cap_and_projection": rate_used,
|
|
181
|
+
"rate_source": rate_src,
|
|
182
|
+
"active_assignments": None,
|
|
183
|
+
"error": f"COLAB_USAGE_PARSE_FAILED: colab usage exited with status {usage_rc}",
|
|
184
|
+
}
|
|
185
|
+
text = raw_usage or ""
|
|
186
|
+
m_bal = CURRENT_BALANCE_RE.search(text)
|
|
187
|
+
if not m_bal:
|
|
188
|
+
eff_pre, eff_post, rate_used, rate_src = _select_rate(None)
|
|
189
|
+
return {
|
|
190
|
+
"ok": False,
|
|
191
|
+
"code": "COLAB_USAGE_PARSE_FAILED",
|
|
192
|
+
"balance": None,
|
|
193
|
+
"rate_measured": None,
|
|
194
|
+
"rate_measured_pre_open": eff_pre,
|
|
195
|
+
"rate_measured_post_open": eff_post,
|
|
196
|
+
"rate_for_projection": rate_used,
|
|
197
|
+
"rate_used_for_cap_and_projection": rate_used,
|
|
198
|
+
"rate_source": rate_src,
|
|
199
|
+
"active_assignments": None,
|
|
200
|
+
"error": "COLAB_USAGE_PARSE_FAILED: missing 'Current balance: <float> compute units'",
|
|
201
|
+
}
|
|
202
|
+
balance = float(m_bal.group(1))
|
|
203
|
+
if balance < 0.0:
|
|
204
|
+
eff_pre, eff_post, rate_used, rate_src = _select_rate(None)
|
|
205
|
+
return {
|
|
206
|
+
"ok": False,
|
|
207
|
+
"code": "COLAB_USAGE_PARSE_FAILED",
|
|
208
|
+
"balance": None,
|
|
209
|
+
"rate_measured": None,
|
|
210
|
+
"rate_measured_pre_open": eff_pre,
|
|
211
|
+
"rate_measured_post_open": eff_post,
|
|
212
|
+
"rate_for_projection": rate_used,
|
|
213
|
+
"rate_used_for_cap_and_projection": rate_used,
|
|
214
|
+
"rate_source": rate_src,
|
|
215
|
+
"active_assignments": None,
|
|
216
|
+
"error": f"COLAB_USAGE_PARSE_FAILED: negative balance {balance} is impossible",
|
|
217
|
+
}
|
|
218
|
+
m_rate = USAGE_RATE_RE.search(text)
|
|
219
|
+
if m_rate:
|
|
220
|
+
rate_measured: float | None = float(m_rate.group(1))
|
|
221
|
+
if rate_measured < 0.0:
|
|
222
|
+
eff_pre, eff_post, rate_used, rate_src = _select_rate(None)
|
|
223
|
+
return {
|
|
224
|
+
"ok": False,
|
|
225
|
+
"code": "COLAB_USAGE_PARSE_FAILED",
|
|
226
|
+
"balance": None,
|
|
227
|
+
"rate_measured": None,
|
|
228
|
+
"rate_measured_pre_open": eff_pre,
|
|
229
|
+
"rate_measured_post_open": eff_post,
|
|
230
|
+
"rate_for_projection": rate_used,
|
|
231
|
+
"rate_used_for_cap_and_projection": rate_used,
|
|
232
|
+
"rate_source": rate_src,
|
|
233
|
+
"active_assignments": None,
|
|
234
|
+
"error": f"COLAB_USAGE_PARSE_FAILED: negative usage rate {rate_measured} is impossible",
|
|
235
|
+
}
|
|
236
|
+
else:
|
|
237
|
+
rate_measured = None
|
|
238
|
+
eff_pre, eff_post, rate_for_projection, rate_source = _select_rate(rate_measured)
|
|
239
|
+
m_assign = ACTIVE_ASSIGNMENTS_RE.search(text)
|
|
240
|
+
active_assignments = int(m_assign.group(1)) if m_assign else None
|
|
241
|
+
return {
|
|
242
|
+
"ok": True,
|
|
243
|
+
"code": None,
|
|
244
|
+
"balance": balance,
|
|
245
|
+
"rate_measured": rate_measured,
|
|
246
|
+
"rate_measured_pre_open": eff_pre,
|
|
247
|
+
"rate_measured_post_open": eff_post,
|
|
248
|
+
"rate_for_projection": rate_for_projection,
|
|
249
|
+
"rate_used_for_cap_and_projection": rate_for_projection,
|
|
250
|
+
"rate_source": rate_source,
|
|
251
|
+
"active_assignments": active_assignments,
|
|
252
|
+
"error": None,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def evaluate_cap_guard(
|
|
257
|
+
*,
|
|
258
|
+
mode: str = "live",
|
|
259
|
+
cap: float = 0.05,
|
|
260
|
+
margin: float = 0.01,
|
|
261
|
+
start_balance: float | None = None,
|
|
262
|
+
current_balance: float | None = None,
|
|
263
|
+
rate_ccu_per_hour: float = 0.0,
|
|
264
|
+
elapsed_s: float = 0.0,
|
|
265
|
+
usage_parse_error: str | None = None,
|
|
266
|
+
) -> dict[str, Any]:
|
|
267
|
+
ceiling = round(max(0.0, float(cap) - float(margin)), 6)
|
|
268
|
+
if usage_parse_error:
|
|
269
|
+
return {
|
|
270
|
+
"mode": mode,
|
|
271
|
+
"tripped": True,
|
|
272
|
+
"code": "COLAB_USAGE_PARSE_FAILED",
|
|
273
|
+
"spend_ccu": None,
|
|
274
|
+
"ceiling_ccu": ceiling,
|
|
275
|
+
"usage_parse_error": usage_parse_error,
|
|
276
|
+
}
|
|
277
|
+
if (start_balance is not None and float(start_balance) < 0.0) or (
|
|
278
|
+
current_balance is not None and float(current_balance) < 0.0
|
|
279
|
+
):
|
|
280
|
+
return {
|
|
281
|
+
"mode": mode,
|
|
282
|
+
"tripped": True,
|
|
283
|
+
"code": "COLAB_USAGE_PARSE_FAILED",
|
|
284
|
+
"spend_ccu": None,
|
|
285
|
+
"ceiling_ccu": ceiling,
|
|
286
|
+
"usage_parse_error": "COLAB_USAGE_PARSE_FAILED: negative balance is impossible",
|
|
287
|
+
}
|
|
288
|
+
if mode not in ("fake", "dry_run", "offline") and (start_balance is None or current_balance is None):
|
|
289
|
+
return {
|
|
290
|
+
"mode": mode,
|
|
291
|
+
"tripped": True,
|
|
292
|
+
"code": "COLAB_USAGE_PARSE_FAILED",
|
|
293
|
+
"spend_ccu": None,
|
|
294
|
+
"ceiling_ccu": ceiling,
|
|
295
|
+
"usage_parse_error": "COLAB_USAGE_PARSE_FAILED: missing start_balance or current_balance in non-fake mode",
|
|
296
|
+
}
|
|
297
|
+
drop = (
|
|
298
|
+
round(max(0.0, float(start_balance) - float(current_balance)), 6)
|
|
299
|
+
if start_balance is not None and current_balance is not None
|
|
300
|
+
else 0.0
|
|
301
|
+
)
|
|
302
|
+
rate_est = round(max(0.0, (float(rate_ccu_per_hour) * float(elapsed_s)) / 3600.0), 6)
|
|
303
|
+
spend = round(max(drop, rate_est), 6)
|
|
304
|
+
tripped = spend >= ceiling
|
|
305
|
+
return {
|
|
306
|
+
"mode": mode,
|
|
307
|
+
"tripped": tripped,
|
|
308
|
+
"code": "CAP_GUARD_TRIPPED" if tripped else None,
|
|
309
|
+
"spend_ccu": spend,
|
|
310
|
+
"ceiling_ccu": ceiling,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def parse_vitals_log(vitals_text: str) -> list[dict[str, Any]]:
|
|
315
|
+
blocks = [b.strip() for b in re.split(r"^===\s*VITALS\s+", vitals_text, flags=re.M) if b.strip()]
|
|
316
|
+
samples: list[dict[str, Any]] = []
|
|
317
|
+
for block in blocks:
|
|
318
|
+
lines = block.splitlines()
|
|
319
|
+
header = lines[0] if lines else ""
|
|
320
|
+
ts = header.split()[0] if header else ""
|
|
321
|
+
phase = None
|
|
322
|
+
worker_count = 0
|
|
323
|
+
oom_kill = False
|
|
324
|
+
setup_bytes: int | None = None
|
|
325
|
+
in_proc = False
|
|
326
|
+
for line in lines[1:]:
|
|
327
|
+
if OOM_KILL_RE.search(line):
|
|
328
|
+
oom_kill = True
|
|
329
|
+
if line.startswith("SETUP_BYTES:"):
|
|
330
|
+
in_proc = False
|
|
331
|
+
raw_sb = line[len("SETUP_BYTES:") :].strip()
|
|
332
|
+
if raw_sb.isdigit():
|
|
333
|
+
setup_bytes = int(raw_sb)
|
|
334
|
+
continue
|
|
335
|
+
if line.startswith("PROC:"):
|
|
336
|
+
in_proc = True
|
|
337
|
+
continue
|
|
338
|
+
if line.startswith("HARNESS:"):
|
|
339
|
+
in_proc = False
|
|
340
|
+
payload = line[len("HARNESS:") :].strip()
|
|
341
|
+
try:
|
|
342
|
+
phase = json.loads(payload).get("phase")
|
|
343
|
+
except Exception:
|
|
344
|
+
m = re.search(r'"phase"\s*:\s*"([^"]+)"', payload) or re.search(
|
|
345
|
+
r"phase:\s*([a-z0-9_-]+)", payload, re.I
|
|
346
|
+
)
|
|
347
|
+
phase = m.group(1) if m else None
|
|
348
|
+
continue
|
|
349
|
+
if in_proc and WORKER_PROC_RE.search(line):
|
|
350
|
+
worker_count += 1
|
|
351
|
+
samples.append(
|
|
352
|
+
{
|
|
353
|
+
"timestamp": ts,
|
|
354
|
+
"phase": phase,
|
|
355
|
+
"worker_proc_count": worker_count,
|
|
356
|
+
"oom_kill": oom_kill,
|
|
357
|
+
"setup_bytes": setup_bytes,
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
return samples
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def evaluate_vitals_and_stall_guard(
|
|
364
|
+
*,
|
|
365
|
+
vitals_text: str,
|
|
366
|
+
idle_seconds: float = 0.0,
|
|
367
|
+
setup_idle_seconds: float | None = None,
|
|
368
|
+
stall_s: float = 300.0,
|
|
369
|
+
setup_stall_s: float = 600.0,
|
|
370
|
+
) -> dict[str, Any]:
|
|
371
|
+
samples = parse_vitals_log(vitals_text)
|
|
372
|
+
current_phase = "setup"
|
|
373
|
+
if samples:
|
|
374
|
+
last = samples[-1]
|
|
375
|
+
if isinstance(last["phase"], str) and last["phase"].strip():
|
|
376
|
+
current_phase = last["phase"].strip()
|
|
377
|
+
if (
|
|
378
|
+
isinstance(last["phase"], str)
|
|
379
|
+
and (last["phase"].startswith("run") or last["phase"].startswith("setup"))
|
|
380
|
+
and last["worker_proc_count"] == 0
|
|
381
|
+
and last.get("oom_kill")
|
|
382
|
+
):
|
|
383
|
+
return {
|
|
384
|
+
"tripped": True,
|
|
385
|
+
"code": "DEAD_HARNESS_TRIPPED",
|
|
386
|
+
"cgroup_oom": True,
|
|
387
|
+
"reason": f"cgroup OOM kill detected in phase={last['phase']} with 0 active worker processes",
|
|
388
|
+
"samples_count": len(samples),
|
|
389
|
+
}
|
|
390
|
+
if len(samples) >= 2:
|
|
391
|
+
last_two = samples[-2:]
|
|
392
|
+
if all(
|
|
393
|
+
isinstance(s["phase"], str) and s["phase"].startswith("run") and s["worker_proc_count"] == 0
|
|
394
|
+
for s in last_two
|
|
395
|
+
):
|
|
396
|
+
return {
|
|
397
|
+
"tripped": True,
|
|
398
|
+
"code": "DEAD_HARNESS_TRIPPED",
|
|
399
|
+
"cgroup_oom": any(bool(s.get("oom_kill")) for s in last_two),
|
|
400
|
+
"reason": "2 consecutive vitals samples in phase=run with 0 active worker processes",
|
|
401
|
+
"samples_count": len(samples),
|
|
402
|
+
}
|
|
403
|
+
if current_phase.startswith("setup"):
|
|
404
|
+
eff_setup_idle = float(setup_idle_seconds if setup_idle_seconds is not None else idle_seconds)
|
|
405
|
+
if eff_setup_idle >= float(setup_stall_s):
|
|
406
|
+
return {
|
|
407
|
+
"tripped": True,
|
|
408
|
+
"code": "SETUP_STALL_TRIPPED",
|
|
409
|
+
"cgroup_oom": False,
|
|
410
|
+
"reason": f"No new setup.log output (SETUP_BYTES unchanged) in phase=setup for {eff_setup_idle}s (setup_stall_s={setup_stall_s}s)",
|
|
411
|
+
"samples_count": len(samples),
|
|
412
|
+
}
|
|
413
|
+
elif float(idle_seconds) >= float(stall_s):
|
|
414
|
+
return {
|
|
415
|
+
"tripped": True,
|
|
416
|
+
"code": "STALL_GUARD_TRIPPED",
|
|
417
|
+
"cgroup_oom": False,
|
|
418
|
+
"reason": f"No output row or vitals progress for {idle_seconds}s (stall_s={stall_s}s)",
|
|
419
|
+
"samples_count": len(samples),
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
"tripped": False,
|
|
423
|
+
"code": None,
|
|
424
|
+
"cgroup_oom": False,
|
|
425
|
+
"reason": None,
|
|
426
|
+
"samples_count": len(samples),
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def check_host_ram_projection(
|
|
431
|
+
*,
|
|
432
|
+
current_anon_mb: float,
|
|
433
|
+
projected_delta_mb: float,
|
|
434
|
+
mem_total_mb: float,
|
|
435
|
+
cgroup_max_mb: float | None = None,
|
|
436
|
+
safety_factor: float = 1.05,
|
|
437
|
+
) -> dict[str, Any]:
|
|
438
|
+
limits = [float(mem_total_mb)]
|
|
439
|
+
if cgroup_max_mb is not None:
|
|
440
|
+
limits.append(float(cgroup_max_mb))
|
|
441
|
+
limit_mb = min(limits)
|
|
442
|
+
projected_mb = round((float(current_anon_mb) + float(projected_delta_mb)) * float(safety_factor), 1)
|
|
443
|
+
if projected_mb > limit_mb:
|
|
444
|
+
raise ValueError(
|
|
445
|
+
f"HOST_RAM_PROJECTION_EXCEEDED: projected {projected_mb:.1f} MB > ceiling {limit_mb:.1f} MB"
|
|
446
|
+
)
|
|
447
|
+
return {"ok": True, "projected_mb": projected_mb, "limit_mb": limit_mb}
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def run_between_item_fetch(
|
|
451
|
+
*,
|
|
452
|
+
remote_dir: Path,
|
|
453
|
+
local_dir: Path,
|
|
454
|
+
next_item: dict[str, Any] | None = None,
|
|
455
|
+
) -> dict[str, Any]:
|
|
456
|
+
local_dir.mkdir(parents=True, exist_ok=True)
|
|
457
|
+
current_json = remote_dir / "current.json"
|
|
458
|
+
copied: list[str] = []
|
|
459
|
+
previous_item = None
|
|
460
|
+
if current_json.exists():
|
|
461
|
+
previous_item = json.loads(current_json.read_text(encoding="utf-8"))
|
|
462
|
+
for rel in previous_item.get("files") or []:
|
|
463
|
+
src = remote_dir / rel
|
|
464
|
+
if src.exists():
|
|
465
|
+
dst = local_dir / rel
|
|
466
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
467
|
+
shutil.copy2(src, dst)
|
|
468
|
+
copied.append(rel)
|
|
469
|
+
shutil.copy2(current_json, local_dir / "current.json")
|
|
470
|
+
if next_item is not None:
|
|
471
|
+
current_json.write_text(json.dumps(next_item, indent=2) + "\n", encoding="utf-8")
|
|
472
|
+
return {"copied_files": copied, "previous_item": previous_item}
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def main(argv: list[str] | None = None) -> int:
|
|
476
|
+
parser = argparse.ArgumentParser(description="sdlab C2 launcher guards CLI")
|
|
477
|
+
parser.add_argument("--mode", default="stub")
|
|
478
|
+
parser.add_argument("--elapsed-s", type=float, default=0.0)
|
|
479
|
+
parser.add_argument("--wall-s", type=float, default=3000.0)
|
|
480
|
+
parser.add_argument("--grace-s", type=float, default=60.0)
|
|
481
|
+
parser.add_argument("--cap", type=float, default=0.05)
|
|
482
|
+
parser.add_argument("--margin", type=float, default=0.01)
|
|
483
|
+
parser.add_argument("--rate", type=float, default=0.0)
|
|
484
|
+
parser.add_argument("--start-balance", type=float, default=None)
|
|
485
|
+
parser.add_argument("--current-balance", type=float, default=None)
|
|
486
|
+
parser.add_argument("--usage-parse-error", default=None, help="Usage parse failure message causing cap guard to trip")
|
|
487
|
+
parser.add_argument("--verify-sha", action="append", default=[], help="Verify PATH=SHA256 before opening window")
|
|
488
|
+
parser.add_argument("--scrub-file", action="append", default=[], help="Scrub Colab/OAuth tokens from file in place")
|
|
489
|
+
parser.add_argument("--vitals-file", type=Path, default=None, help="Evaluate vitals.log and stall guard")
|
|
490
|
+
parser.add_argument("--idle-seconds", type=float, default=0.0)
|
|
491
|
+
parser.add_argument("--setup-idle-seconds", type=float, default=None)
|
|
492
|
+
parser.add_argument("--stall-s", type=float, default=300.0)
|
|
493
|
+
parser.add_argument("--setup-stall-s", type=float, default=600.0)
|
|
494
|
+
parser.add_argument("--check-ram", action="store_true", help="Run host/cgroup RAM projection check")
|
|
495
|
+
parser.add_argument("--current-anon-mb", type=float, default=512.0)
|
|
496
|
+
parser.add_argument("--projected-delta-mb", type=float, default=1024.0)
|
|
497
|
+
parser.add_argument("--mem-total-mb", type=float, default=12288.0)
|
|
498
|
+
parser.add_argument("--cgroup-max-mb", type=float, default=None)
|
|
499
|
+
args = parser.parse_args(argv)
|
|
500
|
+
|
|
501
|
+
if args.verify_sha:
|
|
502
|
+
pairs: dict[str, str] = {}
|
|
503
|
+
for item in args.verify_sha:
|
|
504
|
+
if "=" not in item:
|
|
505
|
+
print(f"REFUSED: invalid --verify-sha argument {item!r}", file=sys.stderr)
|
|
506
|
+
return 2
|
|
507
|
+
k, v = item.split("=", 1)
|
|
508
|
+
pairs[k] = v
|
|
509
|
+
try:
|
|
510
|
+
verified = verify_input_shas(pairs)
|
|
511
|
+
print(json.dumps({"verified_shas": verified}))
|
|
512
|
+
return 0
|
|
513
|
+
except ValueError as exc:
|
|
514
|
+
print(str(exc), file=sys.stderr)
|
|
515
|
+
return 1
|
|
516
|
+
|
|
517
|
+
if args.scrub_file:
|
|
518
|
+
for fpath in args.scrub_file:
|
|
519
|
+
scrub_file_in_place(Path(fpath))
|
|
520
|
+
return 0
|
|
521
|
+
|
|
522
|
+
if args.check_ram:
|
|
523
|
+
try:
|
|
524
|
+
res = check_host_ram_projection(
|
|
525
|
+
current_anon_mb=args.current_anon_mb,
|
|
526
|
+
projected_delta_mb=args.projected_delta_mb,
|
|
527
|
+
mem_total_mb=args.mem_total_mb,
|
|
528
|
+
cgroup_max_mb=args.cgroup_max_mb,
|
|
529
|
+
)
|
|
530
|
+
print(json.dumps(res))
|
|
531
|
+
return 0
|
|
532
|
+
except ValueError as exc:
|
|
533
|
+
print(str(exc), file=sys.stderr)
|
|
534
|
+
return 1
|
|
535
|
+
|
|
536
|
+
if args.vitals_file is not None:
|
|
537
|
+
txt = args.vitals_file.read_text(encoding="utf-8", errors="replace") if args.vitals_file.exists() else ""
|
|
538
|
+
vit = evaluate_vitals_and_stall_guard(
|
|
539
|
+
vitals_text=txt,
|
|
540
|
+
idle_seconds=args.idle_seconds,
|
|
541
|
+
setup_idle_seconds=args.setup_idle_seconds,
|
|
542
|
+
stall_s=args.stall_s,
|
|
543
|
+
setup_stall_s=args.setup_stall_s,
|
|
544
|
+
)
|
|
545
|
+
print(json.dumps(vit))
|
|
546
|
+
return 1 if vit["tripped"] else 0
|
|
547
|
+
|
|
548
|
+
wall = evaluate_wall_guard(mode=args.mode, elapsed_s=args.elapsed_s, wall_s=args.wall_s, grace_s=args.grace_s)
|
|
549
|
+
cap = evaluate_cap_guard(
|
|
550
|
+
mode=args.mode,
|
|
551
|
+
cap=args.cap,
|
|
552
|
+
margin=args.margin,
|
|
553
|
+
start_balance=args.start_balance,
|
|
554
|
+
current_balance=args.current_balance,
|
|
555
|
+
rate_ccu_per_hour=args.rate,
|
|
556
|
+
elapsed_s=args.elapsed_s,
|
|
557
|
+
usage_parse_error=args.usage_parse_error,
|
|
558
|
+
)
|
|
559
|
+
print(json.dumps({"wall": wall, "cap": cap}))
|
|
560
|
+
return 1 if (wall["tripped"] or cap["tripped"]) else 0
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
if __name__ == "__main__":
|
|
564
|
+
raise SystemExit(main())
|