KB-4820 rev 2

G8B-RP — Directus Roles/Policies/Permissions Execution Prompt v0.2 FINAL

12 min read Revision 2

G8B-RP — Directus TAC Roles/Policies/Permissions Execution Prompt v0.2

Status: ✅ AUTHORIZED — GPT PASS, User GO 2026-04-28 Phiên: S186 | Ngày: 2026-04-28 Scope: G8B-RP ONLY — roles + policies + access bindings + permissions. Token provisioning DEFERRED (gate riêng trước G11). Executor: Claude Code via SSH contabo — tất cả lệnh trên VPS Effort: medium Canonical design: P9-G8A-directus-roles-readiness-design.md v0.3 Directus: 11.5.1 — Policies model (Role → Access → Policy → Permissions) Revision: v0.1 → v0.2 (GPT R1, 4 patches) + API discovery patch


✅ AUTHORIZED — User GO 2026-04-28. Agent chạy ngay, không cần hỏi lại.

⚠️ Gate này là G8B-RP (Roles/Policies/Permissions). KHÔNG claim full G8B hay P9 G8 PASS. Token provisioning = gate riêng. (GPT R1#2)


0. Mục tiêu

Tạo 2 roles + 2 policies + 2 access bindings + 84 permission rows cho 14 TAC collections theo G8A design v0.3.

PASS: 2 roles + 2 policies + 2 access + 84 permissions (28 agent + 56 admin) đúng full matrix. Gate A/B/C unchanged.

FAIL: Missing/wrong/extra objects, matrix mismatch, Gate A/B/C drift → §5.

Expected objects

Object Count
Roles 2 (tac-agent, tac-admin)
Policies 2 (tac-agent-policy, tac-admin-policy)
Access bindings 2
Permission rows (agent) 28
Permission rows (admin) 56
Total permissions 84

1. Pre-checks

1a. VPS identity

ssh contabo
hostname  # Expected: vmi3080463

1b. Gate A/B/C intact

# Gate A
docker exec postgres psql -U directus -d directus -t -c \
  "SELECT count(*) FROM pg_tables WHERE schemaname='public' AND tablename LIKE 'tac_%'"
# Expected: 14
docker exec postgres psql -U directus -d directus -t -c \
  "SELECT count(*) FROM pg_proc JOIN pg_namespace n ON pronamespace=n.oid WHERE nspname='public' AND proname LIKE 'fn_tac_%'"
# Expected: 7
docker exec postgres psql -U directus -d directus -t -c \
  "SELECT count(*) FROM pg_trigger t JOIN pg_class c ON t.tgrelid=c.oid JOIN pg_namespace n ON c.relnamespace=n.oid WHERE n.nspname='public' AND t.tgname LIKE 'trg_tac_%'"
# Expected: 6

# Token
ADMIN_TOKEN=$(docker exec directus printenv DIRECTUS_ADMIN_TOKEN 2>/dev/null || echo "")
if [ -z "$ADMIN_TOKEN" ]; then
  ADMIN_TOKEN=$(docker exec directus printenv ADMIN_TOKEN 2>/dev/null || echo "")
fi
if [ -z "$ADMIN_TOKEN" ]; then
  ADMIN_TOKEN=$(grep -oP 'DIRECTUS_ADMIN_TOKEN=\K.*' /opt/incomex/docker/.env 2>/dev/null || echo "")
fi
if [ -z "$ADMIN_TOKEN" ]; then echo "TOKEN UNAVAILABLE — STOP"; exit 1; fi
echo "TOKEN=****${ADMIN_TOKEN: -4}"

# Gate B
COLL_COUNT=$(curl -s http://localhost:8055/collections \
  -H "Authorization: Bearer $ADMIN_TOKEN" | \
  jq '[.data[] | select(.collection | startswith("tac_"))] | length')
# Expected: 14

# Gate C
docker exec postgres psql -U directus -d directus -t -c \
  "SELECT sum((xpath('/row/cnt/text()', query_to_xml('SELECT count(*) AS cnt FROM public.' || quote_ident(tablename), false, true, '')))[1]::text::int) FROM pg_tables WHERE schemaname='public' AND tablename LIKE 'tac_%'"
# Expected: 61

1c. Snapshot existing TAC objects + classify (GPT R1#3)

# Roles
curl -s http://localhost:8055/roles \
  -H "Authorization: Bearer $ADMIN_TOKEN" | \
  jq '[.data[] | select(.name | test("tac-")) | {id, name}]'

# Policies
curl -s http://localhost:8055/policies \
  -H "Authorization: Bearer $ADMIN_TOKEN" | \
  jq '[.data[] | select(.name | test("tac-")) | {id, name, admin_access, app_access}]'

# Access bindings (all — filter later)
curl -s http://localhost:8055/access \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq '.data | length' 

# Permissions with tac_* collections
curl -s "http://localhost:8055/permissions?limit=-1" \
  -H "Authorization: Bearer $ADMIN_TOKEN" | \
  jq '[.data[] | select(.collection | startswith("tac_"))] | length'

Classify:

  • 0 tac- roles/policies/permissions → Clean slate. Proceed.
  • Exact match target (same names, correct flags) → Skip creation, treat as satisfied.
  • Partial or mismatchSTOP, report exact state.
  • Unknown tac- objects* → STOP.
  • KHÔNG blanket delete.

1d. Confirm D11 model

docker exec postgres psql -U directus -d directus -t -c \
  "SELECT column_name FROM information_schema.columns WHERE table_name='directus_permissions' AND column_name='policy'"
# Expected: "policy"

Không có → STOP.

1e. API Discovery — verify endpoints + payload shape (RÚT KINH NGHIỆM)

G8A probe chỉ kiểm PG catalog, KHÔNG kiểm REST API. Trước khi POST, agent PHẢI verify:

# Verify /policies endpoint exists
HTTP_POL=$(curl -s -o /dev/null -w "%{http_code}" \
  http://localhost:8055/policies -H "Authorization: Bearer $ADMIN_TOKEN")
echo "GET_POLICIES=$HTTP_POL"
# Expected: 200 (not 404/403)

# Verify /access endpoint exists
HTTP_ACC=$(curl -s -o /dev/null -w "%{http_code}" \
  http://localhost:8055/access -H "Authorization: Bearer $ADMIN_TOKEN")
echo "GET_ACCESS=$HTTP_ACC"
# Expected: 200

# Verify /permissions endpoint exists + has policy field in data
curl -s "http://localhost:8055/permissions?limit=1" \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq '.data[0] | keys'
# Expected: includes "policy", "collection", "action"

# Reference pattern: inspect existing AI Agent role + its permissions
# to discover actual payload format used in this Directus instance
AI_AGENT_ROLE=$(curl -s http://localhost:8055/roles \
  -H "Authorization: Bearer $ADMIN_TOKEN" | \
  jq -r '.data[] | select(.name == "AI Agent") | .id')
if [ -n "$AI_AGENT_ROLE" ]; then
  echo "AI_AGENT_ROLE_ID=$AI_AGENT_ROLE"
  # Get access bindings for this role
  curl -s "http://localhost:8055/access?filter[role][_eq]=$AI_AGENT_ROLE" \
    -H "Authorization: Bearer $ADMIN_TOKEN" | jq '.data'
  # Get one permission row shape
  curl -s "http://localhost:8055/permissions?limit=1" \
    -H "Authorization: Bearer $ADMIN_TOKEN" | jq '.data[0]'
fi

Nếu /policies trả 404: Directus version có thể dùng API path khác. STOP, report endpoints available.

Nếu permission row không có policy field: Legacy model, prompt không đúng. STOP.

Agent PHẢI adapt payload format theo actual API shape, KHÔNG hardcode assume.


2. Execution (D11: Role → Access → Policy → Permissions)

All API calls dùng safety wrapper:

set -euo pipefail
HTTP_CODE=$(curl -s -o /tmp/g8b_response.json -w "%{http_code}" \
  -X POST http://localhost:8055/<endpoint> \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$PAYLOAD")
RESPONSE=$(cat /tmp/g8b_response.json)
echo "HTTP=$HTTP_CODE"
if ! echo "$RESPONSE" | jq . >/dev/null 2>&1; then echo "INVALID JSON — STOP"; exit 1; fi

Step 1 — Roles (skip if exact match exists)

// POST /roles — tac-agent
{"name":"tac-agent","icon":"smart_toy","description":"TAC daily operations (API-only)"}
// POST /roles — tac-admin
{"name":"tac-admin","icon":"admin_panel_settings","description":"TAC bootstrap + emergency"}

Capture: TAC_AGENT_ROLE_ID, TAC_ADMIN_ROLE_ID.

Step 2 — Policies (skip if exact match exists)

// POST /policies — tac-agent-policy
{"name":"tac-agent-policy","icon":"policy","description":"CRU core, CRUD members, read vocab/config","admin_access":false,"app_access":false}
// POST /policies — tac-admin-policy
{"name":"tac-admin-policy","icon":"shield","description":"Full CRUD all 14 tac_*","admin_access":false,"app_access":false}

Capture: TAC_AGENT_POLICY_ID, TAC_ADMIN_POLICY_ID.

Step 3 — Access Bindings (skip if exists)

// POST /access
{"role":"<TAC_AGENT_ROLE_ID>","policy":"<TAC_AGENT_POLICY_ID>"}
{"role":"<TAC_ADMIN_ROLE_ID>","policy":"<TAC_ADMIN_POLICY_ID>"}

Step 4 — Permissions (84 rows)

tac-agent-policy (28 rows per G8A §4.1):

Collections (4 core) Actions Rows
tac_logical_unit, tac_unit_version, tac_publication, tac_change_set create, read, update 4×3=12
tac_publication_member, tac_change_set_member create, read, update, delete 2×4=8
8 vocab/config tables read 8×1=8
Subtotal 28

tac-admin-policy (56 rows per G8A §4.2):

Collections Actions Rows
All 14 tac_* create, read, update, delete 14×4=56

Per permission:

{"policy":"<POLICY_ID>","collection":"<name>","action":"<create|read|update|delete>"}

Agent PHẢI adapt payload format theo §1e discovery. Nếu actual shape khác, dùng actual shape.

Step 5 — Token: DEFERRED

Log "Token provisioning deferred — separate pre-G11 gate."


3. Post-verification — Full Matrix (GPT R1#4)

3a. Roles + policies + access

curl -s http://localhost:8055/roles -H "Authorization: Bearer $ADMIN_TOKEN" | \
  jq '[.data[] | select(.name | test("tac-")) | .name] | sort'
# Expected: ["tac-admin","tac-agent"]

curl -s http://localhost:8055/policies -H "Authorization: Bearer $ADMIN_TOKEN" | \
  jq '.data[] | select(.name | test("tac-")) | {name, admin_access, app_access}'
# Expected: both false/false

# Access bindings count for TAC roles
# (verify 2 bindings matching role→policy)

3b. Full matrix comparison — KHÔNG chỉ count

Agent build deterministic expected tuple set (84 tuples):

(tac-agent-policy, tac_logical_unit, create)
(tac-agent-policy, tac_logical_unit, read)
(tac-agent-policy, tac_logical_unit, update)
... [28 tuples total for agent-policy]
(tac-admin-policy, tac_logical_unit, create)
(tac-admin-policy, tac_logical_unit, read)
(tac-admin-policy, tac_logical_unit, update)
(tac-admin-policy, tac_logical_unit, delete)
... [56 tuples total for admin-policy]

Query actual:

curl -s "http://localhost:8055/permissions?limit=-1" \
  -H "Authorization: Bearer $ADMIN_TOKEN" | \
  jq '[.data[] | select(.collection | startswith("tac_")) | {policy, collection, action}]'

PASS only if:

  • expected - actual = 0 (no missing)
  • actual - expected = 0 (no extra)

Count + spot-check = secondary evidence.

3c. Gate A/B/C unchanged

Tables=14, fn=7, trg=6. Collections=14. Seed=61.

ALL 3a–3c PASS → G8B-RP PASS.


4. Hard Exclusions

# Cấm
1 Không DDL
2 Không seed/data mutation trong tac_*
3 Không Directus collection metadata changes
4 Không registry/birth/catalog/DOT writes
5 Không G11
6 Không corpus migration
7 Không broad admin grants beyond G8A matrix
8 Không blanket delete roles/policies/permissions
9 VPS only qua SSH contabo
10 Token provisioning DEFERRED

5. Failure handling

Clean slate → creation fail: STOP. Cleanup only objects created by this run. Partial existing → mismatch: STOP. Report exact state. Không blanket delete. Permission matrix incomplete: Report split (created vs missing). Chờ GPT/User. D11 model mismatch: STOP immediately. API endpoint 404: STOP. Report available endpoints. Directus version may differ.


6. Action Log

Path: knowledge/dev/laws/dieu38-trien-khai/reports/p9-g8b-directus-roles-permissions-log-YYYY-MM-DD.md

No-overwrite. Secret hygiene — mask tokens.

Nội dung: pre-checks + snapshot, API discovery results (§1e), role/policy/access IDs, permission full matrix comparison (expected vs actual), token deferral note, Gate A/B/C unchanged, G8B-RP PASS/FAIL.


7. Sau PASS

STOP. G8B-RP PASS ≠ full G8B PASS. Token provisioning = gate riêng.

Upload action log → GPT review → token gate hoặc G11.


G8B-RP v0.2 FINAL | S186 | 2026-04-28 | GPT PASS + API discovery patch ✅ AUTHORIZED — execute immediately

Back to Knowledge Hub knowledge/dev/laws/dieu38-trien-khai/P9-G8B-RP-directus-roles-permissions-execution-prompt-v0-2.md