Opens the 409 `apply_effect_unproven` gate for attribute training, the second family after contracts to have its effect settled rather than merely its magnitude. `ApplyEffect` replaces the single-family `AddContractMatches` struct: Core dispatches on `kind`, so an unproven family must be impossible to express, not merely discouraged. The shared half of an apply -- the exactly-once key, Core's transaction, the error mapping and the client's payload -- is now one `finish_consumable_apply`, so a new family cannot quietly acquire its own idempotency format or its own success shape. Two refusals are training-specific and both prevent silent corruption rather than merely being tidy: a non-player target has no attributes to write, and a cross-class target would train a different attribute from the one printed on the card, because a keeper's slots mean DIV/HAN/KIC/REF/SPD/POS where an outfielder's mean PAC/SHO/PAS/DRI/DEF/PHY. `attributeList` now prefers Core's `effective_attributes` and only falls back to the immutable definition when Core does not send them -- reading the definition regardless would silently drop every applied training off the card the client draws. Tests pin the verb split on `item/resource/<rid>` (POST applies, PUT stays quick-sell, GET is not an economy route at all), the digit guard, the exact JSON Core deserialises for both effects, and that no shipped training card exceeds the ceiling the host declares to Core.
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.