From 25ddb2f5d41f28153ec8a35696772f75c494667d Mon Sep 17 00:00:00 2001 From: Duncan Date: Tue, 25 Aug 2026 13:08:43 -0400 Subject: [PATCH] refactor(desktop): extract spawn helpers and reshape permission card props Post-merge adaptation to origin/main. Lift the git-credential-nostr wiring and BUZZ_ACP_PERMISSION_POLICY injection out of the now-oversized spawn_agent_child into named helpers (configure_git_credential_helper, inject_spawn_permission_policy), and collapse PermissionRequestCardBlock to take the TimelineMessage plus the isKnownAgentPubkey predicate so the null-gate lives inside the block instead of at every MessageRow callsite. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- .../src-tauri/src/managed_agents/agent_env.rs | 36 +++++++++++ .../src/managed_agents/permission_policy.rs | 14 +++++ .../src-tauri/src/managed_agents/runtime.rs | 44 ++++---------- .../src/features/messages/ui/MessageRow.tsx | 20 ++----- .../ui/PermissionRequestCardBlock.test.mjs | 44 ++++++++++---- .../ui/PermissionRequestCardBlock.tsx | 59 ++++++++----------- 6 files changed, 124 insertions(+), 93 deletions(-) diff --git a/desktop/src-tauri/src/managed_agents/agent_env.rs b/desktop/src-tauri/src/managed_agents/agent_env.rs index 59b300d9d1..59f96fc257 100644 --- a/desktop/src-tauri/src/managed_agents/agent_env.rs +++ b/desktop/src-tauri/src/managed_agents/agent_env.rs @@ -116,6 +116,42 @@ pub(crate) fn build_buzz_agent_provider_defaults(cmd: &mut std::process::Command } } +/// Wire the git-credential-nostr helper onto `cmd` for Buzz relay git (NIP-98). +/// +/// Sets ephemeral `GIT_CONFIG_*` vars scoped to the relay HTTP URL and mirrors +/// the agent key into `NOSTR_PRIVATE_KEY`. When the helper binary is absent, +/// the agent simply lacks automatic Buzz git auth — a warning, not a failure. +pub(super) fn configure_git_credential_helper( + cmd: &mut std::process::Command, + relay_url: &str, + private_key_nsec: &str, + agent_name: &str, +) { + let Some(cred_helper) = super::resolve_command("git-credential-nostr") else { + eprintln!( + "buzz-desktop: git-credential-nostr not found — agent {agent_name} will not have automatic Buzz git auth", + ); + return; + }; + let relay_http_url = crate::relay::relay_http_base_url(relay_url); + cmd.env("NOSTR_PRIVATE_KEY", private_key_nsec); + cmd.env("GIT_TERMINAL_PROMPT", "0"); + cmd.env("GIT_CONFIG_COUNT", "2"); + cmd.env( + "GIT_CONFIG_KEY_0", + format!("credential.{relay_http_url}/git.helper"), + ); + cmd.env( + "GIT_CONFIG_VALUE_0", + cred_helper.to_string_lossy().replace('\\', "/"), + ); + cmd.env( + "GIT_CONFIG_KEY_1", + format!("credential.{relay_http_url}/git.useHttpPath"), + ); + cmd.env("GIT_CONFIG_VALUE_1", "true"); +} + /// Parse newline-delimited `KEY=VALUE` lines from a baked env blob. /// Blank lines are skipped. Each non-blank line must contain `=`; the key /// is everything before the first `=`, the value is everything after (values diff --git a/desktop/src-tauri/src/managed_agents/permission_policy.rs b/desktop/src-tauri/src/managed_agents/permission_policy.rs index fc0d9c3883..c2ce6807b8 100644 --- a/desktop/src-tauri/src/managed_agents/permission_policy.rs +++ b/desktop/src-tauri/src/managed_agents/permission_policy.rs @@ -120,6 +120,20 @@ pub fn apply_permission_policy_update( Ok(()) } +/// Resolve the effective policy and inject `BUZZ_ACP_PERMISSION_POLICY` so the +/// running process and the UI-visible setting stay in sync. Returns the policy +/// so the caller can stamp it onto the spawn-config snapshot. +pub fn inject_spawn_permission_policy( + command: &mut std::process::Command, + record: &ManagedAgentRecord, + definitions: &[AgentDefinition], + global: &super::global_config::GlobalAgentConfig, +) -> PermissionPolicy { + let (policy, _) = resolve_effective_permission_policy(record, definitions, global); + command.env("BUZZ_ACP_PERMISSION_POLICY", policy.as_str()); + policy +} + #[cfg(test)] mod tests { use super::*; diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index 9753a2afcf..bfa5824e0a 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -783,42 +783,24 @@ pub fn spawn_agent_child( command.env_remove(key); } - // Inject BUZZ_ACP_PERMISSION_POLICY — resolved here so the running process - // and the UI-visible setting are always in sync. - let (effective_permission_policy, _) = - resolve_effective_permission_policy(record, &personas, &global); - command.env( - "BUZZ_ACP_PERMISSION_POLICY", - effective_permission_policy.as_str(), + // Inject BUZZ_ACP_PERMISSION_POLICY, keeping the running process and the + // UI-visible setting in sync; the returned policy is stamped below. + let effective_permission_policy = super::permission_policy::inject_spawn_permission_policy( + &mut command, + record, + &personas, + &global, ); command.env("BUZZ_ACP_RELAY_OBSERVER", "true"); // Git credential helper: NIP-98 auth for Buzz relay git via git-credential-nostr. - // Ephemeral GIT_CONFIG_COUNT env vars scoped to relay HTTP URL; NOSTR_PRIVATE_KEY mirrors BUZZ_PRIVATE_KEY. - if let Some(cred_helper) = resolve_command("git-credential-nostr") { - let relay_http_url = crate::relay::relay_http_base_url(&effective_relay_url); - - command.env("NOSTR_PRIVATE_KEY", &record.private_key_nsec); - command.env("GIT_TERMINAL_PROMPT", "0"); - command.env("GIT_CONFIG_COUNT", "2"); - command.env( - "GIT_CONFIG_KEY_0", - format!("credential.{relay_http_url}/git.helper"), - ); - let helper = cred_helper.to_string_lossy().replace('\\', "/"); - command.env("GIT_CONFIG_VALUE_0", helper); - command.env( - "GIT_CONFIG_KEY_1", - format!("credential.{relay_http_url}/git.useHttpPath"), - ); - command.env("GIT_CONFIG_VALUE_1", "true"); - } else { - eprintln!( - "buzz-desktop: git-credential-nostr not found — agent {} will not have automatic Buzz git auth", - record.name, - ); - } + super::agent_env::configure_git_credential_helper( + &mut command, + &effective_relay_url, + &record.private_key_nsec, + &record.name, + ); // User env (descriptor.env): fully-layered floor→runtime→definition→global→persona→agent, // reserved-key filtered. Written last so user-explicit values win over Buzz-set env. diff --git a/desktop/src/features/messages/ui/MessageRow.tsx b/desktop/src/features/messages/ui/MessageRow.tsx index accb2b09b7..e04591204d 100644 --- a/desktop/src/features/messages/ui/MessageRow.tsx +++ b/desktop/src/features/messages/ui/MessageRow.tsx @@ -33,7 +33,6 @@ import { KIND_STREAM_MESSAGE_DIFF, } from "@/shared/constants/kinds"; import { getConfigNudgeAuthorPubkey } from "@/features/messages/ui/configNudgeAuthPubkey"; -import { getPermissionRequestAgentPubkey } from "@/features/messages/ui/permissionRequestAuthPubkey"; import { PermissionRequestCardBlock } from "@/features/messages/ui/PermissionRequestCardBlock"; import { cn } from "@/shared/lib/cn"; import { normalizePubkey } from "@/shared/lib/pubkey"; @@ -688,20 +687,11 @@ export const MessageRow = React.memo( <> {renderBody()} - {channelId && message.isAgent ? ( - - ) : null} + {continuationMetadataNode} pubkey === agentPubkey; +} + async function renderBlock({ content, signerPubkey = AGENT_PUBKEY, @@ -173,12 +193,13 @@ async function renderBlock({ QueryClientProvider, { client: qc }, createElement(PermissionRequestCardBlock, { - content, - interactive: true, - agentPubkey, - signerPubkey, - editSignerPubkey, - ownerPubkey, + message: makeMessage({ + content, + signerPubkey, + editSignerPubkey, + ownerPubkey, + }), + isKnownAgentPubkey: makeIsKnownAgentPubkey(agentPubkey), channelId: CHANNEL_ID, }), ), @@ -315,11 +336,12 @@ test("test_expiry_disables_buttons_after_clock_tick", async () => { QueryClientProvider, { client: qc }, createElement(PermissionRequestCardBlock, { - content: makePendingContent(EXPIRY_SECS), - interactive: true, - agentPubkey: AGENT_PUBKEY, - signerPubkey: AGENT_PUBKEY, - ownerPubkey: OWNER_PUBKEY, + message: makeMessage({ + content: makePendingContent(EXPIRY_SECS), + signerPubkey: AGENT_PUBKEY, + ownerPubkey: OWNER_PUBKEY, + }), + isKnownAgentPubkey: makeIsKnownAgentPubkey(AGENT_PUBKEY), channelId: CHANNEL_ID, }), ), diff --git a/desktop/src/features/messages/ui/PermissionRequestCardBlock.tsx b/desktop/src/features/messages/ui/PermissionRequestCardBlock.tsx index d958fb778c..7f4f4ee4b8 100644 --- a/desktop/src/features/messages/ui/PermissionRequestCardBlock.tsx +++ b/desktop/src/features/messages/ui/PermissionRequestCardBlock.tsx @@ -8,6 +8,8 @@ */ import * as React from "react"; +import type { TimelineMessage } from "@/features/messages/types"; +import { getPermissionRequestAgentPubkey } from "@/features/messages/ui/permissionRequestAuthPubkey"; import { useIdentityQuery } from "@/shared/api/hooks"; import { computePermissionRequest } from "@/shared/lib/computePermissionRequest"; import { normalizePubkey } from "@/shared/lib/pubkey"; @@ -15,51 +17,40 @@ import { AttachmentGroup } from "@/shared/ui/attachment"; import { PermissionRequestCard } from "@/shared/ui/permission-request-card"; export type PermissionRequestCardBlockProps = { - /** Message body content — may contain the sentinel or may not. */ - content: string; - /** Whether this is an interactive render surface. Non-interactive → no card. */ - interactive: boolean; - /** - * Normalized hex pubkey of the known agent that signed the kind-9 event. - * Undefined → card disabled (no agent signer on this message). - */ - agentPubkey: string | undefined; - /** Raw signer pubkey from the signed event envelope. */ - signerPubkey: string | undefined; - /** - * Signer pubkey of the most recent authorized kind-40003 edit, if any. - * Used to enforce edit authenticity: only agent-signed edits resolve the card. - */ - editSignerPubkey?: string; - /** Verified owner pubkey for the agent (from the agent's profile). */ - ownerPubkey?: string | null; - /** Channel ID for routing the permission decision click. */ - channelId: string; + /** The message that may carry a permission-request sentinel. */ + message: TimelineMessage; + /** Predicate identifying a known agent signer (enables the card). */ + isKnownAgentPubkey: (pubkey: string) => boolean; + /** Channel ID for routing the decision click; falsy → no card. */ + channelId: string | null | undefined; }; export const PermissionRequestCardBlock = React.memo( function PermissionRequestCardBlock({ - content, - interactive, - agentPubkey, - signerPubkey, - editSignerPubkey, - ownerPubkey, + message, + isKnownAgentPubkey, channelId, }: PermissionRequestCardBlockProps) { const identityQuery = useIdentityQuery(); const viewerPubkey = identityQuery.data?.pubkey; + if (!channelId || !message.isAgent) return null; + + const agentPubkey = getPermissionRequestAgentPubkey( + message, + isKnownAgentPubkey, + ); const request = computePermissionRequest( - content, - interactive, + message.body, + true, agentPubkey, - signerPubkey, - editSignerPubkey, + message.signerPubkey, + message.editSignerPubkey, ); if (request === null || !agentPubkey) return null; + const ownerPubkey = message.ownerPubkey; const isOwner = !!viewerPubkey && !!ownerPubkey && @@ -80,11 +71,7 @@ export const PermissionRequestCardBlock = React.memo( ); }, (prev, next) => - prev.content === next.content && - prev.interactive === next.interactive && - prev.agentPubkey === next.agentPubkey && - prev.signerPubkey === next.signerPubkey && - prev.editSignerPubkey === next.editSignerPubkey && - prev.ownerPubkey === next.ownerPubkey && + prev.message === next.message && + prev.isKnownAgentPubkey === next.isKnownAgentPubkey && prev.channelId === next.channelId, );