File added.
Preview size limit exceeded, changes collapsed.
Implements OEG's side of the 7 operator-triggered outbound federation-lifecycle operations (ADR-0046 / ADR-0047 / ADR-0048) as a stateless, synchronous HTTP proxy to FM:
POST /partner, GET/DELETE /{federationContextId}/partner, GET /fed-context-id, POST /{federationContextId}/zones, GET/DELETE /{federationContextId}/zones/{zoneId}.
OEG forwards the raw request body to FM byte-for-byte and relays FM's response (status, body, content-type) back unaltered. The two POST bodies (POST /partner, POST /{federationContextId}/zones) are validated for shape against the GSMA schema before being forwarded — a request missing required fields is rejected with 400 and FM is never called — but the validated object is only a gate: what's actually sent to FM is always the caller's original bytes, not a rebuilt payload, so a field outside OEG's model still reaches FM unchanged. FM's response is never parsed against a schema, only checked for basic JSON well-formedness (→ 502 if unusable), and FM being unreachable → 503.
fm/requirements-and-design.md §L.3b says FM exposes "matching internal endpoints... under /internal/federation/" but doesn't name the per-operation paths.
In the absence of that table, I implemented OEG's client (fm_client.py) to mirror the operator-facing path 1:1 under that prefix. This is currently an assumption, not a confirmed contract — please confirm or correct it before this merges:
POST /partner -> POST /internal/federation/partnerGET /{federationContextId}/partner -> GET /internal/federation/{federationContextId}/partnerDELETE /{federationContextId}/partner -> DELETE /internal/federation/{federationContextId}/partnerGET /fed-context-id -> GET /internal/federation/fed-context-idPOST /{federationContextId}/zones -> POST /internal/federation/{federationContextId}/zonesGET /{federationContextId}/zones/{zoneId} -> GET /internal/federation/{federationContextId}/zones/{zoneId}DELETE /{federationContextId}/zones/{zoneId} -> DELETE /internal/federation/{federationContextId}/zones/{zoneId}Please also confirm this /internal/federation/* prefix is a separate surface from FM's public /operatorplatform/federation/v1/* EWBI path. Per ADR-0046 the two are meant to be distinct — different auth (platform service credential vs. partner fed-mgmt token) and /internal/* is never exposed through Nginx/Ingress. If FM's actual implementation instead reuses the public EWBI path for this, fm_client.py's base path needs to change to match.
application/problem+json ProblemDetails (ADR-0047) — OEG relays whatever FM sends unaltered.FederationRequestData.partnerCallbackCredentials.clientSecret passes through this call. OEG doesn't log or persist request/response bodies on this path — please confirm FM doesn't either, beyond what §S.4 requires.update_federation (PATCH /{federationContextId}/partner) — intentionally excluded per current requirements.federation-manager.yaml (currently v1.2.0, ~20 ops) against the "OPG.04 v6.0 / OpenAPI v1.4.0 / 74 operations" language in FM's own docs (ADR-0048 open item).Automated (in this MR)
tests/unit/test_fm_client.py — FmClient.relay transport behavior (method/URL/body/headers forwarded correctly; FM error responses passed through untouched; timeout/connection failure raise FmUnavailableError)tests/unit/test_federation_manager_endpoints.py — router-level relay contract, via a fake FmClient (no real network):
X-Correlator to FM and relay a successful FM response byte-for-byte409) relayed unaltered, status/body/content-type preserved503 ProblemDetails400, FM never calledPOST /partner, POST /{federationContextId}/zones) → 400, FM never called502operationIdsruff check cleanmypy clean (71 source files)Manual / pending FM's side
/internal/federation/* exists, to confirm the path mapping listed aboveapplication/problem+json ProblemDetails for its own errors, per ADR-0047partnerCallbackCredentials.clientSecret from the request/response bodyFile added.
Preview size limit exceeded, changes collapsed.