404e859cb6a58bcf92de4f1da0024fb4ec035da5
62 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
80a8bc4520 |
feat(adapter): FIFA17 squad extension v1 + full-replacement PUT builder
Add fut::squad_ext::Fifa17SquadExtensionV1 — the versioned, adapter-owned
payload Core stores opaquely alongside the canonical squad. Carries the
FIFA-only wire state that is not Core-canonical:
- custom[] opaque 33-int string, round-tripped verbatim
- squad_type observed FIFA token
- kit_numbers keyed by owned_card_id (kit follows the PLAYER, proven
by the swap/formation captures), never by slot/definition
- manager opaque item ref (not a squad player; not shaped)
- kicktakers opaque role refs; relationship to captain UNKNOWN, so
preserved verbatim and never normalized to the captain
- client_reported chemistry/rating/starRating shadow, never authoritative
from_payload enforces the payload schema version first (distinct from Core's
DB schema); an unknown version is rejected, never coerced.
build_squad_write turns a parsed PUT + host wire->owned resolver into a
canonical ProposedSquad + extension, refusing on unresolved ids or a
duplicate owned item. Identity resolution is explicitly NOT authorization.
Refactor the
|
||
|
|
b50e0359f7 |
feat(adapter): extract shared FIFA17 FUT item-shaping primitive
Move the per-item card shaper (CoreOwnedItem, Fifa17Identity,
ItemIdentityResolver, ShapeStats, shape_item) out of club_response into
fut::item so /club and the upcoming squad projection emit byte-identical
items from one source of truth. club_response keeps only the /club
{itemData:[...]} envelope and re-exports the moved types for API
stability. shape_item is now pub; no behavior change (all /club and
oracle-parity tests unchanged and green).
Adds item-shaper tests: full-field identity mapping and the duplicate
owned-copy invariant (two instances of one definition keep distinct wire
ids, share one asset id).
|
||
|
|
550a59d12c | feat(adapter): FIFA17 squad full-replacement wire parser + reverse-map scaffolding (unrouted) | ||
|
|
36fe1caa3f |
feat(fifa17): deterministic base-card seed generator + full identity catalog
scripts/seed_fifa17_cards.py: deterministic pipeline from committed FIFA17 data (pool.json + roster.json + leagues/nations/teams tables) -> the card-definition identity catalog. CardDefinitionId is opaque + deterministic (fifa17_<asset>), version 0 (base cards only; resource_id == asset_id). --check mode diffs against committed output (drift-detection mutation-proven). Provenance embedded. Generated openfut-adapter-fifa17/data/fifa17-card-identities.json: all 17,563 base assets. Semantic definition coverage (to /tmp, not committed here): 17,547 resolvable; 16 skipped for missing roster name (reported, never fabricated). Adapter loads the committed catalog (test: 17,563 entries, Ronaldo fifa17_20801 -> asset 20801 v0). Phase commit 3/5. NOT owned inventory: this is 'which cards exist', not 'which the user owns'. Core content seeding + dev-owned set next. |
||
|
|
f55c401b6c |
feat(fifa17): card-definition identity catalog + owned-item wire-id policy
fut::catalog — Fifa17CardCatalog maps a semantic CardDefinitionId to a FIFA 17 render identity (resource_id = (version<<24)|asset_id; version 0 => resource== asset). Versioned JSON (schema_version=1, game=fifa17); validates schema/game, rejects asset_id > 24 bits, and rejects two card ids claiming one resource_id. Unknown definitions resolve to None (callers drop, never fabricate). Fifa17WireItemIdPolicy carries the owned-item namespace (base 100_000_000, first id 100_000_001, per the oracle) supplied to the generic store. Adds serde derive to the adapter. 8 catalog tests; 3/3 mutations killed (resourceId-drops-version, conflict-detection-off, asset-range-off). Phase commit 2/5. No card->asset DATA shipped: the synthetic Core catalogue is unmappable (see seed plan); the loader + format land now, population later. |
||
|
|
c0a3f68ded |
feat(utas): FIFA17 UTAS migration host + /club adapter mappings
openfut-utas-host: the first live UTAS host. Serves GET /ut/game/<title>/club from OpenFUT Core via the FIFA17 adapter and reverse-proxies every other UTAS route verbatim to the Python oracle. Plaintext HTTP/1.1 keep-alive (no TLS); route classification before execution; a Core error on /club degrades to an empty page and never falls back to Python. CoreAccess is a host-owned boundary (the adapter stays transport-agnostic). openfut-adapter-fifa17::fut: owned_query (wire parse + FIFA id->name mapping, unknown id = hard error), entities (id<->name from committed tables), and club_response (FIFA _item shaping; drops items lacking a real FIFA asset id, never fabricates one). openfut-core submodule advanced to the reconciled trunk (6acae54 = 8c8a4116 multi-game + eab522a replace_squad/SquadRules + the /club semantic query). 11 host tests + adapter fut tests; 10/10 host mutations killed. rare=SP UNKNOWN. Retail rendering of Core inventory still blocked on the Core-card->asset-id identity decision (next phase). |
||
|
|
84e81f2037 |
tls: extract a shared listener; move FIFA 17's profile into its adapter
The redirector was the only host that spoke TLS, so its TLS lived inside
it. The roster host needs the same listener, and that made the choice
explicit: share this code or copy it.
Copying it is what already went wrong. On 2026-08-11 the Rust redirector
served one certificate while the container served another. ProtoSSL
caches the server certificate per backend, so the redirector -- the first
TLS connection of a session -- decided what the client expected, and
every later service failed its handshake. Silently: Python's socketserver
swallows ssl.SSLError as OSError. Three gates went to it. One place to
configure TLS is the structural fix, so it exists before the second host
does rather than after.
Split along the line the architecture already draws:
openfut-tls how to build an acceptor. Game-independent.
Knows nothing about which suites any client
offers.
adapter-fifa17::tls what FIFA 17 was OBSERVED to offer: the six
enabled suites, the two refused, the TLS 1.2
window, the EA SNI. Plain strings, so the
adapter keeps its lean dependencies -- reading
a card table should not build OpenSSL.
redirector-host joins the two. Chooses no cipher of its own.
Behaviour is unchanged, and shown to be:
* tests/fifa17_tls_profile.rs carries over every case from the deleted
module -- FIFA's eight suites negotiate AES256-GCM-SHA384, each enabled
suite works alone, RC4-only is refused, ECDHE-only is refused. Deleting
a module must not quietly delete its evidence.
* one test pins the composed values literally against the host as it was
when gates 1-14 passed. A "pure refactor" that cannot fail is not a
claim, it is an assumption.
* the rebuilt binary self-tests to the same TLSv1.2 / AES256-GCM-SHA384
the retail client negotiated at 17:09 today.
Two improvements fall out of having one place to look:
* the startup banner now prints cert_sha256. The mismatch above raised no
error at startup and broke the client much later with nothing logged;
it is now the first line of the log.
* tls_min/tls_max print as TLSv1.2 rather than SslVersion(771). This line
is gate evidence and gets read by people.
Nothing deployed and nothing restarted: FIFA is mid-session on the
running redirector, which is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
b1bc7a764e |
adapter: port the FUT roster-update response, held to the live oracle's bytes
Next component in the migration order (Roster -> LSX -> UTAS). Adapter layer
only: no host, no runtime replacement, nothing armed.
The response is shaped as much by http.server.BaseHTTPRequestHandler as by the
oracle's handler code, so it is captured over the wire rather than reasoned
about:
* HTTP/1.0 status line -- protocol_version is left at its default, so the
reply is 1.0 even though the client asks for 1.1
* send_response injects Server: and Date: BEFORE the handler's own headers
* POST answers with headers only: the handler writes the body `if method ==
"GET"`, so a POST advertises Content-Length: 67 and then sends nothing
That last one is preserved, not corrected. It looks like a bug, but "obviously a
bug" has been the wrong call before in this port, and a test now asserts it so a
future cleanup has to argue with something.
Date and Server are volatile and are MASKED in the fixture rather than dropped,
so their presence and position are still asserted. Server is additionally
recorded verbatim: it carries the container's Python version, so a drift away
from roster::ORACLE_SERVER fails a test instead of silently changing every byte
we emit.
generate_roster.py --check FAILS when it cannot reach the oracle rather than
passing, and mutation-testing the mutation harness itself caught two "surviving"
mutations that were really sed no-ops. With application verified, all four
mutations (header order, Content-Length, XML body, Connection) are killed.
|
||
|
|
f451406058 |
audit: eliminate deployment-address hardcoding; single typed endpoint config
Mandatory OpenFUT architecture audit. Two real defects found and fixed, plus
the config surface tightened so neither class can recur.
DEFECT 1 -- hidden localhost fallback. The Rust host defaulted POW hosts to
127.0.0.1 while every other URL followed OPENFUT_ADVERTISE, so a remote
deployment would emit loopback POW URLs and fail far from the cause. It also
diverged from the deployed Python entrypoint, which derives them
(POW_HOST="${POW_HOST:-$ADV:8094}"). POW endpoints now derive from the
advertised address; explicit overrides still win.
DEFECT 2 -- Default gave loopback silently. `Endpoints::default()` and
`AdapterConfig::default()` supplied 127.0.0.1, so anything constructing a
config by omission got loopback with no signal. Both `Default` impls are
REMOVED. Loopback is now `Endpoints::loopback()` / `AdapterConfig::loopback()`:
an explicit, greppable decision. Production uses `advertising(host)`.
CONFIGURABILITY. `blaze_port` and `utas_port` are now config, not literals.
The advertised Blaze port is our choice -- the client goes wherever
<serverinstanceinfo> sends it -- and 8099 is the client's own built-in default
but still deployment config. A bad port value is an error, not a silent
fallback to the previous one.
TEST-NET EVERYWHERE. Committed fixtures and tests used the lab's real LAN
address; a test that passes because its constant matches the current lab
proves nothing about relocatability. Redirector fixtures regenerated on
RFC 5737 TEST-NET-1/2/3 plus loopback. Harness scripts no longer default the
client IP to the lab address -- client-state.sh now requires it.
SEVEN REQUIRED TESTS in tests/deployment_config.rs plus host-side coverage:
remote config never silently becomes localhost; missing advertise fails
clearly; bind may differ from advertise; changing the Blaze port changes the
redirect; changing the host updates all 200+ generated URLs with no
stragglers; no helper bypasses central config; mutations are detectable.
MUTATION TESTED, and it found a hole in the audit tests themselves. Hardcoding
utas_base, reverting the POW derivation and re-hardcoding the Blaze port were
all caught. Making the redirector read `bind` instead of `advertise` was NOT:
`advertising()` sets bind == advertise, so the two sources were
indistinguishable. That is the single most likely bypass -- the oracle really
does read bind for nucleusConnect -- so the test now forces bind != advertise
and asserts the bind address never reaches the wire. Re-mutated: caught.
Wire behaviour unchanged: oracle fixtures still current, 153 tests green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
8aab2c0d41 |
adapter: FIFA 17 redirector response; Nucleus deliberately not ported
REDIRECTOR. The first hop's <serverinstanceinfo> XML, byte-for-byte against the oracle across three advertised addresses. Owns the response only; TLS and HTTP transport belong to a host, exactly as the Blaze adapter owns dispatch while the sidecar owns the socket. The <secure>0</secure> field is the client being told the second hop is plaintext -- independent corroboration of the plaintext Blaze finding, now expressed in code. NUCLEUS IS NOT PORTED, and that is a finding rather than an omission. Instrumented across every live session: listener bound YES 0.0.0.0:42131 since 00:21:32 handler logs on connect YES unconditional, before any parsing client received the URL YES OSDK_NUCLEUS fetched 10+ times client connected NO zero requests, including 4 full FUT flows So the long-standing nucleusConnect=0.0.0.0 anomaly is explained: FIFA never follows that URL on this path. The invalid address has never mattered because nothing dials it. Porting the stub would add an untested component for no parity gain. TLS CONSTRAINT RECORDED, NOT RESOLVED. All 9 observed handshakes negotiated AES256-GCM-SHA384 = TLS 1.2 with STATIC RSA key exchange. rustls supports only forward-secret (EC)DHE suites and cannot serve that. Whether the client also OFFERS ECDHE is unknown -- OpenSSL follows client preference by default, so preferring static RSA does not prove it is the only option. This must be instrumented from a real ClientHello before a TLS stack is chosen; the module docs say so rather than guessing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e091921b18 |
blaze-host: safe sidecar lifecycle, Blaze switch, build identity
Prerequisites for the live FIFA A/B. Two safeguards here exist because the corresponding failure actually happened, not because it was imagined. BUILD IDENTITY. build.rs stamps commit + working-tree cleanliness; the host prints commit, tree state, profile and a fingerprint of the bundled config table at startup, into both the log and the trace. A dirty tree prints an explicit "do NOT treat results from this binary as parity evidence" warning. The previous step left four sidecars running, two serving mutated builds, and nothing in their output said so. SIDECAR LIFECYCLE (sidecar.sh). start/stop/status/check-orphans/with. Start refuses when any sidecar is already running or the port is busy. Stop kills, waits, then PROVES it: PID gone AND port free AND no stray processes, failing if any check does not hold. `with -- CMD` traps EXIT/INT/TERM so cleanup runs however the command exits. Bug found and fixed while testing it: orphan detection used `pgrep -f`, which matched any process whose command line merely mentioned the name -- including the shell running the test script. It now matches the resolved executable via /proc/PID/exe. `pgrep -x` is unusable because Linux truncates the process name to "openfut-blaze-h". BLAZE SWITCH (blaze-switch.sh). Redirects Blaze to the sidecar with a scoped NAT rule instead of editing the frozen Python oracle, whose redirector advertises a hardcoded BLAZE_PORT = 42130. Rules match only <LAN_IP>:42130; 127.0.0.1:42130 is deliberately left alone so Python stays reachable on loopback and the A/B compares real Python against real Rust. Verified both directions live: LAN->Rust with the switch on, LAN->Python with it off. Bug found and fixed: `off` reported success while two rules remained active and rollback had NOT happened. It matched `--comment "tag"` with quotes this iptables does not emit -- and the verification used the SAME broken matcher, so it confirmed its own failure. A rollback that lies is worse than one that fails. Now matched on the bare tag, verified with iptables-save plus a tag-independent check that nothing still redirects the port. Second flaw fixed: `sidecar.sh stop` originally warned about a live switch and then stopped anyway, creating the exact broken state it warned about. It now REFUSES, with --force as the deliberate override. The general rule this all converges on, now stated in the README: a verification must not share the failure mode of the thing it verifies. 116 tests still passing; clippy clean; Python backend untouched and contract suite 446/446. NAT table left clean, no orphan processes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
cf961603fe |
openfut-adapter-fifa17: FIFA 17 Blaze adapter, oracle-tested
The second migration step: the layer above the codec, deciding WHAT to say
rather than how to encode it. Sits on openfut-protocol-blaze and supplies
what that crate deliberately refuses to know.
blaze/ids.rs component/command/notification tables
blaze/config.rs injectable identity + endpoints, nothing hardcoded
blaze/session.rs per-connection state
blaze/client_config.rs the fetchClientConfig tables
blaze/responses.rs 16 Blaze::* response bodies
blaze/dispatch.rs (component, command) -> Vec<Frame>
Parity is tested, not asserted. fixtures/generate.py drives the real
blaze_responder_v3b.dispatch() and records 49 request->response(s)
transactions, replayed in order against a shared session per connection so
ordering-dependent behaviour is exercised: preAuth captures the locale later
ALOC fields echo, login sets the auth code getAuthToken returns. Comparison
is byte-for-byte including frame count and order.
98 tests green across both crates; clippy clean.
MUTATION TESTED, and it found a real defect in this commit's own design.
Swapping two post-login notifications and flipping one enum inside
AccountInfo both turned the suite red as intended. Hardcoding an address in
utas_base() did NOT -- the config templating substituted raw hosts directly,
making those helpers dead code that merely looked load-bearing. The table now
templates on URL-level tokens ({utas_base}, {nucleus_base},
{pow_content_url}) so they are the single place a URL shape is defined, and
the mutation is caught.
The client config table (227-243 rows per CFID) is generated from the oracle
rather than transcribed: it is reverse-engineered data, not logic, and 400
hand-copied string literals would add a typo class no reviewer can catch. The
generator substitutes real addresses back in and diffs against the oracle for
every section before writing, so the templating is verified rather than
assumed.
Reproduces one known defect deliberately: nucleusConnect is built from BIND,
not advertise, so the live split deployment tells a client on another machine
to reach Nucleus at http://0.0.0.0:42131. Confirmed against the running
container. Reproduced because it is what the only proven-working config does;
fixing it needs live validation and is a separate change. It also implies the
Nucleus stub is not reached in the current remote flow.
Blaze carries no FUT domain state -- no coins, packs, clubs or squads on this
wire -- so Session stays a session key, locale, service name, auth code and a
flag. That boundary will need defending when UTAS is migrated.
Not wired into anything. The crate answers frames; it opens no socket and
owns no runtime. The Python backend remains the live service and the oracle,
and is unmodified (contract suite still green).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|