Commit a9f60fa3 authored by Dimitrios Gogos's avatar Dimitrios Gogos
Browse files

feat: Add ADR-0035 for QoD session referencing a platform-seeded service...

feat: Add ADR-0035 for QoD session referencing a platform-seeded service specification and update related documentation
parent c42740e1
Loading
Loading
Loading
Loading
+4 −5
Changes for decisions/0033-async-network-capability-caller-id.md: 4 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -123,11 +123,10 @@ What becomes harder:

Deferred or excluded:

- The QoD `service_specification_id` question is **not** resolved here. A QoD session has no
  CAMARA registration step and therefore no onboarded specification to reference; see
  [oeg/persistence-model.md](/oeg/persistence-model.md) §J.1 open question OQ-QOD-01. Traffic
  Influence is unaffected — it resolves its specification from the registered app
  (`app_registration_id`, ADR-0011).
- The QoD `service_specification_id` question is out of scope here and settled separately by
  ADR-0035: the platform seeds one QoD specification at deployment and OEG reads its id from
  configuration. Traffic Influence needs no seeded row — it resolves its specification from its
  required `appId` (`app_registration_id`, ADR-0011).

Revisit trigger:

+148 −0
Changes for decisions/0035-qod-session-service-specification.md: 148 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
type: Decision Record
title: "ADR-0035: A QoD session references a single platform-seeded service specification"
description: "CAMARA QoD has no registration step, so there is no onboarded specification to resolve from. The platform seeds one QoD service specification at deployment; OEG holds its id in configuration and sends it on every activate. Profile names stay unvalidated free strings."
tags: [adr]
timestamp: 2026-08-05T00:00:00Z
status: "accepted"
---

# ADR-0035: A QoD session references a single platform-seeded service specification

**Status:** accepted
**Date:** 2026-08-05
**Affects:** [oeg/persistence-model.md](/oeg/persistence-model.md), [srm/architecture.md](/srm/architecture.md), [srm/end-to-end-examples.md](/srm/end-to-end-examples.md), [srm/interface-contract.md](/srm/interface-contract.md)

## Context

`command.srm.network.capability.activate` requires `service_specification_id`
([srm/interface-contract.md](/srm/interface-contract.md) §B.5), and the requirement is
structural rather than advisory: an activate creates a session-holder `service_instance`, and
`service_instance.service_specification_id` is `NOT NULL REFERENCES service_specification(id)`.
Without a real catalog row the insert cannot succeed, so the session never activates.

Every other API family has something to resolve that id from. Edge Application Management
registers an application through `POST /apps`, which onboards a specification and yields
`app_registration_id` (ADR-0010, ADR-0011). Traffic Influence carries a **required** `appId` on
every request and resolves the same way. **CAMARA QoD has no equivalent.** Its entire surface is
`/sessions`, `/sessions/{sessionId}`, `/sessions/{sessionId}/extend`, `/qos-profiles` and
`/qos-profiles/{name}` — there is no registration or onboarding call, and the request body
carries `device`, `applicationServer`, `qosProfile`, `duration`, ports and an optional webhook.
Nothing in it identifies a catalog entry.

Faced with a required field and no input to source it from, implementations minted a fresh UUID
per session. That value references no row, so it satisfies no foreign key; SRM answers
`failed_before_start` and the next request invents another one. The gap was equally visible in
the specs: SRM's worked example carried the unresolved placeholder
`"<premium-qod-session spec id>"`, and OEG's draft described the value as "OEG-minted per
session" — an accurate description of the behaviour that cannot work.

Options considered:

- **Option A:** one specification seeded at platform deployment; OEG holds its id in
  configuration.
- **Option B:** one specification per `app_provider_id`, created get-or-create on first use.
  Correct owner and no seeding step, but OEG must persist a provider → id mapping to avoid an
  SRM call on the request path, and it makes OEG a catalog writer for something no customer
  registered.
