Skip to content

feat(projects): add agent and CLI project-home support - #6590

Draft
thomaspblock wants to merge 3 commits into
mainfrom
projects-channel-first-pt1-agent-cli
Draft

feat(projects): add agent and CLI project-home support#6590
thomaspblock wants to merge 3 commits into
mainfrom
projects-channel-first-pt1-agent-cli

Conversation

@thomaspblock

Copy link
Copy Markdown
Contributor

Summary

  • inject bounded project-home identity and repository context into managed agent sessions
  • add project-aware CLI flows for creating projects, repositories, issues, and related channels
  • match project homes through the existing relay query surface, then filter channel metadata client-side

This is Part 1 of the channel-first Projects stack. Part 2 contains project creation and model foundations.

Testing

  • cargo fmt --all -- --check
  • cargo clippy -p buzz-cli -p buzz-acp --all-targets -- -D warnings
  • cargo test -p buzz-cli -p buzz-acp — 1,184 tests passed, 1 doc test ignored
  • full pre-push gate passed

Post-Deploy Monitoring & Validation

  • validate project-home agent context and project-aware CLI commands against a staging relay
  • healthy signals: project context matches the active channel, explicit repo coordinates remain stable, and normal channels receive no project block
  • failure signals: cross-channel project context, duplicate project creation, or commands targeting an unrelated repository; mitigate by reverting this PR

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
thomaspblock requested a review from a team as a code owner August 23, 2026 01:02
@thomaspblock
thomaspblock marked this pull request as draft August 23, 2026 03:53

@thomaspblock thomaspblock left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 by created_at, then the first parseable event wins.
  • crates/buzz-cli/src/commands/project_channel.rs:27-31: let project = pick_oldest_listed(&projects); followed by if 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

  1. An attacker who knows a project channel UUID publishes a listed kind:30621 carrying that buzz-channel and an a tag for the attacker's repository. This is protocol-valid and requires no authority over the channel.
  2. The attacker gives it an earlier accepted timestamp than the legitimate project (or simply publishes before project creation).
  3. ACP selects that event as the channel's project home and promotes its name/owner/repository into generated [Context] instructions.
  4. 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.
  5. 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 thomaspblock left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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_announcement at lines 94-104, but this fallback does not call it.

Trigger scenario

  1. The caller already owns repo 30617:<caller>:app, bound to channel A (or unbound).
  2. They own a repository-empty project home with slug app in channel B.
  3. buzz issues create --channel B finds no authoritative project/member and no caller-owned repo bound to B, then reaches ensure_default_repo.
  4. 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 its buzz-channel.
  5. 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-101 likewise returns only tag.get(1) for each maintainers tag.
  • VISION_PROJECTS.md:27 and NIP-34 model maintainers as 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 thomaspblock left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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-205 now calls require_repo_channel_binding before reusing or attaching a same-slug existing repository, so a repository bound to channel A cannot route a channel-B issue.
  • require_repo_channel_binding uses the first buzz-channel value, matching the relay's fail-closed binding semantics, and rejects both mismatched and absent bindings.
  • ACP's multi_tag_values and CLI's tag.as_slice()[1..] now inspect every pubkey value in every maintainers tag, 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.

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