feat: demo SaaS + training flow security hardening (8/8 review gate passed)

- Demo accounts: super_admin-only provisioning into isolated DEMO_ORG_ID tenant,
  30-day UTC trial on first login, revocable, one-time credential delivery via
  optional SES/webhook (never persisted). Adds boto3 dependency.
- Analytics/report/export/privacy: shared bounded scan budget across users/groups/
  sessions, tenant-consistent session/user/group joins, scalar-only CSV export
  (no nested persisted-value stringification).
- Ownership/tenant isolation: canonical owner-tenant predicate for list/read/chat;
  client sees is_owned only, never owner_user_id.
- Lifecycle/races: status transition validation, analyzing is an in-progress gate
  (no duplicate reanalysis), structured-ready publication, stale-variant revalidation.
- Auth/setup/consent/JWT/OAuth/config: fail-closed consent, bounded JWT lifetime,
  provider-subject atomic OAuth identity, repeated-secret rejection, strict Persona
  trait validation.
- Chat/session/privacy: pre-seller opener redaction, corrupt-session recovery,
  role-aware completed-chat dashboard routing.
- Frontend: Training→product→personas→practice flow, demo/role/demo guards,
  is_owned-based ownership display, 320×568 and 500×768 responsive E2E.
- 8 independent exact-five-key review scopes passed; backend 509, frontend 26,
  production build 1775 modules, isolated E2E 15.
This commit is contained in:
Macky
2026-08-25 06:39:06 +07:00
parent 8a632b5e6a
commit 3c22d88bcd
66 changed files with 8531 additions and 733 deletions

View File

