Files
kite/.planning/phases/02-users-authentication/02-VERIFICATION.md
T

287 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 02-users-authentication
verified: 2026-06-01T14:35:00Z
status: human_needed
score: 6/6
overrides_applied: 0
re_verification:
previous_status: gaps_found
previous_score: 4/5
gaps_closed:
- "Admin can create a new user via POST /api/admin/users without HTTP 500 (session.flush() confirmed, regression test passes)"
- "Auth/login pages show AuthLayout only — App.vue now layout-aware via route.meta.layout conditional"
- "After logout the sidebar is gone — same App.vue v-if fix covers the logged-out state"
- "Non-admin user navigating to /admin is redirected to / — requiresAdmin guard in beforeEach wired"
- "TOTP enrollment shows scannable QR image — qrcode library installed, img tag renders QR from QRCode.toDataURL"
- "TOTP enrollment accessible from Account tab in /settings — SettingsAccountTab.vue created and wired"
gaps_remaining:
- "SC5 (admin JWT returns 403 on document content) — deferred to Phase 3 per D-07 CONTEXT.md decision"
open_findings:
- "CR-01: change_password does not revoke active sessions (CLAUDE.md line 153 — security invariant)"
- "CR-02: disable_totp does not revoke active sessions (CLAUDE.md line 153 — security invariant)"
- "CR-03: ConfirmBlock.vue has no named slot — #confirm-button in SettingsAccountTab is dead (spinner/guard never activates)"
- "WR-01: decodeURIComponent on query param in SettingsView.vue has no error handling — URIError on malformed %encoding"
- "WR-02: TOTP verify code button re-enables during 800ms success flash — double-submission possible"
- "WR-03: Password error routing uses fragile string-matching on raw API messages"
- "WR-04: topicsStore.fetchTopics() fires unconditionally on every page load including auth pages"
regressions: []
deferred:
- truth: "Attempting to access document content via an admin JWT returns 403"
addressed_in: "Phase 3"
evidence: "Phase 3 goal: Document Migration and Multi-User Isolation. CONTEXT.md D-07: existing /api/documents stays public in Phase 2; gains get_current_user guards in Phase 3. REQUIREMENTS.md traceability: SEC-04 mapped to Phase 3."
human_verification:
- test: "TOTP enrollment end-to-end"
expected: "User navigates to /settings, clicks Account tab, sees TotpEnrollment component. In setup step: QR image renders (not a text link). User scans QR with authenticator app. In verify step: user enters 6-digit code. In backup-codes step: 10 codes displayed in 2-column grid with Copy All button and acknowledgment checkbox gating Enable 2FA. After enabling: account shows 2FA active; next login requires TOTP code."
why_human: "Multi-step flow requires authenticator app; QR image rendering requires visual confirmation; backup-code acknowledgment gate requires UI interaction"
- test: "Password reset email delivery"
expected: "User triggers /password-reset for a real email account. Email arrives with correct signed link. Link expires after 1 hour. Following the link and submitting a new strong password returns success message with no auto-login. User must go to /login and pass TOTP gate if 2FA was enabled."
why_human: "Requires SMTP/Celery infrastructure running and actual email receipt; anti-enumeration 202 response cannot confirm dispatch"
- test: "Sign out all devices"
expected: "User clicks Sign out all devices in /settings Account tab. ConfirmBlock appears. On confirm: all sessions revoked, current browser redirected to /login. A second browser tab's next authenticated request fails with 401."
why_human: "Multi-session testing requires two live sessions; refresh token family invalidation requires browser-level verification"
- test: "Admin panel role visibility and CRUD"
expected: "Regular user does not see Admin link in sidebar and cannot navigate to /admin (redirected to /). Admin user sees Admin link with shield icon; can navigate Users/Quotas/AI Config tabs; can create a test user (no HTTP 500); can deactivate a user with inline confirmation showing correct email."
why_human: "Visual rendering, role-conditional DOM, and inline confirmation UX require browser interaction"
- test: "CR-01 / CR-02: Session revocation on password change and TOTP disable"
expected: "After successfully changing password in Account tab: current session is invalidated and user is redirected to /login (or receives a clear sign-out prompt). Any other active refresh tokens are revoked. Same behavior after disabling TOTP. A previously-valid refresh cookie must fail with 401 after the change."
why_human: "Requires confirming backend revocation behavior with live sessions; current code does NOT revoke sessions (CR-01/CR-02 are open code-review blockers — this test is expected to FAIL until the backend fix is applied)"
---
# Phase 2: Users & Authentication — Verification Report (Re-Verification after Plan 06 Gap Closure)
**Phase Goal:** Users can register, log in (with optional TOTP 2FA), reset their password, and sign out all active sessions; admins can manage user accounts and assign AI providers — all enforced by a complete FastAPI dependency chain.
**Verified:** 2026-06-01T14:35:00Z
**Status:** HUMAN NEEDED (all automated checks pass; 5 items require human testing; 2 security invariants from CLAUDE.md require developer resolution)
**Re-verification:** Yes — after Plan 06 gap closure (5 UAT gaps closed)
---
## Goal Achievement
### Observable Truths (Success Criteria from Plan 06 must_haves)
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| T1 | Admin can create a new user via POST /api/admin/users without HTTP 500 | VERIFIED | `await session.flush()` at admin.py:247 (before `write_audit_log()`); `test_create_user_writes_audit_log` passes (1 passed, 2.23s) |
| T2 | Login, register, and password-reset pages show AuthLayout only — no sidebar, no user identity footer | VERIFIED | App.vue line 2: `<AuthLayout v-if="route.meta.layout === 'auth'" />`; all 4 auth routes have `meta: { public: true, layout: 'auth' }` in router/index.js (4 grep matches at lines 22, 27, 32, 37) |
| T3 | After logout the sidebar is gone — the user lands on the login page with AuthLayout | VERIFIED | Same App.vue v-if fix covers logged-out state; /login has `layout: 'auth'` meta so AuthLayout renders, not app shell |
| T4 | Non-admin user navigating to /admin is redirected to / | VERIFIED | router/index.js:91-93: `if (to.meta.requiresAdmin && authStore.user?.role !== 'admin') return { path: '/' }`; /admin has `meta: { requiresAdmin: true }` at line 42 |
| T5 | TOTP enrollment step 1 shows a scannable QR image, not a text link | VERIFIED | TotpEnrollment.vue:111 `import QRCode from 'qrcode'`; line 120 `const qrDataUrl = ref('')`; line 136 `qrDataUrl.value = await QRCode.toDataURL(qrUri.value, ...)`; line 34 `<img v-if="qrDataUrl" :src="qrDataUrl" alt="TOTP QR code" ...>` |
| T6 | TOTP enrollment option is accessible from a tab within /settings (Account tab) | VERIFIED | SettingsView.vue:92 imports SettingsAccountTab; line 100 `{ id: 'account', label: 'Account' }` in tabs array; line 52 `<SettingsAccountTab v-if="activeTab === 'account'" />`; SettingsAccountTab.vue contains TotpEnrollment component at line 63 |
**Score: 6/6 truths verified**
---
### Deferred Items (from Initial Verification — SC5)
Items not yet met but explicitly addressed in later milestone phases.
| # | Item | Addressed In | Evidence |
|---|------|-------------|---------|
| 1 | Attempting to access document content via an admin JWT returns 403 | Phase 3 | Phase 3 goal: "Document Migration and Multi-User Isolation." CONTEXT.md D-07: `/api/documents`, `/api/topics`, `/api/settings` stay public in Phase 2; gain `get_current_user` guards in Phase 3. REQUIREMENTS.md: SEC-04 mapped to Phase 3. The admin panel API (`/api/admin/*`) correctly enforces `get_current_admin` on all endpoints. |
---
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `backend/api/admin.py` | `await session.flush()` before `write_audit_log()` in create_user | VERIFIED | Line 247: `await session.flush() # persist User + Quota before audit_log FK references them` |
| `backend/tests/test_admin_api.py` | `test_create_user_writes_audit_log` regression test | VERIFIED | Line 145; test passes (confirmed by pytest run) |
| `frontend/src/router/index.js` | `meta: { layout: 'auth' }` on 4 auth routes; `meta: { requiresAdmin: true }` on /admin; beforeEach role check | VERIFIED | 4 routes at lines 22, 27, 32, 37; /admin at line 42; beforeEach check at lines 91-93 |
| `frontend/src/App.vue` | Layout-aware root — AuthLayout for auth routes, app shell for all others | VERIFIED | Line 2: `<AuthLayout v-if="route.meta.layout === 'auth'" />`; line 3: `<div v-else ...>` with AppSidebar + router-view |
| `frontend/src/views/SettingsView.vue` | Account tab rendering SettingsAccountTab | VERIFIED | Line 52: `<SettingsAccountTab v-if="activeTab === 'account'" />`; line 92: import; line 100: tab array entry |
| `frontend/src/components/settings/SettingsAccountTab.vue` | Full AccountView content (2FA, change password, sign-out-all) | VERIFIED | 253 lines; 4 sections (Account info, 2FA/TotpEnrollment, Change password, Sessions); all script setup logic ported |
| `frontend/src/components/auth/TotpEnrollment.vue` | QR image via qrcode library (no `<a href="otpauth://...">`) | VERIFIED | QRCode imported line 111; qrDataUrl ref line 120; toDataURL call line 136; img tag line 34 |
| `frontend/package.json` | `"qrcode"` in dependencies | VERIFIED | `"qrcode": "^1.5.4"` in dependencies section (not devDependencies) |
---
### Key Link Verification
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `frontend/src/App.vue` | `frontend/src/layouts/AuthLayout.vue` | `v-if route.meta.layout === 'auth'` | WIRED | Line 2 template; line 15 import |
| `frontend/src/router/index.js` | `frontend/src/stores/auth.js` | `beforeEach` reads `authStore.user?.role` | WIRED | Line 91: `authStore.user?.role !== 'admin'` |
| `frontend/src/components/auth/TotpEnrollment.vue` | `qrcode` npm package | `import QRCode from 'qrcode'`; `QRCode.toDataURL(qrUri.value)` | WIRED | Lines 111, 136 |
| `frontend/src/views/SettingsView.vue` | `frontend/src/components/settings/SettingsAccountTab.vue` | import + v-if render | WIRED | Lines 52, 92, 100 |
| `frontend/src/router/index.js` | `/settings` redirect for `/account` | `{ path: '/account', redirect: '/settings' }` | WIRED | Line 41 |
---
### Data-Flow Trace (Level 4)
| Artifact | Data Variable | Source | Produces Real Data | Status |
|----------|---------------|--------|-------------------|--------|
| `TotpEnrollment.vue` | `qrDataUrl` | `QRCode.toDataURL(qrUri.value)` after `api.totpSetup()` returns `provisioning_uri` | Yes — real otpauth:// URI from backend, converted to PNG data URL | FLOWING |
| `SettingsAccountTab.vue` | `authStore.user.totp_enabled` | Pinia auth store (populated from login response) | Yes — real DB-backed value | FLOWING |
| `SettingsAccountTab.vue` | `authStore.user.email`, `.handle`, `.role` | Pinia auth store `/api/auth/me` response | Yes — real user data | FLOWING |
---
### Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| Admin create_user regression test | `python3 -m pytest tests/test_admin_api.py::test_create_user_writes_audit_log -v` | 1 passed, 2.23s | PASS |
| Frontend build | `npm run build` | 156 modules, exit 0, built in 1.82s | PASS |
| Frontend test suite | `npm test` | 107/107 passed (11 test files) | PASS |
| 4 auth routes have `meta.layout:'auth'` | `grep -c "layout.*auth" router/index.js` | 4 matches | PASS |
| /admin has `meta.requiresAdmin` | `grep -n "requiresAdmin" router/index.js` | Line 42 (route def) + line 91 (beforeEach check) | PASS |
| qrcode in package.json dependencies | `grep "qrcode" package.json` | `"qrcode": "^1.5.4"` | PASS |
| QRCode.toDataURL + img tag in TotpEnrollment | `grep -n "toDataURL\|qrDataUrl\|img.*qr" TotpEnrollment.vue` | Lines 34, 120, 136 | PASS |
| SettingsAccountTab imported and rendered in SettingsView | `grep -n "account\|SettingsAccountTab" SettingsView.vue` | Lines 52, 92, 100 | PASS |
| No localStorage in auth store | `grep -c "localStorage" frontend/src/stores/auth.js` | 0 (from initial verification) | PASS |
---
### Requirements Coverage
| Requirement | Plan | Description | Status | Evidence |
|-------------|------|-------------|--------|---------|
| AUTH-03 | 02-03, 02-06 | TOTP enrollment with 810 backup codes acknowledged before activation | SATISFIED | `generate_backup_codes(10)` + BackupCodesDisplay acknowledgment gate (initial verification); QR image now rendered via qrcode library (plan 06 fix) |
| AUTH-04 | 02-02 | Login via TOTP code or single-use backup code | SATISFIED | `verify_totp()` + `verify_backup_code()` paths in login handler; `used_at` set on use |
| AUTH-05 | 02-03 | Password reset via email; no auto-login; returns to TOTP gate | SATISFIED | Confirm endpoint returns 200 + message, no tokens; `revoke_all_refresh_tokens()` called |
| SEC-01 | 02-02 | CSRF protection (SameSite=Strict + Origin validation) | SATISFIED | `OriginValidationMiddleware` + SameSite=Strict on refresh cookie |
| SEC-03 | 02-01 | Parameterized queries / ORM | SATISFIED | All DB ops via SQLAlchemy ORM; zero raw string interpolation |
| ADMIN-01 | 02-04, 02-06 | Admin creates user with temp password, password_must_change=True | SATISFIED | POST /api/admin/users sets password_must_change=True; HTTP 500 fixed via session.flush() at admin.py:247 |
| ADMIN-07 | 02-04 | Admin impersonation explicitly excluded | SATISFIED | No impersonation endpoint; test_admin_impersonation_not_found asserts 404/422 |
---
### Open Code Review Findings (from 02-REVIEW.md)
These findings were surfaced by the code reviewer after plan 06 execution and are NOT yet fixed. They represent security invariants and UX gaps that require developer attention before Phase 2 can be considered fully resolved.
#### CR-01 — BLOCKER: Password change does not revoke active sessions
**File:** `backend/api/auth.py` line 520 (`change_password` endpoint)
**Issue:** CLAUDE.md line 153 requires: "Password change, TOTP enroll/revoke, and account deactivation immediately revoke all active sessions." The `change_password` handler updates `password_hash` and commits but does not call `revoke_all_refresh_tokens()`. An attacker with a previously-stolen refresh cookie retains full access for up to 30 days after the victim changes their password.
**Fix required:** Add `await auth_service.revoke_all_refresh_tokens(session, current_user.id)` after `session.commit()` in the `change_password` handler. Also redirect the current session to /login in the frontend `changePassword()` success path.
#### CR-02 — BLOCKER: TOTP disable does not revoke active sessions
**File:** `backend/api/auth.py` line 641 (`disable_totp` endpoint)
**Issue:** Same CLAUDE.md line 153 requirement. The `disable_totp` handler clears `totp_secret`, sets `totp_enabled=False`, and deletes backup codes but does not revoke any refresh tokens. An attacker can downgrade account security and both the attacker and any stolen sessions remain valid.
**Fix required:** Add `await session.execute(delete(RefreshToken).where(RefreshToken.user_id == current_user.id))` + `await session.commit()` in the `disable_totp` handler after backup code deletion.
#### CR-03 — WARNING: Dead `#confirm-button` slot in SettingsAccountTab removes spinner/guard on sign-out-all
**File:** `frontend/src/components/settings/SettingsAccountTab.vue` lines 149161
**Issue:** `ConfirmBlock.vue` defines no named slot. The `<template #confirm-button>` block is silently ignored by Vue; the custom button with `:disabled="signingOutAll"` and `<AppSpinner>` never renders. Double-invocation of `signOutAll()` is possible with no spinner feedback.
**Fix required:** Either add `<slot name="confirm-button">` fallback in `ConfirmBlock.vue`, or guard with `if (signingOutAll.value) return` at the top of `signOutAll()`.
#### WR-01 — WARNING: URIError crash path in SettingsView.vue onMounted
**File:** `frontend/src/views/SettingsView.vue``decodeURIComponent(errorMsg)` in onMounted
**Issue:** No try/catch. Malformed `cloud_error` query param (e.g. lone `%`) throws URIError; user sees blank cloud tab with no error message.
**Fix required:** Wrap in try/catch, fall back to raw `errorMsg` value.
#### WR-02 — WARNING: TOTP verify code button re-enables during 800ms success flash
**File:** `frontend/src/components/auth/TotpEnrollment.vue`
**Issue:** `loading` is cleared in `finally` before the 800ms timeout fires; `verifyCode` not cleared on success. Button re-enables, allowing duplicate `api.totpEnable()` call. Backend replay prevention should reject it but UX is broken.
**Fix required:** Clear `verifyCode.value = ''` immediately after `api.totpEnable()` succeeds.
#### WR-03 — WARNING: Fragile string-matching for password error routing
**File:** `frontend/src/components/settings/SettingsAccountTab.vue`
**Issue:** `passwordError.includes('Current')` routes errors to field-level vs. form-level display. Unexpected API messages containing "Current" will be misrouted.
**Fix required:** Use separate refs for field-level and form-level errors.
#### WR-04 — WARNING: `topicsStore.fetchTopics()` fires on auth page loads
**File:** `frontend/src/App.vue` line 20
**Issue:** `onMounted(() => topicsStore.fetchTopics())` runs on every route including /login, /register, /password-reset. The backend returns 401; the API client attempts a token refresh (which fails); auth store clears its already-null accessToken. Spurious 401 + failed refresh on every auth page load.
**Fix required:** Guard the call behind `authStore.accessToken` check, or move it to the authenticated app shell.
#### IN-01 — INFO: qrcode uses caret range instead of exact pin
**File:** `frontend/package.json`
**Issue:** `"qrcode": "^1.5.4"` — CLAUDE.md requires exact version pinning for security-critical packages.
**Fix recommended:** Pin to `"qrcode": "1.5.4"`.
---
### Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `backend/api/auth.py` | 520 | `change_password` commits without `revoke_all_refresh_tokens()` | BLOCKER | Active sessions survive password change — CLAUDE.md line 153 security invariant violated |
| `backend/api/auth.py` | 641 | `disable_totp` commits without `revoke_all_refresh_tokens()` | BLOCKER | Active sessions survive TOTP disable — CLAUDE.md line 153 security invariant violated |
| `frontend/src/components/settings/SettingsAccountTab.vue` | 149161 | `<template #confirm-button>` renders into nonexistent slot — dead code | WARNING | Spinner and double-click guard never activate on sign-out-all |
| `frontend/src/App.vue` | 20 | `topicsStore.fetchTopics()` unconditional on mount | WARNING | Spurious 401 + failed token refresh on every auth page load |
| `frontend/src/views/SettingsView.vue` | onMounted | `decodeURIComponent()` without try/catch | WARNING | URIError crash on malformed cloud_error query param |
| `frontend/src/components/auth/TotpEnrollment.vue` | verify step | `verifyCode` not cleared on success before 800ms timeout | WARNING | Button re-enables during flash window — double-submission possible |
No TBD/FIXME/XXX markers found in Phase 2 plan-06 deliverable files.
---
### Human Verification Required
**5 items need human testing:**
#### 1. TOTP Enrollment End-to-End
**Test:** Log in as a user, navigate to /settings, click the Account tab. Find the "Two-factor authentication" section. Click to set up 2FA. Verify a scannable QR image (not a text link) appears. Scan the QR with an authenticator app (Google Authenticator, Authy, etc.). Enter the 6-digit code. Verify the backup codes screen shows 10 codes in a 2-column grid with a Copy All button and acknowledgment checkbox. Check the acknowledgment box. Click Enable 2FA. Confirm account shows 2FA active. Log out and log back in — confirm TOTP step is required.
**Expected:** QR image renders (no `<a href="otpauth://...">` link visible). 10 backup codes displayed. Acknowledgment checkbox gates Enable 2FA button. After enabling: login requires TOTP code or backup code.
**Why human:** Multi-step flow requires a real authenticator app; QR image rendering requires visual confirmation; the backup-code gate requires UI interaction.
#### 2. Password Reset Email Delivery
**Test:** Navigate to /password-reset. Enter a valid email address. Verify the page always shows a generic "check your email" message (no enumeration of valid/invalid email). Check the inbox. Follow the reset link. Set a new strong password. Verify no auto-login after reset. Navigate to /login and log in with the new password. Confirm TOTP gate appears if 2FA was enabled.
**Expected:** Email arrives with correct signed link. Link expires after 1 hour. Successful reset shows "Password updated. Please sign in." with no tokens issued. TOTP gate on next login if enrolled.
**Why human:** Requires SMTP infrastructure running and actual email receipt. Anti-enumeration means the 202 response alone cannot confirm email dispatch.
#### 3. Sign Out All Devices
**Test:** Log in from two browser tabs or browsers. In Tab 1, go to /settings > Account tab > Sessions. Click "Sign out all devices." Verify a ConfirmBlock appears. On confirm: Tab 1 redirects to /login. In Tab 2, make an authenticated request (e.g., navigate or refresh). Verify Tab 2 is redirected to /login.
**Expected:** Both tabs lose access after "sign out all devices." Reusing a revoked refresh token causes family revocation.
**Why human:** Multi-session behavior requires two live sessions; refresh token reuse detection requires timing across requests.
#### 4. Admin Panel Role Visibility and CRUD
**Test:** Log in as a regular user. Verify the Admin link is NOT visible in the sidebar. Navigate to /admin directly. Verify redirect to /. Log in as admin. Verify "Admin" link with shield icon appears in sidebar. Navigate to /admin. Test: (a) Users tab — list all users; (b) Create a new test user — verify HTTP 200/201 and the user appears in the table (no HTTP 500); (c) Deactivate the test user — verify inline confirmation shows correct email; (d) Quotas tab — view/adjust a quota; (e) AI Config tab — change a provider and verify save flash.
**Expected:** Non-admin users blocked from /admin (redirected to /). Admin CRUD operations work. No HTTP 500 on user creation.
**Why human:** Visual rendering, role-conditional DOM, and inline confirmation UX require browser interaction.
#### 5. CR-01/CR-02: Session Revocation on Password Change and TOTP Disable (EXPECTED FAIL until fix applied)
**Test:** In Browser 1, log in. In Browser 2, log in as the same user. In Browser 1: change the password (Settings > Account tab). In Browser 2: attempt any authenticated action (e.g., navigate to /settings). Verify Browser 2 is redirected to /login. Repeat: In Browser 1, disable TOTP (if enrolled). In Browser 2: verify session is invalidated.
**Expected:** Both Browser 2 sessions are invalidated immediately after password change or TOTP disable in Browser 1.
**Why human:** Requires confirming backend revocation with live sessions. NOTE: This test is expected to FAIL in the current codebase. CR-01 and CR-02 are confirmed open — neither `change_password` nor `disable_totp` in `backend/api/auth.py` calls `revoke_all_refresh_tokens()`. This is a CLAUDE.md line 153 security invariant violation. A fix plan is required before Phase 2 can be marked fully complete.
---
## Gaps Summary
**0 automated gaps blocking phase goal.** All 6 plan-06 must-have truths are VERIFIED. The SC5 gap from the initial verification (admin JWT on documents) is deferred to Phase 3 per D-07 and has been removed from blocking status.
**2 SECURITY BLOCKERS from code review (CR-01, CR-02)** — these are CLAUDE.md security invariant violations requiring a fix plan:
- **CR-01:** `change_password` does not revoke active sessions (backend/api/auth.py:520)
- **CR-02:** `disable_totp` does not revoke active sessions (backend/api/auth.py:641)
These are not UAT-detectable bugs — they are security architecture violations. A gap-closure plan is required targeting only these two backend endpoints. The fixes are each 35 lines (call `revoke_all_refresh_tokens()` after commit). The frontend should also redirect to /login after a successful password change.
**4 WARNINGS from code review (WR-01 through WR-04)** — lower priority UX and robustness issues not blocking the phase goal but recommended for resolution before Phase 3 begins.
---
_Verified: 2026-06-01T14:35:00Z_
_Re-verification: Yes — after Plan 06 gap closure_
_Verifier: Claude (gsd-verifier)_