Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 1 addition & 59 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -775,63 +775,6 @@ jobs:
${{ steps.artifacts.outputs.exe }}
${{ steps.artifacts.outputs.sig }}

desktop-release-smoke:
name: Desktop release smoke
if: github.repository == 'block/buzz'
runs-on: ubuntu-latest
needs: setup
timeout-minutes: 20
permissions:
contents: read
env:
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.setup.outputs.source_sha }}
persist-credentials: false
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
- name: Install desktop dependencies
run: just desktop-install-ci
- name: Get Playwright version
id: pw-version
run: echo "version=$(cd desktop && node -e \"console.log(require('@playwright/test/package.json').version)\")" >> "$GITHUB_OUTPUT"
- name: Restore Playwright browser cache
id: playwright-cache
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }}
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
- name: Install Playwright Chromium
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: cd desktop && pnpm exec playwright install chromium
- name: Install Playwright system dependencies
run: cd desktop && pnpm exec playwright install-deps chromium
- name: Save Playwright browser cache
if: steps.playwright-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }}
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
- name: Build test relay
run: cargo build --profile ci -p buzz-relay
- name: Run deterministic correctness smoke
env:
BUZZ_E2E_RELAY_BIN: ${{ github.workspace }}/target/ci/buzz-relay
BUZZ_RELEASE_SMOKE_ARTIFACT_DIR: ${{ github.workspace }}/release-smoke-artifacts
run: just desktop-release-smoke
- name: Upload release-smoke diagnostics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: desktop-release-smoke
path: |
release-smoke-artifacts
desktop/test-results
desktop/playwright-release-smoke-report
if-no-files-found: warn
retention-days: 14

assemble-manifest:
name: Assemble multi-platform latest.json
# Only the tag-bound setup path can reach this job.
Expand All @@ -842,10 +785,9 @@ jobs:
needs.release-macos-x64.result == 'success' &&
needs.release-linux.result == 'success' &&
needs.release-windows.result == 'success' &&
needs.desktop-release-smoke.result == 'success' &&
github.ref == format('refs/tags/desktop-v{0}', needs.setup.outputs.version)
runs-on: ubuntu-latest
needs: [setup, release, release-macos-x64, release-linux, release-windows, desktop-release-smoke]
needs: [setup, release, release-macos-x64, release-linux, release-windows]
timeout-minutes: 10
permissions:
contents: write
Expand Down
14 changes: 7 additions & 7 deletions .release/desktop-candidate.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"schema": 2,
"version": "0.5.12",
"base_sha": "757779bb1ef22cc4a1c233344baa0946d907e5a6",
"previous_tag": "desktop-v0.5.11",
"previous_base_sha": "4749bc7be3cdb78c2db4ce4864775ba7ab60b4cc",
"previous_merge_sha": "9e0c6b4320190f80b81998e9e5cbf2214d597dd2",
"tag": "desktop-v0.5.12",
"commit_count": 34
"version": "0.5.14",
"base_sha": "1b3dbcaaea882eeea90359c1db02e306d2f4f50a",
"previous_tag": "desktop-v0.5.13",
"previous_base_sha": "09768100ec3420f0aa7cd278bd00fe0baab5de8d",
"previous_merge_sha": "51beba603886d34e751349d12b33c0c5aeb92c28",
"tag": "desktop-v0.5.14",
"commit_count": 1
}
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# Changelog

## v0.5.14

### Desktop and shared changes

- None

### Other repository changes

