Skip to content

BUZZ-270: Prepare community access verification runbook - #8235

Open
bradseiler wants to merge 6 commits into
codex/buzz-269-draft-prepfrom
codex/buzz-270-draft-prep
Open

bradseiler wants to merge 6 commits into
codex/buzz-269-draft-prepfrom
codex/buzz-270-draft-prep

Conversation

@bradseiler

@bradseiler bradseiler commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Add a community-access verification runbook and an evidence template for an exact deployed revision. Cover invitation repair/revocation, selective timeouts, bans, existing root/audio sessions, tenant isolation, cross-pod limits and all workflow trigger sources.

The local rehearsal uses the integrated stack, isolated services, the PostgreSQL wrapper and a running private relay for the signed route matrix. Timeout expectations name the permitted operations and distinguish signed/manual workflow writes from webhook, event and cron owner controls.

Related issue

BUZZ-270. Top of #8529 → #8241 → #8239 → #8235. Source remediation #8005, #7706, #7828, #8128, #8130, #8237 and #8242 is merged. The repair and lower stack layers still need their own approval and rollout.

Testing

Current head 5b10c52f161d4b09581d83e42907513337d67ba0 in native stack 8725, rebased by Brad through GitHub onto main 44b3c4823. Brad owns further ref updates.

All six documentation patches are unchanged in range-diff. Independent source review finds no restack blockers. Hosted documentation-layer CI may skip runtime domains; skipped jobs supply no runtime result.

Staging: NOT RUN. The evidence template remains NOT RUN. Deployment-owner authorization, disposable fixtures, exact deployed revisions and cleanup evidence are required before a passing verdict. This runbook does not authorize deployment or security-finding closure.

Normal pre-push hooks passed on integrated head 7ee3cf92f59a96349099d48fc115e5b8bb6a9716, including the branch-skew guard, unit tests, desktop checks/tests, Tauri checks, mobile checks/tests and file-size checks. These hooks validate the checked-out top tree; hosted CI remains the gate for each individual PR head.

Current-head documentation CI succeeded; runtime jobs were skipped and provide no runtime execution result. McKeon independently cleared all four current heads: carried patches range-diff unchanged (8/20/21/6), with no overlap between the newer main delta and stack files. The separate main-only security-review authorization check remains failed for this stacked base; no bypass or retarget was performed.

Historical local validation at integrated head 7ee3cf92f5: buzz-db/buzz-relay clippy passed with all targets and warnings denied; package tests passed 2,035/2,035; desktop build, Tauri check/tests, web build and mobile tests passed. The full PostgreSQL lane finished 1,503/1,505, with two failures still unresolved in that run: cluster_global_sample_writer_fails_closed_when_activity_is_masked and hanging_redis_peer_times_out_before_the_health_listener_binds. The runner constructed a postgresql:// URI; the fence test requires literal postgres://, explaining its prefix panic. The Redis test exceeded its scheduling bound at 8.03 seconds; that local failure remains unexplained. No full local just ci pass is claimed. These receipts remain attributed to the historical head.

The separate security-review workflow only authorizes PRs targeting main. Do not retarget or retry this stacked PR to bypass that restriction.

Human review and verification remain required. Existing review decisions are preserved. No merge, deployment, production repair or human acceptance was performed.

Generated with Codex

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is fae3f0346c44bbdf9629a7b5967965391c2bd22f...390994350e3d774b739183afb09cefe9a986f114.
A new review must complete for this exact range. When manual authorization
is required, a user with write access must comment exactly
@buzz-security-review 390994350e3d774b739183afb09cefe9a986f114 to authorize a new review.
Any previous review applies only to its recorded range.

@TheSentinel454

Copy link
Copy Markdown
Contributor

This runbook depends on unmerged siblings. Its local-rehearsal step runs scripts/check-community-ban-route-inventory.py, which only exists in #8239, so the literal command fails at this head. §Invitation lifecycle describes #8237's behavior, and :158 cites #8241's 30-second budget. Either land this last, stacked on the others, or make it a generic community-access verification runbook with no PR or ticket references, so it doesn't go stale once they merge.

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Two blocking issues, both in what the runbook tells an operator to run or expect. Following the local rehearsal as written skips the route behavior tests, and one of its commands fails on a fresh database. The timeout section also expects every write to fail, which isn't what the relay enforces. The rest checks out against source.

Blocking

1. The local rehearsal never runs the route behavior tests, and the cargo test step fails as written. community-access-verification.md L57–65

  • The HTTP and root-WebSocket ban matrix that #8239 adds lives in buzz-test-client, as #[ignore]d tests (community_ban_routes.rs L475–477, L609–611, L650–652 at #8239 head dda7ef88). It needs a running relay, and CI starts that relay with BUZZ_V1_ENABLED=true and then runs cargo test -p buzz-test-client --test community_ban_routes -- --ignored. Neither printed test command selects it. cargo test -p buzz-db -p buzz-relay names other packages and skips ignored tests. scripts/postgres-test-run.sh only selects ignored postgres_tests:: / postgres_ targets (L79–82). The Python guard checks that inventory rows and test registrations line up. It doesn't exercise a single denial. So an operator can finish every step without testing one ingress gate.
  • cargo test --no-fail-fast -p buzz-db -p buzz-relay isn't one of the repo's test lanes, and the Justfile test-unit comments explain why. Many non-ignored buzz-relay tests need a database with the schema loaded. For example, api::media's test_state seeds a community through config.database_url (media.rs L1674–1686). Pointed at an empty disposable database as the runbook says, it failed in a live run: exit 101, 1,670 passed / 8 failed / 910 ignored, and all 8 failures were missing communities. Left unset, those tests fall back to postgres://buzz:buzz_dev@localhost:5432/buzz (test_support.rs L1), which is the shared dev database the runbook says never to use.

