Repository navigation
fix(media): revoke private channel attachment reads - #9077
Conversation
There was a problem hiding this comment.
🤖 Automated review at 3e4c58d5fb62ed627743a40acad6f0322092d2a9: changes requested.
The revocation boundary itself holds. A headless run against an isolated relay in BUZZ_MEDIA_READ_MODE=enforce showed a removed member getting 404 with private, no-store and no Content-Range on GET, HEAD, Range, invalid Range and the thumbnail alias, while a surviving member kept 200/200/206/200. Posting the same blob to an open channel restored the removed member's reads, and off mode kept them. The basic and extended enforce-mode media suites passed. The migrations never build an index on a populated events table, the prebuild/attach path cannot count an invalid build as valid, new partitions inherit the index, and community purge covers both new tables. Five problems block enabling enforce, though.
Blocking
1. The URL-alias reference lookup takes 2–2.5x its own statement limit. media_access sets statement_timeout = '2s' (crates/buzz-db/src/store/media.rs:103). On the PR's own fixture (1.2M rows, 24 partitions), the second lookup (probe 1, the URL/alias needles, readable_only = true) takes 4.1 s plain and 4.7 s after an explicit ANALYZE on isolated PostgreSQL 17.11. That's the same timeout media_reference_full_query_plans hits in CI (media_postgres_tests.rs:396, SQLSTATE 57014). It isn't EXPLAIN overhead or missing statistics. The indexed scan finishes in about 16 ms, but the top Limit node only starts at about 4.7 s, so the cost sits above the scan. In enforce mode, any read that reaches probe 1 would return "authorization unavailable" (503). That includes every attachment posted by URL without a matching x needle. The probe-1 query needs reshaping (or a different plan) so it stays well under the limit, with the test asserting that.
2. Link-preview snapshot images and favicons become unreadable once posted, even to their sender. The desktop uploads the snapshot image and favicon and references them only in a link-preview tag (desktop/src/features/messages/lib/linkPreviewPreparationStore.ts:153-169, desktop/src-tauri/src/link_preview_tags.rs:73-93). publication_hashes scans every tag value (crates/buzz-core/src/media.rs:111), so publishing records a publication marker and ends the uploader preview. But reference_hashes (media.rs:48) and the SQL needles (store/media.rs:112) never count link-preview. Every later read then resolves to Restricted (store/media.rs:189) and returns 404. Count the link-preview snapshot image/favicon fields as references in both the Rust extractor and the SQL probes, with a regression test.
3. Emoji lists and sets skip the 512-reference cap, and each reference costs a serial lookup on the primary database. crates/buzz-relay/src/handlers/media_access.rs:18 exempts kinds 10030/30030. The loop then does a sidecar fetch plus a media_access transaction on a writer connection for every hash, one at a time. All of those share the 8-slot authorization semaphore (crates/buzz-db/src/runtime/mod.rs:2161) with every media GET. A single emoji-set event with a few thousand self-uploaded URLs becomes thousands of serial writer transactions inside one ingest call. Batch the per-event lookups into one query, or put a bound on emoji kinds too.
4. Animated profile pictures are not counted as references. Desktop stores an animated avatar in the kind-0 picture field as <posterUrl>#buzz-anim=<encoded animationUrl> (desktop/src/shared/lib/animatedAvatar.ts:4-12), after uploading both assets. reference_hashes passes the whole string to local_hash, and the suffix check (crates/buzz-core/src/media.rs:30-40) rejects it. As a result neither the poster nor the animation becomes a readable profile reference, and other members get 404 for both in enforce mode. Decode the descriptor into its two URLs for both publication validation and read-reference extraction, keeping the tenant check on each. The agent-creation transfer also rejects fragments (desktop/src-tauri/src/relay/profile_avatar.rs:47,182), so it needs the same handling.
5. Profiles that use image instead of picture lose their avatar. The kind-0 materializer accepts image when picture is absent (crates/buzz-relay/src/handlers/side_effects.rs:1786-1790). The new extractor counts only picture and banner. The recursive publication scan still records the marker and ends the uploader preview, so the accepted avatar becomes unreadable. Mirror the materializer's fallback and add a test with an image-only profile.
Non-blocking
blob_cache_control()now returnsprivate, no-storein every mode, including the defaultoff(crates/buzz-relay/src/api/media.rs:829).MediaReadMode::Offis documented as preserving current behavior. Gate the header on the mode, or document why caching has to stop before enforce is turned on.- The agent-creation change moves this repo's desktop to an upload signed by the agent, because a profile can't reference an image that only another key uploaded. Any other client that uploads with one key and publishes the profile with another needs the same change before enforce. The operator guide should list that as a precondition.
- Every enforce-mode read takes a writer connection and runs up to eight statements (
store/media.rs:85-200), andno-storerepeats that on every view. The operator guide should give the expected per-pod lookup rate against the 8-slot cap. scripts/maintenance/prebuild-media-edit-index.sql:19-21skips a partition whose index matches by definition without requiringindisvalid. A leftover invalid child index therefore blocks every rerun until someone drops it by hand. This is safe, since attach and the 0084 guard both refuse it, but addingAND i.indisvalid(or printing the invalid index names) would let the script recover on its own.- The note against running community deletion between table expansion and the new deletion catalog also applies to a rollback after 0083. Old binaries'
EXPECTED_SCOPED_TABLESdoesn't include the new tables. Say so in the rollback section. - Authorized
416 Range Not Satisfiableresponses carry noCache-Controlheader (crates/buzz-relay/src/api/media.rs:1095-1099,1122-1126), while successful reads and denials useprivate, no-store. Set it on those responses too. - The branch conflicts with
main.
f74850b to
39f643c
Compare
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 Automated review at 39f643cfd6227d35b03a7286a52b5b2baead56c0: no blocking findings. The five blockers from the previous review are fixed, and enforce mode should stay off until the avatar-transfer and client prerequisites below are done.
A headless run against an isolated relay in BUZZ_MEDIA_READ_MODE=enforce confirmed each fix:
| Enforce journey | GET | HEAD | Range | Thumbnail |
|---|---|---|---|---|
| Private member before removal | 200 | 200 | 206 | 200 |
| Removed member, fresh requests | 404 | 404 | 404 | 404 |
| Surviving member | 200 | 200 | 206 | 200 |
| Rejoined member | 200 | 200 | 206 | 200 |
| Link-preview snapshot image and favicon, other member | 200 | 200 | 206 | 200 |
| Animated avatar poster and GIF, other member | 200 | 200 | 206 | 200 |
image-only profile, other member |
200 | 200 | 206 | 200 |
- URL lookup time:
media_reference_full_query_planspasses. On the committed 1.2M-row fixture, the readable URL probe executes in 34 ms (about 88 ms wall without EXPLAIN), and a full production URL-onlyDb::media_accesscall takes 92 ms. Both are well under the 2 s limit.SET LOCAL jit = offstays inside the transaction, and the pooled connection keeps JIT on afterwards. An un-ANALYZEd copy of the fixture still times out, so the timing holds for analyzed statistics only. - Emoji lists and sets: kinds 10030 and 30030 both refuse 513 distinct references with
restricted: too many attachment referencesand accept 512. Each accepted event records its publications in a single 512-hash INSERT. The per-reference sidecar and authorization checks still run one after another, and a 512-reference event takes about 1.57 s to ingest. - Link previews, animated avatars,
imagefallback: the Rust extractor and SQL needles count only snapshot positions 7 and 9 (crates/buzz-core/src/media.rs:99-105,crates/buzz-db/src/store/media.rs:296-316). Each decoded animation URL goes through its own tenant check (media.rs:47-82). Thepicture-then-imagefallback matches the kind-0 materializer, including that"picture": nullblocks it. - Also fixed: the prebuild script now uses
lock_timeout = 0only around the concurrent drop and build steps and drops leftover invalid child indexes. 416 responses now carryCache-Control. Every tested response, including authorized 416s, isprivate, no-store. The operator guide now covers no-store in all modes, the 8-slot lookup rate, rollback versus community deletion, and other clients' signer compatibility.
Non-blocking
- Persona-avatar edits and team snapshot import still publish an avatar the owner uploaded. Both call
sync_managed_agent_profilewithout moving the bytes onto an upload signed by the agent (desktop/src-tauri/src/commands/personas/update.rs:312-323,desktop/src-tauri/src/commands/team_snapshot.rs:786-796). In enforce mode the relay rejects that kind-0, and persona update only logs the failure, so the saved avatar and the published one diverge. The PR description and the existing inline thread already treat this as an enforce prerequisite.docs/operator-private-media.mddoesn't mention it, though, and neither doesdesktop/src/features/agents/AGENTS.md. Operators need these two flows in the guide's enforce checklist. - The client compatibility section (
docs/operator-private-media.md:145-149) is generic. Naming the clients known to need the agent-signed avatar upload, the new desktop app (block/buzz-app) among them, would make the enforce checklist actionable. - The kind-0 fallback probe also matches every current profile whose content contains
\u(crates/buzz-db/src/store/media.rs:267-269). In a community with more than 1,024 such profiles, a read that falls through to that probe without a match exceeds the work budget (:145). In enforce mode it returns 503 "authorization unavailable" instead of 404, after parsing up to 1,024 profiles on the primary database. Python's defaultjson.dumpsproduces such profiles, and so does Go's HTML escaping. The test with more than 1,024 unrelated animated profiles doesn't contain escapes, so it doesn't cover this. Two options: normalize or reject escaped local media URLs when a profile is published, or count escape-only candidates separately from the budget. - The branch conflicts with
main.
49e84c1 to
1f08a8e
Compare
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 Automated review at 1f08a8e0fd2edf19dc94321f7258518149fe1bf6: no blocking findings. This covers the five commits added since the previous review at 39f643cf: the narrowed profile-picture lookup, and avatar transfer for persona edits and snapshot imports. The 25 earlier commits are unchanged apart from rebase conflict resolution.
A headless run against an isolated relay in BUZZ_MEDIA_READ_MODE=enforce confirmed the revocation boundary still holds at this head. Once a member was removed from the private channel, they got 404 on GET, HEAD, a valid Range and an out-of-bounds Range, with no Content-Range. The uploader kept 200. A profile picture with a plain hash stayed readable to another member.
Everything below is nonblocking. Items 1 and 2 should be settled before anyone turns on enforce mode, because the PR's operator guide currently says these flows are covered.
- Edited persona avatars can be saved without a transfer for instances whose key is unavailable (
desktop/src-tauri/src/commands/personas/update.rs:213-216,340-368). Skipping those instances is safe for the immediate publish, since neither the transfer nor the profile publish runs for them. But the save still writes the new avatar into their record, and nothing records that their transfer is outstanding. When the key comes back, startup reconciliation publishes that avatar with no owner transfer (commands/agents_profile.rs:290-325). In enforce mode the profile is then rejected if only the owner uploaded the image. Saving the same avatar again doesn't repair it, because the persona no longer looks changed. Either keep the previous avatar on those instances and report a partial failure, or record the pending transfer so a later owner action can complete it.docs/operator-private-media.mdanddesktop/src/features/agents/AGENTS.mdshould mention the gap until then. - A profile whose picture hash is Unicode-escaped is accepted but can't be read (
crates/buzz-db/src/store/media.rs:263-269). Publication decodes the JSON (crates/buzz-core/src/media.rs:119-130), so a kind-0 with e.g.fin the hash is accepted and recorded. The new literal-hash lookup then can't find it, so the image returns 404 to everyone, including the uploader. Reproduced live: the relay answeredOK … true, then every read got 404. Republishing with a plain hash restored access immediately. This fails closed, and no mainstream JSON encoder escapes ASCII hex, so the impact is small. Still, rejecting a non-canonical hash at publication would be better than accepting a profile whose avatar breaks immediately. Don't restore the broad\uscan. - Persona edits now require the owner key even when the avatar is unchanged (
update.rs:187).update_persona_withdidn't needsigning_keys()before this change. Now a plain rename fails while the keyring is locked or the identity is lost. Taking the key only in the changed-avatar branch keeps the old behavior. - The workspace check before the save is not atomic with the save (
update.rs:251-265). It runs under the agent-store lock, butapply_workspacetakes a different lock (commands/workspace.rs:207-211,255-267). A workspace switch between the check and the retention step can therefore send the persona definition to the new workspace (personas/pending.rs:67-78) while its agent profiles go to the old one, with no retry error. The window is a few statements wide and the old code split these reads the same way, so this isn't new. Passing the captured scope into retention, instead of resolving the active one again, would close it. - Unrelated agent activity can fail an avatar edit. The check compares the whole agent-record store, including runtime fields like
runtime_pidandlast_started_at. Any agent starting or stopping during the upload makes the edit fail with "retry". Comparing only the linked records' avatar fields would avoid that. - Misleading error text. "Workspace changed during avatar upload" in the individual snapshot import (
commands/personas/snapshot/import.rs:520) fires before any upload has happened. "During import" would be accurate.
CI at this head shows no failures. The Codex security review job was still running when this review was written.
1f08a8e to
82c1990
Compare
jmecom
left a comment
There was a problem hiding this comment.
🤖 Reviewed 82c19907540d577149b3c0ebe525797e99cd6f0c. I am withholding approval for the kind-30180 emoji read regression described inline.
The review covered relay reads/publication, database authorization and migration rollout, and desktop avatar transfer paths, with independent source reviews of those areas. The previously reported persona/key-recovery issues remain nonblocking observations; this review does not establish readiness to enable enforce mode.
Validation: an executable linked against this commit's actual buzz-core reproduced 30030: publication=1, readable=1 versus 30180: publication=1, readable=0. cargo test -p buzz-core -p buzz-media -p buzz-db passed (708 passed, 733 infrastructure tests ignored). git diff --check passed. The separate relay test run stalled in audio/configuration tests and was stopped; PostgreSQL media journeys and desktop workflows were not run locally.
TheSentinel454
left a comment
There was a problem hiding this comment.
🤖 Approving at 82c19907. The relay read and publish authorization looks sound, and it defaults to off. The remaining old-desktop avatar follow-ups need to land before anyone turns on enforce.
82c1990 to
558368d
Compare
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 Automated review at 558368d1104cfc54e0272725ad43cb9b8bc6ab16. This review lists only findings that the earlier reviews and inline threads don't already cover. Item 1 should be fixed before merge, because it applies whatever BUZZ_MEDIA_READ_MODE is set to. Items 2–5 should be fixed before anyone turns on enforce. Items 6–9 are minor.
The read check held up in every case I checked against how event reads are authorized: open and private channels, DMs, removed members, deleted and archived channels, community scoping, gated kinds, edits and replaced events. I found no way for a removed member, a non-member or another community to get bytes, and no case where enforce mode allows a read it should refuse. Every finding below was checked at source. None was reproduced live.
Blocking
1. A timed-out index build leaves the database at a version that older binaries refuse. 0084_media_uploaders.sql creates the two tables and commits as its own migration. 0085_media_edit_target.sql then builds the index on events under statement_timeout = '5s'. The upgrade test in migration.rs (run_migrations_through(&pool, 84) followed by the lock-timeout case) confirms that a failed 0085 leaves version 84 applied. The chart defaults to migrate.autoMigrate: true (deploy/charts/buzz/values.yaml:452), so on a database too large to build the index in 5 s:
- the new pod fails 0085 and crash-loops;
- any old pod that restarts finds an unknown version 84 and sqlx refuses to start it;
helm rollbackfails the same way.
Put the fallible index build in the first new migration (index, then tables), or merge both into one migration, so that a timeout records nothing and old pods keep starting.
Before enforce
2. Any member can make a public avatar, emoji or the community icon return 503 for every reader. The first lookup's needle is a bare ["imeta","x <hash>"] (crates/buzz-db/src/store/media.rs:377). Its imeta branch has no kind filter and no recheck that the tag carries a counted url. Each streamed row counts toward the 1,024-row / 16 MiB budget (store/media.rs:138-157) before reference_hashes discards rows that aren't references. Publication never sees the decoy, because x <hash> is not a URL. Ordinary messages can't carry it, since imeta validation requires a url whose hash equals x. But artifact revisions (kind 45010) are stored at handlers/ingest.rs:3141-3154, before imeta validation at :3393-3411, and buzz_core::artifact::validate accepts extra tags. About 1,025 small revisions in an open channel, carrying x-only imeta for a target hash, push every enforce-mode read of that hash over budget. The relay then returns 503, and each such read holds one of the pod-wide authorization slots. Real references to avatars, emoji, link-preview images and the icon never carry x, so the lookup always reaches the decoys first. Add an SQL recheck that the same tag carries a local url for the hash, mirroring the link-preview recheck, and run imeta validation on artifact revisions. Covering this needs a test with more than 1,024 x-only rows next to a readable avatar.
3. Editing a message makes its link-preview image and favicon unreadable to everyone, sender included. Once a live edit exists, the original kind 9/40002 event stops counting as a reference (store/media.rs:254-260). The desktop edit event carries imeta, emoji and mentions, plus an optional ["link-preview","none"], but never the snapshot tags (desktop/src-tauri/src/events.rs:359-381). Meanwhile the renderer keeps the original's non-imeta tags and keeps showing the preview (desktop/src/features/messages/lib/applyEditTagOverlay.mjs:34,53-64). Fixing a typo therefore turns the preview image into a 404. Either carry the snapshot tags forward in the edit, or keep counting the original's link-preview tags while the edit doesn't suppress them.
4. Persona definition avatars (kind 30175) return 404 even to their owner. publication_hashes walks every string in the plaintext persona JSON (crates/buzz-core/src/media.rs:141-160). Publishing a persona with avatar_url therefore records a publication marker and ends the owner's upload preview. reference_hashes only parses kind-0 content (media.rs:117-131), so the persona avatar is never counted. A persona with no deployed agent then can't load its own avatar. Count avatar_url on 30175 as a reference (the existing shared/author-only filter already gates who can read it), or keep it out of the marker scan.
5. Agent and CLI edits break the edited message's attachments. buzz_sdk::build_edit emits only h and e (crates/buzz-sdk/src/builders.rs:428-439), and it is the only path behind buzz messages edit. The original send embeds the file in the body as well as in imeta. After an edit, the original no longer counts, the edit has no imeta, and the body's image URL returns 404. Carry imeta forward in build_edit, or list this in the operator guide's enforce prerequisites alongside the avatar clients.
Minor
- Reports reveal whether a blob exists. A kind-1984 report with an
xtag answers either "report target blob not found" or accepts the report (crates/buzz-relay/src/handlers/report.rs:78-90). Anyone holding a hash can use that to confirm a private blob exists, contrary to the existence-hiding indocs/operator-private-media.md. This behaviour predates the PR. The event-target branch of the same handler already treats unreadable as missing, so the blob branch could checkmedia_accessfirst. - Large profiles can exhaust the work budget. The kind-0 lookup matches any current profile whose content contains the literal hash (
store/media.rs:263-269), and it counts each row's bytes before checking it. With 256 KiB of content per profile, about 64 members paddingaboutwith a victim's avatar hash push that avatar's reads past 16 MiB and into 503. Count bytes only for rows that turn out to be references. - Shadow mode can refuse reads. A transient sidecar error returns 503 early (
crates/buzz-relay/src/api/media_access.rs:56-67), before the enforce-only refusal at:101. Shadow mode then refuses a read even though the guide documents it as never refusing, and nobuzz_media_read_access_totalsample is recorded. - Wrong migration number.
.env.example:189says "Apply migration 0083 first"; the relevant migrations are 0084–0085.
jmecom
left a comment
There was a problem hiding this comment.
🤖 Re-reviewed 558368d1104cfc54e0272725ad43cb9b8bc6ab16. My original kind-30180 finding is fixed in both the Rust extractor and SQL filter. The new production-path regression covers fresh upload, publication, GET/HEAD by uploader and another member, and replacement. The current-head PostgreSQL CI job passed that journey.
I am still withholding approval for the artifact-driven media denial described in item 2 of the latest review. A member who can write artifacts to an open channel can publish valid kind-45010 events containing only imeta: x <victim-hash>. The artifact path returns before ordinary imeta validation, and its validator accepts this extra tag. An executable using the current production validator/extractors confirmed artifact_envelope_valid=true publication_hashes=0 reference_hashes=0.
The database nevertheless selects those rows in probe 0 and counts them before discarding non-references. With 1,025 such artifacts, a lookup fails before reaching a legitimate avatar, emoji or community-icon reference; enforce-mode reads return 503. Please filter non-reference candidates before applying the SQL/Rust work budget and cover the artifact ingest path, with a regression containing more than 1,024 decoys beside a readable avatar. The validator/extractor behavior was executed; the persistence/query/HTTP chain was verified at source, not reproduced end to end.
I do not independently treat migration ordering as another blocker: database rule 5 and this PR's deployment guide explicitly require old binaries to skip auto-migration on restart/rollback and large deployments to prebuild. Those prerequisites still apply.
Validation: 708 local core/media/database package tests passed, 734 infrastructure tests ignored; git diff --check passed. CI run 42293648360 succeeded at this head, including 1,464 PostgreSQL tests. No local desktop workflow was run.
jmecom
left a comment
There was a problem hiding this comment.
🤖 Approved as requested. The findings in my previous review remain unchanged.
Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz> Signed-off-by: Brad Seiler <seiler@squareup.com>
Use strict JSON path mode for retained previews and shared-persona gating; lax mode unwraps matching tags to scalar names, making preview events malformed and shared references fail the pre-budget gate. 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>
Nostr builders strip self p tags by default. Allow self-tagging for the owner blob report so it reaches report media authorization. Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz> Signed-off-by: Brad Seiler <seiler@squareup.com>
558368d to
3d1aa74
Compare
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 Approving at 3d1aa743. Every item from the review at 558368d1 is addressed:
- Migration ordering: the index build and both tables are now one migration (
0086_private_media.sql), index first, so a timed-out build records no version and old pods keep starting. x-only imeta decoys: the first lookup now requires the same imeta tag to carry a matching localurlbefore a row counts toward the budget, and artifact revisions (kind 45010) now run the same imeta validation as messages.- Link previews after an edit: the original's snapshot image and favicon stay referenced unless the original or a live edit carries
["link-preview","none"], in both the Rust extractor and the SQL lookup. - Persona avatars (kind 30175):
avatar_urlnow counts as a reference in both places. - SDK/CLI edits: listed as an enforce blocker in
docs/operator-private-media.md, which was one of the two options. - Report existence oracle: in enforce mode, a blob report now answers "not found" unless the reporter can read the blob.
- Profile padding: the kind-0 lookup now matches only the
picture/image/bannerfields in SQL, so paddingaboutno longer uses up the budget. - Shadow mode: sidecar errors now record a metric and allow the read in shadow mode.
.env.example: now names migration 0086.
Nonblocking: docs/operator-private-media.md:96 says "This review item is deferred, not fixed." That describes the review process, not the system, so "Not yet supported" would read better in an operator guide.
Summary
Removed private-channel members can currently download attachments if they know the URL. With
BUZZ_MEDIA_READ_MODE=enforce, fresh GET, HEAD and Range requests check current membership and readable references on the primary database. Removed members receive 404; remaining members retain access, and rejoining restores access. Thumbnail aliases use the parent attachment’s permissions.Publication checks also prevent copied private URLs from granting new access. They cover attachments, link previews, profile pictures, banners, group pictures and emoji. Persona creation and changed-avatar edits, plus team and individual snapshot imports, transfer animated posters and animations under the agent’s signer before profile publication. Failed persona transfers preserve existing local records; import failures appear in the results.
Authorization uses bounded queries, a two-second statement deadline and transaction-local JIT suppression. An index on edit targets avoids broad scans. Publications have a 512-reference limit, and capacity waits up to 500 ms before refusal.
Protection defaults off. Existing downloaded bytes cannot be recalled. All modes stop creating immutable HTTP cache entries.
Related issue
Related approaches: #7565, #5185, #8451 and #8444. This change evaluates live stored references directly.
Testing
Published head:
3d1aa7431cc495a001a0f13e95e2e422c5a77ab1, rebased on main2cd40badb85de059a6916206e133da7a0a88fb2e. Push hooks were SKIPPED with explicit author approval. At this head, core/DB/relay all-target Clippy (-D warnings), full packages/integration tests/doctests (serial: 2325 passed, 1393 ignored), and eight PostgreSQL migration/media checks passed. The profile-filter mutation returned the expected HTTP 503 (exit 100), then the restored clean tree passed the complete HTTP journey (1 passed, 2395 skipped, exit 0). The corrected headless enforce rerun passed 40 checks with zero failures through real CLI events and HTTP against isolated services; it used an S3 shim and did not exercise desktop UI or SDK/CLI attachment edits. CI and the final independent verdict remain pending. The earlier complete HTTP journey passed atf20b449262(identical source tree), with its evidence attributed separately. Earlier failing preview/report-fixture runs are preserved; they do not count as passes. The initial two-thread package run failed a JWKS warning-capture assertion; the complete serial rerun passed without source changes. The first headless run had a script parsing failure; its corrected rerun is the 40-check pass above.The HTTP journey covers upload/publication, fresh GET/HEAD/Range revocation and rejoin, emoji replacement, edited previews, persona visibility and non-reference decoys. The migration regression forces an index timeout, verifies no 0086 history or media tables, then proves a migrator through the base's 0085 can restart.
Human attachment removal/rejoin testing passed on the pinned
1f08a8e0fd2edf19dc94321f7258518149fe1bf6trial. Native animated persona edits and team/individual import acceptance remain incomplete. That old-snapshot database stays intact and cannot run the reconciled binary; a new trial needs a separate fresh database.Rollout prerequisites
The prebuild timeout finding and avatar-transfer finding are implemented and independently reviewed.
Schema change checklist
Generated with Codex