Release 2.0.0: OOP architecture specs for ETSI SDG OOP Release 2

Summary

First tagged release (v2.0.0) of the Open Operator Platform architecture bundle. It merges develop into main. The repo contains only specs, decision records and vendored third-party API definitions, with no application code. Tag v2.0.0 after merge.

What's in this release

Platform

  • Platform architecture, glossary and future-architecture summary (what comes after Release 2).
  • Platform roles (ADR-0048).
  • Overview, DataBus, HTTP-call and deployment diagrams, drawn in draw.io.

Components (each has overview, requirements and design, implementation and delivery, and interface contract)

Component Scope
OEG Northbound CAMARA API termination, with the app registration, deployment and instance flows. OEG reads are scoped by app provider. QoD session service specification is seeded by the platform.
SRM Orchestration and inventory as a modular monolith. SRM owns async network-capability expiry and federated zone locality and hand-off. Credentials in command payloads are handled by documented rules.
FM Cross-operator EWBI federation, with a persistence model. OEG fronts the federation lifecycle as a sync proxy. FM receives GSMA callbacks through one router and onboards apps at partners.
TF-SDK Vendor adapters inside SRM. Generic capability adapter interface, with topology discovery as a separate interface. Distributed as openop-tfsdk.
Portal Developer portal and admin console: one container with a thin backend. Sessions are keyed by the cookie, and API clients register through Keycloak.
Data Lab Dataspace metadata catalogue on piveau hub with the 6G-DALI profile. Standalone profile with shared ingress and IAM, and a Helm chart alternative to Compose. Versioned outside the release pin.
AI² Agent and MCP server over OEG's APIs.

Architecture Decision Records

70 ADRs (0001–0071, no ADR-0052) in decisions/. Main themes:

  • Messaging and structure: NATS JetStream, SRM modular monolith, TF-SDK in-process, per-component DB isolation, DataBus validation-failure split.
  • Federation and auth: partner-as-tenant, zone identity UUID passthrough, EWBI auth via OAuth2/Keycloak (mTLS deferred), federated lifecycle hand-off over the DataBus, GSMA problem+json errors on the OEG lifecycle proxy, no silent substitution of a resolved target.
  • API and data contracts: CAMARA error model, CloudEvents callbacks, canonical target ports, unsupported manifest variants rejected with 501, terminal-record reads following the CAMARA schema.
  • Security: callback tokens and partner client secrets encrypted in the component DB.

Vendored external specs (commit-pinned, ADR-0026)

CAMARA Edge Application Management, Quality on Demand, Traffic Influence, Device Location; GSMA OPG.04 EWBI Federation API v1.4.0 with an OOP profile overlay; 6G-DALI Metadata Application Profile.

Repository

  • OKF v0.1 bundle layout with frontmatter, per-folder index.md and bundle-absolute links (ADR-0014).
  • scripts/check-spec-versions.sh enforces the Release 2 version pin 2.0 (ADR-0021, ADR-0071).
  • Apache License 2.0.

Reviewing

  • Start at index.md → platform/architecture.md, then a component's index.md.
  • Review per component. The ADRs carry the reasoning, and the component specs cite them.

Checks

  • scripts/check-spec-versions.sh passes
  • Links and index.md listings resolve
  • Frontmatter version is 2.0 on all Release 2 specs

Release steps

  1. Merge to main.
  2. Tag v2.0.0 on the merge commit.``
Edited by Dimitrios Gogos

Merge request reports

