docs: align R2 federation and instance lifecycle contracts

Where each amended contract lands in code (all opened 2026-10-15):

flowchart LR
  subgraph spec["architecture !15 (this MR)"]
    A51["ADR-0051<br/>agreements via OEG relay"]
    A68["ADR-0068<br/>Helm apps over EWBI"]
    A53["ADR-0053 + SRM F.4a<br/>partner instance lifecycle"]
    RD["OEG instance read<br/>SRM + saved failed attempts"]
  end
  A51 -- "POST/PATCH /internal/agreements<br/>GET /internal/local-zones" --> FM35["FM !35"]
  A51 -- "ZONES ADD/REMOVE callback" --> FM36["FM !36"]
  A51 -- "/platform/v1/federation/agreements" --> OEG42["OEG !42"]
  A51 -- "Agreements dialog" --> P15["portal !15"]
  A68 -- "HELM artefact + DOCKER files" --> FM37["FM !37"]
  A53 -- "GET /internal/federated-service-instances/{id}" --> SRM37["SRM !37"]
  RD -- "GET /appinstances merge" --> OEG43["OEG !43"]

The decision that needs explicit review, the instance read:

sequenceDiagram
  participant AP as App Provider
  participant OEG
  participant DB as OEG Postgres
  participant SRM
  AP->>OEG: GET /appinstances?appId=&region=
  OEG->>SRM: GET /internal/service-instances
  SRM-->>OEG: runtime rows (SRM stays the runtime source)
  OEG->>DB: failed attempts of this App Provider, confirmed deletions
  DB-->>OEG: saved rows
  Note over OEG: add failed attempts with no endpoints or cluster refs<br/>drop rows OEG confirmed terminated<br/>same instance in both, SRM row wins
  OEG-->>AP: 200 merged list

Record the contracts exercised by the Release 2 portal test: agreement management through OEG, local-zone selection, partner zone callbacks and subscription cleanup, and Helm onboarding over EWBI. Clarify that SRM reads FM for the zone of a partner-hosted instance while leaving its local-inventory foreign key null.

The instance-read amendment needs an explicit design review: OEG supplements SRM runtime results with its own saved failed attempts, scoped by the originating operation's App Provider and the request filters. It omits runtime rows that OEG has confirmed as terminated. SRM remains the runtime source; OEG invents no endpoints or partner inventory.

Review ADR-0051, ADR-0053, ADR-0068, FM requirements, OEG instance flow/interface, and SRM F.4a together. This is the companion specification for the code MRs below, particularly OEG instance visibility and SRM termination.

Validation: documentation diff checks passed. The described combined behavior was exercised on three operator test clusters; individual implementation and integration results belong to the component MRs.

Edited by Sergio Gimenez

Merge request reports

Loading
+21 −3
Changes for decisions/0051-oeg-fronts-federation-lifecycle-ewbi-byte-for-byte.md: 21 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -9,8 +9,8 @@ status: "accepted"

# ADR-0051: OEG fronts eight GSMA federation-lifecycle operations on its own platform namespace, scoped by partner id, forwarding the raw payload byte-for-byte to FM (operator-triggered outbound only)

