diff --git a/desktop/src/features/onboarding/welcomeGuide.test.mjs b/desktop/src/features/onboarding/welcomeGuide.test.mjs index b3def930f1..8fe8aa069c 100644 --- a/desktop/src/features/onboarding/welcomeGuide.test.mjs +++ b/desktop/src/features/onboarding/welcomeGuide.test.mjs @@ -9,6 +9,7 @@ import { pickWelcomeGuideAgentForRelay, pickWelcomeTeamStarterAgentForRelay, welcomeStarterRuntimeUpdate, + welcomeTeammateHasExpectedAccess, WELCOME_GUIDE_AGENT_NAME, WELCOME_GUIDE_PERSONA_ID, WELCOME_TEAM_ID, @@ -379,3 +380,33 @@ test("starter matching prefers running, then deployed instances", () => { deployed, ); }); + +test("internal local Welcome teammates accept enforced owner-only access", () => { + const teammate = makeAgent({ + respondTo: "owner-only", + respondToAllowlist: [], + }); + assert.equal(welcomeTeammateHasExpectedAccess(teammate, PUB_B, true), true); + assert.equal(welcomeTeammateHasExpectedAccess(teammate, PUB_B, false), false); +}); + +test("provider Welcome teammates still require the lead allowlist", () => { + const teammate = makeAgent({ + backend: { type: "provider", id: "remote", config: {} }, + respondTo: "owner-only", + respondToAllowlist: [], + }); + assert.equal(welcomeTeammateHasExpectedAccess(teammate, PUB_B, true), false); + assert.equal( + welcomeTeammateHasExpectedAccess( + { + ...teammate, + respondTo: "allowlist", + respondToAllowlist: [PUB_B], + }, + PUB_B, + true, + ), + true, + ); +}); diff --git a/desktop/src/features/onboarding/welcomeGuide.ts b/desktop/src/features/onboarding/welcomeGuide.ts index aa8deb7c19..d550ce1bce 100644 --- a/desktop/src/features/onboarding/welcomeGuide.ts +++ b/desktop/src/features/onboarding/welcomeGuide.ts @@ -6,6 +6,7 @@ import { addChannelMembers, createManagedAgent, discoverAcpRuntimes, + getAgentAccessOwnerOnly, getChannelMembers, listManagedAgents, updateManagedAgent, @@ -260,6 +261,25 @@ export function welcomeStarterRuntimeUpdate( }; } +export function welcomeTeammateHasExpectedAccess( + teammate: ManagedAgent, + leadPubkey: string, + agentAccessOwnerOnly: boolean, +) { + if (agentAccessOwnerOnly && teammate.backend.type === "local") { + return ( + teammate.respondTo === "owner-only" && + teammate.respondToAllowlist.length === 0 + ); + } + return ( + teammate.respondTo === "allowlist" && + teammate.respondToAllowlist.some( + (pubkey) => normalizePubkey(pubkey) === normalizePubkey(leadPubkey), + ) + ); +} + /** * Ensure the complete built-in Welcome Team is ready for kickoff. * The team itself is Rust-seeded; this only activates personas, creates any @@ -271,11 +291,13 @@ async function provisionWelcomeTeam( ): Promise { const existingAgents = await listManagedAgents(); await ensureWelcomeTeamPersonasActive(); - const [personas, runtimeCatalog, globalConfig] = await Promise.all([ - listPersonas(), - discoverAcpRuntimes(), - getGlobalAgentConfig(), - ]); + const [personas, runtimeCatalog, globalConfig, agentAccessOwnerOnly] = + await Promise.all([ + listPersonas(), + discoverAcpRuntimes(), + getGlobalAgentConfig(), + getAgentAccessOwnerOnly(), + ]); const personasById = new Map( personas.map((persona) => [persona.id, persona]), ); @@ -322,11 +344,11 @@ async function provisionWelcomeTeam( const leadPubkey = lead.pubkey; for (const index of [1, 2] as const) { const teammate = welcomeAgents[index]; - const alreadyAllowsLead = - teammate.respondTo === "allowlist" && - teammate.respondToAllowlist.some( - (pubkey) => normalizePubkey(pubkey) === normalizePubkey(leadPubkey), - ); + const alreadyAllowsLead = welcomeTeammateHasExpectedAccess( + teammate, + leadPubkey, + agentAccessOwnerOnly, + ); if (!alreadyAllowsLead) { const updated = await updateManagedAgent({ pubkey: teammate.pubkey, diff --git a/desktop/src/features/onboarding/welcomeKickoff.test.mjs b/desktop/src/features/onboarding/welcomeKickoff.test.mjs index 6f472b8068..e9ebe80afa 100644 --- a/desktop/src/features/onboarding/welcomeKickoff.test.mjs +++ b/desktop/src/features/onboarding/welcomeKickoff.test.mjs @@ -199,6 +199,42 @@ test("running teammates restart when their allowlist does not include the lead", ); }); +test("internal running teammates do not restart for enforced owner-only access", () => { + assert.equal( + welcomeTeammateNeedsRestart( + { + ...honey, + backend: { type: "local" }, + status: "running", + needsRestart: false, + respondTo: "owner-only", + respondToAllowlist: [], + }, + fizz.pubkey, + true, + ), + false, + ); +}); + +test("internal running teammates still restart for runtime changes", () => { + assert.equal( + welcomeTeammateNeedsRestart( + { + ...honey, + backend: { type: "local" }, + status: "running", + needsRestart: true, + respondTo: "owner-only", + respondToAllowlist: [], + }, + fizz.pubkey, + true, + ), + true, + ); +}); + test("opener keeps partial-readiness warm and mentions only online teammates", () => { const agentSet = { lead: fizz, teammates: [honey, bumble] }; const introTeammates = selectWelcomeKickoffIntroTeammates( diff --git a/desktop/src/features/onboarding/welcomeKickoff.ts b/desktop/src/features/onboarding/welcomeKickoff.ts index fd5dac3947..0acf922f95 100644 --- a/desktop/src/features/onboarding/welcomeKickoff.ts +++ b/desktop/src/features/onboarding/welcomeKickoff.ts @@ -3,6 +3,7 @@ import * as React from "react"; import { managedAgentsQueryKey, useAcpRuntimesQuery, + useAgentAccessOwnerOnlyQuery, useManagedAgentsQuery, } from "@/features/agents/hooks"; import { useGlobalAgentConfig } from "@/features/agents/useGlobalAgentConfig"; @@ -14,6 +15,7 @@ import { pickWelcomeTeamStarterAgentForRelay, WELCOME_TEAM_STARTERS, type WelcomeTeamStarterDefinition, + welcomeTeammateHasExpectedAccess, } from "@/features/onboarding/welcomeGuide"; import { isWelcomeChannel } from "@/features/onboarding/welcome"; import { getThreadReference } from "@/features/messages/lib/threading"; @@ -357,13 +359,15 @@ async function markerExists(channelId: string, marker: string) { export function welcomeTeammateNeedsRestart( agent: ManagedAgent, leadPubkey: string, + agentAccessOwnerOnly = false, ) { return ( agent.status === "running" && (agent.needsRestart || - agent.respondTo !== "allowlist" || - !agent.respondToAllowlist.some( - (pubkey) => normalizePubkey(pubkey) === normalizePubkey(leadPubkey), + !welcomeTeammateHasExpectedAccess( + agent, + leadPubkey, + agentAccessOwnerOnly, )) ); } @@ -493,6 +497,8 @@ export function useWelcomeKickoff( const { activeCommunity } = useCommunities(); const runtimesQuery = useAcpRuntimesQuery(); const managedAgentsQuery = useManagedAgentsQuery(); + const agentAccessOwnerOnlyQuery = useAgentAccessOwnerOnlyQuery(); + const agentAccessOwnerOnly = agentAccessOwnerOnlyQuery.data; const { globalConfig, isLoading: configLoading } = useGlobalAgentConfig(); const channelId = activeChannel?.id ?? null; const isActiveWelcome = isWelcomeChannel(activeChannel); @@ -553,7 +559,8 @@ export function useWelcomeKickoff( !channelId || !isActiveWelcome || configLoading || - runtimesQuery.isPending + runtimesQuery.isPending || + agentAccessOwnerOnly === undefined ) { return; } @@ -607,7 +614,11 @@ export function useWelcomeKickoff( ); if ( isTeammate && - welcomeTeammateNeedsRestart(agent, resolvedAgentSet.lead.pubkey) + welcomeTeammateNeedsRestart( + agent, + resolvedAgentSet.lead.pubkey, + agentAccessOwnerOnly, + ) ) { return restartWelcomeTeammate(agent); } @@ -682,6 +693,7 @@ export function useWelcomeKickoff( })(); }, [ activeCommunity?.relayUrl, + agentAccessOwnerOnly, channelId, configLoading, isActiveWelcome,