"""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