**Status:** accepted — amended 2026-09-15 after FM-side review of the first version, and 2026-09-24 to add partner registration to the OEG proxy (see "Partner registration and our identity"), and 2026-10-13 to add the identity read. Originally drafted as ADR-0048 (and its error-model companion as ADR-0047); renumbered to 0051/0050 at 2026-09-16 to clear the collision with `develop`'s ADR-0047 (terminal-record-read) and `docs/platform-roles`'s ADR-0048 (platform-roles) — content unchanged by the renumbering.
**Date:** 2026-09-08 (amended 2026-09-15, 2026-09-24, 2026-10-13)
**Status:** accepted — amended 2026-09-15 after FM-side review of the first version, and 2026-09-24 to add partner registration to the OEG proxy (see "Partner registration and our identity"), 2026-10-13 to add the identity read, and 2026-10-15 to add agreements and our zone list (see "Agreements and our zones"), and 2026-10-16 so a partner's one agreement is edited in place (the `PATCH` row there). Originally drafted as ADR-0048 (and its error-model companion as ADR-0047); renumbered to 0051/0050 at 2026-09-16 to clear the collision with `develop`'s ADR-0047 (terminal-record-read) and `docs/platform-roles`'s ADR-0048 (platform-roles) — content unchanged by the renumbering.
**Date:** 2026-09-08 (amended 2026-09-15, 2026-09-24, 2026-10-13, 2026-10-15, 2026-10-16)
**Affects:** [oeg/architecture.md](/oeg/architecture.md), [oeg/interface-contract.md](/oeg/interface-contract.md), [fm/requirements-and-design.md](/fm/requirements-and-design.md), [fm/implementation-and-delivery.md](/fm/implementation-and-delivery.md), [fm/overview-and-architecture.md](/fm/overview-and-architecture.md), [platform/architecture.md](/platform/architecture.md), amends this ADR's own first version, restores the namespace part of [ADR-0046](/decisions/0046-oeg-fronts-federation-lifecycle-ewbi-as-sync-proxy.md) it had superseded, builds on [ADR-0050](/decisions/0050-oeg-federation-lifecycle-proxy-errors-are-gsma-problem-json.md), [ADR-0042](/decisions/0042-ewbi-oauth2-client-credentials-mtls-deferred.md)

## Context
@@ -80,12 +80,30 @@ so OEG proxies it on the same namespace and under the same rules as the eight op
There is no `DELETE`: partners are retired via `status`, never removed (FM §L.2). The body is FM's
own L.2 partner schema, not a GSMA schema — it is forwarded byte-for-byte like every other operation
here, and since it can carry the partner's client secret (ADR-0067), the no-body-logging rule below
applies to it unchanged. Federation agreements and routing rules stay on FM's L.2 API.
applies to it unchanged. Routing rules stay on FM's L.2 API.

`GET /identity` exists because FM accepts `CreateFederation` only with our own identity
(`partnerStatusLink`, `origOPFederationId`, `origOPCountryCode`, and MNCs among ours) and fills in
the optional fields left out; the operator reads those values here instead of typing them.

### Agreements and our zones (added 2026-10-15)

A partner sees none of our zones and can deploy nothing until an agreement grants them (FM §L.2,
REQ-FED-05). The hosting operator records that agreement, so it belongs in the same operator
workflow as registering the partner; without a relay the only way to write one was `kubectl exec`
into FM. OEG proxies it the same way, plus the read the operator needs to pick zones:

| OEG path (under `/platform/v1/federation`) / FM path (under `/internal`) | FM action |
|---|---|
| `POST /agreements` | record what a partner may use here: our zones, the GSMA terms offered in each, the requests allowed, validity |
| `PATCH /agreements/{agreementId}` | change any of its terms in place: a partner has one agreement (a second `POST` answers `409`) |
| `GET /local-zones` | our own zones, the ones an agreement can offer (partner zones left out) |

Agreements are read embedded in `GET /partners/{partnerOpId}`, as FM §L.2 already had it; there is
no agreement list or `DELETE`, since agreements are retired through `status`. The bodies are FM's
L.2 schema, forwarded byte-for-byte; FM validates them and answers a bad one with a `400`
`ProblemDetails` naming the field, like a partner registration.

### The eight operations

OEG serves these under its own platform namespace, `/platform/v1/federation`, gated by an operator
+12 −3
Changes for decisions/0053-srm-fm-federated-lifecycle-handoff-over-the-databus.md: 12 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -9,7 +9,7 @@ status: "proposed"

# ADR-0053: SRM hands federated deploys and terminates to FM over the DataBus, and FM reports the outcome the same way

