mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Harden relay policy remediation paths
Co-authored-by: npub102wg7q285p64ch2fjvstmf2ntn2sz3c4u5hmwatalc76mhsuauysftjtfj <7a9c8f0147a0755c5d499320bda5535cd5014715e52fb7757dfe3dadde1cef09@buzz.block.builderlab.xyz> Signed-off-by: npub102wg7q285p64ch2fjvstmf2ntn2sz3c4u5hmwatalc76mhsuauysftjtfj <7a9c8f0147a0755c5d499320bda5535cd5014715e52fb7757dfe3dadde1cef09@buzz.block.builderlab.xyz>
This commit is contained in:
parent
1a328cfa8b
commit
62c9f36578
@@ -122,10 +122,10 @@ const overrides = new Map([
|
||||
// runtime log path. Load-bearing crash-recovery surface; queued to split.
|
||||
// internal-owner-only: persistence choke point normalizes local agent access.
|
||||
// internal-local-relay-lockdown: load/save rejects legacy disallowed pins.
|
||||
// internal-local-relay-lockdown round 2: remediation-safe changed-pin checks and tests.
|
||||
["src-tauri/src/managed_agents/storage.rs", 1464],
|
||||
// internal-local-relay-lockdown round 2: guard initial and incremental huddle enrollment.
|
||||
["src-tauri/src/huddle/mod.rs", 1006],
|
||||
// internal-local-relay-lockdown round 3: fail-loud save baselines and regressions.
|
||||
["src-tauri/src/managed_agents/storage.rs", 1496],
|
||||
// internal-local-relay-lockdown rounds 2-3: guarded enrollment; OSS fast-path.
|
||||
["src-tauri/src/huddle/mod.rs", 1007],
|
||||
// harness-persona-sync: persona-runtime resolution threaded into the spawn
|
||||
// path here. Load-bearing feature growth; queued to split in the resolver
|
||||
// unify refactor followup. +26 for resolve_effective_prompt_model_provider
|
||||
|
||||
@@ -790,8 +790,9 @@ pub async fn add_channel_members(
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let uuid = parse_channel_uuid(&channel_id)?;
|
||||
let relay_url = crate::relay::relay_ws_url_with_override(&state);
|
||||
let local_agents = crate::managed_agents::load_managed_agents(&app)?;
|
||||
crate::managed_agents::validate_local_agent_members(&local_agents, &pubkeys, &relay_url)?;
|
||||
crate::managed_agents::validate_local_agent_members_from_store(&pubkeys, &relay_url, || {
|
||||
crate::managed_agents::load_managed_agents(&app)
|
||||
})?;
|
||||
let role_str = match role.as_deref() {
|
||||
Some("admin") => Some("admin"),
|
||||
Some("bot") => Some("bot"),
|
||||
|
||||
@@ -99,9 +99,10 @@ fn validate_huddle_agent_enrollment(
|
||||
state: &AppState,
|
||||
pubkeys: &[String],
|
||||
) -> Result<(), String> {
|
||||
let records = crate::managed_agents::load_managed_agents(app)?;
|
||||
let relay_url = crate::relay::relay_ws_url_with_override(state);
|
||||
crate::managed_agents::validate_local_agent_members(&records, pubkeys, &relay_url)
|
||||
crate::managed_agents::validate_local_agent_members_from_store(pubkeys, &relay_url, || {
|
||||
crate::managed_agents::load_managed_agents(app)
|
||||
})
|
||||
}
|
||||
|
||||
// ── Tauri commands ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -12,7 +12,8 @@ pub(crate) use agent_env::{
|
||||
baked_build_env, build_buzz_agent_provider_defaults, discovery_env_with_baked_floor,
|
||||
};
|
||||
pub(crate) use relay_policy::{
|
||||
validate_local_agent_members, validate_local_agent_relay, validate_managed_agent_relay_pin,
|
||||
validate_local_agent_members_from_store, validate_local_agent_relay,
|
||||
validate_managed_agent_relay_pin,
|
||||
};
|
||||
mod backend;
|
||||
pub(crate) mod config_bridge;
|
||||
|
||||
@@ -89,6 +89,39 @@ pub(crate) fn validate_managed_agent_relay_pin(record: &ManagedAgentRecord) -> R
|
||||
validate_local_agent_relay(&record.backend, &record.relay_url)
|
||||
}
|
||||
|
||||
/// Load and validate local members only when the internal-build policy applies.
|
||||
/// OSS builds must not make ordinary membership depend on managed-agent store health.
|
||||
pub(crate) fn validate_local_agent_members_from_store<F>(
|
||||
pubkeys: &[String],
|
||||
relay_url: &str,
|
||||
load: F,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
F: FnOnce() -> Result<Vec<ManagedAgentRecord>, String>,
|
||||
{
|
||||
validate_local_agent_members_from_store_with_policy(
|
||||
pubkeys,
|
||||
relay_url,
|
||||
super::internal_build(),
|
||||
load,
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_local_agent_members_from_store_with_policy<F>(
|
||||
pubkeys: &[String],
|
||||
relay_url: &str,
|
||||
internal: bool,
|
||||
load: F,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
F: FnOnce() -> Result<Vec<ManagedAgentRecord>, String>,
|
||||
{
|
||||
if !internal {
|
||||
return Ok(());
|
||||
}
|
||||
validate_local_agent_members(&load()?, pubkeys, relay_url)
|
||||
}
|
||||
|
||||
/// Reject attachment of locally managed agents to a disallowed effective relay.
|
||||
/// Unknown pubkeys and provider-backed records are outside this policy.
|
||||
pub(crate) fn validate_local_agent_members(
|
||||
@@ -218,6 +251,33 @@ mod tests {
|
||||
assert_eq!(calls.get(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oss_member_enrollment_does_not_load_the_agent_store() {
|
||||
let loads = std::cell::Cell::new(0);
|
||||
assert!(validate_local_agent_members_from_store_with_policy(
|
||||
&["human".into()],
|
||||
"not-even-a-relay",
|
||||
false,
|
||||
|| {
|
||||
loads.set(loads.get() + 1);
|
||||
Err("broken store".into())
|
||||
},
|
||||
)
|
||||
.is_ok());
|
||||
assert_eq!(loads.get(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn internal_member_enrollment_fails_loudly_on_broken_store() {
|
||||
assert!(validate_local_agent_members_from_store_with_policy(
|
||||
&["human".into()],
|
||||
"wss://buzz.block.builderlab.xyz",
|
||||
true,
|
||||
|| Err("broken store".into()),
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn internal_policy_fails_closed_on_missing_empty_or_malformed_allowlist() {
|
||||
for allowlist in [
|
||||
|
||||
@@ -319,22 +319,33 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_managed_agent_save_baseline_with<F>(
|
||||
load: F,
|
||||
) -> Result<(Vec<ManagedAgentRecord>, Vec<ManagedAgentRecord>), String>
|
||||
where
|
||||
F: FnOnce() -> Result<Vec<ManagedAgentRecord>, String>,
|
||||
{
|
||||
let stored = load()?;
|
||||
let definitions = stored
|
||||
.iter()
|
||||
.filter(|record| record.pubkey.is_empty())
|
||||
.cloned()
|
||||
.collect();
|
||||
let instances = stored
|
||||
.into_iter()
|
||||
.filter(|record| !record.pubkey.is_empty())
|
||||
.collect();
|
||||
Ok((definitions, instances))
|
||||
}
|
||||
|
||||
/// Save the keyed agent *instances*, preserving the key-less definitions that
|
||||
/// share the unified store: callers pass exactly the records they loaded via
|
||||
/// [`load_managed_agents`], and this re-reads the definition half from disk
|
||||
/// before the wholesale rewrite so a definition is never dropped by an
|
||||
/// instance-side save (and vice versa via [`save_agent_definitions`]).
|
||||
pub fn save_managed_agents(app: &AppHandle, records: &[ManagedAgentRecord]) -> Result<(), String> {
|
||||
let stored = load_agent_store(app).unwrap_or_default();
|
||||
let definitions = stored
|
||||
.iter()
|
||||
.filter(|record| record.pubkey.is_empty())
|
||||
.cloned()
|
||||
.collect();
|
||||
let previous_instances: Vec<_> = stored
|
||||
.into_iter()
|
||||
.filter(|record| !record.pubkey.is_empty())
|
||||
.collect();
|
||||
let (definitions, previous_instances) =
|
||||
load_managed_agent_save_baseline_with(|| load_agent_store(app))?;
|
||||
validate_changed_relay_pins_with(records, &previous_instances, |record| {
|
||||
super::validate_managed_agent_relay_pin(record)
|
||||
})?;
|
||||
@@ -836,9 +847,9 @@ mod tests {
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use super::{
|
||||
agent_keyring_name, hydrate_keys_with, migrate_inline_key, persist_agent_keys_with,
|
||||
retain_managed_agent_instances, validate_changed_relay_pins_with, KeyMigration, KeyStore,
|
||||
KeyringProbe, ManagedAgentRecord,
|
||||
agent_keyring_name, hydrate_keys_with, load_managed_agent_save_baseline_with,
|
||||
migrate_inline_key, persist_agent_keys_with, retain_managed_agent_instances,
|
||||
validate_changed_relay_pins_with, KeyMigration, KeyStore, KeyringProbe, ManagedAgentRecord,
|
||||
};
|
||||
|
||||
/// In-memory [`KeyStore`] for testing the migrate decision without the OS
|
||||
@@ -968,6 +979,27 @@ mod tests {
|
||||
.expect("sample record")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_baseline_propagates_store_errors_without_rewriting_from_empty() {
|
||||
let result = load_managed_agent_save_baseline_with(|| Err("broken store".into()));
|
||||
assert_eq!(result.unwrap_err(), "broken store");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_baseline_preserves_definitions_and_instances() {
|
||||
let instance = record_with_key("nsec1realkey");
|
||||
let mut definition = instance.clone();
|
||||
definition.pubkey.clear();
|
||||
|
||||
let (definitions, instances) = load_managed_agent_save_baseline_with(|| {
|
||||
Ok(vec![definition.clone(), instance.clone()])
|
||||
})
|
||||
.expect("baseline");
|
||||
|
||||
assert_eq!(definitions, vec![definition]);
|
||||
assert_eq!(instances, vec![instance]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instance_filter_keeps_legacy_pins_available_for_remediation() {
|
||||
let mut pinned = record_with_key("nsec1realkey");
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ensureChannelAgentPresetInChannel,
|
||||
provisionChannelManagedAgent,
|
||||
} from "@/features/agents/channelAgents";
|
||||
import { localAgentRelayAllowedQueryKey } from "@/features/agents/localAgentRelayPolicyQuery";
|
||||
import { resolveSnapshotAvatarPng } from "@/features/agents/ui/snapshotAvatarPng";
|
||||
import {
|
||||
channelsQueryKey,
|
||||
@@ -911,18 +912,16 @@ export function useRuntimeFileConfigQuery(
|
||||
|
||||
export const bakedBuildEnvKeysQueryKey = ["baked-build-env-keys"] as const;
|
||||
export const bakedBuildEnvQueryKey = ["baked-build-env"] as const;
|
||||
export const localAgentRelayAllowedQueryKey = [
|
||||
"local-agent-relay-allowed",
|
||||
] as const;
|
||||
export const agentAccessOwnerOnlyQueryKey = [
|
||||
"agent-access-owner-only",
|
||||
] as const;
|
||||
|
||||
export function useLocalAgentRelayAllowedQuery(options?: {
|
||||
enabled?: boolean;
|
||||
}) {
|
||||
export function useLocalAgentRelayAllowedQuery(
|
||||
communityId: string | null,
|
||||
options?: { enabled?: boolean },
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: localAgentRelayAllowedQueryKey,
|
||||
queryKey: localAgentRelayAllowedQueryKey(communityId),
|
||||
queryFn: () => getLocalAgentRelayAllowed(),
|
||||
enabled: options?.enabled ?? true,
|
||||
staleTime: 30_000,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { localAgentRelayAllowedQueryKey } from "./localAgentRelayPolicyQuery.ts";
|
||||
|
||||
test("local agent relay policy cache is scoped by community", () => {
|
||||
assert.notDeepEqual(
|
||||
localAgentRelayAllowedQueryKey("community-a"),
|
||||
localAgentRelayAllowedQueryKey("community-b"),
|
||||
);
|
||||
assert.deepEqual(localAgentRelayAllowedQueryKey(null), [
|
||||
"local-agent-relay-allowed",
|
||||
"none",
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export const localAgentRelayAllowedQueryKey = (communityId: string | null) =>
|
||||
["local-agent-relay-allowed", communityId ?? "none"] as const;
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { getActivePersonas } from "@/features/agents/lib/catalog";
|
||||
import { resolvePersonaRuntime } from "@/features/agents/lib/resolvePersonaRuntime";
|
||||
import { getUsableTeams } from "@/features/agents/lib/teamPersonas";
|
||||
import { useCommunities } from "@/features/communities/useCommunities";
|
||||
import { AddChannelBotPersonasSection } from "@/features/channels/ui/AddChannelBotPersonasSection";
|
||||
import { AddChannelBotTeamsSection } from "@/features/channels/ui/AddChannelBotTeamsSection";
|
||||
import { useInChannelPersonaIds } from "@/features/channels/ui/useInChannelPersonaIds";
|
||||
@@ -64,7 +65,11 @@ export function AddChannelBotDialog({
|
||||
onOpenChange,
|
||||
}: AddChannelBotDialogProps) {
|
||||
const personasQuery = usePersonasQuery();
|
||||
const localRelayPolicy = useLocalAgentRelayAllowedQuery({ enabled: open });
|
||||
const { activeCommunity } = useCommunities();
|
||||
const localRelayPolicy = useLocalAgentRelayAllowedQuery(
|
||||
activeCommunity?.id ?? null,
|
||||
{ enabled: open },
|
||||
);
|
||||
const teamsQuery = useTeamsQuery();
|
||||
const inChannelPersonaIds = useInChannelPersonaIds(
|
||||
channelId,
|
||||
|
||||
@@ -1541,6 +1541,7 @@ test("internal build blocks local agents on a non-allowlisted community", async
|
||||
await page.getByTestId("channel-intro-action-create-agent").click();
|
||||
|
||||
await expect(page.getByTestId("local-agent-relay-blocked")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Fizz" }).click();
|
||||
await expect(page.getByRole("button", { name: "Add agent" })).toBeDisabled();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user