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:
7a9c8f0147a0755c5d499320bda5535cd5014715e52fb7757dfe3dadde1cef09@buzz.block.builderlab.xyz
2026-07-23 13:07:26 -07:00
parent 1a328cfa8b
commit 62c9f36578
11 changed files with 147 additions and 30 deletions
+4 -4
View File
@@ -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
+3 -2
View File
@@ -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"),
+3 -2
View File
@@ -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 ────────────────────────────────────────────────────────────
+2 -1
View File
@@ -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 [
+45 -13
View File
@@ -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");
+6 -7
View File
@@ -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,
+1
View File
@@ -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();
});