Files
tools-legacy/openspec/changes/archive/2026-09-30-fpagent-tools-ui/design.md
T
adebisi-fa 109a69961a docs(openspec): fpagent-tools-ui change — proposal, specs, design, tasks
Synced main specs (tools-app, fingerprint-wizard); change archived at
2026-09-30-fpagent-tools-ui. Includes openspec project config and the
agent skills installed with it.
2026-09-30 23:12:51 +01:00

105 lines
5.7 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.
# Design
## Context
`app.py` (Flask + python-socketio) serves the UI from `public/{mode}` and talks
to libfprint via PyGObject; fpagent replaces the capture side entirely with a
guarded Socket.IO server on `127.0.0.1:9000` (enroll-only: `fprint-enroll`,
`fprint-enroll-status`, `fprint-enroll-done`, `fprint-enroll-stop`,
`agent-hello`). The YOUreka Auth pattern is proven in `one-school-chat`
(`src/lib/server/yrka-auth.ts` + `routes/login/+page.server.ts`): portal
`POST /api/web/auth` → redirect → `GET /api/web/auth/{sessionId}/cookie` →
store the PB cookie value under an httpOnly `session` cookie → PocketBase SDK
`authRefresh()` when near expiry. fpagent's permit logic
(`internal/permit/permit.go`) defines the OpenFGA check this app replicates for
domain filtering. See proposal.md for motivation and scope.
## Goals / Non-Goals
**Goals:**
- Zero-configuration operation: every URL and the port hardcoded in source.
- Smallest reliable stack: Express + static CDN pages, no build step, no
session store, no template engine.
- Credentials stay server-side where possible: a2loft calls proxied same-origin
with phone injected from the session; only the PocketBase token is handed to
the page (fpagent's contract requires the page to emit it).
**Non-Goals:**
- clockio/identify/verify (fpagent is enroll-only by design).
- TypeScript, bundlers, component frameworks, server-side templating.
- Permission caching or a domain-management UI (catalog is hardcoded).
- Changes to a2loft's API contract or fpagent itself.
## Decisions
1. **Static CDN front-end, no build step.** The existing `public/capture` page
is Vue 2 + Bootstrap 4 + socket.io + axios via CDN; the wizard reuses that
stack unchanged in shape. Alternative (SvelteKit like one-school-chat)
rejected: new toolchain in this repo for one page.
2. **Session = PB cookie value in an httpOnly cookie; no server store.** Same
as one-school-chat; any instance can serve any request, and refresh happens
server-side per request via the PocketBase SDK against `pb.itcass.net`.
Alternative (express-session + store) rejected: nothing server-side needs
more than the cookie already carries.
3. **Token delivery via `GET /auth/session` JSON** rather than templating: the
wizard is a static page; one fetch returns token, profile, agent URL, and
permitted domains. The token must live in the page because fpagent's
`fprint-enroll` requires it — that is the agent's designed flow, and fpagent
re-validates it per request.
4. **Domain filtering server-side, parallel, fail-closed, cached 5 min.**
Five domains × ≤2 sequential verify calls each, run with `Promise.all`
per (re)check; results cached in an in-memory `Map<phone, {allowed,
checkedAt}>` with a 5-minute TTL (one constant, matching the session
refresh buffer). `/auth/session` serves the cached set within the TTL;
a force-refresh endpoint re-checks all catalog domains, bypassing and
then replacing the cache, and is refused while the cache is fresh. The
filter is UX only; fpagent re-enforces at enroll time, so a revoked
permission degrades gracefully (enroll rejected) even mid-session, and
the wizard's refresh control covers the residual staleness window.
5. **a2loft behind same-origin proxies.** The page calls
`POST /api/:domain/staff/:reference` (lookup) and
`POST /api/:domain/staff/:reference/fingerprints` (save); the server
attaches `phone` from the session and forwards the page-supplied `otp` as
query params — a2loft's contract (`?phone=&otp=`, same response shapes) is
untouched. This keeps the operator's a2loft credential flow out of
page-to-third-party calls and lets the phone come from the trusted session.
6. **Hardcoded config incl. port 4173.** Moving an endpoint or the port is a
one-constant source change; env-var plumbing is unused flexibility.
7. **Python retirement in the same change, gated on a parity walkthrough.**
`app.py` + `requirement.txt` + `public/` are deleted only after the new
wizard completes a real capture-and-save round trip; git history preserves
them. Rollback = revert.
## Risks / Trade-offs
- [a2loft auth contract drifts (no longer `?phone=&otp=`)] → only the two
proxy handlers change; page and specs unaffected.
- [Revoked permission lingers in the UI up to 5 minutes] → bounded by the
cache TTL; fpagent re-enforces at enroll, and the wizard's refresh control
(enabled once the cache elapses) re-checks on demand.
- [Port 4173 already in use on a laptop] → deliberate hardcode; change the one
constant (and fpagent's `-allowed-origins`) if a machine conflicts.
- [Browser → `http://127.0.0.1:9000` blocked as mixed content] → loopback is a
potentially trustworthy origin in all current browsers; works from both http
and https pages. Watch for corporate-policy exceptions in the field.
- [CORS misconfig blocks the wizard silently] → README documents the fpagent
`-allowed-origins` requirement (`http://localhost:4173` in dev); wizard shows
connection state from `agent-hello` so an unreachable/blocked agent is
visible.
- [Vue 2 is EOL] → acceptable for an internal tool page already on it; the
page is isolated and small.
## Migration Plan
1. Build `tools/` alongside the untouched python app (different port; no
interference).
2. Parity walkthrough on field hardware: login → domain filter → lookup →
capture ×2 → save; fingerprints verify against a2loft as before.
3. Delete `app.py`, `requirement.txt`, `public/`; update README.
4. Deploy note: fpagent must include the tools origin in `-allowed-origins`.
## Open Questions
None — a2loft's contract (unchanged, `?phone=&otp=`), hardcoded endpoints, and
the domain catalog were all confirmed with the operator.