- **Option C:** one specification per QoS profile, resolved by profile name — the pattern EAM
  uses for `appId`. This is the shape the platform grows into once profiles carry real limits,
  but it presumes a profile catalog that does not exist: `/qos-profiles` is unimplemented and
  REQ-SRM-08 defers profile validation.
- **Option D:** relax `service_specification_id` for `capability_type=qod_session` in the SRM
  contract. Does not remove the work — the session holder still needs a row, so SRM ends up
  seeding one internally — and it is a cross-component contract change for something OEG can
  resolve on its own side.

A per-session, per-device or per-tenant specification was rejected outright. A specification is a
reusable blueprint; minting one per session or per device places instance data in the type layer,
and per-tenant differentiation belongs in `api_subscriptions`, which already owns "who can call
what, at what quota tier".

## Decision

**Option A.** The platform seeds **one** QoD service specification at deployment, alongside the
bootstrap that materializes zones, network domains and the `qod_session` capability:

| Field | Value |
|---|---|
| `ref` | `qod-session` |
| `version` | `1` |
| `app_provider_id` | the platform's own provider identity, not a customer's |
| `service_deployment_units` | **empty** — a QoD session deploys no software |
| `service_capability_requirements` | one entry: `capability_kind=qod_session`, `domain_kind=network`, `is_required=true`, `policy={}` |

OEG holds the resulting id in configuration
(`qod_settings.service_specification_id` / `QOD_SETTINGS__SERVICE_SPECIFICATION_ID`) and sends it
as `service_specification_id` on every `command.srm.network.capability.activate` for
`capability_type=qod_session`. The value is identical on every session.

**Profile names remain unvalidated.** `qosProfile` continues to travel as `profile_ref`, a free
string the adapter resolves (REQ-SRM-08). The seeded requirement's `policy` is empty, so any
profile name and any duration within CAMARA's own `1..86400` bound is accepted. The network
remains the only authority that can refuse: SRM matches `qod_session` against the live capability
surface at plan time and answers `failed_before_start` when no binding can serve it.

**Per-session variation is unaffected.** Everything a caller can vary — profile, duration, device,
application server, ports — is written per session into
`capability_instance.parameters_snapshot`, merged over the requirement's `parameters` template
([srm/canonical-parameters-schema.md](/srm/canonical-parameters-schema.md) §0, request values win).
One specification therefore serves an unbounded number of distinct sessions.

**Tightening path.** An operator that wants to constrain QoD adds specifications rather than
changing the contract: one per offering, each carrying real limits in its requirement's `policy`
and defaults in `parameters`. OEG's resolution then moves from a configured constant to a lookup
keyed on `qosProfile` — the same shape EAM already uses for `appId` (ADR-0011) — and
`/qos-profiles` is served from that mapping. Per-tenant entitlement layers on top through
`api_subscriptions` without touching the catalog. The command shape, the `qod_sessions` schema
and the CAMARA surface are unchanged by either step; only the resolution function and the row
count change. This decision is deliberately the single-row case of that lookup, not a placeholder
to be discarded.

**Profile ownership (resolves open question F2).** When `profile_ref` does acquire a validated
catalog, it is expressed in the **catalog layer** — a `service_specification` per offering with a
`qod_session` capability requirement — and not as a new `capability_profile` table under the
topology layer. A QoS profile is an operator *product* decision, carrying product-lifecycle
fields CAMARA's `QosProfile` already defines (`status` of `ACTIVE`/`INACTIVE`/`DEPRECATED`,
`minDuration`, `maxDuration`); the capability surface describes what the network can physically
reach. `service_capability_requirement` is the existing bridge between the two, so no new SRM
table is needed. Whether such a catalog is built at all remains open; who owns it does not.

## Consequences

What becomes easier:

- The QoD activate path works end to end: the foreign key resolves, the session holder is
  created, and sessions reach `AVAILABLE`.
- One configuration value in OEG and one line of code; no request-path call to SRM, consistent
  with the rule that OEG resolves catalog ids locally.
- No schema migration, no contract change, and nothing observable on the CAMARA surface.
- The platform is permissive by default, which is the intended posture for an initial deployment:
  operators opt into constraints rather than out of them.

