File size: 12,822 Bytes
4879fc7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 | """Local dev backend for the test environment (see testenv/README.md).
Runs the REAL bucket-sync FastAPI app against a **filesystem-persistent
FakeHub**: every bucket lives under ``--root`` as ``{org}/{bucket}/...``, so
state survives restarts and the dashboard (in ``LOCAL_BUCKET_DIR`` mode) reads
the very same files the backend writes. No HF org, tokens, or Spaces β the
whole stack runs offline.
Identity is scriptable, mirroring tests/conftest.py: ANY bearer token resolves
to ``--user`` (an org admin, so organizer-gated features like broadcasts work
too), and agent bucket-ownership proofs work because the fake honors the same
``hf://buckets/{org}/{slug}-{agent}/...`` layout on disk.
python scripts/dev_server.py --root ../.testenv/buckets --seed
"""
from __future__ import annotations
import argparse
import logging
import os
import sys
from pathlib import Path
_BACKEND = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(_BACKEND)) # app.*
sys.path.insert(0, str(_BACKEND / "tests")) # fakes (the canonical hub fake)
# Dev-friendly limits unless the caller pins their own β the production
# default (5 raw msgs/min) gets in the way of hammering a test stack.
# Must be set before app.config reads the env.
os.environ.setdefault("RAW_MESSAGE_PER_MINUTE", "60")
os.environ.setdefault("RAW_MESSAGE_PER_HOUR", "1000")
from app.audit import AuditLogger # noqa: E402
from app.config import Settings # noqa: E402
from app.dedup import PromotionLRU # noqa: E402
from app.deps import ( # noqa: E402
get_audit,
get_bucket_write_limiter,
get_dedup,
get_hub,
get_notifier,
get_org_roles,
get_raw_message_limiter,
get_read_model,
get_registration_limiter,
get_settings_dep,
get_verification_status,
get_verifier,
)
from app.main import app as fastapi_app # noqa: E402
from app.notify import Notifier # noqa: E402
from app.org_roles import OrgRoles # noqa: E402
from app.rate_limit import CompoundLimiter, TokenBucket # noqa: E402
from app.read_model import ReadModel # noqa: E402
from app.verification import VerificationStatusStore # noqa: E402
from app.verifier import Verifier # noqa: E402
from fakes import FakeHub, FakeJobRunner, seed_agent # noqa: E402
log = logging.getLogger("dev-server")
class PersistentFakeHub(FakeHub):
"""FakeHub that mirrors every bucket to a directory tree.
Layout: ``{root}/{org}/{bucket}/{path...}`` β the central bucket therefore
lives at ``{root}/{ORG}/{SLUG}-main-bucket/``, which is exactly what the
dashboard's ``LOCAL_BUCKET_DIR`` should point at. All reads stay in-memory
(loaded once at boot); every write/delete goes through to disk.
"""
def __init__(self, settings: Settings, root: Path):
super().__init__(settings)
self._root = root
self._load()
# ββ disk mirroring ββββββββββββββββββββββββββββββββββββββββββββ
def _file(self, bucket: str, path: str) -> Path:
return self._root / bucket / path
def _persist(self, bucket: str, path: str) -> None:
data = self.buckets.get(bucket, {}).get(path)
if data is None:
return
f = self._file(bucket, path)
f.parent.mkdir(parents=True, exist_ok=True)
f.write_bytes(data)
def _load(self) -> None:
if not self._root.is_dir():
return
n = 0
for org_dir in self._root.iterdir():
if not org_dir.is_dir():
continue
for bucket_dir in org_dir.iterdir():
if not bucket_dir.is_dir():
continue
bucket = f"{org_dir.name}/{bucket_dir.name}"
files = self.buckets.setdefault(bucket, {})
for f in bucket_dir.rglob("*"):
if f.is_file():
files[str(f.relative_to(bucket_dir))] = f.read_bytes()
n += 1
if n:
log.info("loaded %d files from %s", n, self._root)
# ββ write-through overrides βββββββββββββββββββββββββββββββββββ
def seed(self, path: str, text: str, bucket: str | None = None) -> None:
super().seed(path, text, bucket)
self._persist(bucket or self._settings.central_bucket, path)
def write_text_central(self, path: str, text: str) -> None:
super().write_text_central(path, text)
self._persist(self._settings.central_bucket, path)
def write_bytes_central(self, path: str, data: bytes) -> None:
super().write_bytes_central(path, data)
self._persist(self._settings.central_bucket, path)
def write_many_central(self, items: list[tuple[bytes, str]]) -> None:
super().write_many_central(items)
for _, p in items:
self._persist(self._settings.central_bucket, p)
def delete_central(self, path: str) -> None:
super().delete_central(path)
f = self._file(self._settings.central_bucket, path)
if f.is_file():
f.unlink()
def write_bytes_to_bucket(self, bucket: str, path: str, data: bytes) -> None:
super().write_bytes_to_bucket(bucket, path, data)
self._persist(bucket, path)
def append_jsonl_audit(self, path: str, line: str) -> None:
super().append_jsonl_audit(path, line)
self._persist(self._settings.audit_bucket, path)
def write_bytes_audit(self, path: str, data: bytes) -> None:
super().write_bytes_audit(path, data)
self._persist(self._settings.audit_bucket, path)
def copy_file_to_central(self, src_bucket: str, src_xet_hash: str, dest_path: str) -> None:
super().copy_file_to_central(src_bucket, src_xet_hash, dest_path)
self._persist(self._settings.central_bucket, dest_path)
# ββ live disk fallbacks βββββββββββββββββββββββββββββββββββββββ
# Files dropped into {root}/{org}/{bucket}/ AFTER boot (the "agent writes
# to their scratch bucket, then promotes" workflow) are picked up without
# a restart: reads fall back to disk and cache in.
def read_bytes(self, uri) -> bytes:
try:
return super().read_bytes(uri)
except FileNotFoundError:
from app.naming import SourceURI, parse_source_uri
parsed = uri if isinstance(uri, SourceURI) else parse_source_uri(uri)
if parsed is not None:
f = self._file(f"{parsed.org}/{parsed.bucket}", parsed.path)
if f.is_file():
data = f.read_bytes()
self.buckets.setdefault(f"{parsed.org}/{parsed.bucket}", {})[parsed.path] = data
return data
raise
def bucket_exists(self, bucket: str) -> bool:
return super().bucket_exists(bucket) or (self._root / bucket).is_dir()
def copy_tree_to_central(self, src_bucket: str, src_prefix: str, dest_prefix: str):
out = super().copy_tree_to_central(src_bucket, src_prefix, dest_prefix)
for _src, dest, _size in out:
self._persist(self._settings.central_bucket, dest)
return out
def seed_world(hub: PersistentFakeHub, settings: Settings) -> None:
"""A small, believable starting state: two registered agents with scratch
buckets (handshakes in place, so every source-URI flow works out of the
box) and a couple of board messages. Idempotent β seeding an already
seeded world just rewrites the same files."""
for agent, hf_user in (("byte-bandit", "bb-hf"), ("delta-coder", "dc-hf")):
seed_agent(hub, agent, hf_user=hf_user)
scratch = settings.agent_bucket(agent)
hub.seed(".bucket-sync-handshake", hf_user, bucket=scratch)
hub.seed("subscribe.md", "following", bucket=scratch)
hub.seed(
"message_board/20260707-090000-000_byte-bandit.md",
"---\nagent: byte-bandit\ntimestamp: 2026-07-07 09:00 UTC\nvia: raw\ntype: agent\n---\n"
"joining; planning an arithmetic-coder baseline\n",
)
hub.seed(
"message_board/20260707-091500-000_delta-coder.md",
"---\nagent: delta-coder\ntimestamp: 2026-07-07 09:15 UTC\nvia: raw\ntype: agent\n---\n"
"@byte-bandit interested β comparing notes on context models\n",
)
log.info("seeded 2 agents + 2 board messages")
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--root", default=str(_BACKEND.parent / ".testenv" / "buckets"),
help="directory that plays the role of HF bucket storage")
ap.add_argument("--org", default="local-org")
ap.add_argument("--slug", default="collab")
ap.add_argument("--user", default="tester",
help="every bearer token resolves to this HF user (an org admin)")
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=8100)
ap.add_argument("--seed", action="store_true", help="seed agents + board messages")
args = ap.parse_args()
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
settings = Settings(
HF_TOKEN="dev-admin-token",
ORG=args.org,
COLLAB_SLUG=args.slug,
AUDIT_BUCKET=f"{args.org}-private/{args.slug}-audit",
)
root = Path(args.root).resolve()
root.mkdir(parents=True, exist_ok=True)
hub = PersistentFakeHub(settings, root)
hub.whoami_user = args.user
hub.whoami_email = f"{args.user}@example.com"
hub.whoami_orgs = {args.org}
# The dev user is an org admin: organizer-gated paths (broadcast, /v1/me
# is_organizer) behave like production for an organizer session.
hub.org_roles = {args.user: "admin"}
hub.org_roles_by_email = {f"{args.user}@example.com": (args.user, "admin")}
if args.seed:
seed_world(hub, settings)
read_model = ReadModel(hub, settings)
dedup = PromotionLRU(settings.dedup_lru_size)
verification = VerificationStatusStore(hub, runs_prefix=settings.verification_runs_prefix)
# Every singleton in app/deps.py reads the env-backed settings, so each one
# this Settings() must reach needs an override below β the notifier included,
# or /v1/healthz and every wait= route 500s on a Settings ValidationError.
notifier = Notifier(
max_waiters_per_owner=settings.longpoll_max_waiters_per_owner,
max_waiters_total=settings.longpoll_max_waiters_total,
wake_spread_s=settings.longpoll_wake_spread_s,
wake_spread_threshold=settings.longpoll_wake_spread_threshold,
)
# The verifier posts verdict messages, so it holds the notifier too β without
# it a verdict would land silently and never wake a parked watcher.
verifier = Verifier(settings, hub, read_model, verification, FakeJobRunner(),
spawn=lambda _name, fn: fn(), notifier=notifier)
def compound(burst: int, sustained: int) -> CompoundLimiter:
return CompoundLimiter(
TokenBucket(capacity=burst, refill_per_minute=burst),
TokenBucket(capacity=sustained, refill_per_minute=sustained),
)
fastapi_app.dependency_overrides.update({
get_settings_dep: lambda: settings,
get_hub: lambda: hub,
get_read_model: lambda: read_model,
get_notifier: lambda: notifier,
get_org_roles: lambda: OrgRoles(hub, settings),
get_audit: lambda: AuditLogger(hub),
get_dedup: lambda: dedup,
get_verification_status: lambda: verification,
get_verifier: lambda: verifier,
# Real (env-tunable) production limiter shapes β the point of the
# test environment is realism, just with dev-friendly defaults.
get_bucket_write_limiter: lambda: compound(
settings.bucket_write_burst, settings.bucket_write_per_minute),
get_raw_message_limiter: lambda: compound(
settings.raw_message_per_minute, settings.raw_message_per_hour),
get_registration_limiter: lambda: TokenBucket(
capacity=settings.registration_per_minute,
refill_per_minute=settings.registration_per_minute),
})
log.info("central bucket on disk: %s", root / settings.central_bucket)
log.info("bearer identity: %s (admin of %s)", args.user, args.org)
import uvicorn
uvicorn.run(fastapi_app, host=args.host, port=args.port, log_level="info")
if __name__ == "__main__":
main()
|