**Status:** proposed (amended 2026-09-25: SRM keeps instance rows for partner targets, superseded on 2026-10-08; 2026-09-29: `partner_not_found`; 2026-10-08: order-local `federated_handoff` instead of partner inventory)
**Status:** proposed (amended 2026-09-25: SRM keeps instance rows for partner targets, superseded on 2026-10-08; 2026-09-29: `partner_not_found`; 2026-10-08: order-local `federated_handoff` instead of partner inventory; 2026-10-15: SRM reads FM's instance before a federated terminate)
**Date:** 2026-09-22
**Affects:** [fm/overview-and-architecture.md](/fm/overview-and-architecture.md) (open item G4, UC-FM-07), [fm/requirements-and-design.md](/fm/requirements-and-design.md) (the SRM→FM hand-off section §L.3a, FM's DataBus sections §K.2 and §M), [srm/interface-contract.md](/srm/interface-contract.md) (stream configuration §D, the federated hand-off flow §F.4a), [srm/persistence-model.md](/srm/persistence-model.md) (`service_order.federated_handoff`), [platform/architecture.md](/platform/architecture.md) (DataBus configuration)
**Amends:** ADR-0001 and ADR-0022, where they make SRM the only consumer of `OOP_TASKS`; ADR-0046, where it keeps the DataBus to `command.srm.*` / `event.srm.*`
@@ -120,8 +120,17 @@ isn't built for request/reply.
| `command.fm.service.terminate` | `OOP_TASKS` | the `SrmServiceTerminateV1` SRM received, unshaped | the instance is absent from SRM's local inventory (partner-hosted; SRM never wrote a partner `service_instance`) |

The envelope keeps the original `operation_id` and `correlation_id`. SRM does no GSMA shaping; FM
alone maps to `InstallApp` and `RemoveApp` (ADR-0044). Terminate has no lookup step: FM resolves the
instance from its own `federation_transactions` records.
alone maps to `InstallApp` and `RemoveApp` (ADR-0044). FM resolves the partner instance from
its own `federation_transactions` records.

**2026-10-15 amendment:** before accepting a terminate for an instance absent locally, SRM
reads `GET /internal/federated-service-instances/{service_instance_id}` to retain its `zone_id`
in the order-local hand-off. This lets the terminal SRM event satisfy §C.2 without a partner
`service_instance` row. If FM doesn't know the instance, SRM fails before starting; if FM can't be reached, the command
is redelivered, not failed. The order's
`target_service_instance_id` stays null because that column references local inventory. The
request to FM remains the original command; SRM does not select an EWBI target or store partner
inventory.

Two rules make this durable:

+18 −5
Changes for decisions/0068-fm-onboards-apps-at-partners-over-ewbi-before-installapp.md: 18 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -9,8 +9,8 @@ status: "proposed"

# ADR-0068: FM onboards an application at a partner over the EWBI before the first `InstallApp`, and the partner resolves `InstallApp` from that onboarding

**Status:** proposed
**Date:** 2026-09-25
**Status:** proposed — amended 2026-10-15 to onboard Helm units (see "Helm units")
**Date:** 2026-09-25 (amended 2026-10-15)
**Affects:** [fm/requirements-and-design.md](/fm/requirements-and-design.md) (outbound deploy path, inbound onboarding operations), [fm/persistence-model.md](/fm/persistence-model.md) (`shared_resource_catalogues` resource types, `federation_agreements.zone_resources`), [ADR-0018](0018-federation-tenancy-partner-as-tenant.md) (the agreement's app mapping), [ADR-0054](0054-fm-receives-gsma-callbacks-through-one-router.md) (one more callback kind)

## Context
@@ -63,8 +63,20 @@ is treated as success.
**Only public repositories in this release.** A deployment unit whose image sits in a private
repository is refused before anything is sent. GSMA would carry the pull credential inside
`fileRepoLocation`, but OEG does not keep the App Provider's credential today (it stores only a
`secret://` reference with no store behind it), so there is nothing to send. Container units come
first; Helm units need image references SRM does not record yet.
`secret://` reference with no store behind it), so there is nothing to send.

**Helm units (added 2026-10-15).** TF-SDK's Kubernetes adapter runs only Helm, so a container-only
onboarding left no CAMARA application deployable on a partner. A Helm unit goes to the partner as
an `UploadArtefact` with descriptor type `HELM`, `repoType: PUBLICREPO` and
`artefactRepoLocation.repoURL` set to the chart. GSMA still requires every component to name its
images as uploaded files (`componentSpec.images`, at least one), and SRM records only the chart, so
FM-A reads the chart's `values.yaml` for `image` blocks (`registry`, `repository`, `tag`, with
`Chart.yaml`'s `appVersion` as the default tag; subcharts are left to their own release) and sends
one `UploadFile` per image, `fileType: DOCKER`, by reference as above. It doesn't render the
chart. A chart that isn't an http(s) archive, can't be fetched, or declares no image that way is
refused before anything is sent. FM-B accepts a public `HELM` artefact and turns it into one helm
deployment unit with the chart as `artifact_ref`, adding up its components' CPU and memory, since
one release deploys them all.

**`zoneInfo.flavourId` comes from the zone subscription.** FM-A stores the `flavoursSupported` a
partner returned on `ZoneSubscribe` and uses one of them. A zone FM-A has not subscribed to is not
@@ -102,7 +114,8 @@ specification, version, `appId`, state). FM-B keeps `file`, `artefact` and `appl
  deploys cost none.
- Deleting a specification in SRM-A does not yet deboard it at partners (`DeleteApp` is not
  called). That is left for a later change.
- Private-repository images and Helm units are refused for now, with an error that says so.
- Private-repository images are refused for now, with an error that says so; so are Helm charts
  whose images FM can't read from `values.yaml`.
- The agreement's `app_mappings` stays as the fallback for `InstallApp` until every partner
  onboards over the EWBI, and can be removed then.

+3 −3
Changes for decisions/index.md: 3 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -51,8 +51,9 @@ the reasoning lives here. Never renumber; supersede instead (see the record-deci
* [ADR-0046](0046-oeg-fronts-federation-lifecycle-ewbi-as-sync-proxy.md) - operator-triggered federation establishment and zone subscription proxy synchronously through OEG to FM (no DataBus, no internal schema); scope and payload unchanged from ADR-0043, inbound EWBI stays entirely with FM (supersedes ADR-0043's transport; scope/payload/namespace corrected by ADR-0051)
* [ADR-0047](0047-terminal-record-read-follows-camara-schema.md) - terminal-record read behaviour follows each family's own vendored CAMARA schema: Traffic Influence returns 200 with `state: deleted`, QoD stays 404 (amends ADR-0034's blanket 404 rule and its 204 delete code)
* [ADR-0048](0048-platform-roles.md) - three realm roles (app-provider, operator, platform-admin); new users get app-provider but call only the API products granted to them (each CAMARA scope gated by a product role); tenancy is the `organization` claim, else `sub`; development mode switches login off on localhost (amends ADR-0006's tenant identity and ADR-0035's entitlement note)
* [ADR-0049](0049-data-lab-standalone-profile-shared-ingress-and-iam.md) - Data Lab is a standalone compose profile behind the platform Nginx and Keycloak (realm `dataspace`); no DataBus/SRM/OEG coupling in the catalogue release
* [ADR-0050](0050-oeg-federation-lifecycle-proxy-errors-are-gsma-problem-json.md) - OEG's federation-lifecycle proxy endpoints (§N.7, ADR-0046) return OPG.04 `application/problem+json` `ProblemDetails` for every error - FM's errors relayed unchanged, OEG's own 400/401/403/502/503 from a fixed template; ADR-0030's `ErrorInfo` rule is scoped to OEG's CAMARA northbound surface
* [ADR-0051](0051-oeg-fronts-federation-lifecycle-ewbi-byte-for-byte.md) - OEG fronts eight GSMA federation-lifecycle operations on its own platform namespace, `/platform/v1/federation/partners/{partnerOpId}/federations/...`, scoped by a registered `partnerOpId` (create/read/update/delete federation, fed-context-id lookup, zone list/subscribe/details/unsubscribe), forwarding the raw GSMA request/response byte-for-byte to FM including the query string, pinned to GSMA OPG.04 v6.0 / artifact v1.4.0; operator-triggered outbound only, separated from inbound EWBI at the ingress; also proxies partner registration (`POST`/`GET`/`PATCH` `/platform/v1/federation/partners[/{partnerOpId}]` onto FM's L.2, amended 2026-09-24) and our own federation identity (`GET /platform/v1/federation/identity`, amended 2026-10-13); keeps ADR-0046's sync-HTTP/stateless/no-DataBus transport and ADR-0050's error model (amends this ADR's own first version, which used the bare canonical OPG.04 paths and lost the partner addressing GSMA gets for free from the URL)
* [ADR-0051](0051-oeg-fronts-federation-lifecycle-ewbi-byte-for-byte.md) - OEG fronts eight GSMA federation-lifecycle operations on its own platform namespace, `/platform/v1/federation/partners/{partnerOpId}/federations/...`, scoped by a registered `partnerOpId` (create/read/update/delete federation, fed-context-id lookup, zone list/subscribe/details/unsubscribe), forwarding the raw GSMA request/response byte-for-byte to FM including the query string, pinned to GSMA OPG.04 v6.0 / artifact v1.4.0; operator-triggered outbound only, separated from inbound EWBI at the ingress; also proxies partner registration (`POST`/`GET`/`PATCH` `/platform/v1/federation/partners[/{partnerOpId}]` onto FM's L.2, amended 2026-09-24) our own federation identity (`GET /platform/v1/federation/identity`, amended 2026-10-13), and agreements with the zones they can offer (`/platform/v1/federation/agreements[/{agreementId}]`, `/platform/v1/federation/local-zones`, amended 2026-10-15); keeps ADR-0046's sync-HTTP/stateless/no-DataBus transport and ADR-0050's error model (amends this ADR's own first version, which used the bare canonical OPG.04 paths and lost the partner addressing GSMA gets for free from the URL)
* [ADR-0053](0053-srm-fm-federated-lifecycle-handoff-over-the-databus.md) - SRM hands federated deploys and terminates to FM as `command.fm.service.deploy` / `.terminate` on `OOP_TASKS`, and FM reports the outcome as `event.fm.operation.completed` on `OOP_EVENTS`; zone lookups stay synchronous HTTP reads. `completion_basis` separates a confirmed deploy from a terminate the partner only accepted. SRM writes a `service_order` and tracks pending partner targets in order-local `federated_handoff` (no partner zone/instance inventory); `payload_snapshot` stays frozen command audit; completes the order from FM's answer. FM waits 30 minutes by default for a deploy callback. Amends ADR-0001/0022 (FM consumes `command.fm.>`) and ADR-0046's DataBus scope; resolves G4
* [ADR-0054](0054-fm-receives-gsma-callbacks-through-one-router.md) - FM receives every OPG.04 callback on `/operatorplatform/federation/v1/callbacks/{kind}`: one router that checks the `fed-mgmt-notif` scope and that `federationContextId` is an outbound context of the calling partner, then a per-kind idempotent handler. `onPartnerStatusEvent` first; `FM_PARTNER_STATUS_LINK` moves to `/callbacks/partner-status`. The `federation` realm gets `fed-mgmt-notif` as an optional scope on partner clients
* [ADR-0055](0055-topology-discovery-is-a-separate-adapter-interface.md) - topology discovery is a separate TopologyDiscoveryAdapter interface, not optional methods on CapabilityExecutionAdapter (amends ADR-0027)
@@ -68,8 +69,7 @@ the reasoning lives here. Never renumber; supersede instead (see the record-deci
* [ADR-0065](0065-portal-sessions-keyed-by-the-cookie.md) - portal login sessions live in PostgreSQL, tokens encrypted with a per-session key only the cookie holds; login attempts are single-use rows; a session lasts as long as Keycloak accepts its refresh token
* [ADR-0066](0066-api-clients-through-keycloak-client-registration.md) - developers' API clients are Keycloak clients registered by a narrow provisioner, products fixed at creation within the creator's grants, secret shown once, registration token encrypted under one app-wide key; a refused token on delete is reported; no reconciliation with Keycloak in Release 2
* [ADR-0067](0067-partner-client-secret-encrypted-in-fm-db.md) - proposed: FM accepts the partner client secret typed in through the registry and stores it AES-GCM encrypted in fm_db (`partner_op_secrets`) under one app-wide key, never returned; `our_client_secret_ref` files stay as the other source, one per partner; no key → 501 (amends FM #21's no-secret-accepted criterion)
* [ADR-0068](0068-fm-onboards-apps-at-partners-over-ewbi-before-installapp.md) - FM onboards an application at a partner over the EWBI (`UploadFile`, `UploadArtefact`, `OnboardApplication`) on the first `InstallApp` of that specification version, with derived GSMA ids, public repositories and container units only for now, and `flavourId` from the zone subscription; the partner creates the specification in its SRM under the originating partner as tenant and resolves `InstallApp` from that onboarding, replacing ADR-0018's manual app mapping. Adds `onApplicationOnboardStatusEvent` to ADR-0054's router
* [ADR-0049](0049-data-lab-standalone-profile-shared-ingress-and-iam.md) - Data Lab is a standalone compose profile behind the platform Nginx and Keycloak (realm `dataspace`); no DataBus/SRM/OEG coupling in the catalogue release
* [ADR-0068](0068-fm-onboards-apps-at-partners-over-ewbi-before-installapp.md) - FM onboards an application at a partner over the EWBI (`UploadFile`, `UploadArtefact`, `OnboardApplication`) on the first `InstallApp` of that specification version, with derived GSMA ids, public repositories only for now, container units and Helm charts (images read from the chart's `values.yaml`, amended 2026-10-15), and `flavourId` from the zone subscription; the partner creates the specification in its SRM under the originating partner as tenant and resolves `InstallApp` from that onboarding, replacing ADR-0018's manual app mapping. Adds `onApplicationOnboardStatusEvent` to ADR-0054's router
* [ADR-0069](0069-data-lab-helm-chart-alternative-to-compose.md) - The Data Lab can also be deployed with the OOP Helm chart: off by default, UI on a NodePort, one Virtuoso, hub and search cluster-internal
* [ADR-0070](0070-data-lab-piveau-hub-and-6gdali-map.md) - Data Lab adopts piveau-hub as catalogue engine and the 6G-DALI Metadata Application Profile (DCAT-AP 3.0 + DQV + PROV-O) as normative metadata profile
* [ADR-0071](0071-data-lab-own-version-outside-release-pin.md) - data-lab/ specs are released as 0.1.0 and are excluded from the Release 2 pin (extends ADR-0021 exceptions)
+4 −2
Changes for fm/implementation-and-delivery.md: 4 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -70,12 +70,14 @@ Settings are environment variables with the `FM_` prefix (`core/config.py`; a `.
| `FM_SRM_INTERNAL_URL` | `http://localhost:8081` | SRM internal HTTP base URL |
| `FM_KEYCLOAK_ISSUER` | `http://localhost:8090/realms/federation` | Issuer partners' tokens must carry |
| `FM_KEYCLOAK_JWKS_URL` | unset | Where FM fetches the signing keys when it can't reach the issuer URL; unset means the issuer's JWKS |
| `FM_KEYCLOAK_AUDIENCE` | `federation-manager` | Audience partner tokens must carry in `aud`. Not built yet |
| `FM_FEDERATION_ID`, `FM_COUNTRY_CODE`, `FM_MCC`, `FM_MNCS` | `oop-local`, `ES`, `214`, `07` | Our OPG.04 identity, sent on `CreateFederation` and served on `GET /internal/identity` |
| `FM_PLATFORM_CAPS` | `serviceAPIs` | Platform capabilities we advertise |
| `FM_PARTNER_STATUS_LINK` | `http://127.0.0.1:8082/operatorplatform/federation/v1/callbacks/partner-status` | Callback URL handed to partners; a deployment sets the URL partners can reach |
| `FM_INSTANCE_STATUS_LINK` | `http://127.0.0.1:8082/operatorplatform/federation/v1/callbacks/instance-status` | Same, for `onInstanceStatusEvent` after our `InstallApp` |
| `FM_AVAIL_ZONE_NOTIF_LINK` | unset | `availZoneNotifLink` for zone notifications |
| `FM_ALLOW_INSECURE_PARTNER_ENDPOINTS` | `false` | Allow plain-HTTP partner endpoints; development only |
| `FM_ZONE_OFFER_SWEEP_SECONDS` | `600` | How often every federated partner's zone offer is re-checked, besides each agreement write and each `valid_from`/`valid_until` (REQ-FED-04). Not built yet |
| `FM_BOOTSTRAP_FEDERATION` | `false` | At startup, federate with every active partner that has no outbound context (ADR-0043) |
| `FM_PARTNER_SECRET_KEY` | unset | Key (base64url, 32 bytes) that encrypts typed-in partner secrets; unset, FM accepts only secret files (ADR-0067) |

