Download voyager_runtime_contract.py from Erinaldordso/Minecraft-Friends-AI-ZeroGPU: direct link, hf CLI and curl.
- Browser
- Download file 26.7 kB
-
https://huggingface.co/spaces/Erinaldordso/Minecraft-Friends-AI-ZeroGPU/resolve/main/voyager_runtime_contract.py
- Command line
-
hf download hf://spaces/Erinaldordso/Minecraft-Friends-AI-ZeroGPU/voyager_runtime_contract.py
-
curl -L -o voyager_runtime_contract.py https://huggingface.co/spaces/Erinaldordso/Minecraft-Friends-AI-ZeroGPU/resolve/main/voyager_runtime_contract.py
26.7 kB
| """Check that both bot processes run the pinned MineDojo Voyager engine.""" | |
| from __future__ import annotations | |
| import inspect | |
| import json | |
| import os | |
| import subprocess | |
| import time | |
| from pathlib import Path | |
| UPSTREAM_COMMIT = "55e45a880755d0c8c66ca7fb5fe7962ac8974f89" | |
| # Methods that decide the next task, the generated program, and the success | |
| # verdict. A boot-time patch can rewrite these while leaving the class objects | |
| # pointing at the upstream files, so class identity alone is not proof. | |
| # | |
| # Curriculum.propose_next_ai_task is compared against .upstream-expected/, which | |
| # is the pristine upstream file plus the one declared recovery hunk recorded in | |
| # ADAPTED_METHODS. Everything else must be byte-identical to upstream. | |
| DECISION_METHODS = { | |
| "curriculum": ("propose_next_ai_task", "get_task_context", "parse_ai_message"), | |
| "action": ("process_ai_message",), | |
| "critic": ("check_task_success",), | |
| } | |
| # (role, method) -> why the local file differs from pristine upstream. | |
| ADAPTED_METHODS = { | |
| ("curriculum", "propose_next_ai_task"): ( | |
| "parse-failure retry feeds the rejected proposal back to the model; at " | |
| "temperature 0 upstream re-sends an identical prompt and deterministically " | |
| "reproduces the same rejected task" | |
| ), | |
| } | |
| # Local classes allowed to sit between the learning loop and a pinned upstream | |
| # decision agent, with the attribute that holds the upstream delegate. | |
| # | |
| # The attestation resolves the delegate and attests that object, so the report | |
| # describes the code that actually answers the decision call instead of an | |
| # object that was replaced after the check. Nothing else may wrap a decision | |
| # agent: an undeclared wrapper fails the boot. | |
| DECLARED_WRAPPERS = { | |
| "critic": { | |
| "class_name": "VerifiedCreativeCritic", | |
| "delegate_attribute": "upstream", | |
| "reason": ( | |
| "two deny-only guards around the pinned upstream CriticAgent, whose verdict " | |
| "is otherwise unchanged. (1) It returns False when the attempt ended in a " | |
| "Mineflayer onError event, which upstream render_human_message cannot " | |
| "evaluate (it asserts the last event is 'observe' and dereferences status " | |
| "first). (2) It returns False when the model reported success but the live " | |
| "world contradicts it on an objective with a countable postcondition; " | |
| "mechanical_verdict() is three-valued and returns None whenever this " | |
| "deployment cannot judge the objective, so the upstream Critic keeps the " | |
| "verdict everywhere else. Measured need for (2): the Critic returned " | |
| "success: true with the reasoning 'having a netherite_pickaxe in their " | |
| "inventory means they have successfully mined gold, stone bricks, bricks and " | |
| "polished andesite', and separately returned success: true while its own " | |
| "evidence line read 'Live Inventory has 0/1 ...'. Those reached " | |
| "completed_tasks.json. The wrapper can only deny a success, never create one." | |
| ), | |
| }, | |
| } | |
| REFERENCE_DIR = ".upstream-reference" | |
| EXPECTED_DIR = ".upstream-expected" | |
| MANIFEST_FILE = "MANIFEST.json" | |
| DIVERGENCE_FILE = "DIVERGENCE.json" | |
| # Every upstream path this deployment rewrites at boot, with the reason. The | |
| # attestation attests five decision methods; those are not the whole difference | |
| # between this tree and the pinned commit. Without this list, `checkout_verified` | |
| # reads as "the tree is pristine", which it is not. | |
| # | |
| # The set below was measured by running the real boot patcher | |
| # (_patch_runtime_source + _pin_node_versions) against a clean checkout of the | |
| # pinned commit and hashing the tree before and after. verify_divergence() | |
| # re-measures it at boot and fails if the real set is not exactly this one, so a | |
| # future edit to the patcher cannot silently widen the divergence. | |
| ADAPTED_UPSTREAM_FILES = { | |
| "voyager/agents/action.py": ( | |
| "max_tokens=2048 on the Action LLM (upstream sets none, so app.py's 384-token " | |
| "default truncated the official multi-function programs), creativeGiveItem in " | |
| "base_skills, nullable status numerics, and 8k-context observation bounds" | |
| ), | |
| "voyager/agents/critic.py": "nullable status numerics so a disconnect cannot raise", | |
| "voyager/agents/skill.py": ( | |
| "SkillManager built Chroma() with no deadline, and that call embeds through " | |
| "OpenAIEmbeddings(), which in langchain 0.0.350 uses the openai 0.28.1 client " | |
| "with no request timeout. One stalled /v1/embeddings response parked the " | |
| "constructor forever: the log stopped at 'Loading Skill Manager' and learn() " | |
| "was never reached, so nothing failed and no counter moved. The open now runs " | |
| "under a wall-clock deadline with a bounded embedding timeout. The closing " | |
| "assert is replaced by a resync, because an index one document out of sync is " | |
| "recoverable from skills.json and the assert turned that into a dead process" | |
| ), | |
| "voyager/agents/curriculum.py": ( | |
| "the one declared decision adaptation (parse-failure retry feedback), nullable " | |
| "status numerics, AIMessage import, and task_history() compaction of the prompt. " | |
| "run_qa (not an attested decision method) no longer asserts that a qa_cache " | |
| "vector hit is present in qa_cache.json: the two drift apart, and the bare " | |
| "assert raised AssertionError with an empty message outside the try that wraps " | |
| "rollout(), so learn() died every cycle with nothing in the error counters. A " | |
| "hit whose text is missing from the JSON is now a miss, and the lookup is bounded. " | |
| "__init__ no longer asserts that the qa_cache vector count equals the JSON " | |
| "count either: measured live at 58 vectors against 55 entries, that assert " | |
| "raised with rc=1 and the runner restart-looped with zero model calls, and it " | |
| "is the same drift one layer earlier. The JSON is the source of truth and the " | |
| "index is a cache, so __init__ now reconciles the two by dropping stale " | |
| "vectors and indexing missing questions, which is the manual step the " | |
| "upstream failure message asked the operator to perform. The qa_cache Chroma " | |
| "call is also opened under a wall-clock deadline with a bounded embedding " | |
| "client, for the same reason as SkillManager: it embeds during construction " | |
| "through a client that has no default request timeout" | |
| ), | |
| "voyager/control_primitives/exploreUntil.js": ( | |
| "two exploration modes chosen by the target: y=0 walks the surface, a purely " | |
| "vertical downward vector mines a controlled staircase instead of asking the " | |
| "pathfinder for a lower goal. The upstream surface-only veto (dy = 0) is gone " | |
| "because it silently rewrote every underground ore request into a walk" | |
| ), | |
| "voyager/control_primitives/mineBlock.js": ( | |
| "upstream collectBlock waits for a drop, which Creative never produces; the " | |
| "Creative branch uses bot.dig and verifies the block actually changed. The " | |
| "search floor is ABSOLUTE (MAX_DIG_DEPTH below the bot) instead of the old " | |
| "relative -2, and the primitive digs a staircase toward a known ore layer " | |
| "when nothing is in range. The relative floor moved down with the bot, so it " | |
| "permanently excluded the layer underneath: iron at y=48 was visible and " | |
| "rejected forever, and every ore task looped until the stall watchdog fired. " | |
| "The approach is bidirectional: measured live, the bot dug past the gold " | |
| "layer looking for gold and stalled at y=51 with gold at y=52-53 above it, " | |
| "where the descent-only helper returned success without digging and the same " | |
| "'No gold_ore within 32 blocks' error repeated with the bot motionless. An " | |
| "ore layer can be above or below, so the primitive now mines overhead and " | |
| "steps up as well as digging down" | |
| ), | |
| "voyager/control_primitives/placeItem.js": ( | |
| "replaced by the local voyager_place.js: verified surface placement for Creative" | |
| ), | |
| "voyager/control_primitives_context/exploreUntil.js": ( | |
| "documents the horizontal and vertical descent modes to the Action Agent" | |
| ), | |
| "voyager/env/bridge.py": ( | |
| "10s timeout on the /pause and /stop posts so a disconnect cannot hang Python; " | |
| "135s limit on /start, whose Node lifecycle has a 120s startup deadline" | |
| ), | |
| "voyager/env/process_monitor.py": ( | |
| "bounded waits: stop() gives the node 10s after SIGTERM then escalates to " | |
| "kill(), and run() gives up on the ready line after 60s. reset() calls " | |
| "mineflayer.stop() on the recovery path, so an unbounded wait there parked " | |
| "the whole learn loop with no exception and no log" | |
| ), | |
| "voyager/env/mineflayer/index.js": ( | |
| "identity from env, pinned protocol 1.19, Creative toolkit at spawn, step " | |
| "disconnect handling, NaN movement guard, /health route, /pause neutralised, " | |
| "pathfinder bounds, inventory-quantity guard in evaluateCode, paced chat packets, " | |
| "persistent Creative inventory restoration, and per-connection lifecycle guards" | |
| ), | |
| "voyager/env/mineflayer/lib/observation/base.js": "exposes verified_actions and step_start_position for the advisory evidence", | |
| "voyager/env/mineflayer/mineflayer-collectblock/package.json": "pinned to the 2023-compatible dependency tree", | |
| "voyager/env/mineflayer/mineflayer-collectblock/src/CollectBlock.ts": "@ts-expect-error -> @ts-ignore so the pinned TypeScript build succeeds", | |
| "voyager/env/mineflayer/package.json": "pinned to the 2023-compatible dependency tree", | |
| "voyager/prompts/action_template.txt": ( | |
| "adds Creative ground truth facts, including the exact y of every ore layer " | |
| "in the superflat map, so the Action Agent knows an ore is straight down " | |
| "instead of exploring horizontally for it; the response contract is unchanged" | |
| ), | |
| "voyager/prompts/curriculum.txt": "appends environment facts; the task format is unchanged", | |
| "voyager/voyager.py": ( | |
| "records the last valid observation instead of an empty one on a parse failure; " | |
| "recovers the connection in place instead of a hard Mineflayer reset; and runs " | |
| "every boot step through _bounded_step so a wedged bridge raises instead of " | |
| "parking the loop forever. _bounded_step itself delegates to _with_deadline, " | |
| "which runs the call on a daemon thread and abandons it on timeout: the earlier " | |
| "`with ThreadPoolExecutor(...)` form raised TimeoutError on schedule and then " | |
| "blocked forever in its own shutdown(wait=True) join, so the deadline was " | |
| "decorative and the runner sat 343s past a 180s limit. The engine owns its " | |
| "own deadline method; helpers on CurriculumAgent/SkillManager cannot satisfy it" | |
| ), | |
| } | |
| ADDED_UPSTREAM_FILES = { | |
| "voyager/env/mineflayer/lib/voyager_connection.js": ( | |
| "per-connection chat queue, disconnect/startup deadlines, truthful readiness " | |
| "and Creative recovery that preserves existing stacks and equipment" | |
| ), | |
| "minecraft_ids_1_19.json": "minecraft-data@3.31.0/pc/1.19 registry used to reject non-existent block names", | |
| "voyager/control_primitives/creativeGiveItem.js": ( | |
| "Creative-only helper so an Obtain task has a path at all; upstream has no " | |
| "equivalent because upstream assumes Survival drops" | |
| ), | |
| "voyager/control_primitives_context/creativeGiveItem.js": "documents that helper to the Action Agent", | |
| "voyager_verification.py": "advisory world evidence handed to the official Critic; never decides", | |
| } | |
| # Directories the boot legitimately creates that are not upstream content. | |
| GENERATED_PREFIXES = ( | |
| "node_modules/", | |
| ".git/", | |
| f"{REFERENCE_DIR}/", | |
| f"{EXPECTED_DIR}/", | |
| ) | |
| GENERATED_PARTS = frozenset({"node_modules", "__pycache__"}) | |
| # npm rewrites a lock file next to every package.json. Upstream does not track | |
| # them (a fresh checkout has none), so one appearing is a build artifact, not a | |
| # divergence. Applied only to the added-file scan: a *tracked* file is always | |
| # hashed against the manifest, so this cannot hide a change to upstream content. | |
| NPM_ARTIFACT_NAMES = frozenset({"package-lock.json", ".package-lock.json"}) | |
| def _is_npm_artifact(relative: str) -> bool: | |
| return relative.rsplit("/", 1)[-1] in NPM_ARTIFACT_NAMES | |
| def _digest(path) -> str: | |
| import hashlib | |
| return hashlib.sha256(path.read_bytes()).hexdigest() | |
| def _is_generated(relative: str) -> bool: | |
| if relative.startswith(GENERATED_PREFIXES): | |
| return True | |
| parts = relative.split("/") | |
| if any(part in GENERATED_PARTS for part in parts): | |
| return True | |
| return any(part.endswith(".egg-info") for part in parts) | |
| def write_upstream_manifest(home) -> None: | |
| """Record the pristine tree, before the patcher touches anything. | |
| Called right after the checkout, when the tree still matches the pinned | |
| commit. Only written once, so a reused checkout cannot overwrite it. | |
| """ | |
| from pathlib import Path | |
| home = Path(home) | |
| destination = home / REFERENCE_DIR / MANIFEST_FILE | |
| if destination.exists(): | |
| return | |
| destination.parent.mkdir(parents=True, exist_ok=True) | |
| entries = { | |
| path.relative_to(home).as_posix(): _digest(path) | |
| for path in sorted(home.rglob("*")) | |
| if path.is_file() and not _is_generated(path.relative_to(home).as_posix()) | |
| } | |
| destination.write_text(json.dumps(entries, indent=1, sort_keys=True) + "\n", encoding="utf-8") | |
| def divergence_report(home) -> dict: | |
| """Measure the real divergence between the live tree and the pristine one.""" | |
| from pathlib import Path | |
| home = Path(home) | |
| manifest_path = home / REFERENCE_DIR / MANIFEST_FILE | |
| if not manifest_path.exists(): | |
| raise RuntimeError( | |
| "Missing upstream manifest; cannot prove which files this boot rewrote" | |
| ) | |
| pristine = json.loads(manifest_path.read_text(encoding="utf-8")) | |
| modified, removed = [], [] | |
| for relative, digest in pristine.items(): | |
| path = home / relative | |
| if not path.exists(): | |
| removed.append(relative) | |
| elif _digest(path) != digest: | |
| modified.append(relative) | |
| added = [] | |
| for path in sorted(home.rglob("*")): | |
| if not path.is_file(): | |
| continue | |
| relative = path.relative_to(home).as_posix() | |
| if relative in pristine or _is_generated(relative) or _is_npm_artifact(relative): | |
| continue | |
| added.append(relative) | |
| return {"modified": sorted(modified), "removed": sorted(removed), "added": sorted(added)} | |
| def verify_divergence(home) -> dict: | |
| """Fail the boot if the tree differs from the pinned commit in an undeclared way. | |
| Called by the boot right after the last source-mutating step and before npm | |
| install, so the measurement sees only tracked upstream content -- no | |
| node_modules, no generated package-lock.json. The report is published next to | |
| the manifest for the runner to put into the attestation. | |
| """ | |
| from pathlib import Path | |
| home = Path(home) | |
| report = divergence_report(home) | |
| declared = { | |
| "modified": set(ADAPTED_UPSTREAM_FILES), | |
| "removed": set(), | |
| "added": set(ADDED_UPSTREAM_FILES), | |
| } | |
| undeclared = { | |
| kind: sorted(set(report[kind]) - declared[kind]) for kind in ("modified", "removed", "added") | |
| } | |
| missing = { | |
| kind: sorted(declared[kind] - set(report[kind])) for kind in ("modified", "removed", "added") | |
| } | |
| problems = [] | |
| for kind, names in undeclared.items(): | |
| if names: | |
| problems.append(f"undeclared {kind}: {', '.join(names)}") | |
| for kind, names in missing.items(): | |
| if names: | |
| problems.append(f"declared but not actually {kind}: {', '.join(names)}") | |
| if problems: | |
| raise RuntimeError( | |
| "Upstream divergence does not match the declared contract; refusing to " | |
| "attest a tree this deployment does not describe: " + "; ".join(problems) | |
| ) | |
| destination = home / REFERENCE_DIR / DIVERGENCE_FILE | |
| destination.parent.mkdir(parents=True, exist_ok=True) | |
| destination.write_text(json.dumps(report, indent=1, sort_keys=True) + "\n", encoding="utf-8") | |
| return report | |
| def read_divergence(home) -> dict: | |
| """Read the report published by verify_divergence() during the boot.""" | |
| from pathlib import Path | |
| try: | |
| value = json.loads( | |
| (Path(home) / REFERENCE_DIR / DIVERGENCE_FILE).read_text(encoding="utf-8") | |
| ) | |
| except (OSError, ValueError): | |
| return {} | |
| return value if isinstance(value, dict) else {} | |
| def exit_report_path(home, bot_username: str): | |
| """Where a runner records why it died. | |
| Deliberately outside the pinned checkout: the divergence check compares that | |
| tree against the upstream commit, so a runtime file written inside it would | |
| look like an undeclared addition and fail the next boot. Each bot gets its | |
| own file because both runners share one source root. | |
| """ | |
| from pathlib import Path | |
| return Path(home).resolve().parent / f"last-exit-{bot_username}.json" | |
| def read_exit_report(home, bot_username: str) -> dict: | |
| """Read the last recorded runner failure, if any. | |
| Measured need: Gemini_Bot reached restart_count 9 with the only visible | |
| symptom being 'runner=1'. The HF log stream served a frozen window and the app | |
| routes no runner log, so the cause was invisible from outside. | |
| """ | |
| try: | |
| value = json.loads( | |
| exit_report_path(home, bot_username).read_text(encoding="utf-8") | |
| ) | |
| except (OSError, ValueError): | |
| return {} | |
| return value if isinstance(value, dict) else {} | |
| # Anything matching these is replaced before a traceback is written out. The | |
| # report is meant to be read from the status endpoint, so it must not be a way | |
| # to leak a credential. | |
| SECRET_PATTERNS = ( | |
| r"AIza[0-9A-Za-z_\-]{10,}", | |
| r"hf_[0-9A-Za-z]{10,}", | |
| r"sk-[0-9A-Za-z]{10,}", | |
| ) | |
| def _redact(text: str) -> str: | |
| import re | |
| for pattern in SECRET_PATTERNS: | |
| text = re.sub(pattern, "<redacted>", text) | |
| return text | |
| def record_exit(home, bot_username: str, exc: BaseException) -> dict: | |
| """Publish the failure that ended a runner, so the next restart is diagnosable. | |
| Lives here rather than in voyager_runner.py so it can be exercised directly: | |
| that module imports the pinned upstream package, which a unit test should not | |
| need. Returns the record it wrote, or {} if it could not write one. | |
| """ | |
| import traceback | |
| frames = [ | |
| line for line in traceback.format_exception(type(exc), exc, exc.__traceback__) if line.strip() | |
| ] | |
| record = { | |
| "bot_username": bot_username, | |
| "at": time.time(), | |
| "pid": os.getpid(), | |
| "error": _redact(f"{type(exc).__name__}: {exc}"), | |
| "traceback": [_redact(line.rstrip()) for line in frames[-30:]], | |
| } | |
| try: | |
| path = exit_report_path(home, bot_username) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_text(json.dumps(record, indent=1) + "\n", encoding="utf-8") | |
| return record | |
| except OSError as write_error: | |
| # Never mask the original failure. | |
| print(f"[voyager] could not write the exit report: {write_error}", flush=True) | |
| return {} | |
| # The single declared source adaptation. Kept here so the boot patcher and the | |
| # attestation compare against the same text. | |
| CURRICULUM_RETRY_MARKER = " messages=messages,\n max_retries=max_retries - 1," | |
| CURRICULUM_RETRY_ADAPTATION = ( | |
| ' messages=messages + [\n' | |
| ' AIMessage(content=curriculum[:1200]),\n' | |
| ' HumanMessage(content=(\n' | |
| ' f"The previous proposal was rejected: {e}. "\n' | |
| ' "Propose a different objective in the required format."\n' | |
| ' )),\n' | |
| ' ],\n' | |
| ' max_retries=max_retries - 1,' | |
| ) | |
| def apply_curriculum_retry_adaptation(text: str) -> str: | |
| if text.count(CURRICULUM_RETRY_MARKER) != 1: | |
| raise RuntimeError("Voyager curriculum retry marker changed") | |
| return text.replace(CURRICULUM_RETRY_MARKER, CURRICULUM_RETRY_ADAPTATION, 1) | |
| def _reference_source(home: Path, relative: str) -> str: | |
| """Read the pristine upstream file snapshotted before any boot patch.""" | |
| reference = home / REFERENCE_DIR / relative | |
| if not reference.exists(): | |
| raise RuntimeError( | |
| f"Missing pristine upstream snapshot for {relative}; cannot attest the runtime" | |
| ) | |
| return reference.read_text(encoding="utf-8") | |
| def _expected_source(home: Path, relative: str) -> str: | |
| reference = home / EXPECTED_DIR / relative | |
| if not reference.exists(): | |
| raise RuntimeError( | |
| f"Missing adapted upstream reference for {relative}; cannot attest the runtime" | |
| ) | |
| return reference.read_text(encoding="utf-8") | |
| def _class_method(text: str, class_name: str, method_name: str) -> str | None: | |
| """Extract one method's source from a module's text.""" | |
| import ast | |
| try: | |
| tree = ast.parse(text) | |
| except SyntaxError: | |
| return None | |
| for node in ast.walk(tree): | |
| if isinstance(node, ast.ClassDef) and node.name == class_name: | |
| for child in node.body: | |
| if isinstance(child, ast.FunctionDef) and child.name == method_name: | |
| return ast.get_source_segment(text, child) or "" | |
| return None | |
| def _method_source(cls, method_name: str) -> str | None: | |
| """Read the live method source from the file the class was loaded from.""" | |
| try: | |
| return _class_method( | |
| Path(inspect.getfile(cls)).read_text(encoding="utf-8"), cls.__name__, method_name | |
| ) | |
| except (OSError, TypeError): | |
| return None | |
| def _reference_method( | |
| home: Path, relative: str, class_name: str, method_name: str, expected: bool = False | |
| ) -> str | None: | |
| reader = _expected_source if expected else _reference_source | |
| return _class_method(reader(home, relative), class_name, method_name) | |
| def attest_original_runtime(voyager, home: Path) -> dict: | |
| """Fail before learning if the engine or any of its agents was swapped.""" | |
| home = Path(home).resolve() | |
| actual = subprocess.check_output( | |
| ["git", "-C", str(home), "rev-parse", "HEAD"], | |
| stderr=subprocess.DEVNULL, timeout=10, text=True, | |
| ).strip() | |
| if actual != UPSTREAM_COMMIT: | |
| raise RuntimeError(f"Unexpected Voyager upstream checkout: {actual}") | |
| components = { | |
| "engine": (voyager, "voyager/voyager.py", "Voyager"), | |
| "curriculum": (voyager.curriculum_agent, "voyager/agents/curriculum.py", "CurriculumAgent"), | |
| "action": (voyager.action_agent, "voyager/agents/action.py", "ActionAgent"), | |
| "critic": (voyager.critic_agent, "voyager/agents/critic.py", "CriticAgent"), | |
| "skill_library": (voyager.skill_manager, "voyager/agents/skill.py", "SkillManager"), | |
| } | |
| wrappers: dict[str, str] = {} | |
| for role, (component, path, class_name) in components.items(): | |
| declaration = DECLARED_WRAPPERS.get(role) | |
| if declaration is not None: | |
| live_name = type(component).__name__ | |
| if live_name == declaration["class_name"]: | |
| delegate = getattr(component, declaration["delegate_attribute"], None) | |
| if delegate is None: | |
| raise RuntimeError( | |
| f"Voyager {role} is wrapped by {live_name} but exposes no " | |
| f"{declaration['delegate_attribute']!r} delegate; refusing to " | |
| "attest a decision method that cannot be reached" | |
| ) | |
| component = delegate | |
| wrappers[f"{role}.{live_name}"] = declaration["reason"] | |
| elif live_name != class_name: | |
| raise RuntimeError( | |
| f"Voyager {role} is {live_name}, which is neither the pinned " | |
| f"{class_name} nor the declared wrapper {declaration['class_name']}" | |
| ) | |
| cls = type(component) | |
| if cls.__name__ != class_name or Path(inspect.getfile(cls)).resolve() != home / path: | |
| raise RuntimeError(f"Voyager {role} is not the pinned upstream class") | |
| for method_name in DECISION_METHODS.get(role, ()): | |
| live = _method_source(cls, method_name) | |
| adapted = (role, method_name) in ADAPTED_METHODS | |
| reference = _reference_method( | |
| home, path, class_name, method_name, expected=adapted | |
| ) | |
| if live is None or reference is None: | |
| raise RuntimeError( | |
| f"Cannot read Voyager {role}.{method_name} for attestation" | |
| ) | |
| if live.strip() != reference.strip(): | |
| raise RuntimeError( | |
| f"Voyager {role}.{method_name} differs from the pinned upstream " | |
| f"implementation; refusing to learn with a substituted decision method" | |
| ) | |
| return { | |
| "engine": "MineDojo/Voyager", | |
| "upstream_commit": actual, | |
| "source_root": str(home), | |
| "checkout_verified": True, | |
| "decision_methods_verified": sorted( | |
| f"{role}.{name}" for role, names in DECISION_METHODS.items() for name in names | |
| ), | |
| "declared_adaptations": { | |
| f"{role}.{name}": reason for (role, name), reason in ADAPTED_METHODS.items() | |
| }, | |
| "declared_wrappers": wrappers, | |
| "components": list(components), | |
| "creative_adapters": True, | |
| } | |
| def write_attestation(path: str, attestation: dict, bot: str) -> None: | |
| if not path: | |
| return | |
| destination = Path(path) | |
| destination.parent.mkdir(parents=True, exist_ok=True) | |
| record = {**attestation, "bot_username": bot, "pid": os.getpid()} | |
| temporary = destination.with_suffix(destination.suffix + ".tmp") | |
| temporary.write_text(json.dumps(record, sort_keys=True) + "\n", encoding="utf-8") | |
| temporary.replace(destination) | |
| def read_attestation(path: Path, pid: int | None) -> dict | None: | |
| if pid is None: | |
| return None | |
| try: | |
| record = json.loads(Path(path).read_text(encoding="utf-8")) | |
| except (OSError, ValueError): | |
| return None | |
| return record if record.get("pid") == pid and record.get("checkout_verified") else None | |