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

feat: define ports as a generic canonical target attribute across capabilities

parent a512bffe
Loading
Loading
Loading
Loading
+130 −0
Changes for decisions/0037-canonical-target-ports.md: 130 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
type: Decision Record
title: "ADR-0037: Ports are a generic canonical target attribute, not a QoD-only extra"
description: "target.device.ports and target.application_server.ports join srm.params/v1's canonical target descriptor, generic across every capability that targets a device or application server rather than allowlisted per capability_type under parameters.extra."
tags: [adr]
timestamp: 2026-08-06T00:00:00Z
status: "accepted"
---

# ADR-0037: Ports are a generic canonical target attribute, not a QoD-only extra

**Status:** accepted
**Date:** 2026-08-06
**Affects:** [srm/canonical-parameters-schema.md](/srm/canonical-parameters-schema.md), [oeg/persistence-model.md](/oeg/persistence-model.md), [oeg/interface-contract.md](/oeg/interface-contract.md)

## Context

CAMARA QoD's `CreateSession` accepts `devicePorts` and `applicationServerPorts` (each a
`PortsSpec`: a list of individual ports and/or `{from, to}` ranges). Neither field is represented
anywhere in SRM's canonical Capability Request shape (`srm.params/v1`,
[srm/canonical-parameters-schema.md](/srm/canonical-parameters-schema.md) §1): the CAMARA→canonical
mapping table has no row for them, and OEG's `build_activate_command` never sent them. ADR-0035
already describes the intended behaviour — "*everything a caller can vary — profile, duration,
device, application server, ports — is written per session into
`capability_instance.parameters_snapshot`*" — but that was never implemented, and the canonical
schema never defined where "ports" would live within it. The gap surfaced as a CAMARA-surface bug:
`POST /sessions` silently dropped ports, and `GET /sessions/{id}` could never echo them back since
nothing downstream of the API layer knew about them.

Two places in the canonical shape could hold ports:

- **`target.device` / `target.application_server`.** These already carry the addressing fields a
  capability's target is identified by (`phone_number`, `ipv4`, `ipv6`, `network_access_id`).
  Ports are the same kind of thing: a property of *what* the capability acts on, not of *how* it
  should behave.
- **`parameters.extra`.** Reserved (§1) for "allowlisted extras only when a capability defines
  keys under `parameters.extra` — unknown keys are rejected at validation." This is scoped per
  `capability_type`: QoD would need its own allowlist entry, Traffic Influence (or any future
  capability with a device/application-server target) would need to repeat the exercise
  independently, with no shared shape between them.

Traffic Influence already has a *port*-adjacent field — `parameters.traffic_filters.source.port` /
`.destination.port` — but that is a single port paired with a protocol, describing a traffic filter
rule, not a set of ports/ranges scoping *where a capability applies on a device or server*. The two
are not the same concept and this decision does not merge them.

## Decision

**`ports` becomes an optional field on `target.device` and `target.application_server`** in
`srm.params/v1`, alongside their existing addressing fields:

```jsonc
"target": {
  "device": {
    "phone_number": "+123456789",
    "ipv4": "198.51.100.1",
    "ipv6": "2001:db8:85a3::370:7344",
    "network_access_id": null,
    "ports": { "ranges": [{ "from": 5000, "to": 5010 }], "ports": [80, 443] }
  },
  "application_server": {
    "ipv4": "198.51.100.0/24",
    "ipv6": "2001:db8:85a3::8a2e:370:7335",
    "ports": { "ranges": null, "ports": [443] }
  }
}
```

`ports.ranges` and `ports.ports` mirror CAMARA QoD's `PortsSpec` shape directly (a list of
`{from, to}` range objects and/or a flat list of individual ports); at least one of the two is
present when `ports` is present at all. This is additive to `srm.params/v1` — existing rows and
adapters that omit `ports` are unaffected, so no `schema_version` bump.

**Generic, not QoD-scoped.** Because `ports` lives on the shared `target.device` /
`target.application_server` objects rather than under a capability-specific `parameters.extra`
allowlist entry, any current or future capability whose target includes a device or application
server gets port support automatically — no per-capability allowlisting, no repeated design work.
The CAMARA→canonical mapping table gains:

| CAMARA field | Canonical path |
|---|---|
| `devicePorts` | `target.device.ports` |
| `applicationServerPorts` | `target.application_server.ports` |

**Traffic Influence's `traffic_filters` is unchanged.** A single filter port+protocol pair
describing a traffic rule is a distinct concept from a set of ports/ranges scoping a target; this
decision does not fold one into the other.

**OEG must send what it accepts.** `build_activate_command` populates `target.device.ports` /
`target.application_server.ports` from the request's `devicePorts` / `applicationServerPorts`
whenever present, matching how it already forwards addressing fields.

## Consequences

What becomes easier:

- QoD's `devicePorts`/`applicationServerPorts` have a defined destination in the canonical shape,
  closing the gap between ADR-0035's description and the actual command/schema.
- Any future capability that targets a device or application server (present or not-yet-designed)
  inherits port support for free — it is a property of the target descriptor, not something each
  capability must separately opt into via `parameters.extra`.