What becomes harder:

- The specification is owned by a synthetic platform provider rather than a customer, so
  `service_specification.app_provider_id` does not match the `app_provider_id` on sessions
  realized under it. Nothing constrains the two to match, and tenant isolation is enforced on
  `service_instance.app_provider_id`, so reads stay correctly scoped — but the catalog row reads
  as platform-owned rather than customer-owned.
- The seeded row must exist before the first session, which makes deployment ordering
  significant: it belongs with the topology bootstrap, not with application onboarding.
- OEG cannot yet return `400 OUT_OF_RANGE` for a duration outside a profile's range, or `404` for
  an unknown profile, because it holds no per-profile limits. Both are defined by the CAMARA QoD
  spec. Accepted while the platform is permissive; they become available with the per-offering
  specifications described above.

Deferred or excluded:

- `/qos-profiles` and `/qos-profiles/{name}` remain unimplemented.
- `api_subscriptions` is not populated for QoD; per-tenant entitlement is not enforced.
- Traffic Influence is unaffected and out of scope here — it resolves `service_specification_id`
  from its required `appId` via `app_registration_id` (ADR-0011) and needs no seeded row.

Revisit trigger:

- The first requirement to differentiate QoD by offering or by customer. At that point the
  resolution function changes as described, and this decision becomes the default arm of that
  lookup rather than the whole of it.
+1 −0
Changes for decisions/index.md: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -37,3 +37,4 @@ the reasoning lives here. Never renumber; supersede instead (see the record-deci
* [ADR-0032](0032-databus-validation-failure-split-by-answerability.md) - DataBus validation failures split by answerability: dead-letter only unattributable messages; every readable-envelope failure returns failed_before_start
* [ADR-0033](0033-async-network-capability-caller-id.md) - async network-capability caller-facing ids (QoD sessionId, trafficInfluenceID) are OEG-minted and adopted as service_instance.id; external_ref reverts to the realization ref
* [ADR-0034](0034-network-capability-delete-is-async.md) - network-capability delete is async via command.srm.network.capability.deactivate, and its completion is consumed to a terminal record state
* [ADR-0035](0035-qod-session-service-specification.md) - a QoD session references one platform-seeded service specification held in OEG config; profile names stay unvalidated, and QoS profiles are a catalog-layer concern (resolves F2)
+24 −18
Changes for oeg/persistence-model.md: 24 added lines, 18 removed lines.
Original line number Diff line number Diff line
@@ -281,7 +281,7 @@ Indexes: `BTREE(operation_id)`, `BTREE(app_registration_id)`.
|---|---|---|
| `session_id` | UUID PK | OEG-minted; the CAMARA `sessionId` **and** SRM's `service_instance.id` (ADR-0033) |
| `operation_id` | UUID FK -> `operations` | The **activate** operation that created this row; the deactivate operation references back via its own `metadata` |
| `service_specification_id` | UUID | Sent on the activate command. **Open — see OQ-QOD-01 below.** |
| `service_specification_id` | UUID | The platform-seeded QoD specification, read from OEG config; identical on every row today (ADR-0035). Stored per row so the offering a session was provisioned under stays auditable once per-offering specifications exist |
| `qos_profile` | VARCHAR(256) | CAMARA `qosProfile` → command `network_capability.profile_ref` |
| `duration_seconds` | INTEGER | CAMARA `duration` → command `parameters.duration_seconds` |
| `state` | VARCHAR(32) | `REQUESTED`/`AVAILABLE`/`UNAVAILABLE` are activate-derived and surface as CAMARA `qosStatus`; `DELETION_REQUESTED`/`DELETED`/`ERROR` are teardown-derived and surface as `404` |
@@ -292,22 +292,29 @@ Indexes: `BTREE(operation_id)`, `BTREE(app_registration_id)`.

Indexes: `BTREE(operation_id)`, `BTREE(tenant_id)`.

> **OQ-QOD-01 — where does a QoD session's `service_specification_id` come from?**
> `service_instance.service_specification_id` is `NOT NULL REFERENCES service_specification(id)`
> in `srm_db`, so every session must reference a real onboarded specification. Unlike Traffic
> Influence, a QoD session has no CAMARA registration step and therefore no `appId` to resolve
> from — CAMARA QoD exposes only `/sessions`, `/sessions/{id}`, `/sessions/{id}/extend`,
> `/qos-profiles` and `/qos-profiles/{name}`. Minting a fresh id per session is **not** viable:
> it satisfies no foreign key and defeats the field's stated purpose (catalog/profile
> validation, [srm/interface-contract.md](/srm/interface-contract.md) §B.5).
> Candidate resolutions: (a) one QoD specification seeded at platform bootstrap alongside the
> `qod_session` capability, its id in OEG config; (b) one per `app_provider_id`, created
> get-or-create on first use via `POST /internal/catalog/service-specifications`; (c) relax the
> field for `capability_type=qod_session` in the SRM contract. Not resolved; blocks the QoD
> activate path end to end. Related: `/qos-profiles` is currently unimplemented, and whatever
> backs it is the natural home for a profile → specification mapping.

