fix(team-mode): reject team_create from hard-reject agents (#3987)

When a hard-reject agent (e.g. prometheus) called team_create with an
explicit `lead` in the spec, the eligibility check that runs in the
no-lead branch of shouldReuseCallerLeadSession was bypassed. The
caller session was never registered in the team, the spawned lead
ran as a detached child, and replies routed to the spawned lead
never reached the caller — the caller became an orphan that could
send but never receive.

Move the caller eligibility guard to the top of team_create.execute
so it runs unconditionally before any team-run state mutates. Throw
an actionable error naming the agent and explaining hard-reject
agents cannot lead teams regardless of an explicit `lead` in the
spec.
This commit is contained in:
ZeyuFu
2026-05-16 01:40:13 -04:00
parent 1723a8b74c
commit 4c36005467
2 changed files with 37 additions and 0 deletions
@@ -284,6 +284,34 @@ describe("team lifecycle tools", () => {
expect(hasRuntime(created.teamRunId)).toBe(false)
})
test("team_create denies a hard-reject caller even when spec has an explicit lead field", async () => {
// given
const teamCreateTool = createTeamCreateToolForTest()
const prometheusContext = {
...createToolContext("lead-session"),
agent: "prometheus",
}
const inlineSpec = {
name: "alpha-team",
lead: { kind: "subagent_type", subagent_type: "sisyphus" },
members: [{ kind: "category", name: "member-a", category: "quick", prompt: "Do the assigned work" }],
}
// when
let errorMessage = ""
try {
await teamCreateTool.execute({ inline_spec: inlineSpec }, prometheusContext)
} catch (error) {
errorMessage = error instanceof Error ? error.message : String(error)
}
// then
expect(errorMessage).toContain("team_create denied")
expect(errorMessage).toContain("prometheus")
expect(errorMessage).toContain("hard-reject")
expect(createTeamRunMock).not.toHaveBeenCalled()
})
test("team_reject_shutdown records the rejection reason", async () => {
// given
const createTool = createTeamCreateToolForTest()
@@ -8,7 +8,9 @@ import { mergeCategories } from "../../../shared/merge-categories"
import type { OpencodeClient } from "../../../tools/delegate-task/types"
import type { BackgroundManager } from "../../background-agent/manager"
import type { TmuxSessionManager } from "../../tmux-subagent/manager"
import { getAgentConfigKey } from "../../../shared/agent-display-names"
import { resolveCallerTeamLead } from "../resolve-caller-team-lead"
import { AGENT_ELIGIBILITY_REGISTRY } from "../types"
import { loadTeamSpec, normalizeTeamSpecInput } from "../team-registry/loader"
import { validateSpec } from "../team-registry/validator"
import { createTeamRun } from "../team-runtime/create"
@@ -197,6 +199,13 @@ export function createTeamCreateTool(
if (!leadSessionId) throw new Error("team_create requires leadSessionId or tool context sessionID")
const projectRoot = typeof runtimeContext.directory === "string" ? runtimeContext.directory : process.cwd()
const callerTeamLead = resolveCallerTeamLead(runtimeContext.agent)
if (callerTeamLead.displayName !== undefined) {
const callerAgentKey = getAgentConfigKey(callerTeamLead.displayName)
const callerRegistryEntry = AGENT_ELIGIBILITY_REGISTRY[callerAgentKey]
if (callerRegistryEntry?.verdict === "hard-reject") {
throw new Error(`team_create denied: caller '${callerAgentKey}' is a hard-reject agent and cannot create teams regardless of an explicit 'lead' in the spec. ${callerRegistryEntry.rejectionMessage ?? `Agent '${callerAgentKey}' is not eligible to lead a team.`}`)
}
}
const defaultCategoryName = resolveDefaultInlineCategory(executorConfig?.userCategories)
const spec = args.teamName
? await deps.loadTeamSpec(args.teamName, config, projectRoot, { callerTeamLead })