Loading
+40 −0
Changes for ai2/diagrams/overview.drawio.svg: 40 added lines, 0 removed lines.
Original line number Diff line number Diff line
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1000px" height="520px" viewBox="0 0 1000 520" content="&lt;mxfile host=&quot;Electron&quot; type=&quot;device&quot;&gt;&lt;diagram id=&quot;d&quot; name=&quot;Page-1&quot;&gt;&lt;mxGraphModel dx=&quot;1000&quot; dy=&quot;520&quot; grid=&quot;1&quot; gridSize=&quot;10&quot; guides=&quot;1&quot; page=&quot;1&quot; pageWidth=&quot;1000&quot; pageHeight=&quot;520&quot; background=&quot;#ffffff&quot;&gt;&lt;root&gt;&lt;mxCell id=&quot;0&quot;/&gt;&lt;mxCell id=&quot;1&quot; parent=&quot;0&quot;/&gt;&lt;mxCell id=&quot;l1&quot; value=&quot;&amp;lt;b&amp;gt;Caller&amp;lt;/b&amp;gt;&quot; style=&quot;rounded=1;arcSize=4;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#9e9e9e;align=left;spacingLeft=12;fontSize=13;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;20&quot; y=&quot;20&quot; width=&quot;960&quot; height=&quot;80&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;l2&quot; value=&quot;&amp;lt;b&amp;gt;Agent&amp;lt;/b&amp;gt;&quot; style=&quot;rounded=1;arcSize=4;whiteSpace=wrap;html=1;fillColor=#e8f0fb;strokeColor=#6c8ebf;align=left;spacingLeft=12;fontSize=13;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;20&quot; y=&quot;140&quot; width=&quot;960&quot; height=&quot;100&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;l3&quot; value=&quot;&amp;lt;b&amp;gt;MCP server&amp;lt;/b&amp;gt;&quot; style=&quot;rounded=1;arcSize=4;whiteSpace=wrap;html=1;fillColor=#e8f0fb;strokeColor=#6c8ebf;align=left;spacingLeft=12;fontSize=13;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;20&quot; y=&quot;290&quot; width=&quot;960&quot; height=&quot;90&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;l4&quot; value=&quot;&amp;lt;b&amp;gt;OEG&amp;lt;/b&amp;gt;&quot; style=&quot;rounded=1;arcSize=4;whiteSpace=wrap;html=1;fillColor=#eaf5e6;strokeColor=#82b366;align=left;spacingLeft=12;fontSize=13;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;20&quot; y=&quot;420&quot; width=&quot;960&quot; height=&quot;80&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;portal&quot; value=&quot;&amp;lt;b&amp;gt;Portal backend&amp;lt;/b&amp;gt;&quot; style=&quot;rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#9e9e9e;fontSize=12;fontColor=#222222;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;300&quot; y=&quot;40&quot; width=&quot;220&quot; height=&quot;40&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;agent&quot; value=&quot;&amp;lt;b&amp;gt;agent (FastAPI)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;ai2-ai-agent :9013&quot; style=&quot;rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#6c8ebf;fontSize=12;fontColor=#222222;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;300&quot; y=&quot;160&quot; width=&quot;220&quot; height=&quot;60&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;groq&quot; value=&quot;&amp;lt;b&amp;gt;Groq-hosted LLM&amp;lt;/b&amp;gt;&quot; style=&quot;rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#9e9e9e;fontSize=12;fontColor=#222222;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;700&quot; y=&quot;150&quot; width=&quot;200&quot; height=&quot;36&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;redis&quot; value=&quot;&amp;lt;b&amp;gt;Redis (optional)&amp;lt;/b&amp;gt;&quot; style=&quot;rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#9e9e9e;fontSize=12;fontColor=#222222;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;700&quot; y=&quot;196&quot; width=&quot;200&quot; height=&quot;36&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;mcp&quot; value=&quot;&amp;lt;b&amp;gt;MCP server (FastMCP)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;ai2-mcp :8004 · 20 tools&quot; style=&quot;rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#6c8ebf;fontSize=12;fontColor=#222222;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;300&quot; y=&quot;305&quot; width=&quot;220&quot; height=&quot;60&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;oeg&quot; value=&quot;&amp;lt;b&amp;gt;OEG CAMARA APIs&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;checks the token's scopes&quot; style=&quot;rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#82b366;fontSize=12;fontColor=#222222;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;300&quot; y=&quot;435&quot; width=&quot;220&quot; height=&quot;50&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;e0&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#555555;strokeWidth=1.3;endArrow=block;endFill=1;startArrow=none;startFill=1;exitX=0.5;exitY=1.0;exitDx=0;exitDy=0;entryX=0.5;entryY=0.0;entryDx=0;entryDy=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;portal&quot; target=&quot;agent&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;e1&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#555555;strokeWidth=1.3;endArrow=block;endFill=1;startArrow=none;startFill=1;exitX=1.0;exitY=0.1333;exitDx=0;exitDy=0;entryX=0.0;entryY=0.5;entryDx=0;entryDy=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;agent&quot; target=&quot;groq&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;e2&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#555555;strokeWidth=1.3;endArrow=block;endFill=1;startArrow=block;startFill=1;exitX=1.0;exitY=0.9;exitDx=0;exitDy=0;entryX=0.0;entryY=0.5;entryDx=0;entryDy=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;agent&quot; target=&quot;redis&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;e3&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#555555;strokeWidth=1.3;endArrow=block;endFill=1;startArrow=none;startFill=1;exitX=0.5;exitY=1.0;exitDx=0;exitDy=0;entryX=0.5;entryY=0.0;entryDx=0;entryDy=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;agent&quot; target=&quot;mcp&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;e4&quot; style=&quot;edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;strokeColor=#555555;strokeWidth=1.3;endArrow=block;endFill=1;startArrow=none;startFill=1;exitX=0.5;exitY=1.0;exitDx=0;exitDy=0;entryX=0.5;entryY=0.0;entryDx=0;entryDy=0;&quot; edge=&quot;1&quot; parent=&quot;1&quot; source=&quot;mcp&quot; target=&quot;oeg&quot;&gt;&lt;mxGeometry relative=&quot;1&quot; as=&quot;geometry&quot;&gt;&lt;/mxGeometry&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;t0&quot; value=&quot;POST /groq-mcp {query, session_id}&amp;lt;br&amp;gt;+ Bearer token&quot; style=&quot;text;html=1;align=left;verticalAlign=middle;fontSize=10.5;fontColor=#222222;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;416&quot; y=&quot;104&quot; width=&quot;260&quot; height=&quot;28&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;t1&quot; value=&quot;chat completions&quot; style=&quot;text;html=1;align=center;verticalAlign=middle;fontSize=10.5;fontColor=#222222;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;530&quot; y=&quot;152&quot; width=&quot;160&quot; height=&quot;14&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;t2&quot; value=&quot;recent turns&quot; style=&quot;text;html=1;align=center;verticalAlign=middle;fontSize=10.5;fontColor=#222222;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;530&quot; y=&quot;198&quot; width=&quot;160&quot; height=&quot;14&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;t3&quot; value=&quot;MCP over HTTP (/mcp),&amp;lt;br&amp;gt;same Bearer token&quot; style=&quot;text;html=1;align=left;verticalAlign=middle;fontSize=10.5;fontColor=#222222;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;416&quot; y=&quot;250&quot; width=&quot;200&quot; height=&quot;28&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;mxCell id=&quot;t4&quot; value=&quot;HTTPS/HTTP,&amp;lt;br&amp;gt;same Bearer token&quot; style=&quot;text;html=1;align=left;verticalAlign=middle;fontSize=10.5;fontColor=#222222;&quot; vertex=&quot;1&quot; parent=&quot;1&quot;&gt;&lt;mxGeometry x=&quot;416&quot; y=&quot;387&quot; width=&quot;200&quot; height=&quot;28&quot; as=&quot;geometry&quot;/&gt;&lt;/mxCell&gt;&lt;/root&gt;&lt;/mxGraphModel&gt;&lt;/diagram&gt;&lt;/mxfile&gt;">
<rect x="0" y="0" width="1000" height="520" fill="#ffffff"/>
<defs><marker id="ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#555555"/></marker><marker id="am" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#d79b00"/></marker></defs>
<rect x="20" y="20" width="960" height="80" rx="8" fill="#f5f5f5" stroke="#9e9e9e"/>
<text x="33" y="60.0" font-family="Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" fill="#222222" text-anchor="start" dominant-baseline="central">Caller</text>
<rect x="20" y="140" width="960" height="100" rx="8" fill="#e8f0fb" stroke="#6c8ebf"/>
<text x="33" y="190.0" font-family="Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" fill="#222222" text-anchor="start" dominant-baseline="central">Agent</text>
<rect x="20" y="290" width="960" height="90" rx="8" fill="#e8f0fb" stroke="#6c8ebf"/>
<text x="33" y="335.0" font-family="Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" fill="#222222" text-anchor="start" dominant-baseline="central">MCP server</text>
<rect x="20" y="420" width="960" height="80" rx="8" fill="#eaf5e6" stroke="#82b366"/>
<text x="33" y="460.0" font-family="Helvetica, Arial, sans-serif" font-size="13" font-weight="bold" fill="#222222" text-anchor="start" dominant-baseline="central">OEG</text>
<rect x="300" y="40" width="220" height="40" rx="6" fill="#ffffff" stroke="#9e9e9e"/>
<text x="410.0" y="60.0" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="bold" fill="#222222" text-anchor="middle" dominant-baseline="central">Portal backend</text>
<rect x="300" y="160" width="220" height="60" rx="6" fill="#ffffff" stroke="#6c8ebf"/>
<text x="410.0" y="182.5" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="bold" fill="#222222" text-anchor="middle" dominant-baseline="central">agent (FastAPI)</text>
<text x="410.0" y="197.5" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="normal" fill="#222222" text-anchor="middle" dominant-baseline="central">ai2-ai-agent :9013</text>
<rect x="700" y="150" width="200" height="36" rx="6" fill="#ffffff" stroke="#9e9e9e"/>
<text x="800.0" y="168.0" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="bold" fill="#222222" text-anchor="middle" dominant-baseline="central">Groq-hosted LLM</text>
<rect x="700" y="196" width="200" height="36" rx="6" fill="#ffffff" stroke="#9e9e9e"/>
<text x="800.0" y="214.0" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="bold" fill="#222222" text-anchor="middle" dominant-baseline="central">Redis (optional)</text>
<rect x="300" y="305" width="220" height="60" rx="6" fill="#ffffff" stroke="#6c8ebf"/>
<text x="410.0" y="327.5" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="bold" fill="#222222" text-anchor="middle" dominant-baseline="central">MCP server (FastMCP)</text>
<text x="410.0" y="342.5" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="normal" fill="#222222" text-anchor="middle" dominant-baseline="central">ai2-mcp :8004 · 20 tools</text>
<rect x="300" y="435" width="220" height="50" rx="6" fill="#ffffff" stroke="#82b366"/>
<text x="410.0" y="452.5" font-family="Helvetica, Arial, sans-serif" font-size="12" font-weight="bold" fill="#222222" text-anchor="middle" dominant-baseline="central">OEG CAMARA APIs</text>
<text x="410.0" y="467.5" font-family="Helvetica, Arial, sans-serif" font-size="11" font-weight="normal" fill="#222222" text-anchor="middle" dominant-baseline="central">checks the token's scopes</text>
<polyline points="410,80 410,160" fill="none" stroke="#555555" stroke-width="1.3" marker-end="url(#ah)"/>
<polyline points="520,168 700,168" fill="none" stroke="#555555" stroke-width="1.3" marker-end="url(#ah)"/>
<polyline points="520,214 700,214" fill="none" stroke="#555555" stroke-width="1.3" marker-end="url(#ah)" marker-start="url(#ah)"/>
<polyline points="410,220 410,305" fill="none" stroke="#555555" stroke-width="1.3" marker-end="url(#ah)"/>
<polyline points="410,365 410,435" fill="none" stroke="#555555" stroke-width="1.3" marker-end="url(#ah)"/>
<text x="416" y="112.0" font-family="Helvetica, Arial, sans-serif" font-size="10.5" font-weight="normal" fill="#222222" text-anchor="start" dominant-baseline="central">POST /groq-mcp {query, session_id}</text>
<text x="416" y="124.0" font-family="Helvetica, Arial, sans-serif" font-size="10.5" font-weight="normal" fill="#222222" text-anchor="start" dominant-baseline="central">+ Bearer token</text>
<text x="610.0" y="159.0" font-family="Helvetica, Arial, sans-serif" font-size="10.5" font-weight="normal" fill="#222222" text-anchor="middle" dominant-baseline="central">chat completions</text>
<text x="610.0" y="205.0" font-family="Helvetica, Arial, sans-serif" font-size="10.5" font-weight="normal" fill="#222222" text-anchor="middle" dominant-baseline="central">recent turns</text>
<text x="416" y="258.0" font-family="Helvetica, Arial, sans-serif" font-size="10.5" font-weight="normal" fill="#222222" text-anchor="start" dominant-baseline="central">MCP over HTTP (/mcp),</text>
<text x="416" y="270.0" font-family="Helvetica, Arial, sans-serif" font-size="10.5" font-weight="normal" fill="#222222" text-anchor="start" dominant-baseline="central">same Bearer token</text>
<text x="416" y="395.0" font-family="Helvetica, Arial, sans-serif" font-size="10.5" font-weight="normal" fill="#222222" text-anchor="start" dominant-baseline="central">HTTPS/HTTP,</text>
<text x="416" y="407.0" font-family="Helvetica, Arial, sans-serif" font-size="10.5" font-weight="normal" fill="#222222" text-anchor="start" dominant-baseline="central">same Bearer token</text>
</svg>
+367 −0
Changes for ai2/implementation-and-delivery.md: 367 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
type: Implementation & Delivery
title: "AI² — Implementation & Delivery"
description: "How the AI² code is laid out, its settings and their defaults in each deployment, how it is built and deployed, what it logs, how it is tested, its known gaps, what is not yet scheduled, and the changes developers make most often."
tags: [ai2, implementation, delivery]
timestamp: 2026-10-21T00:00:00Z
component: AI²
version: "2.0"
status: "ETSI SDG OOP"
---