**`traffic_influences`** — the OEG-side record for `POST /traffic-influences`. Same shape and lifecycle as `qod_sessions`: `traffic_influence_id` is OEG-minted and adopted by SRM as `service_instance.id` (ADR-0033), the row is written at request time, advanced by the activate completion, and driven to a terminal state by the deactivate completion (ADR-0034). It does **not** carry OQ-QOD-01: its `service_specification_id` is resolved from the request's registered `appId` via `app_registrations.app_registration_id` (ADR-0011).
> **Where the QoD `service_specification_id` comes from (ADR-0035).** A QoD session has no CAMARA
> registration step and therefore no `appId` to resolve from — CAMARA QoD exposes only
> `/sessions`, `/sessions/{id}`, `/sessions/{id}/extend`, `/qos-profiles` and
> `/qos-profiles/{name}`, and none of those onboards anything. The platform therefore **seeds one
> QoD service specification at deployment** (`ref=qod-session`, no deployment units, a single
> `qod_session`/`network` capability requirement with an empty `policy`), alongside the bootstrap
> that materializes zones, network domains and the `qod_session` capability itself. OEG holds its
> id in configuration (`QOD_SETTINGS__SERVICE_SPECIFICATION_ID`) and sends the same value on every
> activate — no lookup, and no call to SRM on the request path.
>
> Profile names stay **unvalidated**: `qosProfile` travels as `profile_ref`, a free string the
> adapter resolves (REQ-SRM-08), and the network is the only authority that can refuse a session.
> Everything a caller varies — profile, duration, device, application server, ports — is written
> per session into `capability_instance.parameters_snapshot`, so one specification serves an
> unbounded number of distinct sessions.
>
> An operator that later wants to constrain QoD adds one specification per offering, each carrying
> real limits in its requirement's `policy`; OEG's resolution then moves from the configured
> constant to a lookup keyed on `qosProfile`, and `/qos-profiles` is served from that mapping.
> Per-tenant entitlement layers on top through `api_subscriptions`. Neither step changes the
> command shape, this table, or the CAMARA surface.

**`traffic_influences`** — the OEG-side record for `POST /traffic-influences`. Same shape and lifecycle as `qod_sessions`: `traffic_influence_id` is OEG-minted and adopted by SRM as `service_instance.id` (ADR-0033), the row is written at request time, advanced by the activate completion, and driven to a terminal state by the deactivate completion (ADR-0034). It needs no seeded specification (ADR-0035): its `service_specification_id` is resolved from the request's required `appId` via `app_registrations.app_registration_id` (ADR-0011).

