fix(agent-teams): harden lead auth and require teammate categories
This commit is contained in:
parent
e984ce7493
commit
fe05a1f254
@ -84,10 +84,7 @@ export function appendInboxMessage(teamName: string, agentName: string, message:
|
|||||||
assertValidInboxAgentName(agentName)
|
assertValidInboxAgentName(agentName)
|
||||||
withInboxLock(teamName, () => {
|
withInboxLock(teamName, () => {
|
||||||
const path = getTeamInboxPath(teamName, agentName)
|
const path = getTeamInboxPath(teamName, agentName)
|
||||||
if (!existsSync(path)) {
|
const messages = existsSync(path) ? parseInboxFile(readFileSync(path, "utf-8")) : []
|
||||||
writeJsonAtomic(path, [])
|
|
||||||
}
|
|
||||||
const messages = readInboxMessages(teamName, agentName)
|
|
||||||
messages.push(TeamInboxMessageSchema.parse(message))
|
messages.push(TeamInboxMessageSchema.parse(message))
|
||||||
writeInboxMessages(teamName, agentName, messages)
|
writeInboxMessages(teamName, agentName, messages)
|
||||||
})
|
})
|
||||||
@ -153,18 +150,27 @@ export function readInbox(
|
|||||||
return selected
|
return selected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedSet = unreadOnly ? new Set(selected) : null
|
||||||
|
let changed = false
|
||||||
|
|
||||||
const updated = messages.map((message) => {
|
const updated = messages.map((message) => {
|
||||||
if (!unreadOnly) {
|
if (!unreadOnly) {
|
||||||
|
if (!message.read) {
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
return { ...message, read: true }
|
return { ...message, read: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selected.some((selectedMessage) => selectedMessage.timestamp === message.timestamp && selectedMessage.from === message.from && selectedMessage.text === message.text)) {
|
if (selectedSet?.has(message)) {
|
||||||
|
changed = true
|
||||||
return { ...message, read: true }
|
return { ...message, read: true }
|
||||||
}
|
}
|
||||||
return message
|
return message
|
||||||
})
|
})
|
||||||
|
|
||||||
writeInboxMessages(teamName, agentName, updated)
|
if (changed) {
|
||||||
|
writeInboxMessages(teamName, agentName, updated)
|
||||||
|
}
|
||||||
return selected
|
return selected
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||||
import type { BackgroundManager } from "../../features/background-agent"
|
import type { BackgroundManager } from "../../features/background-agent"
|
||||||
import { buildShutdownRequestId, readInbox, sendPlainInboxMessage, sendStructuredInboxMessage } from "./inbox-store"
|
import { buildShutdownRequestId, readInbox, sendPlainInboxMessage, sendStructuredInboxMessage } from "./inbox-store"
|
||||||
import { getTeamMember, listTeammates, readTeamConfigOrThrow, updateTeamConfig } from "./team-config-store"
|
import { getTeamMember, listTeammates, readTeamConfigOrThrow } from "./team-config-store"
|
||||||
import { validateAgentNameOrLead, validateTeamName } from "./name-validation"
|
import { validateAgentNameOrLead, validateTeamName } from "./name-validation"
|
||||||
import { resumeTeammateWithMessage } from "./teammate-runtime"
|
import { resumeTeammateWithMessage } from "./teammate-runtime"
|
||||||
import {
|
import {
|
||||||
@ -25,13 +25,6 @@ function resolveSenderFromContext(config: TeamConfig, context: TeamToolContext):
|
|||||||
return matchedMember?.name ?? null
|
return matchedMember?.name ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
function claimLeadSession(teamName: string, nextLeadSessionId: string): TeamConfig {
|
|
||||||
return updateTeamConfig(teamName, (current) => ({
|
|
||||||
...current,
|
|
||||||
leadSessionId: nextLeadSessionId,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createSendMessageTool(manager: BackgroundManager): ToolDefinition {
|
export function createSendMessageTool(manager: BackgroundManager): ToolDefinition {
|
||||||
return tool({
|
return tool({
|
||||||
description: "Send direct or broadcast team messages and protocol responses.",
|
description: "Send direct or broadcast team messages and protocol responses.",
|
||||||
@ -57,12 +50,8 @@ export function createSendMessageTool(manager: BackgroundManager): ToolDefinitio
|
|||||||
if (senderError) {
|
if (senderError) {
|
||||||
return JSON.stringify({ error: senderError })
|
return JSON.stringify({ error: senderError })
|
||||||
}
|
}
|
||||||
let config = readTeamConfigOrThrow(input.team_name)
|
const config = readTeamConfigOrThrow(input.team_name)
|
||||||
let actor = resolveSenderFromContext(config, context)
|
const actor = resolveSenderFromContext(config, context)
|
||||||
if (!actor && requestedSender === "team-lead") {
|
|
||||||
config = claimLeadSession(input.team_name, context.sessionID)
|
|
||||||
actor = "team-lead"
|
|
||||||
}
|
|
||||||
if (!actor) {
|
if (!actor) {
|
||||||
return JSON.stringify({ error: "unauthorized_sender_session" })
|
return JSON.stringify({ error: "unauthorized_sender_session" })
|
||||||
}
|
}
|
||||||
@ -234,12 +223,8 @@ export function createReadInboxTool(): ToolDefinition {
|
|||||||
if (agentError) {
|
if (agentError) {
|
||||||
return JSON.stringify({ error: agentError })
|
return JSON.stringify({ error: agentError })
|
||||||
}
|
}
|
||||||
let config = readTeamConfigOrThrow(input.team_name)
|
const config = readTeamConfigOrThrow(input.team_name)
|
||||||
let actor = resolveSenderFromContext(config, context)
|
const actor = resolveSenderFromContext(config, context)
|
||||||
if (!actor && input.agent_name === "team-lead") {
|
|
||||||
config = claimLeadSession(input.team_name, context.sessionID)
|
|
||||||
actor = "team-lead"
|
|
||||||
}
|
|
||||||
if (!actor) {
|
if (!actor) {
|
||||||
return JSON.stringify({ error: "unauthorized_reader_session" })
|
return JSON.stringify({ error: "unauthorized_reader_session" })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,12 +51,12 @@ function withTeamLock<T>(teamName: string, operation: () => T): T {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLeadMember(teamName: string, cwd: string, model: string): TeamLeadMember {
|
function createLeadMember(teamName: string, cwd: string, leadModel: string): TeamLeadMember {
|
||||||
return {
|
return {
|
||||||
agentId: `team-lead@${teamName}`,
|
agentId: `team-lead@${teamName}`,
|
||||||
name: "team-lead",
|
name: "team-lead",
|
||||||
agentType: "team-lead",
|
agentType: "team-lead",
|
||||||
model,
|
model: leadModel,
|
||||||
joinedAt: nowMs(),
|
joinedAt: nowMs(),
|
||||||
cwd,
|
cwd,
|
||||||
subscriptions: [],
|
subscriptions: [],
|
||||||
@ -82,7 +82,7 @@ export function createTeamConfig(
|
|||||||
description: string,
|
description: string,
|
||||||
leadSessionId: string,
|
leadSessionId: string,
|
||||||
cwd: string,
|
cwd: string,
|
||||||
model: string,
|
leadModel: string,
|
||||||
): TeamConfig {
|
): TeamConfig {
|
||||||
ensureTeamStorageDirs(teamName)
|
ensureTeamStorageDirs(teamName)
|
||||||
|
|
||||||
@ -93,7 +93,7 @@ export function createTeamConfig(
|
|||||||
createdAt: nowMs(),
|
createdAt: nowMs(),
|
||||||
leadAgentId,
|
leadAgentId,
|
||||||
leadSessionId,
|
leadSessionId,
|
||||||
members: [createLeadMember(teamName, cwd, model)],
|
members: [createLeadMember(teamName, cwd, leadModel)],
|
||||||
}
|
}
|
||||||
|
|
||||||
return withTeamLock(teamName, () => {
|
return withTeamLock(teamName, () => {
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export function createTeamCreateTool(): ToolDefinition {
|
|||||||
input.description ?? "",
|
input.description ?? "",
|
||||||
context.sessionID,
|
context.sessionID,
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
context.agent ?? "native",
|
"native/team-lead",
|
||||||
)
|
)
|
||||||
ensureInbox(config.name, "team-lead")
|
ensureInbox(config.name, "team-lead")
|
||||||
|
|
||||||
|
|||||||
@ -70,7 +70,7 @@ export interface SpawnTeammateParams {
|
|||||||
teamName: string
|
teamName: string
|
||||||
name: string
|
name: string
|
||||||
prompt: string
|
prompt: string
|
||||||
category?: string
|
category: string
|
||||||
subagentType: string
|
subagentType: string
|
||||||
model?: string
|
model?: string
|
||||||
planModeRequired: boolean
|
planModeRequired: boolean
|
||||||
@ -100,17 +100,20 @@ async function getSystemDefaultModel(client: PluginInput["client"]): Promise<str
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resolveSpawnExecution(params: SpawnTeammateParams): Promise<SpawnExecution> {
|
async function resolveSpawnExecution(params: SpawnTeammateParams): Promise<SpawnExecution> {
|
||||||
if (!params.category) {
|
if (params.model) {
|
||||||
const launchModel = parseModel(params.model)
|
const launchModel = parseModel(params.model)
|
||||||
return {
|
return {
|
||||||
agentType: params.subagentType,
|
agentType: params.subagentType,
|
||||||
teammateModel: params.model ?? "native",
|
teammateModel: params.model,
|
||||||
...(launchModel ? { launchModel } : {}),
|
...(launchModel ? { launchModel } : {}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!params.categoryContext?.client) {
|
if (!params.categoryContext?.client) {
|
||||||
throw new Error("category_requires_client_context")
|
return {
|
||||||
|
agentType: params.subagentType,
|
||||||
|
teammateModel: "native",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentContext = resolveParentContext({
|
const parentContext = resolveParentContext({
|
||||||
@ -179,7 +182,7 @@ export async function spawnTeammate(params: SpawnTeammateParams): Promise<TeamTe
|
|||||||
agentId: `${params.name}@${params.teamName}`,
|
agentId: `${params.name}@${params.teamName}`,
|
||||||
name: params.name,
|
name: params.name,
|
||||||
agentType: execution.agentType,
|
agentType: execution.agentType,
|
||||||
...(params.category ? { category: params.category } : {}),
|
category: params.category,
|
||||||
model: execution.teammateModel,
|
model: execution.teammateModel,
|
||||||
prompt: params.prompt,
|
prompt: params.prompt,
|
||||||
color: assignNextColor(current),
|
color: assignNextColor(current),
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export function createSpawnTeammateTool(manager: BackgroundManager, options?: Ag
|
|||||||
team_name: tool.schema.string().describe("Team name"),
|
team_name: tool.schema.string().describe("Team name"),
|
||||||
name: tool.schema.string().describe("Teammate name"),
|
name: tool.schema.string().describe("Teammate name"),
|
||||||
prompt: tool.schema.string().describe("Initial teammate prompt"),
|
prompt: tool.schema.string().describe("Initial teammate prompt"),
|
||||||
category: tool.schema.string().optional().describe("Optional category (spawns sisyphus-junior with category model/prompt)") ,
|
category: tool.schema.string().optional().describe("Required category for teammate metadata and routing"),
|
||||||
subagent_type: tool.schema.string().optional().describe("Agent name to run (default: sisyphus-junior)"),
|
subagent_type: tool.schema.string().optional().describe("Agent name to run (default: sisyphus-junior)"),
|
||||||
model: tool.schema.string().optional().describe("Optional model override in provider/model format"),
|
model: tool.schema.string().optional().describe("Optional model override in provider/model format"),
|
||||||
plan_mode_required: tool.schema.boolean().optional().describe("Enable plan mode flag in teammate metadata"),
|
plan_mode_required: tool.schema.boolean().optional().describe("Enable plan mode flag in teammate metadata"),
|
||||||
@ -46,19 +46,15 @@ export function createSpawnTeammateTool(manager: BackgroundManager, options?: Ag
|
|||||||
return JSON.stringify({ error: agentError })
|
return JSON.stringify({ error: agentError })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!input.category || !input.category.trim()) {
|
||||||
|
return JSON.stringify({ error: "category_required" })
|
||||||
|
}
|
||||||
|
|
||||||
if (input.category && input.subagent_type && input.subagent_type !== "sisyphus-junior") {
|
if (input.category && input.subagent_type && input.subagent_type !== "sisyphus-junior") {
|
||||||
return JSON.stringify({ error: "category_conflicts_with_subagent_type" })
|
return JSON.stringify({ error: "category_conflicts_with_subagent_type" })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.category && input.model) {
|
const resolvedSubagentType = input.subagent_type ?? "sisyphus-junior"
|
||||||
return JSON.stringify({ error: "category_conflicts_with_model_override" })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.category && !options?.client) {
|
|
||||||
return JSON.stringify({ error: "category_requires_client_context" })
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolvedSubagentType = input.category ? "sisyphus-junior" : input.subagent_type ?? "sisyphus-junior"
|
|
||||||
|
|
||||||
const teammate = await spawnTeammate({
|
const teammate = await spawnTeammate({
|
||||||
teamName: input.team_name,
|
teamName: input.team_name,
|
||||||
@ -100,7 +96,7 @@ export function createForceKillTeammateTool(manager: BackgroundManager): ToolDef
|
|||||||
team_name: tool.schema.string().describe("Team name"),
|
team_name: tool.schema.string().describe("Team name"),
|
||||||
agent_name: tool.schema.string().describe("Teammate name"),
|
agent_name: tool.schema.string().describe("Teammate name"),
|
||||||
},
|
},
|
||||||
execute: async (args: Record<string, unknown>): Promise<string> => {
|
execute: async (args: Record<string, unknown>, context: TeamToolContext): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const input = TeamForceKillInputSchema.parse(args)
|
const input = TeamForceKillInputSchema.parse(args)
|
||||||
const teamError = validateTeamName(input.team_name)
|
const teamError = validateTeamName(input.team_name)
|
||||||
@ -112,6 +108,9 @@ export function createForceKillTeammateTool(manager: BackgroundManager): ToolDef
|
|||||||
return JSON.stringify({ error: agentError })
|
return JSON.stringify({ error: agentError })
|
||||||
}
|
}
|
||||||
const config = readTeamConfigOrThrow(input.team_name)
|
const config = readTeamConfigOrThrow(input.team_name)
|
||||||
|
if (context.sessionID !== config.leadSessionId) {
|
||||||
|
return JSON.stringify({ error: "unauthorized_lead_session" })
|
||||||
|
}
|
||||||
const member = getTeamMember(config, input.agent_name)
|
const member = getTeamMember(config, input.agent_name)
|
||||||
if (!member || !isTeammateMember(member)) {
|
if (!member || !isTeammateMember(member)) {
|
||||||
return JSON.stringify({ error: "teammate_not_found" })
|
return JSON.stringify({ error: "teammate_not_found" })
|
||||||
@ -149,7 +148,7 @@ export function createProcessShutdownTool(manager: BackgroundManager): ToolDefin
|
|||||||
team_name: tool.schema.string().describe("Team name"),
|
team_name: tool.schema.string().describe("Team name"),
|
||||||
agent_name: tool.schema.string().describe("Teammate name"),
|
agent_name: tool.schema.string().describe("Teammate name"),
|
||||||
},
|
},
|
||||||
execute: async (args: Record<string, unknown>): Promise<string> => {
|
execute: async (args: Record<string, unknown>, context: TeamToolContext): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const input = TeamProcessShutdownInputSchema.parse(args)
|
const input = TeamProcessShutdownInputSchema.parse(args)
|
||||||
const teamError = validateTeamName(input.team_name)
|
const teamError = validateTeamName(input.team_name)
|
||||||
@ -165,6 +164,9 @@ export function createProcessShutdownTool(manager: BackgroundManager): ToolDefin
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = readTeamConfigOrThrow(input.team_name)
|
const config = readTeamConfigOrThrow(input.team_name)
|
||||||
|
if (context.sessionID !== config.leadSessionId) {
|
||||||
|
return JSON.stringify({ error: "unauthorized_lead_session" })
|
||||||
|
}
|
||||||
const member = getTeamMember(config, input.agent_name)
|
const member = getTeamMember(config, input.agent_name)
|
||||||
if (!member || !isTeammateMember(member)) {
|
if (!member || !isTeammateMember(member)) {
|
||||||
return JSON.stringify({ error: "teammate_not_found" })
|
return JSON.stringify({ error: "teammate_not_found" })
|
||||||
|
|||||||
@ -181,12 +181,13 @@ describe("agent-teams tools functional", () => {
|
|||||||
//#when
|
//#when
|
||||||
const config = await executeJsonTool(tools, "read_config", { team_name: "core" }, context) as {
|
const config = await executeJsonTool(tools, "read_config", { team_name: "core" }, context) as {
|
||||||
name: string
|
name: string
|
||||||
members: Array<{ name: string }>
|
members: Array<{ name: string; model?: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(config.name).toBe("core")
|
expect(config.name).toBe("core")
|
||||||
expect(config.members.map((member) => member.name)).toEqual(["team-lead"])
|
expect(config.members.map((member) => member.name)).toEqual(["team-lead"])
|
||||||
|
expect(config.members[0]?.model).toBe("native/team-lead")
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const deleted = await executeJsonTool(tools, "team_delete", { team_name: "core" }, context) as {
|
const deleted = await executeJsonTool(tools, "team_delete", { team_name: "core" }, context) as {
|
||||||
@ -213,6 +214,7 @@ describe("agent-teams tools functional", () => {
|
|||||||
team_name: "core",
|
team_name: "core",
|
||||||
name: "worker_1",
|
name: "worker_1",
|
||||||
prompt: "Handle release prep",
|
prompt: "Handle release prep",
|
||||||
|
category: "quick",
|
||||||
},
|
},
|
||||||
context,
|
context,
|
||||||
)
|
)
|
||||||
@ -340,6 +342,30 @@ describe("agent-teams tools functional", () => {
|
|||||||
expect(teammate?.model).toBe(`${resolvedModel.providerID}/${resolvedModel.modelID}`)
|
expect(teammate?.model).toBe(`${resolvedModel.providerID}/${resolvedModel.modelID}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("spawn_teammate requires a category", async () => {
|
||||||
|
//#given
|
||||||
|
const { manager } = createMockManager()
|
||||||
|
const tools = createAgentTeamsTools(manager)
|
||||||
|
const context = createContext()
|
||||||
|
|
||||||
|
await executeJsonTool(tools, "team_create", { team_name: "core" }, context)
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await executeJsonTool(
|
||||||
|
tools,
|
||||||
|
"spawn_teammate",
|
||||||
|
{
|
||||||
|
team_name: "core",
|
||||||
|
name: "worker_1",
|
||||||
|
prompt: "Handle release prep",
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
) as { error?: string }
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.error).toBe("category_required")
|
||||||
|
})
|
||||||
|
|
||||||
test("rejects category with incompatible subagent_type", async () => {
|
test("rejects category with incompatible subagent_type", async () => {
|
||||||
//#given
|
//#given
|
||||||
const { manager } = createMockManager()
|
const { manager } = createMockManager()
|
||||||
@ -483,6 +509,7 @@ describe("agent-teams tools functional", () => {
|
|||||||
team_name: "core",
|
team_name: "core",
|
||||||
name: "worker_1",
|
name: "worker_1",
|
||||||
prompt: "Handle release prep",
|
prompt: "Handle release prep",
|
||||||
|
category: "quick",
|
||||||
},
|
},
|
||||||
context,
|
context,
|
||||||
) as { name: string; session_id: string; task_id: string }
|
) as { name: string; session_id: string; task_id: string }
|
||||||
@ -617,6 +644,7 @@ describe("agent-teams tools functional", () => {
|
|||||||
team_name: "core",
|
team_name: "core",
|
||||||
name: "worker_1",
|
name: "worker_1",
|
||||||
prompt: "Handle release prep",
|
prompt: "Handle release prep",
|
||||||
|
category: "quick",
|
||||||
},
|
},
|
||||||
leadContext,
|
leadContext,
|
||||||
)
|
)
|
||||||
@ -653,6 +681,78 @@ describe("agent-teams tools functional", () => {
|
|||||||
expect(configAfterShutdown.members.some((member) => member.name === "worker_1")).toBe(false)
|
expect(configAfterShutdown.members.some((member) => member.name === "worker_1")).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("force_kill_teammate requires lead session authorization", async () => {
|
||||||
|
//#given
|
||||||
|
const { manager } = createMockManager()
|
||||||
|
const tools = createAgentTeamsTools(manager)
|
||||||
|
const leadContext = createContext()
|
||||||
|
|
||||||
|
await executeJsonTool(tools, "team_create", { team_name: "core" }, leadContext)
|
||||||
|
await executeJsonTool(
|
||||||
|
tools,
|
||||||
|
"spawn_teammate",
|
||||||
|
{
|
||||||
|
team_name: "core",
|
||||||
|
name: "worker_1",
|
||||||
|
prompt: "Handle release prep",
|
||||||
|
category: "quick",
|
||||||
|
},
|
||||||
|
leadContext,
|
||||||
|
)
|
||||||
|
|
||||||
|
const teammateContext = createContext("ses-worker-1")
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const unauthorized = await executeJsonTool(
|
||||||
|
tools,
|
||||||
|
"force_kill_teammate",
|
||||||
|
{
|
||||||
|
team_name: "core",
|
||||||
|
agent_name: "worker_1",
|
||||||
|
},
|
||||||
|
teammateContext,
|
||||||
|
) as { error?: string }
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(unauthorized.error).toBe("unauthorized_lead_session")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("process_shutdown_approved requires lead session authorization", async () => {
|
||||||
|
//#given
|
||||||
|
const { manager } = createMockManager()
|
||||||
|
const tools = createAgentTeamsTools(manager)
|
||||||
|
const leadContext = createContext()
|
||||||
|
|
||||||
|
await executeJsonTool(tools, "team_create", { team_name: "core" }, leadContext)
|
||||||
|
await executeJsonTool(
|
||||||
|
tools,
|
||||||
|
"spawn_teammate",
|
||||||
|
{
|
||||||
|
team_name: "core",
|
||||||
|
name: "worker_1",
|
||||||
|
prompt: "Handle release prep",
|
||||||
|
category: "quick",
|
||||||
|
},
|
||||||
|
leadContext,
|
||||||
|
)
|
||||||
|
|
||||||
|
const teammateContext = createContext("ses-worker-1")
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const unauthorized = await executeJsonTool(
|
||||||
|
tools,
|
||||||
|
"process_shutdown_approved",
|
||||||
|
{
|
||||||
|
team_name: "core",
|
||||||
|
agent_name: "worker_1",
|
||||||
|
},
|
||||||
|
teammateContext,
|
||||||
|
) as { error?: string }
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(unauthorized.error).toBe("unauthorized_lead_session")
|
||||||
|
})
|
||||||
|
|
||||||
test("rolls back teammate and cancels background task when launch fails", async () => {
|
test("rolls back teammate and cancels background task when launch fails", async () => {
|
||||||
//#given
|
//#given
|
||||||
const { manager, cancelCalls } = createFailingLaunchManager()
|
const { manager, cancelCalls } = createFailingLaunchManager()
|
||||||
@ -668,6 +768,7 @@ describe("agent-teams tools functional", () => {
|
|||||||
team_name: "core",
|
team_name: "core",
|
||||||
name: "worker_1",
|
name: "worker_1",
|
||||||
prompt: "Handle release prep",
|
prompt: "Handle release prep",
|
||||||
|
category: "quick",
|
||||||
},
|
},
|
||||||
context,
|
context,
|
||||||
) as { error?: string }
|
) as { error?: string }
|
||||||
@ -709,6 +810,7 @@ describe("agent-teams tools functional", () => {
|
|||||||
team_name: "core",
|
team_name: "core",
|
||||||
name: "worker_1",
|
name: "worker_1",
|
||||||
prompt: "Handle release prep",
|
prompt: "Handle release prep",
|
||||||
|
category: "quick",
|
||||||
model: "invalid-format",
|
model: "invalid-format",
|
||||||
},
|
},
|
||||||
context,
|
context,
|
||||||
@ -742,6 +844,7 @@ describe("agent-teams tools functional", () => {
|
|||||||
team_name: "core",
|
team_name: "core",
|
||||||
name: "worker_1",
|
name: "worker_1",
|
||||||
prompt: "Handle release prep",
|
prompt: "Handle release prep",
|
||||||
|
category: "quick",
|
||||||
model: "openai/gpt-5.3-codex/reasoning",
|
model: "openai/gpt-5.3-codex/reasoning",
|
||||||
},
|
},
|
||||||
context,
|
context,
|
||||||
@ -792,6 +895,7 @@ describe("agent-teams tools functional", () => {
|
|||||||
team_name: "core",
|
team_name: "core",
|
||||||
name: "worker_1",
|
name: "worker_1",
|
||||||
prompt: "Handle release prep",
|
prompt: "Handle release prep",
|
||||||
|
category: "quick",
|
||||||
},
|
},
|
||||||
leadContext,
|
leadContext,
|
||||||
)
|
)
|
||||||
@ -827,14 +931,13 @@ describe("agent-teams tools functional", () => {
|
|||||||
expect(Array.isArray(ownInbox)).toBe(true)
|
expect(Array.isArray(ownInbox)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("allows lead session to rotate after restart using team-lead identity", async () => {
|
test("rejects unknown session claiming team-lead identity", async () => {
|
||||||
//#given
|
//#given
|
||||||
const { manager } = createMockManager()
|
const { manager } = createMockManager()
|
||||||
const tools = createAgentTeamsTools(manager)
|
const tools = createAgentTeamsTools(manager)
|
||||||
const originalLead = createContext("ses-main")
|
const leadContext = createContext("ses-main")
|
||||||
await executeJsonTool(tools, "team_create", { team_name: "core" }, originalLead)
|
await executeJsonTool(tools, "team_create", { team_name: "core" }, leadContext)
|
||||||
|
const unknownContext = createContext("ses-unknown")
|
||||||
const restartedLead = createContext("ses-restarted")
|
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const sendResult = await executeJsonTool(
|
const sendResult = await executeJsonTool(
|
||||||
@ -848,20 +951,43 @@ describe("agent-teams tools functional", () => {
|
|||||||
summary: "restart",
|
summary: "restart",
|
||||||
content: "Lead session migrated",
|
content: "Lead session migrated",
|
||||||
},
|
},
|
||||||
restartedLead,
|
unknownContext,
|
||||||
) as { success?: boolean; error?: string }
|
) as { success?: boolean; error?: string }
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(sendResult.error).toBeUndefined()
|
expect(sendResult.success).toBeUndefined()
|
||||||
expect(sendResult.success).toBe(true)
|
expect(sendResult.error).toBe("unauthorized_sender_session")
|
||||||
|
|
||||||
//#when
|
//#when
|
||||||
const config = await executeJsonTool(tools, "read_config", { team_name: "core" }, restartedLead) as {
|
const config = await executeJsonTool(tools, "read_config", { team_name: "core" }, leadContext) as {
|
||||||
leadSessionId: string
|
leadSessionId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
//#then
|
//#then
|
||||||
expect(config.leadSessionId).toBe("ses-restarted")
|
expect(config.leadSessionId).toBe("ses-main")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects unknown session claiming team-lead inbox", async () => {
|
||||||
|
//#given
|
||||||
|
const { manager } = createMockManager()
|
||||||
|
const tools = createAgentTeamsTools(manager)
|
||||||
|
const leadContext = createContext("ses-main")
|
||||||
|
await executeJsonTool(tools, "team_create", { team_name: "core" }, leadContext)
|
||||||
|
const unknownContext = createContext("ses-unknown")
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await executeJsonTool(
|
||||||
|
tools,
|
||||||
|
"read_inbox",
|
||||||
|
{
|
||||||
|
team_name: "core",
|
||||||
|
agent_name: "team-lead",
|
||||||
|
},
|
||||||
|
unknownContext,
|
||||||
|
) as { error?: string }
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(result.error).toBe("unauthorized_reader_session")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("clears old inbox when teammate is removed then re-spawned", async () => {
|
test("clears old inbox when teammate is removed then re-spawned", async () => {
|
||||||
@ -878,6 +1004,7 @@ describe("agent-teams tools functional", () => {
|
|||||||
team_name: "core",
|
team_name: "core",
|
||||||
name: "worker_1",
|
name: "worker_1",
|
||||||
prompt: "First run",
|
prompt: "First run",
|
||||||
|
category: "quick",
|
||||||
},
|
},
|
||||||
context,
|
context,
|
||||||
)
|
)
|
||||||
@ -913,6 +1040,7 @@ describe("agent-teams tools functional", () => {
|
|||||||
team_name: "core",
|
team_name: "core",
|
||||||
name: "worker_1",
|
name: "worker_1",
|
||||||
prompt: "Second run",
|
prompt: "Second run",
|
||||||
|
category: "quick",
|
||||||
},
|
},
|
||||||
context,
|
context,
|
||||||
)
|
)
|
||||||
@ -1004,6 +1132,7 @@ describe("agent-teams tools functional", () => {
|
|||||||
team_name: "core",
|
team_name: "core",
|
||||||
name: "worker_1",
|
name: "worker_1",
|
||||||
prompt: "Handle release prep",
|
prompt: "Handle release prep",
|
||||||
|
category: "quick",
|
||||||
},
|
},
|
||||||
leadContext,
|
leadContext,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -9,6 +9,7 @@ describe("agent-teams types", () => {
|
|||||||
agentId: "worker@team",
|
agentId: "worker@team",
|
||||||
name: "worker",
|
name: "worker",
|
||||||
agentType: "team-lead",
|
agentType: "team-lead",
|
||||||
|
category: "quick",
|
||||||
model: "native",
|
model: "native",
|
||||||
prompt: "do work",
|
prompt: "do work",
|
||||||
color: "blue",
|
color: "blue",
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export const TeamTeammateMemberSchema = z.object({
|
|||||||
agentType: z.string().refine((value) => value !== "team-lead", {
|
agentType: z.string().refine((value) => value !== "team-lead", {
|
||||||
message: "agent_type_reserved",
|
message: "agent_type_reserved",
|
||||||
}),
|
}),
|
||||||
category: z.string().optional(),
|
category: z.string(),
|
||||||
model: z.string(),
|
model: z.string(),
|
||||||
prompt: z.string(),
|
prompt: z.string(),
|
||||||
color: z.string(),
|
color: z.string(),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user