KB-6DBF

G8B-Token — TAC Token Provisioning Gate v0.4 FINAL

14 min read Revision 1
g8btokendirectusgsmp9v0.4

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 existSTOP. 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/mismatchSTOP.

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