Hub CI passed on 651f370a7ca601913597517d875418c7196b8057 (vet, fmt, test, migration-check). Not merged, not deployed. Waiting for the independent review.
Move Memoria onto rastrillo fb806a2: refusal hook and confirm-on-click links open
On branch chore/rastrillo-confirm
Pins amadan.net/rastrillo/rastrillo at fb806a2. It was at 29e4491 (2026-09-08). This brings in rastrillo #63 (auth.Config.Refused), #64 (magic-link confirm), the strict CSP and UTC stored times. Head: 651f370a7ca601913597517d875418c7196b8057. Same shape as oficina/sheets 6d700d6 (#46).
Changes
- ErrProofUnset. Sets
ProofOff: true, which matches today: Memoria never had proof-of-work on sign-in. - #63, the refusal hook. Before this, a verified address that is not admitted got rastrillo's bare-text 403. Now
app.refusedrenders Memoria's door inside that 403, with "That address can't sign in to Memoria…" in the error callout and the address form as the way on. -?err=refusedcannot summon the sentence. -beginGuard(gate 33) is unchanged. - #64, confirm on click. -
app.renderConfirmdraws the estilo door with a lede and one Sign in button that posts the token back. There is no address field, and the form is in templates/signin.html. - auth's no-store,Referrer-Policy: strict-origin, X-Frame-Options, X-Robots-Tag and the addedframe-ancestors 'none'all survive, and a test checks them. -POST /auth/verifyis mounted standalone and, under LocalGuard, in Home mode. - *
NewAuthtakes `app**, because auth now needsAdmit,refusedandrenderConfirm` from it. app.ServeOptionssets the router and error page, and main uses it. It sets no CSP. Standalone serves rastrillo's strict default unchanged, because nothing Memoria draws or scripts writes an inline style, so nothing needed migrating. - In Home mode, connect.FormPolicy keeps'unsafe-inline', which estilo's Home-only home_link.html needs for onestyle="overflow-wrap: anywhere". -TestHomeCSPPreservesPinnedFrameworkDirectivesnow allows exactly that one style-src difference.- tokens.css is re-copied from the pinned module.
doctor --fixrefuses while the old path is required by native.go. Credits are regenerated withmake credits. - AGENTS.md now records the doctor behaviour and the CSP rule.
Tests
- The link-following helpers now open the confirm door, read the token off it, and POST it with Origin and
Sec-Fetch-Site: same-origin. This covers the Home fixture (homeRedirect users, nativeRigs) and the memoriatest client (signedInClient, the deep-link test, the admission gate, the dead-link outcome). TestEmailedLinkOpensTheConfirmDoorruns through the production stack. Two GETs mint nothing; a cross-site POST and one with no Origin both get 403 without spending the link; the door's POST signs in and opens the index; a replay gives?err=expired.TestHomeLocalLinkOpensTheConfirmDoorcovers the same in Home mode, plus LocalGuard on the POST.TestTheConfirmDoorKeepsEveryPolicychecks the headers, both CSP values and token escaping.- The admission gate now asserts the refusal is the door, and that the admitted path mints a session.
TestKeymailRefusalIsTheDoorbegins a Keymail sign-in as an owner and has it come back after the owner was removed: 403 door, no session. The admitted half's session opens the index. The fake Keymail now does the token exchange.TestStandaloneServesRastrillosDefaultCSPandTestNoPageCarriesAStyleAttributerun in CI, over the markup of signed-out and signed-in pages.TestPagesRenderUnderTheProductionCSPis a Chromium test throughrastrillo.Handler, andmake browsernow runs it. It records CSP violations, and also writes one inline style to prove the policy refuses it.- Native sign-in (native_exchange) passes.
- I broke the code on purpose to check each new test fails. All went red, and all were reverted: removing Refused or RenderConfirm, a 200 refusal, dropping or unguarding the POST mounts, header overrides in renderConfirm, a looser CSP, and
style=added on<main>, the index heading and the confirm form. I chose all of these myself, so the independent pass AGENTS.md requires is still owed. GOTOOLCHAIN=go1.26.5 make ciandmake browserare green locally.
Stored times (UTC)
- SQL on DATETIME columns: - The only SQL on a DATETIME column is
Order("updated_at desc")in store.go, the documents list. Old rows are… +0000 UTCand new rows…+00:00, all UTC. The order is still right to the second, so no back-fill is needed. - The breaker window and the 8-day tally retention compare UTCYYYY-MM-DDTEXT, so they are unaffected. The CountingSince checks are Go-side after.UTC(). The sync heartbeat staleness istime.Since, which is instant-based. Custody starts counting attime.Now().UTC(). - No julianday, strftime or datetime appears anywhere. - Writes: every explicit write was already
time.Now().UTC(), GORM's stamps are UTC, and no parsed or external time is stored. Notion's edited time is TEXT and compared only for equality. - Displays changed: a document's Updated, the Notion connector's Verified, and a run's Started and Finished printed a clock time with no zone. The driver now reads times back in UTC rather than the host zone, so on a non-UTC host those values changed meaning. They now go through
stamp(), which converts to UTC and says "UTC", as the sources page already did. The sources line now also converts with.UTC()first. -TestStampSaysUTCandTestRunTimesSayUTCcover this, and they fail against the old formatting.
Not done
- Review: the independent review is being arranged. Merge and deploy: neither has happened, on purpose.
- The design spec still calls
GET /auth/verify"the landing". tools/drive does not show the sign-in pages.
GPT-6-Astra (Codex), reviewing for session 21a32889.
VERDICT: CHANGES-REQUIRED
Reviewed the whole branch at 651f370a7ca601913597517d875418c7196b8057 against fetched origin/main (d7777f713a6002278c254bc4e7f3a0bc387e04c6), after reading this write-up, REVIEW-BRIEF.md and the branch's AGENTS.md. Work was confined to detached scratch worktree /tmp/memoria-review-21a32889. No branch edits, commits, pushes or merges. All temporary mutations and diagnostic tests were reverted; the worktree is clean.
- P2 — The strict standalone CSP strips all styling from native sign-in approval.
internal/memoria/serving.go:14–24,go.mod:25, and the page inventory ininternal/memoriatest/csp_browser_test.go:158–193. The retainedamadan.net/rastrillo/native/server@…2a5d07b2c8d3renders/native/authorize?id=…fromauth.go:248, whose entire stylesheet is an inline<style>block. The old default allowed it; fb806a2's default blocks it. This route is mounted in Memoria but omitted from both new page inventories; the native exchange tests use the bare router and never check browser rendering. I temporarily added a real/native/requestfollowed by/native/authorizeto the production-CSP browser test, without changing production code.GOTOOLCHAIN=go1.26.5 make browserthen failed withthe production CSP refused: style-src-elem inline. I inspected the screenshot: the approval card, colours, spacing and styled button are gone, leaving browser-default text and a small default button. The completed approval uses the same template. Fix: make native/server's approval template compatible with the strict CSP (prefer an external stylesheet, then bump its pin), and exercise native approval/completion under the production stack in the browser test. Keep the strict policy rather than restoring global unsafe-inline. The claim that no standalone page relies on inline styles needs to include this dependency-rendered page.
Independent mutation pass (2026-10-06). Nine isolated production-code mutations, selected and run by this reviewer; seven were caught, two survived. Every run used GOTOOLCHAIN=go1.26.5 make ci and, where listed, make browser; no bare go test.
| Mutation | Result | | --- | --- | | Set the refusal's error only for GET, losing it on magic-link POST | CI red: admission/refusal gate | | Make refusal Cache-Control public/cacheable | CI red: admission/refusal gate | | Have shared renderStatus overwrite Referrer-Policy with strict-origin-when-cross-origin | CI red: confirm-header test (also existing page-header tests) | | Overwrite that header only when renderConfirm runs with Home configured | CI and browser green — note below | | Replace standalone POST redemption with a redirect back to GET | CI red: confirm flow, native exchange/expiry/revocation, admission and deep-link tests | | Call Home LocalGuard but fall through into Verify after its rejection | CI red: Home confirm flow | | Append style-src-attr 'unsafe-inline' to the confirm page's baseline CSP | CI and browser red: exact policy assertions and live inline-style probe | | Have chrome.js write a style attribute at load | CI red: vendored-script identity; browser red: actual CSP violations | | Have search.js assign main.style.outline on pages carrying rst-search | CI and browser green — note below |
Notes / coverage hardening:
internal/memoriatest/signin_confirm_test.go:190checks confirm headers only through standaloneservedApp;home_confirm_test.gochecks the Home flow but not these headers. Add the same header assertions through the production Home stack, expecting connect.FormPolicy plus the separate frame-ancestors policy. The Home-only mutation above weakens protection of the live token URL against same-origin Referer logging without either suite noticing. This is a demonstrated coverage gap; I did not find that overwrite in the submitted production code.internal/memoriatest/csp_browser_test.go:123–134observes CSP violations, which does not enforce the broader claim that scripts never write inline styles. A directelement.styleproperty assignment survived both suites. Inspect the live DOM for[style]after load/interactions (before the deliberate probe), or narrow the claimed guarantee. The current app scripts themselves contain no such writes; this is a guard limitation, separate from the real native<style>regression above.
Other checks:
- Baseline
GOTOOLCHAIN=go1.26.5 make ciandGOTOOLCHAIN=go1.26.5 make browserpassed. After reverting the diagnostic browser extension, browser passed again. CI also passed with a temporary independent old/new timestamp compatibility probe, subsequently removed. beginGuardis unchanged. Refused magic-link and Keymail identities get the 403 door without a session; admitted identities get usable sessions. POST mounts, GET-does-not-spend, replay rejection, escaping, standalone confirm headers and LocalGuard behaviour checked out.- Native exchange, expiry and revocation tests pass for standalone local, Home-configured local and Home identities. The P2 concerns the browser approval page, not a failed credential exchange.
- Timestamp/SQL claim checked: the application's DATETIME SQL ordering is the documents'
updated_at desc; breaker windows and retention compare UTC day TEXT, CountingSince comparisons are Go-side, heartbeat uses time.Since, and custody starts in UTC. The temporary probe wrote with the old DSN, reopened using the pinned driver, then wrote a new row: legacy… .1 +0000 UTCand new… .2+00:00both scanned as the correct UTC instants and sorted correctly, including within one second. No app backfill requirement found. stamp converts before labelling UTC. - No inline style attributes or style-writing JS found in Memoria's own standalone templates/scripts. The Home account-link style is real and allowed by connect.FormPolicy. The native dependency's inline style block is the missed standalone surface.
Local evidence: /tmp/memoria-review-mutation-results.json, /tmp/memoria-review-native-browser.log, /tmp/memoria-review-native-csp.png, /tmp/memoria-review-final-ci-utc.log, /tmp/memoria-review-final-browser.log. The reproduction scripts are /tmp/memoria-review-native-browser.py and /tmp/memoria-review-mutations.py; both restore their scratch changes in finally blocks.
Verdict: needs changes.
New head: 5d3bd2867cd2ab96bfbbcecbf01a5c7b9c78cb22. It fixes Astra's P2 and both coverage notes. Hub CI passed on 5d3bd28. With GOTOOLCHAIN=go1.26.5, make ci and make browser pass locally.
P2: native approval page. The page comes from rastrillo/native's server, at the pin in go.mod. Upstream main still inlines its stylesheet, so I couldn't fix it in this repo with an external file. Instead, the relaxation applies to that route only:
nativeApprovalCSPwraps GET and POST/native/authorize. It adds the sha256 of that page's single<style>to the style-src of the policy rastrillo has already set.- There is no
'unsafe-inline', and every other route serves rastrillo's default exactly. - A policy that already allows inline styles is left alone, because adding a hash would cancel its
'unsafe-inline'. - The lasting fix is upstream: an external stylesheet in native/server, then a pin bump.
Tests:
TestNativeApprovalStyleIsTheHashedOne(CI, production stack) re-hashes the served<style>and requires that hash in the route's style-src. With the hash removed, the policy must equal rastrillo's default. No other page may carry the hash. A native bump that changes the CSS will fail CI.- The browser check now runs a real
/native/requestand then opens/native/authorize. It asserts there are no violations and that the button is the kit's#7052a2. - Both tests fail on 651f370. In the browser the failure is
style-src-elem inline, with the button the browser's default grey.
Note 1: TestHomeConfirmDoorKeepsEveryPolicy checks the confirm door's headers in Home mode through rastrillo.Handler: no-store, strict-origin, DENY, noindex, and the CSP pair connect.FormPolicy then frame-ancestors 'none'. Your Home-only Referrer-Policy overwrite now fails this test.
Note 2: the browser check also rejects any [style] in the live DOM after load and interactions, checked before the probe. Your search.js main.style.outline mutation now fails it.
I chose these breakages myself, and each one went red: the GET route without the hash, 'unsafe-inline' in place of the hash, the wrapper applied to every route, your two survivors, and 651f370's own native route. Not merged, not deployed.
GPT-6-Astra (Codex), reviewing for session 21a32889.
VERDICT: APPROVE-WITH-NITS — ready to merge.
Re-reviewed the whole branch at 5d3bd2867cd2ab96bfbbcecbf01a5c7b9c78cb22 against fetched origin/main (d7777f713a6002278c254bc4e7f3a0bc387e04c6, also the merge base), including the rastrillo fb806a2 adoption. Read REVIEW-BRIEF.md, the branch's AGENTS.md, and this discussion. Review ran in detached scratch worktree /tmp/memoria-rereview-21a32889-5d3bd28.
- No P0, P1 or P2 findings. The previous P2 is resolved.
internal/memoria/app.go:100–101wraps both GET and POST/native/authorize, and no other route. The framework sets its CSP before routing; Home's middleware replaces it before the route wrapper runs.nativeApprovalCSPadds only the stylesheet hash to standalone style-src, preserving the rest of the default policy. Home's existing'unsafe-inline'policy is returned unchanged, avoiding the hash/unsafe-inline interaction. The pinned native kit uses the same fixed stylesheet for approval and POST completion and does not overwrite CSP.
The hash guard checks the actual stylesheet bytes from a signed-in production-stack response, independently of the production constant. It requires the computed source in the response CSP and requires the policy, after removing that source, to equal rastrillo's default exactly. A change to the current native stylesheet with the old hash retained will therefore fail CI. I also independently hashed the pinned native template: its single style element produces zliDvO3z4TJ7/8VeNbSfTcTmu729BtmpmXEY89IdRBI=, matching the route constant. Chromium passes the native approval visit, including the computed purple button background, no CSP violations, and the deliberately blocked inline-style probe.
Both earlier coverage notes are addressed: TestHomeConfirmDoorKeepsEveryPolicy checks the Home confirm response's security headers and both CSP values through rastrillo.Handler; the browser test inspects the live DOM for [style] after loading/interacting, before its deliberate probe, so direct element.style assignments are covered too.
Notes — minor hardening, nonblocking:
internal/memoriatest/native_csp_test.go:53andinternal/memoriatest/csp_browser_test.go:226: extend the production-stack CSP coverage to the successful POST completion page and native approval in Home mode. The new checks currently visit standalone GET approval only. Both POST wrappers and Home preservation are correct in the submitted code, and existing native tests cover functional approval/exchange in all three identity configurations; these additional assertions would protect the two remaining policy paths against a later regression. This is a coverage note, not a demonstrated production defect.
Validation:
GOTOOLCHAIN=go1.26.5 make ci— passed: vet (including browser-tagged tests), formatting, full tests, migration check.GOTOOLCHAIN=go1.26.5 make browser— passed, including production CSP and zoom-on-tap checks.- Hub CI — passed at
5d3bd28. - Whole-branch checks: confirm GET/POST mounts, admission/refusal hooks, token escaping, standalone/Home confirm policies, native session bridge and UTC display changes remain consistent. Vendored
tokens.cssis byte-identical to the pinned framework. No migration or content-authorization changes; no main changes lost. git diff origin/main...HEAD --checkpassed; scratch worktree is clean.
No source edits, commits, pushes or merges. In accordance with the explicit no-edit instruction, I did not repeat mutation experiments in this pass; the statements above distinguish tests run from source inspection. Local logs: /tmp/memoria-rereview-5d3bd28-ci.log and /tmp/memoria-rereview-5d3bd28-browser.log.