mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:40:43 +00:00
fix(subagents): enforce explicit spawn allowlists
This commit is contained in:
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agents/subagents: enforce `subagents.allowAgents` for explicit same-agent `sessions_spawn(agentId=...)` calls instead of auto-allowing requester self-targets. Fixes #72827. Thanks @oiGaDio.
|
||||
- ACP/sessions_spawn: let explicit `sessions_spawn(runtime="acp")` bootstrap turns run while `acp.dispatch.enabled=false` still blocks automatic ACP thread dispatch. Fixes #63591. Thanks @moeedahmed.
|
||||
- Google Meet: route local Chrome joins through OpenClaw browser control instead of raw default Chrome, so agents use the configured OpenClaw browser profile when opening Meet.
|
||||
- Plugins/discovery: follow symlinked plugin directories in global and workspace plugin roots while keeping broken links ignored and existing package safety checks in place. Fixes #36754; carries forward #72695 and #63206. Thanks @Quackstro, @ming1523, and @xsfX20.
|
||||
|
||||
@@ -979,7 +979,7 @@ for provider examples and precedence.
|
||||
- `runtime`: optional per-agent runtime descriptor. Use `type: "acp"` with `runtime.acp` defaults (`agent`, `backend`, `mode`, `cwd`) when the agent should default to ACP harness sessions.
|
||||
- `identity.avatar`: workspace-relative path, `http(s)` URL, or `data:` URI.
|
||||
- `identity` derives defaults: `ackReaction` from `emoji`, `mentionPatterns` from `name`/`emoji`.
|
||||
- `subagents.allowAgents`: allowlist of agent ids for `sessions_spawn` (`["*"]` = any; default: same agent only).
|
||||
- `subagents.allowAgents`: allowlist of agent ids for explicit `sessions_spawn.agentId` targets (`["*"]` = any; default: same agent only). Include the requester id when self-targeted `agentId` calls should be allowed.
|
||||
- Sandbox inheritance guard: if the requester session is sandboxed, `sessions_spawn` rejects targets that would run unsandboxed.
|
||||
- `subagents.requireAgentId`: when true, block `sessions_spawn` calls that omit `agentId` (forces explicit profile selection; default: false).
|
||||
|
||||
|
||||
@@ -244,7 +244,7 @@ See [Configuration reference](/gateway/configuration-reference) and
|
||||
### Allowlist
|
||||
|
||||
<ParamField path="agents.list[].subagents.allowAgents" type="string[]">
|
||||
List of agent ids that can be targeted via `agentId` (`["*"]` allows any). Default: only the requester agent.
|
||||
List of agent ids that can be targeted via explicit `agentId` (`["*"]` allows any). Default: only the requester agent. If you set a list and still want the requester to spawn itself with `agentId`, include the requester id in the list.
|
||||
</ParamField>
|
||||
<ParamField path="agents.defaults.subagents.allowAgents" type="string[]">
|
||||
Default target-agent allowlist used when the requester agent does not set its own `subagents.allowAgents`.
|
||||
|
||||
@@ -1100,6 +1100,38 @@ describe("spawnAcpDirect", () => {
|
||||
expect(failed.error).toContain("agentId is not allowed");
|
||||
});
|
||||
|
||||
it("rejects explicit ACP self-targets when the subagent allowlist excludes the requester", async () => {
|
||||
replaceSpawnConfig({
|
||||
...hoisted.state.cfg,
|
||||
acp: {
|
||||
...hoisted.state.cfg.acp,
|
||||
allowedAgents: ["codex", "writer"],
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
subagents: {
|
||||
allowAgents: ["writer"],
|
||||
maxSpawnDepth: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await spawnAcpDirect(
|
||||
createSpawnRequest({
|
||||
agentId: "codex",
|
||||
}),
|
||||
{
|
||||
...createRequesterContext(),
|
||||
agentSessionKey: "agent:codex:subagent:parent",
|
||||
},
|
||||
);
|
||||
|
||||
const failed = expectFailedSpawn(result, "forbidden");
|
||||
expect(failed.errorCode).toBe("subagent_policy");
|
||||
expect(failed.error).toContain("agentId is not allowed");
|
||||
});
|
||||
|
||||
it("spawns Matrix thread-bound ACP sessions from top-level room targets", async () => {
|
||||
enableMatrixAcpThreadBindings();
|
||||
hoisted.sessionBindingBindMock.mockImplementationOnce(
|
||||
|
||||
@@ -82,6 +82,7 @@ import {
|
||||
} from "./subagent-capabilities.js";
|
||||
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
|
||||
import { countActiveRunsForSession, getSubagentRunByChildSessionKey } from "./subagent-registry.js";
|
||||
import { resolveSubagentTargetPolicy } from "./subagent-target-policy.js";
|
||||
import { resolveInternalSessionKey, resolveMainSessionAlias } from "./tools/sessions-helpers.js";
|
||||
|
||||
const log = createSubsystemLogger("agents/acp-spawn");
|
||||
@@ -786,24 +787,18 @@ function resolveAcpSubagentEnvelopeState(params: {
|
||||
};
|
||||
}
|
||||
|
||||
if (params.targetAgentId !== requesterAgentId) {
|
||||
const allowAgents =
|
||||
const targetPolicy = resolveSubagentTargetPolicy({
|
||||
requesterAgentId,
|
||||
targetAgentId: params.targetAgentId,
|
||||
requestedAgentId: params.requestedAgentId,
|
||||
allowAgents:
|
||||
resolveAgentConfig(params.cfg, requesterAgentId)?.subagents?.allowAgents ??
|
||||
params.cfg.agents?.defaults?.subagents?.allowAgents ??
|
||||
[];
|
||||
const allowAny = allowAgents.some((value) => value.trim() === "*");
|
||||
const normalizedTargetId = normalizeOptionalLowercaseString(params.targetAgentId) ?? "";
|
||||
const allowSet = new Set(
|
||||
allowAgents
|
||||
.filter((value) => value.trim() && value.trim() !== "*")
|
||||
.map((value) => normalizeOptionalLowercaseString(normalizeAgentId(value)) ?? ""),
|
||||
);
|
||||
if (!allowAny && !allowSet.has(normalizedTargetId)) {
|
||||
const allowedText = allowSet.size > 0 ? Array.from(allowSet).join(", ") : "none";
|
||||
return {
|
||||
error: `agentId is not allowed for sessions_spawn (allowed: ${allowedText})`,
|
||||
};
|
||||
}
|
||||
params.cfg.agents?.defaults?.subagents?.allowAgents,
|
||||
});
|
||||
if (!targetPolicy.ok) {
|
||||
return {
|
||||
error: targetPolicy.error,
|
||||
};
|
||||
}
|
||||
|
||||
const childCapabilities = resolveSubagentCapabilities({
|
||||
|
||||
@@ -14,6 +14,7 @@ const hoisted = vi.hoisted(() => ({
|
||||
pruneLegacyStoreKeysMock: vi.fn(),
|
||||
registerSubagentRunMock: vi.fn(),
|
||||
emitSessionLifecycleEventMock: vi.fn(),
|
||||
resolveAgentConfigMock: vi.fn(),
|
||||
configOverride: {} as Record<string, unknown>,
|
||||
}));
|
||||
|
||||
@@ -46,7 +47,7 @@ describe("spawnSubagentDirect seam flow", () => {
|
||||
pruneLegacyStoreKeysMock: hoisted.pruneLegacyStoreKeysMock,
|
||||
registerSubagentRunMock: hoisted.registerSubagentRunMock,
|
||||
emitSessionLifecycleEventMock: hoisted.emitSessionLifecycleEventMock,
|
||||
resolveAgentConfig: () => undefined,
|
||||
resolveAgentConfig: hoisted.resolveAgentConfigMock,
|
||||
resolveSubagentSpawnModelSelection: () => "openai-codex/gpt-5.4",
|
||||
resolveSandboxRuntimeStatus: () => ({ sandboxed: false }),
|
||||
sessionStorePath: "/tmp/subagent-spawn-session-store.json",
|
||||
@@ -61,6 +62,11 @@ describe("spawnSubagentDirect seam flow", () => {
|
||||
hoisted.pruneLegacyStoreKeysMock.mockReset();
|
||||
hoisted.registerSubagentRunMock.mockReset();
|
||||
hoisted.emitSessionLifecycleEventMock.mockReset();
|
||||
hoisted.resolveAgentConfigMock.mockReset();
|
||||
hoisted.resolveAgentConfigMock.mockImplementation(
|
||||
(cfg: { agents?: { list?: Array<{ id?: string }> } }, agentId: string) =>
|
||||
cfg.agents?.list?.find((agent) => agent.id === agentId),
|
||||
);
|
||||
hoisted.configOverride = createConfigOverride();
|
||||
installAcceptedSubagentGatewayMock(hoisted.callGatewayMock);
|
||||
|
||||
@@ -76,6 +82,84 @@ describe("spawnSubagentDirect seam flow", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects explicit same-agent targets when allowAgents excludes the requester", async () => {
|
||||
hoisted.configOverride = createConfigOverride({
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: os.tmpdir(),
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "task-manager",
|
||||
workspace: "/tmp/workspace-task-manager",
|
||||
subagents: {
|
||||
allowAgents: ["planner"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "planner",
|
||||
workspace: "/tmp/workspace-planner",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await spawnSubagentDirect(
|
||||
{
|
||||
task: "spawn myself explicitly",
|
||||
agentId: "task-manager",
|
||||
},
|
||||
{
|
||||
agentSessionKey: "agent:task-manager:main",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: "forbidden",
|
||||
error: "agentId is not allowed for sessions_spawn (allowed: planner)",
|
||||
});
|
||||
expect(hoisted.callGatewayMock).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ method: "agent" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("allows omitted agentId to default to requester even when allowAgents excludes requester", async () => {
|
||||
hoisted.configOverride = createConfigOverride({
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: os.tmpdir(),
|
||||
},
|
||||
list: [
|
||||
{
|
||||
id: "task-manager",
|
||||
workspace: "/tmp/workspace-task-manager",
|
||||
subagents: {
|
||||
allowAgents: ["planner"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "planner",
|
||||
workspace: "/tmp/workspace-planner",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await spawnSubagentDirect(
|
||||
{
|
||||
task: "spawn default target",
|
||||
},
|
||||
{
|
||||
agentSessionKey: "agent:task-manager:main",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: "accepted",
|
||||
childSessionKey: expect.stringMatching(/^agent:task-manager:subagent:/),
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts a spawned run across session patching, runtime-model persistence, registry registration, and lifecycle emission", async () => {
|
||||
const operations: string[] = [];
|
||||
let persistedStore: Record<string, Record<string, unknown>> | undefined;
|
||||
|
||||
@@ -8,10 +8,7 @@ import type { SubagentSpawnPreparation } from "../context-engine/types.js";
|
||||
import { listRegisteredPluginAgentPromptGuidance } from "../plugins/command-registry-state.js";
|
||||
import type { SubagentLifecycleHookRunner } from "../plugins/hooks.js";
|
||||
import { isValidAgentId, normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
} from "../shared/string-coerce.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import type { DeliveryContext } from "../utils/delivery-context.types.js";
|
||||
import type { BootstrapContextMode } from "./bootstrap-files.js";
|
||||
import {
|
||||
@@ -29,6 +26,7 @@ import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
|
||||
import { buildSubagentInitialUserMessage } from "./subagent-initial-user-message.js";
|
||||
import { countActiveRunsForSession, registerSubagentRun } from "./subagent-registry.js";
|
||||
import { resolveSubagentSpawnAcceptedNote } from "./subagent-spawn-accepted-note.js";
|
||||
import { resolveSubagentTargetPolicy } from "./subagent-target-policy.js";
|
||||
export {
|
||||
SUBAGENT_SPAWN_ACCEPTED_NOTE,
|
||||
SUBAGENT_SPAWN_SESSION_ACCEPTED_NOTE,
|
||||
@@ -744,25 +742,19 @@ export async function spawnSubagentDirect(
|
||||
requesterGroupSpace: ctx.agentGroupSpace,
|
||||
requesterMemberRoleIds: ctx.agentMemberRoleIds,
|
||||
});
|
||||
if (targetAgentId !== requesterAgentId) {
|
||||
const allowAgents =
|
||||
const targetPolicy = resolveSubagentTargetPolicy({
|
||||
requesterAgentId,
|
||||
targetAgentId,
|
||||
requestedAgentId,
|
||||
allowAgents:
|
||||
resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ??
|
||||
cfg?.agents?.defaults?.subagents?.allowAgents ??
|
||||
[];
|
||||
const allowAny = allowAgents.some((value) => value.trim() === "*");
|
||||
const normalizedTargetId = normalizeLowercaseStringOrEmpty(targetAgentId);
|
||||
const allowSet = new Set(
|
||||
allowAgents
|
||||
.filter((value) => value.trim() && value.trim() !== "*")
|
||||
.map((value) => normalizeLowercaseStringOrEmpty(normalizeAgentId(value))),
|
||||
);
|
||||
if (!allowAny && !allowSet.has(normalizedTargetId)) {
|
||||
const allowedText = allowSet.size > 0 ? Array.from(allowSet).join(", ") : "none";
|
||||
return {
|
||||
status: "forbidden",
|
||||
error: `agentId is not allowed for sessions_spawn (allowed: ${allowedText})`,
|
||||
};
|
||||
}
|
||||
cfg?.agents?.defaults?.subagents?.allowAgents,
|
||||
});
|
||||
if (!targetPolicy.ok) {
|
||||
return {
|
||||
status: "forbidden",
|
||||
error: targetPolicy.error,
|
||||
};
|
||||
}
|
||||
const childSessionKey = `agent:${targetAgentId}:subagent:${crypto.randomUUID()}`;
|
||||
const requesterRuntime = resolveSandboxRuntimeStatus({
|
||||
|
||||
62
src/agents/subagent-target-policy.test.ts
Normal file
62
src/agents/subagent-target-policy.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
resolveSubagentAllowedTargetIds,
|
||||
resolveSubagentTargetPolicy,
|
||||
} from "./subagent-target-policy.js";
|
||||
|
||||
describe("subagent target policy", () => {
|
||||
it("defaults to requester-only when no allowlist is configured", () => {
|
||||
expect(
|
||||
resolveSubagentTargetPolicy({
|
||||
requesterAgentId: "main",
|
||||
targetAgentId: "main",
|
||||
requestedAgentId: "main",
|
||||
}),
|
||||
).toEqual({ ok: true });
|
||||
expect(
|
||||
resolveSubagentTargetPolicy({
|
||||
requesterAgentId: "main",
|
||||
targetAgentId: "other",
|
||||
requestedAgentId: "other",
|
||||
}),
|
||||
).toMatchObject({ ok: false, allowedText: "main" });
|
||||
});
|
||||
|
||||
it("keeps omitted agentId self-spawns allowed even when an allowlist is configured", () => {
|
||||
expect(
|
||||
resolveSubagentTargetPolicy({
|
||||
requesterAgentId: "task-manager",
|
||||
targetAgentId: "task-manager",
|
||||
allowAgents: ["planner"],
|
||||
}),
|
||||
).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("rejects explicit self-targets when the configured allowlist excludes the requester", () => {
|
||||
expect(
|
||||
resolveSubagentTargetPolicy({
|
||||
requesterAgentId: "task-manager",
|
||||
targetAgentId: "task-manager",
|
||||
requestedAgentId: "task-manager",
|
||||
allowAgents: ["planner", "checker"],
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
allowedText: "checker, planner",
|
||||
error: "agentId is not allowed for sessions_spawn (allowed: checker, planner)",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves allowed target ids without auto-adding requester for explicit allowlists", () => {
|
||||
expect(
|
||||
resolveSubagentAllowedTargetIds({
|
||||
requesterAgentId: "main",
|
||||
allowAgents: ["planner"],
|
||||
configuredAgentIds: ["main", "planner"],
|
||||
}),
|
||||
).toEqual({
|
||||
allowAny: false,
|
||||
allowedIds: ["planner"],
|
||||
});
|
||||
});
|
||||
});
|
||||
84
src/agents/subagent-target-policy.ts
Normal file
84
src/agents/subagent-target-policy.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { normalizeAgentId } from "../routing/session-key.js";
|
||||
|
||||
export type SubagentTargetPolicyResult =
|
||||
| { ok: true }
|
||||
| { ok: false; allowedText: string; error: string };
|
||||
|
||||
function normalizeAllowAgents(allowAgents: readonly string[] | undefined): {
|
||||
configured: boolean;
|
||||
allowAny: boolean;
|
||||
allowedIds: string[];
|
||||
} {
|
||||
if (!Array.isArray(allowAgents)) {
|
||||
return {
|
||||
configured: false,
|
||||
allowAny: false,
|
||||
allowedIds: [],
|
||||
};
|
||||
}
|
||||
const allowedIds = allowAgents
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value && value !== "*")
|
||||
.map((value) => normalizeAgentId(value))
|
||||
.filter(Boolean);
|
||||
return {
|
||||
configured: true,
|
||||
allowAny: allowAgents.some((value) => value.trim() === "*"),
|
||||
allowedIds: Array.from(new Set(allowedIds)).toSorted((a, b) => a.localeCompare(b)),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveSubagentAllowedTargetIds(params: {
|
||||
requesterAgentId: string;
|
||||
allowAgents?: readonly string[];
|
||||
configuredAgentIds?: readonly string[];
|
||||
}): { allowAny: boolean; allowedIds: string[] } {
|
||||
const requesterAgentId = normalizeAgentId(params.requesterAgentId);
|
||||
const policy = normalizeAllowAgents(params.allowAgents);
|
||||
if (!policy.configured) {
|
||||
return {
|
||||
allowAny: false,
|
||||
allowedIds: requesterAgentId ? [requesterAgentId] : [],
|
||||
};
|
||||
}
|
||||
if (policy.allowAny) {
|
||||
const configuredIds = (params.configuredAgentIds ?? [])
|
||||
.map((id) => normalizeAgentId(id))
|
||||
.filter(Boolean);
|
||||
return {
|
||||
allowAny: true,
|
||||
allowedIds: Array.from(new Set(configuredIds)).toSorted((a, b) => a.localeCompare(b)),
|
||||
};
|
||||
}
|
||||
return {
|
||||
allowAny: false,
|
||||
allowedIds: policy.allowedIds,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveSubagentTargetPolicy(params: {
|
||||
requesterAgentId: string;
|
||||
targetAgentId: string;
|
||||
requestedAgentId?: string;
|
||||
allowAgents?: readonly string[];
|
||||
}): SubagentTargetPolicyResult {
|
||||
const requesterAgentId = normalizeAgentId(params.requesterAgentId);
|
||||
const targetAgentId = normalizeAgentId(params.targetAgentId);
|
||||
if (!params.requestedAgentId?.trim() && targetAgentId === requesterAgentId) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const allowed = resolveSubagentAllowedTargetIds({
|
||||
requesterAgentId,
|
||||
allowAgents: params.allowAgents,
|
||||
});
|
||||
if (allowed.allowAny || allowed.allowedIds.includes(targetAgentId)) {
|
||||
return { ok: true };
|
||||
}
|
||||
const allowedText = allowed.allowedIds.length > 0 ? allowed.allowedIds.join(", ") : "none";
|
||||
return {
|
||||
ok: false,
|
||||
allowedText,
|
||||
error: `agentId is not allowed for sessions_spawn (allowed: ${allowedText})`,
|
||||
};
|
||||
}
|
||||
@@ -47,12 +47,6 @@ describe("agents_list tool", () => {
|
||||
expect(result.details).toMatchObject({
|
||||
requester: "main",
|
||||
agents: [
|
||||
{
|
||||
id: "main",
|
||||
configured: true,
|
||||
model: "anthropic/claude-opus-4.5",
|
||||
agentRuntime: { id: "pi", source: "defaults" },
|
||||
},
|
||||
{
|
||||
id: "codex",
|
||||
name: "Codex",
|
||||
@@ -64,6 +58,31 @@ describe("agents_list tool", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("returns requester as the only target when no subagent allowlist is configured", async () => {
|
||||
loadConfigMock.mockReturnValue({
|
||||
agents: {
|
||||
list: [{ id: "main", default: true }, { id: "codex" }],
|
||||
},
|
||||
} satisfies OpenClawConfig);
|
||||
|
||||
const { createAgentsListTool } = await import("./agents-list-tool.js");
|
||||
const result = await createAgentsListTool({ agentSessionKey: "agent:main:main" }).execute(
|
||||
"call",
|
||||
{},
|
||||
);
|
||||
|
||||
expect(result.details).toMatchObject({
|
||||
requester: "main",
|
||||
allowAny: false,
|
||||
agents: [
|
||||
{
|
||||
id: "main",
|
||||
configured: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("marks OPENCLAW_AGENT_RUNTIME as the effective runtime source", async () => {
|
||||
vi.stubEnv("OPENCLAW_AGENT_RUNTIME", "codex");
|
||||
loadConfigMock.mockReturnValue({
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
resolveAgentConfig,
|
||||
resolveAgentEffectiveModelPrimary,
|
||||
} from "../agent-scope.js";
|
||||
import { resolveSubagentAllowedTargetIds } from "../subagent-target-policy.js";
|
||||
import type { AnyAgentTool } from "./common.js";
|
||||
import { jsonResult } from "./common.js";
|
||||
import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-helpers.js";
|
||||
@@ -102,14 +103,7 @@ export function createAgentsListTool(opts?: {
|
||||
|
||||
const allowAgents =
|
||||
resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ??
|
||||
cfg?.agents?.defaults?.subagents?.allowAgents ??
|
||||
[];
|
||||
const allowAny = allowAgents.some((value) => value.trim() === "*");
|
||||
const allowSet = new Set(
|
||||
allowAgents
|
||||
.filter((value) => value.trim() && value.trim() !== "*")
|
||||
.map((value) => normalizeAgentId(value)),
|
||||
);
|
||||
cfg?.agents?.defaults?.subagents?.allowAgents;
|
||||
|
||||
const configuredAgents = Array.isArray(cfg.agents?.list) ? cfg.agents?.list : [];
|
||||
const configuredIds = configuredAgents.map((entry) => normalizeAgentId(entry.id));
|
||||
@@ -122,23 +116,16 @@ export function createAgentsListTool(opts?: {
|
||||
configuredNameMap.set(normalizeAgentId(entry.id), name);
|
||||
}
|
||||
|
||||
const allowed = new Set<string>();
|
||||
allowed.add(requesterAgentId);
|
||||
if (allowAny) {
|
||||
for (const id of configuredIds) {
|
||||
allowed.add(id);
|
||||
}
|
||||
} else {
|
||||
for (const id of allowSet) {
|
||||
allowed.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
const all = Array.from(allowed);
|
||||
const allowed = resolveSubagentAllowedTargetIds({
|
||||
requesterAgentId,
|
||||
allowAgents,
|
||||
configuredAgentIds: configuredIds,
|
||||
});
|
||||
const all = allowed.allowedIds;
|
||||
const rest = all
|
||||
.filter((id) => id !== requesterAgentId)
|
||||
.toSorted((a, b) => a.localeCompare(b));
|
||||
const ordered = [requesterAgentId, ...rest];
|
||||
const ordered = all.includes(requesterAgentId) ? [requesterAgentId, ...rest] : rest;
|
||||
const agents: AgentListEntry[] = ordered.map((id) => {
|
||||
const agentRuntime = resolveAgentRuntimeMetadata(cfg, id);
|
||||
return {
|
||||
@@ -152,7 +139,7 @@ export function createAgentsListTool(opts?: {
|
||||
|
||||
return jsonResult({
|
||||
requester: requesterAgentId,
|
||||
allowAny,
|
||||
allowAny: allowed.allowAny,
|
||||
agents,
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user