Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ coworld/evidence/
!Dockerfile
!.dockerignore
coworld/**/__pycache__/
!coworld/gota/conductor/.contracts/
!coworld/gota/conductor/.contracts/**

# Python conversion helpers.
__pycache__/
Expand Down
23 changes: 23 additions & 0 deletions coworld/gota/conductor/.contracts/001-bounded-policies/CONTRACT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
title: Bounded embedded policies
description: Run GOTA policies in the native game with fixed formats, budgets and public outputs.
---

# Bounded embedded policies

## Rules

- Accept ten BASIC source policies with the declared seat order and byte limit. Do not execute uploaded native code.
- Keep per-tick decisions inside the game. Disable external model and oracle calls for this runtime.
- Use the existing engine and BASIC VM. Configuration cannot supply executable commands or image references.
- Enforce the engine tick limit, host wall-clock budget and outer attempt deadline. Stop the process group on failure.
- Publish scores and the compressed public replay only after the engine stops and output validation succeeds.
- Missing results, compilation failures and execution timeouts fail the attempt. Wait for the atomic result marker
and terminate the remaining server before publishing outputs.
- Keep source programs, private logs and credentials out of the replay and result envelope.
- Build game and runner independently. Pin both images by digest for hosted qualification.

## Checks

Run this directory's `check.py` against the composed runner image. It exercises ten actual policies, a complete native
game, scores and compressed replay integrity. Inspect failure cleanup and qualify hosted resource isolation separately.
3 changes: 3 additions & 0 deletions coworld/gota/conductor/.contracts/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# GOTA execution contracts

- [Bounded embedded policies](001-bounded-policies/CONTRACT.md).
4 changes: 4 additions & 0 deletions coworld/gota/conductor/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# GOTA in Conductor

Read the [local contracts](.contracts/index.md) and [operating guide](README.md).
Keep game decisions in the native engine and infrastructure protocol translation in the host.
13 changes: 13 additions & 0 deletions coworld/gota/conductor/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Build the game and runner artifacts independently, then compose the approved pair.
ARG RUNNER_IMAGE
ARG GAME_IMAGE
FROM ${RUNNER_IMAGE} AS runner
FROM ${GAME_IMAGE}
USER root
RUN apt-get update && apt-get install -y --no-install-recommends python3 && rm -rf /var/lib/apt/lists/*
COPY --from=runner /usr/local/bin/node /usr/local/bin/node
COPY --from=runner /opt/worlds/server.mjs /opt/worlds/server.mjs
COPY coworld/gota/conductor/host.py /opt/gota/host.py
RUN useradd --system --create-home world
USER world
ENTRYPOINT ["node", "/opt/worlds/server.mjs"]
19 changes: 19 additions & 0 deletions coworld/gota/conductor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# GotA in Universe

The game executes ten BASIC policies inside its existing native VM loop. Conductor owns the enclosing process,
deadline and result transport. A policy gets its own hero's observation and cannot access another policy's source.

The [host](host.py) accepts Conductor process input, stages the existing Coworld file protocol, and publishes the
result and compressed replay after the native process stops. It accepts raw UTF-8 BASIC up to 1 MiB per seat.
LLM network calls are disabled for this fast profile. The game must finish within 60 seconds of host startup and
within the attempt's outer deadline. A slow interactive coaching variant needs a separate, qualified runtime.

Build the game with `coworld/Dockerfile`, using `GAME=gods_of_the_arena` and `ENTRY=gota`. Build Conductor's runner
artifact independently. Compose them with this directory's Dockerfile, passing `GAME_IMAGE` and `RUNNER_IMAGE`.
Hosted preparation must pin both images by digest. Local development may use explicit local image tags.

Register command `["python3", "/opt/gota/host.py"]`, policy format `basic-source-v1`, and a 60-second outer
budget (including allocation). Publish [world.json](world.json) using the installed runtime's immutable name.
Seat IDs are `seat-0` through `seat-9`; the first five are red and the last five blue.

Private policy logs remain inside the attempt. The public replay excludes policy source and PRINT output.
121 changes: 121 additions & 0 deletions coworld/gota/conductor/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Qualify a native GotA game, replay publication, and the outer execution budget."""

import argparse
import gzip
import hashlib
import json
import secrets
import subprocess
import time
import uuid
from pathlib import Path
from urllib.error import URLError
from urllib.request import Request, urlopen


def docker(*args):
return subprocess.check_output(["docker", *args], text=True).strip()


def main():
parser = argparse.ArgumentParser()
parser.add_argument("image")
args = parser.parse_args()
token = secrets.token_hex(32)
container = docker(
"run",
"--detach",
"--rm",
"--name",
"gota-participation-" + uuid.uuid4().hex[:10],
"--label",
"owner=gota-participation-qualification",
"--platform",
"linux/amd64",
"--cap-drop=ALL",
"--security-opt",
"no-new-privileges",
"--memory",
"1g",
"--cpus",
"2",
"-p",
"127.0.0.1::8080",
"-e",
"RUNNER_TOKEN=" + token,
"-e",
"WORLD_RUNTIME=gota",
"-e",
'WORLD_COMMAND=["python3","/opt/gota/host.py"]',
args.image,
)
boot = None
try:
origin = "http://127.0.0.1:" + docker("port", container, "8080/tcp").rsplit(":", 1)[1]

def call(path, body=None, method=None, raw=False):
headers = {"Authorization": "Bearer " + token}
if boot:
headers["x-runner-boot"] = boot
if isinstance(body, dict):
body = json.dumps(body).encode()
headers["Content-Type"] = "application/json"
with urlopen(Request(origin + path, data=body, headers=headers, method=method), timeout=10) as response:
data = response.read()
return data if raw or not data else json.loads(data)

for _ in range(100):
try:
boot = call("/health")["bootId"]
break
except (URLError, ConnectionError):
time.sleep(0.1)
assert boot, "Runner did not start"
policies = []
for slot in range(10):
file = Path(__file__).parent.parent / "players" / ("rusher.bas" if slot < 5 else "puller.bas")
source = file.read_bytes()
digest = hashlib.sha256(source).hexdigest()
call("/inputs/" + digest, source, "PUT")
policies.append(
{"role": f"seat-{slot}", "format": "basic-source-v1", "blob": {"digest": digest, "bytes": len(source)}}
)
attempt = secrets.token_hex(32)
path = "/attempts/" + attempt
started = time.monotonic()
call(
path,
{
"protocol": 1,
"id": attempt,
"runId": attempt,
"runtime": "gota",
"deadlineAt": int(time.time() * 1000) + 120_000,
"seed": 0,
"policies": policies,
"config": {"max_ticks": 28800},
},
"PUT",
)
while True:
result = call(path)
if result["status"] not in ("running", "queued"):
break
assert time.monotonic() - started < 70, "Native game exceeded its 60 second budget"
time.sleep(0.2)
assert result["status"] == "succeeded", result
assert len(result["result"]["scores"]) == 10, result
replay = call(path + "/outputs/replay", raw=True)
decoded = gzip.decompress(replay)
assert len(decoded) > 1000, "Replay was empty"
assert hashlib.sha256(replay).hexdigest() == result["outputs"][0]["digest"]
print(
f"Qualified: ten embedded policies, complete game, hashed replay; "
f"{time.monotonic() - started:.2f}s, {len(replay)} replay bytes."
)
finally:
subprocess.run(["docker", "stop", "--time", "5", container], check=False, stdout=subprocess.DEVNULL)


if __name__ == "__main__":
main()
115 changes: 115 additions & 0 deletions coworld/gota/conductor/host.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""Run the native GotA engine through Conductor's process protocol."""

import gzip
import hashlib
import json
import os
import secrets
import subprocess
import time
from pathlib import Path


def run():
root = Path(os.environ["WORLD_SCRATCH_PATH"])
request = json.loads(Path(os.environ["WORLD_INPUT_PATH"]).read_text())
policies = {policy["role"]: policy for policy in request["policies"]}
roles = [f"seat-{slot}" for slot in range(10)]
if request.get("seats") or set(policies) != set(roles):
raise ValueError("GotA requires ten embedded seats")
supplied = request["config"]
if set(supplied) - {"max_ticks", "draft_mode"}:
raise ValueError("Unknown GotA configuration")
ticks = supplied.get("max_ticks", 28_800)
if type(ticks) is not int or not 1 <= ticks <= 28_800:
raise ValueError("Invalid tick budget")
draft_mode = supplied.get("draft_mode", "open")
if draft_mode not in ("open", "team"):
raise ValueError("Invalid draft mode")
seed = request["seed"]
if seed >= 2**31:
seed -= 2**32
config = {"seed": seed, "max_ticks": ticks, "draft_mode": draft_mode, "tokens": [], "players": []}
seats = []
for slot, role in enumerate(roles):
policy = policies[role]
if policy["format"] != "basic-source-v1":
raise ValueError("Unsupported policy format")
source = Path(policy["path"])
if source.stat().st_size > 1024 * 1024:
raise ValueError("Policy is too large")
body = source.read_bytes()
body.decode("utf-8")
config["tokens"].append(secrets.token_hex(24))
config["players"].append({"name": role})
seats.append(
{
"slot": slot,
"file_uri": source.as_uri(),
"content_hash": hashlib.sha256(body).hexdigest(),
"size_bytes": len(body),
"log_uri": (root / f"private-{slot}.log").as_uri(),
"artifact_uri": (root / f"private-{slot}.zip").as_uri(),
}
)
files = {
"CONFIG": ("game-config.json", config),
"PLAYER_SEATS": ("game-seats.json", {"schema": "coworld-player-seats/1", "seats": seats}),
}
environment = {
"PATH": "/usr/local/bin:/usr/bin:/bin",
"HOME": str(root),
"COGAME_LLM": "off",
"COGAME_ORACLE": "off",
"COGAME_HOST": "127.0.0.1",
"COGAME_PORT": "8081",
}
for key, (name, value) in files.items():
path = root / name
path.write_text(json.dumps(value))
environment[f"COGAME_{key}_URI"] = path.as_uri()
for key, name in (
("RESULTS", "game-result.json"),
("SAVE_REPLAY", "game-replay"),
("PLAYER_FAILURE", "game-failure.json"),
):
environment[f"COGAME_{key}_URI"] = (root / name).as_uri()
deadline = min(request["deadlineAt"] / 1000, time.time() + 60)
child = subprocess.Popen(
["/usr/local/bin/polyworld"], env=environment, cwd=root, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
try:
while not (root / "game-result.json").exists():
if (root / "game-failure.json").exists():
raise RuntimeError("Policy execution failed")
if child.poll() is not None:
raise RuntimeError("Game exited before publishing its result")
if time.time() >= deadline:
raise TimeoutError("GotA exceeded its execution budget")
time.sleep(0.02)
finally:
if child.poll() is None:
child.terminate()
try:
child.wait(timeout=2)
except subprocess.TimeoutExpired:
child.kill()
child.wait()
result = json.loads((root / "game-result.json").read_text())
if len(result.get("scores", [])) != 10:
raise ValueError("Invalid GotA result")
with (root / "game-replay").open("rb") as source, gzip.open(root / "replay.gz", "wb") as target:
while block := source.read(64 * 1024):
target.write(block)
output = {
"result": result,
"artifacts": [{"name": "replay", "path": "replay.gz", "mediaType": "application/gzip"}],
}
path = Path(os.environ["WORLD_OUTPUT_PATH"])
temporary = path.with_suffix(".pending")
temporary.write_text(json.dumps(output))
temporary.replace(path)


if __name__ == "__main__":
run()
56 changes: 56 additions & 0 deletions coworld/gota/conductor/world.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "gota",
"runtime": "gota-local",
"roles": [
{
"id": "seat-0",
"policyFormat": "basic-source-v1",
"control": "embedded"
},
{
"id": "seat-1",
"policyFormat": "basic-source-v1",
"control": "embedded"
},
{
"id": "seat-2",
"policyFormat": "basic-source-v1",
"control": "embedded"
},
{
"id": "seat-3",
"policyFormat": "basic-source-v1",
"control": "embedded"
},
{
"id": "seat-4",
"policyFormat": "basic-source-v1",
"control": "embedded"
},
{
"id": "seat-5",
"policyFormat": "basic-source-v1",
"control": "embedded"
},
{
"id": "seat-6",
"policyFormat": "basic-source-v1",
"control": "embedded"
},
{
"id": "seat-7",
"policyFormat": "basic-source-v1",
"control": "embedded"
},
{
"id": "seat-8",
"policyFormat": "basic-source-v1",
"control": "embedded"
},
{
"id": "seat-9",
"policyFormat": "basic-source-v1",
"control": "embedded"
}
]
}
Loading