diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 6cbbf6260..484d7c7dc 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,55 @@ +## 2026-05-12 | RLS Canary Wave1.3 outbound message 已套用 + +**背景**:Wave1.2 `awooop_projects` 完成後,剩餘中低風險候選為 `awooop_outbound_message` 與 `awooop_run_state`。本輪先做 query-path rehearsal,再選擇較不會牽動 worker lease / Run FSM 的 outbound evidence table。 + +**範圍校正**: +- `awooop_run_state` 暫不納入: + - 牽涉 `platform_worker` SKIP LOCKED、`run_state_machine` transition、approvals、Operator Console list/detail。 + - `platform_operator_service.list_runs()` 目前用 `get_db_context("awoooi")` 支援跨 project list;若直接 tenant policy,未來非 `awoooi` rows 會被隱藏。 +- `awooop_outbound_message` 納入 Wave1.3: + - production evidence:`awoooi=290`、`sent=290`、`null_project_id_rows=0`。 + - runtime write paths:`TelegramGateway._mirror_outbound_message()` 與 `ChannelHub._interim_feedback_task()` 都用 `get_db_context(project_id)` 後呼叫 `record_outbound_message()`。 + - Operator Console Run detail link 會帶 `project_id`,detail 查詢 context 可與 row tenant 對齊。 + +**新增 artifact**: +- `scripts/ops/awooop-rls-canary-wave1-3-outbound-message.sql` +- `scripts/ops/awooop-rls-canary-wave1-3-outbound-message-rollback.sql` +- `docs/runbooks/AWOOOP-RLS-CANARY-WAVE1-3.md` + +**production apply**: +- 已同步到 188 `/home/ollama/awoooi-ops/`。 +- Apply 前 gate: + - runtime access audit:`BLOCKED=0 ALLOW=10`。 + - manual script audit:`BLOCKED=0 REVIEW=5 PASS=13`。 + - `/api/v1/health` → 200 healthy。 + - preflight:`awooop_outbound_message rls=false force=false policies=0`,count `290`,NULL `0`。 + - Run detail smoke:`/runs/d385b7fe-8666-58ec-9072-9ac917adb6cf/detail?project_id=awoooi` → 200,`outbound_messages=1`。 +- 以 188 postgres/operator socket path 執行 Wave1.3 SQL;result:`COMMIT`。 + +**套用後驗證**: +- Run detail smoke 仍為 200,`outbound_messages=1`。 +- `/api/v1/health` → 200 healthy。 +- direct app-role behavior: + - no `app.project_id` → `0` rows。 + - `app.project_id='awoooi'` → `290` rows。 + - `app.project_id='ewoooc'` → `0` rows。 + - rollback-only insert under `awoooi` context + `awoooi` row → allowed。 + - rollback-only insert under `ewoooc` context + `awoooi` row → `InsufficientPrivilegeError`。 + - after probe count → `290`,未留下測試資料。 +- `scripts/ops/awooop-rls-preflight.sh --exact-counts`: + - `PASS=7 WARN=1 BLOCKED=1`。 + - `awooop_outbound_message` → `rls=true force=true policies=1 fail_open=false`。 + - 剩餘 blocker 表:`audit_logs`、`awooop_run_state`、`incidents`、`knowledge_entries`、`playbooks`。 + +**整體進度**: +- Wave 0:MOMO PostgreSQL backup → AwoooP 失敗通知接線完成。 +- Wave 1:GitHub deploy 競爭停用、RLS live 驗證、role bootstrap、API runtime access path、manual script gate、Wave1 空表 canary、Wave1.1 MCP tool registry、Wave1.2 projects、Wave1.3 outbound message 已完成。 +- 尚未完成:token rotation(需外部輪換)、188 certbot 正式修復、剩餘 RLS waves、188 local Ollama 停用窗口。 + +**下一步**: +- 下一個 RLS target 不宜直接套高流量表;先修 `awooop_run_state` 的 Operator Console cross-project list path 或改成明確 tenant filter 必填,再考慮 Run FSM canary。 +- `incidents` / `knowledge_entries` / `playbooks` / `audit_logs` 需另做高流量 query-path 與 rollback rehearsal。 + ## 2026-05-12 | RLS Canary Wave1.2 projects 已套用 **背景**:Wave1.1 完成 `awooop_mcp_tool_registry` 後,剩餘低行數候選是 `awooop_projects`。這張表同時支撐 tenant runtime checks 與 Operator Console 跨租戶 project list,不能直接用單一 tenant policy 熱開。 diff --git a/docs/runbooks/AWOOOP-RLS-CANARY-WAVE1-3.md b/docs/runbooks/AWOOOP-RLS-CANARY-WAVE1-3.md new file mode 100644 index 000000000..f17f847c2 --- /dev/null +++ b/docs/runbooks/AWOOOP-RLS-CANARY-WAVE1-3.md @@ -0,0 +1,108 @@ +# 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. diff --git a/scripts/ops/awooop-rls-canary-wave1-3-outbound-message-rollback.sql b/scripts/ops/awooop-rls-canary-wave1-3-outbound-message-rollback.sql new file mode 100644 index 000000000..724aef415 --- /dev/null +++ b/scripts/ops/awooop-rls-canary-wave1-3-outbound-message-rollback.sql @@ -0,0 +1,14 @@ +-- Rollback for AwoooP RLS Canary Wave 1.3. +-- This only removes the wave1.3 policy and disables RLS on outbound messages. +-- It intentionally does not touch data. + +BEGIN; + +SET LOCAL lock_timeout = '5s'; +SET LOCAL statement_timeout = '30s'; + +DROP POLICY IF EXISTS awooop_outbound_message_tenant ON awooop_outbound_message; +ALTER TABLE awooop_outbound_message NO FORCE ROW LEVEL SECURITY; +ALTER TABLE awooop_outbound_message DISABLE ROW LEVEL SECURITY; + +COMMIT; diff --git a/scripts/ops/awooop-rls-canary-wave1-3-outbound-message.sql b/scripts/ops/awooop-rls-canary-wave1-3-outbound-message.sql new file mode 100644 index 000000000..4a4f254ed --- /dev/null +++ b/scripts/ops/awooop-rls-canary-wave1-3-outbound-message.sql @@ -0,0 +1,69 @@ +-- AwoooP RLS Canary Wave 1.3: outbound message evidence table +-- Date: 2026-05-12 +-- +-- Scope: +-- - awooop_outbound_message +-- +-- Why this table: +-- Latest production evidence before staging: all rows are project_id='awoooi' +-- and all rows have send_status='sent'. Runtime write paths call +-- get_db_context(project_id) before inserting outbound messages. +-- +-- Safety: +-- - fail-closed policy only; no NULL/empty-string app.project_id bypass. +-- - aborts if target is missing project_id, has NULL project_id, or has +-- more rows than the reviewed canary cap. +-- - run with a migration/operator role, not through the production app role. + +BEGIN; + +SET LOCAL lock_timeout = '5s'; +SET LOCAL statement_timeout = '30s'; + +DO $$ +DECLARE + total_rows bigint; + null_project_rows bigint; +BEGIN + IF to_regclass('public.awooop_outbound_message') IS NULL THEN + RAISE EXCEPTION 'RLS canary target table does not exist: awooop_outbound_message'; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'awooop_outbound_message' + AND column_name = 'project_id' + ) THEN + RAISE EXCEPTION 'RLS canary target missing project_id: awooop_outbound_message'; + END IF; + + SELECT COUNT(*), COUNT(*) FILTER (WHERE project_id IS NULL) + INTO total_rows, null_project_rows + FROM awooop_outbound_message; + + IF null_project_rows <> 0 THEN + RAISE EXCEPTION 'RLS canary target has NULL project_id rows: %, nulls=%', + 'awooop_outbound_message', null_project_rows; + END IF; + + IF total_rows > 1000 THEN + RAISE EXCEPTION 'RLS canary wave1.3 reviewed cap exceeded: %, rows=%', + 'awooop_outbound_message', total_rows; + END IF; +END +$$; + +ALTER TABLE awooop_outbound_message ENABLE ROW LEVEL SECURITY; +ALTER TABLE awooop_outbound_message FORCE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS outbound_msg_tenant_isolation ON awooop_outbound_message; +DROP POLICY IF EXISTS awooop_outbound_message_tenant ON awooop_outbound_message; + +CREATE POLICY awooop_outbound_message_tenant ON awooop_outbound_message + FOR ALL TO awooop_app + USING (project_id = current_setting('app.project_id', TRUE)) + WITH CHECK (project_id = current_setting('app.project_id', TRUE)); + +COMMIT;