# AI² — Implementation & Delivery

**Companion documents:**
[ai2/overview-and-architecture.md](/ai2/overview-and-architecture.md),
[ai2/requirements-and-design.md](/ai2/requirements-and-design.md),
[ai2/interface-contract.md](/ai2/interface-contract.md)

Code: the `ai2` repository. The agent is in `ai_agent/`, the MCP server in `mcp_module/`. The
platform deploys AI² with the chart in the helm repository (`oop-platform-chart/charts/ai2`).

---

## O. Code Layout

### O.1 Agent (`ai_agent/`)

| Folder | Holds |
|---|---|
| `app.py` | The FastAPI app, cross-origin settings, the routers, and the local `uvicorn` entry point |
| `routes/` | `groq.py`: the chat routes. `debug.py`: `/health` and `/debug/objgraph` |
| `sub_agents/` | One file per LLM provider. `groq_agent.py` builds the MCP client, the Groq model and the agent for one request |
| `prompts/` | System prompts |
| `memory.py` | Session memory in Redis |
| `security.py` | The bearer-token check and `AUTH_ENABLED` |
| `utils.py` | Server-sent event helpers |

### O.2 MCP Server (`mcp_module/`)

| Folder | Holds |
|---|---|
| `app.py` | The FastMCP server, the `SAFE_TOOLS` list, `/health`, and the FastAPI app that mounts MCP |
| `tools/` | One file per CAMARA API: `edge_application.py`, `qod.py`, `traffic_influence.py`, `location.py`. `oeg_request.py` builds OEG addresses and headers; `oeg_errors.py` turns OEG errors into tool errors |
| `models/` | Pydantic input models, one file per API. They follow the CAMARA API-to-MCP tool mapping, or define custom models where none exists |
| `security/` | `authorization.py`: the token passed on to OEG |

### O.3 Elsewhere

| Folder | Holds |
|---|---|
| `tests/ai_agent/`, `tests/mcp_module/` | pytest tests (§S) |
| `deployment/docker/` | Docker Compose file for local runs |
| `deployment/helm/ai2/` | The AI2 repository's own chart (§Q) |
| `.gitlab-ci.yml` | The pipeline (§Q) |

Each service has its own `Dockerfile` and `requirements.txt`. The agent uses `langchain_groq` for
the model and `mcp_use` as MCP client and agent loop; the MCP server uses `fastmcp` and `httpx`.
Versions are pinned in each `requirements.txt`. There is no import-linter configuration, so the
pipeline's architecture stage is skipped.

---

## P. Configuration

Environment variables. Locally, each service reads a `.env` file in its folder.

### P.1 Agent

| Group | Settings | Notes |
|---|---|---|
| Server | `APP_HOST`, `APP_PORT` | `127.0.0.1` and `9013` for a local run; the image binds `0.0.0.0:9013` |
| MCP | `MCP_SERVER_URL` | Required. The chart sets `http://<mcp service>:<port>/mcp`. The image default `http://127.0.0.1:8004/mcp` only works with both on one host |
| Groq | `GROQ_API_KEY`, `GROQ_MODEL_NAME` | The key is required; the chart reads it from `secrets.groqApiKey`. Model default in code: `openai/gpt-oss-20b`. The model must support tool calling (overview E5) |
| Memory | `REDIS_URL`, `MEMORY_WINDOW_TURNS`, `MEMORY_TTL_SECONDS`, `MEMORY_KEY_PREFIX` | `REDIS_URL` is a Redis address such as `redis://host:6379/0`; empty turns memory off. Defaults 6 turns, 3600 seconds, `ai_agent:session`. Whole numbers of at least 1 (§L.2 in the design) |
| Login | `AUTH_ENABLED` | `true` by default; only the value `false` (any case) turns it off |
| Telemetry | `MCP_USE_ANONYMIZED_TELEMETRY` | The library sends usage data unless this is `false` |

`MCP_SERVER_URL` and `GROQ_API_KEY` are read on each chat request, not at start. A missing value
shows as a `500` on the first chat, not as a failed start.

### P.2 MCP Server

| Group | Settings | Notes |
|---|---|---|
| Server | `MCP_HOST`, `MCP_PORT` | `127.0.0.1` and `8004` for a local run; the image binds `0.0.0.0:8004` |
| OEG | `OEG_SERVICE_URL` | Required. OEG's base address, without a path; the chart sets `mcp.oegUrl`, else `http://<global.oeg.host>:<global.oeg.port>` (`http://oeg:80`). Read on each tool call. The image default `http://127.0.0.1:32263` is a development leftover |
| Login | `AUTH_ENABLED` | As for the agent |

### P.3 Defaults by Deployment

| Setting | Code | Platform chart | AI2 repository chart | Docker Compose |
|---|---|---|---|---|
| `GROQ_MODEL_NAME` | `openai/gpt-oss-20b` | `qwen/qwen3-32b` | `qwen/qwen3.8-27b` | `qwen/qwen3.8-27b` |
| `MEMORY_WINDOW_TURNS` | `6` | `3` | `3` | `10` |
| `MEMORY_TTL_SECONDS` | `3600` | `3600` | `3600` | `3600` |
| `AUTH_ENABLED` | `true` | From `global.auth.enabled` | From `global.auth.enabled` | Not set (`true`) |
| Telemetry | On | Off | Off | On |
| Redis | — | Own Deployment, `redis:7.4-alpine` | Own Deployment, `redis:7.4-alpine` | `redis:7-alpine`, host port `6385` |

The READMEs give other values (window `6` or `10`). Which models are allowed is open (overview F3).

### P.4 Chart Values

Both charts take the same values.

| Group | Values | Notes |
|---|---|---|
| Images | `mcp.image.*`, `aiAgent.image.*` (`repository`, `tag`, `pullPolicy`), `aiAgent.waitForMcpImage` | Tags `2.0.0`. The init container image defaults to `curlimages/curl:8.11.1` |
| Replicas | `mcp.replicaCount`, `aiAgent.replicaCount` | `1` each |
| Settings | `mcp.port`, `mcp.oegUrl`, `aiAgent.port`, `aiAgent.groqModelName`, `aiAgent.memory.windowTurns`, `aiAgent.memory.ttlSeconds`, `aiAgent.memory.keyPrefix` | Become the ConfigMap |
| Redis | `redis.enabled`, `redis.image`, `redis.port`, `redis.resources` | `false` turns memory off. Requests 50m CPU and 64Mi; limit 128Mi |
| Secret | `secrets.groqApiKey`, `secrets.existingSecret` | Set the key in `environments/<env>/secrets.values.yaml`, kept out of Git, or name an existing Secret holding `GROQ_API_KEY`. Empty: the deploy script warns and every chat fails |
| Services | `mcp.service.*`, `aiAgent.service.*` (`type`, `port`, `nodePort`; the agent also `name`) | `ClusterIP` in both charts; NodePort `32004` and `32013` in kind |

Umbrella values: `ai2.enabled: true`, `ai2.fullnameOverride: ai2`, which names the services
`ai2-mcp` and `ai2-ai-agent`. In the umbrella chart every value above sits under `ai2.`, for example
`ai2.secrets.groqApiKey` and `ai2.mcp.service.nodePort`. For kind the key goes in
`helm/environments/kind/secrets.values.yaml`.

### P.5 Portal Settings Pointing at AI²

| Setting | Notes |
|---|---|
| `AI2__CHAT_URL` (chart `ai2.chatUrl`) | Default `http://ai2-ai-agent:9013/groq-mcp`. Empty turns the chat off ([portal §P](/portal/implementation-and-delivery.md)) |
| `AI2__TIMEOUT` | Default `120` seconds. Not in the portal's chart |

---

