Praxis CI
c4c20a3722
feat(P01): SLICE-04 VC issuer key migration SQLite→Postgres (R-VC-MIG-01)
...
- TASK-04-01 server/vc/issuer_keys.py: refactor to IssuerKeyStore
Protocol (runtime_checkable). PraxisStore + PgStore both implement it
(R-VC-MIG-03). Functions now accept IssuerKeyStore instead of
PraxisStore. _fetch_private_key_enc rewritten to use
get_public_key_row (protocol method) instead of store._connect()
(PgStore has no _connect). Backward-compatible — all 19 v0.3 VC
tests still pass.
- TASK-04-02 db/pg_store.py: IssuerKeyStore methods (already implemented
in TASK-01-06): init/get_active/get_public_key_row/set_superseded.
get_public_key_row queries by id (not status) → finds superseded keys
(R-VC-MIG-01 fallback). db/store.py get_public_key_row now also
returns private_key_enc (protocol alignment).
- TASK-04-03 server/vc/migrate_keys.py: migrate_issuer_keys() one-time
procedure. R-VC-MIG-01: archives v0.3 public key as superseded BEFORE
generating the fresh v0.4 active key (step 2 before step 3). G-027
first-boot path: no v0.3 active key in SQLite → skip archive, generate
fresh key only. Idempotent (no-op if Postgres already has an active key).
- TASK-04-04 server/vc/verification.py: verify_credential now accepts
pg_store + sqlite_store kwargs. G-011 two-store fallback (binding):
(a) Postgres for key lookup (active + superseded); (b) Postgres for
credential, fall back to SQLite if not found (v0.3 creds stay in
SQLite); (c) SQLite-only if no Postgres (v0.3 compat).
- TASK-04-05 tests/test_vc_migration.py: 9 tests — migration archives +
generates fresh, idempotent, G-027 first-boot, archive-before-active
ordering (R-VC-MIG-01), v0.3 VC verifies against superseded key in
Postgres (R-VC-MIG-01 critical), v0.4 VC verifies, tamper detection,
G-011(b) SQLite fallback, G-011(c) SQLite-only.
---ci---
project: praxis
phase: 1
milestone: v0.4
status: execute
persona: security-engineer
task: 04-01,04-02,04-03,04-04,04-05
requirements:
covered: [REQ-MT-01]
grill:
- G-011 (two-store fallback semantics — explicit in verify_credential)
- G-027 (first-boot: no v0.3 key → skip archive, fresh key only)
risks:
- R-VC-MIG-01 (archived-before-active — tested in test_migration_archives_before_activating_r_vc_mig_01 + test_v03_vc_verifies_against_superseded_key_in_pg)
---/ci---
2026-08-04 00:55:16 +00:00
Praxis CI
e39521d51d
feat(P01): SLICE-03 operator auth — argon2id + signed cookies + rate limit
...
- TASK-03-01 server/auth/passwords.py: argon2id via argon2-cffi
PasswordHasher (t=3, m=64MiB, p=4 — exceeds OWASP). hash/verify/
needs_rehash; verify returns False on mismatch (uniform 401 path).
- TASK-03-02 server/auth/cookies.py: get_session_middleware_kwargs()
→ Starlette SessionMiddleware (itsdangerous HMAC-SHA256, D-056).
Cookie praxis_op, httpOnly, SameSite=strict, max_age=28800 (8h).
PRAXIS_COOKIE_SECURE default true; false logs WARNING (R-AUTH-01).
G-031 reframe documented: k-anon defense-in-depth is the PRIMARY
mitigation (sniffed cookie → no PII); secure flag is SECONDARY.
- TASK-03-03 server/auth/rate_limit.py: slowapi Limiter (in-memory,
D-041), 5/minute per IP on login. reset_login_rate_limit() helper.
- TASK-03-04 server/auth/dependencies.py + models.py: current_operator
Depends — reads signed-cookie session, fetches operator from PgStore,
401 on missing/invalid/inactive (clears session), 503 if no Postgres.
Never trusts the client (D-057).
- TASK-03-05 server/auth/routes.py: APIRouter(prefix=/api/operator)
with POST /login (rate-limited, rehash-on-login), POST /logout
(auth-gated, clears session), GET /me (auth-gated, React guard).
- TASK-03-06 tests/test_auth.py: 18 unit tests (mocked PgStore) —
passwords, cookie config, rate limit, 401/503 cases, login/logout/me,
rehash-on-login.
- pyproject.toml: added itsdangerous>=2.1 (SessionMiddleware dep).
---ci---
project: praxis
phase: 1
milestone: v0.4
status: execute
persona: security-engineer
task: 03-01,03-02,03-03,03-04,03-05,03-06
requirements:
covered: [REQ-AUTH-01, REQ-NFR-AUTH-01]
grill:
- G-031 (R-AUTH-01 reframe: k-anon primary, secure flag secondary)
---/ci---
2026-08-04 00:52:16 +00:00
Praxis CI
fb109337a5
feat(P01): TASK-01-03 asyncpg pool lifespan + graceful degradation
...
Add @asynccontextmanager lifespan to the FastAPI app that creates an
asyncpg pool (min=1, max=10, command_timeout=10 — D-050) on app.state.pg_pool
and a PgStore on app.state.pg_store when PRAXIS_PG_DSN is set, applies
pg_migrations on startup, and closes the pool on shutdown.
Graceful degradation (REQ-NFR-MT-01): if PRAXIS_PG_DSN is unset, the
server starts with a WARNING and pg_pool/pg_store are None. The learner
voice loop (SQLite PraxisStore) is unaffected. Auth/operator routes will
return 503 (wired in SLICE-06).
---ci---
project: praxis
phase: 1
milestone: v0.4
status: execute
persona: backend-engineer
task: 01-03
requirements:
covered: [REQ-MT-01, REQ-NFR-MT-01]
---/ci---
2026-08-04 00:47:30 +00:00
Praxis CI
f04b9b3588
feat(P01): SLICE-01+02 — Dockerfile, .dockerignore, docker-compose.yml, FastAPI StaticFiles, PRAXIS_DB_PATH env (G-102 fix)
...
SLICE-01 (lead-developer): multi-stage Dockerfile (node:22-slim→python:3.12-slim),
.dockerignore (excludes secrets/node_modules/.git), docker-compose.yml
(port 8789, SQLite volume, env injection for all voice-service vars)
SLICE-02 (backend-engineer+data-engineer): FastAPI mounts client/dist as
StaticFiles at / after API routes (D-023, REQ-DEPLOY-13).
G-102 MUST fix: db/store.py + db/migrate.py now read PRAXIS_DB_PATH
from env so the Docker volume mount persists SQLite data.
G-105 FIX: Dockerfile copies pyproject.toml before source (pip install
layer cached, source changes don't invalidate).
REQ-DEPLOY-01, 02, 13, 16 covered.
---ci---
project: praxis
phase: 1
milestone: v0.2
status: execute
slice: 01-02
wave: 1
---/ci---
2026-08-01 14:16:39 +00:00