G8B-Token — TAC Token Provisioning Gate v0.4 FINAL
G8B-Token — TAC Token Provisioning Gate v0.4
Status: ✅ AUTHORIZED — GPT PASS, User GO 2026-04-29 Phiên: S186 | Ngày: 2026-04-29 Scope: G8B-Token ONLY — Directus users + static tokens cho tac-agent/tac-admin Executor: Claude Code via SSH
contabo— tất cả lệnh trên VPS Effort: medium Prerequisite: G8B-RP PASS (re-verified 2026-04-29) Revision: v0.1 → v0.2 (R1) → v0.3 (R2) → v0.4 (R3, 5 fixes) Registry ref:knowledge/other/specs/ai-agent-registry.md
✅ AUTHORIZED — User GO 2026-04-29. Agent chạy ngay, không cần hỏi lại.
⚠️ Gate này KHÔNG gửi POST/DELETE/PATCH/PUT vào /items/tac_*. Auth test = READ-ONLY only.
0. Mục tiêu
Tạo 2 Directus users, gán vào tac-agent/tac-admin roles, provision static tokens, store trong GSM.
PASS: 2 users + 2 tokens in GSM + read-only auth test PASS + Gate A/B/C unchanged.
FAIL: GSM unavailable, creation fail, auth fail → §5.
Canonical names
| Item | Value |
|---|---|
| GSM project | github-chatgpt-ggcloud |
| Agent secret | DIRECTUS_TAC_AGENT_TOKEN |
| Admin secret | DIRECTUS_TAC_ADMIN_TOKEN |
| Agent email | tac-agent@incomex.local |
| Admin email | tac-admin@incomex.local |
| Token length | openssl rand -hex 16 (32 chars) |
1. Pre-checks
1a. VPS + admin token
ssh contabo
hostname # Expected: vmi3080463
ADMIN_TOKEN=$(docker exec incomex-directus printenv DIRECTUS_ADMIN_TOKEN 2>/dev/null || echo "")
if [ -z "$ADMIN_TOKEN" ]; then
ADMIN_TOKEN=$(docker exec incomex-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 "ADMIN_TOKEN=****${ADMIN_TOKEN: -4}"
1b. GSM preflight — deterministic
GCP_PROJECT="github-chatgpt-ggcloud"
# Step 1: gcloud available
which gcloud || { echo "GCLOUD NOT FOUND — STOP"; exit 1; }
# Step 2: active auth
GSM_ACCOUNT=$(gcloud auth list --filter=status:ACTIVE --format="value(account)" 2>/dev/null)
if [ -z "$GSM_ACCOUNT" ]; then echo "GSM AUTH INACTIVE — STOP"; exit 1; fi
echo "GSM_ACCOUNT=$GSM_ACCOUNT"
# Step 3: project accessible
gcloud projects describe "$GCP_PROJECT" --format="value(projectId)" >/dev/null 2>&1
if [ $? -ne 0 ]; then echo "GSM PROJECT INACCESSIBLE — STOP"; exit 1; fi
echo "GSM_PROJECT=$GCP_PROJECT"
# Step 4: check target secrets exist or not
for SECRET_NAME in DIRECTUS_TAC_AGENT_TOKEN DIRECTUS_TAC_ADMIN_TOKEN; do
EXISTS=$(gcloud secrets describe "$SECRET_NAME" --project="$GCP_PROJECT" 2>/dev/null && echo "EXISTS" || echo "ABSENT")
echo "SECRET_${SECRET_NAME}=$EXISTS"
done
GSM preflight FAIL → STOP. Không tạo user/token.
1c. Classify pre-existing secrets + users
Secrets:
- Both absent → Clean slate. Will create + add first version.
- Exist, but TAC users absent → Allowed for this gate. Will add new version.
- Exist AND TAC users exist → STOP. Possible prior run. Không overwrite/rotation without explicit authorization.
Users:
curl -s "http://localhost:8055/users?filter[email][_in]=tac-agent@incomex.local,tac-admin@incomex.local" \
-H "Authorization: Bearer $ADMIN_TOKEN" | \
jq '[.data[] | {id, email, role, status}]'
- 0 users → Clean slate. Proceed.
- Exact match (correct email + correct role + active) → STOP unless explicit rotation authorized.
- Partial/mismatch → STOP.
1d. G8B-RP intact
curl -s http://localhost:8055/roles -H "Authorization: Bearer $ADMIN_TOKEN" | \
jq '[.data[] | select(.name | test("tac-")) | .name] | sort'
# Expected: ["tac-admin","tac-agent"]
TAC_AGENT_ROLE_ID=$(curl -s http://localhost:8055/roles -H "Authorization: Bearer $ADMIN_TOKEN" | \
jq -r '.data[] | select(.name=="tac-agent") | .id')
TAC_ADMIN_ROLE_ID=$(curl -s http://localhost:8055/roles -H "Authorization: Bearer $ADMIN_TOKEN" | \
jq -r '.data[] | select(.name=="tac-admin") | .id')
PERM_COUNT=$(curl -s "http://localhost:8055/permissions?limit=-1" \
-H "Authorization: Bearer $ADMIN_TOKEN" | \
jq '[.data[] | select(.collection | startswith("tac_"))] | length')
echo "TAC_PERMISSIONS=$PERM_COUNT"
# Expected: 84
1e. Gate A/B/C unchanged
# Gate A: tables=14, fn=7, trg=6 | Gate B: 14 | Gate C: 61
2. Execution — GSM-first, create-then-PATCH
Flow: GSM proven → generate → store GSM → verify read-back → create users WITHOUT token → PATCH token → auth test from GSM.
Step 1 — Generate tokens
TAC_AGENT_TOKEN=$(openssl rand -hex 16)
TAC_ADMIN_TOKEN=$(openssl rand -hex 16)
echo "TAC_AGENT_TOKEN=****${TAC_AGENT_TOKEN: -4}"
echo "TAC_ADMIN_TOKEN=****${TAC_ADMIN_TOKEN: -4}"
Step 2 — Store in GSM FIRST (before Directus)
GCP_PROJECT="github-chatgpt-ggcloud"
GSM_VERSION_IDS=()
for SECRET_NAME in DIRECTUS_TAC_AGENT_TOKEN DIRECTUS_TAC_ADMIN_TOKEN; do
# Determine token value
if [ "$SECRET_NAME" = "DIRECTUS_TAC_AGENT_TOKEN" ]; then
TOKEN_VAL="$TAC_AGENT_TOKEN"
else
TOKEN_VAL="$TAC_ADMIN_TOKEN"
fi
# Create secret if absent
gcloud secrets describe "$SECRET_NAME" --project="$GCP_PROJECT" >/dev/null 2>&1 || \
gcloud secrets create "$SECRET_NAME" --replication-policy=automatic --project="$GCP_PROJECT"
# Add version
VERSION_OUTPUT=$(echo -n "$TOKEN_VAL" | gcloud secrets versions add "$SECRET_NAME" \
--project="$GCP_PROJECT" --data-file=- --format="value(name)" 2>&1)
GSM_EXIT=$?
if [ "$GSM_EXIT" -ne 0 ]; then
echo "GSM ADD VERSION FAIL for $SECRET_NAME — STOP"
echo "No Directus users created yet."
exit 1
fi
GSM_VERSION_IDS+=("$SECRET_NAME:$VERSION_OUTPUT")
echo "GSM_VERSION=$SECRET_NAME:$(echo "$VERSION_OUTPUT" | grep -oP '[0-9]+$')"
# Read-back verify
READ_BACK=$(gcloud secrets versions access latest --secret="$SECRET_NAME" --project="$GCP_PROJECT")
if [ "$READ_BACK" != "$TOKEN_VAL" ]; then
echo "GSM READ-BACK MISMATCH for $SECRET_NAME — STOP"
exit 1
fi
done
echo "GSM_STORE_VERIFIED=PASS"
Step 3 — Create Directus users WITHOUT token
CREATED_USER_IDS=()
# tac-agent-user (no token in payload)
TMPFILE=$(mktemp); chmod 600 "$TMPFILE"
cat > "$TMPFILE" <<PAYLOAD
{
"email": "tac-agent@incomex.local",
"password": "$(openssl rand -hex 16)",
"role": "$TAC_AGENT_ROLE_ID",
"status": "active"
}
PAYLOAD
HTTP_CODE=$(curl -s -o /tmp/g8b_resp.json -w "%{http_code}" \
-X POST http://localhost:8055/users \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d @"$TMPFILE")
rm -f "$TMPFILE"
echo "CREATE_AGENT=$HTTP_CODE"
if echo "$HTTP_CODE" | grep -q '^2'; then
AGENT_USER_ID=$(jq -r '.data.id' /tmp/g8b_resp.json)
CREATED_USER_IDS+=("$AGENT_USER_ID")
echo "AGENT_USER_ID=$AGENT_USER_ID"
else
echo "AGENT CREATE FAIL — STOP. GSM versions exist but no Directus users."
echo "ORPHAN_GSM_VERSIONS: ${GSM_VERSION_IDS[*]}"
rm -f /tmp/g8b_resp.json
exit 1
fi
# tac-admin-user (no token in payload)
TMPFILE=$(mktemp); chmod 600 "$TMPFILE"
cat > "$TMPFILE" <<PAYLOAD
{
"email": "tac-admin@incomex.local",
"password": "$(openssl rand -hex 16)",
"role": "$TAC_ADMIN_ROLE_ID",
"status": "active"
}
PAYLOAD
HTTP_CODE=$(curl -s -o /tmp/g8b_resp.json -w "%{http_code}" \
-X POST http://localhost:8055/users \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d @"$TMPFILE")
rm -f "$TMPFILE"
echo "CREATE_ADMIN=$HTTP_CODE"
if echo "$HTTP_CODE" | grep -q '^2'; then
ADMIN_USER_ID=$(jq -r '.data.id' /tmp/g8b_resp.json)
CREATED_USER_IDS+=("$ADMIN_USER_ID")
echo "ADMIN_USER_ID=$ADMIN_USER_ID"
else
echo "ADMIN CREATE FAIL — cleanup agent user"
curl -s -X DELETE "http://localhost:8055/users/$AGENT_USER_ID" \
-H "Authorization: Bearer $ADMIN_TOKEN" > /dev/null
echo "CLEANED_AGENT=$AGENT_USER_ID"
echo "ORPHAN_GSM_VERSIONS: ${GSM_VERSION_IDS[*]}"
rm -f /tmp/g8b_resp.json
exit 1
fi
rm -f /tmp/g8b_resp.json
Step 4 — PATCH tokens onto users (registry-proven pattern)
# PATCH agent token
TMPFILE=$(mktemp); chmod 600 "$TMPFILE"
echo "{\"token\":\"$TAC_AGENT_TOKEN\"}" > "$TMPFILE"
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-X PATCH "http://localhost:8055/users/$AGENT_USER_ID" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d @"$TMPFILE")
rm -f "$TMPFILE"
echo "PATCH_AGENT_TOKEN=$HTTP_CODE"
if ! echo "$HTTP_CODE" | grep -q '^2'; then
echo "PATCH AGENT TOKEN FAIL — users exist but tokenless"
echo "CREATED_USERS: ${CREATED_USER_IDS[*]}"
echo "ORPHAN_GSM_VERSIONS: ${GSM_VERSION_IDS[*]}"
exit 1
fi
# PATCH admin token
TMPFILE=$(mktemp); chmod 600 "$TMPFILE"
echo "{\"token\":\"$TAC_ADMIN_TOKEN\"}" > "$TMPFILE"
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-X PATCH "http://localhost:8055/users/$ADMIN_USER_ID" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d @"$TMPFILE")
rm -f "$TMPFILE"
echo "PATCH_ADMIN_TOKEN=$HTTP_CODE"
if ! echo "$HTTP_CODE" | grep -q '^2'; then
echo "PATCH ADMIN TOKEN FAIL — admin user tokenless"
echo "CREATED_USERS: ${CREATED_USER_IDS[*]}"
echo "ORPHAN_GSM_VERSIONS: ${GSM_VERSION_IDS[*]}"
exit 1
fi
Step 5 — Clear secrets from memory
unset TAC_AGENT_TOKEN TAC_ADMIN_TOKEN
echo "TOKEN_VARS_CLEARED"
3. Post-verification — READ-ONLY auth tests from GSM
KHÔNG gửi POST/DELETE/PATCH/PUT vào /items/tac_*.
3a. Read tokens from GSM
GCP_PROJECT="github-chatgpt-ggcloud"
AGENT_TEST_TOKEN=$(gcloud secrets versions access latest \
--secret=DIRECTUS_TAC_AGENT_TOKEN --project="$GCP_PROJECT")
ADMIN_TEST_TOKEN=$(gcloud secrets versions access latest \
--secret=DIRECTUS_TAC_ADMIN_TOKEN --project="$GCP_PROJECT")
echo "AGENT_TEST=****${AGENT_TEST_TOKEN: -4}"
echo "ADMIN_TEST=****${ADMIN_TEST_TOKEN: -4}"
3b. Agent auth — identity + role
AGENT_ME=$(curl -s http://localhost:8055/users/me \
-H "Authorization: Bearer $AGENT_TEST_TOKEN")
echo "AGENT_EMAIL=$(echo "$AGENT_ME" | jq -r '.data.email')"
echo "AGENT_ROLE=$(echo "$AGENT_ME" | jq -r '.data.role')"
# Expected: tac-agent@incomex.local, TAC_AGENT_ROLE_ID
3c. Agent read vocab (should PASS)
HTTP=$(curl -s -o /dev/null -w "%{http_code}" \
http://localhost:8055/items/tac_lu_lifecycle_vocab \
-H "Authorization: Bearer $AGENT_TEST_TOKEN")
echo "AGENT_READ_VOCAB=$HTTP"
# Expected: 200
3d. Admin auth — identity + role
ADMIN_ME=$(curl -s http://localhost:8055/users/me \
-H "Authorization: Bearer $ADMIN_TEST_TOKEN")
echo "ADMIN_EMAIL=$(echo "$ADMIN_ME" | jq -r '.data.email')"
echo "ADMIN_ROLE=$(echo "$ADMIN_ME" | jq -r '.data.role')"
# Expected: tac-admin@incomex.local, TAC_ADMIN_ROLE_ID
3e. Admin read all 14 (no writes)
for T in tac_lu_lifecycle_vocab tac_uv_lifecycle_vocab tac_review_state_vocab \
tac_pub_lifecycle_vocab tac_cs_lifecycle_vocab tac_section_type_vocab \
tac_publication_type_vocab tac_birth_gate_config tac_logical_unit \
tac_unit_version tac_publication tac_change_set \
tac_publication_member tac_change_set_member; do
HTTP=$(curl -s -o /dev/null -w "%{http_code}" \
"http://localhost:8055/items/$T" \
-H "Authorization: Bearer $ADMIN_TEST_TOKEN")
echo "ADMIN_READ_$T=$HTTP"
done
# Expected: all 200
3f. Gate A/B/C + seed unchanged
# Gate A: 14/7/6 | Gate B: 14 | Gate C: 61
3g. Clear test vars
unset AGENT_TEST_TOKEN ADMIN_TEST_TOKEN
ALL 3b–3f PASS → G8B-Token PASS.
4. Hard Exclusions
| # | Cấm |
|---|---|
| 1 | Không DDL |
| 2 | Không POST/DELETE/PATCH/PUT vào /items/tac_* |
| 3 | Không modify roles/policies/permissions |
| 4 | Không registry/birth/catalog/DOT writes |
| 5 | Không G11 |
| 6 | Không log full token/password — mask ****last4 |
| 7 | Temp file chmod 600 cho mọi payload chứa secret |
| 8 | VPS only via SSH contabo |
| 9 | GSM primary only — STOP nếu fail |
| 10 | Không overwrite/destroy existing GSM secret versions |
5. Failure handling
Track CREATED_USER_IDS + GSM_VERSION_IDS.
GSM preflight fail: STOP. Không tạo gì. GSM store fail: STOP. Tokens in memory → cleared. Không tạo Directus users. Agent user create fail: STOP. Log orphan GSM versions. Admin user create fail: Cleanup agent user do run này tạo. Log orphan GSM versions. Token PATCH fail: STOP. Users exist but tokenless. Log orphan GSM versions + created user IDs. Auth test fail: STOP. Report exact test results + all IDs.
Orphan GSM versions: Nếu Directus activation fail sau GSM store → log exact version IDs. Không tự disable/destroy — chờ GPT/User quyết định. Không claim PASS.
6. Action Log
Path: knowledge/dev/laws/dieu38-trien-khai/reports/p9-g8b-token-provisioning-log-YYYY-MM-DD.md
No-overwrite. Secret hygiene — KHÔNG log token/password. Chỉ ****last4.
Nội dung: GSM preflight (project, auth, secret status), GSM version IDs, user IDs, PATCH results, auth test results (agent identity + read, admin identity + 14 reads), Gate A/B/C unchanged, orphan tracking if any, PASS/FAIL.
7. Sau PASS
G8B-Token PASS → Full G8 PASS (G8B-RP + G8B-Token).
STOP. Upload action log → GPT confirm → G11 User final approval.
G8B-Token v0.4 FINAL | S186 | 2026-04-29 | GPT PASS ✅ AUTHORIZED — execute immediately