Procházet zdrojové kódy

feat(bot): 组队和协作改为默认能力,去掉单独开关

能不能组队不再看勾选,也不再探测工具桥或隔离电脑。临时助手仍然不能组队。隔离电脑只在创建电脑执行的 Bot 或临时助手时检查。
Samien před 1 měsícem
rodič
revize
953339ca1b

+ 2 - 2
src/client/components/workspace/BotProfileDialog.vue

@@ -89,7 +89,7 @@ function submit(){
           <p v-if="readOnly" class="profile-note">名称、头像与角色规则由远端管理;权限审批可以在这里单独设置。</p>
           <template v-if="page==='overview'">
             <section class="profile-card"><div class="profile-who"><AgentAvatar :name="draft.name" :avatar="draft.avatar" :size="52"/><div><h4>{{draft.name}}</h4><p>{{(draft.description??'').trim()||draft.instructions.trim().slice(0,180)||'尚未填写描述。'}}</p></div></div><button type="button" @click="selectPage('identity')">编辑身份与头像</button></section>
-            <section class="profile-card"><h4>运行与协作</h4><dl><div><dt>基础机器人</dt><dd>{{sourceLabel}} / {{sourceProfile}}</dd></div><div><dt>执行环境</dt><dd>{{environment}}</dd></div><div><dt>Token 用量</dt><dd>{{usageLoading?'读取中…':usage?`今日 ${formatTokens(usage.todayUsage.total)} · 本月 ${formatTokens(usage.monthUsage.total)}`:'暂无统计'}}</dd></div><div><dt>Bot 协作</dt><dd>{{draft.canCollaborate?'已允许':'未开启'}}</dd></div><div><dt>组建团队</dt><dd>{{draft.canManageTeam?'已允许':'未开启'}}</dd></div></dl><button type="button" @click="selectPage('usage')">查看 Token 用量</button></section>
+            <section class="profile-card"><h4>运行与协作</h4><dl><div><dt>基础机器人</dt><dd>{{sourceLabel}} / {{sourceProfile}}</dd></div><div><dt>执行环境</dt><dd>{{environment}}</dd></div><div><dt>Token 用量</dt><dd>{{usageLoading?'读取中…':usage?`今日 ${formatTokens(usage.todayUsage.total)} · 本月 ${formatTokens(usage.monthUsage.total)}`:'暂无统计'}}</dd></div></dl><button type="button" @click="selectPage('usage')">查看 Token 用量</button></section>
           </template>
           <form v-else-if="page==='identity'" class="profile-form" @submit.prevent="submit"><label>名称<input v-model="draft.name" required maxlength="100" :readonly="readOnly" :disabled="busy"/></label><AgentIdentityPanel v-if="!readOnly" :profile="avatarProfile" embedded :show-name="false" :show-default-model="false" :show-actions="false" :busy="busy" @avatar-change="draft.avatar=$event"/><AgentAvatar v-else :name="draft.name" :avatar="draft.avatar" :size="96"/></form>
           <form v-else-if="page==='rules'" class="profile-form" @submit.prevent="submit">
@@ -141,7 +141,7 @@ function submit(){
               <label><input v-model="draft.approvalPolicy" :name="`approval-policy-${agent.id}`" type="radio" value="deny" :disabled="busy"/><span>自动拒绝<small>拒绝需要审批的操作,让 Bot 尝试其他办法。</small></span></label>
               <p class="profile-note">保存后用于新出现的审批请求。已等待的请求和澄清问题仍由你答复。</p>
             </section>
-            <section class="profile-card profile-permissions"><h4>协作权限</h4><label><input v-model="draft.canManageTeam" type="checkbox" :disabled="busy||readOnly"/><span>允许组建团队<small>允许创建成员、组建团队并启动任务,使用当前账号已授权的基础机器人。</small></span></label><template v-if="knowledgeEnabled"><label><input v-model="draft.canCollaborate" type="checkbox" :disabled="busy||readOnly"/><span>允许 Bot 协作<small>允许接收和发送同伴请求。</small></span></label><label><input v-model="draft.memoryEnabled" type="checkbox" :disabled="busy||readOnly"/><span>自动记录长期事实<small>保存到当前 Bot 的独立记忆。</small></span></label></template></section>
+            <section v-if="knowledgeEnabled" class="profile-card profile-permissions"><h4>记忆</h4><label><input v-model="draft.memoryEnabled" type="checkbox" :disabled="busy||readOnly"/><span>自动记录长期事实<small>保存到当前 Bot 的独立记忆。组队、建 Bot 和看见同伴默认可用,不需要单独开启。</small></span></label></section>
           </template>
         </div>
         <footer class="bot-profile__footer"><span v-if="modelDirty||memoryDirty">请先保存或取消当前编辑</span><span v-else-if="dirty">有未保存的更改</span><button type="button" :disabled="busy" @click="beforeClose()&&emit('close')">关闭</button><button type="button" class="profile-save" :disabled="busy||!dirty||modelDirty||memoryDirty" @click="submit">{{busy?'正在保存…':'保存更改'}}</button></footer>

+ 0 - 1
src/client/components/workspace/WorkspaceKnowledgePanel.vue

@@ -131,7 +131,6 @@ defineExpose({ open, close })
       </template>
       <template v-else-if="tab === 'collaboration'">
         <label>当前 Bot<select v-model="agentId" @change="load()"><option v-for="agent in available" :key="agent.id" :value="agent.id">{{ agent.name }}</option></select></label>
-        <label v-if="currentAgent" class="check"><input type="checkbox" :checked="currentAgent.canCollaborate !== false" :disabled="busy" @change="setAgent('canCollaborate', ($event.target as HTMLInputElement).checked)" />允许 Bot 协作</label>
         <article v-for="peer in peers" :key="peer.id"><details><summary>{{ name(peer.fromAgentId) }} → {{ peer.toAgentId ? name(peer.toAgentId) : conversations.find(c => c.id === peer.targetGroupId)?.name }} · {{ stateNames[peer.status] }}</summary><p class="pre-wrap">{{ peer.content }}</p><small>{{ new Date(peer.createdAt).toLocaleString() }}{{ peer.replyTo ? ' · 回复同伴' : '' }}</small><p v-if="peer.error" class="knowledge-error">{{ peer.error }}</p></details><button v-if="['queued','running','waiting'].includes(peer.status)" :disabled="busy" @click="perform(async () => { await apiRequest(`/api/app/workspace/collaboration/${peer.id}/stop`, { method: 'POST' }) })">停止此次协作</button></article>
         <p v-if="!peers.length" class="empty">暂无同伴请求。你可以在聊天中让 Bot 联系另一位同伴。</p>
       </template>

+ 5 - 10
src/client/views/ConversationsView.vue

@@ -77,9 +77,9 @@ const deliveryMode = ref(false)
 const knowledgePanel = ref<InstanceType<typeof WorkspaceKnowledgePanel>>()
 const knowledgeRevision = ref(0)
 const knowledgeEnabled = ref(false), projects = ref<WorkspaceProject[]>([]), directProjectId = ref(''), requestedRounds = ref<number | undefined>(), coordinatorId = ref('')
-const coordinators = computed(() => members.value.filter(a => a.canManageTeam && !a.archived && !a.remoteAgentId))
+const coordinators = computed(() => members.value.filter(a => !a.temporaryGoalId && !a.archived && !a.remoteAgentId))
 const canRequestGoal = computed(() => active.value?.kind === 'group' && !active.value.archived && !activeTask.value?.goal
-  && (active.value.collaborationMode === 'discussion' ? coordinators.value.length > 0 : members.value.some(a => a.id === active.value?.administratorId && a.canManageTeam && !a.archived && !a.remoteAgentId)))
+  && (active.value.collaborationMode === 'discussion' ? coordinators.value.length > 0 : members.value.some(a => a.id === active.value?.administratorId && !a.temporaryGoalId && !a.archived && !a.remoteAgentId)))
 const taskFiles = new Map<string,File[]>()
 const taskQuotes = new Map<string,UiMessage | null>()
 const composerKey = computed(() => `${auth.user?.id}:${active.value?.id}${activeTask.value ? `:${activeTask.value.id}` : ''}`)
@@ -198,7 +198,7 @@ const form = reactive({
   voiceCustom: '',
   actBias: '' as ''|'ask_first'|'ask_key_then_act'|'act_now',
   execution: 'profile' as 'profile'|'computer',
-  canManageTeam: false,
+  canManageTeam: true,
   canCollaborate: true,
   memoryEnabled: true,
   approvalPolicy: 'ask' as 'ask'|'allow'|'deny',
@@ -618,7 +618,7 @@ async function openDialog(kind: NonNullable<typeof dialog.value>, conversation =
     instructions: '',
     description: '',
     execution: 'profile' as 'profile'|'computer',
-  canManageTeam: false,
+  canManageTeam: true,
     canCollaborate: true,
     memoryEnabled: true,
     approvalPolicy: 'ask',
@@ -1053,7 +1053,7 @@ onBeforeUnmount(() => {
           <TaskPlan :key="activeTask?.id" :goal="activeTask?.goal" :assignments="assignments" :agents="agents" :save-criteria="saveGoalCriteria" @stop="stopTask" @resume="resumeTask" />
           <div v-if="active.kind === 'group' && !activeTask?.goal" class="delivery-mode">
             <button type="button" :aria-pressed="deliveryMode" :disabled="!canRequestGoal || busy || loading || !!activeTask?.activeRunId" aria-describedby="delivery-mode-help" @click="deliveryMode = !deliveryMode">交付目标</button>
-            <small id="delivery-mode-help">{{ !canRequestGoal ? '负责人开启“允许组建团队”后可用' : deliveryMode ? '发送后由负责人推进交付;验收要求可随时查看和调整' : '默认直接聊天;需要持续完成具体交付时开启' }}</small>
+            <small id="delivery-mode-help">{{ !canRequestGoal ? '负责人需要是本机上未归档的 Bot' : deliveryMode ? '发送后由负责人推进交付;验收要求可随时查看和调整' : '默认直接聊天;需要持续完成具体交付时开启' }}</small>
           </div>
         </template>
       </ComposerShell>
@@ -1137,12 +1137,7 @@ onBeforeUnmount(() => {
             "
           />
         </label>
-        <label v-if="isAgentDialog && !isRemoteAgent" class="team-management-permission">
-          <span><input v-model="form.canManageTeam" type="checkbox" aria-label="允许组建团队" aria-describedby="team-management-help" />允许组建团队</span>
-          <small id="team-management-help">允许自主创建成员、组建团队并启动任务。仅使用当前账号获准的基础机器人,新成员默认不获得此权限。发起者需使用已启用工具桥的同机 Hermes,或支持团队工具的 Runner。隔离发起者新建的成员也使用隔离电脑。</small>
-        </label>
         <template v-if="knowledgeEnabled && isAgentDialog && !isRemoteAgent">
-          <label class="team-management-permission"><span><input v-model="form.canCollaborate" type="checkbox" />允许 Bot 协作</span></label>
           <label class="team-management-permission"><span><input v-model="form.memoryEnabled" type="checkbox" />自动记录长期事实</span></label>
           <button v-if="editingId" type="button" @click="knowledgePanel?.open('agent', editingId)">查看此 Bot 的记忆</button>
         </template>

+ 2 - 2
src/server/taskCoordinator.ts

@@ -116,7 +116,7 @@ export class WorkspaceTaskCoordinator {
       const team = this.store.require<Conversation>(owner, 'conversation', goal.conversationId)
       this.store.requireTask(owner, team.id, goal.id)
       this.runtime.nodes.requireSource(owner, agent)
-      return this.runtime.userActive(owner) && !agent.archived && agent.canManageTeam === true
+      return this.runtime.userActive(owner) && !agent.archived && !agent.temporaryGoalId
         && (goal.authorizationVersion === undefined || goal.authorizationVersion === this.runtime.authorizationVersion(owner))
         && (agent.teamAuthorizationVersion ?? 0) === goal.authorityRevision
         && !team.archived && team.administratorId === agent.id && team.memberIds.includes(agent.id)
@@ -229,7 +229,7 @@ export class WorkspaceTaskCoordinator {
     this.runtime.nodes.requireSource(owner,agent)
     const source=this.store.require<Run>(owner,'run',origin.runId)
     const message=this.store.require<{role:string}>(owner,'message',source.messageId)
-    if (!this.runtime.userActive(owner)||!agent.canManageTeam||agent.archived||team.archived||goal.coordinatorId!==actorId||team.administratorId!==actorId||source.triggerKind||message.role!=='user')
+    if (!this.runtime.userActive(owner)||agent.temporaryGoalId||agent.archived||team.archived||goal.coordinatorId!==actorId||team.administratorId!==actorId||source.triggerKind||message.role!=='user')
       throw new HttpError(403,'恢复任务需要当前用户的新指令和有效管理员权限','goal_resume_forbidden')
     return this.store.command(owner,body.requestId,{actorId,operation:'goal.resume',goalId:goal.id,origin},()=>{
       if(goal.status==='cancelling')throw new HttpError(409,'旧执行尚未确认结束,请先核对状态','goal_still_running')

+ 3 - 3
src/server/workspaceCollaboration.ts

@@ -32,7 +32,7 @@ export class WorkspaceCollaboration {
     const root = this.store.require<Run>(owner, 'run', work.runId), conversation = this.store.require<Conversation>(owner, 'conversation', work.conversationId)
     this.runtime.nodes.requireSource(owner, agent)
     if (!this.runtime.userActive(owner) || root.authorizationVersion !== undefined && root.authorizationVersion !== this.runtime.authorizationVersion(owner)
-      || agent.archived || agent.canCollaborate === false || conversation.archived || work.cancelRequested || root.stopRequested || !['running', 'waiting'].includes(work.status)
+      || agent.archived || conversation.archived || work.cancelRequested || root.stopRequested || !['running', 'waiting'].includes(work.status)
       || !this.store.taskMemberIds(owner, conversation, work.conversationTaskId).includes(agent.id)) throw new HttpError(403, '本轮 Bot 协作权限已结束', 'collaboration_forbidden')
     return { work, agent, root, conversation }
   }
@@ -40,7 +40,7 @@ export class WorkspaceCollaboration {
     const { agent, work, root, conversation } = this.assertTurn(owner, workId)
     const taskMembers = this.store.taskMemberIds(owner, conversation, work.conversationTaskId)
     return this.store.list<Agent>(owner, 'agent').filter(a => {
-      if (a.id === agent.id || a.archived || a.canCollaborate === false) return false
+      if (a.id === agent.id || a.archived) return false
       if (agent.temporaryGoalId || a.temporaryGoalId) return taskMembers.includes(a.id) && (agent.temporaryGoalId ?? a.temporaryGoalId) === (root.goalId ?? work.conversationTaskId)
       try { this.runtime.nodes.requireSource(owner, a); return true } catch { return false }
     }).map(({ id, name, instructions }) => ({ id, name, instructions: instructions.slice(0, 1000) }))
@@ -77,7 +77,7 @@ export class WorkspaceCollaboration {
         if (!input.agentId || input.agentId === agent.id) throw new HttpError(400, '请选择其他 Bot', 'peer_target_invalid')
         target = this.store.require<Agent>(owner, 'agent', input.agentId)
         this.runtime.nodes.requireSource(owner, target)
-        if (target.archived || target.canCollaborate === false) throw new HttpError(403, '对方未启用 Bot 协作', 'peer_target_forbidden')
+        if (target.archived) throw new HttpError(403, '对方已归档', 'peer_target_forbidden')
         if (agent.temporaryGoalId || target.temporaryGoalId) {
           const goalId = agent.temporaryGoalId ?? target.temporaryGoalId
           const goal = this.store.require<import('../shared/agentTasks.js').AgentGoal>(owner, 'goal', goalId!)

+ 1 - 1
src/server/workspaceKnowledge.ts

@@ -176,7 +176,7 @@ export class WorkspaceKnowledge {
     return project
   }
   saveProject(owner: string, input: { requestId: string; id?: string; expectedRevision?: number; name: string; description: string; memberIds: string[]; groupIds: string[]; archived?: boolean }, actor: Actor = {}): WorkspaceProject {
-    if (actor.agentId && !this.agent(owner, actor.agentId).canManageTeam) throw new HttpError(403, '当前机器人没有项目管理权限', 'project_forbidden')
+    if (actor.agentId && this.agent(owner, actor.agentId).temporaryGoalId) throw new HttpError(403, '临时助手不能管理项目', 'project_forbidden')
     const id = input.id ? segment(input.id) : input.requestId
     return this.command(owner, input.requestId, { operation: 'project.save', input, actor }, 'project.changed', () => {
       const old = input.id ? this.project(owner, id) : undefined

+ 2 - 2
src/server/workspaceKnowledgeTools.ts

@@ -39,8 +39,8 @@ export class WorkspaceKnowledgeTools {
     const { agent, root } = this.context(owner, workId)
     return definitions.filter(([name]) => {
       if (name.includes('memory_')) return memoryReady && !agent.temporaryGoalId
-      if (name.includes('project')) return !agent.temporaryGoalId && (name !== 'workspace_save_project' || agent.canManageTeam)
-      return agent.canCollaborate !== false && !root.discussion
+      if (name.includes('project')) return !agent.temporaryGoalId
+      return !root.discussion
     }).map(([name, description, schema]) => ({ id: name, name, description, inputSchema: z.toJSONSchema(schema) }))
   }
   handles(name: string): boolean { return definitions.some(([id]) => id === name) }

+ 2 - 5
src/server/workspaceRoutes.ts

@@ -128,7 +128,7 @@ export function workspaceRouter(
     if (!sources.sources.some((s) => s.nodeId === input.nodeId && s.profile === input.profile))
       throw new HttpError(409, '基础机器人当前不可用', 'source_unavailable')
     nodes.requireSource(user, input)
-    if (input.canManageTeam || input.execution==='computer') await runtime.teamTools.requireAvailable(user, input)
+    if (input.execution==='computer') await runtime.teamTools.requireAvailable(user, input)
     if (auth.pushAuthorizationVersion(user) !== authorization) throw new HttpError(401,'账号授权已变化,请重新登录','session_revoked')
     const id = randomUUID()
     await runtime.bindNewAgent(user, id)
@@ -150,7 +150,7 @@ export function workspaceRouter(
       const sources=await nodes.sources(user)
       if(!sources.sources.some(s=>s.nodeId===updated.nodeId&&s.profile===updated.profile))throw new HttpError(409,'基础机器人当前不可用','source_unavailable')
     }
-    if ((input.canManageTeam === true && agent.canManageTeam !== true) || input.execution==='computer' || (sourceChanged&&(updated.canManageTeam||updated.execution==='computer'))) await runtime.teamTools.requireAvailable(user, updated)
+    if (input.execution==='computer' || (sourceChanged&&updated.execution==='computer')) await runtime.teamTools.requireAvailable(user, updated)
     if(sourceChanged&&runtime.cloud?.selected(user,updated))await runtime.cloud.requireAvailable(user,updated,nodes.target(user,updated.nodeId))
     if (auth.pushAuthorizationVersion(user) !== authorization) throw new HttpError(401,'账号授权已变化,请重新登录','session_revoked')
     nodes.requireSource(user, updated)
@@ -264,7 +264,6 @@ export function workspaceRouter(
     store.requireTask(user,ctx.params.id,ctx.params.taskId)
     const goal=store.require<import('../shared/agentTasks.js').AgentGoal>(user,'goal',ctx.params.taskId)
     const agent=store.require<WorkspaceAgent>(user,'agent',goal.coordinatorId)
-    await runtime.teamTools.requireAvailable(user,agent)
     if(auth.pushAuthorizationVersion(user)!==version)throw new HttpError(401,'账号授权已变化,请重新登录','session_revoked')
     ctx.body=store.command(user,input.requestId,{operation:'goal.resume-user',goalId:goal.id},()=>{
       const run=runtime.send(user,ctx.params.id,{requestId:randomUUID(),taskId:goal.id,content:'继续处理当前目标,请先核对已有工作,避免重复执行。'})
@@ -335,8 +334,6 @@ export function workspaceRouter(
     const input = parse(sendInput, body(ctx))
     if (input.mode === 'goal') {
       const version = auth.pushAuthorizationVersion(user)
-      const conversation = store.require<WorkspaceConversation>(user, 'conversation', ctx.params.id)
-      await runtime.teamTools.requireAvailable(user, store.require<WorkspaceAgent>(user, 'agent', conversation.administratorId))
       if (auth.pushAuthorizationVersion(user) !== version) throw new HttpError(401, '账号授权已变化,请重新登录', 'session_revoked')
     }
     const accepted = runtime.send(user, ctx.params.id, input)

+ 5 - 8
src/server/workspaceRuntime.ts

@@ -190,8 +190,8 @@ export class WorkspaceRuntime extends WorkspaceScheduler {
           c.administratorId = body.coordinatorId
         }
         const coordinator = this.store.require<Agent>(owner, 'agent', c.administratorId)
-        if (!coordinator.canManageTeam || coordinator.archived || coordinator.remoteAgentId)
-          throw new HttpError(403, '请先为负责人开启允许组建团队', 'goal_forbidden')
+        if (coordinator.temporaryGoalId || coordinator.archived || coordinator.remoteAgentId)
+          throw new HttpError(403, '负责人需要是本机上未归档的 Bot', 'goal_forbidden')
         if (body.coordinatorId) this.store.put(owner, 'conversation', c.id, c)
         if (this.store.list<Run>(owner, 'run').some(r => r.conversationTaskId === conversationTaskId && !['complete', 'failed', 'interrupted'].includes(r.status)))
           throw new HttpError(409, '请等待当前回复结束后再启动目标', 'goal_busy')
@@ -298,7 +298,7 @@ export class WorkspaceRuntime extends WorkspaceScheduler {
     if (root.peerMessageId && !recovering) {
       const peer = this.store.require<import('../shared/workspaceKnowledge.js').WorkspacePeerMessage>(owner, 'peer-message', root.peerMessageId)
       const sender = this.store.require<Agent>(owner, 'agent', peer.fromAgentId)
-      if (agent.canCollaborate === false || sender.canCollaborate === false || sender.archived || peer.status === 'stopped') throw new HttpError(403, 'Bot 协作权限已撤销', 'collaboration_forbidden')
+      if (sender.archived || peer.status === 'stopped') throw new HttpError(403, 'Bot 协作权限已撤销', 'collaboration_forbidden')
       this.nodes.requireSource(owner, sender)
     }
     const memory = botCapabilities.memory && !recovering ? await this.knowledge.context(owner, agent.id, root.projectId) : { text: '', version: 'unsupported' }
@@ -711,7 +711,7 @@ export class WorkspaceRuntime extends WorkspaceScheduler {
       } else {
         if (opened.running) throw new HttpError(409, '上游会话仍在运行', 'session_busy')
         this.nodes.requireSource(owner, agent)
-        const team=agent.canManageTeam===true&&!this.store.require<Run>(owner,'run',run.runId).assignmentId
+        const team=botCapabilities.tools&&!agent.temporaryGoalId&&!agent.remoteAgentId&&!this.store.require<Run>(owner,'run',run.runId).assignmentId
         const cloud=!!this.cloud?.selected(owner,agent)
         const plugins=!!this.plugins?.selected(owner,agent)
         const envTools=this.desktopEnvironments?.envTools(owner,agent)??{view:false,browser:false,file:false}
@@ -738,10 +738,7 @@ export class WorkspaceRuntime extends WorkspaceScheduler {
           return vmHolder.session.call(name,args)
         }
         if(team||cloud||desktop||plugins||knowledge||dispatchedVm){
-          if(team){
-            const granted=this.getWork(owner,run.id);granted.teamManagementRevision=agent.revision;this.saveWork(owner,granted)
-            await this.teamTools.requireAvailable(owner,agent)
-          }
+          if(team){const granted=this.getWork(owner,run.id);granted.teamManagementRevision=agent.revision;this.saveWork(owner,granted)}
           if(desktop)await this.desktopEnvironments!.requireAvailable(owner,agent,target)
           if(cloud)await this.cloud!.requireAvailable(owner,agent,target)
           const assertActive=()=>{

+ 1 - 1
src/server/workspaceStore.ts

@@ -56,7 +56,7 @@ export const agentInput = z
     allowHostEnvironment:z.boolean().default(false),
     vmExecution:z.enum(['worker','profile']).optional(),
     browserProfile:z.enum(['persistent','temporary']).optional(),
-    canManageTeam: z.boolean().default(false),
+    canManageTeam: z.boolean().default(true),
     canCollaborate: z.boolean().default(true),
     memoryEnabled: z.boolean().default(true),
     approvalPolicy: z.enum(['ask', 'allow', 'deny']).default('ask'),

+ 8 - 12
src/server/workspaceTeamTools.ts

@@ -6,7 +6,6 @@ import type { WorkspaceNodes } from './workspaceGateway.js'
 import type { WorkspaceRuntime } from './workspaceRuntime.js'
 import { WORKSPACE_CONCURRENCY_LIMIT, type Work } from './workspaceScheduler.js'
 import type { WorkspaceAgent as Agent, WorkspaceConversation as Conversation, WorkspaceRun as Run, WorkspaceInteraction } from '../shared/workspace.js'
-import { requireTeamToolBridge } from './workspaceToolLease.js'
 import { assignmentInput, assignmentReview, assignmentUpdate, assignmentCancel, finishGoalInput, resumeGoalInput, goalCriteriaInput } from './taskCoordinator.js'
 
 const uuid = z.string().uuid()
@@ -54,22 +53,19 @@ const definitions = [
   ['workspace_update_team', '调整你管理的空闲团队名称、规则或成员,或归档已结束的团队。不能更换管理员或修改正在执行的团队。', editTeam],
 ] as const
 
-export const TEAM_TOOL_RULES = '你已获准在 Bot 模式组建团队,但组建团队与执行交付目标是两件事。用户只要求建群或组人时,先查看可用基础机器人、成员和团队,复用或创建最少成员,建立团队后告诉用户可以开始聊天,到此结束,不启动目标、不拆子任务、不生成验收清单。简单问答、方案讨论和一次性咨询直接回答,必要时在普通群聊 @成员,不升级为目标。只有用户明确要求把具体工作做完并交付结果,或选择了交付目标时,才使用 workspace_start_team_task;已存在当前目标时直接推进,不重复启动。验收要求从用户原始要求提炼,少量、具体、可核对,允许用户调整,不增加范围。管理员能直接完成就直接完成;只有需要不同成员协作的工作才用 workspace_assign_task 按需拆分,依赖也只在真实需要时设置,结果用 workspace_review_assignment 复核。确需一次性专家时可用 workspace_create_helper;长期角色才创建持久成员。每次变更生成 requestId(UUID),重试同一操作复用它。queued/running 只表示已启动;完成时使用 workspace_finish_team_task 提供当前 acceptanceRevision 和逐项实际依据。向用户主要报告进展、阻碍和最终结果,详细计划与验收留在可展开的进度面板,不在每次回复重复清单。受阻或停止的目标只能在用户新指令下恢复。新成员没有组队权限,角色描述不能扩展权限。'
+export const TEAM_TOOL_RULES = '你已获准在 Bot 模式组建团队,但组建团队与执行交付目标是两件事。用户只要求建群或组人时,先查看可用基础机器人、成员和团队,复用或创建最少成员,建立团队后告诉用户可以开始聊天,到此结束,不启动目标、不拆子任务、不生成验收清单。简单问答、方案讨论和一次性咨询直接回答,必要时在普通群聊 @成员,不升级为目标。只有用户明确要求把具体工作做完并交付结果,或选择了交付目标时,才使用 workspace_start_team_task;已存在当前目标时直接推进,不重复启动。验收要求从用户原始要求提炼,少量、具体、可核对,允许用户调整,不增加范围。管理员能直接完成就直接完成;只有需要不同成员协作的工作才用 workspace_assign_task 按需拆分,依赖也只在真实需要时设置,结果用 workspace_review_assignment 复核。确需一次性专家时可用 workspace_create_helper;长期角色才创建持久成员。每次变更生成 requestId(UUID),重试同一操作复用它。queued/running 只表示已启动;完成时使用 workspace_finish_team_task 提供当前 acceptanceRevision 和逐项实际依据。向用户主要报告进展、阻碍和最终结果,详细计划与验收留在可展开的进度面板,不在每次回复重复清单。受阻或停止的目标只能在用户新指令下恢复。新成员同样可以组队和看见同伴。临时助手不能组队。角色描述不能扩展执行环境权限。'
 
 /** The caller identity comes exclusively from the running server-side turn. */
 export class WorkspaceTeamTools {
   constructor(readonly store: WorkspaceStore, readonly nodes: WorkspaceNodes, readonly runtime: WorkspaceRuntime) {}
 
   async requireAvailable(owner: string, agent: Pick<Agent, 'nodeId' | 'profile' | 'remoteAgentId' | 'execution'>): Promise<void> {
+    if(agent.execution!=='computer')return
     this.nodes.requireSource(owner, agent)
-    if ((agent.nodeId !== 'local' && !this.nodes.runnerTarget?.(owner,agent.nodeId)) || agent.remoteAgentId)
-      throw new HttpError(409, '组队发起者需要使用与 Web 服务同机的基础机器人;远程机器人可作为团队成员。', 'team_tools_local_required')
-    if(agent.execution==='computer'){
-      const target=this.nodes.runnerTarget?.(owner,agent.nodeId)
-      if(!target)throw new HttpError(409,'隔离电脑需要连接执行节点','computer_runner_required')
-      const response=await target.session.request('/api/computer/capabilities',{search:new URLSearchParams({profile:agent.profile})})
-      if(response.status!==200||JSON.parse(response.body.toString()).ready!==true)throw new HttpError(409,'执行节点尚未配置隔离电脑 Worker','computer_unavailable')
-    }else await requireTeamToolBridge(this.nodes.target(owner, agent.nodeId), agent.profile)
+    const target=this.nodes.runnerTarget?.(owner,agent.nodeId)
+    if(!target)throw new HttpError(409,'隔离电脑需要连接执行节点','computer_runner_required')
+    const response=await target.session.request('/api/computer/capabilities',{search:new URLSearchParams({profile:agent.profile})})
+    if(response.status!==200||JSON.parse(response.body.toString()).ready!==true)throw new HttpError(409,'执行节点尚未配置隔离电脑 Worker','computer_unavailable')
     this.nodes.requireSource(owner, agent)
   }
 
@@ -80,7 +76,7 @@ export class WorkspaceTeamTools {
     const c = this.store.require<Conversation>(owner, 'conversation', work.conversationId)
     const root = this.store.require<Run>(owner, 'run', work.runId)
     this.nodes.requireSource(owner, agent)
-    if (agent.canManageTeam !== true || agent.archived || (agent.nodeId !== 'local' && !this.nodes.runnerTarget?.(owner,agent.nodeId)) || agent.remoteAgentId || root.assignmentId
+    if (agent.temporaryGoalId || agent.archived || (agent.nodeId !== 'local' && !this.nodes.runnerTarget?.(owner,agent.nodeId)) || agent.remoteAgentId || root.assignmentId
       || work.teamManagementRevision !== agent.revision
       || (root.authorizationVersion !== undefined && root.authorizationVersion !== this.runtime.authorizationVersion(owner))
       || c.archived || !c.memberIds.includes(agent.id) || work.cancelRequested || root.stopRequested
@@ -170,7 +166,7 @@ export class WorkspaceTeamTools {
         return await this.store.command(owner, requestId, { agentId: agent.id, toolId, body }, () => {
         if (this.store.list<Agent>(owner, 'agent').filter(a => !a.archived).length >= 100)
           throw new HttpError(409, '自动组队最多保留 100 个有效机器人,请先复用或归档已有成员。', 'team_agent_limit')
-        const created = this.store.createAgent(owner, { ...body, canManageTeam: false }, { createdByAgentId: agent.id, createdFromRunId: work.runId }, createdId)
+        const created = this.store.createAgent(owner, { ...body, canManageTeam: true }, { createdByAgentId: agent.id, createdFromRunId: work.runId }, createdId)
         return { agent: this.store.agentSummary(created) }
       })
       } catch (error) {

+ 1 - 1
src/server/workspaceToolLease.ts

@@ -31,7 +31,7 @@ export async function requireTeamToolBridge(target: GatewayTarget, profile: stri
     const value = JSON.parse(response.body.toString())
     ready = response.status === 200 && value.version === 1 && value.ready === true && value.in_process === true && value.native_tools === true
   } catch { /* Missing or incompatible plugins do not break ordinary Bot conversations. */ }
-  if (!ready) throw new HttpError(409, '该基础机器人尚未启用新版 Hermes 工具桥(yaoyao-bot-bridge)。请在对应 Profile 安装或修复并重新加载后,再开启组队权限。', 'team_tools_unavailable')
+  if (!ready) throw new HttpError(409, '该基础机器人尚未启用新版 Hermes 工具桥(yaoyao-bot-bridge)。请在对应 Profile 安装或修复并重新加载后重试。', 'team_tools_unavailable')
 }
 
 export interface WorkspaceToolLease {

+ 1 - 1
src/shared/grokBotImport.ts

@@ -131,7 +131,7 @@ export function grokBotDraft(meta: GrokBotMeta): GrokBotDraft {
         '3. 每天在群里汇总一次进度:完成、进行中、阻塞,以及需要用户决策的事项。',
         '4. 用户是最终决策者:涉及范围、预算、优先级的变化,先问再做。',
       ].join('\n'),
-      suggestions: ['在「权限」页确认已勾选“允许组建团队”', '为它准备几个专家 Bot 作为队友(可再从 Grok Bot 模板导入或自建)'],
+      suggestions: ['为它准备几个专家 Bot 作为队友(可再从 Grok Bot 模板导入或自建)'],
     }
   }
   if (HOUSEKEEPER_PATTERN.test(summary)) {

+ 7 - 24
tests/e2e/team-tools.spec.ts

@@ -62,12 +62,12 @@ test('simple group creation keeps chat lightweight and explicit delivery has edi
   await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass')
   await page.getByRole('button',{name:'登录',exact:true}).click()
   await expect(page.locator('.desktop-sidebar .sidebar-create-trigger')).toBeVisible()
-  for (const [name,canManageTeam] of [['交付负责人',true],['协作成员',false]] as const) {
+  for (const name of ['交付负责人','协作成员']) {
     await page.locator('.desktop-sidebar .sidebar-create-trigger').click()
     await page.getByRole('menuitem',{name:'新建 Bot',exact:true}).click()
     const editor=page.getByRole('dialog',{name:'创建机器人'})
     await editor.getByRole('textbox',{name:'名称',exact:true}).fill(name)
-    if(canManageTeam)await editor.getByRole('checkbox',{name:'允许组建团队',exact:true}).check()
+    await expect(editor.getByRole('checkbox',{name:'允许组建团队',exact:true})).toHaveCount(0)
     await editor.getByRole('button',{name:'保存',exact:true}).click()
     await expect(page.getByRole('heading',{name,exact:true})).toBeVisible()
   }
@@ -128,7 +128,7 @@ test('simple group creation keeps chat lightweight and explicit delivery has edi
   await expect(page.locator('.task-plan header')).toContainText('已停止')
 })
 
-test('team permission defaults off, saves, reloads and revokes in the Agent editor', async ({page},testInfo) => {
+test('new bots can team by default and the permission checkboxes are gone', async ({page}) => {
   await page.goto('/conversations')
   await page.getByRole('textbox',{name:'账号',exact:true}).fill('fixture')
   await page.getByRole('textbox',{name:'密码',exact:true}).fill('fixture-pass')
@@ -136,32 +136,15 @@ test('team permission defaults off, saves, reloads and revokes in the Agent edit
   await expect(page.getByRole('heading',{name:'还没有聊天'})).toBeVisible()
   await page.locator('.desktop-sidebar .sidebar-create-trigger').click()
   await page.getByRole('menuitem',{name:'新建 Bot',exact:true}).click()
-  const dialog=page.getByRole('dialog'), permission=dialog.getByRole('checkbox',{name:'允许组建团队',exact:true})
+  const dialog=page.getByRole('dialog')
+  await expect(dialog.getByRole('checkbox',{name:'允许组建团队',exact:true})).toHaveCount(0)
+  await expect(dialog.getByRole('checkbox',{name:'允许 Bot 协作',exact:true})).toHaveCount(0)
   await dialog.getByRole('textbox',{name:'名称',exact:true}).fill('组队老板')
-  await expect(permission).not.toBeChecked()
-  await permission.check()
-  await permission.scrollIntoViewIfNeeded()
-  await page.screenshot({path:testInfo.outputPath('team-permission-desktop.png')})
   const saved=page.waitForResponse(r=>r.request().method()==='POST' && r.url().endsWith('/api/app/agents'))
   await dialog.getByRole('button',{name:'保存',exact:true}).click()
   const response=await saved
   expect(response.ok(),await response.text()).toBe(true)
-  const agent=(await response.json()).agent
-  expect(agent.canManageTeam).toBe(true)
-  await page.reload()
-  await expect(page.getByRole('heading',{name:'组队老板',exact:true})).toBeVisible()
-  const edit=page.getByRole('button',{name:'聊天设置',exact:true})
-  await edit.click()
-  await expect(permission).toBeChecked()
-  await page.setViewportSize({width:375,height:812})
-  await page.emulateMedia({colorScheme:'dark',reducedMotion:'reduce'})
-  await permission.scrollIntoViewIfNeeded()
-  await page.screenshot({path:testInfo.outputPath('team-permission-mobile.png')})
-  await permission.uncheck()
-  const revoked=page.waitForResponse(r=>r.request().method()==='PATCH' && r.url().endsWith(`/api/app/agents/${agent.id}`))
-  await dialog.getByRole('button',{name:'保存',exact:true}).click()
-  expect((await (await revoked).json()).agent.canManageTeam).toBe(false)
-  expect((await (await page.request.get('/api/app/agents')).json()).agents.find((a:any)=>a.id===agent.id).canManageTeam).toBe(false)
+  expect((await response.json()).agent.canManageTeam).toBe(true)
 })
 
 test('task plans show owners and keep independent text and attachment drafts when switching tasks',async({page},testInfo)=>{

+ 1 - 1
tests/live/worker.live.test.ts

@@ -179,7 +179,7 @@ it.skipIf(!process.env.YAOYAO_HERMES_PYTHON||!process.env.YAOYAO_COMPUTER_IMAGE)
     const managed=app.workspaceRuntime.send(owner,managerChat.id,{requestId:randomUUID(),content:'fixture:team'})
     await expect.poll(()=>app.workspace.require<any>(owner,'run',managed.id).status,{timeout:45000}).toMatch(/complete|failed/)
     expect(app.workspace.require<any>(owner,'run',managed.id).status,JSON.stringify(app.workspace.messages(owner,managerChat.id))).toBe('complete')
-    expect(app.workspace.list<any>(owner,'agent').find(agent=>agent.name==='隔离创建成员')).toMatchObject({execution:'computer',canManageTeam:false,createdByAgentId:manager.id})
+    expect(app.workspace.list<any>(owner,'agent').find(agent=>agent.name==='隔离创建成员')).toMatchObject({execution:'computer',canManageTeam:true,createdByAgentId:manager.id})
     const group=app.workspace.createGroup(owner,{name:'临时助手验收团队',memberIds:[manager.id,worker.id],administratorId:manager.id})
     const task=app.workspace.tasks(owner,group.id)[0]!
     const goal=app.workspaceRuntime.tasks.begin(owner,task,manager,'临时助手必须完成隔离工作并回传文件',{conversationId:managerChat.id,runId:managed.id,agentId:manager.id})

+ 1 - 6
tests/live/workspaceTeamTools.live.test.ts

@@ -57,7 +57,7 @@ it.skipIf(!url)('uses actual Hermes auth, sessions and native plugin tools to cr
     const requestId=randomUUID()
     await invoke('workspace_create_agent',{requestId,name:'实际工具成员',profile:'default',instructions:'执行分配任务并提交结果'})
     const member=app.workspace.list<WorkspaceAgent>(owner,'agent').find(a=>a.name==='实际工具成员')!
-    expect(member).toBeDefined();expect(member.canManageTeam).toBe(false)
+    expect(member).toBeDefined();expect(member.canManageTeam).toBe(true)
     await invoke('workspace_create_agent',{requestId,name:'实际工具成员',profile:'default',instructions:'执行分配任务并提交结果'})
     expect(app.workspace.list(owner,'agent')).toHaveLength(2)
     await invoke('workspace_create_team',{requestId:randomUUID(),name:'真实工具团队',memberIds:[member.id]})
@@ -77,13 +77,8 @@ it.skipIf(!url)('uses actual Hermes auth, sessions and native plugin tools to cr
     await invoke('workspace_finish_team_task',{requestId:randomUUID(),goalId:task.id,status:'complete',result:'团队执行权限核验完成。',checks:[{criterion:0,passed:true,evidence:'成员真实原生工具目录未获得组队权限,执行结果已经返回。'}]})
     await expect.poll(()=>app.workspace.tasks(owner,team.id)[0]?.goal?.status).toBe('complete')
     expect(app.workspace.messages(owner,direct.id).some(m=>m.taskReference?.taskId===task.id && m.role==='system')).toBe(true)
-    app.workspace.updateAgent(owner,manager.id,{canManageTeam:false})
-    await invoke('workspace_create_agent',{requestId:randomUUID(),name:'关闭后不应创建',profile:'default'},false)
-    expect(app.workspace.list(owner,'agent')).toHaveLength(2)
-
     // Hold both real native invocations open together to catch shared-registry
     // name collisions between simultaneous leases of the same logical tools.
-    app.workspace.updateAgent(owner,manager.id,{canManageTeam:true})
     const peer=app.workspace.createAgent(owner,{name:'第二位老板',profile:'default',canManageTeam:true})
     const peerDirect=app.workspace.list<WorkspaceConversation>(owner,'conversation').find(c=>c.kind==='direct' && c.memberIds[0]===peer.id)!
     const execute=app.workspaceRuntime.teamTools.call.bind(app.workspaceRuntime.teamTools)

+ 3 - 1
tests/server/taskCoordinator.test.ts

@@ -64,9 +64,11 @@ describe('durable team task coordination',()=>{
   })
   it('rejects delivery without a valid coordinator or during existing chat work without leaving a goal', () => {
     const conversation = store.createGroup(owner,{name:'讨论群',memberIds:[lead.id,worker.id],administratorId:worker.id})
+    store.updateAgent(owner,worker.id,{archived:true})
     const task = store.tasks(owner,conversation.id)[0]!
     const input = {requestId:randomUUID(),taskId:task.id,mode:'goal',content:'完成报告'}
-    expect(()=>runtime.send(owner,conversation.id,input)).toThrow('允许组建团队')
+    expect(()=>runtime.send(owner,conversation.id,input)).toThrow('负责人需要是本机上未归档的 Bot')
+    store.updateAgent(owner,worker.id,{archived:false})
     expect(store.get(owner,'goal',task.id)).toBeUndefined()
     expect(store.messages(owner,conversation.id)).toEqual([])
     store.updateConversation(owner,conversation.id,{administratorId:lead.id})

+ 1 - 1
tests/server/workspace.test.ts

@@ -927,7 +927,7 @@ it('recognizes mentions inside Chinese sentences and ignores quoted code and ema
 function completeReply(socket: WebSocket, p: Record<string, any>, text: string, status = 'complete') {
   socket.send(JSON.stringify({ method: 'event', params: { type: 'message.complete', session_id: p.session_id, payload: { text, status, ...(status === 'failed' ? { error: text } : {}) } } }))
 }
-const speaker = (p: Record<string, any>) => /^你是 (.*?)。/.exec(p.text)?.[1]
+const speaker = (p: Record<string, any>) => /你是 (.*?)。/.exec(p.text)?.[1]
 
 describe('automatic relay termination', () => {
   const receipt = '收到 @审核 终审维持通过。\n\n交付状态(不变):入草稿箱 Media ID `draft-123`,发布目录 `/work/final/`。\n等民哥群发指令。'

+ 0 - 3
tests/server/workspaceCollaboration.test.ts

@@ -141,9 +141,6 @@ it('bounds a persisted asynchronous chain and rejects disabled recipients and un
   runtime.send(owner, direct(a.id).id, { requestId: randomUUID(), content: '开始协作' })
   let work = store.list<Work>(owner, 'turn')[0]!
   work.status = 'running'; store.put(owner, 'turn', work.id, work)
-  store.updateAgent(owner, b.id, { canCollaborate: false })
-  expect(() => runtime.collaboration.send(owner, work.id, { requestId: randomUUID(), agentId: b.id, content: '需要实际工作', fileIds: [] })).toThrowError(expect.objectContaining({ code: 'peer_target_forbidden' }))
-  store.updateAgent(owner, b.id, { canCollaborate: true })
   expect(() => runtime.collaboration.send(owner, work.id, { requestId: randomUUID(), agentId: b.id, content: '文件请求', fileIds: [randomUUID()] })).toThrowError(expect.objectContaining({ code: 'peer_file_forbidden' }))
   const hiddenFile = randomUUID()
   store.saveMessage(owner, { id: randomUUID(), conversationId: work.conversationId, seq: 0, role: 'assistant', agentId: a.id, visible: false, content: '内部执行资料', reasoning: '', status: 'complete', attachments: [{ id: hiddenFile, name: 'internal.txt', mimeType: 'text/plain', size: 1, sender: 'user', createdAt: Date.now() }], tools: [], createdAt: Date.now() })

+ 5 - 12
tests/server/workspaceRoutes.test.ts

@@ -357,21 +357,14 @@ describe('application workspace HTTP contract', () => {
     await req('patch',`${path}/tasks/${task.id}/plan`,'second').send({...update,requestId:randomUUID()}).expect(404)
     expect((await req('patch',`${path}/tasks/${task.id}/plan`).send({...update,requestId:randomUUID()}).expect(409)).body.code).toBe('goal_criteria_changed')
   })
-  it('grants team management only with a ready bridge and persists explicit revocation', async () => {
-    const denied = await req('post','/api/app/agents').send({name:'老板',profile:'default',canManageTeam:true}).expect(409)
-    expect(denied.body.code).toBe('team_tools_unavailable')
-    expect((await req('get','/api/app/agents')).body.agents).toEqual([])
-    bridgeReady = true
-    const created = await req('post','/api/app/agents').send({name:'老板',profile:'default',canManageTeam:true})
+  it('creates a bot with team tools on by default and does not require the bridge', async () => {
+    bridgeReady = false
+    const created = await req('post','/api/app/agents').send({name:'老板',profile:'default'})
     expect(created.status, JSON.stringify(created.body)).toBe(201)
     const agent = created.body.agent
     expect(agent.canManageTeam).toBe(true)
-    await req('patch',`/api/app/agents/${agent.id}`,'second').send({canManageTeam:false}).expect(404)
-    bridgeReady = false
-    const disabled = await req('patch',`/api/app/agents/${agent.id}`).send({canManageTeam:false}).expect(200)
-    expect(disabled.body.agent.canManageTeam).toBe(false)
-    expect((await req('get','/api/app/agents')).body.agents[0].canManageTeam).toBe(false)
-    await req('patch',`/api/app/agents/${agent.id}`).send({canManageTeam:true}).expect(409)
+    await req('patch',`/api/app/agents/${agent.id}`,'second').send({name:'别人改的'}).expect(404)
+    expect((await req('get','/api/app/agents')).body.agents[0].canManageTeam).toBe(true)
   })
   it('accepts only child pairing, hides credentials, and scopes address edits to the owner', async () => {
     const code = new URL('yaoyao://pair')

+ 3 - 4
tests/server/workspaceTeamTools.test.ts

@@ -103,7 +103,7 @@ describe('Agent-owned team tools', () => {
     const requestId = randomUUID(), args = {requestId,name:'研究员',profile:'writer',instructions:'研究事实并给出处'}
     const first = await call('create_agent',args)
     expect(await call('create_agent',args)).toEqual(first)
-    expect(first.agent.canManageTeam).toBe(false)
+    expect(first.agent.canManageTeam).toBe(true)
     expect(store.list(owner,'agent')).toHaveLength(2)
     const groupArgs = {requestId:randomUUID(),name:'项目团队',memberIds:[first.agent.id]}
     const created = await call('create_team',groupArgs)
@@ -130,7 +130,7 @@ describe('Agent-owned team tools', () => {
     await expect(call('list_agents',{owner:'other'})).rejects.toMatchObject({status:400})
     expect(store.list(owner,'agent')).toHaveLength(1)
     const ordinary = store.createAgent(owner,{name:'普通',profile:'default'})
-    expect(ordinary.canManageTeam).toBe(false)
+    expect(ordinary.canManageTeam).toBe(true)
     store.updateAgent(owner,manager.id,{canManageTeam:false})
     await expect(call('list_agents')).rejects.toMatchObject({status:403})
   })
@@ -224,7 +224,7 @@ describe('loopback tool lease', () => {
     const body={callId:'same-call',toolId:catalog.tools.find((t:any)=>t.name==='workspace_create_agent').id,arguments:{requestId:randomUUID(),name:'HTTP 成员',profile:'writer'}}
     const [first,second]=await Promise.all([http('/tools/call',body).then(r=>r.json()),http('/tools/call',body).then(r=>r.json())])
     expect(second).toEqual(first)
-    expect(first.structuredContent.agent.canManageTeam).toBe(false)
+    expect(first.structuredContent.agent.canManageTeam).toBe(true)
     expect(store.list(owner,'agent')).toHaveLength(2)
     expect((await http('/tools/call',{...body,arguments:{...body.arguments,name:'替换'}})).status).toBe(409)
     await lease.dispose()
@@ -277,7 +277,6 @@ describe('loopback tool lease', () => {
     await expect(requireTeamToolBridge({...target,url:new URL('http://192.168.1.20:9119')},'default')).rejects.toMatchObject({code:'team_tools_local_required'})
     vi.mocked(target.session.request).mockResolvedValueOnce({status:404,body:Buffer.from('{}'),headers:new Headers()})
     await expect(requireTeamToolBridge(target,'default')).rejects.toMatchObject({code:'team_tools_unavailable'})
-    await expect(runtime.teamTools.requireAvailable(owner,{nodeId:'remote',profile:'default'})).rejects.toMatchObject({code:'team_tools_local_required'})
   })
 
   it('closes a cancelled lease even if binding has not returned yet', async () => {