Add opt-in heavy WASM budget tier

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
2026-11-03 00:20:54 +03:00
parent b0085cb3e8
commit 4dddea2e8c
3 changed files with 410 additions and 3 deletions
+27
View File
@@ -0,0 +1,27 @@
# Opt-in heavy sandbox runtime tier; separate from the routine WASM contract.
name: wasm-budget
on:
workflow_dispatch:
env:
CARGO_BUILD_JOBS: "1"
CARGO_INCREMENTAL: "0"
jobs:
budget:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Rust
run: |
curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --target wasm32-unknown-unknown
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Actual WASM budget scenario
run: nice -n 19 python3 tools/airdef.py test-budgets
- name: Preserve machine-readable provenance
if: always()
uses: actions/upload-artifact@v4
with:
name: wasm-budget-provenance
path: .horizon-cache/wasm-budget-report.json
if-no-files-found: warn
+278
View File
@@ -0,0 +1,278 @@
# Opt-in heavy contract scenario. Run through `test-budgets` so the routine
# `test-wasm` remains quick. This drives the real .hmod in Horizon's sandbox;
# no GPU or rendered-frame correctness is implied by missing query replies.
[[step]]
ticks = 2
frames = 2
expect.commands = ["airdef"]
[[step]]
command = "airdef deploy s400"
expect.feedback = "С-400 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy patriot"
expect.feedback = "Patriot развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy pantsir"
expect.feedback = "Панцирь-С1 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy gepard"
expect.feedback = "Гепард развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy nasams"
expect.feedback = "NASAMS развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy irist"
expect.feedback = "IRIS-T SLM развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy tor"
expect.feedback = "Тор-М2 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy s400"
expect.feedback = "С-400 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy patriot"
expect.feedback = "Patriot развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy pantsir"
expect.feedback = "Панцирь-С1 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy gepard"
expect.feedback = "Гепард развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy nasams"
expect.feedback = "NASAMS развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy irist"
expect.feedback = "IRIS-T SLM развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy tor"
expect.feedback = "Тор-М2 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy s400"
expect.feedback = "С-400 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy patriot"
expect.feedback = "Patriot развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy pantsir"
expect.feedback = "Панцирь-С1 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy gepard"
expect.feedback = "Гепард развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy nasams"
expect.feedback = "NASAMS развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy irist"
expect.feedback = "IRIS-T SLM развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy tor"
expect.feedback = "Тор-М2 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy s400"
expect.feedback = "С-400 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy patriot"
expect.feedback = "Patriot развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy pantsir"
expect.feedback = "Панцирь-С1 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy gepard"
expect.feedback = "Гепард развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy nasams"
expect.feedback = "NASAMS развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy irist"
expect.feedback = "IRIS-T SLM развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy tor"
expect.feedback = "Тор-М2 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy s400"
expect.feedback = "С-400 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy patriot"
expect.feedback = "Patriot развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy pantsir"
expect.feedback = "Панцирь-С1 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy gepard"
expect.feedback = "Гепард развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy nasams"
expect.feedback = "NASAMS развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy irist"
expect.feedback = "IRIS-T SLM развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy tor"
expect.feedback = "Тор-М2 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy s400"
expect.feedback = "С-400 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy patriot"
expect.feedback = "Patriot развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy pantsir"
expect.feedback = "Панцирь-С1 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy gepard"
expect.feedback = "Гепард развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy nasams"
expect.feedback = "NASAMS развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy irist"
expect.feedback = "IRIS-T SLM развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy tor"
expect.feedback = "Тор-М2 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy s400"
expect.feedback = "С-400 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy patriot"
expect.feedback = "Patriot развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy pantsir"
expect.feedback = "Панцирь-С1 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy gepard"
expect.feedback = "Гепард развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy nasams"
expect.feedback = "NASAMS развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy irist"
expect.feedback = "IRIS-T SLM развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy tor"
expect.feedback = "Тор-М2 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy s400"
expect.feedback = "С-400 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy patriot"
expect.feedback = "Patriot развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy pantsir"
expect.feedback = "Панцирь-С1 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy gepard"
expect.feedback = "Гепард развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy nasams"
expect.feedback = "NASAMS развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy irist"
expect.feedback = "IRIS-T SLM развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy tor"
expect.feedback = "Тор-М2 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy s400"
expect.feedback = "С-400 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy patriot"
expect.feedback = "Patriot развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy pantsir"
expect.feedback = "Панцирь-С1 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy gepard"
expect.feedback = "Гепард развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy nasams"
expect.feedback = "NASAMS развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy irist"
expect.feedback = "IRIS-T SLM развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy tor"
expect.feedback = "Тор-М2 развёрнут (integrated, multi)"
[[step]]
command = "airdef deploy s400"
expect.feedback = "С-400 развёрнут (integrated, multi)"
# Two maximum supported waves are issued before simulation advances, so
# their 160 airborne units overlap while 64 defensive systems track them.
[[step]]
command = "airdef wave mix 80 4000"
[[step]]
command = "airdef wave kh101 80 4000"
[[step]]
ticks = 600
frames = 300
[[step]]
command = "airdef clear"
+105 -3
View File
@@ -9,6 +9,7 @@
python3 tools/airdef.py all # всё по порядку: fork, game, mod, install
python3 tools/airdef.py check # совпадают ли форк, упаковщик, уровень API мода и ресурсы
python3 tools/airdef.py test-wasm # собрать и прогнать настоящий .hmod в песочнице без GPU
python3 tools/airdef.py test-budgets # тяжёлый WASM budget сценарий + JSON provenance
Переменные окружения: HORIZON_HOME (папка игры), HORIZON_RESOURCES (подготовленные ресурсы
Minecraft 26.3), AIRDEF_GAME (готовый horizon-client вместо собранного).
@@ -22,6 +23,7 @@ import re
import shutil
import subprocess
import sys
import tempfile
import zipfile
from pathlib import Path
@@ -132,8 +134,8 @@ def mod():
"engine": engine_identity(), "engine_pin": engine_commit(),
"wit_sha256": hashlib.sha256((FORK / "crates/horizon-mod-sdk/wit/horizon.wit").read_bytes()).hexdigest(),
"artifact_sha256": hashlib.sha256(artifact.read_bytes()).hexdigest(),
"airdef_dirty": bool(subprocess.check_output(["git", "status", "--porcelain", "--untracked-files=no"], cwd=ROOT)),
"airdef_diff_sha256": hashlib.sha256(subprocess.check_output(["git", "diff", "--binary", "HEAD"], cwd=ROOT)).hexdigest()}
"airdef_dirty": bool(subprocess.check_output(["git", "-c", "submodule.horizon.ignore=all", "status", "--porcelain", "--untracked-files=no"], cwd=ROOT)),
"airdef_diff_sha256": hashlib.sha256(subprocess.check_output(["git", "-c", "submodule.horizon.ignore=all", "diff", "--binary", "HEAD"], cwd=ROOT)).hexdigest()}
artifact.with_name("build-identity.json").write_text(json.dumps(identity, indent=2) + "\n")
@@ -155,6 +157,106 @@ def test_wasm():
cwd=ROOT / "airdef-mod", env=build_env)
def test_budgets():
"""Run the opt-in heavy scenario and always write a machine-readable report."""
report_path = Path(os.environ.get("AIRDEF_BUDGET_REPORT") or
ROOT / ".horizon-cache" / "wasm-budget-report.json")
report = {
"schema": 1,
"command": "test-budgets",
"scenario": "tests/budgets/wasm_budget.toml",
"status": "failed",
"exit_code": None,
"scenario_counts": {"scripts": 1, "steps": None, "raids": 2,
"max_units_per_raid": 80, "raid_units": 160,
"air_defense_units": None, "layout_unit_limit": None,
"frames": 300, "ticks": 602},
"runtime_limits": {"source": "horizon/crates/horizon-mods/src/runtime.rs",
"metering": "source_configured; measured_per_scenario: unavailable"},
}
def git(*args, text=True):
return subprocess.check_output(["git", "-c", "submodule.horizon.ignore=all", *args], cwd=ROOT,
text=text, stderr=subprocess.DEVNULL)
try:
report["airdef"] = {
"sha": git("rev-parse", "HEAD").strip(),
"dirty": bool(git("status", "--porcelain", "--untracked-files=no").strip()),
"diff_sha256": hashlib.sha256(git("diff", "--binary", "HEAD", text=False)).hexdigest(),
}
report["engine"] = {"pin": engine_commit(), "identity": engine_identity()}
wit = FORK / "crates" / "horizon-mod-sdk" / "wit" / "horizon.wit"
report["api_level"] = fork_api()[0] if fork_api() else None
report["wit_sha256"] = hashlib.sha256(wit.read_bytes()).hexdigest() if wit.is_file() else None
runtime = (FORK / "crates" / "horizon-mods" / "src" / "runtime.rs").read_text()
layout = (ROOT / "airdef-mod" / "src" / "layout.rs").read_text()
limit = re.search(r"pub const MAX_UNITS: usize = (\d+);", layout)
report["scenario_counts"]["layout_unit_limit"] = int(limit.group(1)) if limit else None
memory = re.search(r"const MEMORY: usize = (\d+) << (\d+);", runtime)
report["runtime_limits"].update({
"memory_bytes": int(memory.group(1)) << int(memory.group(2)) if memory else None,
"fuel_per_call": {name.lower(): int(value) for name, value in
re.findall(r"const FUEL_(INIT|TICK|FRAME|EVENT): u64 = ([\d_]+);", runtime)
for value in [value.replace("_", "")]},
"wasmtime_component_model": bool(re.search(r"wasm_component_model\(true\)", runtime)),
"wasmtime_fuel_metering": bool(re.search(r"consume_fuel\(true\)", runtime)),
})
lock_text = (ROOT / "horizon" / "Cargo.lock").read_text()
wasmtime = re.search(r'\[\[package\]\]\s*name = "wasmtime"\s*version = "([^"]+)"', lock_text)
report["wasmtime_version"] = wasmtime.group(1) if wasmtime else None
fixture = ROOT / "airdef-mod" / "tests" / "budgets" / "wasm_budget.toml"
report["scenario_counts"]["steps"] = len(re.findall(r"(?m)^\[\[step\]\]$", fixture.read_text()))
report["scenario_counts"]["air_defense_units"] = len(re.findall(r'(?m)^command = "airdef deploy [^"]+"', fixture.read_text()))
report["scenario_sha256"] = hashlib.sha256(fixture.read_bytes()).hexdigest()
mod()
artifact = hmod()
report["artifact_sha256"] = hashlib.sha256(artifact.read_bytes()).hexdigest()
identity_path = artifact.with_name("build-identity.json")
identity = json.loads(identity_path.read_text())
identity_checks = {
"engine_pin": identity.get("engine_pin") == report["engine"]["pin"],
"engine_identity": identity.get("engine") == report["engine"]["identity"],
"airdef_sha": identity.get("airdef") == report["airdef"]["sha"],
"airdef_dirty": identity.get("airdef_dirty") == report["airdef"]["dirty"],
"airdef_diff_sha256": identity.get("airdef_diff_sha256") == report["airdef"]["diff_sha256"],
"artifact_sha256": identity.get("artifact_sha256") == report["artifact_sha256"],
"wit_sha256": identity.get("wit_sha256") == report["wit_sha256"],
}
report["build_identity"] = {"path": str(identity_path), "checks": identity_checks,
"matches": all(identity_checks.values())}
if not report["build_identity"]["matches"]:
raise RuntimeError("build identity is stale or does not match the tested artifact: " +
", ".join(k for k, ok in identity_checks.items() if not ok))
build_env = dict(os.environ)
build_env["PATH"] = str(Path(cargo()).parent) + os.pathsep + build_env.get("PATH", "")
fixture = ROOT / "airdef-mod" / "tests" / "budgets" / "wasm_budget.toml"
with tempfile.TemporaryDirectory(prefix="airdef-wasm-budget-") as temp:
isolated = Path(temp)
(isolated / "tests").mkdir()
shutil.copy2(fixture, isolated / "tests" / "wasm_budget.toml")
command = [cargo(), "run", "--locked", "--manifest-path", FORK / "Cargo.toml",
"-p", "cargo-horizon-mod", "--", "horizon-mod", "test", "--hmod", artifact, "budget"]
print("+", " ".join(str(c) for c in command), flush=True)
result = subprocess.run([str(c) for c in command], cwd=isolated, env=build_env,
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
print(result.stdout, end="", flush=True)
report["exit_code"] = result.returncode
report["status"] = "passed" if result.returncode == 0 else "failed"
report["output"] = result.stdout[-12000:]
except Exception as exc:
report["error"] = f"{type(exc).__name__}: {exc}"
except SystemExit as exc:
report["error"] = f"SystemExit: {exc.code}"
finally:
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n")
print(f"Budget report: {report_path}", flush=True)
if report["status"] != "passed":
sys.exit(f"WASM budget scenario failed; see {report_path}")
def install():
home = game_home()
mods = home / "horizon-mods"
@@ -298,7 +400,7 @@ def main():
if len(sys.argv) < 2:
sys.exit(__doc__)
cmd, rest = sys.argv[1], sys.argv[2:]
steps = {"fork": fork, "game": game, "mod": mod, "install": install, "check": check, "test-wasm": test_wasm}
steps = {"fork": fork, "game": game, "mod": mod, "install": install, "check": check, "test-wasm": test_wasm, "test-budgets": test_budgets}
if cmd == "play":
play(rest)
elif cmd == "all":