Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3594f0b
test(auth): bind anonymous proof to exact session resource
seonghobae Aug 16, 2026
203109b
feat(auth): bind anonymous proof to exact session resource
seonghobae Aug 16, 2026
6c85a2f
feat(auth): expose anonymous session authorization
seonghobae Aug 16, 2026
09a2901
style(auth): apply rustfmt to anonymous authorization tests
seonghobae Aug 16, 2026
37e31af
test(auth): pin anonymous denial precedence
seonghobae Aug 16, 2026
fc4d03b
docs(auth): explain anonymous session authorization
seonghobae Aug 16, 2026
de4a7eb
feat(auth): authorize anonymous commands from loaded session
cursoragent Aug 16, 2026
d24c4c2
feat(auth): apply session commands only after anonymous authorization
cursoragent Aug 16, 2026
c55b2ce
fix(auth): classify anonymous commands from loaded records
cursoragent Aug 16, 2026
1218eed
feat(participant): persist anonymous assessment identity
cursoragent Aug 16, 2026
5bfdf39
docs(traceability): name Active PR #118 for participant persist
cursoragent Aug 16, 2026
8beaa8e
revert(participant): leave persist/reload on Active PR #114
cursoragent Aug 16, 2026
6af059b
fix(auth): tell the truth about supplied command records
cursoragent Aug 16, 2026
dc05f8c
fix(auth): satisfy clippy doc-markdown on command timeline
cursoragent Aug 16, 2026
9b02576
fix(auth): stop claiming command records were store-loaded
cursoragent Aug 16, 2026
e823650
docs(traceability): name honesty successor Active PR #144
cursoragent Aug 16, 2026
c1224b9
fix(auth): name persist landing #147 and drop leftover load claims
cursoragent Aug 16, 2026
cb33bd9
docs(traceability): name honesty successor Active PR #159
cursoragent Aug 16, 2026
5024941
fix(auth): name persist landing #158 instead of superseded #147
cursoragent Aug 16, 2026
005bbd8
fix(auth): drop closed #158 persist landing and loaded names
cursoragent Aug 16, 2026
fb8c3f8
docs(traceability): name honesty successor Active PR #225
cursoragent Aug 16, 2026
d29ca9a
fix(auth): keep anonymous authorization after rebase onto main
cursoragent Aug 17, 2026
60bc5fc
docs(traceability): drop merged Active PR leftovers after rebase
cursoragent Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target/
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable product and architecture changes are recorded here. Releases use imm
## Unreleased

### Added
- Anonymous session command authorization compares the verified actor to the supplied participant tenant/owner and session reference, then applies a lifecycle command only after that check. The command entry point does not accept a caller-built resource scope and does not claim those aggregates were store-loaded. The lower-level exact-resource check remains available for callers that already hold a stored `ResourceScope`. Participant persist/reload remains Target.
- Public `POST /v1/sessions` and `GET /v1/sessions/{session_ref}` start and reload a created session from the stored published release locked in the same transaction. `Idempotency-Key` is the durable session reference. Exact replay after later persist Suspend or Retire returns the original session; a new session after that later persist fails closed with RFC 9457 problem details that tell the buyer to publish the release.
- New assessment sessions start only from a currently published release. Durable start locks the stored `instrument_release` row (`created_session_for_start` / `start_created_assessment_session` / `start_created_assessment_session_from_stored_release`) so a stale in-memory Published object cannot insert after persist Suspend or Retire. First insert through `persist_assessment_session` takes the same lock, so a reconstituted Created aggregate cannot insert after that later persist. When that lock finds a missing or unpublished release, persist still classifies an exact stored Created row as duplicate so a concurrent retry after the first insert commits cannot turn a later Suspend or Retire into a false unpublished failure. A draft, suspended, retired, missing, or digest-mismatched stored release fails closed before a first insert when no exact stored row exists. Exact replay of an already stored start or Created row still returns the original session after a later persist Suspend or Retire, so a buyer who already started can retry. Reconstituting stored identity remains load, not start.
- Assessment-session command persist locks the created-session header row until the caller transaction ends, so a concurrent Activate-only worker cannot count a shorter history and then rewind a later Pause/Resume projection.
Expand Down
11 changes: 4 additions & 7 deletions docs/TRACEABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ An active PR, architecture document, conversation decision, or scheduler plan is
| Operation-scoped capability health | PRD §7, §13 | `docs/OPERABILITY.md` §3–4; Deployment/Operations | ADR-0011, ADR-0017 | **Implemented** domain health/readiness contract in `src/health.rs` plus `src/postgres_health.rs` PostgreSQL major/write-readiness and caller-declared relation presence; HTTP probes, measured thresholds, and deployment evidence remain Target |
| Korean/English exact locale versions | PRD §3.1, §9.9 | TRD §28; instrument release + locale governance | ADR-0013, ADR-0019 | **Partially implemented**: locale is pinned/validated by `src/instrument.rs`; actual English/Korean form content, rights, translation, invariance and serving are Target |
| WCAG 2.2 AA supported reference client | PRD §9.10 | TRD §27; Quality Attributes | ADR-0002, ADR-0013 | Target; no reference client implementation on evaluated main |
| EMA/ESM longitudinal flow | PRD §4 | TRD §16; UML longitudinal sequence; logical ERD extension | ADR-0008 | External Gyeot/TEPP dependencies + Target Commons enrollment/orchestration adapter; observation-time/membership ingest exists only on an Active PR and is not protected-main truth |
| EMA/ESM longitudinal flow | PRD §4 | TRD §16; UML longitudinal sequence; logical ERD extension | ADR-0008 | External Gyeot/TEPP dependencies + Target Commons enrollment/orchestration adapter; `src/longitudinal_observation.rs` records validity, recorded, received, and ingested clocks with explicit membership shares. Enrollment state, PostgreSQL persistence, HTTP, Gyeot collection, and TEPP kernels remain Target |
| Measurement Workbench | PRD §6 | C4/component view; UML publication-evidence sequence; Measurement Governance | ADR-0001, ADR-0002, ADR-0004, ADR-0019 | Target; fast-mlsirm/Inkspan/RankWeave are External dependencies |
| Headless replaceable clients | PRD §7 | TRD §1, §18; C4 | ADR-0001, ADR-0002 | Architecture established; public transport is Target |
| Community/Hosted/Enterprise profiles | PRD §7, §13 | TRD deployment sections; Deployment/Operations | ADR-0011, ADR-0017 | Target deployment packaging/evidence |
Expand Down Expand Up @@ -93,6 +93,7 @@ Current protected-main Rust module surface on `085ef4b4714796a77fd4645eeb46b028f

```text
src/lib.rs
├── anonymous_authorization.rs # Active PR #225 supplied-record anonymous session command authorization (not protected-main truth)
├── authorization.rs # fail-closed tenant/task authorization context and gates
├── consent.rs # purpose-specific consent + research contribution lifecycle
├── data_rights.rs # export/deletion lifecycle and retention evidence
Expand Down Expand Up @@ -128,15 +129,11 @@ migrations/
└── 0012_integration_consumption.sql
```

Still-Target logical modules/adapters include remaining product aggregate persistence/repositories, remaining public/admin HTTP and event transports, live fast-mlsirm/Keyverse/Gyeot/TEPP/semantic-data-portal adapters, research-release staging, deterministic narrative mapping, longitudinal enrollment persistence, participant identity-link history persistence, runtime health transports/metrics, and Measurement Workbench orchestration. Active PR #232 adds persist-backed session HTTP on the #218 sealed start path (`src/session_http.rs`, `openapi/sessions.yaml`); those files are not protected-main truth.
Still-Target logical modules/adapters include remaining product aggregate persistence/repositories, remaining public/admin HTTP and event transports, live fast-mlsirm/Keyverse/Gyeot/TEPP/semantic-data-portal adapters, research-release staging, deterministic narrative mapping, longitudinal enrollment persistence, participant identity-link history persistence, runtime health transports/metrics, and Measurement Workbench orchestration.

### Active implementation work that is not protected-main truth

**Active PR** #232 persist-backed session HTTP on the sealed start path is not protected-main truth until an unchanged reviewed/check-clean head is integrated. `POST /v1/sessions` and `GET /v1/sessions/{session_ref}` call `start_created_assessment_session_from_stored_release` / `load_assessment_session`. `Idempotency-Key` is the durable session reference. Exact replay after later persist Suspend or Retire returns the original session. A new session after that later persist fails closed. RFC 9457 problem details tell the buyer the next action. Command HTTP, tenant isolation, and leftover #149 / #161 / #219 / #218 / #209 / #205 / #198 remain outside this slice.

**Active PR** #60 exclusive outbox delivery-lease persistence is not protected-main truth until an unchanged reviewed/check-clean head is integrated. Pending outbox rows accept one fenced worker lease, recover expiry from the database clock without transferring the fence, reject a future caller timestamp that would steal a still-live lease, and reject stale or zero-window claims. Live side-effect execution remains outside this slice.

Protected-main `0c695b98f38369db8c80d4f8a54ab1fdb3022716` now includes #228 claim-next scoring-job poll (`claim_next_scoring_job` takes the oldest due queued or retry-scheduled row with `FOR UPDATE SKIP LOCKED`) and #235 longitudinal observation-time ingest. Those slices are no longer Active PR work on this branch. Live `fast-mlsirm` execution, scoring HTTP, a claim-next loop that calls `run_scoring_worker_attempt_with_result_snapshot`, enrollment persistence, Gyeot collection, and TEPP kernels remain later slices.
**Active PR** #86 anonymous-session resource authorization, plus follow-up #104, #118, #135, #144, #159, and honesty successor #225 that compare the verified actor to the supplied participant tenant/owner and session and apply a lifecycle command only after that check, is not protected-main truth until an unchanged reviewed/check-clean head is integrated. The command entry point does not accept a caller-built `ResourceScope` and does not claim the aggregates were store-loaded. Persist/reload of `assessment_participant` remains Target. Append-only identity-link history persist remains a later slice. HTTP transport remains outside this slice. Persist-backed session HTTP, exclusive outbox delivery leases, longitudinal observation clocks/membership, and claim-next scoring-job poll are already on protected main.

## 5. ADR traceability by concern

Expand Down
13 changes: 13 additions & 0 deletions docs/adr/0003-keyverse-identity-and-anonymous-participation.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ The mapping between operational and research identities is stored in a restricte

Keyverse claims establish authenticated subject and coarse scopes. Psychometrics Commons performs resource-level decisions for instrument administration, result ownership, research roles, data export, deletion, and release approval. A Keyverse administrator is not automatically a Psychometrics Commons research data steward.

Anonymous session commands are a product-owned gate after the short-lived proof has already been verified. This slice is an as-built library: `authorize_anonymous_session_command` / `apply_anonymous_session_command` compare the verified actor to the supplied `ParticipantRecord` and `AssessmentSession`. They do not accept a caller-built `ResourceScope`. They do not prove those records were loaded from the store. Persist/reload of `assessment_participant` remains Target. HTTP transport remains Target.

Fail-closed classification order is a product contract: trusted server time, exclusive expiry, supplied-participant tenant, session/participant ownership, actor participant, then session identity. Named tests: `anonymous_command_authorization_fails_closed_for_zero_or_expired_server_time`, `anonymous_command_authorization_rejects_compound_foreign_tenant_and_inconsistent_supplied_pair_as_cross_tenant`, and `anonymous_command_authorization_rejects_actor_when_supplied_participant_and_session_agree`.

Trusted server time and exclusive authenticator validity follow NIST SP 800-63-4 (Temoshok et al., 2025). That publication does not specify the tenant-then-owner-then-session error order.

The lower-level `authorize_anonymous_session(actor, resource, now)` check remains for callers that already hold a stored assessment-session `ResourceScope`. It is not sufficient by itself for a command against a different supplied session.

## Invariants

1. Core anonymous assessment does not require a Keyverse account.
Expand All @@ -61,6 +69,7 @@ The product does not rely on blanket PII masking that destroys operational utili

- token validation and audience-confusion tests;
- cross-tenant authorization tests;
- anonymous command-path tests that classify tenant before ownership and leave the session unmutated on authorization failure;
- anonymous-to-account linking replay and conflict tests;
- research-release joinability tests;
- account deletion/export end-to-end tests.
Expand All @@ -74,3 +83,7 @@ The product does not rely on blanket PII masking that destroys operational utili
## Reversal conditions

Revisit if Keyverse cannot meet a deployment's residency or federation requirements. A replacement must remain OIDC-compatible and preserve subject-mapping semantics without moving credentials into the product database.

## References

Temoshok, D., Proud-Madruga, D., Choong, Y.-Y., Galluzzo, R., Gupta, S., LaSalle, C., Lefkovitz, N., & Regenscheid, A. (2025). *Digital identity guidelines* (NIST Special Publication 800-63-4). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-63-4
2 changes: 1 addition & 1 deletion docs/architecture/ERD.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ The target ERD deliberately includes several logical entities that are not yet p
- Physical `assessment_session` exists only on Active PR #218 (`migrations/0014_assessment_session.sql`): Created identity (participant, release, version, digest, locale, creation time) plus a current-state projection. New sessions start only from a stored published release locked in the same transaction; first insert through `persist_assessment_session` takes the same lock; when that lock finds a missing or unpublished release, persist still classifies an exact stored Created row as duplicate; exact replay of an already stored start or Created row still returns the original session after a later persist Suspend or Retire; reconstitution is load, not start. Physical `assessment_session_command` (`migrations/0016_assessment_session_command.sql`) stores append-only command history so later states reload by replaying Activate/Pause/Resume. A shorter persist than already stored fails closed and does not rewind that projection. Command persist locks the header row with `SELECT … FOR UPDATE` before inserting or counting commands. Load reconstitutes created identity without re-checking current publication eligibility. Persist-backed HTTP create/reload sits on this start path. Protected main still has the `src/session.rs` aggregate only.
- `item_delivery_event` reflects the already-merged `src/item_delivery.rs` domain primitive; durable persistence/API orchestration is still Target.
- `consent_ledger` and `consent_event` persist the already-merged `src/consent.rs` append-only ledger. Physical persistence is carried by Active PR #49 (`migrations/0005_consent_lifecycle.sql`); HTTP consent transport and derived snapshot tables remain Target.
- `participant_identity_link` is the persistence target accepted by ADR-0020. The current `src/participant.rs` `keyverse_subject_ref` field is an application-domain first-link projection, not the future mutable persistence source of truth.
- `participant_identity_link` is the persistence target accepted by ADR-0020. The current `src/participant.rs` `keyverse_subject_ref` field is an application-domain first-link projection, not the future mutable persistence source of truth. Persist/reload of `assessment_participant` remains Target. Append-only identity-link history persist remains Active PR #52; it is not protected-main truth until integrated. Do not name closed #158, #147, #133, #114, or #124 as the current persist landing.
- `longitudinal_enrollment`, `longitudinal_observation_record`, and `temporal_analysis_submission` make the ADR-0008 Commons-owned Gyeot/TEPP orchestration boundary explicit. No TEPP analytical kernel is duplicated here.
- `integration_outbox`, `integration_delivery_attempt`, `integration_inbox`, and `integration_consumption` reflect `src/integration.rs` domain semantics. Outbox/inbox/delivery-attempt tables are on protected main. Exclusive outbox delivery-lease columns (`lease_worker_ref`, `lease_ref`, `lease_fencing_token`, `lease_expires_at`, `delivery_lease_generation`) and database-clock expiry recovery exist only on Active PR #60 until merged. `integration_consumption` pending/processing/completed/quarantined persistence is already on protected main.

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/SECURITY_AND_DATA.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ sharing_token_audience/expiry if used

Rules:

- Tenant context for state-changing requests is derived from authenticated authorization, not an untrusted body field or implicit default.
- Tenant context for state-changing requests is derived from authenticated authorization or, for an anonymous session command, from the supplied `ParticipantRecord` argument. The command gate does not prove those records were store-loaded. Persist/reload of `assessment_participant` remains Target. Append-only identity-link history persist remains Active PR #52. Do not name closed #158, #147, #133, #114, or #124 as the current persist landing. Tenant is not taken from an untrusted body field, a caller-invented `ResourceScope`, or an implicit default.
- Public opaque identifiers are identifiers, not authorization capabilities.
- Research steward, instrument publisher, participant result owner, and identity administrator are distinct authorities.
- A sharing link, if introduced, must be revocable, scoped to an exact resource/audience, expire by default, and not reveal raw responses unless explicitly permitted by the participant and product policy.
Expand Down
10 changes: 10 additions & 0 deletions docs/architecture/UML.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ Export cannot complete with a deletion-retention exception. Exact terminal repla

## 6. Anonymous assessment happy-path sequence

This sequence is target transport. The as-built command gate compares supplied records and does not perform the load.

```mermaid
sequenceDiagram
autonumber
Expand All @@ -296,6 +298,12 @@ sequenceDiagram
DB-->>A: session_ref + pinned instrument version
A-->>C: session resource + item-delivery contract

C->>A: activate session
A->>DB: load assessment_participant + assessment_session
A->>A: authorize anonymous command from supplied records
A->>DB: atomically state=Active
A-->>C: activation accepted

loop each presented item / response
A->>DB: append ItemDeliveryEvent(sequence, item version, payload digest)
P->>C: answer presented item
Expand All @@ -306,6 +314,8 @@ sequenceDiagram
end

C->>A: complete session
A->>DB: load assessment_participant + assessment_session
A->>A: authorize anonymous command from supplied records
A->>DB: atomically state=Completed + freeze ResponseSnapshot + outbox scoring request
A-->>C: completion accepted / scoring pending

Expand Down
Loading
Loading