Skip to content
Closed
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
2 changes: 2 additions & 0 deletions agents.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"/><title>Agents — Buzz</title></head><body><script type="module" src="/src/agents.ts"></script></body></html>
2 changes: 2 additions & 0 deletions src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ fn main() {
"media_copy_image",
"media_stream_base",
"get_os_idle_seconds",
"agents_port_listen",
"agents_port_send",
"workspace_pick_folder",
"repositories_folder_validate",
"plugin_import_folder",
Expand Down
102 changes: 102 additions & 0 deletions src-tauri/capabilities/agents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "agents-page",
"description": "The hidden page that runs agents: relay, agent, plugin host and classifier commands, without screens, windows, account setup or OS shell commands.",
"webviews": ["agents"],
"platforms": ["macOS"],
"permissions": [
"allow-identity-restore",
"allow-relay-sign",
"allow-identity-prepare-remote-agent-authorization",
"allow-identity-sign-builderlab-binding",
"allow-relay-decode-read-state",
"allow-relay-sign-read-state",
"allow-relay-publish-read-state",
"allow-relay-http",
"allow-relay-workflow-runs",
"allow-relay-admin-discover",
"allow-relay-admin-request",
"allow-relay-admin-attachment",
"allow-relay-admin-save-attachment",
"allow-relay-project-git",
"allow-relay-project-git-cancel",
"allow-relay-channel-sign",
"allow-relay-channel-publish",
"allow-relay-kit-sign",
"allow-relay-kit-prepare",
"allow-relay-kit-decode",
"allow-relay-direct-message",
"allow-relay-decode-sidebar",
"allow-relay-sign-sidebar",
"allow-relay-decode-reminders",
"allow-relay-sign-reminder",
"allow-relay-agent-resolve",
"allow-relay-git-authorization",
"allow-relay-agent-log-proof",
"allow-relay-agent-observer",
"allow-relay-archive",
"allow-relay-agent-memories-read",
"allow-relay-agent-library",
"allow-relay-upload-begin",
"allow-relay-upload-chunk",
"allow-relay-upload",
"allow-relay-upload-cancel",
"allow-media-download",
"allow-media-stream-base",
"allow-get-os-idle-seconds",
"allow-repositories-folder-validate",
"allow-plugin-catalog",
"allow-plugin-change",
"allow-plugin-reload",
"allow-plugin-module",
"allow-plugin-recover",
"allow-plugin-host-run-command",
"allow-plugin-host-request",
"allow-plugin-host-upload",
"allow-plugin-host-process-spawn",
"allow-plugin-host-process-write",
"allow-plugin-host-process-kill",
"allow-plugin-host-mcp-serve",
"allow-plugin-host-mcp-reply",
"allow-plugin-host-mcp-close",
"allow-plugin-host-read-file",
"allow-plugin-host-resolve-directory",
"allow-oauth-callback-begin",
"allow-oauth-callback-wait",
"allow-oauth-callback-cancel",
"allow-app-agent-list",
"allow-app-agent-create",
"allow-app-agent-portable",
"allow-app-agent-port",
"allow-app-agent-rename",
"allow-app-agent-set-picture",
"allow-app-agent-set-type",
"allow-app-agent-delete",
"allow-app-agent-begin-delete",
"allow-app-agent-begin-leave",
"allow-app-agent-hold-removal",
"allow-app-agent-release-removal",
"allow-app-agent-join",
"allow-app-agent-forget",
"allow-app-agent-publish",
"allow-app-agent-publish-profile",
"allow-app-agent-query",
"allow-app-agent-search-profiles",
"allow-app-agent-upload",
"allow-app-agent-media",
"allow-app-agent-git-auth",
"allow-app-agent-remember",
"allow-app-agent-claim",
"allow-app-agent-release",
"allow-jev-classifier-status",
"allow-jev-classifier-set-key",
"allow-jev-classifier-clear-key",
"allow-app-agent-state-load",
"allow-app-agent-state-set",
"allow-decide",
"core:app:allow-version",
"core:resources:allow-close",
"allow-agents-port-listen",
"allow-agents-port-send"
]
}
2 changes: 2 additions & 0 deletions src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@
"core:window:allow-close",
"core:window:allow-start-dragging",
"core:window:allow-internal-toggle-maximize",
"allow-agents-port-listen",
"allow-agents-port-send",
{
"identifier": "opener:allow-open-url",
"allow": [
Expand Down
119 changes: 119 additions & 0 deletions src-tauri/src/agents_port.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//! The port between the screens' page and the page that runs agents. Each
//! page opens its end with a channel, and a frame one page sends goes to the
//! other's channel. Only those two webviews take part, and the sender is the
//! webview the frame came from, so neither page listens to app events and no
//! other page can speak for either.

use std::{collections::HashMap, sync::Mutex};

use serde_json::Value;
use tauri::{ipc::Channel, Runtime, State, Webview};

const PAGES: [&str; 2] = ["main", "agents"];

/// The other page of the pair, for `label`.
fn peer(label: &str) -> Option<&'static str> {
match label {
"main" => Some("agents"),
"agents" => Some("main"),
_ => None,
}
}

/// Each page's open end, by its webview's label. A page that loads again
/// opens a new one, which replaces the last.
#[derive(Default)]
pub(crate) struct AgentsPort(Mutex<HashMap<&'static str, Channel<Value>>>);

impl AgentsPort {
fn open(&self, label: &str, frames: Channel<Value>) -> Result<(), String> {
let page = PAGES
.into_iter()
.find(|page| *page == label)
.ok_or("Only Buzz's own pages use the agents port")?;
self.0
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.insert(page, frames);
Ok(())
}
/// Sends `frame` from `label`'s page to its peer's. A peer that is not
/// open yet misses it; it says when it is, and the sender sends again.
fn send(&self, label: &str, frame: Value) -> Result<(), String> {
let to = peer(label).ok_or("Only Buzz's own pages use the agents port")?;
let channel = self
.0
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.get(to)
.cloned();
match channel {
Some(channel) => channel.send(frame).map_err(|error| error.to_string()),
None => Ok(()),
}
}
}

#[tauri::command]
pub(crate) fn agents_port_listen<R: Runtime>(
webview: Webview<R>,
port: State<'_, AgentsPort>,
frames: Channel<Value>,
) -> Result<(), String> {
port.open(webview.label(), frames)
}

#[tauri::command]
pub(crate) fn agents_port_send<R: Runtime>(
webview: Webview<R>,
port: State<'_, AgentsPort>,
frame: Value,
) -> Result<(), String> {
port.send(webview.label(), frame)
}

#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex as StdMutex};
use tauri::ipc::InvokeResponseBody;

fn end() -> (Channel<Value>, Arc<StdMutex<Vec<Value>>>) {
let heard = Arc::new(StdMutex::new(Vec::new()));
let into = heard.clone();
let channel = Channel::new(move |body| {
if let InvokeResponseBody::Json(json) = body {
into.lock()
.unwrap()
.push(serde_json::from_str(&json).unwrap());
}
Ok(())
});
(channel, heard)
}

#[test]
fn frames_go_only_to_the_other_page_of_the_pair() {
let port = AgentsPort::default();
let (main, to_main) = end();
let (agents, to_agents) = end();
// Before the other page is open, a frame for it is missed, not held.
port.send("main", Value::from("early")).unwrap();
port.open("main", main).unwrap();
port.open("agents", agents).unwrap();
port.send("main", Value::from(1)).unwrap();
port.send("agents", Value::from(2)).unwrap();
assert_eq!(*to_agents.lock().unwrap(), vec![Value::from(1)]);
assert_eq!(*to_main.lock().unwrap(), vec![Value::from(2)]);
// A reloaded page's new end replaces its old one.
let (again, to_again) = end();
port.open("agents", again).unwrap();
port.send("main", Value::from(3)).unwrap();
assert_eq!(*to_again.lock().unwrap(), vec![Value::from(3)]);
assert_eq!(to_agents.lock().unwrap().len(), 1);
// No other page opens an end or sends.
let (huddle, _) = end();
assert!(port.open("huddle", huddle).is_err());
assert!(port.send("huddle", Value::from(4)).is_err());
}
}
Loading
Loading