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
17 changes: 12 additions & 5 deletions docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,11 @@ servers do not support these operations.

In communities the build flag `BUZZ_OWNER_MCP_COMMUNITIES` enables, an agent's
runs in this app on a bundled harness list `owner_channel_list`, `owner_channel_get`,
`owner_member_list`, `owner_read`, `owner_canvas_get` and `owner_canvas_history`.
`owner_member_list`, `owner_read`, `owner_canvas_get`, `owner_canvas_history`
and `owner_media_get`.
They read the owner's channels, one channel's details and
the owner's rights in it, its roster, its messages, whole threads and searches,
and its Canvas and earlier revisions, through the
its Canvas and earlier revisions, and the community's media, through the
owner's own signed-in session, including private and archived channels, DMs and
Solo chats the agent is not in. Message reads and Canvas history page with a
`next` cursor, as `read` and the app's history do. A search covers only the
Expand All @@ -91,10 +92,16 @@ channels' matches still fill its page, so `limited` with few results means
narrowing it by channel or time. A message shows its newest edit by its author
or, as in the app, by the author's verified owner. One read fetches a page's edits;
when they fill it, the answer says `edits_limited` and some messages may show earlier text. They write
nothing: no membership, subscription, read state, post, agent start or Canvas
nothing to the community: no membership, subscription, read state, post, agent start or Canvas
change. Content comes from the relay's own answer, not the owner's open views,
so it changes no unread counts. They do not save the owner's attachments: a
saved file would be readable before the call's final checks. The app
so it changes no unread counts. `owner_media_get` gets one media blob of the
agent's community, by its URL or `sha256[.ext]`, as `media_get` does but signed
as the owner: the app holds it in memory, up to 500 MiB, until protection has
reviewed the answer, and saves it to a local file only after the call's final
checks, while the owner is still signed in and the agent was not deleted. A
refused, cancelled or failed call saves nothing. Agents running on a remote
execution target do not list it and are refused it, since its file would be
on this device. The app
decides the agent, owner and community;
answers name them in `executedAs`. Each call needs the agent's owner signed in
to that community now and fails, returning nothing, when the account, community
Expand Down
9 changes: 5 additions & 4 deletions docs/buzz-help.md
Original file line number Diff line number Diff line change
Expand Up @@ -836,10 +836,11 @@ available rather than silently discarding it.

- In communities where your build enables it, an agent running on this
device with a built-in harness can list your channels, including private and
archived ones, DMs and Solo chats it is not in, and see a channel's details
and members, read with your access. It cannot change anything this way, and
it stops working when you sign out, switch accounts, disconnect or stop its
turn.
archived ones, DMs and Solo chats it is not in, and read a channel's details,
members, messages and Canvas, and the community's files, with your access.
It cannot change anything this way, and it stops working when you sign out,
switch accounts, disconnect or stop its turn. An agent on a remote execution
target cannot get files this way.