## Q. Build and Deployment

**Images.** Each service builds from `python:3.12-slim` with pip and runs `uvicorn`. Neither image
sets a user, so both run as root. Registry: `labs.etsi.org:5050/oop/code/ai2/ai_agent` and
`…/mcp_module`.

**Pipeline.** `.gitlab-ci.yml` runs mypy, import-linter (skipped, no configuration), ruff check,
ruff format check, pytest and GitLab SAST. On `main`, `develop` and tags it builds both images with
`docker buildx --provenance=false` and pushes them. Tags: the Git tag; `latest` on `main`;
`develop` on `develop`; the branch name with `/` as `-` otherwise.

**Platform chart.** `oop-platform-chart/charts/ai2` in the helm repository (chart 2.0.0):

| The chart | How |
|---|---|
| Agent | Deployment `ai2-ai-agent`. An init container polls the MCP server's `/health` every 3 seconds before the agent starts |
| MCP server | Deployment `ai2-mcp` |
| Redis | Deployment and Service on `emptyDir`, no snapshots, non-root, read-only root filesystem. A NetworkPolicy admits only the agent |
| Settings | ConfigMap (§P) and Secret `GROQ_API_KEY` |
| Probes | Readiness and liveness on `/health` for both services |
| Login off | `global.auth.enabled: false` sets `AUTH_ENABLED=false` on both |

**AI2 repository chart.** `deployment/helm/ai2` (chart 2.0.0) has the same templates as the
platform chart. Only the model differs: `qwen/qwen3.8-27b` here, `qwen/qwen3-32b` in the platform
chart. Which chart is authoritative is open (overview F10). Both values files pin image tag `2.0.0`;
the pipeline tags `develop` images `develop`. Compose's Redis host port `6385` has no NodePort
equivalent, since NodePorts must be in `30000-32767`.

**kind.** `helm/scripts/helm-deploy.sh` deploys the umbrella chart with
`environments/kind/values.yaml` and, if present, `secrets.values.yaml`. The agent is at
`http://localhost:32013`, the MCP server at `http://localhost:32004`. To run locally built
images, load them with `kind load docker-image` and set `pullPolicy: IfNotPresent`.

**Local.** `docker compose --env-file .env up -d` in `deployment/docker/` starts Redis, the MCP
server and the agent, with every port on the host. This is AI²'s own Compose file; the platform's
single root Compose layout with an `ai` profile ([platform §Docker Compose Profiles](/platform/architecture.md))
is the target, and this spec does not cover it. Without Docker: `python -m ai_agent.app` and
`python -m mcp_module.app` from the repository root, each with its `.env`. Use Python 3.12, as the
images do, and install with `pip install -r ai_agent/requirements.txt -r mcp_module/requirements.txt`.
Build an image with `docker build -t <name> ai_agent` (or `mcp_module`). Compose's Redis runs with
the image defaults and no volume, so its data goes when the container is removed.

---

## R. Observability

- **Logs.** The agent logs at `INFO` and runs the agent library in verbose mode; the library's own
  logger is limited to `ERROR`. Whether verbose mode logs prompts, answers or tool results is not
  verified. The MCP server logs at `DEBUG`, including every OEG address. Plain
  text, not the platform's JSON format (overview F11).
- **Request id.** None. A correlator reaches OEG only when the model passes `xCorrelator`.
- **Health.** `/health` on both services shows only that the process runs. It does not check
  Groq, Redis or OEG.
- **Metrics.** None.
- **Portal side.** The portal logs AI² refusing a token as a security event
  ([portal §R](/portal/implementation-and-delivery.md)).

Troubleshooting in the cluster:

```bash
kubectl get pods -n oop -l app.kubernetes.io/name=ai2
kubectl logs -f deployment/ai2-ai-agent -n oop
kubectl logs -f deployment/ai2-mcp -n oop
```

A `401` from the agent means the request had no bearer token. A tool error naming OEG's status
means OEG refused the call, usually because the user lacks that API product.

---

## S. Testing

| Kind | Covers | Runs With |
|---|---|---|
| Unit, agent | `400` without `query` on both routes, `401` without a token, success with login off, `500` when the agent run fails | `pytest` from the repository root |
| Unit, MCP server | Token passing and refusal; unknown routes and `/health/` answer `404`; input rules; resource rules; correlator; address joining; idempotency key length; OEG error to tool error | `pytest` |
| Contract, MCP to OEG | For every registered tool: method, path, headers and body against a mocked OEG; CAMARA errors become tool errors; no HTTP call without a token; transport failures never look like success. A check fails when a registered tool has no case | `pytest` (`tests/mcp_module/test_tool_requests.py`) |
| Static | mypy, ruff lint and format, GitLab SAST | GitLab pipeline |

Run one file with `pytest tests/mcp_module/test_oeg_errors.py`. The MCP server README reports use
with Open-WebUI 0.6.34 on Python 3.11; no test covers it.

Missing coverage: session memory with Redis, a successful stream, the agent passing the token to
the MCP server, the portal's chat contract, any test against a real OEG or Groq, and chart
rendering.

---

## T. Known Gaps

Unspecified means no reason has been agreed.

| Gap | Why It Is Acceptable for Now | Workaround |
|---|---|---|
| Tokens are not validated (overview F1) | Unspecified. The umbrella chart keeps both services `ClusterIP` | Do not expose the agent outside the cluster |
| Memory is not tied to the caller (F4) | Unspecified | `redis.enabled: false` |
| User data goes to Groq (F3) | Unspecified | None |
| No time budget; a run is not stopped when the caller leaves (F5) | Unspecified | Check the state in OEG before retrying |
| No confirmation before writes; retries can repeat creates (F6, F7) | Unspecified | Ask the agent to use an idempotency key; check state before retrying |
| No federation tools, though the public documentation says AI² manages federation | Unspecified | The portal's Federation tab |
| OEG operations without a tool (§K.1 in the design) | Unspecified | The portal, or OEG directly |
| The two charts differ in the model (F10) | Unspecified | Deploy with the platform chart |
| Missing settings show at the first chat, as a plain `500` | Unspecified | Check the agent log after the first chat |
| Health checks miss Groq, Redis and OEG | Unspecified | Watch the logs |
| Containers run as root; no pod security settings for the agent or MCP server | Unspecified | None |
| No CPU or memory requests or limits for the agent and MCP server | Unspecified | None |
| OEG scopes reads and deletes by app provider, not organization, so developers of one organization do not see each other's resources through AI² ([oeg/overview-and-architecture.md F5](/oeg/overview-and-architecture.md#f-open-questions)) | Open in OEG's spec | None in AI² |
| Plain logs, `DEBUG` on the MCP server, no request id or metrics (F11) | Unspecified | None |
| `/debug/objgraph` needs no token (F9) | Unspecified | Keep the agent `ClusterIP` |
| The system prompt ends mid-sentence; `TOOL_USING_ASSISTANT_PROMPT` and `UserPhoneInput` are unused | Unspecified | None |
| What happens at the 20-model-call limit is not verified | Unspecified | None |
| READMEs are out of date: "Quart backend", other defaults | Unspecified | Use this spec |
| The portal keeps the conversation list only in the browser tab ([portal §U.9](/portal/implementation-and-delivery.md)) | Accepted for now in the portal's spec | — |

---

## U. Next Release

No AI² work is scheduled for the next release. These are intentions with some evidence, not plans.

### U.1 Conversation History That Survives Reload

- **Missing.** The portal loses the conversation list on reload ([portal §U.9](/portal/implementation-and-delivery.md)).
- **Plan.** Unspecified.
- **Already in place.** The agent keeps recent turns per `session_id` (§L in the design).

### U.2 Other LLM Providers

- **Missing.** Only Groq is implemented.
- **Plan.** Unspecified. Intent: AI² should not depend on one LLM provider. No provider is agreed
  (overview F3).
- **Already in place.** The sub-agent layout (`sub_agents/`, `routes/`) and the portal's provider-free
  `ai2.chatUrl`.

### U.3 More Tools and Prompts

- **Missing.** Tools for the OEG operations listed in §K.1 of the design; MCP prompts.
- **Plan.** Unspecified. The MCP server README's roadmap names prompts, more tool calls and fuller
  parameter checks.
- **Already in place.** The tool pattern and its wire-contract tests (§V.1).

### U.4 Agent-to-Agent and Operator Agents