Fix: replace the cargo test line with just test-unit (with #8239 it runs the route guard and the infra-free relay lanes) or the guard alone. Keep scripts/postgres-test-run.sh. Then add a separate route-E2E step: start a task-owned relay on a non-default port with BUZZ_V1_ENABLED=true against the isolated PostgreSQL, Redis and media backing, set RELAY_URL / RELAY_HTTP_URL / DATABASE_URL explicitly, and run the two buzz-test-client commands from #8239's TESTING.md. Don't point to the shared-service startup script. Record source-test results and relay behavior results separately.

2. The timeout section expects writes that the relay allows to fail. community-access-verification.md L139–140

"Writes fail until expiry or untimeout" doesn't hold for the routes listed just above it:

An operator following the runbook would log these as failures, or come away believing a timeout contains more than it does. Fix: name the content and ephemeral writes that should fail, list the allowed exceptions as positive controls, and keep the read and existing root/audio session controls. Ephemeral restrictions on other pods are only bounded by a 30-second cache (state.rs L1709–1732), not enforced immediately, so the expected result should say that.

Checked and fine

  • The four buzz moderation commands and their flags match lib.rs L2053–2099 and moderation.rs L24–85, and timeout correctly requires an expiry.
  • BUZZ_POSTGRES_ADMIN_URL, PGHOST/PGPORT/PGUSER/PGPASSWORD, REDIS_URL, PG_BIN_DIR, the privilege text, and both doc anchors match CONTRIBUTING.md, crates/buzz-db/TESTING.md, and postgres-test-run.sh.
  • The 30-day v1 drain matches MAX_INVITE_TTL_SECS, and measuring it from the last serving or rollback issuance rather than from the merge is right. The cutoff behavior matches #8237, and the webhook owner-ban behavior (point-in-time check, generic denial, deny on lookup failure, timeout allowed, existing runs untouched) matches #8242. The runbook correctly treats both as prerequisites, not current main behavior.
  • A timeout doesn't block root or audio admission or disconnect existing sessions, and reads stay available.
  • Isolation, secret handling, staging fault authorization, the rule against disabling shared Redis, cleanup, and keeping NOT RUN separate from deployed evidence are all sound.

Nonblocking

  • L111–117, L122: spell out the two boundaries separately. V1 expiry includes the expiry second (invite_token.rs L181 rejects only expiry < now), while #8237's cutoff rejects now >= cutoff. Deterministic boundary cases need both.
  • The rehearsal candidate is described as including only #8239, but the invitation, live-session and webhook cases also need #8237, #8241 and #8242 where they apply.
  • BUZZ-271 and BUZZ-272 are referenced in the body without links. Readers who can't resolve those IDs need the PR links.
  • Nothing links to the new docs/testing/ directory yet.
  • The description doesn't have buzz-review-completed yet (AGENTS.md step 4).

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Star Lord’s automated source review via Wes’s account, integrating independent Rocket, Mantis and Groot coverage.

Reviewed head eb3b03afc3238de7a46916ca8d0d40a9fd6353da against base 326e2301cb4b1edcb8a72d01ac4b19a83545365f (merge-base 19aa619fdd70e916269b343232276d9ee68276df).

Two P2 documentation corrections remain, both already described in the existing review. We independently confirmed them and are linking that feedback instead of duplicating inline comments:

  • Local rehearsal omits the route behavior matrix and uses an unprepared database lane (docs/testing/community-access-verification.md:57–64). The printed Cargo command selects only buzz-db/buzz-relay, while #8239’s HTTP/root-WebSocket matrix is an ignored buzz-test-client target; the PostgreSQL wrapper’s filter does not select it either. Non-ignored media tests also require a populated database before that wrapper runs. Use the established unit/PostgreSQL lanes and add an explicit route-E2E step against task-owned, isolated services with the accessory API enabled. Record these results separately.
  • Timeout expectations are broader than the enforced policy (same file, lines 139–140; evidence template line 32). Reports and unban/untimeout commands are explicitly allowed by allowed_while_timed_out; personal read-state writes are also allowed. Specify the content/ephemeral operations that must be denied and retain permitted operations as positive controls, including the documented cross-pod cache bound.

No additional actionable finding from this review. The runbook’s NOT RUN status, deployment prerequisites, isolated fixtures, authorized fault-injection boundary and cleanup requirements should remain.

Validation: read-only source/documentation inspection, including the public PR description (no attached images) and #8239’s pinned route-test source. No commands from the runbook, PR tests, applications, staging operations or fault injection were executed. CI snapshot: 11 successful checks, 32 skipped; it does not establish runtime, cross-pod, deployment or human-verification results. This is a non-blocking COMMENT review, not approval or deployment authorization.

TheSentinel454 added a commit that referenced this pull request Oct 29, 2026
## Summary

A workflow whose saved owner is banned from the community can no longer
create runs through any trigger: webhook, channel-event, cron, or signed
command trigger. Every one of those doors already calls
`WorkflowEngine::check_owner_authority`, which re-checks the owner's
channel role. That gate now also reads the owner's authoritative
restriction state. A ban denies the run, and a failed lookup fails
closed. Unbanning restores admission.

On the webhook door the denial is the same generic 404 as before, and no
run is queued. On the event and cron paths, a ban denial is logged at
WARN at most once per workflow every ten minutes, and at DEBUG in
between. A failed lookup and every other denial still warn every time.
The handler-local ban lookup in `api/bridge.rs` is removed because the
shared gate covers it.

This is a point-in-time check: a ban committed after the lookup can race
with run creation. It does not cancel existing runs. Community timeouts
do not block runs in this change.

This PR also carries the shared mesh receive fix needed for independent
validation (see Shared carry).

### Related

BUZZ-272.

- Invitations: [#8237](#8237).
- Route coverage: [#8239](#8239).
- Live-session revalidation:
[#8241](#8241).
- Staging runbook: [#8235](#8235).

### Testing

Postgres lane:
- `on_event_denies_run_while_owner_banned`: a banned owner's
channel-event trigger creates no run, and after unban it creates one.
- `cron_tick_skips_banned_owners_without_consuming_the_claim`: one tick
skips a banned owner and an agent whose human owner is banned, while an
unbanned workflow on the same tick still fires. After unban, a tick for
the same scheduled instant fires both, so the denial did not consume
their scheduled-fire claim.
- `on_event_denies_run_when_restriction_lookup_fails`: the event path
fails closed when the restriction read errors, and fires once the read
succeeds.
-
`webhook_admission_enforces_owner_ban_tenant_scope_and_lookup_failure`:
the HTTP door returns the same generic 404 for a wrong secret, a banned
owner, a forbidden role and a failed restriction lookup, with no run
created. It also covers another tenant's allowed control and unban.

Unit lane:
- `ban_denial_warns_once_per_interval_while_other_denials_always_warn`.

Results:
- At `81c367f48`, CI passed: Unit Tests 572/572, PostgreSQL Tests
1169/1169 and Relay E2E. A local-relay E2E also passed:
  - unbanned webhook: 202 and one run;
  - banned webhook: generic 404 and no run;
  - banned event trigger: no run;
  - after unban: one run.
- At `72c8d8bbc` (merged with main), CI passed: Unit Tests (including
all 171 buzz-workflow unit tests), PostgreSQL Tests 1227/1227 (all four
ban tests listed by name), and both `Build (linux/amd64|arm64)` image
jobs, which build with `--locked`. A Blox run at the same head also
passed `cargo metadata --locked`, fmt and clippy, and the buzz-workflow
unit tests passed there with no database reachable.
- Not covered: the signed trigger has no dedicated test. It calls the
same gate, which was confirmed by reading the code.

### Shared carry

#8237, #8239, #8241 and #8242 each carry the same mesh-receive fix, boot
and scraper regressions, and push-lease fixture correction. Whichever
lands first owns them, and the others drop them on their next main
merge.

### What got simpler

The ban check now lives in the one gate every run-creation door already
shares, so there is one fewer handler-local branch. Cron, event and
signed triggers get the same protection without per-door code. The cron
loop body is now `cron_tick(now)`, so a test can drive one tick without
the 60-second sleep.

buzz-workflow's infra-free unit tests ran in no CI lane. `just
test-unit` and its cargo fallback in `scripts/run-tests.sh` now run
them.

buzz-review-completed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: seiler <seiler@squareup.com>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Signed-off-by: Luke Tornquist <tornquist@squareup.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Luke Tornquist <tornquist@squareup.com>
Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@bradseiler
bradseiler force-pushed the codex/buzz-270-draft-prep branch from eb3b03a to 3909943 Compare October 30, 2026 01:23
@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Oct 30, 2026
@bradseiler
bradseiler changed the base branch from main to codex/buzz-269-draft-prep October 30, 2026 01:23
@bradseiler
bradseiler added this pull request to stack #8725 October 30, 2026 01:23

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Both blocking issues from the last review are mostly fixed. The timeout section now matches what the relay enforces. The local rehearsal now runs just test-unit, the PostgreSQL wrapper, and the route matrix against a private relay. One relay test the rehearsal needs is still missing, so it doesn't cover everything CI does.

Blocking

1. The local rehearsal still skips the relay-admin ban/timeout regression test. community-access-verification.md L71–88

CI's Relay E2E lane runs two community-ban targets against the running relay: community_ban_routes and regression_relay_admin_ban_gate (_ci-relay.yml L633–634). The runbook prints only the first. The second is the inventoried timeout_admin_denial case (community-ban-regressions.tsv L19): banned and timed-out administrators must be refused on relay-admin kinds 9030–9033, and an unrestricted administrator must still succeed. The test is #[ignore]d (L128–130), and neither just test-unit nor scripts/postgres-test-run.sh selects it. The large-roster script linked below the matrix checks discovery and reconciliation, not this. So an operator can finish the rehearsal without ever checking that a banned or timed-out admin is refused.

Fix: after the community_ban_routes command, add

REPRO_RELAY_HTTP=<private relay HTTP URL> REPRO_HOST=<private relay host:port> \
  cargo test -p buzz-test-client --test regression_relay_admin_ban_gate -- --ignored --nocapture

with DATABASE_URL pointing at the same disposable database. This target doesn't read RELAY_URL or RELAY_HTTP_URL. Without the two REPRO_* variables it falls back to localhost:3999 (L21–28), so the runbook has to set them.

Checked and fine

  • The timeout exceptions at L161–167 match allowed_while_timed_out (ingest.rs L2336–2343): reports (1984), unban (9041) and untimeout (9043). They also match the ban-only Git gate (deny_banned_git_principal) and the 30-second cached check on the ephemeral path (enforce_cached_write_restriction).
  • The workflow-owner section matches check_owner_authority (buzz-workflow/src/lib.rs L164–197) and all four places that call it: signed triggers (command_executor.rs), webhooks with a generic 404 (bridge.rs), channel events, and cron, which checks before it claims the fire slot. Bans are denied, timeouts aren't, and a failed lookup denies. The signed trigger separately goes through the ingest timeout gate, as the runbook says.
  • just test-unit is the right replacement for the bare cargo test. The private-relay setup (BUZZ_V1_ENABLED=true, BUZZ_KLIPY_API_KEY unset, explicit RELAY_URL/RELAY_HTTP_URL/DATABASE_URL, a warning off start-relay-for-tests.sh) matches the community_ban_routes header and TESTING.md.
  • The prerequisites now ask for the whole integrated stack and #8529's repair order. Every merged fix the PR description names is an ancestor of this head. The psql requirement matches upgrade_postgres_tests.rs. All relative links and anchors resolve.

Nonblocking

  • L66: just test-unit already runs check-community-ban-route-inventory.py and its fixture test (TESTING.md L57), so the separate guard line runs it twice. Drop it.
  • L133–139: say outright that a v1 token is still accepted at now == expiry (payload.e < now rejects, invite_token.rs L202), and that the cutoff rejects at now >= cutoff. Deterministic boundary tests need both edges.
  • L180–184: "If BUZZ-271 is deployed" and "30 seconds in the proposed implementation" conflict with prerequisite 2, which already requires #8241 (BUZZ-271) in every serving revision. Make the step unconditional, link #8241, and drop "proposed".
  • L205–207: the cron case "after unban, the same due cron instant must remain available" holds only if the unban lands inside cron_fire_instant's 60-second window (lib.rs L600). The ignored PostgreSQL test at L2302 already pins it. File it with the deterministic local checks, like the cutoff boundaries, rather than as a staging expectation.
  • L12–17 and L180: link #8529, #8241, #8239, #8237 and #8242 instead of leaving bare numbers and BUZZ IDs. Nothing in the repo links to docs/testing/ yet either. A pointer from TESTING.md's community-ban section would make the runbook findable.
  • L90 and L166 aren't wrapped like the rest of the file.

matt2e added a commit that referenced this pull request Oct 31, 2026
Units 6-9 of the 23-30 Oct desktop Playwright flake audit. All are
test-only. origin/main (1eb67bb, now including #8680) hasn't touched
these specs since fae3f03. No open PR touches the tests changed here:
#6134 (conflicting since 10-19) only swaps page.goto calls in three of
these files, #8187's hunk starts just after workflow-local-controls:151,
#5778 (draft, conflicting) creates projects-v3-screenshots from an
older base, and #8622, #7452, #7812, #8244 and #3342 edit other tests
in the same files.

Unit 6: measuring before an animation finishes

onboarding.spec.ts:1487, "hosted community address line stays within
the card for a long name"

Run: #8715 (Integration shard 1), which doesn't touch onboarding.
Signature: the centring check at :1545 expects <= 2 and receives 5.13.

Cause: "Connect and continue" ends in showPage("owned")
(WelcomeSetup.tsx:323), which mounts an OnboardingSlideTransition. Its
content slides in from translateX(48px) over 650 ms (components.css:521,
:676). fill() doesn't wait for stability, and the surface, input and
suffix boxes come from three boundingBox round trips, so they can be
read at different points of the slide.

Change: waitForAnimations(page) after the fill, before measuring.

onboarding.spec.ts:2289, "connected first-community profile keeps
navigation inside the card and balances the avatar editor"

Runs: main 40746677244 and 40925172215, and #8666 (Integration
shard 1). Signature: the spacing check at :2485 expects <= 4 and
receives 4.255.

Cause: the avatar dialog opens with MODAL_CONTENT_MOTION_CLASS, a 200 ms
zoom-in-95 (modalMotion.ts:5), and has a 250 ms height transition.
dialogBox is read in one round trip and the preview and editor boxes in
a later one, so a scale change between them skews the gap comparison.

Change: waitForAnimations(page) once the dialog is visible.

projects-v3-screenshots.spec.ts:280, "projects v3 workspace screenshot
states", failing in the expectInsetSection call at :893

Runs: main 39643531998, 40759209995 and 40969909906 (Smoke shard 8),
each passing on retry. Signature: the header's right inset at :326
expects 16 and receives 8. Four more runs fail the menu edge check at
:328 with fractional values (expected 984, received 975.6875):
39622053330, 39641311877, 40281448810 and 40856574572.

Cause: switching the context rail back from chat to repository narrows
it from the thread panel's 380 px to the repository's 280 px
(useProjectPanelWidths.ts:5, :17), animated by transition-[width]
duration-200 (ProjectContextRail.tsx:31). The workspace widens with it,
and expectInsetSection reads the workspace, header and menu boxes in
three separate round trips. The fractional widths are mid-transition
values.

Change: waitForAnimations(page) before that call, the only one that
follows a rail width change.

Unit 7: fill() racing a caret move the app makes a frame later

mention-recipients.spec.ts:376, "editing to a longer typed member
drops the original shorter reference", and :97, "ambiguous added
mention blocks editing before clearing the draft"

Runs: main 40969965981 (Smoke shard 5, a run with every earlier fix)
for :376. 40969858676 for :97, before the fixes. Signature: the
edit_message poll at :428 receives null. The error context shows the
edited message as "@scout hello @scout Jones hello": fill() appended.

Cause: entering edit mode sets the content, then focuses its end in a
requestAnimationFrame (MessageComposer.tsx:413). toHaveText passes as
soon as the content lands, before that frame. If the frame lands
between fill()'s select-all and its typing, the selection collapses and
the text is appended. #8682's replaceComposerText handles this, but
these two call sites (:115 and :403) were missed.

Change: replaceComposerText at both.

workflow-local-controls.spec.ts:151, "inserts template variables with
keyboard control and restores the caret"

Runs: main 40926393897 and 40957616146 (before the fixes), and #8239,
which changes no desktop files. Signature: :194 expects "Keep {{auth"
and receives "Hello {{trigger.text}}Keep {{auth".

Cause: selectVariable restores the caret in a requestAnimationFrame
(WorkflowTemplateTextarea.tsx:66). Setting the controlled value has
already put the caret at the end, so the selectionStart poll passes
before that frame runs. The frame then collapses fill()'s select-all.

Change: after the poll, wait two animation frames, then fill. This is a
frame barrier, not a timer. The app queued its frame before the
evaluate, so it runs first.

Unit 8: clicks that hit the wrong element

profile.spec.ts:1128, "renders agent profile ingress subviews from the
Playwright mock bridge", failing at :1543

Run: #8523, which changes no desktop files. Its base (ef4130d)
already includes #8681. Signature: archiveAgentRow.click() times out
because three stacked Sonner success toasts (data-expanded="true")
intercept the pointer.

Cause: the test's Stop and Start steps raise success toasts
(useAgentLifecycleActions.ts:51, :61). Sonner stacks them bottom right,
over the profile panel's action rows, and pauses their dismissal while
the pointer is over them. #8681 fixed this test's motion window, and
fixed the toast problem only in agent-availability:602.

Change: press the five action rows (:1528-:1547) through a local
pressActionRow helper: toBeEnabled, focus, Enter, as
agent-availability.spec.ts:744-745 does. toBeEnabled keeps the wait
click() gave, since these rows are disabled while a delete or archive
is pending.

config-bridge-screenshots.spec.ts:314, "07 — profile side panel —
Configuration section"

Runs: main 40967147375 (before the fixes), and #8235, which changes no
desktop files. Signature: "Model" isn't visible within 10 s at :352.
The error context shows nadia's panel ("Not managed on this device"),
not Charlie's.

Cause: the spec clicked .last() of the rows with a
[data-testid^="message-avatar-"] descendant. That matches only
UserAvatar's -image and -fallback children. Radix renders the image
only once it loads, and each fallback only after its own 200 ms delayMs
timer (UserAvatar.tsx:67, :119). Neither avatar in #agents has a URL,
so the matching set fills in row by row as the timers fire. Resolved
in between, .last() is nadia's row (seeded at -85 s), just above
Charlie's "reporting in." row (-80 s). The note blamed a misplaced
click; the code points at locator resolution instead.

Change: pick Charlie's row by its text, as profile.spec.ts does for
Charlie's other #agents message. Then check the panel's name row reads
charlie before opening Runtime, so a wrong panel fails there and says
so.

Unit 9: the createWorkflow copies (preventive)

navigation.spec.ts:153, "direct workflow detail links close back to
workflows"

Runs: 7 failures on main, the last at 29 Oct 16:12Z, none after #8681.
Signature: the Edit workflow dialog has no "Trigger: Message Posted"
at :176. Runs: 14 first-attempt failures in Smoke shard 7, all passing
on retry; on main 39827234118, 40718018673, 40789486699, 40802051064,
40840949170, 40891643205 and 40909291143.

Cause: the same race #8681 fixed in workflows.spec's createWorkflow.
Picking a channel opens the trigger pane (WorkflowDialog.tsx:702), and
for Message Posted the conditions editor expands "Message text" by
default (WorkflowTriggerConditions.tsx:157), rendering an input with
that label. After "Send Message", AnimatePresence mode="wait"
(WorkflowFormBuilder.tsx:907) keeps the trigger body mounted through
its exit and holds the step body back. An unscoped
getByLabel("Message text") can fill the trigger's filter. The step then
has no text, which yamlToFormState rejects (workflowFormTypes.ts:348),
so the Edit dialog opens in YAML mode (WorkflowDialog.tsx:88). That is
the signature the earlier audit recorded for navigation:153.

Change: wait for the step inspector's "Action" header and "Step
details" control, then fill inside the inspector, as workflows.spec
does.

workflows.spec.ts:716 ("captures the built editor at desktop and
narrow widths", fill at :726) and :820 ("pane routes use stable IDs and
Form/YAML changes stay synchronized", fill at :831) pick a channel and
then add a Send Message step the same way, with no failures in the
window. So the wait moves into one addSendMessageStep helper in
tests/helpers/workflows.ts, rather than four copies. Both specs'
createWorkflow and those two tests call it. A change to the inspector's
structure then needs one fix.

Left out: workflow-reaction-picker.spec.ts:203-204. The note listed it,
but its trigger is Reaction Added, whose condition fields are "Reaction
emoji", "Author" and "Message" (workflowConditionExpression.ts:40). No
trigger control there is labelled "Message text", so the unscoped fill
can only reach the step's textarea.

Verification

Not run locally: Playwright bogs this machine down, and the Flake Check
lane will repeat the touched specs. pnpm typecheck and pnpm check pass.
biome's 3 warnings are in untouched files. An ad hoc tsc ran over the
eight touched specs, using the desktop tsconfig with only those files,
and over HEAD's copies of them. Both gave the same errors: onboarding
16, navigation 7, profile 7, workflow-local-controls 7,
projects-v3-screenshots 2, and none in mention-recipients,
config-bridge-screenshots or workflows. The messages are identical once
line numbers are stripped, and all are pre-existing (no Node types,
import.meta.env, MockBridgeOptions drift and the like). Once the helper
moved to tests/helpers/workflows.ts, a rerun over navigation,
workflows and the helper gave navigation's same 7 and none in the other
two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
matt2e added a commit that referenced this pull request Oct 31, 2026
…kspace (#8744)

## Summary

Units 6–9 and 14 of the 23–30 Oct desktop Playwright flake audit, in two
commits. The specs now settle before they measure or act, and
remote-owned-mentions clears its draft reliably. Everything is
test-only. This is one of six PRs split from the reviewed `flakey-tests`
branch (siblings below).

### Unit 6: measuring before an animation finishes

Each spec now calls `waitForAnimations(page)` before measuring.

- `onboarding.spec.ts:1487` (#8715, Integration shard 1; the centring
check at :1545 receives 5.13): `showPage("owned")` slides content in
from `translateX(48px)` over 650 ms. The spec reads its three boxes in
separate `boundingBox` round trips, so they can come from different
points of the slide.
- `onboarding.spec.ts:2289` (main 40746677244 and 40925172215, and
#8666; the spacing check at :2485 receives 4.255): the avatar dialog has
a 200 ms zoom-in and a 250 ms height transition.
- `projects-v3-screenshots.spec.ts:280`, at :893 (main 39643531998,
40759209995 and 40969909906, plus four runs with fractional menu-edge
values): the context rail narrows from 380 to 280 px over 200 ms.

### Unit 7: `fill()` racing a caret move the app makes a frame later

- `mention-recipients.spec.ts:376` and `:97` (main 40969965981 and
40969858676): edit mode focuses the end of the composer in a
`requestAnimationFrame`. If that frame lands mid-`fill()`, the edit is
appended instead of replacing the text. **Change:** use #8682's
`replaceComposerText` at both call sites.
- `workflow-local-controls.spec.ts:151` (main 40926393897 and
40957616146, and #8239): `selectVariable` restores the caret in a
`requestAnimationFrame` after the spec's poll has passed. **Change:**
wait two animation frames before filling.

### Unit 8: clicks that hit the wrong element

- `profile.spec.ts:1128`, at :1543 (#8523): stacked Sonner success
toasts intercept the pointer. **Change:** a local `pressActionRow`
helper presses the five action rows with `toBeEnabled`, focus and Enter.
- `config-bridge-screenshots.spec.ts:314` (main 40967147375, and #8235):
`.last()` resolved while avatar fallbacks were still filling in on 200
ms timers, so it opened nadia's panel instead of Charlie's. **Change:**
pick Charlie's row by its text, and check the panel's name before
opening Runtime.

### Unit 9: the `createWorkflow` copies (preventive)

- `navigation.spec.ts:153` (7 failures on main, none since #8681; 14
first-attempt failures in Smoke shard 7): an unscoped
`getByLabel("Message text")` can fill the trigger's filter while
`AnimatePresence` keeps it mounted. **Change:** wait for the step
inspector, then fill inside it, as `workflows.spec` already does. The
wait now lives in one `addSendMessageStep` helper in
`tests/helpers/workflows.ts`, which `navigation.spec.ts` and
`workflows.spec.ts` (`createWorkflow`, :716, :820) all call.
- `workflow-reaction-picker.spec.ts:203-204` is left out. Its Reaction
Added trigger has no control labelled "Message text".

### Unit 14 (second commit): `remote-owned-mentions.spec.ts:566`

- **Run:** main 40998438103 (Smoke shard 9, job 123877347301). It passed
on retry and failed 1 of the 6 attempts it got in the window.
`sourceRecord()` at :673 returned thread A's draft instead of null.
- **Cause:** on ProseMirror, `fill("")` selects through the DOM, then
presses Delete. If ProseMirror hasn't seen the selection yet, it blocks
the Delete. Navigating to B then saves A's live text as its draft. This
is not a product bug: had the editor really been empty, the cleanup
would have cleared A's draft.
- **Change:** delete the text with one Backspace per character from the
end, then wait for the composer to read empty. The guard assertions are
unchanged.

### Related issue

None found. Sibling #8743 also edits `navigation.spec.ts`, in a separate
hunk. The two merge cleanly in either order. No open PR touches the
tests changed here. #6134, #8187, #5778, #8622, #7452, #7812, #8244 and
#3342 edit other tests in the same files.

### Testing

Not run locally, because Playwright bogs this machine down. Flake Check
will repeat the touched specs. `pnpm typecheck` and `pnpm check` pass,
and biome's 3 warnings are in untouched files. Ad hoc `tsc` runs over
the touched specs and the helper found the same pre-existing errors as
on their `main` copies.

Pushed with `git push --no-verify`, because the pre-push suites fail
when the laptop is under load.

### Readiness

An agent review of the combined `flakey-tests` branch (b432453af) found
no blockers. `buzz-review-completed` is not added, because the human
test step (AGENTS.md "Before opening a PR", step 3) hasn't happened yet.

### Siblings

- #8739 ci: Hermit fetch deadline and sccache fallback
- #8740 test(relay): thread_window aux-page hold and `api::media`
Postgres tests
- #8741 test(mobile): RunnerTests title fade and alert wait
- #8742 fix(desktop): deep-linked message hold (product change)
- #8743 test(desktop): four specs racing scroll settles and focus

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
matt2e added a commit that referenced this pull request Nov 3, 2026
message-target-highlight.spec.ts:79 is the most frequent unfixed
desktop flake. It failed in all 7 main runs whose SHA carries every
earlier flake fix (40969965981, 40970005965, 40970053997, 40980258899,
40980404335, 40980513346, 40983029157), and in 26 jobs since those
fixes landed, on main and on PRs #8235, #8239, #8241, #8666, #8680,
attempts failed. Only the two cases that pick mounted[2] fail: "same
channel, row loaded off screen" (15 jobs) and "other channel, row
further up" (11 jobs). The target scrolls in and flashes, then the
timeline lands on the newest message, so `stillOnScreen` at :179 reads
false (twice it was `flashedOnScreen` at :153).

Cause

The triage note blamed the thread panel opening. The failure videos
from run 40983029157 (Smoke shard 6) show that is a precondition, not
the trigger:
- Other channel: the panel opens at +0.36 s, the target flashes at
  +0.7 s, the older page prepends by +1.04 s, the target is still on
  screen at +1.47 s, and the timeline sits at the exact floor by
  +1.97 s.
- Same channel: the panel and the flash land in the same frame. The
  snap follows about 0.5 s later, with the load-older spinner still
  showing.

The panel opening never moved the target. A later floor report did:
TimelineMessageList's onScroll reporting within 32 px of the bottom,
during or after the older-page load that only a mounted[2] target sets
off. onVirtualizerAtBottomStateChange(true) reset the anchor to
at-bottom and set virtualizerAtBottomRef. With the panel open, the
split-panel settle, the viewport-resize settle or the next append
settle then pinned the timeline to the floor. The exact-floor landing
points at our own settleAtBottom rather than restored scroll. Which
Virtua layout pass emits the floor report is still unidentified.

Change

Accepting a virtual route target now sets targetHoldRef in
useAnchoredScroll. Only the two targetMessageId paths set it, on first
mount and in the later target effect, by passing holdTarget to
scrollToMessage. The public scrollToMessage, which MessageTimeline's
oldest-unread pill and find-in-channel stepping call, doesn't hold. It
also replaces an earlier hold, so a find jump after a deep link
follows the floor as on main. While the hold is set:
- shouldSettleForSplitPanel and shouldSettleVirtualizedViewportResize
  return false;
- a floor report still updates isAtBottom and the new-message count,
  but keeps the message anchor, so live appends don't settle either;
- a commit that changes the newest message sets isAtBottom false,
  clears virtualizerAtBottomRef and adds the rows after the previous
  newest one to the new-message count. Virtua reports bottom state
  only from scroll events, and an append below the viewport emits
  none. Without this, a floor report left isAtBottom true, so
  MessageTimeline hid the Jump to latest / N new pill while rows piled
  up out of sight (Review-Proven Rule 6). A realistic trigger is a
  mention or search hit on one of the last few messages in a busy
  channel: scrollToIndex clamps at the floor and emits that report.
  Clearing virtualizerAtBottomRef means a later release doesn't snap
  past the new rows either. The hold only acts on an arrival, so a
  reader at the floor with nothing new sees no pill. Counting rows
  rather than the length change matters: a cold target is merged in
  older than the head window, and the history page that fills the gap
  keeps both ends of the list. classifyTimelineMessageDelta calls that
  an append, but nothing arrived below, so it raises no pill. Neither
  does deleting the newest message;
- viewport reflow re-centres the target through
  virtualScrollToMessage, from the viewport-resize observer and when
  the split panel opens or closes. Rows rewrap around the target
  without a scroll event, so the target can drift, and a floor report
  from before the reflow can keep isAtBottom true while the newest
  rows drop below the fold. The re-centring scroll refreshes the
  bottom state. Virtua's scrollToIndex cancels any earlier one still
  in flight, so a burst of resizes only restarts its measure loop.
  The split-panel call fires only on a transition, not on mount or
  when the virtualizer API registers.

The hold is released by reader scroll input (a wheel, a touch drag, a
pointerdown on the scroller itself, a scroll key aimed at it), Jump to
latest, the reader's own send, and a channel change. The input
listeners are the ones that already cancel bottom intent in
useVirtualizedBottomSettle, now shared as listenForReaderScroll. A
release at the floor restores the at-bottom anchor, so the timeline
follows live messages again. onTargetReached does not release it.

The artifacts never show the panel pushing the target off screen, so
skipping the settle is the fix aimed at the CI failure. The re-centring
keeps the target's place and the pill right after a reflow.

Known limits:
- Programmatic scrolls (focus moves, scrollTop writes) don't release
  the hold. A find or unread-pill jump replaces it. A resize after
  such a scroll (Tab focus into a lower row, say) re-centres the
  target, away from the focused row.
- ChannelPane's composer-height settle still calls scrollToBottom
  within 32 px of the floor, which releases the hold.
- Live messages that arrive while the hold is set near the floor are
  not followed until reader input, Jump to latest or the reader's own
  send. The pill shows them, with their count.
- Under the hold, only rows after the previous newest message count.
  A live message that sorts just ahead of it (a same-second timestamp)
  raises no pill until a later row lands.

Unit before/after

useAnchoredScroll.lifecycle.test.mjs replays the CI sequence: accept a
route target, clear it, open the split panel, report the floor, resize
the viewport and append a live message. It asserts no bottom settle,
and that the panel opening and the resize each re-centre the target.
Then it asserts the append shows the pill (isAtBottom false, one new
message). A scrollbar pointerdown after that releases the hold, and a
resize neither settles nor re-centres. One case per release path
asserts the resize settle resumes and the re-centring stops. One of
them is a find jump: a public scrollToMessage on a held timeline, then
a floor report. A descendant pointerdown (row interaction) keeps the
hold, and a resize still re-centres. Reader input at the floor resumes
following a live append. Under a hold, a history page filling the gap
below the target, and then deleting the newest message, leave
isAtBottom true and the count at 0. A commit that adds more history
and one live row counts 1. The split-panel and viewport-resize policy
tables gain a held row.

Mutations over the lifecycle file and both policy tables (29 tests,
all pass intact):
- hold never set on accept (the old behaviour): 12 fail, including
  the CI-shaped hold case
- split-panel settle ignores the hold: 13 fail
- viewport-resize settle ignores the hold: 13 fail
- floor report resets a held anchor: 12 fail (the reset anchor is
  never re-centred)
- reader input does not release: 6 fail
- Jump to latest, own send, channel change don't release: 1 each
- release skips the at-bottom resync: 1 fails
- a held append doesn't raise the pill: 2 fail
- a held append leaves virtualizerAtBottomRef set: 1 fails
- every scrollToMessage holds (this commit's first draft): 1 fails
- a public jump keeps an earlier hold: 1 fails
- the held-append branch as first drafted (any non-prepend growth,
  counted by length): 1 fails, the gap-fill case
- a newest-id gate but a length-delta count: 1 fails
- a missing previous newest counts every row: 1 fails
- no rowsBelow > 0 guard: 1 fails
- rows counted from the previous newest, off by one: 2 fail
- no split-panel re-centre: 12 fail
- no viewport-resize re-centre: 12 fail
- re-centre ignores the hold: 1 fails

pnpm typecheck and pnpm check pass. All 46 tests in the files that
import the touched modules pass.

Only hosted CI can show

Playwright was not run locally. Whether the spec now holds in Smoke
shard 6 and in Flake Check's repeats is unverified, and so is the
unidentified Virtua floor report. scroll-history.spec.ts and the
other timeline specs need the full desktop smoke run to show the
hold doesn't regress bottom-follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
matt2e added a commit that referenced this pull request Nov 4, 2026
## Summary

A product fix for the most frequent unfixed desktop E2E flake,
`message-target-highlight.spec.ts:79`. A deep-linked message now holds
its place against bottom settles until the reader scrolls. This is one
of six PRs split from the reviewed `flakey-tests` branch (siblings
below).

- **Runs:** it failed in all 7 main runs whose SHA carries every earlier
flake fix (40969965981, 40970005965, 40970053997, 40980258899,
40980404335, 40980513346, 40983029157). It also failed in 26 jobs since
those fixes landed, on main and on PRs #8235, #8239, #8241, #8666 and
#8680. Only the two cases that pick `mounted[2]` fail: "same channel,
row loaded off screen" (15 jobs) and "other channel, row further up" (11
jobs). The target scrolls in and flashes, then the timeline lands on the
newest message. `stillOnScreen` at :179 reads false (twice it was
`flashedOnScreen` at :153).

### Cause

The failure videos (run 40983029157, Smoke shard 6) show that the thread
panel opening is a precondition, not the trigger. A later floor report
moved the target: `TimelineMessageList`'s `onScroll` reported within 32
px of the bottom, during or after the older-page load that only a
`mounted[2]` target sets off. `onVirtualizerAtBottomStateChange(true)`
reset the anchor to at-bottom. Then the split-panel settle, the
viewport-resize settle or the next append settle pinned the timeline to
the floor. Which Virtua layout pass emits the floor report is still
unidentified.

### Change

Accepting a virtual route target sets `targetHoldRef` in
`useAnchoredScroll`. Only the two `targetMessageId` paths set it, via
`holdTarget`. The public `scrollToMessage`, which the oldest-unread pill
and find-in-channel use, doesn't hold, and it replaces any earlier hold.
While the hold is set:

- The split-panel and viewport-resize settles are skipped.
- A floor report still updates `isAtBottom` and the new-message count,
but keeps the message anchor, so live appends don't settle either.
- A commit that changes the newest message sets `isAtBottom` false and
clears `virtualizerAtBottomRef`. It adds the rows after the previous
newest one to the new-message count. So the "Jump to latest" / "N new
messages" pill still appears for rows below the fold (Review-Proven Rule
6). A history page that fills the gap below a cold target raises no
pill, and neither does deleting the newest message.
- Viewport reflow re-centres the target through
`virtualScrollToMessage`. It runs from the resize observer and when the
split panel opens or closes.

The hold is released by reader scroll input: a wheel, a touch drag, a
pointerdown on the scroller itself, or a scroll key. Those listeners
already cancel bottom intent in `useVirtualizedBottomSettle`, and this
PR shares them as `listenForReaderScroll`. Jump to latest, the reader's
own send and a channel change also release it. A release at the floor
restores the at-bottom anchor, so the timeline follows live messages
again. `onTargetReached` does not release the hold.

### Known limits

- Programmatic scrolls (focus moves, `scrollTop` writes) don't release
the hold, but a find or unread-pill jump replaces it. A resize after
such a scroll re-centres the target.
- ChannelPane's composer-height settle still calls `scrollToBottom`
within 32 px of the floor, which releases the hold.
- Live messages that arrive under a hold near the floor aren't followed
until reader input, Jump to latest or the reader's own send. The pill
shows them, with their count.
- Under the hold, only rows after the previous newest message count. A
same-second row that sorts just ahead of it raises no pill until a later
row lands.
- Agent review raised two optional gaps, and scrolling recovers from
both:
- If the previous newest message is deleted and a new row arrives in the
same deferred commit, 0 rows are counted.
- A thread reply sent with "also send to channel" doesn't arm the bottom
intent, so it shows as "1 new message" instead of being followed.

### Related issue

None found.

### Testing

`useAnchoredScroll.lifecycle.test.mjs` replays the CI sequence: accept a
route target, clear it, open the split panel, report the floor, resize
and append a live message. It asserts no bottom settle, a re-centre on
the panel opening and on the resize, and the pill on the append. Further
cases cover:
- each release path;
- a find jump on a held timeline;
- a descendant pointerdown keeping the hold;
- a gap-fill page and deleting the newest message, neither of which
raises the pill.

The split-panel and viewport-resize policy tables each gain a held row.

Mutations were run over the lifecycle file and both policy tables (29
tests, all pass intact). Each of the 20 mutations fails at least one
test:

| Mutation | Tests that fail |
|---|---|
| Hold never set (the old behaviour) | 12 |
| Split-panel settle ignores the hold | 13 |
| Viewport-resize settle ignores the hold | 13 |
| Floor report resets a held anchor | 12 |
| No split-panel re-centre | 12 |
| No viewport-resize re-centre | 12 |
| Reader input doesn't release the hold | 6 |
| Held append raises no pill | 2 |

The commit message lists the other 12, each failing 1 or 2 tests.

`pnpm typecheck` and `pnpm check` pass. All 46 tests in the files that
import the touched modules pass.

Only hosted CI can show whether the spec now holds in Smoke shard 6 and
in Flake Check's repeats, because Playwright was not run locally. It
also has to show that `scroll-history.spec.ts` and the other timeline
specs keep bottom-follow working.

Pushed with `git push --no-verify`, because the pre-push suites fail
when the laptop is under load.

### Readiness and human check

An agent review of the combined `flakey-tests` branch (b432453af) found
no blockers. This changes desktop app behaviour, so it needs a human
in-app check before `buzz-review-completed` is added. Suggested check,
in `just dev` against a channel with a few pages of history:

1. Open a message link (a mention or search hit) to a message several
screens up, once in the same channel and once from another channel. The
message should flash and stay on screen after older history loads,
including when the thread panel opens or closes, or the window is
resized.
2. With the target still held, have someone post in the channel. The
timeline stays put, and the pill reads "1 new message".
3. Wheel-scroll to the bottom, or click Jump to latest. The next message
that arrives is followed.

### Siblings

- #8739 ci: Hermit fetch deadline and sccache fallback
- #8740 test(relay): thread_window aux-page hold and `api::media`
Postgres tests
- #8741 test(mobile): RunnerTests title fade and alert wait
- #8743 test(desktop): four specs racing scroll settles and focus
- #8744 test(desktop): five settle classes and remote-owned-mentions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@bradseiler
bradseiler force-pushed the codex/buzz-270-draft-prep branch from 3909943 to 842df2e Compare November 4, 2026 16:21
@bradseiler
bradseiler force-pushed the codex/buzz-270-draft-prep branch from 842df2e to a7f58ec Compare November 4, 2026 16:27

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Requesting changes at a7f58ec8. The two runbook files are unchanged since the last review at 39099435 (review); this head only rebases onto the moved stack base. I re-checked each point against this head, and they all still apply.

Blocking

1. The local rehearsal still skips the relay-admin ban/timeout regression test. community-access-verification.md L71–82

CI's Relay E2E lane runs two community-ban targets against the running relay: community_ban_routes and regression_relay_admin_ban_gate (_ci-relay.yml L669–670). The runbook prints only the first. The second is the inventoried timeout_admin_denial case (community-ban-regressions.tsv L19): banned and timed-out administrators must be refused on relay-admin kinds 9030–9033, and an unrestricted administrator must still succeed. It is #[ignore]d, and neither just test-unit nor scripts/postgres-test-run.sh selects it. So an operator can finish the rehearsal without ever checking that a banned or timed-out admin is refused.

Fix: after the community_ban_routes command, add

REPRO_RELAY_HTTP=<private relay HTTP URL> REPRO_HOST=<private relay host:port> \
  cargo test -p buzz-test-client --test regression_relay_admin_ban_gate -- --ignored --nocapture

with DATABASE_URL pointing at the same disposable database. This target doesn't read RELAY_URL or RELAY_HTTP_URL; without the two REPRO_* variables it falls back to localhost:3999 (L21–26), so the runbook has to set them.

Nonblocking (still open from the last review)

  • L66: just test-unit already runs check-community-ban-route-inventory.py and its fixture test (Justfile L400–401), so the separate guard line runs it twice. Drop it.
  • L133–139: say outright that a v1 token is still accepted at now == expiry (payload.e < now rejects) and that the cutoff rejects at now >= cutoff (invite_token.rs L202). Deterministic boundary tests need both edges.
  • L180–184: "If BUZZ-271 is deployed" and "the proposed implementation" conflict with prerequisite 2, which already requires #8241 (BUZZ-271) in every serving revision. Make the step unconditional, link #8241, and drop "proposed".
  • L205–207: "after unban, the same due cron instant must remain available" holds only if the unban lands inside the cron loop's 60-second catch-up window (buzz-workflow/src/lib.rs L525–530). File it with the deterministic local checks, like the cutoff boundaries, rather than as a staging expectation.
  • L12–17 and L180: link #8529, #8241, #8239, #8237 and #8242 instead of leaving bare numbers and BUZZ IDs. A pointer from TESTING.md's community-ban section would also make the runbook findable; nothing in the repo links to docs/testing/ yet.
  • L90 and L166 aren't wrapped like the rest of the file.

@bradseiler
bradseiler force-pushed the codex/buzz-270-draft-prep branch from a7f58ec to 2c00ed0 Compare November 4, 2026 18:10

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Requesting changes at 2c00ed0b. The blocker from the last review is fixed: the local rehearsal now runs regression_relay_admin_ban_gate with REPRO_RELAY_HTTP, REPRO_HOST and DATABASE_URL, which are exactly what the test reads (L21–28), and the ${VAR:?} guards fail loudly instead of falling back to localhost:3999. One timing statement in the timeout section is now wrong, though, and it could let a regression pass.

Blocking

1. The timeout section allows 30 seconds for cross-pod ephemeral denial, but the restriction cache is 1 second. community-access-verification.md L176–177

Ephemeral kinds and observer frames take the write-restriction check before ingest (event.rs L717–725). That check goes through restriction_state_cached (ingest.rs L2380–2408), which serves a cached row only while it is younger than RESTRICTION_CACHE_MAX_AGE. On this base that is 1 second (state.rs L1913–1916, L2584–2601). An operator who allows 30 seconds would record a timeout that takes 20 seconds to reach another pod as a pass.

Fix: refer to the deployed RESTRICTION_CACHE_MAX_AGE (1 second at this revision) instead of a fixed number, and say that the measured delay includes request and measurement latency on top of the cache age. Keep the separate 30-second live-session revalidation budget in the existing-sessions section; that one is still right (state.rs L92–113).

Checked and fine

  • The added command matches CI's Relay E2E step (_ci-relay.yml L669–670). The target is #[ignore]d, and neither just test-unit nor scripts/postgres-test-run.sh selects it, so the "do not omit it" note is accurate.
  • The PostgreSQL setup, the private-relay setup, the four buzz moderation commands, the timeout exceptions (reports, unban, untimeout, personal read state, Git), the invitation and v1 cutoff behavior, the workflow owner-ban checks across all four trigger sources, and the evidence template all still match source at this head.

Nonblocking

  • L89–91: the regression covers ban denials on all of 9030–9033 (with a banned owner for owner-only 9032), but checks the timed-out admin and the unrestricted admin only on 9031 (L203–248, L290–350). Something like "ban denials across 9030–9033, plus timed-out and unrestricted 9031 controls" would describe it exactly.
  • L66: just test-unit already runs check-community-ban-route-inventory.py (Justfile L400–401), so this line runs it twice. Drop it.
  • L144–150: say outright that a v1 token is still accepted at now == expiry and that the cutoff rejects at now >= cutoff (invite_token.rs L202). Deterministic boundary tests need both edges.
  • L191–195: "If BUZZ-271 is deployed" and "the proposed implementation" conflict with prerequisite 2, which already requires #8241 in every serving revision. Make the step unconditional, link #8241, and drop "proposed".
  • L217–218: "after unban, the same due cron instant must remain available" only holds if the unban lands inside the cron loop's 60-second catch-up window (buzz-workflow/src/lib.rs L525–530). Put it with the deterministic local checks rather than the staging expectations.
  • L12–17: link #8237, #8529, #8241, #8239 and #8242 instead of leaving bare numbers. A pointer from TESTING.md's community-ban section would also make the runbook findable, since nothing in the repo links to docs/testing/ yet.
  • L101 and L177 aren't wrapped like the rest of the file.

@bradseiler
bradseiler force-pushed the codex/buzz-270-draft-prep branch from 2c00ed0 to 446cca1 Compare November 4, 2026 22:43

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 No blocking findings at 446cca11. The blocker from the last review is fixed: L176–181 now measures cross-pod ephemeral denial against the deployed RESTRICTION_CACHE_MAX_AGE, which is 1 second at this revision (state.rs L1927), and keeps request and measurement latency separate.

Checked and fine

  • The new outage step (L202–207) matches the revalidator in this tree: 5-second base grace, a stable per-connection offset under 9 seconds, 5-second cadence and scan budget, a 1-second query deadline, and the 30-second worst case the compile-time assertion enforces (state.rs L92–123). Confirmed denials skip the outage grace, as the step says.
  • The writer-stall step (L208–215) matches live_authorization.rs: the 900 ms cap is the 1-second deadline minus 100 ms, a tighter operator cap wins, the setting is transaction-local so the writer's own timeout survives, and an unsafe connection is closed rather than reused (L10–63). The query joins users, so a users-table lock is the right stall. query_timeout, statement_cancelled and pool_timeout are the labels buzz_live_authorization_batches_total emits (state.rs L1110–1138, L1332–1338).
  • Every command in the local rehearsal resolves at this head. community_ban_routes and regression_relay_admin_ban_gate compile, and the two REPRO_* variables match what the second target reads.
  • Invitation, timeout-exemption, existing-session and workflow owner-ban statements still match source. All relative links and anchors resolve.

Nonblocking

  • L66: just test-unit already runs check-community-ban-route-inventory.py (Justfile L400), so this line runs it twice. Drop it.
  • L144–150: say that a v1 token is still accepted at now == expiry while the cutoff rejects at now >= cutoff (invite_token.rs L201). The deterministic boundary table (L449–456) covers cutoff equality but has no now == expiry row, so the "exercise boundary cases in deterministic local tests" instruction has nothing to point at for that edge.
  • L194–202: "If BUZZ-271 is deployed", "the proposed implementation" and the new "The proposed worker" conflict with prerequisite 2, which requires #8241 in every serving revision, and the worker is in this tree. Make step 5 unconditional and say "this implementation"; keep the requirement to prove the deployed revision contains it.
  • L234–235: "after unban, the same due cron instant must remain available" only holds if the unban lands inside cron_fire_instant's 60-second window (buzz-workflow/src/lib.rs L600, L841–851). Put same-instant recovery with the deterministic local checks and use a later due instant for the staging unban control.
  • L89–91: the regression checks ban denials across 9030–9033 but the timed-out and unrestricted administrator controls only on 9031 (L203–248, L290–350). Describe those exact cases.
  • L12–17: link #8237, #8529, #8241, #8239 and #8242 instead of leaving bare numbers. A pointer from TESTING.md's community-ban section would make the runbook findable; nothing outside docs/testing/ links to it yet.
  • L101 and L213 aren't wrapped like the rest of the file.

@bradseiler
bradseiler force-pushed the codex/buzz-270-draft-prep branch 2 times, most recently from 019b984 to 040ad30 Compare November 5, 2026 01:05

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 No blocking findings at 040ad308. Both runbook files are byte-identical to the head of the last review, 446cca11 (review). This head only rebases onto a newer codex/buzz-269-draft-prep.

The base moved in three files: scripts/check-community-ban-route-inventory.py (it now classifies the whole route method chain and fails unknown helpers such as on and *_service), its fixture test, and the scanner wording in TESTING.md. None of it changes anything the runbook says:

The nonblocking points from the last review still apply unchanged.

@bradseiler
bradseiler force-pushed the codex/buzz-270-draft-prep branch from 040ad30 to ede9d79 Compare November 6, 2026 23:59
@bradseiler
bradseiler force-pushed the codex/buzz-270-draft-prep branch 2 times, most recently from 565f0d9 to 7774a19 Compare November 10, 2026 00:29
@bradseiler
bradseiler force-pushed the codex/buzz-270-draft-prep branch from 7774a19 to 7ee3cf9 Compare November 10, 2026 00:41
bradseiler and others added 6 commits November 9, 2026 22:21
Add a repeatable staging runbook and versioned evidence template. Execution remains gated on deployed prerequisites and a disposable community owner.

Co-authored-by: Codex <noreply@openai.com>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Signed-off-by: Local Builder <1c476cb1127ea270cb900bdc3891060140bdd1173f91498030ee9b4db01c2f20@buzz.block.builderlab.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Co-authored-by: Sarnow <1894dcbafaa4df1ac607a9bf879ac0b7d77fbccb0e0f4619ecc252c8adbad8a2@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Run the ignored ban and timeout regression against the same private relay and disposable database, with its required REPRO endpoint inputs.

Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
Record the 30-second grace and jitter formula, five-second sweep granularity, and isolated writer-stall rehearsal. Include server cancellation, acquisition timeout, and pool/session recovery evidence.

Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
@bradseiler
bradseiler force-pushed the codex/buzz-270-draft-prep branch from 7ee3cf9 to 5b10c52 Compare November 10, 2026 06:21

This branch was previously deployed

1 inactive (outdated) deployment
codex-review — 39099435 Deployed Oct 30, 2026 by bradseiler via Run Codex Security Review #10841
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants