KB-5E65 rev 4

23-P3D4C2U Option D — Base Table + Field Allowlist Implementation Prompt (rev4 CONSOLIDATED)

12 min read Revision 4
p3d4c2upromptimplementationoption-dbase-tablefield-allowlistrev4consolidated

23-P3D4C2U Option D — Base Table + Field Allowlist Implementation Prompt (rev4 CONSOLIDATED)

Date: 2026-05-08 Status: PROMPT rev4 — chờ GPT/User final review. CHƯA dispatch. Report: knowledge/dev/laws/dieu44-trien-khai/reports/23-p3d4c2u-option-d-base-table-field-allowlist-implementation-report.md Scope: Expose event_outbox base table via Directus field allowlist + table_registry. NO view. NO Nuxt. Self-contained: prompt này chứa đầy đủ mọi bước, không tham chiếu rev trước.


Hard Boundaries

  • ❌ KHÔNG PG view / materialized view
  • ❌ KHÔNG worker / cron / refresh
  • ❌ KHÔNG new table / write path
  • ❌ KHÔNG custom Directus extension
  • ❌ KHÔNG Nuxt code / bespoke notification UI
  • ❌ KHÔNG Directus click-config (API only)
  • ❌ KHÔNG Directus write / mark-read
  • ❌ KHÔNG expose unsafe fields (safe_payload, correlation_id, causation_id, payload, body, raw_payload, vector, embedding, secret, token, password, ssn, personal_data)
  • ❌ KHÔNG touch IU runtime / Đ43 machinery
  • ❌ KHÔNG tạo PG role hoặc Directus role mới
  • ✅ Directus permission + field metadata (API)
  • ✅ table_registry row (convention channel)
  • ✅ Report MUST be created even on FAIL/BLOCKED

Step 0: Verify Clean State + Full Inventory

0A. View rollback residual = 0

GET /collections/v_event_outbox_table → expect 404
SELECT viewname FROM pg_views WHERE viewname = 'v_event_outbox_table'; → expect 0 rows
SELECT * FROM table_registry WHERE table_id = 'tbl_event_outbox'; → expect 0 rows

0B. event_outbox is base table with real PK

SELECT relkind FROM pg_class WHERE relname = 'event_outbox'; → expect 'r' (table)
SELECT conname FROM pg_constraint WHERE conrelid = 'event_outbox'::regclass AND contype = 'p'; → PK exists
SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'event_outbox' ORDER BY ordinal_position;
SELECT count(*) FROM event_outbox;

Report: live_columns=<list>, event_outbox_row_count=<N>.

0C. Directus sees event_outbox

GET /collections/event_outbox → expect collection info

0D. ALL permissions on event_outbox — CLASSIFIED by scope

GET /permissions?filter[collection][_eq]=event_outbox

Classify each:

Scope Description Unsafe field leak = ?
TABLE_MODULE_USER_FACING Role used by DirectusTable STOP
PUBLIC Public Access policy STOP
INTERNAL_ADMIN Administrator Report only — UNLESS Table Module uses admin token (then STOP)
AGENT_INTERNAL AI agent role Report only
UNKNOWN Cannot classify STOP

Report: permission_audit_by_scope=<classified list>.

0E. DirectusTable fetch path PROOF (CRITICAL GATE)

Agent must provide code-level evidence:

# Find DirectusTable component
grep -rn "DirectusTable\|directus-table" web/
# Check fields selector usage
grep -n "fields" <found file>
# Check auth method
grep -n "readItems\|useDirectus\|fetch\|token" <found file>

Report:

directus_table_file=<path>
directus_table_uses_fields_selector=YES_FROM_REGISTRY|YES_HARDCODED|NO|UNKNOWN
directus_table_field_source=<actual table_registry column name>
directus_table_auth=PUBLIC_ROLE|USER_ROLE|ADMIN_TOKEN|UNKNOWN

If fields_selector = NO or UNKNOWN → STOP NEEDS_TABLE_MODULE_FIELD_FILTER. If auth = ADMIN_TOKEN AND admin permission leaks unsafe fields → STOP.

0F. PG role convention

SELECT grantee, privilege_type FROM information_schema.role_table_grants
WHERE table_name = 'system_issues' AND privilege_type = 'SELECT';

Report: pg_read_role=<name>.

0G. Directus role convention

GET /permissions?filter[collection][_eq]=system_issues&filter[action][_eq]=read

Report: directus_read_role_id=<uuid>, directus_read_role_name=<name>.

0H. table_registry write channel

SELECT id, collection, status FROM table_registry ORDER BY id;

Report: write_channel=DIRECTUS_API|RAW_SQL.

0I. table_registry LIVE schema (DO NOT guess columns)

SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'table_registry' ORDER BY ordinal_position;

Report: fields_config_column=<actual name>.

0J. system_issues row (precedent shape)

SELECT * FROM table_registry WHERE collection = 'system_issues';

0K. Duplicate check

SELECT * FROM table_registry WHERE table_id = 'tbl_event_outbox' OR collection = 'event_outbox';

Exists compatible → skip. Conflict → STOP.

0L. IU + event core snapshot

SELECT count(*) FROM iu_notification_event;
SELECT count(*) FROM iu_notification_read;
SELECT count(*) FROM event_outbox;
SELECT count(*) FROM event_type_registry WHERE active = true;

Step 1: Directus Permission — Field Allowlist

Only include fields that EXIST in live schema (from 0B).

Approved metadata fields (verify each exists in live_columns):

id, occurred_at, created_at, event_domain, event_type, event_stream,
delivery_lane, event_severity, event_subject_table, event_subject_ref,
canonical_address, actor_ref, source_system, payload_classification

DENY list (must NOT appear in permission fields):

safe_payload, correlation_id, causation_id,
payload, body, raw_payload, vector, embedding,
secret, token, password, ssn, personal_data

Idempotent check:

GET /permissions?filter[collection][_eq]=event_outbox&filter[action][_eq]=read&filter[role][_eq]=<directus_read_role_id>
  • Not found → create (POST /permissions)
  • Found + compatible (same fields or narrower) → keep. If narrower: adjust registry fields to match permission, OR widen permission (only metadata fields) and report permission_updated_by_pack=P3D4C2U_D
  • Found + broader (includes unsafe fields) → STOP

NO create/update/delete permissions. Track: permission_created_by_pack=P3D4C2U_D.

Report: permission_allowed_fields=<list>.


Step 2: Directus Field Labels (API, idempotent)

For each allowed field in event_outbox:

  1. GET /fields/event_outbox/<field> — verify exists
  2. If exists → PATCH label (Vietnamese). Preserve other settings.
  3. Do NOT touch fields outside event_outbox.

Labels: Thời điểm, Tạo lúc, Phạm vi, Loại sự kiện, Luồng, Kênh, Mức độ, Bảng nguồn, Đối tượng, Địa chỉ, Tác nhân, Nguồn, Phân loại payload, ID.

Track: field_metadata_changed_by_pack=P3D4C2U_D.


Step 3: table_registry Row

Idempotent: Check 0K first.

Use ACTUAL column names from 0I. Copy shape from 0J (system_issues precedent). Use write channel from 0H.

collection = 'event_outbox'
table_id = 'tbl_event_outbox'
status = 'draft'
description = 'Bảng thông báo sự kiện hệ thống — metadata only, read-only'
default_sort = '-occurred_at'
fields = <only fields matching permission allowlist>

Field list in registry MUST match or be subset of permission allowlist. Report: permission_registry_fieldset_match=PASS|NARROWER_PERMISSION|FAIL.

Track: registry_row_created_by_pack=P3D4C2U_D.


Step 4: Smoke Tests (INTENDED ROLE — not admin)

Admin token only for setup/inventory. All read smoke uses intended Directus role.

# Test Expected
S1 GET /items/system_issues?limit=1 (intended role) Data returned
S2 GET /items/event_outbox?limit=5&fields=id,event_domain,event_type,occurred_at (intended role) Data or empty array (not 403)
S3a GET /items/event_outbox?fields=safe_payload (intended role) 403 / field omitted / empty — NOT value
S3b GET /items/event_outbox?fields=correlation_id (intended role) Same
S3c GET /items/event_outbox?fields=causation_id (intended role) Same
S4 POST /items/event_outbox (intended role) 403
S5 table_registry row 1 row, status=draft
S6 Full response GET /items/event_outbox?limit=1 → list all keys No unsafe key
S7 IU + event core counts Match 0L
S8 Route /knowledge/registries/system_issue (if tool can verify) Renders
S9 Route /knowledge/registries/event_outbox (if tool can verify, after draft row) Renders

Empty table handling for S3a-S3c + S6:

If event_outbox_row_count = 0:

  • Response [] does NOT prove field blocking
  • MUST ALSO verify permission metadata:
    GET /permissions?filter[collection][_eq]=event_outbox&filter[action][_eq]=read→ confirm fields list does NOT include safe_payload/correlation_id/causation_id
    
  • Report: unsafe_field_denial_method=PERMISSION_METADATA_VERIFIED_WITH_EMPTY_DATA

If event_outbox_row_count > 0:

  • Response with data: unsafe key present = FAIL, absent = PASS
  • Report: unsafe_field_denial_method=FIELD_OMITTED_IN_NONEMPTY_RESPONSE

