mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
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 <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
<>
|
||||
<SentFromThreadLine channelId={channelId} tags={message.tags} />
|
||||
{renderBody()}
|
||||
{channelId && message.isAgent ? (
|
||||
<PermissionRequestCardBlock
|
||||
agentPubkey={getPermissionRequestAgentPubkey(
|
||||
message,
|
||||
isKnownAgentPubkey,
|
||||
)}
|
||||
channelId={channelId}
|
||||
content={message.body}
|
||||
editSignerPubkey={message.editSignerPubkey}
|
||||
interactive
|
||||
ownerPubkey={message.ownerPubkey}
|
||||
signerPubkey={message.signerPubkey}
|
||||
/>
|
||||
) : null}
|
||||
<PermissionRequestCardBlock
|
||||
message={message}
|
||||
isKnownAgentPubkey={isKnownAgentPubkey}
|
||||
channelId={channelId}
|
||||
/>
|
||||
{continuationMetadataNode}
|
||||
<MessageReactions
|
||||
messageId={message.id}
|
||||
|
||||
@@ -149,6 +149,26 @@ async function makeQueryClient(viewerPubkey) {
|
||||
|
||||
// ── Render helper ─────────────────────────────────────────────────────────────
|
||||
|
||||
// Build a minimal TimelineMessage carrying the sentinel. The block reads
|
||||
// kind/isAgent/body/signerPubkey/editSignerPubkey/ownerPubkey; other fields
|
||||
// are irrelevant to the render gates under test.
|
||||
function makeMessage({ content, signerPubkey, editSignerPubkey, ownerPubkey }) {
|
||||
return {
|
||||
kind: 9,
|
||||
isAgent: true,
|
||||
body: content,
|
||||
signerPubkey,
|
||||
editSignerPubkey,
|
||||
ownerPubkey,
|
||||
};
|
||||
}
|
||||
|
||||
// The block resolves `agentPubkey` from the signer via this predicate, so a
|
||||
// forged signer (signer ≠ agent) fails the gate exactly as production does.
|
||||
function makeIsKnownAgentPubkey(agentPubkey) {
|
||||
return (pubkey) => 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,
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user