Skip to content
Merged
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
13 changes: 11 additions & 2 deletions ahbg/android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,19 @@ canonical runtime (ahbg/runtime + frozen Grok engine + UCNS geometry)

```bash
cd ahbg/android
gradle assembleDebug -PruntimeUrl=http://10.0.2.2:8765 -PrevenueCatApiKey=rc_public_key
gradle assembleDebug -PruntimeUrl=http://10.0.2.2:8765

# signed release against the production HTTPS endpoint
gradle assembleRelease \
-PruntimeUrl=https://ahbg.interdependentway.org \
-PrevenueCatApiKey=rc_public_key \
-PahbgStoreFile=/secure/ahbg-release.jks -PahbgStorePassword=... \
-PahbgKeyAlias=... -PahbgKeyPassword=...
```

- `runtimeUrl` defaults to `http://10.0.2.2:8765` (host machine from emulator).
- `runtimeUrl` defaults to `https://ahbg.interdependentway.org` for release
builds; debug may point at a local emulator host. Cleartext is allowed only
for `10.0.2.2`/`localhost` in debug via `network_security_config.xml`.
- `revenueCatApiKey` is a RevenueCat **public** API key provisioned at build
time and never committed. Without a key the app builds and runs on the free
tier (`NoopPremiumStore`).
Expand Down
31 changes: 25 additions & 6 deletions ahbg/android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,40 @@ android {
applicationId = "org.interdependency.ahbg"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "0.1.0"
// Runtime bridge URL. 10.0.2.2 reaches the host machine from the
// Android emulator. Override with -PruntimeUrl=https://... for a real
// deployment; the mobile layer never embeds the engine itself.
val runtimeUrl = (project.findProperty("runtimeUrl") as String?) ?: "http://10.0.2.2:8765"
versionCode = 2
versionName = "0.2.0"
// Production runtime endpoint. Release builds must use HTTPS; debug
// builds may point at a local emulator host through
// -PruntimeUrl=http://10.0.2.2:8765. The mobile layer never embeds
// the engine itself.
val runtimeUrl = (project.findProperty("runtimeUrl") as String?) ?: "https://ahbg.interdependentway.org"
buildConfigField("String", "RUNTIME_URL", "\"$runtimeUrl\"")
// RevenueCat public API key. Provisioned at build time; never committed.
val revenueCatKey = (project.findProperty("revenueCatApiKey") as String?) ?: "REVENUECAT_KEY_NOT_PROVISIONED"
buildConfigField("String", "REVENUECAT_API_KEY", "\"$revenueCatKey\"")
}

signingConfigs {
create("release") {
val storeFilePath = (project.findProperty("ahbgStoreFile") as String?).orEmpty()
if (storeFilePath.isNotEmpty()) {
storeFile = file(storeFilePath)
storePassword = project.findProperty("ahbgStorePassword") as String?
keyAlias = project.findProperty("ahbgKeyAlias") as String?
keyPassword = project.findProperty("ahbgKeyPassword") as String?
}
}
}

