# AwoooP RLS Canary Wave 1.3

This wave targets one outbound evidence table:

- `awooop_outbound_message`

Status: applied in production on 2026-05-12.

Production state:

- `awooop_outbound_message`: `rls=true force=true policies=1`
- Operator Console Run detail smoke:
  - run: `d385b7fe-8666-58ec-9072-9ac917adb6cf`
  - `project_id=awoooi`
  - HTTP 200
  - `outbound_messages=1`

## Why This Table

`awooop_outbound_message` stores outbound notification evidence for Run
Timeline. It is less risky than `awooop_run_state` for this wave because it is
not the worker lease/state-machine source of truth.

Latest live evidence before staging:

```text
project_id=awoooi rows=290
send_status=sent rows=290
null_project_id_rows=0
```

`awooop_run_state` remains blocked because it is used by worker lease,
state-machine transitions, approvals, and Operator Console list/detail paths.
It needs a separate query-path and cross-tenant Operator Console design pass.

## Runtime Paths

Write paths:

- `TelegramGateway._mirror_outbound_message()` opens `get_db_context(project_id)`
  and then calls `record_outbound_message()`.
- `ChannelHub._interim_feedback_task()` opens `get_db_context(project_id)` and
  then calls `record_outbound_message()`.
- `record_outbound_message()` creates a shadow run if needed and inserts the
  outbound row with the same `project_id`.

Read path:

- `platform_operator_service.get_run_detail()` reads outbound rows for the
  selected `run_id`. Operator Console links from the runs list include
  `project_id`, so the DB context matches the selected tenant.

## Apply

```bash
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 \
  -f scripts/ops/awooop-rls-canary-wave1-3-outbound-message.sql
```

The SQL aborts if:

- table is missing,
- `project_id` is missing,
- any `project_id` is NULL,
- row count exceeds the reviewed canary cap of 1000 rows.

## Verification

Expected after apply:

- no `app.project_id`: direct reads return no rows.
- `app.project_id='awoooi'`: current outbound rows remain visible.
- `app.project_id='ewoooc'`: no `awoooi` outbound rows are visible.
- rollback-only insert under `app.project_id='awoooi'` is allowed and rolled
  back.
- rollback-only insert under mismatched context is blocked by RLS.
- `/api/v1/platform/runs/{run_id}/detail?project_id=awoooi` still returns the
  selected run timeline and outbound evidence.
- global RLS preflight remains blocked only by later-wave tables.

Production verification from 2026-05-12:

- `/api/v1/health` from API pod: HTTP 200, `status=healthy`.
- `/api/v1/platform/runs/d385b7fe-8666-58ec-9072-9ac917adb6cf/detail?project_id=awoooi`:
  - HTTP 200
  - `counts.outbound_messages=1`
- `scripts/ops/awooop-rls-preflight.sh --exact-counts`:
  - `PASS=7 WARN=1 BLOCKED=1`
  - `awooop_outbound_message rls=true force=true policies=1`
  - remaining blocker tables: `audit_logs`, `awooop_run_state`,
    `incidents`, `knowledge_entries`, `playbooks`.
- Direct app-role behavior:
  - `outbound_no_context=0`
  - `outbound_awoooi_context=290`
  - `outbound_ewoooc_context=0`
  - `insert_awoooi_context_awoooi_row=allowed`
  - `insert_ewoooc_context_awoooi_row=blocked:InsufficientPrivilegeError`
  - `outbound_after_probe=290`

## Rollback

```bash
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 \
  -f scripts/ops/awooop-rls-canary-wave1-3-outbound-message-rollback.sql
```

Rollback removes the Wave1.3 policy and disables RLS on
`awooop_outbound_message`. It does not modify data.
