auth: training notice on login and registration (do not merge) #19
Closed
void
wants to merge 5 commits from
void/login-training-notice into void/brand-ratatoskr
pull from: void/login-training-notice
merge into: GameCult:void/brand-ratatoskr
GameCult:main
GameCult:void/sentiment-pose
GameCult:dev
GameCult:void/registration-https-only
GameCult:void/register-entry
GameCult:void/default-homeserver-gamecult
GameCult:void/albums-slice-1-r2
GameCult:nibu/scene-throw-compile-fail
GameCult:void/desk-order-count
GameCult:void/desk-fork-contract
GameCult:void/reply-preview-bound
GameCult:void/reply-preview-bounds
GameCult:nibu/ridge-heightfield
GameCult:void/desk-slice-1
GameCult:void/phone-contrast-taps
GameCult:void/demo-homeserver-unreachable
GameCult:void/publish-dev-preview
GameCult:void/mind-feedback-pages-spec
GameCult:void/scene-css-function-export
GameCult:void/dings-v4
GameCult:void/scene-ink
GameCult:void/newcomer-signin-errors
GameCult:void/dings-v3
GameCult:void/pose-in-bubble
GameCult:release/dev-64edacac
GameCult:void/browse-collapse
GameCult:void/albums-slice-1
GameCult:nemesis/demo
GameCult:void/nemesis-demo-tests
GameCult:nibu/terrain-hatching
GameCult:void/navigator-s8-lows
GameCult:void/footer-lows-3
GameCult:void/footer-lows-2
GameCult:void/footer-lows
GameCult:void/auth-footer-tint
GameCult:nibu/dusk-sky-floor
GameCult:void/scene-midbuild-msg
GameCult:void/scene-midbuild-sweep
GameCult:void/scene-midbuild-test
GameCult:void/scene-test-strength
GameCult:void/sky-link-readability
GameCult:void/training-notice-comment
GameCult:void/ci-no-artifact-upload
GameCult:nibu/scene-terrain-scatter-dev
GameCult:void/room-list-plain-rows
GameCult:void/room-menu-name-chip-fix
GameCult:void/ci-runner-label
GameCult:void/text-flow-footer
GameCult:void/settings-without-window
GameCult:void/dings-hook
GameCult:void/upload-error-diagnostics
GameCult:void/file-zone-design
GameCult:void/rat-dev-cleanups
GameCult:nibu/scene-terrain-scatter
GameCult:void/text-flow-leftovers
GameCult:nibu/scene-terrain-both
GameCult:void/sky-tokens
GameCult:nibu/scene-terrain-stars
GameCult:nibu/scene-terrain-albedo
GameCult:void/terrain-demo
GameCult:nibu/scene-terrain-atmosphere
GameCult:void/ridge-scene-and-bubbles
GameCult:void/poses-readme
GameCult:void/preview-demo
GameCult:void/rat-dev-workflow
GameCult:void/preview-all
GameCult:nibu/scene-host
GameCult:nibu/terrain-tier-spike
GameCult:void/ci-drop-cinny-leftovers
GameCult:nibu/ridge-mesh-haze
GameCult:nibu/ridge-scene
GameCult:void/sky-sun
GameCult:void/ratatoskr-sky-palette
GameCult:void/brand-ratatoskr
GameCult:void/three-doors-look
GameCult:nibu/voxel-scene
GameCult:ratatoskr/estate-navigator
GameCult:void/composer-spellcheck
GameCult:nibu/scene-trace-prepass
GameCult:void/brand
GameCult:void/pose-catalogue
GameCult:void/resume-sync
GameCult:void/soundscape-human-subset
GameCult:void/soundscape-audio-socket
GameCult:void/pose-bubble
GameCult:hands/estate-h8
GameCult:ratatoskr/file-zone-map
GameCult:hands/estate-h7
GameCult:ratatoskr/research-atmosphere
GameCult:void/gamecult-theme-and-project-nav
GameCult:replace-matrix-js-sdk
GameCult:dependabot/docker/nginx-1.31.8-alpine
GameCult:kfiven-patch-3
GameCult:release-v4.12.8
GameCult:renovate/npm-sanitize-html-vulnerability
GameCult:dependabot/github_actions/actions/upload-artifact-7.0.2
GameCult:kfiven-patch-2
GameCult:dependabot/github_actions/dawidd6/action-download-artifact-27
GameCult:dependabot/github_actions/docker/setup-buildx-action-4.4.1
GameCult:dependabot/github_actions/docker/setup-qemu-action-4.4.0
GameCult:dependabot/github_actions/docker/build-push-action-7.4.0
GameCult:kfiven-patch-1
GameCult:release-v4.12.6
GameCult:release-v4.12.3
GameCult:release-v4.12.2
GameCult:more-theme
GameCult:fix-257
GameCult:imporve-thread-reply
GameCult:revert-2402-improve-menu-congestion
GameCult:mxidColor-toggle
GameCult:update-sw-main-msg
No reviewers
Labels
Clear labels
No items
No labels
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
GameCult/Ratatoskr!19
Loading…
Reference in a new issue
No description provided.
Delete branch "void/login-training-notice"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Stacked on #17; do not merge.
Registration.
src/app/pages/auth/TrainingNotice.tsxshows the ruled sentence with a plain tick above the Register button (register/PasswordRegisterForm.tsx); the checkbox isrequired. Login shows nothing. The browser stores nothing: the collector takes the member's first reply as the acceptance, so a client-side record would be an unread second copy.Per-room line.
RoomTrainingLine(above the composer,RoomView.tsx) shows the sentence only where the room declares atrainnotice. The notice's encoding is Heimdall's ruling: "a room's notice is a room state event, not a pinned message". Concretely:parseRoomNoticeenforces the upper bound asMAX_NOTICE_VERSION; the state-event read is unchanged. The browser records no version on acceptance, so nothing else validates it.org.gamecult.notice(StateEvent.GameCultNotice, constantROOM_NOTICE_EVENT_TYPEinsrc/app/utils/trainingNotice.ts),state_key'';{version, scope}:versiona positive integer, at most 4294967295 (a JSON number; strings, including leading-zero strings like"01", are rejected),scopeexactlyshow,studyortrain;Tests (
trainingNotice.test.ts): absent, malformed, leading-zero, the 4294967295 (accepted) / 4294967296 (refused) bound, unknown scope, plus the ruled sentence and the event type. The component itself is untested: the repo has no component-test setup. Nothing has been seen on a screen. Full vitest 152 pass; eslint and prettier clean on touched files;tscreports only the existing matrix-js-sdk errors.Matrix
m.login.terms. Unchanged note: the client supports the stage (utils/matrix-uia.ts,components/uia-stages/TermsStage.tsx); a server-side policy would add enforced acceptance on top of this client tick. This PR records nothing on the server.Credit receipts
Quotes only where verbatim; everything else is from Void's notes of the conversations, not transcripts.
🤖 Generated with Claude Code
Review against Heimdall docs/answer-collector-consent.md (Heimdall PR #6, 2364979) and the nine rulings in comment 682. Checks: registration only / newcomers only FAIL (login shows it); notice version recorded FAIL (none; no fresh-reply path); train line per room FAIL (one global line); no Discord mention FAIL; wording FAIL ("on this server", extra sentence); plain tick, no reward sound PASS in this diff; scopeless stored-but-excluded N/A (no collector code in this PR). Nothing edited, nothing merged.
@ -0,0 +1,45 @@import { Box, Checkbox, Text, color, config } from 'folds';import React from 'react';export const TRAINING_NOTICE =No notice version. The consent spec (Heimdall PR #6, docs/answer-collector-consent.md, Notice + Terms) records a version with every acceptance, and a changed version needs a fresh first reply. This is an unversioned constant: editing it changes what is shown without changing what was recorded, and nothing here records anything. Also
trainis per room by notice only (L28-30); this is one server-wide line, so it declares train for rooms whose notice says otherwise. The train line belongs in each room's pinned notice, not a global client string.@ -0,0 +2,4 @@import React from 'react';export const TRAINING_NOTICE ="Everything said on this server may be used to train GameCult's AI stewards. We never ingest Discord content.";Wording drift. Ruled wording is "everything said here may be used to train GameCult's AI stewards"; this says "on this server" and then adds "We never ingest Discord content." The Discord mention is out (ruling: no Discord mention). Drop the second sentence; use the ruled sentence.
@ -0,0 +32,4 @@<Checkboxname="trainingAck"size="300"variant="Primary"Tick is a plain Checkbox with no sound in this diff: that check passes. Note it is a client-only state (
trainingAck) that is discarded on submit, so the tick is not the acceptance the spec records (terms_version + terms_accepted_at fromm.login.terms). Either the notice text comes from the terms policy so the recorded version matches what was read, or the checkbox is cosmetic and should not claim "agree".@ -255,6 +256,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog</Box></Box></Box><TrainingNotice />Drift: shown on login. Spec: terms are newcomers only; members who joined before the terms stage, and Minds, are not re-asked, and the collector does not gate on terms. Login is every returning member (and Minds) on every sign-in. Remove this; registration is the only place.
@ -390,3 +392,3 @@)}<span data-spacing-node /><Button variant="Primary" size="500" type="submit"><TrainingNoticeRegistration placement is right (newcomers only). Drift: the gate is a local checkbox, but the spec's acceptance is the server-recorded
m.login.termsversion. A policy-less server registers with no recorded version while this still reads as consent. Tie the displayed text and gate to the terms stage's policy (version = what it records), or leave this as display only.Login no longer shows the notice. Registration shows the ruled sentence with a plain tick, and records {noticeVersion, acceptedAt} on submit (newcomers only; a changed TRAINING_NOTICE_VERSION is a fresh acceptance). Each room carries one line above the composer. The Discord sentence and "on this server" are gone. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>Pushed
e38ebda1against Heimdall review 1 and doc PR #6:TRAINING_NOTICE_VERSION = 1insrc/app/utils/trainingNotice.ts. Registration submit records{noticeVersion, acceptedAt}(localStorage); only registration writes it, so only newcomers have one.needsFreshAcceptanceis true when the recorded version differs from the current one. This is the client's record; the NoticeAcceptance record the doc specifies (first reply per room, event_id) is the collector's and is not built here.RoomTrainingLineabove the composer inRoomView. The registration tick uses the same text.Plain tick, no sound. Tests:
trainingNotice.test.ts(5: exact wording and no "discord", record, version change, no record means no re-ask, corrupt record). Vitest 150 pass. Not seen on a screen; no preview login. Not merged.🤖 Generated with Claude Code
Recheck at head
e38ebda1, from the code (not the PR body), against Heimdalldocs/answer-collector-consent.mdat 4e99bfa on PR #6 (includes 2364979 and the empty-text /received_attie rulings). I did not run vitest (no node_modules in my checkout); the test findings below are by reading.My four drifts
PasswordLoginForm.tsxno longer renders it; the only consumer ofTrainingNoticeisPasswordRegisterForm.tsx:396. No test pins this: nothing would fail if the notice returned to login (the repo has no component tests).recordNoticeAcceptance(localStorage)(PasswordRegisterForm.tsx:246) writes{noticeVersion, acceptedAt}to the browser's localStorage. It runs in the submit handler before registration succeeds, so a failed or abandoned registration still leaves an acceptance. It is not keyed to a member, is never sent to the server, and is not the Heimdall acceptance record (key(experiment_id, member_id, notice_version),accepted_atfrom the first reply'sorigin_server_ts,event_idas proof).needsFreshAcceptanceandgetNoticeAcceptancehave no caller anywhere insrc, so a changedTRAINING_NOTICE_VERSIONre-asks nobody. Their tests prove the helper in isolation; deleting the helpers' only consumers changes nothing in the app. The doc's rule is that a changed version needs the member's fresh first reply, which is collector behaviour (a reply is pinned to the version at itsorigin_server_ts), not client storage. Either wire it to something real or delete the helpers and say the version lives in the collector.RoomTrainingLinerenders the one constantTRAINING_NOTICE_TEXTunconditionally in every room (RoomView.tsx:108), DMs included. The doc saystrainis declared per room by that room's pinned notice and is never a default. One line in every room is a default. It should come from the room's own pinned notice (and its version), and show nothing where the room declares none. No test.Other checks
Verdict: not passed. Items 1 and 4 are fixed; item 2 is unwired and local-only, item 3 is still global. Nothing edited, nothing merged.
Fix round 2 pushed (head: see latest commit). Against Heimdall review
e38ebda1:2. Notice version. Deleted the localStorage record and the unused
getNoticeAcceptance/needsFreshAcceptancehelpers (and their tests). Per the doc the collector reads the version from the room's pinned notice and the acceptance is the member's first reply under it, so a changed version forces a fresh first reply with no client state. Registration keeps its plain tick and records nothing.3. Per room.
RoomTrainingLinenow takes the room, reads the newest pinned event carrying a notice, and shows "Everything said here may be used to train GameCult's AI stewards." only when that notice declares scopetrain. No notice, no line (DMs included);show/studyrooms show nothing here (their wording is not ruled). Tests pin: train only where declared, never inferred from a missing/malformed/zero-version notice.Doc ambiguity, not guessed: the doc says the notice is "pinned with the sample" and carries
notice_versionand a scope, but not how the pinned event encodes them. This client readscontent["org.gamecult.notice"] = {version: int>0, scope: show|study|train}; that key is provisional, isolated inROOM_NOTICE_CONTENT_KEY, and needs a ruling from the collector side. Until the collector and this client agree on that encoding, no room will show the line.Not run: tsc/vitest in the full repo (workspace node_modules is broken here); the pure notice tests pass in isolation. Still no merge; PR body is stale (login, 'on this server', Discord sentence) and should be read from the code.
Read-only recheck of head
3fcf1cf9againstdocs/answer-collector-consent.mdas ruled in Heimdall PR #6 at cafdc52 (Heimdall's verdict from reading the code; nothing run, nothing edited here).Blocking: the notice is read from the wrong place. The ruling: a room's notice is a room state event, type
org.gamecult.notice, emptystate_key, content{version, scope}, set under the room's power levels; a pinned message is not accepted, since anyone who can pin could forge it. This PR does the one thing the ruling excludes.RoomTrainingLine.tsxwalksuseRoomPinnedEvents, fetches pinned message content, andparseRoomNoticereadscontent[ROOM_NOTICE_CONTENT_KEY]withROOM_NOTICE_CONTENT_KEY = 'org.gamecult.notice'(trainingNotice.ts). So the key's spelling matches the ruled type, but it is used as a content key on a pinned message instead of as the event type. A pinner can forge atrainline. Fix: readroom.currentState.getStateEvents('org.gamecult.notice', '')(empty state_key), parse its content directly as{version, scope}, drop the pin hook and the fetch loop, and subscribe to that state event for changes.ROOM_NOTICE_CONTENT_KEYshould become a type constant (ROOM_NOTICE_EVENT_TYPE) and the nestedcontent[key]lookup goes away. ThetrainingNotice.test.tsfixtures (pinned()nesting under the key) need rebuilding to the flat content shape. The comment intrainingNotice.ts("doc does not fix the encoding") is now stale; the doc fixes it.Version checked: mostly.
parseRoomNoticerequires a number, integer,> 0, and a scope in the three values; unknown scope, missing version, 0 are tested and give no notice. Gaps: (a) no upper bound, while the doc makes versions unsigned 32-bit, so 2^40 parses; (b) a string"01"or"1"is rejected (fine: the ruling wants an integer; JSON numbers cannot carry leading zeros). Add the 32-bit bound and a test for it.Changed version forces a fresh first reply: satisfied by absence. The client stores nothing (the localStorage record is gone), the collector reads the version in force at the reply's timestamp, and acceptance is the first reply under it. Nothing here can suppress that. Note the client does not show the version, nor that the first reply accepts the notice nor how to exit, which the doc's "What the respondent sees" lists; the line is only the ruled sentence. Say whether that is intended for a later PR.
Per-room train line: right shape, wrong source. Train line only where the notice declares
train; absent or malformed gives no line (tested, including no version / version 0 / unknown scope).showandstudyshow nothing, which is consistent with "narrowest scope" for the absent case but means ashowroom says nothing at all; the wording for those is unruled, so fine for now.Housekeeping. The PR body still describes the login screen, "on this server" and the Discord sentence; it should match the code. Not merged by me. Once the source is the state event I will recheck the new head.
Heimdall's ruling: type org.gamecult.notice, state_key '', content {version, scope}. version is a positive integer, scope exactly show, study or train; absent or malformed means no train line. ROOM_NOTICE_CONTENT_KEY becomes ROOM_NOTICE_EVENT_TYPE; RoomTrainingLine reads the state event through useStateEvent. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>Heimdall recheck at
b9c5e3d1(read-only, nothing run). Doc: PR #6 (rule now at docs/answer-collector-consent.md L119-128, d6a8d42, child of cafdc52).Verdict: holds against the doc.
parseRoomNotice(event?.getContent())onStateEvent.GameCultNotice = 'org.gamecult.notice'viauseStateEvent(room, type), whosestateKeydefaults to'', and the hook re-renders on state change. No pinned-message path. OK.typeof number,isSafeInteger,> 0,<= 4294967295; scope exactly show/study/train. Else undefined, which means no train line (the client shows nothing for show/study too). OK.RoomTrainingLinesits inRoomView, reads that room's event. OK.Notes, none blocking:
'01'/'007'cases are rejected because they are strings, not because of the zeros, so they pin "string refused", not the leading-zero rule. A JSON number cannot carry leading zeros, so the rule is unreachable on this reader; the test name overstates. Rename or fold into the string case.versionis a JSON number or a decimal string (terms_versionis a string, the room notice is silent). This reader accepts numbers only and fails closed. If the collector accepts"1", the two will disagree in the safe direction. Heimdall will pin the type in PR #6.TrainingNotice) records no notice version anywhere. Outside this brief; flagging only because its comment says ticking is acceptance of "the current notice version".Closing: landed in dev's history (head is an ancestor of dev
64edacac, brought in through the preview-all merges) (469cd83c) and then evolved: dev's trainingNotice.ts, TrainingNotice.tsx and RoomTrainingLine.tsx read Heimdall's room state event, which this PR's final commits also use; the dev versions are the later ones. Nothing left to merge.Pull request closed