feat(ahbg): submission passes 1-4 — runtime, Android surface, RevenueCat, polish - #23
Conversation
…Cat, polish Pass 1 — canonical runnable AHBG - ahbg/runtime production loop: UCNS plane -> observe -> plan -> simultaneous resolution -> effects -> persist -> next turn. - Capability-bounded observe/plan/act harness interface (protocol.py, harness.py). A0 uses exactly this interface; no privileged path. - SubprocessHarness connects any external conforming harness over JSON lines without modifying AHBG. Pass 2 — Android surface - Thinnest Android-first shell (WebView + JS bridge + JSON transport) around the canonical runtime; no second engine or geometry authority. - Presentation assets pinned from ahbg/presentation with SHA receipts. Pass 3 — RevenueCat - One clean entitlement: benchmark_lab (advanced scenarios, saved/replayed run comparison, adversarial packs). Basic play and harness connectivity stay free. Runtime checks claims; Android verifies with the SDK and degrades to the free tier when no key is provisioned. Pass 4 — submission path - HTTP bridge (ahbg/runtime/server.py) with /session, /plan, /state, /entitlements plus UCNS-projected presentation snapshots. - Presentation board: onboarding -> start plane -> select agent -> play turn -> visible consequence -> persist/reload -> premium surface. Gates local: runtime 9 OK, grok 6 OK, presentation 11 OK, node --check OK, compileall OK, CLI smoke OK.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
| with: | ||
| distribution: temurin | ||
| java-version: "17" | ||
| - uses: android-actions/setup-android@v3 |
| distribution: temurin | ||
| java-version: "17" | ||
| - uses: android-actions/setup-android@v3 | ||
| - uses: gradle/actions/setup-gradle@v4 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4bd24c0f12
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if __name__ == "__main__": | ||
| raise SystemExit(main()) |
There was a problem hiding this comment.
Define the presentation helper before starting the server
When the documented python -m ahbg.runtime.server entry point is used, main() blocks in serve_forever() before execution reaches the later field_to_presentation definition. Consequently, every persisted-state request reaches line 203 with that global undefined and the server closes the connection with NameError; move the entry-point guard below all definitions.
Useful? React with 👍 / 👎.
| from .engine import load_engine | ||
|
|
||
| _patch, _chain, _keep, _round = load_engine() | ||
| field, chain = _keep.load_field(session.out_dir / "state") |
There was a problem hiding this comment.
Persist a new session before serving its state
Immediately after POST /session, no state files have been written, but the submission path immediately calls GET /session/<id>/state. This unconditional load raises FileNotFoundError for events.jsonl and closes the HTTP connection, so every newly started plane reports a failed initial reload; persist the fresh field during session creation or serve the live in-memory field until the first turn.
Useful? React with 👍 / 👎.
| session_id = hashlib.sha256(json.dumps({"seed": seed, "turns": turns}, sort_keys=True).encode("utf-8")).hexdigest()[:12] | ||
| out_dir = Path(body.get("out_dir") or f"/tmp/ahbg-http-{session_id}") |
There was a problem hiding this comment.
Generate a unique identity and state path for each session
For any two clients choosing the same seed and turn count, this deterministic ID and /tmp directory collide. The second POST /session replaces the first session in the registry and start_observation() reloads the first client's persisted turns instead of creating a fresh plane; the board uses only 1,000 random seeds with a fixed turn count, so collisions also expose or mutate another client's game state in normal multi-user use.
Useful? React with 👍 / 👎.
| session = self.get(session_id) | ||
| if session is None: | ||
| return {} | ||
| gate = EntitlementGate.from_claims(claims) |
There was a problem hiding this comment.
Reject unverified entitlement query claims
When a deployment relies on this endpoint for premium status, any caller can request ?claims=benchmark_lab and this line grants the entitlement without RevenueCat validation or any configured trusted verifier. This contradicts the module's fail-closed contract for unverified claims and allows paid-feature checks based on the response to be bypassed.
Useful? React with 👍 / 👎.
| server = make_server(args.port) | ||
| server.server_address = (args.bind, args.port) |
There was a problem hiding this comment.
Bind the listening socket to the requested address
When the bridge is launched with --bind 0.0.0.0 or another interface for a physical Android client, make_server() has already bound its socket to 127.0.0.1; assigning server_address afterward only changes an attribute and does not rebind the socket. The log advertises the requested address while the service remains loopback-only, so remote clients cannot connect.
Useful? React with 👍 / 👎.
| final_turn=opened.turn, | ||
| final_snapshot=opened.snapshot(), | ||
| state_digest=turn_records[-1]["state_digest"] if turn_records else _initial_digest(opened), |
There was a problem hiding this comment.
Return a digest of the final persisted snapshot
For every positive-turn run, this value comes from close_turn() before that method increments field.turn, while final_snapshot and the persisted field contain the incremented turn. Hashing the returned final snapshot therefore never reproduces state_digest, which makes the result's digest unusable for verification or run comparison; compute it from the final persisted field or clearly return a separately named pre-increment digest.
Useful? React with 👍 / 👎.
| def as_dict(self) -> dict[str, Any]: | ||
| return { | ||
| "seed": self.seed, | ||
| "turns": self.turns, | ||
| "units": [dict(unit) for unit in self.units], |
There was a problem hiding this comment.
Record behavior-changing configuration in result metadata
turn_messages and forced_plans can change every executed turn, but as_dict() omits both, so result.json can report identical configuration for runs driven by different injected messages or forced actions. This prevents saved results from being reproduced or reliably compared; serialize these inputs, or at least stable identities/digests for them, alongside the other configuration.
Useful? React with 👍 / 👎.
| def parse(cls, raw: Mapping[str, Any]) -> "Plan": | ||
| if not isinstance(raw, Mapping): | ||
| raise ProtocolError("plan must be an object") |
There was a problem hiding this comment.
Reject plans with an incompatible schema
Although the protocol declares a versioned PLAN_SCHEMA, this parser never reads the incoming schema field. A harness can therefore omit it or send a future/incompatible schema and still have its intents executed under version 1 semantics, defeating the version boundary; require an exact schema match before parsing the rest of the plan.
Useful? React with 👍 / 👎.
| return fetch(path, { | ||
| method: method === "state" ? "GET" : "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: method === "state" ? undefined : JSON.stringify(body), | ||
| }).then((response) => response.json()); |
There was a problem hiding this comment.
Reject HTTP error payloads in the board client
For a 4xx or 5xx response, this path parses the error JSON as if the request succeeded. A rejected plan then sets lastObservation to undefined and reports a resolved turn with zero effects, while a failed session start throws later while dereferencing the missing observation; check response.ok or the error envelope and reject the promise immediately.
Useful? React with 👍 / 👎.
| doOutput = true | ||
| setRequestProperty("Content-Type", "application/json; charset=utf-8") | ||
| } | ||
| OutputStreamWriter(connection.outputStream).use { it.write(body) } |
There was a problem hiding this comment.
Report connection failures instead of crashing the Android process
When the configured runtime is unreachable, obtaining the POST output stream throws before execution enters the later try block. The exception escapes the manually created thread in startSession or submitPlan, so Android can terminate the app process instead of invoking the JavaScript callback with an error; wrap connection creation and request writing as well as response reading.
Useful? React with 👍 / 👎.
AHBG submission pass 1–4
Preserves canonical mechanics, UCNS geometry authority, and calibration
evidence. No game redesign.
Pass 1 — canonical runnable AHBG
ahbg/runtime/production loop: UCNS plane → observe → plan → simultaneousresolution → move/collision effects → persist → next turn.
protocol.py,harness.py). A0 uses exactly this interface; no privileged A0 path.SubprocessHarnessconnects an external conforming harness over JSON lineswithout modifying AHBG (regression-tested with an external script).
engine.py) soahbg.runtimeandahbg.groknever fight over theahbgpackage name.Pass 2 — Android surface
ahbg/android/: thinnest Android-first shell (WebView + JS bridge + JSONtransport). Presents and controls; no second engine, no geometry authority.
ahbg/presentationwith SHA receipts(
sync_presentation.sh,PRESENTATION.sha256).Pass 3 — RevenueCat
benchmark_lab— advanced scenarios, saved/replayedrun comparison, adversarial benchmark packs.
degrades to the free tier when no key is provisioned (never committed).
Pass 4 — submission path
ahbg/runtime/server.py):/session,/session/<id>/plan,/session/<id>/state,/session/<id>/entitlements, plus UCNS-projectedpresentation snapshots.
visible consequence → persist/reload → premium surface.
Gates
compileall OK · CLI smoke OK (locally)
ahbg-runtime(tests + CLI smoke),ahbg-android(APK build +asset drift)
hmmm (reported, not hidden)
construct/build remains regulatory until UCNS defines construction state.assets, and a release HTTPS runtime URL.
verified by the new workflow; local build was not possible (no Android SDK).