Skip to content

feat(channels): support project-home channel surfaces - #6594

Open
thomaspblock wants to merge 4 commits into
projects-channel-first-pt2-creationfrom
projects-channel-first-pt3-channel-shell
Open

feat(channels): support project-home channel surfaces#6594
thomaspblock wants to merge 4 commits into
projects-channel-first-pt2-creationfrom
projects-channel-first-pt3-channel-shell

Conversation

@thomaspblock

Copy link
Copy Markdown
Contributor

Summary

  • classify and render project-home channels through the shared channel glyph and lifecycle helpers
  • let the normal channel pane host a project idle auxiliary surface and focus drawer
  • align channel management, headers, member bars, and empty-channel actions with project channel semantics

This is Part 3 of the channel-first Projects stack, based on #6591. Part 4 adds the project-home navigation and context experience.

Testing

  • focused channel lifecycle, pane helper, and project-home channel tests: 7/7 passed
  • Desktop unit suite: 5,422/5,422 passed
  • E2E-mode Desktop build passed
  • TypeScript, Biome, and differential file-size checks passed
  • full pre-push gate passed

Post-Deploy Monitoring & Validation

  • open normal, temporary, private, and project-home channels in the first staging Desktop session
  • healthy signals: normal channels retain their existing composer/thread behavior and project homes use the project glyph and auxiliary slot
  • failure signals: missing composer, incorrect channel kind, stuck focus drawer, or project chrome on a normal channel; mitigate by reverting this PR

Teach the shared channel shell to identify project homes, host an idle auxiliary surface, and present project-aware channel chrome without changing normal channel behavior.

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
…nel-first-pt3-channel-shell

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
@thomaspblock
thomaspblock marked this pull request as draft August 23, 2026 03:53
Bring the channel-shell stack onto the verified project creation and authority fixes before evaluating Part 3 behavior.

Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
@thomaspblock

Copy link
Copy Markdown
Contributor Author

Cassandra security/adversarial review — needs work

Reviewed the complete Part 3 diff and traced project enumeration through fetchProjectsbuildProjectReadModelsuseProjectsQueryuseIsProjectHomeChannel, including the authority rules added in the current #6590/#6591 base.

P1 — Any member can spoof an arbitrary channel as a project home (confidence: 100)

Evidence

return projects.some((project) => project.projectChannelId === channelId);
desktop/src/features/projects/lib/projectHomeChannel.ts:8

The value being trusted comes directly from the project signer's own presentation metadata:

const channel = getTag(event, "buzz-channel");
desktop/src/features/projects/projectModels.ts:359

channel && isValidProjectChannelId(channel) ? channel : null;
desktop/src/features/projects/projectModels.ts:361

A listed kind:30621 does not need a repository member: validation only imposes an upper bound (if (memberTags.length > MAX_PROJECT_MEMBERS)) at projectModels.ts:189-195. buildProjectReadModels then admits that project at projectModels.ts:497-509. Therefore any relay member who can publish a listed project can set buzz-channel to a victim channel UUID and make every Desktop classify that channel as a project home. This PR then shows project chrome and, in channel management, locks lifecycle conversion because projectLocked disables both ordinary choices (ChannelTypePicker.tsx:57,106-116). This is an authority-confusion/UI-spoofing path, not merely stale display data.

It also contradicts the stack's newly established rule in #6590: a project's buzz-channel is presentation metadata; project-home authority requires a live member repo whose first buzz-channel binds the same channel and whose owner/maintainers authorize the project signer. The Desktop classifier currently bypasses that rule.

Concrete trigger: Mallory publishes a listed kind:30621 with d=spoof and buzz-channel=<engineering-channel-uuid> (zero a tags is accepted). Alice refreshes Desktop; the ordinary engineering channel acquires the project glyph/type and its lifecycle controls become locked even though no repository owner authorized Mallory's claim.

