feat(desktop): disclose agent snapshot payload before import

Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@buzz.block.builderlab.xyz>
This commit is contained in:
npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757
2026-07-27 22:24:00 -04:00
parent bc592e102d
commit 0cfc6e7407
4 changed files with 110 additions and 12 deletions
@@ -65,6 +65,12 @@ pub struct AgentSnapshotImportPreview {
pub has_source_allowlist: bool,
/// Number of source allowlist entries.
pub source_allowlist_count: usize,
/// Full source allowlist entries, surfaced before import so hidden access
/// configuration is never reduced to a count.
pub source_allowlist: Vec<String>,
/// Pretty-printed, validated manifest exactly as decoded from the file.
/// The UI makes this available before confirmation for full payload review.
pub manifest_json: String,
}
/// The confirmation request sent from the UI after the user reviews the preview.
@@ -269,6 +275,10 @@ pub async fn preview_agent_snapshot_import(
}
.to_string();
let manifest_json = serde_json::to_string_pretty(&snapshot)
.map_err(|e| format!("failed to render snapshot manifest: {e}"))?;
let source_allowlist = snapshot.definition.respond_to_allowlist.clone();
Ok(AgentSnapshotImportPreview {
display_name: snapshot.profile.display_name.clone(),
system_prompt: snapshot.definition.system_prompt.clone(),
@@ -280,8 +290,10 @@ pub async fn preview_agent_snapshot_import(
.or_else(|| snapshot.profile.avatar_url.clone()),
memory_level,
memory_entry_count: snapshot.memory.entries.len(),
source_allowlist_count: snapshot.definition.respond_to_allowlist.len(),
has_source_allowlist: !snapshot.definition.respond_to_allowlist.is_empty(),
source_allowlist_count: source_allowlist.len(),
has_source_allowlist: !source_allowlist.is_empty(),
source_allowlist,
manifest_json,
})
})
.await
@@ -70,7 +70,7 @@ export function AgentSnapshotImportDialog({
<Dialog onOpenChange={onOpenChange} open={open}>
<DialogContent
aria-describedby={undefined}
className="max-w-md"
className="max-h-[85vh] max-w-2xl overflow-y-auto"
data-testid="agent-snapshot-import-dialog"
showCloseButton={false}
>
@@ -139,7 +139,7 @@ export function AgentSnapshotImportDialog({
// ── Preview body ──────────────────────────────────────────────────────────────
function PreviewBody({
export function PreviewBody({
preview,
hasMemory,
memoryLevelLabel,
@@ -157,13 +157,27 @@ function PreviewBody({
{/* Agent identity */}
<div className="space-y-1">
<p className="text-sm font-medium">{preview.displayName}</p>
{preview.systemPrompt ? (
<p className="line-clamp-3 text-xs text-muted-foreground">
{preview.systemPrompt}
</p>
) : null}
</div>
{/* Portable behavior — never hide executable configuration behind a summary. */}
<section
className="space-y-2 rounded-md border border-border p-3"
data-testid="agent-snapshot-import-behavior"
>
<div>
<p className="text-sm font-medium">Agent instructions</p>
<p className="text-xs text-muted-foreground">
Review the instructions this agent will follow after import.
</p>
</div>
<pre
className="max-h-48 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/60 p-3 text-xs"
data-testid="agent-snapshot-import-system-prompt"
>
{preview.systemPrompt || "No system prompt included."}
</pre>
</section>
<p className="text-sm text-muted-foreground">
A new agent will be created with a fresh keypair. The imported agent is
independent of the source — identity never travels.
@@ -203,9 +217,19 @@ function PreviewBody({
{preview.sourceAllowlistCount === 1 ? "y" : "ies"})
</p>
<p className="text-xs text-muted-foreground">
This snapshot includes a source-environment pubkey allowlist. Those
identities are not meaningful on your relay.
This snapshot includes source-environment pubkeys. Review every key
before choosing Keep; Clear is safer when you do not recognize them.
</p>
<ul
className="max-h-28 space-y-1 overflow-y-auto rounded bg-muted/60 p-2 font-mono text-xs"
data-testid="agent-snapshot-import-allowlist-values"
>
{preview.sourceAllowlist.map((pubkey) => (
<li className="break-all" key={pubkey}>
{pubkey}
</li>
))}
</ul>
<div className="flex flex-col gap-1.5">
<label className="flex cursor-pointer items-center gap-2">
<input
@@ -234,6 +258,22 @@ function PreviewBody({
</div>
</div>
) : null}
<details
className="rounded-md border border-border p-3"
data-testid="agent-snapshot-import-manifest"
>
<summary className="cursor-pointer text-sm font-medium">
Full embedded manifest
</summary>
<p className="mt-2 text-xs text-muted-foreground">
This is the complete portable payload decoded from the file. Secrets,
credentials, and source identity are not part of the snapshot format.
</p>
<pre className="mt-2 max-h-64 overflow-auto whitespace-pre-wrap break-words rounded bg-muted/60 p-3 text-xs">
{preview.manifestJson}
</pre>
</details>
</div>
);
}
@@ -9,7 +9,7 @@ import test from "node:test";
// memoryErrors strings are carried through to a bounded list element with the
// correct data-testid. No DOM or test renderer is needed.
import { ResultBody } from "./AgentSnapshotImportDialog.tsx";
import { PreviewBody, ResultBody } from "./AgentSnapshotImportDialog.tsx";
/**
* Walk a React element tree (breadth-first) and collect all elements that
@@ -71,6 +71,48 @@ function makeResult(overrides = {}) {
};
}
function makePreview(overrides = {}) {
return {
displayName: "TestBot",
systemPrompt: "Inspect every boundary before changing code.",
avatarUrl: null,
memoryLevel: "none",
memoryEntryCount: 0,
hasSourceAllowlist: true,
sourceAllowlistCount: 2,
sourceAllowlist: ["a".repeat(64), "b".repeat(64)],
manifestJson: '{\n "format": "buzz-agent-snapshot"\n}',
...overrides,
};
}
// ── preview transparency ──────────────────────────────────────────────────────
test("preview_body_discloses_prompt_allowlist_and_full_manifest", () => {
const preview = makePreview();
const element = PreviewBody({
preview,
hasMemory: false,
memoryLevelLabel: "none",
keepAllowlist: false,
onKeepAllowlistChange: () => {},
});
const allText = collectText(element).join(" ");
assert.ok(allText.includes(preview.systemPrompt));
for (const pubkey of preview.sourceAllowlist) {
assert.ok(allText.includes(pubkey), `missing allowlist pubkey ${pubkey}`);
}
assert.ok(allText.includes(preview.manifestJson));
assert.equal(
findAll(
element,
(n) => n.props?.["data-testid"] === "agent-snapshot-import-manifest",
).length,
1,
);
});
// ── memory errors detail list ─────────────────────────────────────────────────
test("result_body_renders_memory_errors_list_with_test_id", () => {
+4
View File
@@ -181,6 +181,10 @@ export type AgentSnapshotImportPreview = {
/** True when the snapshot's respond_to_allowlist is non-empty. */
hasSourceAllowlist: boolean;
sourceAllowlistCount: number;
/** Full source pubkeys shown before import, not only their count. */
sourceAllowlist: string[];
/** Validated, pretty-printed manifest for full payload disclosure. */
manifestJson: string;
};
/** Confirmation sent to `confirm_agent_snapshot_import`. */