Fifth instance of this project's recurring dead-route defect: a handler exists and
is correct, but classify() never produces the route, so every request falls
through to the Python upstream. Invisible in production, where the oracle answers;
on staging, where the upstream is deliberately dead, it is a 502.
GET ut/%s/item is FutViewCards (deser 0x1801293d0, top-level itemData via the
shared card element 0x18013fe00). The oracle answers it with defs_route - the SAME
handler already wired for item/resource and defid: pull every integer out of the
query (idList=a,b,c / definitionId= / resourceId=) and return one definition per
id, or {"itemData": []} when there are none (tools/utas_server.py:1103). So
claiming it is byte-identical parity, not new behaviour.
The query handling is the substance of the fix, not decoration. ut_tail does NOT
strip the query string, so an equality-only arm (`Some("item")`) misses every real
request while passing a no-query unit test - which is precisely how the route
stayed unclaimed. The same latent bug applied to the two arms that were already
there: item/resource and defid only matched with no query, so
`item/resource?resourceId=` and `defid?definitionId=` were ALSO falling through.
All three now mirror the oracle's own `item(\?|$)` pattern.
Verified on staging, all 200 where they were 502:
GET /item -> itemData 0
GET /item?idList=100000003,100000004 -> itemData 2
GET /ut/v2/game/fifa17/item -> itemData 0
GET /item/resource?resourceId=5003012 -> itemData 1
GET /defid?definitionId=200389 -> itemData 1
Does NOT by itself fix the kit selector - GET /item is a definition lookup keyed
by ids the client already holds, not the thing that seeds the club collection, and
the observed session never requested it. It is a real production-masked gap and a
prerequisite for testing anything else on staging.
get_item_is_claimed_and_the_other_item_verbs_are_unaffected pins the claim plus
the three verbs that share the prefix: PUT item stays FutMoveCard on the economy
path and DELETE item/<id> stays QuickSellPath.
cargo test -p openfut-utas-host: 190 passed, 0 failed. clippy -D warnings clean.
openfut-utas-host
The FIFA 17 UTAS migration boundary. It accepts the client-visible HTTP surface, serves migrated routes from Rust/Core plus host-owned durable stores, and proxies only the unclassified tail to the Python behavioral oracle.
FIFA 17 ──HTTP──▶ openfut-utas-host
├── migrated route ──▶ Rust adapter / Core / host stores
└── unclassified tail ──▶ Python UTAS oracle
src/lib.rs::classify is the route-level source of truth. The current Rust surface
includes club/squad/user reads, club rename, auth/session/client data, Store/economy,
packs, owned-item moves, market/trade-pile, and the observed hub support routes.
Safety model
- Classification happens exactly once before execution. There is no "try Rust then Python"; a mutation cannot be double-applied.
- A route classified to Rust never falls back to Python on a Core/store/projection failure. Each handler uses its captured fail-closed or honest-empty wire contract.
PUT …/clubandPUT|POST …/user/clubatomically update the shared account JSON. Every input returns the required zero-atom200 {}response; rejection and persistence failures remain visible in logs.- Numeric
GET …/squad/<n>returns the one Core-backed current squad, matching the Python oracle's single-current-squad behavior. - Python remains the behavioral oracle and rollback backend for routes not yet classified to Rust. New economy behavior belongs in Rust/Core, never Python.
Configuration (env)
| Var | Required | Default | Meaning |
|---|---|---|---|
OPENFUT_UTAS_HOST_ADDR |
yes | — | client-visible host listen address |
OPENFUT_UTAS_PYTHON_URL |
yes | — | Python oracle base for the unclassified tail; must differ from this host |
OPENFUT_FIFA17_CATALOG |
yes | — | FIFA 17 definition identity catalog |
OPENFUT_IDENTITY_STORE |
yes | — | persistent owned-instance ↔ wire-id store |
OPENFUT_PERSONA_ID |
yes | — | non-zero FIFA persona id shared by LSX/Blaze/POW/UTAS |
OPENFUT_MARKET_DB |
yes | — | durable host-owned transfer-market SQLite DB |
OPENFUT_PILE_DB |
yes | — | durable host-owned item-pile SQLite DB |
OPENFUT_CORE_URL |
no | http://127.0.0.1:8080 |
OpenFUT Core base |
OPENFUT_FIFA17_TABLES_DIR |
no | fifa17-recon/data/tables |
FIFA entity tables |
OPENFUT_CLIENTDATA_DB |
no | identity-store sibling clientdata.json |
durable opaque client-data JSON |
OPENFUT_ACCOUNT_PATH |
no | FUT_ACCOUNT_PATH, then identity-store sibling active_account.json |
shared FIFA account/club JSON |
Startup fails if required identity or durable economy state cannot be opened. No placeholder production identity source is substituted.
Identity model (resolved)
FIFA renders an owned card by resolving resourceId & 0xffffff against the
client's own local players table; an invented id renders a blank generic
card (proven live — fut_cards.py:11-21). Two distinct identities, never
conflated, are resolved by [Fifa17IdentityResolver] (the single production path):
- Definition identity (
resourceId/assetId) — the card's real FIFA asset id, from the versionedOPENFUT_FIFA17_CATALOG. An unmapped definition is dropped and counted, never faked. - Instance identity (
id) — a stable, persistent, reversible wire integer from the genericopenfut-identitystore under the FIFA 17 wire-id policy (monotonic from100_000_001). The same owned instance keeps its id across restart and reverses exactly; two copies of one definition share aresourceIdbut get distinctids. The namespace is globally monotonic within(fifa17, owned-item)— no per-account column is needed because Core owned-instance ids are globally-unique UUIDs.
Production has a frozen post-P1 baseline and a hot Python rollback. A source change passing local tests is not deployment approval. Build verification, staging, host restart, and live-client promotion remain operator-gated; the current state and promotion evidence live in the OpenFUT Obsidian vault.
Logs are safe by construction: no auth/session/device/token material — only owner, route, filter summary, counts, status.