The venue ladder has always listed four venues in data/venues.ts, but they shared ONE floor map and differed only by difficulty knobs — so a promotion you survived a whole week for looked exactly like the room you had just left. - venueMap.ts grows a FloorLayout contract (map + props + lights + posts + probes + palette) and the newGrid() primitives every room is painted with. Voltage stays IN venueMap rather than moving to layouts/, so the new rooms can import primitives from it without closing an import cycle. - Three new rooms in src/scenes/floor/layouts/: The Royal (horseshoe public bar, pool room, pokies corner, trough + two cubicles, a beer garden that is plainly the nicest room in the pub, and a DJ "corner" that is a folding table because this pub never built a booth), Elevate (open-air rooftop — most of the grid is sky, the DJ is on an unwalled plinth, you arrive by lift), ROOM (concrete warehouse, pillars you path around, central booth, loading-dock smoking area, twelve lights in the whole venue). - Nothing about a room is a module singleton any more. FloorView, sweep and FloorDemoScene take the layout; the door picks its street plate by venue id. - FloorDemoScene's six hardcoded station spots (TAPS_SPOT, DECKS_SPOT, ...) were Voltage's tile coordinates. At four venues a hardcoded (47,3) puts the bar shift inside The Royal's pool room, so each layout now names its own stations and the scene reads them from there. - tests/floor/layoutInvariants.ts is an executable rulebook: perimeter holds, every walkable tile reachable from the entry, anchors on walkable ground, posts not sealed (a post on a sealed tile freezes the player for the night — that has shipped twice), probes reachable, props on-grid. Proved against Voltage BEFORE the new rooms were authored against it. Dev route #floor:<venueId> boots the floor straight into one room, because otherwise seeing ROOM means surviving three weeks of the ladder. Gate: lint clean, build clean, 821 tests passing (was 784). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
92 lines
3.3 KiB
Python
92 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""creds.py — resolve fleet credentials from on-disk .env files, in process.
|
|
|
|
The fleet rule is that secrets live in .env files per project and are READ FROM
|
|
DISK by whatever needs them; they never travel through a shell, a chat, a doc or
|
|
a commit. So this module takes a key name and hands back its value to the
|
|
caller's own process, and its failure mode prints the paths it TRIED — never a
|
|
value, never a fragment of one.
|
|
|
|
Candidate paths are ordered cheapest-first (this machine, then the fleet's
|
|
shared locations). Every asset script in tools/gen goes through here, which is
|
|
why the batch driver runs unmodified on ultra (Cloudflare creds) and on m3ultra
|
|
(MODELBEAST creds) without a flag.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
# Every .env on the fleet that has ever legitimately held a generation
|
|
# credential. Missing files are skipped silently — most machines have most of
|
|
# these missing, and that is normal, not an error.
|
|
CANDIDATES = (
|
|
"~/Documents/backnforth/.env",
|
|
"~/Documents/MODELBEAST/.env",
|
|
"~/Documents/MODELBEAST/data/agent.env",
|
|
"~/Documents/fluxgod-work/.env",
|
|
"~/Documents/not-tonight/.env",
|
|
)
|
|
|
|
_cache: dict[str, str] | None = None
|
|
|
|
|
|
def _load() -> dict[str, str]:
|
|
"""Merge every candidate .env, first file to define a key wins."""
|
|
global _cache
|
|
if _cache is not None:
|
|
return _cache
|
|
merged: dict[str, str] = {}
|
|
for cand in CANDIDATES:
|
|
path = Path(os.path.expanduser(cand))
|
|
if not path.is_file():
|
|
continue
|
|
try:
|
|
text = path.read_text(errors="ignore")
|
|
except OSError:
|
|
continue
|
|
for line in text.splitlines():
|
|
line = line.strip()
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
key, _, value = line.partition("=")
|
|
key = key.strip()
|
|
# Tolerate `export FOO=bar` and quoted values — both appear on the fleet.
|
|
if key.startswith("export "):
|
|
key = key[len("export "):].strip()
|
|
value = value.strip().strip('"').strip("'")
|
|
if key and value and key not in merged:
|
|
merged[key] = value
|
|
# A real environment variable beats any file: that is how you override for
|
|
# a one-off without editing a shared .env.
|
|
for key in ("CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_API_TOKEN", "MB_TOKEN"):
|
|
if os.environ.get(key):
|
|
merged[key] = os.environ[key]
|
|
_cache = merged
|
|
return merged
|
|
|
|
|
|
def get(key: str, *, required: bool = True) -> str:
|
|
"""Value for `key`, or exit with the paths tried (never the values)."""
|
|
value = _load().get(key, "")
|
|
if value:
|
|
return value
|
|
if not required:
|
|
return ""
|
|
tried = "\n ".join(CANDIDATES)
|
|
raise SystemExit(
|
|
f"credential {key!r} not found on this machine.\n"
|
|
f"looked in (plus the process environment):\n {tried}\n"
|
|
f"put it in one of those, or run this script on a machine that has it."
|
|
)
|
|
|
|
|
|
def have(key: str) -> bool:
|
|
return bool(_load().get(key))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Diagnostic only: which credentials EXIST here. Booleans, never values.
|
|
for k in ("CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_API_TOKEN", "MB_TOKEN"):
|
|
print(f"{k}: {'present' if have(k) else 'MISSING'}")
|