Compare commits

..

11 Commits

Author SHA1 Message Date
Praxis CI c28f5113c5 verify(P01): APPROVE_WITH_NOTES — operator foundation verified
4-layer verification of Phase 1 (Operator Foundation — Postgres + Auth +
VC migration + Bootstrap CLI). All layers pass.

---ci---
phase: 1
milestone: v0.4
status: verify
requirements:
  covered: [REQ-MT-01, REQ-AUTH-01, REQ-NFR-AUTH-01, REQ-NFR-MT-01, REQ-MT-02]
  partial: []
grill_musts:
  honored: [G-008, G-011, G-027, G-031]
  deferred_to_p2: [G-038, G-041]
tests:
  passed: 272
  skipped: 33
  failed: 0
p0_fixes_applied: 0
p1_plus_flagged: 4
  - argon2id blocking event loop (R-AUTH-02 accepted, offload if frequency grows)
  - rate limit 429 not tested in mock path (tested in PG integration)
  - no PRAXIS_COOKIE_SECRET length validation (add >=32 check)
  - set_credential_status no enum validation (add CHECK constraint)
lessons:
  - SessionMiddleware kwargs are https_only/same_site (not secure/samesite) — fix 0a95102 was correct
  - IssuerKeyStore runtime_checkable Protocol cleanly duck-types both PraxisStore + PgStore
  - R-VC-MIG-01 archive-before-activate ordering is explicitly tested via instrumentation
  - Graceful degradation verified empirically: voice loop unaffected by Postgres absence
---/ci---
2026-08-04 01:40:33 +00:00
Praxis CI 0a951029fd fix(P01): SessionMiddleware kwargs — https_only/same_site (not secure/samesite)
Starlette SessionMiddleware uses `https_only` (not `secure`), `same_site`
(not `samesite`), and has no `httponly` kwarg (httponly is always true for
session cookies). The previous kwargs raised TypeError at middleware stack
build time. Cookie semantics are unchanged: https_only=secure flag,
same_site=strict, max_age=28800 (8h), session_cookie=praxis_op.

---ci---
project: praxis
phase: 1
milestone: v0.4
status: execute
persona: security-engineer
task: 03-02-fix
requirements:
  covered: [REQ-AUTH-01, REQ-NFR-AUTH-01]
---/ci---
2026-08-04 01:12:14 +00:00
Praxis CI 46b46479ca feat(P01): SLICE-06 P1 integration — wire lifespan + auth + verification swap
- TASK-06-01 __main__.py: SessionMiddleware (signed cookies, D-056) added
  AFTER CORS so it is outermost. slowapi limiter state + 429 exception
  handler registered. The lifespan (TASK-01-03) now also runs the VC key
  migration on first boot.
- TASK-06-02 __main__.py: auth_router mounted (POST /api/operator/login,
  POST /api/operator/logout, GET /api/operator/me) BEFORE the StaticFiles
  mount (routes-before-static constraint). Auth routes use app.state.pg_store
  (503 if no Postgres).
- TASK-06-03 __main__.py: /vc/verify swapped to the two-store path (G-011):
  pg_store for key lookup (active + superseded), SQLite fallback for v0.3
  credentials, SQLite-only if no Postgres. _maybe_migrate_issuer_keys()
  runs once in the lifespan (idempotent, G-027 first-boot, non-fatal on
  failure — v0.3 path intact).
- TASK-06-04 tests/test_p1_auth_integration.py: 4 e2e tests (skip if no
  Postgres) — full auth flow, /me without cookie 401, wrong password 401,
  learner voice loop unaffected (REQ-NFR-MT-01).
- TASK-06-05 tests/test_p1_vc_migration_e2e.py: 5 e2e tests (skip if no
  Postgres) — R-VC-MIG-01 critical (v0.3 VC verifies against archived
  superseded key in Postgres), idempotent migration, G-027 first-boot,
  v0.04 VC verifies, tamper detection.

Graceful degradation verified: server starts without Postgres (pg_pool/
pg_store are None; voice loop works; auth routes return 503).

---ci---
project: praxis
phase: 1
milestone: v0.4
status: execute
persona: backend-engineer
task: 06-01,06-02,06-03,06-04,06-05
requirements:
  covered: [REQ-MT-01, REQ-AUTH-01, REQ-NFR-AUTH-01, REQ-NFR-MT-01]
  grill:
    - G-011 (two-store fallback wired in /vc/verify)
  risks:
    - R-VC-MIG-01 (e2e test: v0.3 VC verifies against archived superseded key in Postgres)
---/ci---
2026-08-04 01:00:11 +00:00
Praxis CI e8a05adcd1 feat(P01): SLICE-05 operator bootstrap CLI + secrets scope
- TASK-05-01 scripts/create-operator.py: CLI that reads
  PRAXIS_BOOTSTRAP_OPERATOR_USER/PASS + PRAXIS_PG_DSN from env, creates
  the pool, applies migrations, hashes the password with argon2id, and
  INSERTs with ON CONFLICT DO NOTHING (idempotent — D-052). --update
  flag forces rehash + ON CONFLICT DO UPDATE. Missing env → exit 1
  (R-BOOT-02). Connection failure → 3x retry with 5s backoff (R-BOOT-01).