Saved teams from **Your teams** appear in the inline `@` suggestions and the
mention picker. Select a team to insert its members as individual mentions.
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ fn main() {
"app_agent_search_profiles",
"app_agent_upload",
"app_agent_media",
"app_agent_owner_media_stage",
"app_agent_owner_media_publish",
"app_agent_owner_media_discard",
"app_agent_git_auth",
"app_agent_remember",
"app_agent_claim",
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@
"allow-app-agent-search-profiles",
"allow-app-agent-upload",
"allow-app-agent-media",
"allow-app-agent-owner-media-stage",
"allow-app-agent-owner-media-publish",
"allow-app-agent-owner-media-discard",
"allow-app-agent-git-auth",
"allow-app-agent-remember",
"allow-app-agent-claim",
Expand Down
46 changes: 33 additions & 13 deletions src-tauri/src/app_agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ use crate::federated_identity::{
assertion,
};

pub(crate) mod owner_media;

/// A NIP-98 `Authorization` header for one signed HTTP auth event.
fn authorization(event: Value) -> Result<String, String> {
use base64::Engine;
Expand Down Expand Up @@ -56,6 +58,8 @@ pub(crate) struct AppAgentHost {
tokens: AgentTokens,
/// Serializes owner prompts, without holding cross-process identity/custody locks.
protection_upgrade: Arc<Mutex<()>>,
/// Owner media read for review and not yet published.
owner_media: Arc<owner_media::OwnerMedia>,
}
impl AppAgentHost {
/// `packaged` is false for a dev build, which steps aside for a packaged app.
Expand Down Expand Up @@ -103,6 +107,7 @@ impl AppAgentHost {
}),
tokens,
protection_upgrade: Arc::default(),
owner_media: Arc::default(),
}
}
/// The saved identities, for readers outside Agents such as the inventory.
Expand All @@ -119,6 +124,12 @@ impl AppAgentHost {
let mut removals = self.removals();
removals.page += 1;
removals.held.clear();
drop(removals);
self.owner_media.clear();
}
/// Drops held owner media, as the window closing publishes none.
pub(crate) fn owner_media_clear(&self) {
self.owner_media.clear();
}
fn keys(&self) -> std::sync::MutexGuard<'_, HashMap<String, Arc<Secret>>> {
self.keys
Expand Down Expand Up @@ -1412,19 +1423,7 @@ async fn save_media_admitted(
}
}
agent.verify_media_hash(url, &format!("{:x}", hash.finalize()))?;
// The blob's own name, so the same media is saved once; with an extension
// from its type when the URL gives none.
let mut name = url.rsplit('/').next().unwrap_or_default().to_owned();
if !name.contains('.') {
if let Some(ext) = mime
.split_once('/')
.map(|(_, subtype)| subtype)
.filter(|ext| ext.len() <= 8 && ext.bytes().all(|b| b.is_ascii_alphanumeric()))
{
name = format!("{name}.{ext}");
}
}
let path = dir.join(name);
let path = dir.join(media_name(url, &mime));
let persist = || {
file.persist(&path)
.map(drop)
Expand All @@ -1444,6 +1443,22 @@ async fn save_media_admitted(
})
}

/// The blob's own name, so the same media is saved once; with an extension
/// from its type when the URL gives none.
fn media_name(url: &str, mime: &str) -> String {
let mut name = url.rsplit('/').next().unwrap_or_default().to_owned();
if !name.contains('.') {
if let Some(ext) = mime
.split_once('/')
.map(|(_, subtype)| subtype)
.filter(|ext| ext.len() <= 8 && ext.bytes().all(|b| b.is_ascii_alphanumeric()))
{
name = format!("{name}.{ext}");
}
}
name
}

/// Writes one memory entry, encrypted to the agent's owner; a null body
/// deletes it. `after` is the `created_at` of the entry it replaces, or 0.
#[tauri::command]
Expand Down Expand Up @@ -1743,6 +1758,7 @@ mod tests {
hosting: Arc::new(hosting(Err("unused".into()), true)),
tokens: AgentTokens::unconfigured(),
protection_upgrade: Arc::default(),
owner_media: Arc::default(),
};
let agent = AppAgent {
pubkey: key.pubkey().into(),
Expand Down Expand Up @@ -1932,6 +1948,7 @@ mod tests {
hosting: Arc::new(hosting(Err("unused".into()), true)),
tokens: f.tokens.clone(),
protection_upgrade: Arc::default(),
owner_media: Arc::default(),
};
let import = |key: &Secret| {
let key = Secret::parse(&key.hex(), key.pubkey()).unwrap();
Expand Down Expand Up @@ -2028,6 +2045,7 @@ mod tests {
hosting: Arc::new(hosting(Err("unused".into()), true)),
tokens: f.tokens.clone(),
protection_upgrade: Arc::default(),
owner_media: Arc::default(),
};
let agents = host.agents().unwrap();

Expand Down Expand Up @@ -2073,6 +2091,7 @@ mod tests {
hosting: Arc::new(hosting(Err("unused".into()), true)),
tokens: f.tokens.clone(),
protection_upgrade: Arc::default(),
owner_media: Arc::default(),
};
let import = || {
let key = Secret::parse(&key.hex(), key.pubkey()).unwrap();
Expand Down Expand Up @@ -2523,6 +2542,7 @@ mod tests {
hosting: Arc::new(hosting(Err("unused".into()), true)),
tokens: AgentTokens::unconfigured(),
protection_upgrade: Arc::default(),
owner_media: Arc::default(),
};
let import = || {
let prepared = AppAgents::adopt(
Expand Down
Loading
Loading