Skip to content

feat(remote-config): bind the fetch policy to the v2 gateway - #864

Closed
shameondev wants to merge 2 commits into
codex/rc-v2-read-guardfrom
codex/rc-v2-transport
Closed

feat(remote-config): bind the fetch policy to the v2 gateway#864
shameondev wants to merge 2 commits into
codex/rc-v2-read-guardfrom
codex/rc-v2-transport

Conversation

@shameondev

Copy link
Copy Markdown
Contributor

What

Internal gateway transport adapter for Remote Config v2 (no public API, no DI wiring — dormant until the future wiring slice), stacked on the rc-v2 core (#863 tip):

  • RemoteConfigGatewayTransport implements the existing RemoteConfigFetchTransport seam driven by RemoteConfigFetchCoordinator — engine untouched; bytes+ETag go straight into RemoteConfigSnapshotCore.admitCandidate (exact-byte handoff pinned on non-canonical JSON)
  • bootstrap (POST /v3/remote-config-v2/session) + snapshot (POST /v3/remote-config-v2/snapshot) with the opaque session header and strong-ETag If-None-Match; 401 → drop token → single re-bootstrap → retry; second 401 → typed failure, no loops
  • session persisted per RemoteConfigSessionKey(scope, userUid); DeviceRemoteConfigClientContextProvider sources device_installed_at as a DEVICE fact surviving logout (targeting invariant from configurator CONTRACT.md), pinned by test
  • adversarial review during development found and fixed 4 real defects, incl. a token-bearing exception thrown from Request.Builder.header on a dispatcher thread (stranded coordinator waiter + credential in the message) and an RFC3339Nano parse failure that would have silently re-bootstrapped every fetch

Verification

  • ./gradlew test + detektAll (the CI gate): BUILD SUCCESSFUL — agent run + independent re-run by the orchestrating session
  • 38 new tests: MockWebServer request-shape/exact-bytes/304/401-retry matrix (27), real-coordinator integration over a real socket (3), persistent session store (6), Robolectric device context (2)

Accepted deviations documented in-code: Retry-After delta-seconds only; Locale.toLanguageTag() normalization (no legacy iw/in); session environment recorded not compared (bootstrap returns env name, snapshot scope carries env uid — different namespaces).

Implement the internal Remote Config v2 transport adapter behind the existing
RemoteConfigFetchTransport seam, so the resilient fetch policy can talk to the
dark gateway routes without any public API change.

- Bootstrap on a missing or expired session (POST /v3/remote-config-v2/session)
  and read the snapshot (POST /v3/remote-config-v2/snapshot) with the session
  header and the coordinator's exact If-None-Match validator.
- A snapshot 401 drops the session and re-bootstraps exactly once; a second 401
  is a typed failure, so the flow cannot loop.
- The response body reaches durable admission as the exact bytes received,
  paired with the exact ETag: no decode, re-encode or charset round trip.
- Sessions are stored per identity scope under a salted digest key, so an
  identity change addresses a different record and can never reuse the previous
  identity's token. Neither token is ever logged.
- device_installed_at is sourced from PackageManager.firstInstallTime, a device
  fact that survives logout: the server takes min(device_installed_at,
  client.created_at), so a moving value would make a long-time user look new.

Tests use MockWebServer (new test-only dependency, pinned to the SDK's OkHttp
version) and cover the wire shape of both routes, byte equality on a
non-canonical body, 304, 401 recovery and the no-loop bound, 404/503, session
scoping, and the timeout path through the real coordinator.

Claude-Session: https://claude.ai/code/session_018pHXqfbxkMQJFzUZ3jW4A8
Adversarial review of the adapter found four real defects and several
weaker-than-claimed tests. All are addressed here.

- A session or project token containing a non-printable byte reached
  Request.Builder.header, which throws — on an OkHttp dispatcher thread, from
  the mint callback and the 401 retry. That both stranded the coordinator's
  waiter (no completion) and put the credential into the exception message, so
  a token could reach a crash reporter. Tokens are now validated as HTTP header
  values, and request building is failure-typed instead of throwing.
- The RFC3339 expiry parser only accepted exactly three fractional digits and
  an uppercase T/Z, so a Go gateway's RFC3339Nano timestamp parsed as "unknown"
  and every fetch silently re-bootstrapped forever. Replaced with a parser that
  accepts 0-9 fractional digits, either case, and numeric offsets.
- A dead in-memory session shadowed the durable record and skipped its cleanup.
- The session store is now keyed by the anonymous uid the session was minted
  for, not only by the snapshot scope, so a re-minted uid under an unchanged
  scope can never replay the previous identity's token.

Also: bounded (and injectable) snapshot body read, an empty 200 body is a typed
failure rather than an empty admission, Accept header, BCP-47 locale so Hebrew
and Indonesian are not sent as the legacy iw/in codes, and MockWebServer pinned
to the OkHttp version that actually resolves rather than the declared one.

New coverage: non-printable tokens, fractional/lowercase expiry, over-budget
and empty bodies, in-memory session reuse, concurrent fetches each answered
exactly once, and a re-minted uid addressing a different session record.

Claude-Session: https://claude.ai/code/session_018pHXqfbxkMQJFzUZ3jW4A8
@shameondev

Copy link
Copy Markdown
Contributor Author

Консолидировано в release-train PR (один PR на репозиторий). Ветка сохранена.

@shameondev shameondev closed this Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant