R7 adds a research mode: the infrastructure needed to run a randomised controlled pilot of RewindSec without contaminating the ordinary training experience.
It is infrastructure only.
RewindSec does not claim efficacy from the existence of this infrastructure. Learning-effect claims require data collected under an appropriate approved study protocol.
Research-mode enablement is not a substitute for institutional ethics review, participant consent, or study registration where those are required.
Enabling research mode is an operational setting and does not constitute ethics approval, consent, or study registration.
No participant has been recruited, no approval has been obtained, and no approval number, consent text or registration identifier appears anywhere in this repository. Nothing in the application computes a p-value, an effect size, a significance test or an "improvement" label.
Does deterministic counterfactual replay plus structured self-explanation improve protective transfer behaviour compared with a conventional debrief and with factual-consequence simulation?
R7 makes that question testable. It does not answer it, and no part of the software behaves as though it had been answered.
The protocol is rewindsec_phishing_pilot, version 1, and its single source
scenario is phishing_credential_compromise.
That restriction is deliberate:
- all three intervention arms can be implemented on it without Docker, so the pilot can run wherever a browser can;
- every participant can be shown an identical initial scenario, message and decision;
- the factual first decision is therefore measurable identically in every arm, before any intervention exists;
- the immediate transfer probe (
quishing_portal_qr) already exists and is built on the same underlying principle.
Ransomware, MFA fatigue and BEC are not part of this protocol. They remain full RewindSec demonstration modules, and no human-study claim may be made about any of them.
Everything in the data model is keyed by (protocol_key, protocol_version), so
a second source scenario can later be added as a second protocol without
rewriting a table, a route or an export column.
Stable keys, used throughout the code, the database and the export. The learner-facing UI never names one, and the words "control" and "experimental" appear nowhere in the participant flow.
| key | intervention |
|---|---|
awareness_debrief |
first decision → concise static awareness debrief |
factual_consequence |
first decision → the learner's own response executed and shown → concise authored debrief |
counterfactual_replay |
first decision → factual consequence → exact rewind → learner-chosen alternative → paired comparison → structured self-explanation |
After the first decision the participant sees a short authored page stating the three principles the scenario turns on: inspect the request, verify through an independently trusted channel, never present credentials to an unverified request.
The consequence adapter is not constructed and not applied. No branch state, no simulated account access, no rewind, no alternative decision, no state diff and no structured reflection appear. It is the conventional awareness condition, and it is deliberately a real conventional debrief rather than a weakened one — an arm built to lose would not be worth comparing against.
No TrainingExecution row exists for this arm.
The real PhishingConsequenceAdapter runs from a verified S0:
prepare → capture baseline → apply the learner's own action → capture factual result
The resulting state is shown, followed by the same authored debrief Arm A receives. There is no rewind, no alternative execution, no paired counterfactual and no state diff.
Refreshing does not re-apply the action: the intervention row's
factual_result_digest is the idempotency gate, so what the participant sees
cannot drift from what was recorded.
No TrainingExecution row exists for this arm either.
The real RewindSec mechanism, reused unchanged. After the same first decision
and the same factual consequence, the participant chooses an alternative, and
the R2 TrainingService runs the pair over the R1 CounterfactualRuntime. Both
staged digests are passed through, so the service fails closed if the
authoritative run does not reproduce exactly the baseline the participant
started from and exactly the factual outcome they were shown.
The comparison is then displayed and the existing R6 structured
self-explanation is recorded, producing a LearningReflection and its derived
ConceptEvidence.
Exactly one TrainingExecution row exists for this arm, and the same six
TRAINING_* lifecycle events are emitted, once each.
TrainingExecution means one paired counterfactual execution. Arm A
executes nothing and Arm B executes one branch; neither has a pair. Writing a
row for either would have made the table's name false and would have quietly
broken every query and test that relies on its meaning.
StudyIntervention exists instead. Which of its columns are populated is the
arm difference, recorded explicitly rather than inferred.
Every arm sees the same fictional organisation, the same message, the same four choices in the same order, the same confidence control and the same visual treatment. The arm is not consulted anywhere on that page, and the decision is persisted before any arm-specific content is resolved.
Recorded once, and never revisable: choice_id, the raw confidence 0..100,
the server-measured response_time_ms, the authored response quality (derived
server-side, never submitted) and the concept tags.
This is the pre-intervention behavioural measure. Nothing later — not the counterfactual choice, not the reflection, not a transfer response — is treated as the baseline.
All three arms answer the same probe, quishing_portal_qr, immediately after
their intervention. It is reused from learning.transfer rather than copied,
so the instrument is literally the same one the ordinary flow uses.
No feedback is shown afterwards. The completion page says only "Response recorded." Revealing the response quality, the protective answer or the security principle would make the measurement a further training intervention and would contaminate the retention probe that follows it. The ordinary, non-study R6 probe keeps its full feedback; the suppression exists only in study mode.
Set from the moment the immediate response is recorded:
retention_open_at = immediate_completed_at + 7 days
retention_close_at = immediate_completed_at + 14 days
The 7–14 day window is an authored study protocol choice, not evidence that this interval is optimal. Neither figure is calibrated against anything.
Submission is impossible before the window opens and after it closes; inside it exactly one attempt is accepted, and a repeated POST returns the stored first response rather than replacing it. The boundary is inclusive at both ends, so a participant arriving on the instant the window opens is admitted.
smishing_account_notice, version 1 — study-only, and deliberately not
registered in learning.TRANSFER_PROBES, so it is unreachable from the ordinary
R6 transfer routes.
A fictional mobile-message notification claims an account action is required urgently and asks the participant to sign in through the route the message supplies. It tests the same underlying verification principle on a third surface: an email in training, a printed QR code immediately after, a phone message later.
| choice | display | quality |
|---|---|---|
follow_message_and_sign_in |
Use the message route and sign in | RISKY |
inspect_message_details |
Inspect the message details first | PARTIAL |
open_official_service |
Open the known official service independently | PROTECTIVE |
report_suspicious_message |
Report the suspicious message | PROTECTIVE |
Concepts: independent_verification, credential_exposure,
channel_switching.
There is no SMS, no external URL, no clickable destination, no login, no credential form and no network call. The page is static authored simulation.
This is a retention transfer probe. It is not called "far transfer": whether two surfaces are near or far is a judgement about the surfaces, and nothing here is entitled to make it.
Balanced, deterministic, permuted blocks of six — two of each arm per block.
Within a block the order is a permutation derived by keyed HMAC-SHA256 from
(secret, protocol_key, protocol_version, block_index), applied by Fisher–Yates
with rejection sampling over the fixed multiset. The contents of every block
are fixed by construction, so a bug in the shuffle can misorder a block but
cannot unbalance one.
Three properties follow:
- Balanced — every complete block is 2/2/2; a run stopping mid-block is at worst two participants away from balance.
- Unpredictable to a participant — without the secret the next slot's arm is not derivable from the arms already issued. A learner cannot request one, guess one, or submit one: no form field, query string, header or cookie value is consulted by the allocator.
- Reproducible and auditable — given the secret and the slot numbers, the whole sequence can be recomputed later. That is what makes an allocation auditable rather than merely unrecorded.
The allocation is written once at enrollment and never rewritten. No route
updates arm_key or allocation_slot; every /study/admin route is read-only.
The slot is claimed by insertion, not chosen after a count. A unique
constraint on (protocol_key, protocol_version, allocation_slot) means two
simultaneous enrollments computing the same next slot collide; the loser rolls
back, recomputes and retries. Count-then-choose without the constraint would
hand both participants the same slot, the same arm, and an unbalanced block.
REWINDSEC_STUDY_ASSIGNMENT_SECRET
Mandatory when research mode is enabled; the flow fails closed with a 503
without it. It is deliberately not Flask's secret_key: that key has a
documented random development fallback, and it is rotated for reasons unrelated
to the study. An unkeyed permutation would make the sequence public; a random
fallback would make it irreproducible. Both are disqualifying.
Research mode is off by default.
REWINDSEC_STUDY_ENABLED=1
REWINDSEC_STUDY_ASSIGNMENT_SECRET=<high-entropy value>
REWINDSEC_STUDY_ACCESS_CODE=<code given to participants>
With it off, every /study route returns 404 — not 403: a deployment that has
not been configured for research should not advertise that one could exist. No
enrollment can be created, and the ordinary training and learning flows behave
exactly as before.
With it on but either secret missing, the flow serves 503 and creates nothing.
The access code is an access gate and nothing more. It is compared in constant time, never stored on an enrollment row, never rendered, and never logged. Its purpose is to stop an ordinary learner wandering into a research protocol.
No personally identifying information is collected at any point. There is no name, email address, student id, phone number, college registration number, date of birth, gender or demographic field anywhere in the study tables, and none is needed by this protocol. There is no IP address, geolocation, user-agent fingerprint or device identifier: analytics of that kind are not added.
A participant is a UUID4 (participant_id) and an allocation slot.
Every value stored from a form is an authored identifier or a bounded integer. There is no free-text input anywhere in the study flow, so a participant cannot write personal information into the research record even by accident.
StudyEnrollment.session_id exists for authorisation only — it says which
browser session currently owns the enrollment — and is absent from the
research export.
The retention probe opens a week later, and a browser cookie frequently does not survive that. Without a way back, retention attrition would be an artifact of cookie lifetime rather than of anything being measured.
At enrollment the participant is issued one high-entropy opaque code
(secrets.token_urlsafe(24), 192 bits) and shown it once. Only a keyed
HMAC-SHA256 digest is stored. POST /study/resume re-binds the current browser
session to the existing enrollment.
- The code is derived from nothing — not the
participant_id, not thesession_id, not the slot, not a clock — so possessing one reveals nothing about the participant it belongs to. - Resuming changes
session_idand nothing else. Theparticipant_id, the arm, the slot, the first decision, the intervention and every recorded attempt are untouched. - The raw code is never persisted, never logged, never placed in a query string
or a URL, never echoed back into a page, never written to the session and
never exported. Resume is POST-only so a code cannot reach browser history, a
proxy log or a
Refererheader. - Because only a digest is kept, a lost code cannot be recovered or resent. That is the cost of it being unable to identify anyone.
It is a pseudonymous continuity credential, not personal data.
Progress is stored on the enrollment and advanced only through the authored per-arm progression:
awareness_debrief enrolled → source_decision_recorded → intervention_completed
→ immediate_transfer_completed → retention_waiting
→ retention_completed
factual_consequence … → source_decision_recorded → factual_preview
→ intervention_completed → …
counterfactual_replay … → source_decision_recorded → factual_preview
→ counterfactual_completed → reflection_completed
→ intervention_completed → …
Only the immediate successor is a legal transition. A phase an arm does not list is not reachable for that arm at all, which is why cross-arm contamination is a phase error rather than a template accident: an Arm A participant asking for the comparison page is asking for a phase Arm A has never had.
There is no phase field in any form, no phase in any URL, and nothing a browser submits names one. Typing a later route redirects the participant to wherever they actually are.
Baseline behaviour is the factual first decision, stored once.
Outcome variables made available for analysis:
- primary — immediate transfer response quality;
- secondary — retention transfer response quality, response times, raw confidence readings, occurrence of a risky response given with high stated confidence, factual first-choice quality, and (Arm C only) whether the selected structured explanation was the preferred one.
The application computes none of the statistics. No significance test, no p-value, no effect size, no causal claim and no "learning improvement" label is produced anywhere in the code, the dashboard or the export. The software stores and describes observations; analysis belongs in the paper workflow, after real data collected under an appropriate approved protocol exists.
Missingness is represented, never imputed. A participant who never returned did not answer riskily — they did not answer. A missing retention attempt, an expired window and an incomplete intervention are each reported as themselves, and an unobserved measurement exports as an empty cell, never as a zero and never as a response quality. Collapsing the two would manufacture the study's own outcome variable.
/study/admin — instructor-authenticated, read-only, descriptive counts
only: enrolled totals and per-arm counts; intervention, immediate and
retention completion; retention due, completed and expired; baseline, immediate
and retention response-quality counts by arm; and counts of risky responses
given with high stated confidence.
It includes a participant flow table — assigned → completed intervention → completed immediate transfer → eligible for retention → completed retention — as descriptive operational reporting.
No name, no raw session_id (removed from the context by display_dict(), not
merely omitted by the template), no return-code digest, and no statement that
one arm did better than another.
/study/admin/export.csv — instructor-authenticated, one row per
StudyEnrollment, in a fixed column order declared once in
StudyService.EXPORT_COLUMNS and asserted by test.
participant_id is the research correlation identifier. The instructor HTML
label (P-XXXXXX) is a display artifact and is deliberately not a join key.
Deliberately absent: the raw Flask session_id, the return-code digest, the
access code, IP addresses, user agents, credentials and any learner-authored
text. The internal evaluation APIs return a canonical session_id because the
formal harness joins on it; a research export has no such need.
The file is written by the csv module rather than by string concatenation, so
quoting and escaping stay correct by construction.
SecurityEvent remains the single general event timeline, and R7 adds no
STUDY_* event types.
The study artifacts already carry the timestamps a study needs —
created_at, intervention_started_at, intervention_completed_at,
immediate_transfer_completed_at, retention_open_at, retention_close_at,
retention_completed_at, and a created_at on every attempt. A parallel stream
of a dozen STUDY_* events would restate them in a second place that could
disagree, and would recreate exactly the parallel analytics system Milestone 3
removed once already.
Arm C still emits the ordinary six TRAINING_* lifecycle events, because it
runs an ordinary paired execution. Arms A and B emit none, because they run no
pair — and that asymmetry is itself an accurate record of what happened.
No participant recruitment. No claim of ethics approval, consent or
registration. No collection of names, emails or demographics. No p-values,
effect sizes or significance tests. No claim of learning improvement. No "far
transfer" label. No adaptive training. No LLM anywhere. No free text. No fourth
arm. No second source scenario. No change to Docker containment, the ransomware
mechanics, or the normal MFA/BEC training flows. No rewrite of
CounterfactualRuntime.