@@ -23,7 +23,49 @@ filesystem JSON storage (no SQL). i18n TH/EN. No self-registration (admin provis
- **admin** — manages groups/users, sees personas with **secret fields stripped** (IP protection).
- **user** (trainee) — trains against personas, own board.
## 2026-08-25 review checkpoint — exact-current local code gate passed 8/8
> All eight independent review scopes now have complete five-key passes with
> empty security and logic arrays: ownership/tenant isolation; lifecycle/races;
> auth/setup/consent/JWT/OAuth/config; Chat/session/privacy;
> analytics/report/export/privacy; frontend Training-to-Practice; frontend-backend
> API contracts; and final cross-cutting security/correctness.
>
> Wave 3 required two remediation rounds. The first bounded analytics scans,
> enforced tenant-consistent session/user/group joins, disabled analysis actions
> while `analyzing`, and routed completed regular-user chats to `/my/board`. The
> second replaced CSV stringification of arbitrary nested persisted values with a
> bounded scalar-only contract. Fresh analytics review `deleg_bcb1510f` and
> frontend review `deleg_1fed2524` both passed.
>
> Final Wave 4 `deleg_97eb385c` passed API contracts and cross-cutting review.
> Latest evidence: backend **509 passed in 73.27s**; frontend **26/26**; production
> build **1775 modules**; isolated Playwright **15/15** across desktop, 320×568,
> and 500×768; compileall, source security scan, and `git diff --check` passed.
>
> Non-blocking follow-ups: parameterize `_csv_cell` edge cases, add malformed API
> envelope tests for view consumers, and prevent Playwright from reusing an
> unrelated server on port 3000. No live OAuth provider or production runtime was
> verified. Branch is `main`; HEAD is
> `8a632b5e6a3b67c9acd2787e1a686ac17ae69486`; index is empty. No stage, commit,
> push, deploy, reset, or stash occurred; broad user WIP remains untouched.
## Current state — local code/security gate passed; production-operation gate pending
> **2026-08-23 — Demo SaaS:** Implemented and locally verified; not deployed or pushed. Demo accounts use
> role `demo` in the dedicated `DEMO_ORG_ID` tenant. Only `POST /api/admin/demo-accounts` (super_admin
> only) can provision them; generic user creation and other role paths are blocked. The first successful
> password login starts exactly one fixed **30-day UTC trial**; the window is idempotent. A super_admin
> can revoke via `POST /api/admin/demo-accounts/<username>/revoke` (DELETE alias), which invalidates
> existing tokens. Demo users can access only demo-visibility, `ready` groups in the demo tenant;
> backend mutation guards block demo group/persona creation and visibility changes. Creation returns the
> username and temporary password once plus SES/webhook delivery status; the password is never
> retrievable later or persisted in stores, audit logs, delivery events, or application logs. Configure
> `DEMO_ORG_ID` (optionally `DEMO_ORG_NAME`/`DEMO_ORG_SEATS`), the AWS credential chain + `AWS_REGION`
> + verified `SES_FROM_EMAIL` (`SES_REPLY_TO` optional), and HTTPS `DEMO_WEBHOOK_URL` +
> `DEMO_WEBHOOK_SECRET`. Focused demo tests: **10 passed**; full backend: **358 passed**; frontend
> Vitest: **5 passed**; `npm run build`: **passed**. Remaining operational gates are to configure and
> verify the SES sender/AWS credentials and webhook secret, test in a restricted environment, and get
> operator approval. No live SES/webhook delivery, production operation, deploy, or push has been
> performed.
> **2026-08-21:** UX/SAAS 12-point redesign (**PUSHED + LIVE, verified 2026-08-21**). Self-registration
> (`POST /api/auth/register`, role=user, first-created-user = super_admin), super_admin may now
> promote others + is invisible to regular admin, user-created private product groups + admin

View File

@@ -32,14 +32,17 @@ Informed by MiroFish (CrowdSight engine) + the hermes-brain-and-tools CrowdSight
| S4.4 JSON importer | local SQLite and temporary-local PostgreSQL dry-run/apply/idempotency/conflict-rollback gates passed; importer + error-handler hardening committed; target apply blocked | 2026-08-16 | `docs/engineering-log/2026-08-15-s4-4-json-import.md`, `docs/engineering-log/2026-08-15-postgresql-import-gate.md`, `docs/engineering-log/2026-08-16-s4-4-importer-errorhandler-commit.md`, `docs/test-evidence/2026-08-15-postgresql-import.md` | target snapshot checksum/count comparison, retained backup, and operator-approved rollback rehearsal |
| UX/UI redesign + marketing website | implemented + locally verified (build clean, 4/4 unit tests, independent review PASS, responsive verified); uncommitted; deploy pending operator approval | 2026-08-19 | `docs/engineering-log/2026-08-19-ux-redesign-and-marketing-site.md`, `website/`, `git diff` | operator approves push (auto-deploys); confirm production `JWT_SECRET` before deploy |
| OAuth Google + Facebook login/register | implemented + locally verified (348 backend tests incl. 11 new, frontend build + 4/4 unit, manual security review PASS, no new deps); uncommitted | 2026-08-20 | `docs/engineering-log/2026-08-20-oauth-google-facebook.md`, `backend/app/services/oauth.py`, `backend/app/api/oauth_routes.py`, `backend/tests/test_oauth.py` | operator approves push; set OAUTH_* creds + OAUTH_DEFAULT_ORG in EasyPanel env to enable (disabled by default, fail-closed) |
| Training product-to-persona flow + security remediation | exact-current local code/security gate passed 8/8 independent scopes; production-operation gate pending | 2026-08-25 | `docs/engineering-log/2026-08-24-training-security-review.md`, `docs/test-evidence/2026-08-24-training-security-review.md` | operator decides whether to stage/commit/push; live OAuth or production verification remains a separate approval gate |
## Guardrails
- No self-registration; admin provisions users. (Verified: register => 404.)
- Public self-registration may create only role `user`; super-admin bootstrap is
factory-controlled and cannot be re-armed by deleting users.
- One persona = one chat per user (one-shot). Enforced in SessionStore + chat start.
- Latent persona fields never leak to trainees pre-result.
- LLM credentials live in `.env` only; never logged.
## Entry index
- `2026-08-24-training-security-review.md` — current Training flow/security remediation evidence, review timeout handling, and blocked stage/commit/push gate.
- `2026-08-07-build-out.md` — M0M7 build-out, decisions, verification, current state.
- `2026-08-07-security-ux.md` — security hardening (path traversal, IDOR, XSS) + UX/UI polish.
- `2026-08-07-auth-gitea.md` — username login + first-time admin setup + Gitea push.

View File

@@ -0,0 +1,67 @@
# 2026-08-23 — Demo SaaS account provisioning and restricted trial
Date: 2026-08-23
Status: implemented + locally verified; live delivery, production operation, deployment, and push not performed
## Scope
- Added the `demo` role with a dedicated `DEMO_ORG_ID` tenant. Demo accounts can be created only
through `POST /api/admin/demo-accounts` by a `super_admin`; generic user creation and direct
`role=demo` mutations are blocked.
- A demo account starts exactly one fixed **30-day UTC trial** on its first successful password
login. The trial timestamps are record-locked and idempotent. `super_admin` can revoke through
`POST /api/admin/demo-accounts/<username>/revoke` (with the DELETE alias), deactivating the
account and invalidating existing tokens.
- Demo users can access only demo-visibility, `ready` groups in the demo tenant. Backend guards
block cross-tenant access, group/persona creation, and visibility changes for the `demo` role.
- Provisioning returns the generated username and temporary password once, together with SES and
authenticated HTTPS webhook delivery status. Delivery is opt-in/best-effort; the temporary
password is not retrievable later and is not persisted in user stores, audit logs, delivery
events, or application logs.
## Implementation surface
These are the implementation modules documented by this entry; this documentation update did not
modify code or tests.
- Backend configuration/wiring: `backend/.env.example`, `backend/app/config.py`,
`backend/app/factory.py`, `backend/requirements.txt`, `backend/requirements.lock.txt`.
- Identity and API policy: `backend/app/auth/users.py`, `backend/app/api/auth_routes.py`,
`backend/app/api/admin_routes.py`, `backend/app/api/group_routes.py`,
`backend/app/api/chat_routes.py`, `backend/app/api/me_routes.py`, and
`backend/app/api/helpers.py`.
- Delivery and storage: `backend/app/services/demo_delivery.py` and
`backend/app/services/groups.py`.
- Frontend wiring/UI: `frontend/src/api/index.js`, `frontend/src/i18n/index.js`,
`frontend/src/store/auth.js`, `frontend/src/router.spec.js`, and the affected views
`AdminUsers.vue`, `GroupBuilder.vue`, `GroupEdit.vue`, `MyBoard.vue`, `Personas.vue`,
`SessionDetail.vue`, and `Training.vue`.
- Focused regression coverage: `backend/tests/test_demo_accounts.py`.
## Verification evidence
| Command | Result |
|---|---|
| `cd backend && uv run pytest -q tests/test_demo_accounts.py` | **10 passed** |
| `cd backend && uv run pytest -q` | **358 passed** |
| `cd frontend && npm run test:unit` (Vitest) | **5 passed** |
| `cd frontend && npm run build` | **passed** |
## Limitations and remaining operational gates
- SES delivery was not exercised against AWS. Before operational use, configure a verified
`SES_FROM_EMAIL`, `AWS_REGION`, and the normal AWS credential chain (`SES_REPLY_TO` is optional).
- Webhook delivery was not exercised against a live receiver. Configure HTTPS `DEMO_WEBHOOK_URL`
and `DEMO_WEBHOOK_SECRET`; the secret is required for signed delivery.
- Test/local configuration may leave SES and webhook settings blank, in which case delivery is
reported as skipped while the one-time API response remains the credential handoff.
- The remaining gate is a restricted-environment test with verified SES sender/AWS credentials and
webhook secret, followed by operator approval. No live SES/webhook call, production operation,
deployment, or push was performed.
## Documentation updated
- `docs/HANDOFF.md` — added the current Demo SaaS state, verification totals, required configuration,
and explicit operational gates.
- `README.md` — added a minimal Demo SaaS roles/permissions/trial note and pointed to
`backend/.env.example` for demo/SES/webhook environment configuration.

View File

@@ -0,0 +1,406 @@
# 2026-08-24 — Training flow and security review checkpoint
## Goal
Move the regular-user journey to:
`Training → สินค้าของฉัน → เลือกสินค้า → ดู Personas → เริ่มฝึก`
while preserving the admin flow, restricting demo users, enforcing tenant/owner
boundaries, allowing safe retry of `draft`/`failed` products, and preventing
persona/credential leakage.
## Remediation recorded
- Regular users create products through their own route; demo users are blocked
in both frontend navigation and backend authorization.
- Personas links are shown to regular users only for `ready` products.
- Owners retain visibility of their private `draft`/`failed` products; admins
can retry analysis through the edit flow.
- Cross-tenant group IDs are indistinguishable from missing IDs (`404`), and
API responses use `is_owned` rather than exposing `owner_user_id`/`creator_id`.
- Protected routes require both completed setup (`must_setup is False`) and
explicit Terms consent (`accepted_terms is True`); missing markers fail closed.
- Session/debrief/persona responses use closed allowlists; provider/storage
errors are mapped to stable client-safe messages.
- Owner markers and private visibility now form one fail-closed invariant;
normal create/update paths reject unowned-private and malformed-owner records,
while legacy malformed records are excluded from access and indexes.
- Non-ready groups remain visible to their owner/admin for recovery, but their
persona payloads/counts and direct persona access are empty/blocked until
`ready`, including admin responses.
- Owner products stuck in `analyzing` remain reopenable and the Training and
admin edit UIs expose the recovery action instead of dead-ending.
- Judge-only evaluator reasoning and provider paths are excluded from the
persona role-play prompt; only bounded role-play state is retained.
- Transcript serialization keeps seller/customer messages plus an exact
allowlist of server-generated `⏳` scene notes, redacts legacy initial openers,
and transcript-only debriefing receives seller messages only.
- `finishSetup()` remains setup-required unless the API returns both
`must_setup: false` and `accepted_terms: true`.
- JWT lifetimes now use a positive 1720 hour allowlist and token issuance
rejects invalid values before encoding; decode also rejects non-positive `exp`.
- Group visibility now uses one exact allowlist; malformed/non-string/falsey
persisted values are rejected at listing and ID-addressed authorization.
## Current verification
| Check | Result |
|---|---|
| `cd backend && ./.venv/bin/python -m pytest -q` | **457 passed** |
| `cd frontend && npm run test:unit` | **18 passed** |
| `cd frontend && npm run build` | **passed** |
| `cd frontend && npm run test:e2e` | **15 passed** across desktop, 320×568, and 500×768 |
| `cd backend && python3 -m compileall -q app tests` | **passed** |
| `git diff --check` | **passed** |
| Added-line static security scan | **0** shell-injection, eval/exec, pickle, or formatted-SQL matches; hardcoded-secret pattern matched test-only fixture literals only, with **0 production-file matches** |
The Playwright run emitted Vite proxy connection-refused messages for
`/api/auth/oauth/config` because no backend was running for the fixture server.
The tests still passed; this checkpoint does not claim live-provider or
production verification.
## Independent review gate
`deleg_6017c670` was dispatched against the current tree with four bounded
scopes. All four reviewers timed out after roughly ten minutes and returned no
complete five-key JSON payload. This is **no verdict**, not a failed review and
not approval. Fresh batch `deleg_ea020089` then targeted the post-hardening
tree, but its auth, tenant/group, and frontend scopes timed out at 600 seconds
and its integration task was interrupted. It returned no complete five-key
verdict and is also **no verdict, not approval**. Replacement integration review
`deleg_513b1aa3` also timed out at 600 seconds with no complete five-key payload;
it is **no verdict, not approval**. The gate remains closed until replacement
scopes return complete, schema-valid current-tree verdicts with `passed: true`:
Fresh batch `deleg_5c88269e` also targeted the post-hardening tree, but its
delegation owner exited before recording a terminal result. It is
unknown/no-verdict and cannot affect the gate.
Bounded batch `deleg_fc7b3f38` returned one valid negative auth verdict whose findings were remediated, one valid positive tenant verdict, and no usable frontend/integration verdicts, so it did not approve the batch. Latest batch `deleg_73452135` timed out in all four scopes after 600 seconds with no complete five-key payload; it is **no verdict, not approval**. Replacement batch `deleg_191e6862` completed with one valid negative auth verdict, one valid positive frontend verdict, one timeout, and one valid negative lifecycle verdict; its findings were remediated and it did not approve the tree. Fresh batch `deleg_71b85ab4` completed with one valid frontend pass and three timeouts (auth, backend visibility/privacy, and lifecycle); the timeouts are no verdicts, so this batch did not approve the tree. Replacement batch `deleg_b4bdbacb` completed with four valid negative current-tree verdicts; all cited findings were remediated locally and re-verified by focused/full automated tests. Earlier batch `deleg_40657086` produced one valid positive auth verdict, but its group, chat/privacy, and lifecycle scopes were interrupted without complete five-key payloads; it is incomplete/no-approval. Interrupted batch `deleg_735d9a1f` produced no complete five-key payload in any scope, so it is **no verdict, not approval** and is superseded by the current batch. Fresh batch `deleg_4c6a7118` is now running against the exact current tree with eight bounded scopes and an exact five-key schema. It includes a regression gate for the corrected super-admin CSV privacy bypass and malformed active-session recovery. No stage, commit, or push is allowed until every required scope returns a complete `passed: true` verdict with empty blocking arrays.
Older batch `deleg_3fdefb96` completed before the latest hardening and is
superseded; its findings are not approval or a current-tree verdict.
Batch `deleg_b25d8482` likewise completed from a pre-hardening snapshot and is
superseded; only a fresh current-tree verdict can affect this gate.
Batch `deleg_f0a2aa91` also predates the latest hardening and is superseded;
its findings do not close or replace the current-tree gate.
Batch `deleg_08c54d5a` was also dispatched before the latest hardening. Its
auth task was interrupted with no verdict; its tenant and frontend tasks are
superseded and cannot affect the current-tree gate.
Batch `deleg_cd09ba23` exited before recording a terminal result. It is an
unknown/no-verdict batch, predates the current hardening, and cannot affect the
current-tree gate.
Batch `deleg_abeccba2` was dispatched before the latest hardening; all three
tasks were interrupted without complete JSON verdicts and cannot affect the
current-tree gate.
Batch `deleg_bfac6f8b` exited before recording a terminal result. It is an
unknown/no-verdict pre-hardening batch and cannot affect the current-tree gate.
Batch `deleg_5f3f325f` exited before recording a terminal result. It is an
unknown/no-verdict pre-hardening batch and cannot affect the current-tree gate.
```json
{
"passed": true,
"security_concerns": [],
"logic_errors": [],
"suggestions": [],
"summary": "..."
}
```
## Git and safety state
- Branch: `main`
- HEAD: `8a632b5e6a3b67c9acd2787e1a686ac17ae69486`
- Index: clean; no staged paths
- Existing user WIP remains untouched
- No secrets were read, stored, staged, committed, pushed, or deployed
## Next action
Validate exact-current re-check `deleg_0456c47d`. If both exact five-key verdicts
pass with empty blocking arrays, dispatch the remaining six scopes as three queued
waves of at most two heavy reviewers. If either fails, stop the queue and repeat
test-first remediation only for that scope. Commit/push/deploy still require
explicit operator direction.
## 2026-08-25 Wave 2 Chat/session and auth remediation
Independent batch `deleg_ead5db71` returned two complete negative verdicts.
The Chat/session reviewer reproduced three current-tree failures: a second
pre-seller customer opener leaked through public serialization, malformed
prompt-facing Persona fields could be published as ready, and a corrupt
same-scope session status allowed creation of a second session. The auth reviewer
reproduced repeatable public super-admin promotion after deleting all users,
unbounded forged JWT lifetimes, trivially repeated production secrets, unstable
OAuth subject/email identity, implicit OAuth tenant creation, and malformed
consent state accepted by `/auth/me`.
The remediation was test-first. `backend/tests/test_wave2_review_regressions.py`
failed all **12** new regressions before production changes and then passed all
**12**. The current implementation now:
- redacts every customer message before the first seller turn;
- validates bounded, finite, structurally canonical prompt-facing Persona data;
- rejects malformed same-scope session mode/status/outcome before resume/create;
- keeps public registration permanently at role `user`; factory bootstrap is the
only super-admin initialization path;
- bounds decoded JWT lifetime to configured `JWT_EXPIRES_HOURS` and requires
positive finite `iat`/`exp`;
- rejects trivially repeated production JWT/bootstrap secrets;
- persists atomic provider+subject OAuth bindings, rejects email-only auto-link,
and requires a pre-provisioned active default organization; and
- rejects inconsistent consent/setup state and malformed consent timestamps.
Compatibility fixes narrowed Persona validation to bounded recursive JSON while
retaining strict scalar revealable fields, updated stale tests that encoded the
retired email auto-link/first-user promotion behavior, and kept malformed state
fail-closed. Current automated evidence is **496 backend tests passed in 71.80s**,
`compileall` passed, `git diff --check` passed, and the added-line secret/dangerous-
execution scan returned **0 findings**. No live OAuth provider verification is
claimed.
Re-review `deleg_6155669d` returned two complete negative verdicts. Chat/session
still silently normalized malformed explicit `tier` and `recontact`; auth still
accepted duplicate same-user OAuth bindings, low-period repeated secrets, and a
non-null malformed consent timestamp while Terms were unaccepted. Six additional
RED assertions reproduced all four classes. The remediation validates explicit
Persona behavior fields before defaults/coercion, rejects every repeated OAuth
identity pair, detects exact repeated secret periods, and enforces a canonical
unaccepted-consent timestamp of null/absent.
Re-review `deleg_1e21e60c` approved the auth/setup/consent/JWT/OAuth/config
scope with an exact five-key pass after **226 focused** and **502 full** tests.
It found one remaining Chat-only alias-validation defect: malformed explicit
`intent_tier` was skipped when a valid `tier` coexisted. A dedicated RED test
reproduced the acceptance; the guard now validates every explicitly present
alias before normalization.
Exact-current evidence after that narrow patch: focused Wave-2 file **19 passed
in 1.39s**, full backend **503 passed in 71.89s**, compileall and
`git diff --check` passed, and the added-line scan returned **0 findings**.
Incremental current-tree Chat re-review `deleg_8c71eff4` returned an exact
five-key pass with empty blocking arrays. Its live probes confirmed malformed
explicit `intent_tier` fails closed alone and beside a valid `tier`, while
canonical nested Persona data remains accepted. The reviewer independently ran
**136/136** bounded Chat/session/privacy tests, **19/19** Wave-2 regressions,
four targeted tests, compileall, and diff checks. At that historical checkpoint,
Wave 2 was closed and the overall gate was **4/8**; Wave 3 batch
`deleg_1733a8bf` then reviewed analytics/privacy and frontend with exactly two
read-only reviewers while Wave 4 remained queued. The final state is recorded
in the Wave 34 completion section below.
## Wave 1 current-tree review and remediation
`deleg_4d48caf2` returned two schema-valid negative verdicts rather than timing
out. Ownership review proved that `super_admin` could cross the owner/private
boundary in group listings, ID-addressed reads, report/Persona reads, and chat.
Lifecycle review proved three independent gaps: privileged malformed-ready reads,
unvalidated status/transitions, and stale admin variant publication after source
replacement during generation.
The fixes were test-driven:
- Added `backend/tests/test_review_wave1_regressions.py`; its initial run failed.
- Excluded owner-private records from super-admin listing and authorization in
both group and chat route policy implementations.
- Added `GROUP_STATUSES` and explicit transition validation to `GroupStore`.
- Validated structurally ready artifacts before any role receives a persisted
ready group.
- Revalidated the source persona identity under the record lock before appending
an admin variant.
- Updated stale compatibility tests that previously treated malformed-ready
records as readable.
Verification after compatibility remediation:
| Check | Result |
|---|---|
| focused Wave 1 + related suites | **125 passed** |
| backend full suite | **471 passed in 68.53s** |
| frontend unit | **20 passed in 6 files** |
| frontend production build | **passed; 1775 modules transformed** |
| Playwright desktop + 320×568 + 500×768 | **15 passed in 3.6s** |
| compileall / `git diff --check` | **passed / passed** |
| added-line security scan | **0 findings** in checked secret, shell, SQL, and debug patterns |
Remediation re-check `deleg_8d258a4f` was dispatched with exactly two heavy
reviewers. The six untouched scopes remain queued. No stage, commit, push, deploy,
reset, stash, credential, or permission action was performed.
## 2026-08-25 second lifecycle remediation
`deleg_8d258a4f` returned two complete five-key verdicts. Ownership/tenant
isolation passed with empty blocking arrays. Lifecycle/races/readiness/variants
failed on five confirmed classes: malformed-ready privileged indexes,
`publish_analysis()` transition bypass, duplicate Persona IDs, stale same-ID admin
variant publication, and missing trainee source revalidation.
The second remediation was test-first. Six new assertions failed before the patch.
The service now excludes malformed lifecycle records from role indexes, publishes
analysis only from `analyzing`, and rejects duplicate Persona IDs. Both admin and
trainee variant paths now re-authorize and compare the complete source Persona,
sales kit, and safe input snapshot under the source-group lock before publication.
A stale board expectation was updated to match the canonical fail-closed contract.
Current evidence:
| Check | Result |
|---|---|
| focused lifecycle/remediation set | **69 passed in 10.90s** |
| backend full suite | **477 passed in 69.17s** |
| compileall (`app tests`) / `git diff --check` | **passed / passed** |
| added-line static scan | **0 production findings**; 3 password matches are test fixtures |
Because the patch touched the shared visibility boundary, both Wave 1 scopes were
re-dispatched against the exact current tree as `deleg_0456c47d`, with two
read-only reviewers and no overlapping review wave. The other six scopes remain
queued. No stage, commit, push, deploy, reset, stash, credential, or permission
action was performed.
## 2026-08-25 third Wave 1 remediation
`deleg_0456c47d` returned two complete five-key negative verdicts. Ownership
failed because role-first authorization denied a private product to its own user
after promotion to admin or super-admin. Lifecycle failed because the variant
snapshot omitted visibility and had no monotonic signal for a structurally
identical reanalysis.
Five RED regression instances reproduced the exact gaps. The current policy now
recognizes a valid same-tenant owner before applying privileged non-owner denial,
while malformed owner records retain their prior fail-closed 403/404 contracts.
Privileged-owner listing uses the closed owner envelope (`is_owned`) rather than
exposing `owner_user_id`. Every successful analysis publication increments an
`analysis_revision`; variant generation snapshots and revalidates both that
revision and visibility under the source lock for admin and trainee paths.
Current evidence:
| Check | Result |
|---|---|
| RED Wave 1 regression file | **5 expected failures; 17 passed** |
| focused lifecycle/ownership/race set | **74 passed in 12.12s** |
| malformed-owner contract + Wave 1 set | **24 passed in 3.55s** |
| backend full suite | **482 passed in 70.14s** |
| compileall (`app tests`) / `git diff --check` | **passed / passed** |
| added-line static scan | **0 findings** |
Fresh exact-current re-review `deleg_a5931b0d` is pending with exactly two
read-only reviewers. The six untouched scopes remain queued; no overlapping wave
was launched. No stage, commit, push, deploy, reset, stash, credential, or
permission action was performed.
## 2026-08-25 promoted-owner response and frontend remediation
`deleg_a5931b0d` returned two complete five-key verdicts. Lifecycle/races passed
with empty blocking arrays. Ownership failed because promoted owners received
`is_owned=false` from detail/analyze serialization, their private products were
hidden by `Training.vue`, and the router denied an admin owner from Personas.
The ownership gaps were reproduced before the patch: backend Wave 1 had **2
expected failures**, and the frontend Training/router slice had **3 expected
failures**. Serialization now derives ownership from the canonical valid
owner-private predicate rather than current role. Training renders a privileged
owner's own-product section when it contains products, and the router admits a
non-demo canonical owner before applying privileged shared-product policy.
Current-tree evidence after remediation:
| Check | Result |
|---|---|
| backend promoted-owner/Wave 1 regressions | **22 passed in 3.17s** |
| frontend Training + router regressions | **17 passed in 2 files** |
| backend full suite | **482 passed in 70.11s** |
| frontend full unit suite | **24 passed in 6 files** |
| frontend production build | **passed; 1775 modules transformed** |
| isolated Sales Trainer E2E | **15 passed in 3.8s; desktop, 320×568, 500×768** |
| compileall / `git diff --check` / added-line scan | **passed / passed / 0 findings** |
The initial E2E attempt reused port 3000 and tested an unrelated CrowdSight Vite
server; the served title proved the collision. No product change was made for
that infrastructure failure. An isolated Sales Trainer server on port 3001 passed
all 15 cases and was then stopped. Fresh ownership-only reviewer
`deleg_457c2d44` is pending with one read-only agent. Lifecycle remains approved;
the remaining six scopes stay queued. No stage, commit, push, deploy, reset,
stash, credential, or permission action was performed.
## 2026-08-25 cross-tenant promoted-owner remediation
Fresh ownership reviewer `deleg_457c2d44` reproduced a blocking stale-record
path: a promoted super-admin could list, read, and start chat against a private
record in another tenant when that record reused the actor's user ID. The prior
owner predicate validated identity and private visibility but allowed the
super-admin role to bypass tenant equality.
Two parameterized regressions covered a different valid tenant and an invalid
empty tenant. Both failed before production changes. A single canonical helper
now requires a valid matching actor/group tenant together with owner ID and
private visibility. List serialization, group authorization, `is_owned`, and
chat authorization use that policy. Cross-tenant super-admin access remains
available only for ownerless shared records.
| Check | Result |
|---|---|
| RED stale/invalid-tenant owner regressions | **2 expected failures** |
| canonical + stale-tenant focused slice | **4 passed in 1.43s** |
| ownership/security focused set | **125 passed in 11.52s** |
| backend full suite | **484 passed in 70.52s** |
| compileall / `git diff --check` / added-line scan | **passed / passed / 0 findings** |
The attempted focused command naming nonexistent `test_training_flow.py` ran
zero tests and is not evidence; it was replaced by the verified existing-file
command above. `deleg_457c2d44` is superseded by this code patch. Fresh
ownership-only reviewer `deleg_85d91881` is pending with one read-only agent;
the six remaining scopes are still queued. No stage, commit, push, deploy,
reset, stash, credential, or permission action was performed.
## 2026-08-25 Wave 34 completion — eight-scope code gate passed
Wave 3 initially returned two complete negative verdicts in `deleg_1733a8bf`.
The analytics scope found unbounded personal/admin store scans and a missing
session-user-tenant consistency check. The frontend scope found duplicate analysis
actions while status was `analyzing` and a completed regular-user chat link that
routed through `/` instead of `/my/board`. All four findings were reproduced and
remediated with focused regressions, bounded shared scan accounting, tenant-safe
joins, disabled in-progress actions, and role-aware result navigation.
Fresh Wave 3 batch `deleg_1fed2524` approved the frontend scope but found one
remaining analytics export leak: arbitrary nested persisted values were passed to
`str()` and emitted into CSV. A RED sentinel regression reproduced leakage from
`persona_name`, `scenario`, `created_at`, and a malformed username. `_csv_cell`
now exports only bounded strings and finite integer/float scalars; booleans,
composites, non-finite numbers, and unsupported values become empty cells.
Post-fix evidence:
| Check | Result |
|---|---|
| nested-value CSV regression | **1 passed in 0.63s** |
| focused analytics/export/privacy slice | **39 passed, 146 deselected in 7.88s** |
| backend full suite | **509 passed in 73.27s** |
| frontend full unit suite | **26 passed** |
| frontend production build | **passed; 1775 modules transformed** |
| isolated Playwright desktop + 320×568 + 500×768 | **15 passed** |
| compileall / `git diff --check` / source security scan | **passed / passed / 0 findings** |
Analytics re-review `deleg_bcb1510f` returned an exact five-key pass with empty
security and logic arrays. It independently confirmed scalar-only CSV handling,
shared scan budgets, and tenant-consistent session/user/group joins. Wave 3 then
closed at **6/8** approved scopes.
Final Wave 4 batch `deleg_97eb385c` used exactly two concurrent read-only agents:
- Scope 7/8 frontend-backend API contracts: **passed**, empty blocking arrays;
reviewer verification included **26/26** frontend tests and **81/81** focused
backend contract/security tests.
- Scope 8/8 bounded cross-cutting security/correctness: **passed**, empty blocking
arrays; backend, frontend, build, compileall, diff check, and isolated
three-viewport E2E all passed.
The exact-current local code/security gate is therefore **8/8 independently
approved**. Non-blocking follow-ups are to add direct `_csv_cell` parameterization,
expand malformed frontend-envelope tests, and prevent Playwright from reusing an
unrelated server on port 3000. These do not reopen the gate. No live OAuth-provider
verification, production runtime check, stage, commit, push, deploy, reset, stash,
credential, or permission action is claimed. Branch remains `main`; HEAD remains
`8a632b5e6a3b67c9acd2787e1a686ac17ae69486`; the index is empty and the broad
existing working tree remains intentionally dirty.

View File

@@ -0,0 +1,157 @@
# Test evidence — 2026-08-24 Training flow/security remediation
## Scope
Current uncommitted Training → My Products → Personas → Practice flow,
ownership/tenant isolation, lifecycle recovery, session-mode hardening,
analytics/export privacy, frontend contracts, and Wave 1 reviewer remediation.
## Current-tree automated checks
- Backend: `cd backend && ./.venv/bin/python -m pytest -q`**484 passed in 70.52s**.
- Latest focused lifecycle/ownership/race set → **74 passed in 12.12s**.
- Targeted malformed-owner contract + Wave 1 regression set → **24 passed in 3.55s**.
- Frontend unit: `cd frontend && npm run test:unit`**24 passed in 6 files**.
- Frontend production build: `cd frontend && npm run build`**passed**
(`1775` modules transformed).
- Browser journeys: `E2E_BASE_URL=http://127.0.0.1:3001 npm run test:e2e`
**15 passed in 3.8s** across desktop, 320×568, and 500×768. The first attempt
correctly failed because Playwright reused port 3000, which was serving CrowdSight;
rerunning against an isolated Sales Trainer server on port 3001 passed every case.
- Frontend readiness: `curl -fsS http://127.0.0.1:3001/`**passed**;
temporary process `proc_3697d2f91f57` was killed after E2E.
- Python compilation: `cd backend && ./.venv/bin/python -m compileall -q app tests`**passed**.
- Whitespace validation: `git diff --check`**passed**.
- Added-line static scan on the latest lifecycle slice: **0 production findings**
for hardcoded secrets, shell execution, eval/exec, unsafe pickle, formatted SQL,
and debug leftovers. Three password-pattern matches were test fixtures.
- Post-`deleg_457c2d44` stale-tenant ownership regressions failed twice before
remediation, then the focused ownership/security set passed **125 tests in
11.52s**. Canonical private ownership now requires a valid matching tenant
together with owner identity, including for promoted super-admins.
- `ruff`, `mypy`, `eslint`, and `tsc` are unavailable in this environment; no
result is claimed for those tools.
## Wave 1 independent findings and remediation
Batch `deleg_4d48caf2` returned two complete, schema-valid negative verdicts:
1. Ownership/tenant isolation: `super_admin` could list or ID-address owner-private
products and start chat against them.
2. Lifecycle/readiness/races: privileged reads trusted malformed persisted `ready`
state, lifecycle status values/transitions were not validated at the service
boundary, and admin variant generation could append a stale variant after
concurrent source-persona replacement.
Current-tree remediation:
- Owner-private products are excluded from super-admin listings and denied in
group and chat authorization; explicitly shared cross-tenant administration
remains separate.
- `ready` records fail closed for every role unless sales kit, report, and
canonical Personas are structurally valid.
- `GroupStore` now allowlists lifecycle states and explicit permitted transitions,
including `failed → analyzing` retry and `ready → analyzing` reanalysis.
- Variant publication revalidates both source status and the exact source persona
under the group record lock after LLM generation.
- Privileged indexes exclude malformed `ready` and invalid-status records;
`publish_analysis()` accepts only persisted `analyzing` groups.
- Canonical Persona normalization rejects duplicate IDs, and variant publication
compares the complete source Persona, sales kit, and safe input snapshot for
both admin and trainee paths.
- Added `backend/tests/test_review_wave1_regressions.py` and updated stale tests
that previously accepted malformed-ready artifacts.
## Independent review gate
Remediation re-check batch `deleg_8d258a4f` returned complete verdicts:
- Ownership/tenant isolation: **passed** with empty blocking arrays.
- Lifecycle/races/readiness/variants: **failed** with confirmed malformed-index,
publication-transition, duplicate-ID, and stale same-ID/admin+trainee variant gaps.
Batch `deleg_0456c47d` then returned two complete negative verdicts: privileged
owners lost their own private products after promotion; source visibility was not
snapshotted; and structurally identical reanalysis was not detectable. Five RED
regressions reproduced those gaps. The current remediation gives valid owners
precedence without weakening malformed/non-owner denial, snapshots visibility,
and increments/revalidates monotonic `analysis_revision` on every publication.
Fresh exact-current re-review `deleg_a5931b0d` is pending with two reviewers and
no overlapping wave. The remaining six scopes are queued and not dispatched.
The gate remains **closed** until all eight current-tree scopes return complete,
schema-valid verdicts with `passed: true`, `security_concerns: []`, and
`logic_errors: []`. Interrupted, timed-out, truncated, malformed, stale, or partial
verdicts do not approve the tree. Any source remediation makes earlier verdicts
stale for affected scopes.
## Harness limitation
The Playwright fixture server may emit Vite proxy `ECONNREFUSED` messages for
`/api/auth/oauth/config` because no Flask backend or OAuth provider is running.
This evidence is not a live-provider, authenticated production, or deployment
smoke test.
## Wave 2 remediation evidence — 2026-08-25
- Failed independent review: `deleg_ead5db71` returned complete negative
Chat/session and auth/setup/JWT/OAuth/config verdicts with live local probes.
- RED: `tests/test_wave2_review_regressions.py`**12 expected failures** before
production remediation.
- GREEN: the same focused file → **12 passed in 1.16s**.
- Focused compatibility run: **197 passed / 1 stale fixture failure**, followed
by fixture correction; the preceding broader attempt was **157 passed / 29
failures**, dominated by one over-strict Persona validator and stale policy
expectations, and is not reported as a passing gate.
- Full backend: `cd backend && PYTHONPATH= .venv/bin/pytest -q` → **496 passed in
71.80s**.
- Python compilation and `git diff --check`: **passed**.
- Added-line hardcoded-secret/dangerous-execution scan: **0 findings**.
- Re-review `deleg_6155669d`: two complete negative verdicts. It reproduced
malformed explicit Persona trait coercion plus three auth gaps: same-user
duplicate OAuth bindings, low-period repeated secrets, and malformed timestamp
state while Terms were unaccepted.
- Second RED cycle: **6 expected failures / 12 existing passes** in the focused
file before production remediation.
- Second GREEN cycle: focused Wave-2 file → **18 passed in 1.40s**.
- Latest full backend → **502 passed in 72.06s**; compileall, diff check,
Markdown fence check, and added-line scan all passed with **0 scan findings**.
- Re-review `deleg_1e21e60c`: auth/setup/consent/JWT/OAuth/config **passed**
with exact empty blocking arrays after **226 focused** and **502 full** tests;
Chat remained blocked by malformed explicit `intent_tier` coexisting with a
valid `tier`.
- Third Chat RED cycle: the dedicated coexistence regression failed as expected.
- Third Chat GREEN cycle: focused Wave-2 file → **19 passed in 1.39s**; latest
full backend → **503 passed in 71.89s**.
- Latest compileall, diff check, and added-line scan passed with **0 findings**.
- Incremental current-tree Chat re-review `deleg_8c71eff4`: **passed** with
exact empty blocking arrays. Independent checks: **136/136** bounded
Chat/session/privacy tests, **19/19** Wave-2 regressions, four targeted tests,
compileall, and diff checks.
- Wave 3 first review `deleg_1733a8bf`: both scopes failed and were remediated.
Analytics required shared bounded scans and tenant-consistent user/session/group
joins. Frontend required disabled `analyzing` actions and regular-user
completed-chat navigation to `/my/board`.
- Wave 3 re-review `deleg_1fed2524`: frontend **passed**; analytics found nested
persisted values could be stringified into CSV. A sentinel regression failed
before the patch and then passed after scalar-only `_csv_cell` handling.
- Latest analytics evidence: sentinel regression **1 passed in 0.63s**; focused
slice **39 passed, 146 deselected in 7.88s**; full backend **509 passed in
73.27s**; compileall and `git diff --check` passed.
- Analytics re-review `deleg_bcb1510f`: **passed** with exact empty blocking arrays.
- Latest frontend evidence: **26/26** unit tests, production build with **1775
modules transformed**, and **15/15** isolated E2E journeys across desktop,
320×568, and 500×768.
- Final Wave 4 `deleg_97eb385c`: API-contract scope **passed** with **26/26**
frontend and **81/81** focused backend checks; final cross-cutting scope also
**passed**. Both exact verdicts had empty security and logic arrays.
- Independent local code/security gate: **8/8 scopes passed**.
- No live OAuth provider, production runtime, deployment, stage, commit, push,
reset, or stash verification is claimed.
## Git and safety state
- Branch: `main`; HEAD remains `8a632b5e6a3b67c9acd2787e1a686ac17ae69486`.
- Existing broad user WIP remains uncommitted and unstaged.
- No secrets were read or stored. No reset, stash, cleanup, stage, commit, push,
deploy, credential, permission, or production operation was performed.