feat(host): capture unclaimed request bodies; record the consumable-apply wire

Milestone 2: the consumable-apply protocol is now LIVE_PROVEN.

Adds opt-in passthrough BODY logging (OPENFUT_FIFA17_LOG_PASSTHROUGH_BODY=1,
default off, capped at 512 bytes) because a body is what names an unknown
mutation's operands, while also being the one place a request could carry
something that should not reach a log. Staging probe only.

With it, one operator apply captured the whole thing:

    POST /ut/game/fifa17/item/resource/5001004
    {"apply":[{"id":100000003}]}

  source consumable : resource 5001004 (player contract, subtype 201) -- in the PATH
  target item(s)    : wire 100000003 (squad slot 0 GK, resourceId 200389) -- body apply[]
  verb              : POST

There is NO /apply endpoint, exactly as the static route work concluded. The
apply re-uses `ut/%s/item/resource`, which we already serve for GET (definition
lookup); the POST verb on that path is the mutation and nothing claimed it. This
is the wire form of the ApplyCardByRes task (id 0x0e), which is why the source is
a definition id rather than an instance id. `apply` is an array, so one resource
can name several targets.

Fail-closed verified: with the upstream dead the request 502s and Core is left
exactly unchanged -- coins 29,843,976, owned 1993, consumables 17, source card
still owned. No partial mutation.

NOT implemented: the response shape is unobserved and the EFFECT is unreversed.
Our catalog carries contract:7 for 5001004, documented as the matches granted,
but that is observed profile data (INFERRED), so no effect is written on it.

Bonus, caught by the same logging: the client really does request
`club/consumables/development`, which has no arm in
consumable_families_for_category and is served empty. Recorded, not guessed.

Host 120 lib tests, fmt clean.
This commit is contained in:
funman300
2026-08-22 00:23:15 +00:00
parent db5fb37980
commit 739228efdb
2 changed files with 79 additions and 2 deletions
+26 -2
View File
@@ -4025,12 +4025,23 @@ impl Server {
// 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.
// discovered (see docs/CLIENT_ROUTE_SURFACE.md).
eprintln!(
"utas-host owner=PYTHON route=passthrough method={method} path={target} body_len={}",
body.len()
);
// The BODY is what identifies an unknown mutation's operands, but
// it is also the one place a request can carry something we should
// not write to a log, so it is opt-in and capped. Staging probe
// only: OPENFUT_FIFA17_LOG_PASSTHROUGH_BODY=1.
if passthrough_body_logging() && !body.is_empty() {
let cap = body.len().min(512);
eprintln!(
"utas-host PASSTHROUGH-BODY path={target} bytes={} body={}",
body.len(),
String::from_utf8_lossy(&body[..cap])
);
}
let resp = match self.pass.forward(method, target, headers, body) {
Ok(r) => r,
Err(e) => {
@@ -4865,6 +4876,19 @@ fn commerce_settings_enabled() -> bool {
*ENABLED.get_or_init(|| std::env::var("OPENFUT_FIFA17_COMMERCE_SETTINGS").as_deref() == Ok("1"))
}
/// Whether to log unclaimed (passthrough) request BODIES.
///
/// OFF unless `OPENFUT_FIFA17_LOG_PASSTHROUGH_BODY=1`, and capped at 512 bytes.
/// A body is what names an unknown mutation's operands -- it is how the
/// consumable-apply payload was recovered -- but it is also the one place a
/// request could carry something that should not reach a log, so it is opt-in
/// and intended for staging probes only.
fn passthrough_body_logging() -> bool {
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ENABLED
.get_or_init(|| std::env::var("OPENFUT_FIFA17_LOG_PASSTHROUGH_BODY").as_deref() == Ok("1"))
}
/// Whether quick-sell pricing uses the client's own `fcc_discardcoins` table
/// instead of the legacy rating-only ladder.
///