Required direction: derive project-home classification from the same authoritative project↔repository↔channel relation used by the CLI/ACP base fixes (and fail closed on ambiguity), rather than from Project.projectChannelId alone. Add a regression test for a foreign/zero-member project claiming another channel and for an authorized repo-backed project.

Residual risk

I did not live-publish a hostile event to a shared relay. The code path is deterministic and the existing 5,422-test Desktop suite passes without covering this adversarial authority case.

No other security finding survived review of this diff.

Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
@thomaspblock

Copy link
Copy Markdown
Contributor Author

Cassandra security/adversarial re-review — b985807903eb77a982707e8855318ac9b771c393

Verdict: merge-ready in the security/authority lane. No findings.

I reviewed the full 25-file PR diff against projects-channel-first-pt2-creation, then traced the updated home-classification predicate through the project/repository read-model construction and live-head deduplication.

Authority path checked

  • A project event's bare buzz-channel assertion is no longer sufficient.
  • Classification requires a visible, live member repository whose validated buzz-channel equals the asserted project channel.
  • The project signer must equal that repository's owner or appear in its parsed multi-value maintainers tags.
  • Repository owners and maintainers are normalized to lowercase; malformed maintainer pubkeys are removed by the repository parser.
  • Deleted/stale addressable events and unavailable/hidden repositories do not supply authority, so those cases fail closed.
  • Multiple projects cannot weaken the predicate: each candidate must satisfy its own channel binding and signer authority.

Adversarial scenarios exercised/read

  • hostile signer points a project at someone else's channel with no member repo;
  • hostile signer enrolls another owner's repo but lacks maintainer authority;
  • authorized signer uses a repo bound to a different channel;
  • later value in a multi-value maintainers tag grants legitimate authority;
  • mixed-case signer identity;
  • null/unbound channel and missing visible repository.

The focused regression tests cover these acceptance/rejection paths. I also ran the full Desktop unit suite at the exact reviewed HEAD: 5,425 passed, 0 failed. Local HEAD and remote PR head both resolved to b985807903eb77a982707e8855318ac9b771c393 after fetch.

Residual risks: none identified in this lane. GitHub Desktop CI was still running at review time and remains a separate merge gate.

@thomaspblock
thomaspblock marked this pull request as ready for review August 23, 2026 14:44
@thomaspblock

Copy link
Copy Markdown
Contributor Author

Gauge review residuals — head b985807903eb77a982707e8855318ac9b771c393

Report-only findings from correctness/testing review (none blocking; details in channel report):

  1. [50] getChannelIntroKind projectHome param has no production caller. desktop/src/features/channels/ui/useChannelIntro.tsx:84 and :127 call getChannelIntroKind(activeChannel) without the flag, so the "project channel" intro label is reachable only from tests. Assumed Part-4 wiring — confirm, or the surface never ships.

  2. [50] Idle-drawer presence ignores open threads. desktop/src/features/channels/ui/ChannelPane.tsx:467-476: useFocusIdleDrawer is true whenever idleAuxiliaryPanel is set, independent of thread state. When a future caller passes the panel and a thread is open in split view, channelIsCovered marks the timeline inert (line 576) with no covering drawer rendered, and external focused-thread close requests route to onCloseIdleAuxiliaryPanel instead of onCloseThread. Unreachable today (no caller passes the prop) — trip-wire for Part 4.

  3. [50, residual risk] Authority predicate accepts a fully self-signed pair. desktop/src/features/projects/lib/projectHomeChannel.ts:19-23: a repo event self-signed with channelId = any channel UUID plus the same signer's listed project satisfies repository.owner === projectOwner. No check against the channel's creator, so any user knowing a channel UUID can render it as a Project home for all viewers (project glyph, "Project" label, TTL editor hidden via ChannelTypeSettings.tsx:133, lifecycle picker locked). This matches the Rust side's documented trust model (repo ownership = authority), so it may be accepted design — recording it so the acceptance is explicit.

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.

1 participant