feat(remote-config): bind the fetch policy to the v2 gateway - #864
Closed
shameondev wants to merge 2 commits into
Closed
feat(remote-config): bind the fetch policy to the v2 gateway#864shameondev wants to merge 2 commits into
shameondev wants to merge 2 commits into
Conversation
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
This was referenced Aug 7, 2026
Contributor
Author
|
Консолидировано в release-train PR (один PR на репозиторий). Ветка сохранена. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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):
RemoteConfigGatewayTransportimplements the existingRemoteConfigFetchTransportseam driven byRemoteConfigFetchCoordinator— engine untouched; bytes+ETag go straight intoRemoteConfigSnapshotCore.admitCandidate(exact-byte handoff pinned on non-canonical JSON)POST /v3/remote-config-v2/session) + snapshot (POST /v3/remote-config-v2/snapshot) with the opaque session header and strong-ETagIf-None-Match; 401 → drop token → single re-bootstrap → retry; second 401 → typed failure, no loopsRemoteConfigSessionKey(scope, userUid);DeviceRemoteConfigClientContextProvidersourcesdevice_installed_atas a DEVICE fact surviving logout (targeting invariant from configurator CONTRACT.md), pinned by testRequest.Builder.headeron a dispatcher thread (stranded coordinator waiter + credential in the message) and an RFC3339Nano parse failure that would have silently re-bootstrapped every fetchVerification
./gradlew test+detektAll(the CI gate): BUILD SUCCESSFUL — agent run + independent re-run by the orchestrating sessionAccepted deviations documented in-code: Retry-After delta-seconds only;
Locale.toLanguageTag()normalization (no legacyiw/in); session environment recorded not compared (bootstrap returns env name, snapshot scope carries env uid — different namespaces).