|
1 | 1 | import { describe, expect, test } from "bun:test"; |
2 | 2 |
|
| 3 | +import { stringTool, type AgentTool } from "@intx/agent"; |
3 | 4 | import { |
4 | 5 | createFleetMailbox, |
5 | 6 | createSpawnAgentTool, |
@@ -38,6 +39,13 @@ const FULL_MOUNT_PROFILE: AgentProfile = { |
38 | 39 | systemPromptRole: "You do anything.", |
39 | 40 | }; |
40 | 41 |
|
| 42 | +// BUILD_TOOLS-like envelope: built-ins only, no MCP names. |
| 43 | +const BUILD_LIKE_PROFILE: AgentProfile = { |
| 44 | + id: "build-worker", |
| 45 | + systemPromptRole: "You build.", |
| 46 | + capabilities: { mode: "allow", tools: ["read_file", "run_shell"] }, |
| 47 | +}; |
| 48 | + |
41 | 49 | function makeDeps( |
42 | 50 | run: (params: RunSubAgentParams) => Promise<RunSubAgentResult>, |
43 | 51 | capturedTelemetry: TelemetryEvent[], |
@@ -300,6 +308,156 @@ describe("spawn_agent requires_tools preflight", () => { |
300 | 308 | expect(seenTier).toBe("leaf"); |
301 | 309 | }); |
302 | 310 |
|
| 311 | + test("mounted mcp__linear__ tool passes dispatch preflight under a built-ins-only allowlist", async () => { |
| 312 | + const telemetry: TelemetryEvent[] = []; |
| 313 | + let runCalled = false; |
| 314 | + let seenRequires: readonly string[] | undefined; |
| 315 | + const linearTool: AgentTool = stringTool({ |
| 316 | + definition: { |
| 317 | + name: "mcp__linear__list_teams", |
| 318 | + description: "Inherited Linear tool", |
| 319 | + inputSchema: {}, |
| 320 | + }, |
| 321 | + handler: async () => "teams", |
| 322 | + }); |
| 323 | + const base = makeDeps(async (params) => { |
| 324 | + runCalled = true; |
| 325 | + seenRequires = params.requiresTools; |
| 326 | + return { report: "done" }; |
| 327 | + }, telemetry); |
| 328 | + const deps: AgentFleetDeps = { |
| 329 | + ...base, |
| 330 | + profiles: [BUILD_LIKE_PROFILE], |
| 331 | + inheritMcpTools: () => [linearTool], |
| 332 | + }; |
| 333 | + const spawn = createSpawnAgentTool(deps); |
| 334 | + |
| 335 | + const result = await callSpawn(spawn, { |
| 336 | + description: "linear job", |
| 337 | + prompt: "list teams", |
| 338 | + agent: "build-worker", |
| 339 | + requires_tools: ["mcp__linear__list_teams"], |
| 340 | + }); |
| 341 | + |
| 342 | + expect(result.isError).not.toBe(true); |
| 343 | + expect(runCalled).toBe(true); |
| 344 | + const body = JSON.parse(result.content) as { agent_id: string }; |
| 345 | + expect(deps.sessions.get(body.agent_id)?.requiresTools).toEqual([ |
| 346 | + "mcp__linear__list_teams", |
| 347 | + ]); |
| 348 | + expect(seenRequires).toEqual(["mcp__linear__list_teams"]); |
| 349 | + }); |
| 350 | + |
| 351 | + test("unmounted mcp__linear__ tool rejects dispatch preflight as unknown_tool with no run", async () => { |
| 352 | + const telemetry: TelemetryEvent[] = []; |
| 353 | + let runCalled = false; |
| 354 | + const base = makeDeps(async () => { |
| 355 | + runCalled = true; |
| 356 | + return { report: "done" }; |
| 357 | + }, telemetry); |
| 358 | + const deps: AgentFleetDeps = { |
| 359 | + ...base, |
| 360 | + profiles: [BUILD_LIKE_PROFILE], |
| 361 | + }; |
| 362 | + const spawn = createSpawnAgentTool(deps); |
| 363 | + |
| 364 | + const result = await callSpawn(spawn, { |
| 365 | + description: "linear job", |
| 366 | + prompt: "list teams", |
| 367 | + agent: "build-worker", |
| 368 | + requires_tools: ["mcp__linear__list_teams"], |
| 369 | + }); |
| 370 | + |
| 371 | + expect(result.isError).toBe(true); |
| 372 | + expect(result.content).toContain('unknown tool "mcp__linear__list_teams"'); |
| 373 | + expect(result.content).not.toContain("Did you mean"); |
| 374 | + expect(runCalled).toBe(false); |
| 375 | + expect(deps.sessions.list()).toEqual([]); |
| 376 | + expect(telemetry).toEqual([]); |
| 377 | + }); |
| 378 | + |
| 379 | + test("requires_tools stamps only the requested live tool, never the inherited set", async () => { |
| 380 | + const telemetry: TelemetryEvent[] = []; |
| 381 | + let runCalled = false; |
| 382 | + let seenRequires: readonly string[] | undefined; |
| 383 | + const mcpTool = (name: string): AgentTool => |
| 384 | + stringTool({ |
| 385 | + definition: { |
| 386 | + name, |
| 387 | + description: `Inherited ${name}`, |
| 388 | + inputSchema: {}, |
| 389 | + }, |
| 390 | + handler: async () => name, |
| 391 | + }); |
| 392 | + const base = makeDeps(async (params) => { |
| 393 | + runCalled = true; |
| 394 | + seenRequires = params.requiresTools; |
| 395 | + return { report: "done" }; |
| 396 | + }, telemetry); |
| 397 | + const deps: AgentFleetDeps = { |
| 398 | + ...base, |
| 399 | + profiles: [BUILD_LIKE_PROFILE], |
| 400 | + inheritMcpTools: () => [ |
| 401 | + mcpTool("mcp__linear__list_teams"), |
| 402 | + mcpTool("mcp__linear__create_issue"), |
| 403 | + ], |
| 404 | + }; |
| 405 | + const spawn = createSpawnAgentTool(deps); |
| 406 | + |
| 407 | + const result = await callSpawn(spawn, { |
| 408 | + description: "linear job", |
| 409 | + prompt: "list teams", |
| 410 | + agent: "build-worker", |
| 411 | + requires_tools: ["mcp__linear__list_teams"], |
| 412 | + }); |
| 413 | + |
| 414 | + // On-demand: dispatch stamps exactly the requested live tool — the |
| 415 | + // inherited sibling mounts only under its own stamp (run.ts drops it). |
| 416 | + expect(result.isError).not.toBe(true); |
| 417 | + expect(runCalled).toBe(true); |
| 418 | + expect(seenRequires).toEqual(["mcp__linear__list_teams"]); |
| 419 | + }); |
| 420 | + |
| 421 | + test("requires_tools naming a live and an unmounted mcp__ tool rejects the unmounted one with no run", async () => { |
| 422 | + const telemetry: TelemetryEvent[] = []; |
| 423 | + let runCalled = false; |
| 424 | + const base = makeDeps(async () => { |
| 425 | + runCalled = true; |
| 426 | + return { report: "done" }; |
| 427 | + }, telemetry); |
| 428 | + const deps: AgentFleetDeps = { |
| 429 | + ...base, |
| 430 | + profiles: [BUILD_LIKE_PROFILE], |
| 431 | + inheritMcpTools: () => [ |
| 432 | + stringTool({ |
| 433 | + definition: { |
| 434 | + name: "mcp__linear__list_teams", |
| 435 | + description: "Inherited Linear tool", |
| 436 | + inputSchema: {}, |
| 437 | + }, |
| 438 | + handler: async () => "teams", |
| 439 | + }), |
| 440 | + ], |
| 441 | + }; |
| 442 | + const spawn = createSpawnAgentTool(deps); |
| 443 | + |
| 444 | + const result = await callSpawn(spawn, { |
| 445 | + description: "linear job", |
| 446 | + prompt: "list teams and file", |
| 447 | + agent: "build-worker", |
| 448 | + requires_tools: ["mcp__linear__list_teams", "mcp__linear__create_issue"], |
| 449 | + }); |
| 450 | + |
| 451 | + expect(result.isError).toBe(true); |
| 452 | + expect(result.content).toContain( |
| 453 | + 'unknown tool "mcp__linear__create_issue"', |
| 454 | + ); |
| 455 | + expect(result.content).not.toContain("Did you mean"); |
| 456 | + expect(runCalled).toBe(false); |
| 457 | + expect(deps.sessions.list()).toEqual([]); |
| 458 | + expect(telemetry).toEqual([]); |
| 459 | + }); |
| 460 | + |
303 | 461 | test("whitespace-only requires_tools rejects fail-closed with no session, telemetry, or run", async () => { |
304 | 462 | for (const requiresTools of [[" "], ["read_file", " "]]) { |
305 | 463 | const telemetry: TelemetryEvent[] = []; |
|
0 commit comments