feat(projects): add agent and CLI project-home support - #6590
feat(projects): add agent and CLI project-home support#6590thomaspblock wants to merge 3 commits into
Conversation
Give agents bounded project-home context and project-aware CLI operations while keeping channel matching client-filtered through the existing relay query surface. Signed-off-by: Thomas Petersen <thomasp@squareup.com>
thomaspblock
left a comment
There was a problem hiding this comment.
Cassandra adversarial/security review — needs work
The red Unit Tests job is not caused by this diff: it fails linking untouched buzz-voice with could not find native static library 'sherpa-onnx-c-api'. That check should be retried rather than patched in this projects PR.
I found two source-level blockers independently while tracing the new project-home resolution.
P1 — Any relay writer can hijack a channel's agent project context and redirect channel-scoped issues (confidence 100)
Evidence
crates/buzz-acp/src/prompt_project.rs:23-25:!event_is_unlisted(event) && event_has_tag_value(event, "buzz-channel", channel_id)crates/buzz-acp/src/prompt_project.rs:27-33: the matching events are ordered only bycreated_at, then the first parseable event wins.crates/buzz-cli/src/commands/project_channel.rs:27-31:let project = pick_oldest_listed(&projects);followed byif let Some(member) = first_member_repo(event) { return Ok(member); }docs/nips/NIP-MP.md:139:`buzz-channel` on a project is **metadata only**.docs/nips/NIP-MP.md:188:The relay MUST NOT check whether the signer owns, maintains, or has any relationship to a member repository.
Trigger scenario
- An attacker who knows a project channel UUID publishes a listed
kind:30621carrying thatbuzz-channeland anatag for the attacker's repository. This is protocol-valid and requires no authority over the channel. - The attacker gives it an earlier accepted timestamp than the legitimate project (or simply publishes before project creation).
- ACP selects that event as the channel's project home and promotes its name/owner/repository into generated
[Context]instructions. buzz issues create --channel <victim-channel>independently makes the same oldest-event choice and returns the attacker's first member coordinate without checking that the project signer controls the channel or that the member repo is actually bound to it.- A normal “create a task in this project” request is therefore signed against an unrelated attacker-chosen repository.
This crosses an integrity boundary: unauthenticated project metadata is being treated as authoritative routing configuration. Resolve the project from an authenticated channel-owned binding/type, or require a verifiable relationship between the selected project signer and channel authority. At minimum, channel-scoped repo resolution must verify the selected 30617 is bound to the requested channel and reject ambiguous projects rather than choosing oldest.
P1 — Global slug squatting lets any signer block another user's project creation (confidence 100)
Evidence
crates/buzz-cli/src/commands/projects.rs:373-379:other_listed_project(&fetch_projects_by_dtag(client, slug).await?, &caller_pubkey)causes a conflict when any other pubkey has the slug.docs/nips/NIP-MP.md:134:Only the signer can replace their (pubkey, 30621, d) coordinate.docs/nips/NIP-MP.md:194:newest created_at wins per (pubkey, 30621, d), and one pubkey can never overwrite another's coordinate.
Trigger scenario
An attacker publishes listed projects for common slugs (app, website, a known upcoming product name). Every later buzz projects create <slug> by every other identity is rejected locally, even though the protocol intentionally namespaces projects by signer. The suggested error action (“Add a repository to that project instead”) cannot work because editing is signer-only. Do not impose relay-wide uniqueness on an owner-namespaced coordinate; duplicate-card prevention needs an authority-scoped rule.
Additional adversarial risk retained in this PR comment
crates/buzz-cli/src/commands/project_channel.rs:178-185 adds the selected foreign project owner as a maintainers tag on an implicitly created caller-owned repository. Under docs/nips/NIP-MP.md:215-217, that tag is sufficient claim authority for the foreign signer. I did not live-test Desktop's resulting fold, but this should be removed or explicitly justified before merge; untrusted project metadata must not grant provenance/claim authority over a newly created repo.
Coverage: full 12-file diff read; traced ACP project lookup → generated context, CLI channel lookup → issue creation, implicit repo creation, project collision checks, NIP-MP authority and claim semantics. I did not mutate the branch or run a live hostile relay reproduction.
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz> Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
thomaspblock
left a comment
There was a problem hiding this comment.
Cassandra re-review of 7a9af2ac — one routing blocker remains
The original two P1 findings are fixed in the authoritative-selection path: foreign channel/project claims no longer route ACP or CLI, ambiguity fails closed, cross-signer slug/channel squatting is removed, and implicit repo creation no longer grants foreign maintainers authority.
P1 — Existing same-id repository bypasses the new channel-binding check (confidence 100)
Evidence
crates/buzz-cli/src/commands/project_channel.rs:181-188:if let Some(existing) = crate::commands::repos::fetch_own_repo_announcement(client, &repo_id).await? { let _ = try_add_own_repo_to_channel_project(client, channel, &repo_id).await; return Ok(ChannelProjectRepo { repo_owner: existing.pubkey.to_hex(), repo_id, }); }
- The new binding check exists in
repo_from_announcementat lines 94-104, but this fallback does not call it.
Trigger scenario
- The caller already owns repo
30617:<caller>:app, bound to channel A (or unbound). - They own a repository-empty project home with slug
appin channel B. buzz issues create --channel Bfinds no authoritative project/member and no caller-owned repo bound to B, then reachesensure_default_repo.fetch_own_repo_announcement("app")returns the channel-A repository. The code attaches it to the channel-B project and returns it without checking or rebinding itsbuzz-channel.- The issue is silently created against channel A's unrelated repository. Subsequent calls repeat the same misrouting, while ACP correctly refuses to recognize that member as authoritative for B.
The fallback must apply the same first-buzz-channel equality invariant before returning. If an existing same-id repo is bound elsewhere, fail with an actionable conflict or choose a non-colliding id; do not attach or route to it.
Advisory — maintainer authorization reads only the first value (confidence 75)
Evidence
crates/buzz-cli/src/commands/project_channel.rs:88-91:|| repo.tags.iter().any(|tag| { matches!(tag.as_slice(), [name, value, ..] if name == "maintainers" && value.eq_ignore_ascii_case(&signer)) })
crates/buzz-acp/src/prompt_project.rs:93-101likewise returns onlytag.get(1)for eachmaintainerstag.VISION_PROJECTS.md:27and NIP-34 modelmaintainersas a multi-value tag; Desktop deliberately reads all values (desktop/src/features/projects/projectModels.ts:283-285).
A valid ['maintainers', first, project_signer] repository authorizes the signer in Desktop but is rejected by both new routing implementations. Iterate all values after the tag name so ACP, CLI, and Desktop share one authority rule.
Re-review coverage: exact fix diff a6c5f1db..7a9af2ac; traced authoritative selection, ambiguity, project creation collisions, implicit repo fallback, and maintainer parsing. Report-only; no branch mutation.
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz> Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
thomaspblock
left a comment
There was a problem hiding this comment.
Cassandra final security/authority re-review — findings cleared at 7bbed3f1
No remaining security or adversarial findings in the incremental fix.
Verified:
crates/buzz-cli/src/commands/project_channel.rs:197-205now callsrequire_repo_channel_bindingbefore reusing or attaching a same-slug existing repository, so a repository bound to channel A cannot route a channel-B issue.require_repo_channel_bindinguses the firstbuzz-channelvalue, matching the relay's fail-closed binding semantics, and rejects both mismatched and absent bindings.- ACP's
multi_tag_valuesand CLI'stag.as_slice()[1..]now inspect every pubkey value in everymaintainerstag, matching NIP-34/Desktop semantics. - Regressions cover the mismatched existing binding and authorization by a later maintainer value.
- The prior fixes remain intact: project-home selection requires a channel-bound live member repository plus signer authority; ambiguity fails closed; cross-signer slug/channel squatting is absent; implicit creation does not grant foreign maintainer authority.
Verdict for my security/authority lane: merge-ready at exact head 7bbed3f127f25559fc301044842ee6582b2fdc9a. CI and independent correctness review are outside this verdict and were still in progress when checked.
Summary
This is Part 1 of the channel-first Projects stack. Part 2 contains project creation and model foundations.
Testing
cargo fmt --all -- --checkcargo clippy -p buzz-cli -p buzz-acp --all-targets -- -D warningscargo test -p buzz-cli -p buzz-acp— 1,184 tests passed, 1 doc test ignoredPost-Deploy Monitoring & Validation