What becomes harder:

- SRM's adapters must be updated to read `target.*.ports` where relevant to a backend's
  capabilities; a capability that has no use for ports simply ignores the field, same as any other
  target attribute it doesn't act on.

Deferred or excluded:

- **OEG's interim local cache.** Until SRM accepts and echoes `target.*.ports` in
  `parameters_snapshot`, OEG persists `devicePorts`/`applicationServerPorts` on its own
  `qod_sessions` row as a stopgap so `GET /sessions/{id}` can echo what was requested. This is a
  deliberate, temporary exception to the rule in
  [oeg/persistence-model.md](/oeg/persistence-model.md) that `qod_sessions` holds identity,
  ownership and cached status only, with capability detail (device, applicationServer) read live
  from SRM — never duplicated. It exists solely because SRM does not yet round-trip ports; it is
  not a second source of truth this decision endorses going forward.
- No enforcement semantics are specified here — this decision defines where ports live in the
  canonical shape, not what a given backend does with them.

Revisit trigger:

- Once SRM accepts `target.*.ports` on activate and returns it in
  `GET /internal/network-capabilities/{id}`'s `parameters_snapshot`, OEG's `build_session_info`
  must switch to reading ports live from the capability snapshot — exactly as it already does for
  `device`/`applicationServer` — and the interim `qod_sessions.device_ports` /
  `application_server_ports` columns must be dropped. Carrying both a local cache and a live SRM
  read forward indefinitely would silently reintroduce the two-sources-of-truth problem
  [oeg/persistence-model.md](/oeg/persistence-model.md) exists to prevent.
+1 −0
Changes for decisions/index.md: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -39,3 +39,4 @@ the reasoning lives here. Never renumber; supersede instead (see the record-deci
* [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)
* [ADR-0036](0036-srm-owns-network-capability-expiry.md) - SRM owns expiry of async network capabilities and reports it via event.srm.operation.status metadata (qos_status_info); OEG runs no timer and maps it to CAMARA DURATION_EXPIRED
* [ADR-0037](0037-canonical-target-ports.md) - target.device.ports / target.application_server.ports join srm.params/v1's canonical target descriptor, generic across every capability with a device/application-server target rather than allowlisted per capability_type
+10 −3
Changes for srm/canonical-parameters-schema.md: 10 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -83,11 +83,16 @@ though they persist nothing.
      "phone_number": "+123456789",        // CAMARA device.phoneNumber
      "ipv4": "198.51.100.1",              // CAMARA device.ipv4Address
      "ipv6": "2001:db8:85a3::370:7344",   // CAMARA device.ipv6Address
      "network_access_id": null            // CAMARA device.networkAccessIdentifier
      "network_access_id": null,           // CAMARA device.networkAccessIdentifier
      "ports": {                            // CAMARA devicePorts (ADR-0037) — generic on
        "ranges": [{ "from": 5000, "to": 5010 }], // any target.device/application_server, not
        "ports": [80, 443]                        // a QoD-only parameters.extra allowlist entry
      }
    },
    "application_server": {                 // CAMARA applicationServer
      "ipv4": "198.51.100.0/24",
      "ipv6": "2001:db8:85a3::8a2e:370:7335"
      "ipv6": "2001:db8:85a3::8a2e:370:7335",
      "ports": { "ranges": null, "ports": [443] } // CAMARA applicationServerPorts (ADR-0037)
    },
    "application_reference": {              // links target to an onboarded app
      "service_specification_ref": "video-analytics",
@@ -131,8 +136,10 @@ though they persist nothing.
| `device.ipv4Address` | `target.device.ipv4` |
| `device.ipv6Address` | `target.device.ipv6` |
| `device.networkAccessIdentifier` | `target.device.network_access_id` |
| `devicePorts` | `target.device.ports` (ADR-0037; generic across capabilities, not QoD-only) |
| `applicationServer.ipv4Address` | `target.application_server.ipv4` |
| `applicationServer.ipv6Address` | `target.application_server.ipv6` |
| `applicationServerPorts` | `target.application_server.ports` (ADR-0037) |
| `qosProfile` | `parameters.profile_ref` (merged from command `network_capability.profile_ref` when present) |
| `duration` | `parameters.duration_seconds` |
| `appId` | `target.application_reference.external_app_id` |
@@ -299,7 +306,7 @@ For the canonical layer to deliver "no ambiguity," it must be enforced, not aspi
| Get compute domains | topology read | n/a (domain table) | yes |
| Request deploy | deploy | Compute intent already on unit; optional Capability Request (`application_reference`) when needed | yes |
| Deploy success | result | Capability Result (endpoints) | yes |
| Request QoD | activate | Capability Request (device + profile + duration) | yes |
| Request QoD | activate | Capability Request (device + profile + duration + ports, ADR-0037) | yes |
| QoD accepted | result | Capability Result (`session_id` → equal `external_id`/`external_ref`) | yes |
| Request Traffic Influence | activate | Capability Request (traffic_filters) | yes |
| Traffic Influence accepted | result | Capability Result | yes |