feat(host): name unclaimed requests, and record the FUT task vocabulary

Milestone 2 groundwork.

The passthrough arm forwarded to Python without ever recording WHAT was asked
for, so on staging -- where the upstream is deliberately dead -- an unhandled
request produced an anonymous 502. It now logs method, path and body length
before forwarding, which is how the next unclaimed route gets identified:

  utas-host owner=PYTHON route=passthrough method=GET path=/ut/... body_len=0

Also records the FUT TASK vocabulary read out of the live client. The client
drives UTAS through named tasks held in a CardsDLL .rdata table of 0x20-byte
MixedCase/UPPERCASE slots, with a .data descriptor table giving each a task id:

  ApplyCard 0x0d, ApplyCardByRes 0x0e, ConsumeCard, ActivateCard, AssingCard(sic),
  MoveCard, MoveCardByRes, SwapCard, DiscardCard, DiscardCardByRes, ViewCards, ...

So consumable application IS a first-class client action even though the route
table contains no /apply endpoint -- it must ride an existing route. The
descriptor's function pointer is a `mov [rip+flag], cl; ret` setter, not a
request builder, so the request is assembled elsewhere keyed by task id; that is
cheaper to answer with one live capture than with more static tracing.

Search tooling carries mandatory positive controls (tradePile, ut/%s/item, squad
-- all FOUND), so the "no /apply route" result is a valid negative rather than a
failed scan.

Host 120 lib tests, fmt and clippy clean.
This commit is contained in:
funman300
2026-08-22 00:14:11 +00:00
parent 0a7c4e129c
commit db5fb37980
2 changed files with 43 additions and 1 deletions
+31
View File
@@ -105,3 +105,34 @@ So applying a consumable is **not** a dedicated server route. If it reaches the
server at all it must ride `PUT ut/%s/item`, and L5/L6 should be pursued by
capturing that PUT's payload while applying a card — not by looking for an
endpoint that does not exist.
## FUT task vocabulary (2026-08-21, live)
The client drives UTAS through named TASKS, not just URLs. The task-name table
lives in CardsDLL `.rdata` as 0x20-byte inline slots holding MixedCase/UPPERCASE
pairs (`tools/apply_route_search.py`, controls `tradePile`/`ut/%s/item`/`squad`
all FOUND):
```
ViewCards AssingCard(sic) ApplyCard ApplyCardByRes
ActivateCard ConsumeCard DiscardCard DiscardCardByRes
DiscardACard MoveCard MoveCardByRes SwapCard
CreateMatch MatchReady DestroyMatch PlayGame ResetMatch KeepAlive
LoadCategoryDetails LoadSetChallenges StartChallenge LoadSquadChallenge
SaveSquadChallenge SubmitChallenge TagSets SetSbcData
TournamentList TournamentTeams SetUserInfo GetHistorical SetTutData ...
```
A descriptor table in `.data` pairs each name with a task id and a small setter
thunk, e.g. `ApplyCard` id **0x0d** at `0x1802cb170`, `ApplyCardByRes` id **0x0e**
at `0x1802cb1a0`. The thunks are `mov [rip+flag], cl; ret` (a per-task flag), NOT
request builders, so the request is assembled elsewhere keyed by task id.
**So consumable application IS a first-class client action (`ApplyCard` /
`ApplyCardByRes` / `ConsumeCard`), even though no `/apply` URL exists.** It
therefore rides an existing route. Which one is a one-capture question, and the
host now names every unclaimed request:
```
utas-host owner=PYTHON route=passthrough method=GET path=/ut/... body_len=N
```
+12 -1
View File
@@ -4022,10 +4022,21 @@ impl Server {
Route::MarketData => self.handle_marketdata(path, target),
Route::SecurityQuestion => self.handle_security_question(method, target, headers),
Route::Passthrough => {
// Name the request BEFORE forwarding. On staging the upstream is
// deliberately dead, so this line is the only record of what the
// client asked for -- which is exactly how an unclaimed route is
// discovered (see docs/CLIENT_ROUTE_SURFACE.md). Bodies are not
// logged; the path and method are enough to identify a route.
eprintln!(
"utas-host owner=PYTHON route=passthrough method={method} path={target} body_len={}",
body.len()
);
let resp = match self.pass.forward(method, target, headers, body) {
Ok(r) => r,
Err(e) => {
eprintln!("utas-host ERROR passthrough to Python failed: {e}");
eprintln!(
"utas-host ERROR passthrough failed method={method} path={target}: {e}"
);
WireResponse {
status: 502,
headers: vec![(