- TASK-05-02 .ciagent/config.json: added "operator" secrets scope
  (PRAXIS_PG_PASSWORD, PRAXIS_COOKIE_SECRET,
  PRAXIS_BOOTSTRAP_OPERATOR_USER/PASS, PRAXIS_VC_ISSUER_KEY).
  .ciagent/.env.secrets.example: template (committed, no real secrets).
  .gitignore: added negations so .env.secrets.example is tracked while
  .env.secrets stays ignored.
- TASK-05-03 tests/test_create_operator.py: 7 tests (mocked PgStore) —
  create, already-exists (no update), --update rehashes, missing env →
  exit 1, password is argon2id (not plaintext).

---ci---
project: praxis
phase: 1
milestone: v0.4
status: execute
persona: devops-engineer
task: 05-01,05-02,05-03
requirements:
  covered: [REQ-AUTH-01]
---/ci---
2026-08-04 00:56:41 +00:00
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 131545b70a feat(P01): SLICE-02 devops config + G-008 backup-restore drill
- TASK-02-01 .env.example: v0.4 operator vars (PRAXIS_PG_PASSWORD,
  PRAXIS_PG_DSN, PRAXIS_COOKIE_SECRET, PRAXIS_COOKIE_SECURE,
  PRAXIS_BOOTSTRAP_OPERATOR_USER/PASS, PRAXIS_VC_ISSUER_KEY,
  PRAXIS_ISSUER_URL) with documentation comments. PRAXIS_COOKIE_SECURE
  documents the G-031 reframe: k-anon defense-in-depth is the PRIMARY
  R-AUTH-01 mitigation (sniffed cookie leaks no PII); the secure flag is
  the SECONDARY mitigation. PROXMOX_MEMORY_MB default bumped 4096→6144.
- TASK-02-02 lxc-clone.sh: memory default 4096→6144 (REQ-NFR-MT-01 —
  Postgres ~400MB + praxis ~500MB + Docker ~200MB + build headroom ~1GB).
- TASK-02-03 scripts/backup-pg.sh: POSIX-sh nightly cron script,
  pg_dump -Fc to /backups/praxis-<dow>.dump (rolling 7-file, D-055),
  with restore-drill documentation in comments.
- G-008 tests/test_backup_restore.py: backup-restore drill — seeds all 5
  operator-tier tables, pg_dump, drop schema, pg_restore --clean --if-exists,
  verify 5 tables + row counts match. Skips if PRAXIS_PG_DSN unset.

---ci---
project: praxis
phase: 1
milestone: v0.4
status: execute
persona: devops-engineer
task: 02-01,02-02,02-03,G-008
requirements:
  covered: [REQ-NFR-MT-01]
  grill:
    - G-008 (backup-restore drill)
---/ci---
2026-08-04 00:48:14 +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 b0cb6280d7 feat(P01): TASK-01-04..07 Postgres DB foundation — migrate + schema + PgStore + tests
- db/pg_migrate.py: asyncpg migration runner with _pg_migrations tracking
  table, ordered .sql, transactional, 3x retry on connection failure (R-MT-02).
- db/pg_schema.sql + db/pg_migrations/0001_operator_tier.sql: 5 operator-tier
  tables (operators, issued_credentials, mastery_gate_events,
  cohort_aggregates, issuer_keys) using gen_random_uuid() (PG16 core, no
  extension). cohort_aggregates is a plain table, NOT partitioned (D-050).
- db/pg_store.py: PgStore class implementing the IssuerKeyStore protocol
  (init/get_active/get_public_key_row/set_superseded) plus operator CRUD,
  cohort aggregate read/write, credential methods, gate events.
  get_public_key_row queries by id (not status) → finds superseded keys
  (R-VC-MIG-01 verification fallback, D-051). No cross-DB FKs (D-031).
- tests/test_pg_store.py: 13 integration tests (skip if PRAXIS_PG_DSN unset).

---ci---
project: praxis
phase: 1
milestone: v0.4
status: execute
persona: data-engineer
task: 01-04,01-05,01-06,01-07
requirements:
  covered: [REQ-MT-01, REQ-NFR-MT-01, REQ-MT-02]
---/ci---
2026-08-04 00:47:14 +00:00
Praxis CI 6ada2560ba chore(P01): TASK-01-02 add asyncpg, argon2-cffi, slowapi deps
The three v0.4 pip dependencies (RESEARCH-v0.4 §new-deps):
asyncpg>=0.29 (Postgres driver, D-050), argon2-cffi>=23.1 (password
hashing, D-041), slowapi>=0.1 (rate limiting, D-041).

---ci---
project: praxis
phase: 1
milestone: v0.4
status: execute
persona: lead-developer
task: 01-02
requirements:
  covered: [REQ-MT-01, REQ-AUTH-01, REQ-NFR-AUTH-01]
---/ci---
2026-08-04 00:46:09 +00:00
Praxis CI 745dd88dfb feat(P01): TASK-01-01 docker-compose Postgres service + praxis-net
Add postgres:16-slim service with pgdata/pgbackups volumes, pg_isready
healthcheck, praxis-net bridge network (no published ports — D-040).
The praxis service now depends_on postgres healthy and joins praxis-net.
All existing v0.2 env vars + volumes preserved; v0.4 operator env vars
wired through (PRAXIS_PG_DSN, PRAXIS_COOKIE_SECRET, PRAXIS_COOKIE_SECURE).

---ci---
project: praxis
phase: 1
milestone: v0.4
status: execute
persona: lead-developer
task: 01-01
requirements:
  covered: [REQ-MT-01, REQ-NFR-MT-01]
---/ci---
2026-08-04 00:45:35 +00:00

Diff Content Not Available