refactor(agents): migrate .agent/ planning tree to .agents/

Standardize on the .agents/ directory (shared with project skills):
phases/, user_stories/, reports/, screenshots/, validate.sh, and
phase-sessions/ + pipeline.log all move to .agents/ (git mv preserves
history; runtime artifacts move alongside).

Updates every reference in AGENTS.md, README.md, .gitignore, app
docstrings, and test story headers. Historical KB content in data/
and the runtime pipeline.log transcript are left untouched.
This commit is contained in:
2026-09-05 10:57:07 -04:00
parent 766702c750
commit dbf2af26c6
1118 changed files with 664 additions and 664 deletions
+85
View File
@@ -0,0 +1,85 @@
# Story: Admin Sign-In (Single-Admin Auth)
**Phase:** `16_admin_auth.md` · **E2E:** `tests/e2e/test_admin_auth.py`
## Narrative
As **the one admin (the owner)**, I want to **sign in with a password** so
that I — and only I — can **tune how Brain answers** (Phase 15 steering
notes) and **browse the entire Sources catalog**, while **anyone else stays
anonymous** and can still **chat** and **open the documents the chat cites**.
- **Given** I am not signed in
- **When** I open the app
- **Then** chat, suggestion chips, and the document viewer all work, but the
tuning UI is gone (no Tune button, no Tuning panel) and the Sources page
shows a "sign in to view" state; a **Sign in** link is in the header.
- **When** I sign in with the admin password
- **Then** the tuning UI and the full Sources page are unlocked, and the
header offers **Sign out**.
- Anonymous users may open **any indexed document by direct URL** (soft
rule — the catalog is what is gated, not the viewer).
## Acceptance criteria
1. **Config (fail-loud):** `BOR_ADMIN_PASSWORD` (plaintext, `.env`) and
`BOR_SESSION_SECRET` (random hex) — if either is unset the app **refuses
to start** with a message naming the missing variable(s).
2. `POST /api/login {password}` → **204** + signed session cookie
(Starlette `SessionMiddleware`, `same_site=lax`, 12h default max age);
wrong password → **401** `{"detail": "invalid password"}` (constant-time
compare, one generic message — no user enumeration).
3. `POST /api/logout` → **204** (session cleared, idempotent);
`GET /api/whoami` → `{"authenticated": bool, "role": "admin"|"anonymous"}`.
4. **Admin-only:** `GET /api/docs` and `GET`/`POST`/`DELETE` `/api/steering`
→ **403** for anonymous. **Public (unchanged):** `/api/chat`,
`/api/documents/content`, `/api/suggestions`, `/api/health`, all static
pages.
5. **Login page** `/login.html` (no CDN, Phase-08 tokens, WCAG 2.1 AA):
app-frame header, labeled password field, submit ≥44px, `role=alert`
error region; on success redirects to `?next` (same-origin only, default
`/sources.html`); an already-signed-in visit redirects immediately.
6. **Chat page:** header shows **Sign in** when anonymous (→
`/login.html?next=/sources.html`) or **Sign out** when admin (logout →
reload); when anonymous the Tune button (including on localStorage
restored messages, Phase 14) and the Tuning panel/toggle are completely
hidden; the chat flow itself is unchanged.
7. **Sources page:** anonymous sees `#sources-gate` ("sign in to view the
full catalog" + sign-in link) with stat cards and `#docs-table` hidden and
no `/api/docs` call; admin sees the current full Sources page.
8. **Document viewer:** unchanged — anonymous can open any indexed document
by direct URL.
9. Non-goals: no rate limiting / account lockout, no HTTPS enforcement
(homelab HTTP), no multi-user, no per-user history, no schema change.
10. Unit + integration green, `app/` coverage >90%, story E2E green in
isolation, regression suites adapted + green in isolation, one
`--no-gpg-sign` commit.
## Playwright Mapping Rule
**Test Scenario → `tests/e2e/test_admin_auth.py`** (the E2E app server is
started with `BOR_ADMIN_PASSWORD`/`BOR_SESSION_SECRET` set in
`tests/e2e/conftest.py`; the shared `tests/e2e/auth_helpers.py::login`
performs the real form login):
1. `test_anonymous_chat_without_tuning` — question → streamed answer renders;
no `.tune-btn` under the brain bubble; `#steering-toggle` absent;
`#sign-in-link` visible, `#sign-out-btn` not.
2. `test_anonymous_sources_gated_viewer_open` — `/sources.html`:
`#sources-gate` visible, `#docs-table` hidden; then
`/document.html?source=…&path=…` (seeded fixture doc) renders its content
while anonymous.
3. `test_login_wrong_password_shows_error` — wrong password → `#login-error`
(role=alert) visible, no redirect, still anonymous.
4. `test_admin_login_unlocks_sources_and_tuning` — correct password →
redirect to `/sources.html` with stats + table visible; chat page shows
`#steering-toggle` (count badge) and `.tune-btn` under the answer;
`#sign-out-btn` visible, `#sign-in-link` not.
5. `test_logout_returns_to_anonymous` — Sign out → tuning hidden,
`#sign-in-link` back, `/sources.html` gated again.
6. `test_login_page_a11y` — password field labeled, error announced via
role=alert, targets ≥44px, focus-visible; signed-in visit to
`/login.html?next=/` redirects immediately.
**Regression adaptations (this phase makes their preconditions
admin-only):** `test_steering.py` (log in before tuning),
`test_import_documents.py` + `test_document_back_navigation.py` (log in for
Sources-table assertions), `test_header_consistency.py` (header gains the
auth link — extend presence assertions; heights unchanged).