mirror of
https://github.com/block/buzz.git
synced 2026-11-02 23:41:08 +00:00
## Problem Managed agents in internal Buzz builds should answer only their owner. Previously, an agent could keep a broader access setting and respond to other people, which did not match the access policy for internal builds. This PR makes owner-only access effective for every managed agent in internal builds and makes that restriction clear in the Desktop UI. Open source builds remain configurable. ## Changes - Enforce owner-only access when any managed agent starts or is deployed from an internal build. - Show the agent access control as locked to **Only me** in Desktop, with an explanation of why it cannot be changed. - Keep Welcome teammates working under the same rule without triggering unnecessary restarts. - Leave open source build behavior unchanged. This changes effective runtime access without rewriting stored or relay-advertised settings. The companion [#4064](https://github.com/block/buzz/pull/4064) explains the restriction in-thread when someone without access mentions an agent. The enforcement will remain inactive in shipped builds until [squareup/buzz-releases#74](https://github.com/squareup/buzz-releases/pull/74) marks internal releases during the build. ## Screenshots | Before | After | | --- | --- | |  |  | ## Tests Added coverage for: - Runtime enforcement for [locally run agents](e0165f52b5/desktop/src-tauri/src/managed_agents/runtime/tests.rs (L196)) and [deployed agents](e0165f52b5/desktop/src-tauri/src/commands/agents_tests.rs (L510)). - The [current-build deployment path](e0165f52b5/desktop/src-tauri/src/commands/agents_tests.rs (L455)), [invalid stored access](e0165f52b5/desktop/src-tauri/src/managed_agents/access_policy.rs (L98)), and the [local startup guard](e0165f52b5/desktop/src-tauri/src/managed_agents/env_vars/tests.rs (L149)). - Consistent enforcement across [both agent backends](e0165f52b5/desktop/src-tauri/src/managed_agents/access_policy.rs (L112)). - Welcome teammates created as [locally run](e0165f52b5/desktop/src/features/onboarding/welcomeGuide.test.mjs (L384)) or [deployed](e0165f52b5/desktop/src/features/onboarding/welcomeGuide.test.mjs (L393)) agents, including [access-only](e0165f52b5/desktop/src/features/onboarding/welcomeKickoff.test.mjs (L202)) and [runtime-related](e0165f52b5/desktop/src/features/onboarding/welcomeKickoff.test.mjs (L225)) restart behavior. The full Desktop Rust and JavaScript suites, type checks, formatting, clippy, and file-size checks passed. Playwright E2E was not run. --- Originated from Buzz channel [buzz-agent-control](buzz://channel?id=cf5dada7-e26a-4887-ae41-b3bd5f42d3b2). Supersedes #2537. --------- Signed-off-by: Tom Brow <tomb@block.xyz> Signed-off-by: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz> Co-authored-by: npub1tquskdu6yc4h8l7xxtceculxw600grekeq0xg2ukqfrwl7vrzg3quz3gmp <58390b379a262b73ffc632f19c73e6769ef40f36c81e642b960246eff9831222@buzz.block.builderlab.xyz> Co-authored-by: Amp <amp@ampcode.com>
455 lines
12 KiB
JavaScript
455 lines
12 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
activateWelcomeTeamPersonasSequentially,
|
|
buildWelcomeStarterCreateInput,
|
|
LEGACY_WELCOME_GUIDE_SYSTEM_PROMPT,
|
|
pickWelcomeGuideAgent,
|
|
pickWelcomeGuideAgentForRelay,
|
|
pickWelcomeTeamStarterAgentForRelay,
|
|
welcomeStarterRuntimeUpdate,
|
|
welcomeTeammateAccessUpdate,
|
|
welcomeTeammateHasExpectedAccess,
|
|
WELCOME_GUIDE_AGENT_NAME,
|
|
WELCOME_GUIDE_PERSONA_ID,
|
|
WELCOME_TEAM_ID,
|
|
WELCOME_TEAM_STARTERS,
|
|
} from "./welcomeGuide.ts";
|
|
|
|
const PUB_A = "a".repeat(64);
|
|
const PUB_B = "b".repeat(64);
|
|
const PUB_C = "c".repeat(64);
|
|
const RELAY_A = "ws://localhost:3000";
|
|
const RELAY_B = "ws://localhost:3001";
|
|
|
|
function makeAgent(overrides = {}) {
|
|
return {
|
|
pubkey: PUB_A,
|
|
name: WELCOME_GUIDE_AGENT_NAME,
|
|
personaId: null,
|
|
relayUrl: RELAY_A,
|
|
acpCommand: "buzz-acp",
|
|
agentCommand: "buzz-agent",
|
|
agentCommandOverride: null,
|
|
agentArgs: [],
|
|
mcpCommand: "buzz-dev-mcp",
|
|
turnTimeoutSeconds: 120,
|
|
idleTimeoutSeconds: null,
|
|
maxTurnDurationSeconds: null,
|
|
parallelism: 1,
|
|
systemPrompt: null,
|
|
model: null,
|
|
provider: null,
|
|
envVars: {},
|
|
status: "stopped",
|
|
pid: null,
|
|
createdAt: "2026-06-11T00:00:00.000Z",
|
|
updatedAt: "2026-06-11T00:00:00.000Z",
|
|
lastStartedAt: null,
|
|
lastStoppedAt: null,
|
|
lastExitCode: null,
|
|
lastError: null,
|
|
logPath: "",
|
|
startOnAppLaunch: false,
|
|
backend: { type: "local" },
|
|
backendAgentId: null,
|
|
respondTo: "owner-only",
|
|
respondToAllowlist: [],
|
|
teamId: WELCOME_TEAM_ID,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test("pickWelcomeGuideAgent reuses a legacy Kit guide", () => {
|
|
const legacyKit = makeAgent({
|
|
name: "Kit",
|
|
pubkey: PUB_A,
|
|
systemPrompt: LEGACY_WELCOME_GUIDE_SYSTEM_PROMPT,
|
|
});
|
|
|
|
assert.equal(pickWelcomeGuideAgent([legacyKit]), legacyKit);
|
|
});
|
|
|
|
test("pickWelcomeGuideAgent prefers a running legacy guide over stopped builtin Fizz", () => {
|
|
const stoppedBuiltinFizz = makeAgent({
|
|
pubkey: PUB_A,
|
|
personaId: WELCOME_GUIDE_PERSONA_ID,
|
|
status: "stopped",
|
|
});
|
|
const runningLegacyKit = makeAgent({
|
|
name: "Kit",
|
|
pubkey: PUB_B,
|
|
status: "running",
|
|
systemPrompt: LEGACY_WELCOME_GUIDE_SYSTEM_PROMPT,
|
|
});
|
|
|
|
assert.equal(
|
|
pickWelcomeGuideAgent([stoppedBuiltinFizz, runningLegacyKit]),
|
|
runningLegacyKit,
|
|
);
|
|
});
|
|
|
|
test("pickWelcomeGuideAgent ignores non-Kit agents with the legacy prompt", () => {
|
|
const nonKit = makeAgent({
|
|
pubkey: PUB_A,
|
|
name: "Scout",
|
|
systemPrompt: LEGACY_WELCOME_GUIDE_SYSTEM_PROMPT,
|
|
});
|
|
const fizz = makeAgent({
|
|
pubkey: PUB_C,
|
|
personaId: WELCOME_GUIDE_PERSONA_ID,
|
|
});
|
|
|
|
assert.equal(pickWelcomeGuideAgent([nonKit, fizz]), fizz);
|
|
});
|
|
|
|
test("pickWelcomeGuideAgentForRelay ignores Fizz agents from other communities", () => {
|
|
const otherCommunityFizz = makeAgent({
|
|
pubkey: PUB_A,
|
|
personaId: WELCOME_GUIDE_PERSONA_ID,
|
|
relayUrl: RELAY_A,
|
|
status: "running",
|
|
});
|
|
const currentCommunityFizz = makeAgent({
|
|
pubkey: PUB_B,
|
|
personaId: WELCOME_GUIDE_PERSONA_ID,
|
|
relayUrl: RELAY_B,
|
|
status: "stopped",
|
|
});
|
|
|
|
assert.equal(
|
|
pickWelcomeGuideAgentForRelay(
|
|
[otherCommunityFizz, currentCommunityFizz],
|
|
RELAY_B,
|
|
),
|
|
currentCommunityFizz,
|
|
);
|
|
});
|
|
|
|
test("pickWelcomeGuideAgentForRelay returns null when Fizz only exists in another community", () => {
|
|
const otherCommunityFizz = makeAgent({
|
|
pubkey: PUB_A,
|
|
personaId: WELCOME_GUIDE_PERSONA_ID,
|
|
relayUrl: RELAY_A,
|
|
});
|
|
|
|
assert.equal(
|
|
pickWelcomeGuideAgentForRelay([otherCommunityFizz], RELAY_B),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test("starter persona activation is serialized to protect the shared store", async () => {
|
|
const calls = [];
|
|
let activeWrites = 0;
|
|
|
|
await activateWelcomeTeamPersonasSequentially(
|
|
["builtin:fizz", "builtin:honey", "builtin:bumble"],
|
|
async (personaId) => {
|
|
assert.equal(activeWrites, 0, "activation writes must never overlap");
|
|
activeWrites += 1;
|
|
calls.push(personaId);
|
|
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
activeWrites -= 1;
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(calls, ["builtin:fizz", "builtin:honey", "builtin:bumble"]);
|
|
});
|
|
|
|
test("all Welcome starters use the onboarding runtime preference", async () => {
|
|
const claude = {
|
|
id: "claude",
|
|
label: "Claude",
|
|
avatarUrl: "https://runtime/claude.png",
|
|
availability: "available",
|
|
command: "claude-code-acp",
|
|
binaryPath: "/bin/claude-code-acp",
|
|
defaultArgs: [],
|
|
mcpCommand: null,
|
|
installHint: "",
|
|
installInstructionsUrl: "",
|
|
canAutoInstall: false,
|
|
underlyingCliPath: "/bin/claude",
|
|
};
|
|
const buzzAgent = {
|
|
...claude,
|
|
id: "buzz-agent",
|
|
label: "Buzz Agent",
|
|
command: "buzz-agent",
|
|
};
|
|
|
|
for (const starter of WELCOME_TEAM_STARTERS) {
|
|
const input = await buildWelcomeStarterCreateInput(
|
|
starter,
|
|
{
|
|
id: starter.personaId,
|
|
displayName: starter.name,
|
|
systemPrompt: `${starter.name} prompt`,
|
|
model: null,
|
|
provider: null,
|
|
runtime: null,
|
|
avatarUrl: null,
|
|
envVars: {},
|
|
isBuiltIn: true,
|
|
isActive: true,
|
|
},
|
|
[buzzAgent, claude],
|
|
"claude",
|
|
RELAY_A,
|
|
);
|
|
|
|
assert.equal(input.agentCommand, "claude-code-acp");
|
|
assert.equal(input.harnessOverride, true);
|
|
assert.equal(input.personaId, starter.personaId);
|
|
assert.equal(input.teamId, WELCOME_TEAM_ID);
|
|
assert.equal(input.relayUrl, RELAY_A);
|
|
assert.equal(input.spawnAfterCreate, false);
|
|
assert.equal(input.startOnAppLaunch, false);
|
|
}
|
|
});
|
|
|
|
test("existing Welcome starter rematerializes runtime-specific fields atomically", () => {
|
|
const existing = makeAgent({
|
|
pubkey: PUB_A,
|
|
personaId: WELCOME_GUIDE_PERSONA_ID,
|
|
agentCommand: "claude-agent-acp",
|
|
agentCommandOverride: "claude-agent-acp",
|
|
agentArgs: ["--old"],
|
|
mcpCommand: "",
|
|
model: "claude-sonnet",
|
|
provider: "anthropic",
|
|
});
|
|
|
|
assert.deepEqual(
|
|
welcomeStarterRuntimeUpdate(existing, {
|
|
name: "Fizz",
|
|
agentCommand: "codex-acp",
|
|
agentArgs: ["--new"],
|
|
mcpCommand: "buzz-dev-mcp",
|
|
model: "gpt-5.6-sol",
|
|
provider: null,
|
|
}),
|
|
{
|
|
pubkey: PUB_A,
|
|
agentCommand: "codex-acp",
|
|
harnessOverride: true,
|
|
agentArgs: ["--new"],
|
|
mcpCommand: "buzz-dev-mcp",
|
|
model: "gpt-5.6-sol",
|
|
provider: null,
|
|
},
|
|
);
|
|
});
|
|
|
|
test("existing Welcome starter clears stale model and provider for Claude", () => {
|
|
const existing = makeAgent({
|
|
personaId: WELCOME_GUIDE_PERSONA_ID,
|
|
agentCommand: "codex-acp",
|
|
agentArgs: [],
|
|
model: "gpt-5.6-sol",
|
|
provider: "openai",
|
|
});
|
|
|
|
assert.deepEqual(
|
|
welcomeStarterRuntimeUpdate(existing, {
|
|
name: "Fizz",
|
|
agentCommand: "claude-agent-acp",
|
|
agentArgs: [],
|
|
mcpCommand: "",
|
|
}),
|
|
{
|
|
pubkey: PUB_A,
|
|
agentCommand: "claude-agent-acp",
|
|
harnessOverride: true,
|
|
agentArgs: [],
|
|
mcpCommand: "",
|
|
model: null,
|
|
provider: null,
|
|
},
|
|
);
|
|
});
|
|
|
|
test("existing Welcome starter needs no update when runtime already matches", () => {
|
|
const existing = makeAgent({
|
|
personaId: WELCOME_GUIDE_PERSONA_ID,
|
|
agentCommand: "codex-acp",
|
|
agentArgs: ["--same"],
|
|
});
|
|
|
|
assert.equal(
|
|
welcomeStarterRuntimeUpdate(existing, {
|
|
name: "Fizz",
|
|
agentCommand: "codex-acp",
|
|
agentArgs: ["--same"],
|
|
mcpCommand: "buzz-dev-mcp",
|
|
model: null,
|
|
provider: null,
|
|
}),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test("welcome team starter definitions and role identities are stable", () => {
|
|
assert.equal(WELCOME_TEAM_ID, "builtin-team:welcome");
|
|
assert.deepEqual(WELCOME_TEAM_STARTERS, [
|
|
{ name: "Fizz", personaId: "builtin:fizz", role: "lead" },
|
|
{ name: "Honey", personaId: "builtin:honey", role: "teammate" },
|
|
{ name: "Bumble", personaId: "builtin:bumble", role: "teammate" },
|
|
]);
|
|
});
|
|
|
|
test("starter matching ignores user agents with a Welcome persona", () => {
|
|
const honey = WELCOME_TEAM_STARTERS[1];
|
|
const userHoney = makeAgent({
|
|
personaId: honey.personaId,
|
|
teamId: null,
|
|
});
|
|
|
|
assert.equal(
|
|
pickWelcomeTeamStarterAgentForRelay([userHoney], honey, RELAY_A),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test("starter matching uses persona identity rather than display name", () => {
|
|
const honey = WELCOME_TEAM_STARTERS[1];
|
|
const renamedHoney = makeAgent({
|
|
name: "Honey the Helper",
|
|
personaId: honey.personaId,
|
|
});
|
|
const nameOnlyHoney = makeAgent({ name: honey.name, pubkey: PUB_B });
|
|
|
|
assert.equal(
|
|
pickWelcomeTeamStarterAgentForRelay(
|
|
[nameOnlyHoney, renamedHoney],
|
|
honey,
|
|
RELAY_A,
|
|
),
|
|
renamedHoney,
|
|
);
|
|
});
|
|
|
|
test("starter matching is relay scoped and normalizes trailing slashes", () => {
|
|
const bumble = WELCOME_TEAM_STARTERS[2];
|
|
const otherRelay = makeAgent({
|
|
personaId: bumble.personaId,
|
|
relayUrl: RELAY_B,
|
|
status: "running",
|
|
});
|
|
const matchingRelay = makeAgent({
|
|
personaId: bumble.personaId,
|
|
relayUrl: `${RELAY_A}/`,
|
|
pubkey: PUB_B,
|
|
});
|
|
|
|
assert.equal(
|
|
pickWelcomeTeamStarterAgentForRelay(
|
|
[otherRelay, matchingRelay],
|
|
bumble,
|
|
RELAY_A,
|
|
),
|
|
matchingRelay,
|
|
);
|
|
});
|
|
|
|
test("starter matching prefers running, then deployed instances", () => {
|
|
const fizz = WELCOME_TEAM_STARTERS[0];
|
|
const stopped = makeAgent({ personaId: fizz.personaId });
|
|
const deployed = makeAgent({
|
|
personaId: fizz.personaId,
|
|
pubkey: PUB_B,
|
|
status: "deployed",
|
|
});
|
|
const running = makeAgent({
|
|
personaId: fizz.personaId,
|
|
pubkey: PUB_C,
|
|
status: "running",
|
|
});
|
|
|
|
assert.equal(
|
|
pickWelcomeTeamStarterAgentForRelay(
|
|
[stopped, deployed, running],
|
|
fizz,
|
|
RELAY_A,
|
|
),
|
|
running,
|
|
);
|
|
assert.equal(
|
|
pickWelcomeTeamStarterAgentForRelay([stopped, deployed], fizz, RELAY_A),
|
|
deployed,
|
|
);
|
|
});
|
|
|
|
test("owner-only-access policy accepts local Welcome teammates", () => {
|
|
const teammate = makeAgent({
|
|
respondTo: "owner-only",
|
|
respondToAllowlist: [],
|
|
});
|
|
assert.equal(welcomeTeammateHasExpectedAccess(teammate, PUB_B, true), true);
|
|
assert.equal(welcomeTeammateHasExpectedAccess(teammate, PUB_B, false), false);
|
|
});
|
|
|
|
test("access remediation converges for an upgraded owner-only install", () => {
|
|
// Pre-existing installs allowlisted the lead. An owner-only build must move
|
|
// them to owner-only, and the write it makes must satisfy the predicate, so
|
|
// the next provisioning pass makes no further write.
|
|
const allowlisted = makeAgent({
|
|
respondTo: "allowlist",
|
|
respondToAllowlist: [PUB_B],
|
|
});
|
|
const update = welcomeTeammateAccessUpdate(allowlisted, PUB_B, true);
|
|
assert.deepEqual(update, {
|
|
pubkey: PUB_A,
|
|
respondTo: "owner-only",
|
|
respondToAllowlist: [],
|
|
});
|
|
const remediated = makeAgent({
|
|
respondTo: update.respondTo,
|
|
respondToAllowlist: update.respondToAllowlist,
|
|
});
|
|
assert.equal(welcomeTeammateHasExpectedAccess(remediated, PUB_B, true), true);
|
|
assert.equal(welcomeTeammateAccessUpdate(remediated, PUB_B, true), null);
|
|
});
|
|
|
|
test("access remediation allowlists the lead when the build is not owner-only", () => {
|
|
const ownerOnly = makeAgent({
|
|
respondTo: "owner-only",
|
|
respondToAllowlist: [],
|
|
});
|
|
const update = welcomeTeammateAccessUpdate(ownerOnly, PUB_B, false);
|
|
assert.deepEqual(update, {
|
|
pubkey: PUB_A,
|
|
respondTo: "allowlist",
|
|
respondToAllowlist: [PUB_B],
|
|
});
|
|
const remediated = makeAgent({
|
|
respondTo: update.respondTo,
|
|
respondToAllowlist: update.respondToAllowlist,
|
|
});
|
|
assert.equal(
|
|
welcomeTeammateHasExpectedAccess(remediated, PUB_B, false),
|
|
true,
|
|
);
|
|
assert.equal(welcomeTeammateAccessUpdate(remediated, PUB_B, false), null);
|
|
});
|
|
|
|
test("access remediation skips a teammate that already allows the lead", () => {
|
|
const allowlisted = makeAgent({
|
|
respondTo: "allowlist",
|
|
respondToAllowlist: [PUB_B, PUB_C],
|
|
});
|
|
assert.equal(welcomeTeammateAccessUpdate(allowlisted, PUB_B, false), null);
|
|
});
|
|
|
|
test("owner-only-access policy accepts provider Welcome teammates", () => {
|
|
const teammate = makeAgent({
|
|
backend: { type: "provider", id: "remote", config: {} },
|
|
respondTo: "owner-only",
|
|
respondToAllowlist: [],
|
|
});
|
|
assert.equal(welcomeTeammateHasExpectedAccess(teammate, PUB_B, true), true);
|
|
assert.equal(welcomeTeammateHasExpectedAccess(teammate, PUB_B, false), false);
|
|
});
|