| Column | Type | Notes |
|---|---|---|
@@ -519,4 +526,3 @@ Ship JWT validation, `ErrorInfo`, one async deploy path, `GET /platform/v1/opera
| Version | Date | Summary |
|---------|------|---------|
| 2.0 | 2026-07-07 | OOP Release 2.0. |
 No newline at end of file
| 2.0 | 2026-08-05 | Added `qod_sessions` and `traffic_influences` (QodSession/TrafficInfluence entities, ownership rows 9-10, relationships, table definitions) under ADR-0033/ADR-0034; `operations.operation_type` split into activate/deactivate; recorded open question OQ-QOD-01 on the QoD `service_specification_id`. |
+11 −4
Changes for srm/architecture.md: 11 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -93,7 +93,7 @@ clean boundaries.
| Scale / terminate | Same async `command.srm.service.scale` / `.terminate` path as deploy |
| Network capability | `command.srm.network.capability.*` for async lifecycle/session (QoD, Traffic Influence) |
| Sync query/data | Internal HTTP for Device Location/Status; no inventory |
| QoS / capability profiles | Referenced by `profile_ref` in canonical params; optional `capability_profile` catalog deferred |
| QoS / capability profiles | Referenced by `profile_ref` in canonical params — unvalidated today; validation, when added, is a catalog-layer specification per offering (ADR-0035) |
| Public APIs | Owned by OEG/FM, not SRM |
| Placement (REQ-SRM-04) | Capability-surface matching; optional best-effort `query_available_resources`; no stored capacity |
| Subscriptions/callbacks | Owned by OEG; SRM emits state-change events only |
@@ -186,8 +186,15 @@ SRM-owned data is consumed only via SRM internal APIs or published events, never
- **F1 Partial-failure compensation** — rollback behavior when a multi-requirement product is
  half-done (v1 composite = deploy then separate activate; single-realization model records
  latest state only; saga compensation deferred).
- **F2 Capability profile ownership** — if/when `profile_ref` needs a validated catalog
  (`capability_profile`), who manages it.
- **F2 Capability profile validation** — *ownership settled (ADR-0035): a validated profile
  catalog is expressed in the **catalog layer**, as one `service_specification` per offering with
  a `qod_session` capability requirement carrying its limits, onboarded through
  `POST /internal/catalog/service-specifications`. No `capability_profile` table is added under
  the topology layer: a QoS profile is an operator product decision (CAMARA's `QosProfile` carries
  `status` of `ACTIVE`/`INACTIVE`/`DEPRECATED`, `minDuration`, `maxDuration`), while the capability
  surface describes what the network can physically reach, and `service_capability_requirement`
  already bridges the two.* Still open: **whether** such a catalog is built, and when —
  `profile_ref` is an unvalidated free string today.
- **F3 Multi-platform scoping** — when `zone.UNIQUE(ref)` must become
  `zone.UNIQUE(platform_ref, ref)`.
- **F4 Topology-hiding citation** — `persistence-cross-standards-view.md` §5 row 9 cites GSMA
@@ -208,7 +215,7 @@ SRM-owned data is consumed only via SRM internal APIs or published events, never
| REQ-SRM-05 | Interface with infrastructure via TF-SDK adapters for SBI-OAM and SBI-NR. |
| REQ-SRM-06 | Publish `event.srm.operation.status` and `event.srm.operation.completed` per Interface Contract §C (Phase 1 status model). |
| REQ-SRM-07 | Support federated work through the same commands with `source=federation` and `federation_partner_ref`. |
| REQ-SRM-08 | Reference QoS/capability profiles via `profile_ref` in canonical params; optional `capability_profile` catalog deferred. |
| REQ-SRM-08 | Reference QoS/capability profiles via `profile_ref` in canonical params — an unvalidated free string the adapter resolves. Profile validation is deferred; when added it lives in the catalog layer as one `service_specification` per offering, not as a `capability_profile` table (ADR-0035, F2). |
| REQ-SRM-09 | Idempotent command processing: duplicate `operation_id` creates no duplicate runtime rows (enforced at `service_order`). |
| REQ-SRM-10 | Return RFC 7807 problem details for all HTTP **error** responses. |
| REQ-SRM-11 | Periodic reconciliation vs adapter-reported state; emit `event.srm.inventory.drift_detected` / `drift_resolved`. |
Loading