buildTypes {
debug {
// Emulator/localhost cleartext only; controlled by network security config.
}
release {
isMinifyEnabled = false
if ((project.findProperty("ahbgStoreFile") as String?).isNullOrEmpty().not()) {
signingConfig = signingConfigs.getByName("release")
}
}
}
buildFeatures {
Expand Down
3 changes: 2 additions & 1 deletion ahbg/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

<application
android:label="@string/app_name"
android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:name=".MainActivity"
Expand Down
16 changes: 16 additions & 0 deletions ahbg/android/app/src/main/res/drawable/ic_launcher_foreground.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#E8E6F0"
android:pathData="M54,30 A24,24 0 1,1 54,78 A24,24 0 1,1 54,30" />
<path
android:fillColor="#2F3154"
android:pathData="M54,42 A12,12 0 1,1 54,66 A12,12 0 1,1 54,42" />
<path
android:fillColor="#E8E6F0"
android:pathData="M54,18 A6,6 0 1,1 54,30 A6,6 0 1,1 54,18 M54,78 A6,6 0 1,1 54,90 A6,6 0 1,1 54,78 M18,54 A6,6 0 1,1 30,54 A6,6 0 1,1 18,54 M78,54 A6,6 0 1,1 90,54 A6,6 0 1,1 78,54" />
</vector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
4 changes: 4 additions & 0 deletions ahbg/android/app/src/main/res/values/colors.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#10121C</color>
</resources>
15 changes: 15 additions & 0 deletions ahbg/android/app/src/main/res/xml/network_security_config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
<debug-overrides>
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</debug-overrides>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">10.0.2.2</domain>
<domain includeSubdomains="false">localhost</domain>
<domain includeSubdomains="false">127.0.0.1</domain>
Comment on lines +10 to +13

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope emulator cleartext exceptions to debug builds

The domain-config is outside debug-overrides, so its cleartext exceptions apply to release builds as well as debug builds. A release assembled with a localhost or emulator runtimeUrl can therefore send the runtime protocol over HTTP despite the documented debug-only boundary; put these domains in a debug-specific network-security resource or manifest overlay.

Useful? React with 👍 / 👎.

</domain-config>
</network-security-config>
135 changes: 135 additions & 0 deletions ahbg/runtime/construction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""Bind AHBG construct to the UCNS construction state.

UCNS now supplies the authoritative build state for the seven-band Mobius
Seed of Life (``ucns.mobius_seed_construction``). AHBG maps that state onto
its UCNS-derived tiles by ``ucns_slot`` and persists it beside the engine
field. This module adds no geometry: buildable-next is read from UCNS
structural-vesica relations only.

Usage guidance:
The runtime constructs the ledger automatically inside ``run_plane`` and
the HTTP bridge. Direct use is also supported::

ledger = ConstructionLedger.open(field)
for tile_id in ledger.legal_build_tiles(field):
ledger, event = ledger.apply_build(field, tile_id, unit_id="A0")
"""

from __future__ import annotations

import json
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Mapping

_UCNS = Path(__file__).resolve().parents[2] / "libs" / "ucns" / "src"
if str(_UCNS) not in sys.path:
sys.path.insert(0, str(_UCNS))

from ucns.mobius_seed_construction import ( # noqa: E402
ConstructionState,
buildable_slots,
construct,
from_built,
initial_construction_state,
)

LEDGER_SCHEMA = "interdependency.ahbg.construction-ledger/1"


class ConstructionError(ValueError):
"""A construct intent violates the UCNS construction boundary."""


def _slot_for_tile(field: Any, tile_id: str) -> str:
for tile in field.snapshot()["tiles"]:
if tile["tile_id"] == tile_id:
return str(tile.get("ucns_slot") or tile["tile_id"])
raise ConstructionError(f"unknown tile {tile_id}")


def _tile_for_slot(field: Any, slot: str) -> str:
for tile in field.snapshot()["tiles"]:
if str(tile.get("ucns_slot") or tile["tile_id"]) == slot:
return str(tile["tile_id"])
raise ConstructionError(f"no tile for UCNS slot {slot}")


@dataclass(frozen=True)
class ConstructionLedger:
state: ConstructionState

@classmethod
def open(cls, field: Any) -> "ConstructionLedger":
return cls(initial_construction_state())

@classmethod
def load(cls, field: Any, directory: Path) -> "ConstructionLedger":
path = directory / "construction.json"
if not path.exists():
return cls.open(field)
raw = json.loads(path.read_text(encoding="utf-8"))
if raw.get("schema") != LEDGER_SCHEMA:
raise ConstructionError("unknown construction ledger schema")
built = [str(slot) for slot in raw.get("built", [])]
from ucns.mobius_seed import BandSlot

slots = [BandSlot(slot) for slot in built if slot in {item.value for item in BandSlot}]
return cls(from_built(slots))
Comment on lines +75 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject unknown persisted construction slots

When a ledger contains an unknown slot—for example after corruption or loading state written by a newer schema—this comprehension silently drops it and reconstructs a smaller built set. The next dump then permanently normalizes away the missing construction instead of failing closed, so validate every persisted entry and raise ConstructionError on any value outside BandSlot.

Useful? React with 👍 / 👎.


def dump(self, directory: Path) -> None:
directory.mkdir(parents=True, exist_ok=True)
(directory / "construction.json").write_text(
json.dumps(
{
"schema": LEDGER_SCHEMA,
"built": [slot.value for slot in sorted(self.state.built, key=lambda item: item.value)],
"buildable": [slot.value for slot in buildable_slots(self.state)],
},
indent=2,
sort_keys=True,
)
+ "\n",
encoding="utf-8",
)

def legal_build_tiles(self, field: Any) -> tuple[str, ...]:
slots = buildable_slots(self.state)
return tuple(_tile_for_slot(field, slot.value) for slot in slots)

def apply_build(
self,
field: Any,
*,
unit_id: str,
from_tile_id: str,
to_tile_id: str,
) -> tuple["ConstructionLedger", dict[str, Any]]:
unit = field.occupants.get(unit_id)
if unit is None or unit.tile_id != from_tile_id:
raise ConstructionError(f"{unit_id} is not on {from_tile_id}")
slot = _slot_for_tile(field, to_tile_id)
from ucns.mobius_seed import BandSlot

band = BandSlot(slot)
try:
next_state = construct(self.state, band)
except Exception as exc:
raise ConstructionError(f"construct {slot} violates UCNS construction boundary: {exc}") from exc
event = {
"kind": "construct",
"unit_id": unit_id,
"from_tile_id": from_tile_id,
"to_tile_id": to_tile_id,
"ucns_slot": slot,
"built_count": len(next_state.built),
}
return ConstructionLedger(next_state), event

def as_dict(self) -> Mapping[str, Any]:
return {
"schema": LEDGER_SCHEMA,
"built": [slot.value for slot in sorted(self.state.built, key=lambda item: item.value)],
"buildable": [slot.value for slot in buildable_slots(self.state)],
}
47 changes: 35 additions & 12 deletions ahbg/runtime/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,24 +73,47 @@ def plan(self, observation: Mapping[str, Any]) -> dict[str, Any]:
and item.get("action") == "relocate"
and item.get("from_tile_id") == at
]
buildable = [
str(item["to_tile_id"])
for item in legal
if isinstance(item, Mapping)
and item.get("unit_id") == unit_id
and item.get("action") == "construct"
and item.get("from_tile_id") == at
]

choice = self._will.choose_relocate(
self._vessel,
unit_id=unit_id,
at=at,
empty_neighbors=empty_neighbors,
world=field,
)
intents = []
if choice.get("kind") == "relocate":
choice = None
# A0 reference policy: build the first UCNS-buildable tile when one is
# advertised, otherwise relocate through the canonical will. Both
# actions travel the same capability-bounded plan contract.
if buildable:
intents.append(
{
"unit_id": choice["unit_id"],
"action": "relocate",
"from_tile_id": choice["from_tile_id"],
"to_tile_id": choice["to_tile_id"],
"unit_id": unit_id,
"action": "construct",
"from_tile_id": at,
"to_tile_id": sorted(buildable)[0],
}
)
choice = {"kind": "construct"}
else:
choice = self._will.choose_relocate(
self._vessel,
unit_id=unit_id,
at=at,
empty_neighbors=empty_neighbors,
world=field,
)
if choice.get("kind") == "relocate":
intents.append(
{
"unit_id": choice["unit_id"],
"action": "relocate",
"from_tile_id": choice["from_tile_id"],
"to_tile_id": choice["to_tile_id"],
}
)
return {
"schema": "interdependency.ahbg.harness.plan/1",
"session_id": session_id,
Expand Down
14 changes: 6 additions & 8 deletions ahbg/runtime/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,9 @@

* ``observe`` — receive field snapshots and resolved-effect feed;
* ``plan`` — submit a plan payload;
* ``relocate`` — emit move intents between adjacent tiles.

``construct``/build remains regulatory in the frozen engine: it is recorded as
a deferred effect, never emitted as an executable intent, and therefore is not
an advertised capability. UCNS construction authority stays ``hmmm``.
* ``relocate`` — emit move intents between adjacent tiles;
* ``construct`` — emit build intents for UCNS-buildable tiles. Buildable-next
is read from the authoritative UCNS construction state, never re-derived.
"""

from __future__ import annotations
Expand All @@ -31,9 +29,9 @@
PLAN_SCHEMA = "interdependency.ahbg.harness.plan/1"
EFFECT_SCHEMA = "interdependency.ahbg.harness.effect/1"

CAPABILITIES = ("observe", "plan", "relocate")
EXECUTABLE_ACTIONS = ("relocate",)
REGULATORY_ACTIONS = ("construct",)
CAPABILITIES = ("observe", "plan", "relocate", "construct")
EXECUTABLE_ACTIONS = ("relocate", "construct")
REGULATORY_ACTIONS = ()

# Inbox injection markers, shared with the frozen corpus runner. Injected
# instructions are refused, never executed.
Expand Down
Loading
Loading