feat(host): migrate flag-off UTAS reads (season/tournament/champion/clubUser/user-list) to Rust
FUT modes (Seasons/Tournaments/FUT Champions) and the club-identity service are
disabled in this emulator, so these GET reads return {} verbatim from the Python
oracle. Serve them directly from Rust via a new Route::FeatureOffEmpty +
non_economy::feature_off_body() -> {} (byte-identical to the flag-off oracle),
reducing the proxied Python surface.
- The mutating club rename (user/club) stays on Python (needs a Core write).
- Enabling a mode later requires a real Rust handler here, never a Python
fallback (no split authority).
- classify tests: 5 routes owned + user/club/wrong-method lookalikes stay
Passthrough; updated the stale clubUser assertion.
- Deployed to prod-host 2026-08-17; verified owner=RUST 200 {} for all five.
Docs: PRODUCTION_AUTHORITY_MATRIX + PYTHON_RETIREMENT_PLAN updated.
This commit is contained in:
@@ -53,16 +53,17 @@ Legend: owner R = Rust/Core, P = Python oracle (:8199 proxied via PYTHON_FALLBAC
|
||||
| GET /club/stats/{year,consumables,staff,country,league,team} | R | Core aggregation; context buckets keyed nation/league/team (owned=1982); staff={} |
|
||||
| GET /store, /match/keepalive, /captcha, /tfa, /livemessage, /activeMessage | R | unconditional constant acks (byte-identical to the oracle; StaticAck route) |
|
||||
| GET /watchList (+ PUT/POST/DELETE) | R | empty watch list + authoritative Core credits; add/remove is a no-op ack (oracle persists none) |
|
||||
| GET /season, /tournament, /champion, /clubUser, /user/list | R | FUT modes + club-identity off → `{}` (FeatureOffEmpty; byte-identical to the flag-off oracle). Migrated + deployed 2026-08-17 |
|
||||
| POST /ut/.../match/end (DestroyMatch) | R | economy reward (coins credited via Core grant_reward) |
|
||||
|
||||
### NON-ECONOMY — still Python (PYTHON_FALLBACK)
|
||||
| Method/path | Owner | Reason |
|
||||
|---|---|---|
|
||||
| GET /item/resource, /defid | P | `item_def(rid)` shapes `{itemData:[…]}` from `PLAYER_DEFS` (asset=rid&0xffffff → name/rating/pos/attrs) + consumable defs, placeholder fallback ("Player",75,attrs 70). Faithful Rust needs those def tables in the host. Shape captured: `docs/evidence/route-shapes-2026-08-17/defs_resource.json` |
|
||||
| GET /clubUser, /user/list, /user/club | P | club identity (off→{}, flag-gated) + rename (mutating); Core has `clubs` but rename needs a Core write |
|
||||
| POST /user/club (rename) | P | mutating club rename; Core has `clubs` but rename needs a Core write (deferred). Reads `clubUser`/`user/list` are now Rust. |
|
||||
| GET /squad/<n> (n≠0, non-active) | P | no multi-squad Core model (Rust owns squad/0, squad/active, /squad/list, PUT) |
|
||||
| /squad/mode/draft/* | P | FUT Draft mode |
|
||||
| GET /season, /tournament, /champion, /leaderboards, /sbs/* | P | mode-gated (FUT_MODES/_SBC off by default → `{}`); real behavior needs the mode logic ported |
|
||||
| GET /leaderboards, /sbs/* | P | mode-gated (FUT_MODES/_SBC off → `{}`/content; `/sbs/sets` ships content); real behavior needs the mode logic ported. `season`/`tournament`/`champion` are now Rust. |
|
||||
| GET /marketdata (+ /marketdata/pricelimits) | P | suggested pricing; freeze-risk (array vs object container); Python-correct, constant band 150..15000 |
|
||||
| POST /ut/.../match (CREATE), /match/ready (READY), /match (PLAY) | P | match handshake legs; no match ever played in-game (see docs/MATCH_LIFECYCLE.md) |
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ rollback-to-python-p2.sh, :p2-rollback image b1b929953f, profile 39bb3e83).
|
||||
## A. LIVE PRODUCTION REQUIRED (still owns behavior in the live flow)
|
||||
| Component | Role | Rust status | Retire when |
|
||||
|---|---|---|---|
|
||||
| oracle utas_server.py (:8199) NON-ECONOMY | **remaining**: item-defs (item/resource,defid), user-identity (user,clubUser,user/list,user/club), non-active squad/<n>, draft, watchList, marketdata, mode-gated (season/tournament/champion/leaderboards/sbs, off→{}) | **Most non-economy migrated 2026-08-17** (account/sync, auth, userMassInfo, clientdata, hub, club/stats/*, settings, accountinfo, phishing, match/reset, leaderboards/options, static acks — all Rust). See PRODUCTION_AUTHORITY_MATRIX | remaining tail migrated (needs live captures for the data routes; mode-logic port for the gated ones) OR mode-gated ones kept Python |
|
||||
| oracle utas_server.py (:8199) NON-ECONOMY | **remaining**: item-defs (item/resource,defid), user/club rename, non-active squad/<n>, draft, marketdata, mode-gated leaderboards + /sbs/* | **Most non-economy migrated 2026-08-17** (account/sync, auth, userMassInfo, user, clientdata, hub, club/stats/*, settings, accountinfo, phishing, match/reset, leaderboards/options, watchList, static acks, and the flag-off empty reads season/tournament/champion/clubUser/user/list → `{}` — all Rust). See PRODUCTION_AUTHORITY_MATRIX | remaining tail migrated (needs live captures for the data routes; mode-logic port for the gated ones) OR mode-gated ones kept Python |
|
||||
| blaze_responder_v3b.py (:42130 Blaze) | FUT Blaze transport | Rust openfut-blaze-host COMPLETE, gate-proven (Gate 10) | container cutover (operator-gated deploy) |
|
||||
| blaze_responder_v3b.py (:42127 redirector TLS) | first-hop TLS redirect | Rust openfut-redirector-host COMPLETE (OpenSSL) | container cutover + cert consistency |
|
||||
| roster_server.py (:8081) | roster-update XML | Rust openfut-roster-host COMPLETE (unit-only) | container cutover |
|
||||
@@ -34,10 +34,10 @@ rollback-to-python-p2.sh, :p2-rollback image b1b929953f, profile 39bb3e83).
|
||||
|
||||
## Retirement gating
|
||||
1. **Mostly DONE (2026-08-17)**: account/sync, ut/auth (Rust SID mint), userMassInfo (full), clientdata,
|
||||
club/stats/{country,league,team}, and the trivial static acks migrated + deployed. **Remaining on Python**:
|
||||
item-defs, user-identity (user/clubUser/user-list/user-club), non-active squad/<n>, draft, watchList,
|
||||
marketdata (no captured wire shape), and mode-gated season/tournament/champion/leaderboards/sbs
|
||||
(return {} while FUT_MODES/_SBC off). Migrate the data ones once live shapes are captured.
|
||||
club/stats/{country,league,team}, watchList, static acks, and the flag-off empty reads
|
||||
(season/tournament/champion/clubUser/user/list → `{}`) migrated + deployed. **Remaining on Python**:
|
||||
item-defs, user/club rename, non-active squad/<n>, draft, marketdata (freeze-risk), and mode-gated
|
||||
leaderboards + /sbs/* (need the mode logic ported). Migrate the data ones once live shapes are captured.
|
||||
2. Deploy Rust blaze/roster/redirector via container cutover (operator-gated) — then those Python responders
|
||||
are class D/E only.
|
||||
3. POW: build Rust host + reverse bodies (largest blocker) OR keep Python POW as class A indefinitely.
|
||||
|
||||
@@ -46,6 +46,16 @@ pub fn club_stats_staff_body() -> Value {
|
||||
json!({})
|
||||
}
|
||||
|
||||
/// The FUT modes (Seasons / Tournaments / FUT Champions) and the club-identity
|
||||
/// service are disabled in this emulator, so their read routes (`season`,
|
||||
/// `tournament`, `champion`, `clubUser`, `user/list`) return an empty object —
|
||||
/// byte-identical to the Python oracle with `FUT_MODES` / `FUT_CLUB_IDENTITY`
|
||||
/// off. Enabling a mode later requires a real Rust implementation here, never a
|
||||
/// Python fallback (which would reintroduce split authority).
|
||||
pub fn feature_off_body() -> Value {
|
||||
json!({})
|
||||
}
|
||||
|
||||
/// The phishing/security-question action, parsed from the URL tail.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SecurityAction {
|
||||
|
||||
@@ -146,6 +146,13 @@ pub enum Route {
|
||||
/// `GET …/user` — the FUT user profile `{"userInfo": …}`, the same userInfo
|
||||
/// object `userMassInfo` embeds (shared builder). POST /user (create) stays Python.
|
||||
User,
|
||||
/// `GET …/{season,tournament,champion,clubUser,user/list}` — FUT modes and
|
||||
/// the club-identity service are disabled in this emulator, so these reads
|
||||
/// return `{}`, byte-identical to the Python oracle with `FUT_MODES` /
|
||||
/// `FUT_CLUB_IDENTITY` off. The mutating club-identity route (`user/club`
|
||||
/// rename) stays on Python. Turning a feature on later means a real Rust
|
||||
/// handler here — never a Python fallback.
|
||||
FeatureOffEmpty,
|
||||
/// Anything else — proxied verbatim to the Python oracle.
|
||||
Passthrough,
|
||||
}
|
||||
@@ -206,6 +213,11 @@ pub fn classify(method: &str, path: &str) -> Route {
|
||||
Some("livemessage") => Route::StaticAck,
|
||||
Some("activeMessage") => Route::StaticAck,
|
||||
Some("watchList") => Route::WatchList,
|
||||
Some("season") if get => Route::FeatureOffEmpty,
|
||||
Some("tournament") if get => Route::FeatureOffEmpty,
|
||||
Some("champion") if get => Route::FeatureOffEmpty,
|
||||
Some("clubUser") if get => Route::FeatureOffEmpty,
|
||||
Some("user/list") if get => Route::FeatureOffEmpty,
|
||||
_ => Route::Passthrough,
|
||||
}
|
||||
}
|
||||
@@ -2329,6 +2341,7 @@ impl Server {
|
||||
Route::StaticAck => self.handle_static_ack(path),
|
||||
Route::WatchList => self.handle_watchlist(method),
|
||||
Route::User => self.handle_user(),
|
||||
Route::FeatureOffEmpty => self.handle_feature_off_empty(path),
|
||||
Route::SecurityQuestion => self.handle_security_question(method, target, headers),
|
||||
Route::Passthrough => {
|
||||
let resp = match self.pass.forward(method, target, headers, body) {
|
||||
@@ -2717,6 +2730,16 @@ impl Server {
|
||||
json_status(200, &json!({ "auctionInfo": [], "credits": credits, "total": 0 }))
|
||||
}
|
||||
|
||||
/// `GET …/{season,tournament,champion,clubUser,user/list}` — FUT modes and the
|
||||
/// club-identity service are off, so return `{}` (parity with the flag-off
|
||||
/// Python oracle). The mutating `user/club` rename is NOT here — it stays on
|
||||
/// Python until a real Rust club-identity handler exists.
|
||||
fn handle_feature_off_empty(&self, path: &str) -> WireResponse {
|
||||
let tail = ut_tail(path).unwrap_or("");
|
||||
eprintln!("utas-host owner=RUST route=feature-off-empty tail={tail} status=200");
|
||||
json_status(200, &non_economy::feature_off_body())
|
||||
}
|
||||
|
||||
/// `GET …/club/stats/<mode>` — the MY CLUB stat set, computed Core-accurately
|
||||
/// in Rust (no Python). Player tiers + rare from the Core collection,
|
||||
/// staff/consumable families from the catalog kind+subtype, and per-context
|
||||
@@ -3323,9 +3346,10 @@ mod tests {
|
||||
classify("GET", "/ut/game/fifa17/club/stats/country/54"),
|
||||
Route::ClubStats
|
||||
);
|
||||
// clubUser is now Rust-owned (`{}` while club identity is off).
|
||||
assert_eq!(
|
||||
classify("GET", "/ut/game/fifa17/clubUser"),
|
||||
Route::Passthrough
|
||||
Route::FeatureOffEmpty
|
||||
);
|
||||
assert_eq!(
|
||||
classify("GET", "/ut/game/fifa17/tradePile"),
|
||||
@@ -3835,6 +3859,11 @@ mod tests {
|
||||
Route::ClientData,
|
||||
),
|
||||
("POST", "/openfut/account/sync", Route::AccountSync),
|
||||
("GET", "/ut/game/fifa17/season", Route::FeatureOffEmpty),
|
||||
("GET", "/ut/game/fifa17/tournament", Route::FeatureOffEmpty),
|
||||
("GET", "/ut/game/fifa17/champion", Route::FeatureOffEmpty),
|
||||
("GET", "/ut/game/fifa17/clubUser", Route::FeatureOffEmpty),
|
||||
("GET", "/ut/game/fifa17/user/list", Route::FeatureOffEmpty),
|
||||
];
|
||||
for (m, p, want) in owned {
|
||||
assert_eq!(classify(m, p), *want, "OWN: {m} {p}");
|
||||
@@ -3845,6 +3874,8 @@ mod tests {
|
||||
("POST", "/ut/game/fifa17/match/reset"), // match/reset is PUT-only
|
||||
("GET", "/ut/game/fifa17/match/reset"),
|
||||
("PUT", "/ut/game/fifa17/user/accountinfo"),
|
||||
("POST", "/ut/game/fifa17/season"), // FUT-mode reads are GET-only
|
||||
("GET", "/ut/game/fifa17/user/club"), // mutating rename stays Python
|
||||
];
|
||||
for (m, p) in proxied {
|
||||
assert_eq!(classify(m, p), Route::Passthrough, "PROXY: {m} {p}");
|
||||
|
||||
Reference in New Issue
Block a user