Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
172 lines
7.1 KiB
Python
172 lines
7.1 KiB
Python
"""Discover explicit local recipes and admit their output without model calls."""
|
|
|
|
import ast
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import time
|
|
from pathlib import Path
|
|
|
|
|
|
def recipe_paths(root):
|
|
root = Path(root).resolve()
|
|
directory = root / "my-tools"
|
|
if directory.is_symlink():
|
|
return []
|
|
return sorted(p for p in directory.glob("prepare_wbv2_*workflow.py")
|
|
if p.is_file() and not p.is_symlink())[:128]
|
|
|
|
|
|
def _declarations(path):
|
|
if path.stat().st_size > 128 * 1024:
|
|
return None
|
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
values = {}
|
|
for node in tree.body:
|
|
if isinstance(node, ast.Assign) and len(node.targets) == 1:
|
|
target = node.targets[0]
|
|
if isinstance(target, ast.Name) and target.id in {"JOB", "PREPARATION_INPUTS"}:
|
|
values[target.id] = ast.literal_eval(node.value)
|
|
job = values.get("JOB")
|
|
inputs = values.get("PREPARATION_INPUTS", [])
|
|
if not isinstance(job, str) or not re.fullmatch(r"[A-Z0-9][A-Z0-9_-]{0,127}", job):
|
|
return None
|
|
if not isinstance(inputs, list) or len(inputs) > 128 or not all(isinstance(p, str) for p in inputs):
|
|
return None
|
|
return job, inputs
|
|
|
|
|
|
def _local(root, relative):
|
|
path = Path(relative)
|
|
if path.is_absolute() or ".." in path.parts:
|
|
raise ValueError("preparation input escapes repository")
|
|
current = root
|
|
for part in path.parts:
|
|
current /= part
|
|
if current.is_symlink():
|
|
raise ValueError("preparation input is a symlink")
|
|
return current
|
|
|
|
|
|
def _fingerprint(root, context, paths):
|
|
files = {}
|
|
for relative in sorted(set(paths)):
|
|
path = _local(root, relative)
|
|
files[relative] = hashlib.sha256(path.read_bytes()).hexdigest() if path.is_file() else None
|
|
authority = {key: context[key] for key in ("task", "parentSha256")}
|
|
return hashlib.sha256(json.dumps([authority, files], sort_keys=True).encode()).hexdigest()
|
|
|
|
|
|
def recipe_input_paths(root, jobs):
|
|
wanted = {job["id"] for job in jobs}
|
|
paths = set()
|
|
for script in recipe_paths(root):
|
|
paths.add(str(script.relative_to(root)))
|
|
try:
|
|
declaration = _declarations(script)
|
|
if declaration and declaration[0] in wanted:
|
|
for relative in declaration[1]:
|
|
_local(root, relative)
|
|
paths.add(relative)
|
|
except (OSError, SyntaxError, ValueError, TypeError):
|
|
continue
|
|
return paths
|
|
|
|
|
|
def _failed_origin_owned(root, origin, prior, context, identifier, script):
|
|
owned = prior.get("generatedOrigin")
|
|
if (prior.get("state") != "blocked" or prior.get("job") != identifier
|
|
or prior.get("script") != script or prior.get("task") != context["task"]
|
|
or prior.get("parentSha256") != context["parentSha256"]
|
|
or not isinstance(owned, dict)):
|
|
return False
|
|
try:
|
|
relative = str(origin.relative_to(root))
|
|
return (owned.get("path") == relative
|
|
and _local(root, relative).is_file()
|
|
and hashlib.sha256(origin.read_bytes()).hexdigest() == owned.get("sha256"))
|
|
except (OSError, ValueError):
|
|
return False
|
|
|
|
|
|
def prepare_local_contracts(runner, context, jobs):
|
|
from wbv2_autopilot import queue_context, read_json, write_json
|
|
from wbv2_task_dispatch import prepared_contract
|
|
|
|
recipes = {}
|
|
for script in recipe_paths(runner.root):
|
|
try:
|
|
declaration = _declarations(script)
|
|
except (OSError, SyntaxError, ValueError, TypeError):
|
|
continue
|
|
if declaration:
|
|
job, inputs = declaration
|
|
recipes.setdefault(job, []).append((script, inputs))
|
|
prepared = []
|
|
for job in jobs:
|
|
identifier = job["id"]
|
|
choices = recipes.get(identifier, [])
|
|
if not re.fullmatch(r"[A-Z0-9][A-Z0-9_-]{0,127}", identifier) or len(choices) != 1:
|
|
continue
|
|
# An existing origin belongs to the developer, including one being edited.
|
|
# Admission errors must not cause its generator to overwrite those edits.
|
|
origin = _local(runner.root, f"my-tools/local-workflows/{identifier}.json")
|
|
if runner.local_contract_path(job):
|
|
continue
|
|
script, declared = choices[0]
|
|
relative = str(script.relative_to(runner.root))
|
|
inputs = [relative, context["parentManifest"], *job.get("ownedPaths", []), *declared]
|
|
if len(inputs) > 256:
|
|
continue
|
|
try:
|
|
fingerprint = _fingerprint(runner.root, context, inputs)
|
|
except (OSError, ValueError):
|
|
continue
|
|
record_path = runner.directory / "preparation-attempts" / f"{identifier}.json"
|
|
prior = read_json(record_path, {})
|
|
if prior.get("fingerprint") == fingerprint:
|
|
continue
|
|
if runner.stop_requested or (runner.directory / "pause.request").exists():
|
|
break
|
|
if queue_context(runner.root) != context:
|
|
raise InterruptedError("queue changed before local preparation")
|
|
if origin.exists():
|
|
if not _failed_origin_owned(runner.root, origin, prior, context, identifier, relative):
|
|
continue
|
|
origin.unlink()
|
|
started = time.monotonic()
|
|
record = {"schemaVersion": 1, "job": identifier, "script": relative,
|
|
"fingerprint": fingerprint, "state": "running"}
|
|
record.update(task=context["task"], parentSha256=context["parentSha256"])
|
|
write_json(record_path, record)
|
|
proc = runner.run_process(["python3", relative], runner.root,
|
|
runner.directory / "preparation" / f"{identifier}.log",
|
|
min(120, runner.policy["checkTimeoutSeconds"]),
|
|
phase="local_workflow_prepare")
|
|
record.update(seconds=time.monotonic() - started, exitCode=proc["exitCode"])
|
|
if queue_context(runner.root) != context:
|
|
record["state"] = "authority_changed"
|
|
write_json(record_path, record)
|
|
raise InterruptedError("queue changed during local preparation")
|
|
ok = proc["exitCode"] == 0 and not any(proc.get(key) for key in ("interrupted", "timedOut", "logLimit"))
|
|
contract, findings = prepared_contract(runner.root, context, job) if ok else (None, [])
|
|
if not contract:
|
|
try:
|
|
output = str(origin.relative_to(runner.root))
|
|
if _local(runner.root, output).is_file():
|
|
record["generatedOrigin"] = {
|
|
"path": output,
|
|
"sha256": hashlib.sha256(origin.read_bytes()).hexdigest(),
|
|
}
|
|
except (OSError, ValueError):
|
|
pass
|
|
record.update(state="prepared" if contract else "blocked", findings=findings)
|
|
write_json(record_path, record)
|
|
runner.event("local.prepared" if contract else "local.prepare_failed", job=identifier,
|
|
script=relative, seconds=record["seconds"], exitCode=proc["exitCode"])
|
|
if contract:
|
|
prepared.append(identifier)
|
|
if proc.get("interrupted"):
|
|
break
|
|
return prepared
|