feat(desktop): add team sharing to community catalog - #3995
Conversation
5c11456 to
8bd3c8e
Compare
28c614f to
a4c603a
Compare
8ed79f3 to
f573bdc
Compare
wesbillman
left a comment
There was a problem hiding this comment.
Reviewing on Wes's behalf. Requesting changes for two security blockers in the catalog review surface:
-
Verify relay events before they can affect paging, coordinate selection, attribution, or displayed content.
fetchCatalogEventscurrently inserts every rawRelayEventreturned by the socket intobyIdand derivesoldestCreatedAtfrom it;sharedCatalogHeadsthen trusts the rawid,pubkey,created_at, tags, and content.RelayClient.fetchEventsis only a transport fetch and does not verify signatures. A forged/unsigned 30178 event can therefore appear under another publisher, claim or retract their coordinate ahead of a valid signed head, and control the pagination cursor. The backend re-fetch and verification correctly prevents adoption, but it does not protect browsing, publisher attribution, remote image loads, or denial/shadowing of legitimate entries. Please verify ID + signature on a fresh wire-shaped object before adding an event to the paging map or deriving a cursor/head from it, and add regressions for a forged newer head, forged author/content, malformed signature, and an invalid event attempting to controluntil. The equivalent hardening and tests already exist in PR #4220 for kind 30175; this shared helper should preserve that invariant for both 30175 and 30178 rather than regress it. -
Render executable team/member instructions byte-for-byte, not as Markdown, and reject non-reviewable controls at the adoption boundary.
TeamCatalogDialog.tsxpasses bothteam.instructionsandmember.systemPromptthrough the chat Markdown renderer. Markdown can conceal spoiler bodies, link destinations, and image sources, so the instructions reviewed in the dialog can differ materially from the plaintext later executed by the adopted agents. The current team catalog validators only enforce schema/size bounds, so bidi/default-ignorable controls can also make reviewed text differ from execution. Please use the literal instruction-review component/invariant from PR #4220 and apply its executable-text validation to team instructions and every member prompt before publication/adoption. Add coverage proving Markdown syntax stays literal, hidden controls fail closed, and accepted bytes are preserved exactly through adoption.
The feature has unusually substantial coverage otherwise: this head adds/changes 62 test/spec/fixture files, including the Rust adoption/pending/sharing/reconcile suites, TS relay/parser tests, and Playwright catalog/screenshot specs. CI is green at e64aa20ff81e236ca068aab3b9ad93142fb839e3, but the existing fixtures use placeholder sig: "sig" values and therefore do not exercise the first trust boundary.
e64aa20 to
131a906
Compare
3bcca7e to
f869c80
Compare
Implements the backend for 30178 team catalog sharing on the community catalog. No UI callers yet — this PR is the backend half of a two-PR split; desktop PR #3995 (stacked here) adds the TS parse layer, hooks, CommunityCatalogDialog, and e2e. Changes: - team_catalog.rs: 30178 projection builder + size contracts (32 KiB ceiling, per-field bounds, avatar downscaling for raster data URLs); build_team_catalog_event/content, team_catalog_content_from_event, tombstone_team_catalog_coordinate - commands/teams/pending.rs: publish/unshare/tombstone commands with relay-scoped share state; refresh_or_retract_shared_head_at for immediate retraction on member edits that exceed the size contract; persona-edit refresh guard so unrelated personas are never embedded - commands/teams/adopt/: add_team_from_catalog with backend head verification + byte-level rollback across both stores; plan_add with full member provenance (owner, d-tag, member-key, projection-hash); builtin reuse via hint matching; commit_stores atomic writer - event_sync.rs: reconcile_team_catalog_heads_at startup reconcile; republish changed heads, tombstone unrebuildable ones, skip unshared; multi-head continuation so a single pass handles all shared teams - Rust tests: team_catalog/tests.rs (999 lines), adopt/tests.rs (999), pending/tests.rs (679), event_sync_team_catalog_tests.rs (436) - 27 shared JSON parity fixtures (team_catalog_content/) - Existing TeamRecord + AgentDefinition types extended with sharing fields (team_catalog_source, catalog_source, shared flag, etc.) - All commands registered in lib.rs; dormant until UI PR merges Stack: this PR -> #3995 Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
0acacf4 to
16a1ff3
Compare
f869c80 to
72c1491
Compare
Implements the backend for 30178 team catalog sharing on the community catalog. No UI callers yet — this PR is the backend half of a two-PR split; desktop PR #3995 (stacked here) adds the TS parse layer, hooks, CommunityCatalogDialog, and e2e. Changes: - team_catalog.rs: 30178 projection builder + size contracts (32 KiB ceiling, per-field bounds, avatar downscaling for raster data URLs); build_team_catalog_event/content, team_catalog_content_from_event, tombstone_team_catalog_coordinate - commands/teams/pending.rs: publish/unshare/tombstone commands with relay-scoped share state; refresh_or_retract_shared_head_at for immediate retraction on member edits that exceed the size contract; persona-edit refresh guard so unrelated personas are never embedded - commands/teams/adopt/: add_team_from_catalog with backend head verification + byte-level rollback across both stores; plan_add with full member provenance (owner, d-tag, member-key, projection-hash); builtin reuse via hint matching; commit_stores atomic writer - event_sync.rs: reconcile_team_catalog_heads_at startup reconcile; republish changed heads, tombstone unrebuildable ones, skip unshared; multi-head continuation so a single pass handles all shared teams - Rust tests: team_catalog/tests.rs (999 lines), adopt/tests.rs (999), pending/tests.rs (679), event_sync_team_catalog_tests.rs (436) - 27 shared JSON parity fixtures (team_catalog_content/) - Existing TeamRecord + AgentDefinition types extended with sharing fields (team_catalog_source, catalog_source, shared flag, etc.) - All commands registered in lib.rs; dormant until UI PR merges Stack: this PR -> #3995 Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
16a1ff3 to
e5e5c2a
Compare
72c1491 to
dc6f777
Compare
58d52a5 to
76da912
Compare
Implements the backend for 30178 team catalog sharing on the community catalog. No UI callers yet — this PR is the backend half of a two-PR split; desktop PR #3995 (stacked here) adds the TS parse layer, hooks, CommunityCatalogDialog, and e2e. Changes: - team_catalog.rs: 30178 projection builder + size contracts (32 KiB ceiling, per-field bounds, avatar downscaling for raster data URLs); build_team_catalog_event/content, team_catalog_content_from_event, tombstone_team_catalog_coordinate - commands/teams/pending.rs: publish/unshare/tombstone commands with relay-scoped share state; refresh_or_retract_shared_head_at for immediate retraction on member edits that exceed the size contract; persona-edit refresh guard so unrelated personas are never embedded - commands/teams/adopt/: add_team_from_catalog with backend head verification + byte-level rollback across both stores; plan_add with full member provenance (owner, d-tag, member-key, projection-hash); builtin reuse via hint matching; commit_stores atomic writer. After the durable commit, enqueue retention heads for every member copy the add wrote or reactivated plus the adopted team, so a crash before the next boot reconcile cannot lose the only adopted copy; reused built-ins and replays write nothing and enqueue nothing. - event_sync.rs: reconcile_team_catalog_heads_at startup reconcile; republish changed heads, tombstone unrebuildable ones, skip unshared; multi-head continuation so a single pass handles all shared teams - persona_events.rs: domination-aware flush for kind:5 tombstones. A tombstone is signed strictly past the future-dated head it retracts, so its retained created_at is the domination floor. The relay ingest gate rejects events beyond ±900s of server time, so a byte-frozen future-dated replay can age out of the acceptance window and strand the head live forever. Flush now re-dates to now when the floor has passed, publishes at the floor when it is within the window, and leaves the tombstone pending to converge when the floor is further ahead than the window — never emitting an event the relay rejects. - Rust tests: team_catalog/tests.rs, adopt/tests.rs + adopt/tests/ retention.rs (adoption-path pending 30175/30176 enqueue, idempotent replay, reused-builtin/reactivation provenance), pending/tests.rs (incl. gate tests driving both the 30176 and 30178 tombstones through a stub relay that enforces the real ±900s ingest gate, covering the delayed/offline-retry case), event_sync_team_catalog_tests.rs - 27 shared JSON parity fixtures (team_catalog_content/) - Existing TeamRecord + AgentDefinition types extended with sharing fields (team_catalog_source, catalog_source, shared flag, etc.) - All commands registered in lib.rs; dormant until UI PR merges Stack: this PR -> #3995 Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
6a57da7 to
da22f55
Compare
Implements the backend for 30178 team catalog sharing on the community catalog. No UI callers yet — this PR is the backend half of a two-PR split; desktop PR #3995 (stacked here) adds the TS parse layer, hooks, CommunityCatalogDialog, and e2e. Changes: - team_catalog.rs: 30178 projection builder + size contracts (32 KiB ceiling, per-field bounds, avatar downscaling for raster data URLs); build_team_catalog_event/content, team_catalog_content_from_event, tombstone_team_catalog_coordinate - commands/teams/pending.rs: publish/unshare/tombstone commands with relay-scoped share state; refresh_or_retract_shared_head_at for immediate retraction on member edits that exceed the size contract; persona-edit refresh guard so unrelated personas are never embedded - commands/teams/adopt/: add_team_from_catalog with backend head verification + byte-level rollback across both stores; plan_add with full member provenance (owner, d-tag, member-key, projection-hash); builtin reuse via hint matching; commit_stores atomic writer. The commit and the retention enqueue are sequenced inside commit_and_enqueue, the sole route to a durable adoption commit: after the store write succeeds it enqueues retention heads for every member copy the add wrote or reactivated plus the adopted team, so a crash before the next boot reconcile cannot lose the only adopted copy; reused built-ins and replays write nothing and enqueue nothing, and a failed commit enqueues nothing. - event_sync.rs: reconcile_team_catalog_heads_at startup reconcile; republish changed heads, tombstone unrebuildable ones, skip unshared; multi-head continuation so a single pass handles all shared teams - persona_events.rs: domination-aware flush for kind:5 tombstones. A tombstone is signed strictly past the future-dated head it retracts, so its retained created_at is the domination floor. The relay ingest gate rejects events beyond ±900s of server time, so a byte-frozen future-dated replay can age out of the acceptance window and strand the head live forever. Flush now re-dates to now when the floor has passed, publishes at the floor when it is within the window, and leaves the tombstone pending to converge when the floor is further ahead than the window — never emitting an event the relay rejects. - Rust tests: team_catalog/tests.rs, adopt/tests.rs + adopt/tests/ retention.rs (adoption drives commit_and_enqueue through a spy commit + temp-dir scope: commits-then-enqueues, commit-failure enqueues nothing, idempotent replay skips both, reused-builtin/reactivation provenance), pending/tests.rs (incl. gate tests driving both the 30176 and 30178 tombstones through a stub relay that enforces the real ±900s ingest gate, covering the delayed/offline-retry case), event_sync_team_catalog_tests.rs - 27 shared JSON parity fixtures (team_catalog_content/) - Existing TeamRecord + AgentDefinition types extended with sharing fields (team_catalog_source, catalog_source, shared flag, etc.) - All commands registered in lib.rs; dormant until UI PR merges Stack: this PR -> #3995 Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
da22f55 to
43026a7
Compare
f0c8190 to
f82b09b
Compare
Implements the backend for 30178 team catalog sharing on the community catalog. No UI callers yet — this PR is the backend half of a two-PR split; desktop PR #3995 (stacked here) adds the TS parse layer, hooks, CommunityCatalogDialog, and e2e. Changes: - team_catalog.rs: 30178 projection builder + size contracts (32 KiB ceiling, per-field bounds, avatar downscaling for raster data URLs); build_team_catalog_event/content, team_catalog_content_from_event, tombstone_team_catalog_coordinate - commands/teams/sharing.rs: set_team_shared routes publication through the flush loop (the single publisher) rather than submitting the prepared head directly. A direct submit ran outside the store lock and could land a shared head after a concurrent delete_team's tombstone — and 30178 replacement has no deletion watermark, so the deleted team went publicly live again. The flush re-reads each row before publishing, so once the delete has committed the purged head's row is gone and the flush skips it; Published/Queued is derived from the re-read pending flag. - commands/teams/pending.rs: publish/unshare/tombstone helpers with relay-scoped share state; refresh_or_retract_shared_head_at for immediate retraction on member edits that exceed the size contract; persona-edit refresh guard so unrelated personas are never embedded - commands/teams/adopt/: add_team_from_catalog with backend head verification + byte-level rollback across both stores; plan_add with full member provenance (owner, d-tag, member-key, projection-hash); builtin reuse via hint matching; commit_stores atomic writer. The commit and the retention enqueue are sequenced inside commit_and_enqueue, the sole route to a durable adoption commit: after the store write succeeds it enqueues retention heads for every member copy the add wrote or reactivated plus the adopted team, so a crash before the next boot reconcile cannot lose the only adopted copy. A provenance match on an already-active copy is now retained too, so a recovery retry after a crash between the persona write and post-commit retention still enqueues the orphaned member's 30175; reused built-ins and replays write nothing and enqueue nothing, and a failed commit enqueues nothing. - event_sync.rs: reconcile_team_catalog_heads_at startup reconcile; republish changed heads, tombstone unrebuildable ones, skip unshared; multi-head continuation so a single pass handles all shared teams - persona_events.rs: domination-aware flush for kind:5 tombstones. A tombstone is signed strictly past the future-dated head it retracts, so its retained created_at is the domination floor. The relay ingest gate rejects events beyond ±900s of server time, so a byte-frozen future-dated replay can age out of the acceptance window and strand the head live forever. Flush now re-dates to now when the floor has passed, publishes at the floor when it is within the window, and leaves the tombstone pending to converge when the floor is further ahead than the window — never emitting an event the relay rejects. - Rust tests: team_catalog/tests.rs, adopt/tests.rs + adopt/tests/ retention.rs (adoption drives commit_and_enqueue through a spy commit + temp-dir scope: commits-then-enqueues, commit-failure enqueues nothing, idempotent replay skips both, reused-builtin/reactivation provenance, partial-commit retry enqueues the orphaned member head), pending/tests.rs, sharing/tests.rs (incl. a gate test driving a delete's tombstone then a delayed share through a recording relay to prove the purged head is never republished after its tombstone), event_sync_team_catalog_tests.rs - 27 shared JSON parity fixtures (team_catalog_content/) - Existing TeamRecord + AgentDefinition types extended with sharing fields (team_catalog_source, catalog_source, shared flag, etc.) - All commands registered in lib.rs; dormant until UI PR merges Stack: this PR -> #3995 Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
43026a7 to
fb0a637
Compare
f82b09b to
7c9992c
Compare
Implements the backend for 30178 team catalog sharing on the community catalog. No UI callers yet — this PR is the backend half of a two-PR split; desktop PR #3995 (stacked here) adds the TS parse layer, hooks, CommunityCatalogDialog, and e2e. Changes: - team_catalog.rs: 30178 projection builder + size contracts (32 KiB ceiling, per-field bounds, avatar downscaling for raster data URLs); build_team_catalog_event/content, team_catalog_content_from_event, tombstone_team_catalog_coordinate - commands/teams/sharing.rs: set_team_shared routes publication through the flush loop (the single publisher) rather than submitting the prepared head directly. A direct submit ran outside the store lock and could land a shared head after a concurrent delete_team's tombstone — and 30178 replacement has no deletion watermark, so the deleted team went publicly live again. The flush re-reads each row before publishing, so once the delete has committed the purged head's row is gone and the flush skips it; Published/Queued is derived from the re-read pending flag. - commands/teams/pending.rs: publish/unshare/tombstone helpers with relay-scoped share state; refresh_or_retract_shared_head_at for immediate retraction on member edits that exceed the size contract; persona-edit refresh guard so unrelated personas are never embedded - commands/teams/adopt/: add_team_from_catalog with backend head verification + byte-level rollback across both stores; plan_add with full member provenance (owner, d-tag, member-key, projection-hash); builtin reuse via hint matching; commit_stores atomic writer. The commit and the retention enqueue are sequenced inside commit_and_enqueue, the sole route to a durable adoption commit: after the store write succeeds it enqueues retention heads for every member copy the add wrote or reactivated plus the adopted team, so a crash before the next boot reconcile cannot lose the only adopted copy. A provenance match on an already-active copy is now retained too, so a recovery retry after a crash between the persona write and post-commit retention still enqueues the orphaned member's 30175; reused built-ins and replays write nothing and enqueue nothing, and a failed commit enqueues nothing. - event_sync.rs: reconcile_team_catalog_heads_at startup reconcile; republish changed heads, tombstone unrebuildable ones, skip unshared; multi-head continuation so a single pass handles all shared teams - persona_events.rs: domination-aware flush for kind:5 tombstones. A tombstone is signed strictly past the future-dated head it retracts, so its retained created_at is the domination floor. The relay ingest gate rejects events beyond ±900s of server time, so a byte-frozen future-dated replay can age out of the acceptance window and strand the head live forever. Flush now re-dates to now when the floor has passed, publishes at the floor when it is within the window, and leaves the tombstone pending to converge when the floor is further ahead than the window — never emitting an event the relay rejects. - Rust tests: team_catalog/tests.rs, adopt/tests.rs + adopt/tests/ retention.rs (adoption drives commit_and_enqueue through a spy commit + temp-dir scope: commits-then-enqueues, commit-failure enqueues nothing, idempotent replay skips both, reused-builtin/reactivation provenance, partial-commit retry enqueues the orphaned member head), pending/tests.rs, sharing/tests.rs (incl. a gate test driving a delete's tombstone then a delayed share through a recording relay to prove the purged head is never republished after its tombstone), event_sync_team_catalog_tests.rs - 27 shared JSON parity fixtures (team_catalog_content/) - Existing TeamRecord + AgentDefinition types extended with sharing fields (team_catalog_source, catalog_source, shared flag, etc.) - All commands registered in lib.rs; dormant until UI PR merges Stack: this PR -> #3995 Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
fb0a637 to
01b312a
Compare
Implements the backend for 30178 team catalog sharing on the community catalog. No UI callers yet — this PR is the backend half of a two-PR split; desktop PR #3995 (stacked here) adds the TS parse layer, hooks, CommunityCatalogDialog, and e2e. Changes: - team_catalog.rs: 30178 projection builder + size contracts (32 KiB ceiling, per-field bounds, avatar downscaling for raster data URLs); build_team_catalog_event/content, team_catalog_content_from_event, tombstone_team_catalog_coordinate - commands/teams/sharing.rs: set_team_shared routes publication through the flush loop (the single publisher) rather than submitting the prepared head directly. A direct submit ran outside the store lock and could land a shared head after a concurrent delete_team's tombstone — and 30178 replacement has no deletion watermark, so the deleted team went publicly live again. The flush re-reads each row before publishing, so once the delete has committed the purged head's row is gone and the flush skips it; Published/Queued is derived from the re-read pending flag. - commands/teams/pending.rs: publish/unshare/tombstone helpers with relay-scoped share state; refresh_or_retract_shared_head_at for immediate retraction on member edits that exceed the size contract; persona-edit refresh guard so unrelated personas are never embedded - commands/teams/adopt/: add_team_from_catalog with backend head verification + byte-level rollback across both stores; plan_add with full member provenance (owner, d-tag, member-key, projection-hash); builtin reuse via hint matching; commit_stores atomic writer. The commit and the retention enqueue are sequenced inside commit_and_enqueue, the sole route to a durable adoption commit: after the store write succeeds it enqueues retention heads for every member copy the add wrote or reactivated plus the adopted team, so a crash before the next boot reconcile cannot lose the only adopted copy. A provenance match on an already-active copy is now retained too, so a recovery retry after a crash between the persona write and post-commit retention still enqueues the orphaned member's 30175; reused built-ins and replays write nothing and enqueue nothing, and a failed commit enqueues nothing. - event_sync.rs: reconcile_team_catalog_heads_at startup reconcile; republish changed heads, tombstone unrebuildable ones, skip unshared; multi-head continuation so a single pass handles all shared teams - persona_events.rs: domination-aware flush for kind:5 tombstones. A tombstone is signed strictly past the future-dated head it retracts, so its retained created_at is the domination floor. The relay ingest gate rejects events beyond ±900s of server time, so a byte-frozen future-dated replay can age out of the acceptance window and strand the head live forever. Flush now re-dates to now when the floor has passed, publishes at the floor when it is within the window, and leaves the tombstone pending to converge when the floor is further ahead than the window — never emitting an event the relay rejects. - Rust tests: team_catalog/tests.rs, adopt/tests.rs + adopt/tests/ retention.rs (adoption drives commit_and_enqueue through a spy commit + temp-dir scope: commits-then-enqueues, commit-failure enqueues nothing, idempotent replay skips both, reused-builtin/reactivation provenance, partial-commit retry enqueues the orphaned member head), pending/tests.rs, sharing/tests.rs (incl. a gate test driving a delete's tombstone then a delayed share through a recording relay to prove the purged head is never republished after its tombstone), event_sync_team_catalog_tests.rs - 27 shared JSON parity fixtures (team_catalog_content/) - Existing TeamRecord + AgentDefinition types extended with sharing fields (team_catalog_source, catalog_source, shared flag, etc.) - All commands registered in lib.rs; dormant until UI PR merges Stack: this PR -> #3995 Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
01b312a to
3b091fe
Compare
7c9992c to
5f65b69
Compare
Implements the backend for 30178 team catalog sharing on the community catalog. No UI callers yet — this PR is the backend half of a two-PR split; desktop PR #3995 (stacked here) adds the TS parse layer, hooks, CommunityCatalogDialog, and e2e. Changes: - team_catalog.rs: 30178 projection builder + size contracts (32 KiB ceiling, per-field bounds, avatar downscaling for raster data URLs); build_team_catalog_event/content, team_catalog_content_from_event, tombstone_team_catalog_coordinate - commands/teams/sharing.rs: set_team_shared routes publication through the flush loop (the single publisher) rather than submitting the prepared head directly. A direct submit ran outside the store lock and could land a shared head after a concurrent delete_team's tombstone — and 30178 replacement has no deletion watermark, so the deleted team went publicly live again. The flush re-reads each row before publishing, so once the delete has committed the purged head's row is gone and the flush skips it; Published/Queued is derived from the re-read pending flag. - commands/teams/pending.rs: publish/unshare/tombstone helpers with relay-scoped share state; refresh_or_retract_shared_head_at for immediate retraction on member edits that exceed the size contract; persona-edit refresh guard so unrelated personas are never embedded - commands/teams/adopt/: add_team_from_catalog with backend head verification + byte-level rollback across both stores; plan_add with full member provenance (owner, d-tag, member-key, projection-hash); builtin reuse via hint matching; commit_stores atomic writer. The commit and the retention enqueue are sequenced inside commit_and_enqueue, the sole route to a durable adoption commit: after the store write succeeds it enqueues retention heads for every member copy the add wrote or reactivated plus the adopted team, so a crash before the next boot reconcile cannot lose the only adopted copy. A provenance match on an already-active copy is now retained too, so a recovery retry after a crash between the persona write and post-commit retention still enqueues the orphaned member's 30175; reused built-ins and replays write nothing and enqueue nothing, and a failed commit enqueues nothing. - event_sync.rs: reconcile_team_catalog_heads_at startup reconcile; republish changed heads, tombstone unrebuildable ones, skip unshared; multi-head continuation so a single pass handles all shared teams - persona_events.rs: domination-aware flush for kind:5 tombstones. A tombstone is signed strictly past the future-dated head it retracts, so its retained created_at is the domination floor. The relay ingest gate rejects events beyond ±900s of server time, so a byte-frozen future-dated replay can age out of the acceptance window and strand the head live forever. Flush now re-dates to now when the floor has passed, publishes at the floor when it is within the window, and leaves the tombstone pending to converge when the floor is further ahead than the window — never emitting an event the relay rejects. - Rust tests: team_catalog/tests.rs, adopt/tests.rs + adopt/tests/ retention.rs (adoption drives commit_and_enqueue through a spy commit + temp-dir scope: commits-then-enqueues, commit-failure enqueues nothing, idempotent replay skips both, reused-builtin/reactivation provenance, partial-commit retry enqueues the orphaned member head), pending/tests.rs, sharing/tests.rs (incl. a gate test driving a delete's tombstone then a delayed share through a recording relay to prove the purged head is never republished after its tombstone), event_sync_team_catalog_tests.rs - 27 shared JSON parity fixtures (team_catalog_content/) - Existing TeamRecord + AgentDefinition types extended with sharing fields (team_catalog_source, catalog_source, shared flag, etc.) - All commands registered in lib.rs; dormant until UI PR merges Stack: this PR -> #3995 Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
3b091fe to
2c299e9
Compare
5f65b69 to
7ea27a1
Compare
Implements the backend for 30178 team catalog sharing on the community catalog. No UI callers yet — this PR is the backend half of a two-PR split; desktop PR #3995 (stacked here) adds the TS parse layer, hooks, CommunityCatalogDialog, and e2e. Changes: - team_catalog.rs: 30178 projection builder + size contracts (32 KiB ceiling, per-field bounds, avatar downscaling for raster data URLs); build_team_catalog_event/content, team_catalog_content_from_event, tombstone_team_catalog_coordinate validate_team_catalog_content now applies the executable-text concealment validator to every member display name/prompt, name-pool entry, and the team instructions — the same invariant the persona catalog enforces at its parse boundary. Both publish (build_team_catalog_content) and adopt (team_catalog_content_from_event) funnel through this chokepoint, so a signed shared head cannot smuggle invisible/bidi-override characters into text delivered verbatim to the ACP harness (BUZZ_ACP_SYSTEM_PROMPT/BUZZ_ACP_TEAM_INSTRUCTIONS). - commands/teams/sharing.rs: set_team_shared routes publication through the flush loop (the single publisher) rather than submitting the prepared head directly. A direct submit ran outside the store lock and could land a shared head after a concurrent delete_team's tombstone — and 30178 replacement has no deletion watermark, so the deleted team went publicly live again. The flush re-reads each row before publishing, so once the delete has committed the purged head's row is gone and the flush skips it; Published/Queued is derived from the re-read pending flag. - commands/teams/pending.rs: publish/unshare/tombstone helpers with relay-scoped share state; refresh_or_retract_shared_head_at for immediate retraction on member edits that exceed the size contract; persona-edit refresh guard so unrelated personas are never embedded - commands/teams/adopt/: add_team_from_catalog with backend head verification + byte-level rollback across both stores; plan_add with full member provenance (owner, d-tag, member-key, projection-hash); builtin reuse via hint matching; commit_stores atomic writer. The commit and the retention enqueue are sequenced inside commit_and_enqueue, the sole route to a durable adoption commit: after the store write succeeds it enqueues retention heads for every member copy the add wrote or reactivated plus the adopted team, so a crash before the next boot reconcile cannot lose the only adopted copy. A provenance match on an already-active copy is now retained too, so a recovery retry after a crash between the persona write and post-commit retention still enqueues the orphaned member's 30175; reused built-ins and replays write nothing and enqueue nothing, and a failed commit enqueues nothing. - event_sync.rs: reconcile_team_catalog_heads_at startup reconcile; republish changed heads, tombstone unrebuildable ones, skip unshared; multi-head continuation so a single pass handles all shared teams - persona_events.rs: domination-aware flush for kind:5 tombstones. A tombstone is signed strictly past the future-dated head it retracts, so its retained created_at is the domination floor. The relay ingest gate rejects events beyond ±900s of server time, so a byte-frozen future-dated replay can age out of the acceptance window and strand the head live forever. Flush now re-dates to now when the floor has passed, publishes at the floor when it is within the window, and leaves the tombstone pending to converge when the floor is further ahead than the window — never emitting an event the relay rejects. - Rust tests: team_catalog/tests.rs, adopt/tests.rs + adopt/tests/ retention.rs (adoption drives commit_and_enqueue through a spy commit + temp-dir scope: commits-then-enqueues, commit-failure enqueues nothing, idempotent replay skips both, reused-builtin/reactivation provenance, partial-commit retry enqueues the orphaned member head), pending/tests.rs, sharing/tests.rs (incl. a gate test driving a delete's tombstone then a delayed share through a recording relay to prove the purged head is never republished after its tombstone), event_sync_team_catalog_tests.rs - 27 shared JSON parity fixtures (team_catalog_content/) - Existing TeamRecord + AgentDefinition types extended with sharing fields (team_catalog_source, catalog_source, shared flag, etc.) - All commands registered in lib.rs; dormant until UI PR merges Stack: this PR -> #3995 Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2c299e9 to
3700e31
Compare
Implements the backend for 30178 team catalog sharing on the community catalog. No UI callers yet — this PR is the backend half of a two-PR split; desktop PR #3995 (stacked here) adds the TS parse layer, hooks, CommunityCatalogDialog, and e2e. Changes: - team_catalog.rs: 30178 projection builder + size contracts (32 KiB ceiling, per-field bounds, avatar downscaling for raster data URLs); build_team_catalog_event/content, team_catalog_content_from_event, tombstone_team_catalog_coordinate validate_team_catalog_content now applies the executable-text concealment validator to every member display name/prompt, name-pool entry, and the team instructions — the same invariant the persona catalog enforces at its parse boundary. Both publish (build_team_catalog_content) and adopt (team_catalog_content_from_event) funnel through this chokepoint, so a signed shared head cannot smuggle invisible/bidi-override characters into text delivered verbatim to the ACP harness (BUZZ_ACP_SYSTEM_PROMPT/BUZZ_ACP_TEAM_INSTRUCTIONS). - commands/teams/sharing.rs: set_team_shared routes publication through the flush loop (the single publisher) rather than submitting the prepared head directly. A direct submit ran outside the store lock and could land a shared head after a concurrent delete_team's tombstone — and 30178 replacement has no deletion watermark, so the deleted team went publicly live again. The flush re-reads each row before publishing, so once the delete has committed the purged head's row is gone and the flush skips it; Published/Queued is derived from the re-read pending flag. - commands/teams/pending.rs: publish/unshare/tombstone helpers with relay-scoped share state; refresh_or_retract_shared_head_at for immediate retraction on member edits that exceed the size contract; persona-edit refresh guard so unrelated personas are never embedded - commands/teams/adopt/: add_team_from_catalog with backend head verification + byte-level rollback across both stores; plan_add with full member provenance (owner, d-tag, member-key, projection-hash); builtin reuse via hint matching; commit_stores atomic writer. The commit and the retention enqueue are sequenced inside commit_and_enqueue, the sole route to a durable adoption commit: after the store write succeeds it enqueues retention heads for every member copy the add wrote or reactivated plus the adopted team, so a crash before the next boot reconcile cannot lose the only adopted copy. A provenance match on an already-active copy is now retained too, so a recovery retry after a crash between the persona write and post-commit retention still enqueues the orphaned member's 30175; reused built-ins and replays write nothing and enqueue nothing, and a failed commit enqueues nothing. - event_sync.rs: reconcile_team_catalog_heads_at startup reconcile; republish changed heads, tombstone unrebuildable ones, skip unshared; multi-head continuation so a single pass handles all shared teams - persona_events.rs: domination-aware flush for kind:5 tombstones. A tombstone is signed strictly past the future-dated head it retracts, so its retained created_at is the domination floor. The relay ingest gate rejects events beyond ±900s of server time, so a byte-frozen future-dated replay can age out of the acceptance window and strand the head live forever. Flush now re-dates to now when the floor has passed, publishes at the floor when it is within the window, and leaves the tombstone pending to converge when the floor is further ahead than the window — never emitting an event the relay rejects. - Rust tests: team_catalog/tests.rs, adopt/tests.rs + adopt/tests/ retention.rs (adoption drives commit_and_enqueue through a spy commit + temp-dir scope: commits-then-enqueues, commit-failure enqueues nothing, idempotent replay skips both, reused-builtin/reactivation provenance, partial-commit retry enqueues the orphaned member head), pending/tests.rs, sharing/tests.rs (incl. a gate test driving a delete's tombstone then a delayed share through a recording relay to prove the purged head is never republished after its tombstone), event_sync_team_catalog_tests.rs - 27 shared JSON parity fixtures (team_catalog_content/) - Existing TeamRecord + AgentDefinition types extended with sharing fields (team_catalog_source, catalog_source, shared flag, etc.) - All commands registered in lib.rs; dormant until UI PR merges Stack: this PR -> #3995 Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
3700e31 to
3adde41
Compare
Extend the unified add-agent dialog (#5015) into a single Community Catalog surface that browses both shared agents and shared teams. The dialog keeps sections for personas and teams with type-tagged selection and a teams-preferred launch. TeamsSection's discover entry and the new-agent card both open this one dialog. Relay paging, signature verification, NIP-33 head selection, and untrusted content parsing for the kind 30178 team catalog live natively in the fetch_team_catalog Tauri command (team_catalog.rs), structurally mirroring fetch_persona_catalog. A catalog refresh crosses IPC once and never verifies a signature on the webview thread. teamCatalogRelay.ts is now a thin presentation and local-linkage layer over the verified projection. Parsing is all-or-nothing, identical to the add-time re-fetch: a team with any invalid member fails to parse and is dropped from the catalog, matching persona behavior. The prior partial-render of invalid-member teams (a warning banner on an entry that could never be added) is removed. Instruction review renders verbatim in a <pre> on all three surfaces so the text a user reviews is the text sent to the agent. TeamShareDialog publishes and unshares team catalog entries. Playwright e2e covers the unified create/catalog/import navigation, the teams catalog flow, and the screenshot regression set. Stack: #5112 -> this PR Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>








Stack: #5112 → this PR
Restack note: same patch as the reviewed
7ea27a1adrestack except (a) rebased onto currentorigin/main, and (b) a 2-line carry of main's #6501 label fix (Agent instruction→Agent instructions) into the renamedCommunityCatalogDialog.tsx, reconciling the modify/delete againstPersonaCatalogDialog.tsx.Stacks on the team catalog backend PR (#5112). Contains the
desktop/**changes that turn the add-agent surface into a single Community Catalog browsing both shared agents and shared teams.Data layer
Relay paging, signature verification, NIP-33 head selection, and untrusted-content parsing for the kind
30178team catalog live natively in thefetch_team_catalogTauri command (team_catalog.rs), structurally mirroringfetch_persona_catalog(persona_catalog.rs). A catalog refresh crosses IPC once and never verifies a signature on the webview thread.teamCatalogRelay.tsis now a thin presentation and local-linkage layer over the verified projection — it shapes entries for display and links each to a local team, and never parses or verifies.Parsing is all-or-nothing, identical to the add-time re-fetch in
add_team_from_catalog: a team with any invalid member fails to parse and the publication is dropped from the catalog, matching persona behavior. Behavior delta: the previously reviewed partial-render of invalid-member teams — a warning banner on an entry that could never be added — is removed. Invalid publications are dropped entirely rather than surfaced as un-addable.Hooks
useTeamCatalogRelay.tsmirrors the persona catalog hook: a community-keyed query overfetch_team_catalog, live invalidation on kind30178, share/unshare, and add-from-catalog (which invalidates both the teams and personas stores, since adopting a team copies its members as local personas).CommunityCatalogDialog
Single unified surface replacing the former separate dialogs. Agents and Teams appear as labeled sections with type-tagged selection and a teams-preferred launch.
TeamsSection's discover entry and the new-agent card both open this one dialog.PersonaCatalogDialog.tsxis removed; persona browsing now lives inside the unified dialog.TeamShareDialog
Publishes and unshares team catalog entries via
set_team_shared.e2e + screenshots
team-catalog.spec.tscovers the browse + adopt flow;team-catalog-screenshots.spec.tsproduces the pixel-regression set.e2eBridge.tsgainsmockTeamCatalogPublications, which mirrors the native command's head selection and shared gate and performs only a shallowv/name/members-array shape check; per-member validation stays in the Rust command.Follow-ups
None. The shared agent-definition text-safety policy (Unicode-control/bidi/zero-width rejection) already covers the team surface: #5112's
validate_team_catalog_contentchokepoint gates every field delivered verbatim to the harness on both the publish and adopt paths, and this PR's parse layer consumes that verified projection rather than re-validating on the webview thread.