- ci(release): remove desktop smoke gate ([#5914](https://github.com/block/buzz/pull/5914)) ([`1b3dbcaaea882eeea90359c1db02e306d2f4f50a`](https://github.com/block/buzz/commit/1b3dbcaaea882eeea90359c1db02e306d2f4f50a))

[Compare desktop-v0.5.13...desktop-v0.5.14](https://github.com/block/buzz/compare/desktop-v0.5.13...desktop-v0.5.14)

## v0.5.13

### Desktop and shared changes

- fix(desktop): restore the agent trading-card mint button ([#5900](https://github.com/block/buzz/pull/5900)) ([`263c9bf76c18f0cde6cec9fb43d22f8895319380`](https://github.com/block/buzz/commit/263c9bf76c18f0cde6cec9fb43d22f8895319380))
- Projects v3: unify sharing, discussions, and issue ownership ([#5792](https://github.com/block/buzz/pull/5792)) ([`122a8b8988869f0b1a7c056a76f7d16bfb0f6fdd`](https://github.com/block/buzz/commit/122a8b8988869f0b1a7c056a76f7d16bfb0f6fdd))

### Other repository changes

- fix(ci): read Playwright version without nested shell quoting ([#5910](https://github.com/block/buzz/pull/5910)) ([`09768100ec3420f0aa7cd278bd00fe0baab5de8d`](https://github.com/block/buzz/commit/09768100ec3420f0aa7cd278bd00fe0baab5de8d))
- fix(mobile): unwrap batched observer telemetry ([#5805](https://github.com/block/buzz/pull/5805)) ([`0bb7c60f824a05ac4d8c8569ee1e74d200069b45`](https://github.com/block/buzz/commit/0bb7c60f824a05ac4d8c8569ee1e74d200069b45))

[Compare desktop-v0.5.12...desktop-v0.5.13](https://github.com/block/buzz/compare/desktop-v0.5.12...desktop-v0.5.13)

## v0.5.12

### Desktop and shared changes
Expand Down
6 changes: 4 additions & 2 deletions crates/buzz-acp/src/base_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ The `buzz` CLI is your primary interface. Auth env vars: `BUZZ_RELAY_URL`, `BUZZ
| `buzz feed` | `get` |
| `buzz social` | `publish`, `notes` |
| `buzz repos` | `create`, `get`, `list` |
| `buzz issues` | `create`, `get`, `list`, `status` |
| `buzz issues` | `create`, `get`, `list`, `status`, `assign` |
| `buzz pr` | `open`, `update`, `get`, `list`, `status` |
| `buzz upload` | `file` |

Run `buzz --help` or `buzz <group> --help` for full usage. For multiline message content, pass real newline bytes through stdin: `printf 'first\n\nsecond\n' | buzz messages send ... --content -`. Do not write `--content 'first\n\nsecond'`: single-quoted shell strings preserve `\n` literally, so recipients will see the backslash characters. `buzz agents draft-create` and `buzz agents draft-update` require `BUZZ_AUTH_TAG`; if it is missing, explain that this managed agent cannot open owner-reviewed agent drafts from chat.

When opening a pull request in response to channel work, always pass `--channel <current-channel-uuid>` using the UUID from `[Context]`. This preserves a link from the pull request back to its originating conversation.

`buzz pr open`, `buzz issues create`, and `buzz repos create` return a `link` field (a `buzz://` deep link). When you announce that work in a channel message, include the `link` value verbatim — Buzz Desktop renders it as a rich preview card that opens the PR, issue, or repo in-app, the same way GitHub links render. Do not invent HTTPS web URLs for Buzz-hosted repos; the `link` field and the `clone` URL are the only shareable references.
`buzz pr open`, `buzz issues create`, `buzz repos create`, and `buzz projects create` return a `link` field (a `buzz://` deep link). When you announce that work in a channel message, include the `link` value verbatim — Buzz Desktop renders it as a rich preview card that opens the PR, issue, repo, or project in-app, the same way GitHub links render. Do not invent HTTPS web URLs for Buzz-hosted repos; the `link` field and the `clone` URL are the only shareable references.

To assign an issue to someone, run `buzz issues assign --issue <event-id> --repo-owner <hex> --repo-id <id> --assignee <hex> --label <name>` after creating it. Remove an assignment with the matching `buzz issues unassign` arguments. Writing assignee names in the issue body or adding recipients with `issues create --to` is notification/presentation only — Buzz Desktop's Assignees rail and the "Assigned to me" filter read the signed assignment operations. Only operations signed by the issue author or repo owner are trusted for other people; anyone may assign or unassign themselves.

## Conversational Agent Creation

Expand Down
213 changes: 212 additions & 1 deletion crates/buzz-acp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,7 @@ fn handle_relay_observer_control_event(
pool: &mut AgentPool,
observer: Option<&observer::ObserverHandle>,
owner_pubkey_hex: &str,
event_publisher: RelayEventPublisher,
) {
// Defense-in-depth: verify signature even though the relay already checked.
if let Err(e) = buzz_core::verify_event(&event) {
Expand Down Expand Up @@ -1117,12 +1118,162 @@ fn handle_relay_observer_control_event(
Some("switch_model") => {
handle_switch_model_control(&payload, pool, observer);
}
Some("publish_project_owner_announcements") => {
handle_publish_project_owner_announcements_control(
&payload,
keys,
observer,
event_publisher,
);
}
_ => {
tracing::debug!(payload = %payload, "ignoring unknown observer control frame");
}
}
}

#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct ProjectOwnerAnnouncementControl {
request_id: String,
announcements: Vec<ProjectOwnerAnnouncementTemplate>,
}

#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct ProjectOwnerAnnouncementTemplate {
kind: u16,
content: String,
created_at: Option<u64>,
tags: Vec<Vec<String>>,
}

fn handle_publish_project_owner_announcements_control(
payload: &serde_json::Value,
keys: &nostr::Keys,
observer: Option<&observer::ObserverHandle>,
publisher: RelayEventPublisher,
) {
let Ok(control) = serde_json::from_value::<ProjectOwnerAnnouncementControl>(payload.clone())
else {
tracing::warn!("project announcement control frame has an invalid payload");
return;
};
if Uuid::parse_str(&control.request_id).is_err()
|| control.announcements.is_empty()
|| control.announcements.len() > 2
{
tracing::warn!("project announcement control frame has invalid request metadata");
return;
}

let keys = keys.clone();
let observer = observer.cloned();
tokio::spawn(async move {
let events = match build_project_owner_announcement_events(control.announcements, &keys) {
Ok(events) => events,
Err(error) => {
emit_project_owner_control_result(
observer.as_ref(),
&control.request_id,
"error",
&[],
Some(error.to_string()),
);
return;
}
};
let mut published_events = Vec::with_capacity(events.len());
for event in events {
if let Err(error) = publisher.publish_event(event.clone()).await {
emit_project_owner_control_result(
observer.as_ref(),
&control.request_id,
"error",
&published_events,
Some(format!("publish project announcement: {error}")),
);
return;
}
published_events.push(event);
}
emit_project_owner_control_result(
observer.as_ref(),
&control.request_id,
"ok",
&published_events,
None,
);
});
}

fn build_project_owner_announcement_events(
announcements: Vec<ProjectOwnerAnnouncementTemplate>,
keys: &nostr::Keys,
) -> Result<Vec<nostr::Event>> {
let now = nostr::Timestamp::now().as_secs();
announcements
.into_iter()
.map(|template| {
if !matches!(template.kind, 30_617 | 30_621) {
anyhow::bail!("unsupported project announcement kind");
}
if !template.tags.iter().any(|tag| {
tag.first().is_some_and(|value| value == "d")
&& tag.get(1).is_some_and(|value| !value.trim().is_empty())
}) {
anyhow::bail!("project announcement is missing its address");
}
let tags = template
.tags
.into_iter()
.map(|tag| {
nostr::Tag::parse(tag)
.map_err(|error| anyhow::anyhow!("invalid project tag: {error}"))
})
.collect::<Result<Vec<_>>>()?;
let created_at = template.created_at.unwrap_or(now);
if created_at > now.saturating_add(300) {
anyhow::bail!("project announcement timestamp is too far in the future");
}
nostr::EventBuilder::new(nostr::Kind::Custom(template.kind), template.content)
.tags(tags)
.custom_created_at(nostr::Timestamp::from(created_at))
.sign_with_keys(keys)
.map_err(|error| anyhow::anyhow!("sign project announcement: {error}"))
})
.collect()
}

fn emit_project_owner_control_result(
observer: Option<&observer::ObserverHandle>,
request_id: &str,
status: &str,
events: &[nostr::Event],
error: Option<String>,
) {
let Some(observer) = observer else {
return;
};
observer.emit(
"control_result",
None,
&observer::ObserverContext {
channel_id: None,
session_id: None,
turn_id: None,
started_at: None,
},
serde_json::json!({
"type": "publish_project_owner_announcements",
"requestId": request_id,
"status": status,
"events": events,
"error": error,
}),
);
}

/// Handle a `cancel_turn` control frame: signal the in-flight task to cancel.
fn handle_cancel_turn_control(
payload: &serde_json::Value,
Expand Down Expand Up @@ -2442,7 +2593,14 @@ async fn tokio_main() -> Result<()> {
match control_event {
Some(event) => {
if let Some(ref owner_hex) = owner_cache.pubkey {
handle_relay_observer_control_event(&config.keys, event, &mut pool, observer.as_ref(), owner_hex);
handle_relay_observer_control_event(
&config.keys,
event,
&mut pool,
observer.as_ref(),
owner_hex,
relay.event_publisher(),
);
} else {
tracing::warn!("observer control frame received but no owner resolved — dropping");
}
Expand Down Expand Up @@ -5209,6 +5367,59 @@ mod owner_control_command_tests {
ControlSignal::Cancel,
));
}

#[test]
fn project_owner_control_signs_only_addressable_project_events() {
let keys = Keys::generate();
let events = build_project_owner_announcement_events(
vec![
ProjectOwnerAnnouncementTemplate {
kind: 30_621,
content: String::new(),
created_at: Some(1),
tags: vec![vec!["d".to_string(), "project".to_string()]],
},
ProjectOwnerAnnouncementTemplate {
kind: 30_617,
content: String::new(),
created_at: Some(1),
tags: vec![vec!["d".to_string(), "repository".to_string()]],
},
],
&keys,
)
.expect("valid project events");

assert_eq!(events.len(), 2);
assert!(events.iter().all(|event| event.pubkey == keys.public_key()));
assert!(events.iter().all(|event| event.verify().is_ok()));
}

#[test]
fn project_owner_control_rejects_arbitrary_or_unaddressed_events() {
let keys = Keys::generate();
let arbitrary = build_project_owner_announcement_events(
vec![ProjectOwnerAnnouncementTemplate {
kind: 1,
content: String::new(),
created_at: None,
tags: vec![vec!["d".to_string(), "project".to_string()]],
}],
&keys,
);
assert!(arbitrary.is_err());

let unaddressed = build_project_owner_announcement_events(
vec![ProjectOwnerAnnouncementTemplate {
kind: 30_621,
content: String::new(),
created_at: None,
tags: vec![],
}],
&keys,
);
assert!(unaddressed.is_err());
}
}

#[cfg(test)]
Expand Down
Loading
Loading