@@ -115,7 +117,7 @@ database (`adapters/database/`) and DataBus (`adapters/databus/`) modules.

FM authorizes E/WBI requests with OAuth2 Client-Credentials + signed JWT. Our Keycloak `federation` realm issues tokens for inbound partner calls; each partner's Authorization Server issues tokens for calls from our FM to that partner. FastAPI dependencies validate inbound Bearer tokens; `httpx` fetches and caches outbound tokens.

**Inbound:** a FastAPI dependency (`api/security.py`) reads the `Authorization: Bearer` header and `PartnerAuthenticator` validates the JWT signature against Keycloak's JWKS endpoint (cached, refreshed on `kid` miss), and checks issuer, expiry, and the `fed-mgmt` scope. The validated token's client identity resolves to a partner via `partner_ops`.
**Inbound:** a FastAPI dependency (`api/security.py`) reads the `Authorization: Bearer` header and `PartnerAuthenticator` validates the JWT signature against Keycloak's JWKS endpoint (cached, refreshed on `kid` miss), and checks issuer, expiry, audience (`FM_KEYCLOAK_AUDIENCE`, not built yet) and the `fed-mgmt` scope. The validated token's client identity resolves to a partner via `partner_ops`.

**Outbound:** Callers depend only on `PartnerTokenProviderPort`. The current adapter reads the partner secret from the mounted file named by `our_client_secret_ref` or, when the operator typed the secret in through the registry, decrypts it from `partner_op_secrets` (ADR-0067); it then requests a token from `token_endpoint`, caches it until near expiry, and the EWBI client attaches it as a Bearer header:

@@ -262,7 +264,7 @@ remove the FM fleet from load balancing. Tracking it (`fm_partner_federation_hea
### S.2 Integration Tests

- **FM → SRM flow:** Use Docker Compose with FM, SRM (MockAdapter), PostgreSQL, and NATS. Verify that an inbound EWBI deploy request flows through to `event.srm.operation.completed`.
- **Inbound OAuth2:** Verify JWT signature, issuer, expiry, `fed-mgmt` scope, and `azp` → `oauth2_client_id` partner resolution against a test Keycloak realm.
- **Inbound OAuth2:** Verify JWT signature, issuer, expiry, audience, `fed-mgmt` scope, and `azp` → `oauth2_client_id` partner resolution against a test Keycloak realm.
- **Outbound OAuth2:** Verify token acquisition, near-expiry caching, concurrent refresh serialization, mounted-secret rotation, token acquisition with a stored encrypted secret, and Bearer-header injection against a mock partner Authorization Server.
- **DataBus integration:** Verify `command.srm.*` publishing and `event.srm.operation.completed` consumption round-trip.

Loading
Loading