- **Missing.** A2A support and operator agents.
- **Plan.** Future phases in [platform/future-architecture.md §6](/platform/future-architecture.md#6-future-delivery-phases).
- **Already in place.** The MCP server that wraps OEG.

---

## V. Common Changes

### V.1 Add a Tool for an OEG Operation

1. Read the operation in [oeg/interface-contract.md](/oeg/interface-contract.md) and note which API
   product's scope it needs.
2. Add the input model to `mcp_module/models/<api>_models.py`.
3. Add the function to `mcp_module/tools/<api>.py`. Use `get_service_url`, `get_headers` and
   `raise_for_oeg_status`, and set `timeout=15`.
4. Add it to `SAFE_TOOLS` in `mcp_module/app.py`.
5. Add a `Case` to `tests/mcp_module/test_tool_requests.py`. Without one, the coverage check fails.
6. Update [interface-contract.md §2–3](/ai2/interface-contract.md); the tool count and lists in
   design §I.2 and §K.1 (including the "no tool" list); the overview's diagram and §C features;
   §T and §U.3 here; and the MCP server README. The spec is the source; the README follows it.

- **Check:** design §K, [ADR-0030](/decisions/0030-oeg-error-model-is-camara-errorinfo-not-rfc7807.md),
  [ADR-0048](/decisions/0048-platform-roles.md).
- **Run:** `pytest`. The pipeline also runs mypy and ruff.
- **Elsewhere:** none, unless OEG must add the operation first.

**Note:** A tool not listed in `SAFE_TOOLS` is never offered to the model.

### V.2 Follow an OEG API Change

1. Change the base path in `mcp_module/tools/<api>.py` and the affected models.
2. Update the expected paths and bodies in `test_tool_requests.py`.
3. Update design §K.1 and the contract.

- **Check:** the OEG interface contract and the vendored CAMARA specification.
- **Run:** `pytest`.
- **Elsewhere:** release AI² with the OEG version that serves the new path.

### V.3 Change the Model, or Add a Provider

1. Model only: set `config.aiAgent.groqModelName` in both charts (and `GROQ_MODEL_NAME` in Compose).
   Pick a model that supports tool calling.
2. New provider: add `ai_agent/sub_agents/<provider>_agent.py`, routes in `ai_agent/routes/`, and
   register them in `app.py`. Reuse `get_authorization` (`ai_agent/security.py`) and
   `RedisShortTermMemory` (`ai_agent/memory.py`).
3. Add its settings and secret to both charts.
4. If the chat path changes, set the portal's `ai2.chatUrl`.

- **Check:** overview F3; design §M.3.
- **Run:** `pytest`; add error-code tests like `tests/ai_agent/test_error_codes.py`.
- **Elsewhere:** helm repository; portal chart for a new path.

### V.4 Change Session Memory

1. Set `config.aiAgent.memoryWindowTurns`, `memoryTtlSeconds` or `memoryKeyPrefix`, or
   `redis.enabled: false` to turn memory off.
2. To change how memory works, edit `ai_agent/memory.py`.

- **Check:** design §L, overview F4 and F8.
- **Run:** `pytest`. There is no memory test yet; add one with a fake Redis.
- **Elsewhere:** helm repository; the AI2 repository's chart too.

### V.5 Change Deployment Settings

1. Change the platform chart in the helm repository.
2. Make the same change in `deployment/helm/ai2` in the AI2 repository, until overview F10 is
   decided.
3. Environment overrides go in `environments/<env>/values.yaml`; secrets in
   `secrets.values.yaml`.

- **Check:** §P and §Q.
- **Run:** `helm template` on the umbrella chart, then deploy to kind.
- **Elsewhere:** the portal chart if the agent's address changes.

**Note:** Both services must get the same `AUTH_ENABLED`, or login-off mode fails at the MCP
server.

### V.6 Change the Chat Contract With the Portal

1. Change `ai_agent/routes/groq.py`.
2. Change the portal's `portal-gui/src/types/ai2.ts` and `services/ai2/chat.ts`. The portal backend
   forwards the body unchanged.
3. Update [interface-contract.md §1](/ai2/interface-contract.md) and
   [portal §K](/portal/requirements-and-design.md).

- **Check:** REQ-AI2-04, REQ-PORTAL-09.
- **Run:** `pytest` in AI2; the portal's `make check` and `make test`.
- **Elsewhere:** release AI² and the portal together.

---

## Revision History

| Version | Date | Summary |
|---------|------|---------|
| 2.0 | 2026-10-21 | First version, describing the Release 2 implementation. |

ai2/index.md

0 → 100644
+8 −0
Changes for ai2/index.md: 8 added lines, 0 removed lines.
Original line number Diff line number Diff line
# AI² (AI Squared)

Four-part spec; read in order.

* [Overview & Architecture](overview-and-architecture.md) — what AI² is, who can use it, terms, features, boundaries, assumptions, open questions
* [Requirements & Design](requirements-and-design.md) — requirements, interfaces, the chat flow, tool calls to OEG, session memory, security, errors
* [Implementation & Delivery](implementation-and-delivery.md) — code layout, settings, deployment, observability, testing, known gaps, common changes
* [Interface Contract](interface-contract.md) — chat API payloads, every MCP tool, input rules, worked examples
+315 −0
Changes for ai2/interface-contract.md: 315 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
type: Interface Contract
title: "AI² — Interface Contract"
description: "Payloads of the agent's chat API and stream, how session history is put into the prompt, every MCP tool with its OEG operation, inputs and result, the input rules, tool errors, and worked examples."
tags: [ai2, mcp, agent, api, contract, examples]
timestamp: 2026-10-21T00:00:00Z
component: AI²
version: "2.0"
status: "ETSI SDG OOP"
---

# AI² — Interface Contract

**Companion documents:**
[ai2/overview-and-architecture.md](/ai2/overview-and-architecture.md),
[ai2/requirements-and-design.md](/ai2/requirements-and-design.md),
[ai2/implementation-and-delivery.md](/ai2/implementation-and-delivery.md)

This document holds reference detail of the current implementation. Requirements, ownership and
the reasons behind them are in [requirements-and-design.md](/ai2/requirements-and-design.md);
where the two disagree, that document wins. OEG's own request and response schemas are in
[oeg/interface-contract.md](/oeg/interface-contract.md) and the vendored CAMARA specifications.

---

## 1. Agent Chat API

### 1.1 `POST /groq-mcp`

Request: a JSON object. Other fields are ignored.

| Field | Type | Required | Rule |
|---|---|---|---|
| `query` | string | Yes | Not empty |
| `session_id` | string | No | Any value. Names the conversation for session memory |

Header: `Authorization: Bearer <token>`, unless login is off.

Answer `200`:

| Field | Type | Present |
|---|---|---|
| `response` | string | Always. The model's final text |
| `session_id` | string | When the request had one. The same value |

Errors: [requirements-and-design.md §N.1](/ai2/requirements-and-design.md).

### 1.2 As the Portal Uses It

The browser sends `{"query": <text>, "session_id": <chat thread id>}` to the portal's
`POST /api/ai2/chat`. The portal adds the token and forwards the body unchanged. The browser reads
`response`, and on an error shows `detail` or `error` from the body
([portal §K](/portal/requirements-and-design.md)). A body with neither, such as the agent's
plain-text `500`, shows a generic message ("AI² isn't responding right now" for any `5xx`;
portal code `portal-gui/src/services/ai2/chat.ts`).

### 1.3 `GET /groq-mcp/stream`

Query parameters `query` (required) and `session_id` (optional). The answer is
`text/event-stream` with `Cache-Control: no-cache`, `X-Accel-Buffering: no` and
`Access-Control-Allow-Origin: *`.

```text
id: 0
event: message
data: {"chunk": "<up to 50 characters of the answer>"}

id: 1
event: message
data: {"chunk": "<next 50 characters>"}

id: 2
event: complete
data: {"done": true}
```

On failure, one event replaces the rest:

```text
id: 0
event: error
data: {"error": "<message>"}
```

Events are 10 milliseconds apart. The answer is complete before the first event is sent.

### 1.4 Prompt Built From Memory

With history, the agent sends the model this text in place of the query:

```text
Use the recent conversation history below to answer consistently.
History:
USER: <earlier query>
ASSISTANT: <earlier answer>

Current USER request: <query>
```

The system prompt is `CONVERSATION_MEMORY_PROMPT` in `ai_agent/prompts/memory.py`. Its text ends
mid-sentence ("…as concise as possible with the less").

---

## 2. MCP Tools

**Calling conventions.** A tool whose input is a model takes it as the argument `inp`. The other
tools take flat arguments. Every tool also takes an optional `xCorrelator`
([requirements-and-design.md §K.2](/ai2/requirements-and-design.md)). Paths below are relative to
the API's base path (§K.1 there).

"Pending" means the result carries `status_code` and a `message` ending "accepted; completion is
pending." A non-JSON success body comes back as `{"status_code", "body"}`.

### 2.1 Edge Application Management (`edge-application-management/v0.1alpha1`)

| Tool | Arguments | OEG call | Result |
|---|---|---|---|
| `get_app_definitions` | — | `GET /apps` | `{"applications": [...]}`. OEG must answer a list |
| `register_app_definition` | `inp`: application manifest (§3.3) | `POST /apps` | OEG's object, carrying the `appId` |
| `get_app_definition` | `inp`: `{appId}` | `GET /apps/{appId}` | OEG's object. It must contain `appManifest` |
| `delete_app_definition` | `inp`: `{appId}` | `DELETE /apps/{appId}` | `{"status_code": 204}` |
| `get_edge_cloud_zones` | `region`, `status` (`active`, `inactive`, `unknown`) | `GET /edge-cloud-zones` | `{"edgeCloudZones": [...]}`. OEG answers a bare list ([oeg/interface-contract.md §K.7](/oeg/interface-contract.md)); an object would pass through unchanged |
| `instantiate_app` | `inp`: `{name, appId, edgeCloudZoneId, kubernetesClusterRef, subscriptionRequest, idempotencyKey}` | `POST /appinstances` | OEG's object. One zone only |
| `get_app_instances` | `appId`, `appInstanceId`, `region` | `GET /appinstances` | `{"appInstances": [...]}` when OEG answers a list; an object passes through unchanged |
| `delete_app_instance` | `inp`: `{appInstanceId}` | `DELETE /appinstances/{appInstanceId}` | Pending |
| `create_app_deployment` | `inp`: `{appDeploymentName, appId, edgeCloudZones, kubernetesClusterRefs, subscriptionRequest, idempotencyKey}` | `POST /deployments` | OEG's object plus pending note. It must contain `appDeploymentId` |
| `get_app_deployments` | `appId`, `appDeploymentId` | `GET /deployments` | `{"appDeployments": [...]}`. OEG must answer a list |
| `delete_app_deployment` | `inp`: `{appDeploymentId}` | `DELETE /deployments/{appDeploymentId}` | Pending |

### 2.2 Quality on Demand (`quality-on-demand/v1`)

| Tool | Arguments | OEG call | Result |
|---|---|---|---|
| `create_qod_session_oai` | `inp`: session request (§3.1) | `POST /sessions` | OEG's session object |
| `get_qod_session_oai` | `inp`: `{sessionId}` | `GET /sessions/{sessionId}` | The session, checked against §3.1's answer rules |
| `delete_qod_session_oai` | `inp`: `{sessionId}` | `DELETE /sessions/{sessionId}` | `{"status_code": 204}` |

### 2.3 Traffic Influence (`traffic-influence/v0.10`)

| Tool | Arguments | OEG call | Result |
|---|---|---|---|
| `create_traffic_influence` | `inp`: traffic influence request (§3.2) | `POST /traffic-influences` | OEG's object. Creation does not mean the influence is active yet |
| `create_traffic_influence_device` | `inp`: traffic influence request with `device` (§3.2) | `POST /traffic-influence-devices` | OEG's object. Creation does not mean the influence is active yet |
| `get_traffic_influences` | `appId` | `GET /traffic-influences` | `{"trafficInfluences": [...]}`. OEG must answer a list |
| `get_traffic_influence` | `trafficInfluenceID` | `GET /traffic-influences/{trafficInfluenceID}` | OEG's object |
| `delete_traffic_influence` | `trafficInfluenceID` | `DELETE /traffic-influences/{trafficInfluenceID}` | Pending |

### 2.4 Location Retrieval (`location-retrieval/v0.5`)

| Tool | Arguments | OEG call | Result |
|---|---|---|---|
| `retrieve_location` | `inp`: `{device, maxAge, maxSurface}` (§3.4) | `POST /retrieve` | OEG's object: the location time and a circle or polygon |

---

## 3. Input Rules

The MCP server checks these before calling OEG. A failed check is a tool error. OEG checks its
own contract again.

### 3.1 Quality on Demand

| Field | Rule |
|---|---|
| `device` | Optional. If given, at least one of `phoneNumber`, `networkAccessIdentifier`, `ipv4Address`, `ipv6Address` |
| `device.phoneNumber` | `^\+[1-9][0-9]{4,14}$` |
| `device.ipv4Address` | `publicAddress` (IPv4) and at least one of `privateAddress` (IPv4) or `publicPort` (0–65535) |
| `device.ipv6Address` | A valid IPv6 address |
| `applicationServer` | Required: `ipv4Address` or `ipv6Address` |
| `devicePorts`, `applicationServerPorts` | `ranges` (`from`, `to`, 0–65535) or `ports`; at least one, each list non-empty |
| `qosProfile` | Required, 3–256 characters, `^[a-zA-Z0-9_.-]+$` |
| `duration` | Required, seconds, at least 1 |
| `sink` | Optional, `https://…` |
| `sinkCredential` | `credentialType` `PLAIN`, `ACCESSTOKEN` or `REFRESHTOKEN`. `ACCESSTOKEN` needs `accessToken`, `accessTokenExpiresUtc` (in the future, with time zone) and `accessTokenType` (lower-cased) |

The session read must have `sessionId`, `applicationServer`, `qosProfile`, `duration` (at least 1)
and `qosStatus` (`REQUESTED`, `AVAILABLE`, `UNAVAILABLE`). `statusInfo` is `DURATION_EXPIRED`,
`NETWORK_TERMINATED` or `DELETE_REQUESTED` when present.

### 3.2 Traffic Influence

| Field | Rule |
|---|---|
| `apiConsumerId` | Required string |
| `appId` | Required UUID |
| `appInstanceId`, `edgeCloudZoneId` | Optional UUIDs |
| `edgeCloudRegion` | Optional string |
| `device` | `create_traffic_influence_device` only. Optional here; same rules as §3.1. OEG requires it, since no token claim maps to a device ([oeg/overview-and-architecture.md F4](/oeg/overview-and-architecture.md#f-open-questions)), and accepts only a device with `ipv4Address`. Without one: `422`/`MISSING_IDENTIFIER`; other identifiers alone: `422`/`SERVICE_NOT_APPLICABLE` |
| `sourceTrafficFilters.sourcePort`, `destinationTrafficFilters.destinationPort` | 0–65535 |
| `destinationTrafficFilters.destinationProtocol` | String |
| `subscriptionRequest.protocol` | `HTTP`, `MQTT3`, `MQTT5`, `AMQP`, `NATS`, `KAFKA` |
| `subscriptionRequest.sink` | Required, `https://…` |
| `subscriptionRequest.sinkCredential` | `ACCESSTOKEN` only: `accessToken` 1–8192 characters, `accessTokenType` `bearer`, `accessTokenExpiresUtc` in the future |
| `subscriptionRequest.types` | Exactly one: `org.camaraproject.traffic-influence.v1.traffic-influence-change` |
| `subscriptionRequest.config` | Required `subscriptionDetail` (object); optional `subscriptionExpireTime`, `subscriptionMaxEvents` (at least 1), `initialEvent` |
| `idempotencyKey` | Optional, at most 128 characters. Sent as `Idempotency-Key`, not in the body |

### 3.3 Edge Application Management

| Field | Rule |
|---|---|
| `appId` (register) | Optional UUID (versions 1–5). OEG generates one when omitted |
| `name`, `appDeploymentName`, instance `name` | `^[A-Za-z][A-Za-z0-9_]{1,63}$` |
| `appProvider` | `^[A-Za-z][A-Za-z0-9_]{7,63}$` |
| `version` | Up to 64 characters, default `1.0.0` |
| `packageType` | `HELM` or `CONTAINER`. `helm chart`, `helm-chart` and `helmchart` become `HELM` |
| `appRepo` | `type` `PRIVATEREPO` or `PUBLICREPO`; `imagePath` up to 2048; `userName` up to 64; `credentials` up to 2048; `authType` `DOCKER`, `HTTP_BASIC`, `HTTP_BEARER`, `NONE`; `checksum` up to 128 |
| `operatingSystem` | `architecture` `x86_64`/`x86`; `family` `RHEL`, `UBUNTU`, `COREOS`, `WINDOWS`, `OTHER`; `version` `OS_VERSION_UBUNTU_2204_LTS`, `OS_VERSION_RHEL_8`, `OS_MS_WINDOWS_2022`, `OTHER`; `license` `OS_LICENSE_TYPE_FREE`, `OS_LICENSE_TYPE_ON_DEMAND`, `OTHER` |
| `componentSpec` | Up to 100 components: `componentName` up to 64; 1–100 `networkInterfaces` with `interfaceId` `^[A-Za-z0-9][A-Za-z0-9_]{2,30}[A-Za-z0-9]$`, `protocol` `TCP`/`UDP`/`ANY`, `port` 1–65535, `visibilityType` `VISIBILITY_EXTERNAL`/`VISIBILITY_INTERNAL` |
| `requiredResources` | Kubernetes only (`infraKind: kubernetes`); `isStandalone` required; unknown fields refused |
| `requiredResources.applicationResources` | `cpuPool`: `numCPU` 1–256, `memory` 1–16384, `topology` (`minNumberOfNodes` 1–1000, `minNodeCpu` 1–256, `minNodeMemory` 1–16384). `gpuPool`: `numCPU` 1–1024, `memory` 1–16384, `gpuMemory` 1–16, topology adds `minNodeGpuMemory` 1–16 |
| `requiredResources` other | `additionalStorage` `^\d+(GB|MB)$`; `version` up to 64; `networking.primaryNetwork` (`provider`, `version` up to 64); up to 100 `additionalNetworks` (`interfaceType` `netdevice`, `vfio-pci`, `interface`); `addons` unique, from `monitoring`, `ingress` |
| `edgeCloudZones` (deployment) | 1–100 zone ids |
| `kubernetesClusterRefs` | Up to 100 |
| `appId`, `appInstanceId`, `appDeploymentId`, `edgeCloudZoneId` (other tools) | Non-empty strings |
| `idempotencyKey` | Optional, at most 128 characters. Sent as `Idempotency-Key`, not in the body |

### 3.4 Location Retrieval

| Field | Rule |
|---|---|
| `device` | Optional here; same rules as §3.1. OEG requires it, since no token claim maps to a device ([oeg/overview-and-architecture.md F4](/oeg/overview-and-architecture.md#f-open-questions)), so a call without it fails at OEG |
| `maxAge` | Seconds, at least 0 |
| `maxSurface` | Square metres, at least 1 |

---

## 4. Tool Errors

A tool error is text the model reads. The MCP server sends these:

| Text | Cause |
|---|---|
| `Bearer token required to call OEG` | No bearer token and login on |
| `OEG HTTP <status>: <code>: <message>` | OEG answered non-2xx with CAMARA `code` and `message` |
| `OEG HTTP <status>: <reason>` | OEG answered non-2xx without a usable body |
| `OEG_SERVICE_URL environment variable is not set` | MCP server misconfigured |
| `Invalid x-correlator value` | `xCorrelator` outside the allowed alphabet or longer than 256 |
| A validation message | An input broke a rule in §3 |
| `Expected a list of … from OEG`, `OEG response is missing '…'` | OEG answered with an unexpected shape |
| A connection or timeout message | OEG unreachable, or no answer within 15 seconds |

---

## 5. Worked Examples

### 5.1 Create a QoD Session

The chart README's example request:

```http
POST /groq-mcp
Authorization: Bearer eyJ...
Content-Type: application/json

{"query": "Create a QoD session for device with public IP 12.1.0.20, application server IP 198.51.100.10, QoS profile qos-e, duration 3600 seconds"}
```

**Note:** As written, this request cannot pass §3.1: a device `ipv4Address` needs a
`privateAddress` or `publicPort` as well as the public address. The tool refuses that device, so
the model must ask for the missing value or leave the device out. With a public port of `5060`
added, the model makes this MCP `tools/call`:

```json
{
  "name": "create_qod_session_oai",
  "arguments": {
    "inp": {
      "device": {"ipv4Address": {"publicAddress": "12.1.0.20", "publicPort": 5060}},
      "applicationServer": {"ipv4Address": "198.51.100.10"},
      "qosProfile": "qos-e",
      "duration": 3600
    }
  }
}
```

The MCP server sends `POST {OEG_SERVICE_URL}/quality-on-demand/v1/sessions` with that body and the
caller's token. OEG answers `201` with the session, and the agent answers `200` with a summary.

### 5.2 Deploy an Application to Two Zones

```json
{
  "name": "create_app_deployment",
  "arguments": {
    "inp": {
      "appDeploymentName": "demo_app_eu",
      "appId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "edgeCloudZones": ["<zone id 1>", "<zone id 2>"],
      "idempotencyKey": "demo-app-eu-1"
    }
  }
}
```

The MCP server sends `POST …/edge-application-management/v0.1alpha1/deployments` with
`Idempotency-Key: demo-app-eu-1` and the body without `idempotencyKey`. OEG answers `202`. The
tool returns OEG's object with `status_code: 202` and "Application deployment accepted; completion
is pending." To see progress, the user asks again and the model calls `get_app_deployments`.

### 5.3 OEG Refuses

A user without `traffic-influence` asks to list traffic influences. OEG answers `403` with a
CAMARA body. The tool error reads `OEG HTTP 403: <code>: <message>`. The agent answers `200`, and
its text says the account cannot use that API.

---

## Revision History

| Version | Date | Summary |
|---------|------|---------|
| 2.0 | 2026-10-21 | First version, describing the Release 2 implementation. |
+214 −0
Changes for ai2/overview-and-architecture.md: 214 added lines, 0 removed lines.
Original line number Diff line number Diff line
---
type: Architecture Spec
title: "AI² — Overview & Architecture"
description: "What AI² (AI squared) is: the platform's AI component, an agent and an MCP server that turn a chat message into OEG calls made as the caller. Who can use it, its terms, features, boundaries with the portal, OEG, Groq and Redis, assumptions and open questions."
tags: [ai2, mcp, agent, llm, architecture]
timestamp: 2026-10-22T00:00:00Z
component: AI²
version: "2.0"
status: "ETSI SDG OOP"
---

# AI² — Overview & Architecture

**Companion documents:**
[ai2/requirements-and-design.md](/ai2/requirements-and-design.md),
[ai2/implementation-and-delivery.md](/ai2/implementation-and-delivery.md),
[ai2/interface-contract.md](/ai2/interface-contract.md)

**Closest neighbours:**
[portal/requirements-and-design.md](/portal/requirements-and-design.md),
[oeg/overview-and-architecture.md](/oeg/overview-and-architecture.md),
[ADR-0048](/decisions/0048-platform-roles.md)

---

## Mental Model

**AI² turns a chat message into calls to OEG.** The agent gives the message to a large language
model (LLM). The model picks tools from the MCP server, and each tool calls one OEG CAMARA
operation. AI² never calls SRM, FM or infrastructure.

**AI² acts as the caller, never as itself.** It passes the caller's bearer token to OEG on every
tool call. OEG decides what the call may do. AI² holds no OEG credentials of its own.

**AI² remembers a conversation briefly, or not at all.** With Redis configured, the agent keeps
the last few turns of each conversation for a limited time. In the platform chart, nothing is
written to disk.

![AI²: portal backend, agent, Groq, Redis, MCP server and OEG](diagrams/overview.drawio.svg)

---

## A. Summary

**Success definition.** A user asks, in plain language, for something OEG offers. AI² does it
through OEG with that user's own rights, and answers in text.

AI² is the platform's AI component.

### A.1 Scope and Ownership

| Concern | Position | Record |
|---|---|---|
| Shape | Two services, the agent and the MCP server, plus an optional Redis | — |
| Who decides access | OEG, from the scopes in the caller's token. AI² adds no rules of its own | [ADR-0048](/decisions/0048-platform-roles.md) |
| Credentials | No OEG credentials of its own: the portal adds the user's token and AI² passes it on. AI²'s only secret is the Groq API key | [portal/overview-and-architecture.md §D](/portal/overview-and-architecture.md) |
| What tools may call | OEG's public CAMARA APIs only | [platform/future-architecture.md §4](/platform/future-architecture.md#4-ai-agents-how-they-fit) |
| LLM | Groq's hosted API, chosen by `GROQ_MODEL_NAME`. Which providers and models are allowed is open (F3) | — |
| Memory | Optional, in Redis, keyed by the conversation's `session_id` | [requirements-and-design.md §L](/ai2/requirements-and-design.md) |
| Not part of AI² | Federation, direct SRM or FM calls, a database, an `ai-agent` role | [ADR-0048](/decisions/0048-platform-roles.md) |

### A.2 Who Sees What

| Caller | Reaches | Needs |
|---|---|---|
| Portal user | The agent, through the portal's `POST /api/ai2/chat` | Logged in to the portal |
| Software inside the cluster | The agent's `POST /groq-mcp` or `GET /groq-mcp/stream` | A bearer token |
| Another MCP client | The MCP server's `/mcp` | A bearer token. Whether this use is supported is open (F9) |
| Anyone, with login off | Both services | Nothing (`AUTH_ENABLED=false`) |

What a tool call may actually do is OEG's decision. A user's token carries the CAMARA scopes of
the API products granted to them: `edge-application-management`, `quality-on-demand`,
`location-retrieval` and `traffic-influence` (ADR-0048). A user without a product can still
chat, but OEG refuses that product's tool calls and the agent says so in its answer.

---

## B. Terms

Platform-wide terms, including MCP, A2A and API product: [platform/glossary.md](/platform/glossary.md).

| Term | Meaning here |
|---|---|
| **Agent** | The `ai_agent` service. It takes a chat message, runs the LLM and calls tools. Elsewhere called "AI Agent" or "ai-agent" |
| **MCP server** | The `mcp_module` service. It offers OEG operations as MCP tools. Elsewhere called "MCP Module" |
| **Tool** | One MCP function the model may call. Each tool makes one OEG call |
| **Tool error** | A failed tool call reported back to the model as a message, not as an HTTP error to the caller |
| **Sub-agent** | One LLM-backed agent configuration behind its own routes. Today there is one: Groq |
| **Conversation** | All messages sharing one `session_id`. The caller chooses the value |
| **Turn** | One user message and the agent's answer to it |
| **Session memory** | The last turns of a conversation, kept in Redis for a limited time |

---

## C. Features

| ID | Feature | Who |
|---|---|---|
| UC-AI2-01 | Ask, in plain language, for platform actions and get a text answer (the portal's UC-PORTAL-06) | Portal users; callers with a token |
| UC-AI2-02 | Receive the answer as a server-sent event stream | Callers with a token |
| UC-AI2-03 | Register, list, read and delete application definitions; list edge cloud zones; create, list and delete application instances and deployments | As far as OEG allows |
| UC-AI2-04 | Create, read and delete Quality on Demand sessions | As far as OEG allows |
| UC-AI2-05 | Create traffic influence for an application or one device; list, read and delete traffic influence resources | As far as OEG allows |
| UC-AI2-06 | Retrieve a device's location area | As far as OEG allows |
| UC-AI2-07 | Continue a conversation using its recent turns | Any caller that sends a `session_id`, when Redis is configured |
| UC-AI2-08 | Use AI²'s tools from another MCP client | Status open (F9) |

**Note:** AI² cannot manage federation. The public documentation says it can; no tool exists
([implementation-and-delivery.md §T](/ai2/implementation-and-delivery.md)).

---

## D. Boundaries

**Portal: the usual caller.** The portal backend forwards each chat message to the agent with
the user's access token and the body unchanged. It keeps the list of conversations in the browser
tab and maps AI²'s `401` to its own `502` ([portal/requirements-and-design.md §K](/portal/requirements-and-design.md)).

**OEG: every platform action.** Each tool calls one OEG CAMARA operation with the caller's
token. OEG checks the token's scopes and answers in CAMARA's error format, which the tool turns
into a tool error ([oeg/interface-contract.md](/oeg/interface-contract.md)). OEG applies provider
ownership checks to reads and deletes; another provider's resource is hidden with 404. AI² uses
the caller's token and does not bypass those checks. Organization-wide sharing remains unresolved
([OEG ownership rules](/oeg/requirements-and-design.md#i1-authentication-and-ownership),
[OEG F5](/oeg/overview-and-architecture.md#f-open-questions)).

**Keycloak: tokens only.** AI² never calls Keycloak. It receives tokens Keycloak issued and
passes them on without checking them (F1).

**Groq: the LLM.** The agent sends Groq the prompt, the recent turns and every tool result.
Groq is outside the platform, so this data leaves it (F3).

**Redis: session memory.** Redis is optional. When it is down, the agent answers without memory.
In the platform chart only the agent may connect to it; Docker Compose publishes it on the host.

**Future agents.** Enterprise agents over A2A and operator automation agents are future work in
[platform/future-architecture.md §4](/platform/future-architecture.md#4-ai-agents-how-they-fit). None is built.

---

## E. Assumptions

| ID | Assumption | Risk if Wrong |
|---|---|---|
| E1 | Tokens the portal forwards carry the `oeg` audience and the user's product scopes, as for the portal's own OEG calls ([portal E2](/portal/overview-and-architecture.md)) | OEG refuses every tool call; the agent answers that it could not act |
| E2 | OEG checks the product scope on every operation a tool calls, QoD sessions included ([oeg/overview-and-architecture.md §D](/oeg/overview-and-architecture.md#d-boundaries)) | A user could act through AI² beyond their grants. ADR-0048's Consequences say OEG skips QoD scope checks; OEG's spec says it enforces them. Confirm in OEG's code |
| E3 | Only the portal backend and trusted cluster software can reach the agent and MCP services | Anyone with any token string can run the LLM on the platform's Groq key (F1) |
| E4 | The Groq API key is set (`secrets.groqApiKey`) | Every chat answers `500` |
| E5 | The Groq model chosen supports tool calling | The model cannot call tools and answers without acting |

---

## F. Open Questions

Each question names the decision needed. Implementation impact is in
[implementation-and-delivery.md §T](/ai2/implementation-and-delivery.md).

**F1. Must AI² check the caller's token?** The portal's spec says it leaves the check "to OEG
and AI²". AI² only checks that a bearer value is present. Decide whether AI² must validate tokens,
and against which issuer and audience. Affects the chat flow and both services.

**F2. AI² holds the user's full rights.** The portal's open question
([portal F2](/portal/overview-and-architecture.md)) applies here too: AI² can do anything the user
can. A narrower token needs Keycloak token exchange or a separate client.

**F3. May user data go to a hosted LLM?** Prompts, recent turns and OEG results go to Groq. These
can include phone numbers, IP addresses and device locations. A candidate rule (proposal, not
agreed): allow a hosted frontier model only in a private cloud under a data processing agreement,
and put compliance and reliability before latency. Decide which LLM providers and models are allowed and
what data may be sent.

**F4. Must session memory be bound to the user?** The memory key comes only from `session_id`.
A caller who sends another user's `session_id` reads that conversation's recent turns. Decide
whether the key must include the token's subject or tenant.

**F5. What is the time budget for one chat request?** The portal gives up after 120 seconds. One
agent run may make up to 20 model calls and many tool calls, and nothing stops it when the caller
goes away. Decide the budget and whether AI² must stop when the caller disconnects.

**F6. Must the agent confirm before changing things?** The model may create or delete resources
without asking the user. Decide whether writes need confirmation.

**F7. How are duplicate creates avoided?** The model may repeat a create after a tool error or
timeout. Four tools (`instantiate_app`, `create_app_deployment`, `create_traffic_influence` and
`create_traffic_influence_device`) send an `Idempotency-Key`, and only when the model supplies one.
Decide the rule.

**F8. Limits, audit, retention and scale.** There is no per-user limit on LLM use, no record of
which tools ran for whom, no agreed retention for memory (the 3600-second TTL is a default), and
no availability target. Each chart runs one replica of each service. Decide each.

**F9. Who may call the MCP server and the debug route?** In kind, the MCP server and the agent
are NodePorts, and `GET /debug/objgraph` needs no token. Decide whether other MCP clients are
supported and which routes may be exposed.

**F10. Which chart is the source of truth?** The AI2 repository's chart and the platform chart
differ only in the model ([implementation-and-delivery.md §Q](/ai2/implementation-and-delivery.md)).
Decide which one is authoritative and whether they must stay identical.

**F11. Does AI² adopt the platform conventions?** The platform describes JSON logs with
correlation IDs and `/healthz` plus `/readyz` for components. AI² uses plain logs and `/health`.
Decide whether and when it adopts them.

**F12. How is prompt injection handled?** User text and tool results reach the model unfiltered,
and the model can call write tools. Nothing in AI² limits what injected text can make it do,
beyond the caller's own rights. Decide whether defences are needed. Affects the chat flow.

---

## Revision History

| Version | Summary |
|---|---|
| 2.0 | Initial specification for OOP Release 2.0. |
Loading
Loading