Step 5: Publish Decision

  • ALL S1-S9 PASS → UPDATE status='published'
  • S1-S7 PASS + S8/S9 CANNOT_VERIFY → status=draft, DEFERRED_MANUAL_ROUTE_SMOKE
  • Any S1-S7 FAIL → rollback

Report:

published=YES|NO
publish_block_reason=NONE|ROUTE_CANNOT_VERIFY|SMOKE_FAIL|PERMISSION_LEAK|FETCH_PATH_UNSAFE

Step 6: Rollback (scoped to P3D4C2U_D only)

Only remove objects created/updated by THIS pack:

permission_created_by_pack=P3D4C2U_D → DELETE /permissions/<id>
permission_updated_by_pack=P3D4C2U_D → revert to original fields (if tracked)
registry_row_created_by_pack=P3D4C2U_D → DELETE row
field_metadata_changed_by_pack=P3D4C2U_D → revert labels or leave (non-destructive)

DO NOT: drop event_outbox, touch IU, touch event core data, remove pre-existing permissions.


Verification

residual_from_view_path=0
event_outbox_is_base_table=PASS
event_outbox_has_pk=PASS
live_columns=<list>
event_outbox_row_count=<N>
directus_sees_event_outbox=PASS
permission_audit_by_scope=<classified list>
directus_table_file=<path>
directus_table_uses_fields_selector=YES_FROM_REGISTRY|NO|UNKNOWN
directus_table_field_source=<column name>
directus_table_auth=<role/token type>
fetch_path_safe=PASS|STOP
pg_read_role=<name>
directus_read_role=<uuid>/<name>
write_channel=DIRECTUS_API|RAW_SQL
fields_config_column=<actual>
permission_status=CREATED|ALREADY_COMPATIBLE|NARROWER|BROADER_STOP
permission_allowed_fields=<list>
permission_created_by_pack=P3D4C2U_D|SKIPPED
permission_updated_by_pack=P3D4C2U_D|NO
table_registry_fields=<list>
unsafe_fields_absent_from_permission=PASS|FAIL
unsafe_fields_absent_from_registry=PASS|FAIL
permission_registry_fieldset_match=PASS|NARROWER_PERMISSION|FAIL
registry_status=CREATED|ALREADY_COMPATIBLE|CONFLICT
registry_row_created_by_pack=P3D4C2U_D|SKIPPED
registry_initial_status=draft
field_labels_set=PASS|FAIL
smoke_S1=PASS|FAIL
smoke_S2=PASS|FAIL
smoke_S3a_safe_payload=BLOCKED|FAIL
smoke_S3b_correlation_id=BLOCKED|FAIL
smoke_S3c_causation_id=BLOCKED|FAIL
smoke_S4_write_denied=PASS|FAIL
smoke_S5_registry_row=PASS|FAIL
smoke_S6_no_unsafe_keys=PASS|FAIL
smoke_S7_counts_match=PASS|FAIL
smoke_S8_system_issues_route=PASS|CANNOT_VERIFY
smoke_S9_event_outbox_route=PASS|CANNOT_VERIFY
unsafe_field_denial_method=HTTP_FORBIDDEN|FIELD_OMITTED_IN_NONEMPTY|PERMISSION_METADATA_VERIFIED_WITH_EMPTY_DATA|FAIL
published=YES|NO
publish_block_reason=NONE|ROUTE_CANNOT_VERIFY|SMOKE_FAIL|PERMISSION_LEAK|FETCH_PATH_UNSAFE
iu_post_match=PASS
event_core_post_match=PASS
no_view_created=true
no_nuxt_code=true
no_bespoke_ui=true
no_new_role=true
phase_status=PASS|FAIL|BLOCKED
recommendation=READONLY_EXPOSURE_PUBLISHED|READONLY_EXPOSURE_DRAFT_PENDING_SMOKE|NEEDS_TABLE_MODULE_FIELD_FILTER|EXISTING_PERMISSION_LEAK|BLOCKED
next_required_pack=(conditional — see below)

Next pack conditional:

  • PUBLISHED → P3D4C3U_USER_VIEW_SMOKE_AND_MARK_READ_DECISION
  • DRAFT pending route smoke → P3D4C2U_MANUAL_ROUTE_SMOKE
  • Field filter missing → TABLE_MODULE_FIELD_FILTER_EXTENSION_PROMPT_REVIEW
  • Permission leak → FIELD_PERMISSION_REPAIR_PROMPT_REVIEW

P3D4C2U Option D rev4 CONSOLIDATED | Self-contained | Base table + field allowlist | CHƯA dispatch | Chờ GPT/User final review

Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/prompts/23-p3d4c2u-option-d-base-table-field-allowlist-implementation-prompt.md