docs(oeg): F5 is resolved, reads are scoped by app provider

sequenceDiagram
    participant B as Developer B (app_provider_id=bob)
    participant OEG
    participant DB as oeg_db
    B->>OEG: GET /apps/{alice's appId}
    OEG->>DB: record + the operation that created it
    DB-->>OEG: operation.app_provider_id = alice
    OEG-->>B: 404 (not 403, so IDs can't be probed)
    B->>OEG: GET /apps
    OEG-->>B: only bob's apps

oeg/architecture.md F5 still says reads and deletes aren't tenant-scoped and that GET /apps lists every tenant's apps. That stopped being true with OEG 1cc2ac0 (2026-10-12, "enforce app_provider_id ownership on EAM and TI app references"). Apps, app instances, app deployments, QoD sessions and traffic influences all check the app_provider_id of the operation that created them.

On OEG develop (412f8cf), the 52 ownership tests in test_eam_service.py, test_qod_service.py and test_traffic_influence_flows.py pass, for example test_hides_another_providers_app, test_list_excludes_other_providers_resources and test_raises_not_found_for_another_providers_session.

Changes:

  • F5 marked resolved and says what the scope is: the app provider (developer), not tenant_id (organization). Two developers of the same organization don't see each other's resources until ownership moves to the organization, which ADR-0066 lists as later work.
  • oeg/persistence-model.md: the tenant_id rows of qod_sessions and traffic_influences no longer claim to scope reads and deletes.

Docs only, no code change.

🤖 Generated with Claude Code

Merge request reports

Loading
+1 −1
Changes for ai2/implementation-and-delivery.md: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -232,7 +232,7 @@ Unspecified means no reason has been agreed.
| 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 does not yet scope reads and deletes by tenant, and that applies through AI² ([oeg/overview-and-architecture.md F5](/oeg/overview-and-architecture.md#f-open-questions)) | Deferred in OEG's spec | None in AI² |
| 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 |
+1 −1
Changes for ai2/requirements-and-design.md: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -334,7 +334,7 @@ AI² has no agreed retry rules; retrying a chat may repeat writes (overview F7).

| Concern | Position |
|---|---|
| Authentication, authorization, tenant isolation | The caller's token is passed on; OEG decides ([ADR-0048](/decisions/0048-platform-roles.md)). Where OEG does not yet scope reads and deletes by tenant ([oeg/overview-and-architecture.md F5](/oeg/overview-and-architecture.md#f-open-questions)), for example `GET /apps` listing every tenant's apps, the same gap applies through AI². AI² does not validate tokens (F1). Memory is not tied to the caller (F4) |
| Authentication, authorization, tenant isolation | The caller's token is passed on; OEG decides ([ADR-0048](/decisions/0048-platform-roles.md)). OEG scopes reads and deletes to the caller's app provider and hides other providers' resources with 404; AI² inherits that through the caller's token. Sharing within an organization remains open ([oeg/overview-and-architecture.md F5](/oeg/overview-and-architecture.md#f-open-questions)). AI² does not validate tokens (F1). Memory is not tied to the caller (F4) |
| Ownership, state transitions, invariants | AI² owns no platform resource. OEG and SRM own every lifecycle. AI²'s only state is session memory (§L). Invariant: AI² holds no OEG credentials (REQ-AI2-01); its only secret is the Groq API key |
| Transactions, concurrency, consistency | No transactions. Each tool call is a separate OEG call, so a failed run can leave some changes done (§J.3). Same-session requests are not coordinated (§L.3) |
| Idempotency, timeouts, retries, cancellation | Groq: 15 seconds per attempt, up to 3 retries. OEG: 15 seconds, no tool retries. Three tools can send `Idempotency-Key` (§K.2). Portal: 120 seconds. Cancellation and duplicate handling are open (F5, F7) |
+2 −2
Changes for oeg/interface-contract.md: 2 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -2559,7 +2559,7 @@ Indexes: `BTREE(operation_id)`, `BTREE(app_registration_id)`.
| `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` |
| `external_ref` | VARCHAR(255) | NULLABLE; the backend realization ref from the completion's `instances[0].external_ref`. Informational — correlation is on `session_id` |
| `tenant_id` | VARCHAR(255) | token `organization` claim, else `sub` (ADR-0048); scopes reads and deletes |
| `tenant_id` | VARCHAR(255) | token `organization` claim, else `sub` (ADR-0048). Reads and deletes are scoped by the `app_provider_id` of the creating operation, not by this column |
| `created_at` | TIMESTAMPTZ | |
| `updated_at` | TIMESTAMPTZ | |

@@ -2601,7 +2601,7 @@ Indexes: `BTREE(operation_id)`, `BTREE(tenant_id)`.
| `destination_protocol` | VARCHAR(32) | NULLABLE; from `destinationTrafficFilters.destinationProtocol` |
| `state` | VARCHAR(32) | `ordered`/`active`/`error` are activate-derived; `deletion in progress`/`deleted` are teardown-derived |
| `external_ref` | VARCHAR(255) | NULLABLE; backend realization ref. Informational — correlation is on `traffic_influence_id` |
| `tenant_id` | VARCHAR(255) | token `organization` claim, else `sub` (ADR-0048); scopes reads and deletes |
| `tenant_id` | VARCHAR(255) | token `organization` claim, else `sub` (ADR-0048). Reads and deletes are scoped by the `app_provider_id` of the creating operation, not by this column |
| `created_at` | TIMESTAMPTZ | |
| `updated_at` | TIMESTAMPTZ | |

Loading