G8B-RP — Directus Roles/Policies/Permissions Execution Prompt v0.2 FINAL
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.mdv0.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 mismatch → STOP, 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