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,
);