openclaw - ✅(Solved) Fix sessions_spawn accepts unknown agentId with allowAgents:"*" (auto-provisions default-configured subagent, no registry validation) [1 pull requests, 2 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#84040Fetched 2026-05-20 03:44:52
View on GitHub
Comments
2
Participants
3
Timeline
17
Reactions
1
Assignees
Timeline (top)
labeled ×10commented ×2assigned ×1closed ×1

sessions_spawn accepts arbitrary, unknown agentId values and silently instantiates a new "subagent" using default model + empty bootstrap when the requester's subagents.allowAgents contains the * wildcard. There is no registry validation: the wildcard skips the check entirely instead of meaning "any registered agent". This is undocumented behavior with side effects.

Error Message

if (allowed.allowAny) { if (allowed.allowedIds.includes(targetAgentId)) return { ok: true }; return { ok: false, allowedText: allowed.allowedIds.join(", ") || "none", error: agentId "${targetAgentId}" is not in the configured agent registry, }; }

Root Cause

src/agents/subagent-target-policy.ts short-circuits to ok: true when allowAgents contains *:

// resolveSubagentTargetPolicy
const allowed = resolveSubagentAllowedTargetIds({ requesterAgentId, allowAgents });
if (allowed.allowAny || allowed.allowedIds.includes(targetAgentId)) {
  return { ok: true };
}

resolveSubagentAllowedTargetIds does populate configuredIds from params.configuredAgentIds, but those ids are only used to build the display allowed-list — the validation path never consults the registry when allowAny is true:

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)),
  };
}

So * literally means "accept any string as a valid agent id and provision a new agent on demand using defaults." There is no callsite that checks targetAgentId ∈ configuredAgentIds.

Fix Action

Fixed

PR fix notes

PR #84357: fix: constrain wildcard subagent targets

Description (problem / solution / changelog)

Summary

  • Problem: agents.*.subagents.allowAgents: ["*"] let sessions_spawn.agentId target arbitrary unconfigured agent ids, creating ad hoc state roots.
  • Solution: Treat wildcard subagent allowlists as “any configured target” while preserving explicit allowlist entries.
  • What changed: Native subagent and ACP spawn policy now pass configured target ids into the shared target policy; docs and config comments describe the tightened wildcard behavior.
  • What did NOT change: Explicit allowlisted-but-unconfigured target ids still work, including mixed allowlists like ["*", "beta"].

Motivation

  • Closes #84040 by making the wildcard match the configured agent registry instead of acting as an open-ended agent-id creation escape.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #84040
  • This PR fixes a bug or regression

Real behavior proof (required for external PRs)

  • Behavior addressed: sessions_spawn with allowAgents: ["*"] rejects an unconfigured explicit agentId instead of spawning a new arbitrary agent state root.
  • Real environment tested: AWS Crabbox direct provider, provider=aws, lease cbx_687b1eaafdfc, run run_5a730368b161, built branch from source, live Gateway, OpenAI openai/gpt-5.5.
  • Exact steps or command run after this patch: Started Gateway with one configured agent main, main.subagents.allowAgents: ["*"], exposed sessions_spawn, then ran a live parent agent turn that called sessions_spawn once with agentId: "bogus-84040-b397f15f".
  • Evidence after fix: Run output reported ISSUE_84040_FIX_EVIDENCE with finalText: "ISSUE_84040_PARENT_REJECTED_b397f15f", one sessions_spawn tool call, transcript containing both forbidden and configured agent registry, rogueAgentDirExists: false, and rogueWorkspaceDirExists: false.
  • Observed result after fix: The model saw the forbidden tool result and replied with the rejected sentinel; no rogue agent or workspace directory was created.
  • What was not tested: Live Telegram/channel delivery was not involved; this is a Gateway/OpenAI subagent tool-path proof.
  • Before evidence: Current-main AWS repro accepted a bogus agentId and created agents/bogus-84040-.../sessions plus workspace/bogus-84040-....

Root Cause (if applicable)

  • Root cause: The shared subagent target policy treated allowAgents containing "*" as unconditional allow-any before checking whether the target id existed in the configured registry.
  • Missing detection / guardrail: Existing tests covered wildcard acceptance but not wildcard rejection for unconfigured ids.
  • Contributing context (if known): Explicit allowlists historically allowed unconfigured ids, so the fix preserves explicit entries while narrowing only wildcard expansion.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/agents/subagent-target-policy.test.ts, src/agents/openclaw-tools.subagents.sessions-spawn.allowlist.test.ts, src/agents/acp-spawn.test.ts.
  • Scenario the test should lock in: Wildcard allowlists include configured ids plus requester, reject unconfigured ids, and preserve explicit entries in mixed wildcard allowlists.
  • Why this is the smallest reliable guardrail: The shared target-policy test proves the pure contract; native and ACP spawn tests prove both callers pass configured target ids correctly.
  • Existing test that already covers this (if any): Existing wildcard tests only asserted broad acceptance.
  • If no new test is added, why not: N/A.

User-visible / Behavior Changes

subagents.allowAgents: ["*"] now means any configured target agent, not arbitrary agent ids. Operators who intentionally need an unconfigured target can still list that id explicitly.

Diagram (if applicable)

Before:
sessions_spawn(agentId=bogus) -> allowAgents ["*"] -> accepted -> bogus agent/workspace state

After:
sessions_spawn(agentId=bogus) -> allowAgents ["*"] -> registry check -> forbidden

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? Yes
  • Data access scope changed? Yes
  • If any Yes, explain risk + mitigation: The change narrows wildcard delegation to configured agents, reducing accidental or model-driven creation of arbitrary agent state/workspace roots. Explicit allowlist entries preserve intentional compatibility.

Repro + Verification

Environment

  • OS: Linux on AWS Crabbox
  • Runtime/container: Node 24 via repository build on direct AWS Crabbox
  • Model/provider: OpenAI openai/gpt-5.5
  • Integration/channel (if any): Gateway RPC only; no external channel
  • Relevant config (redacted): one configured main agent with subagents.allowAgents: ["*"]; OpenAI API key sourced from environment

Steps

  1. Build OpenClaw from this branch.
  2. Start Gateway with one configured main agent and main.subagents.allowAgents: ["*"].
  3. Run a live parent agent turn that calls sessions_spawn once with an unconfigured bogus agentId.

Expected

  • The tool call returns forbidden with a configured-registry error and no bogus state root is created.

Actual

  • Parent final text was ISSUE_84040_PARENT_REJECTED_b397f15f; transcript contained forbidden and configured agent registry; no rogue agent/workspace directories existed.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

  • Verified scenarios: Target-policy pure contract, native sessions_spawn allowlist enforcement, ACP envelope allowlist enforcement, live AWS Gateway/OpenAI repro path.
  • Edge cases checked: Mixed wildcard-plus-explicit allowlists preserve explicit unconfigured ids; explicit self-target behavior remains denyable; default omitted-agent self-spawn remains allowed.
  • What you did not verify: Channel-specific delivery flows.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Mostly
  • Config/env changes? Yes
  • Migration needed? No
  • If yes, exact upgrade steps: If an operator intentionally used ["*"] to target an unconfigured id, add that id explicitly to allowAgents.

Risks and Mitigations

  • Risk: Operators relying on wildcard to target ad hoc ids will now see a forbidden result.
    • Mitigation: Explicit allowlist entries still allow those ids, and the error lists configured/allowed targets.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • docs/gateway/config-agents.md (modified, +1/-1)
  • docs/gateway/config-tools.md (modified, +1/-1)
  • docs/tools/subagents.md (modified, +1/-1)
  • src/agents/acp-spawn.test.ts (modified, +72/-0)
  • src/agents/acp-spawn.ts (modified, +33/-2)
  • src/agents/openclaw-tools.subagents.sessions-spawn.allowlist.test.ts (modified, +26/-1)
  • src/agents/subagent-spawn.ts (modified, +6/-1)
  • src/agents/subagent-target-policy.test.ts (modified, +55/-0)
  • src/agents/subagent-target-policy.ts (modified, +14/-1)
  • src/config/types.agent-defaults.ts (modified, +1/-1)
  • src/config/types.agents.ts (modified, +1/-1)

Code Example

// Probe 1
sessions_spawn({ agentId: "nopagent", task: "...", mode: "run" })
// → status: "accepted", childSessionKey: "agent:nopagent:subagent:..."

// Probe 2
sessions_spawn({ agentId: "bogus_test_agent_xyz", task: "diagnostic..." })
// → status: "accepted", model: "claude-opus-4-7" (defaults), no agent config

---

// resolveSubagentTargetPolicy
const allowed = resolveSubagentAllowedTargetIds({ requesterAgentId, allowAgents });
if (allowed.allowAny || allowed.allowedIds.includes(targetAgentId)) {
  return { ok: true };
}

---

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)),
  };
}

---

argus-security/   claude/         claude-code/    default/
docs-agent/       erato/          gemini-cli/     git-agent/
graybeard/        iris-artist/    librarian-agent/ main/
newhart/          nhr-agent/      qa-agent/       ...

---

if (allowed.allowAny) {
  if (allowed.allowedIds.includes(targetAgentId)) return { ok: true };
  return {
    ok: false,
    allowedText: allowed.allowedIds.join(", ") || "none",
    error: `agentId "${targetAgentId}" is not in the configured agent registry`,
  };
}

---

agents: {
  subagents: {
    strictRegistry?: boolean; // default true
    // existing fields...
  }
}
RAW_BUFFERClick to expand / collapse

Summary

sessions_spawn accepts arbitrary, unknown agentId values and silently instantiates a new "subagent" using default model + empty bootstrap when the requester's subagents.allowAgents contains the * wildcard. There is no registry validation: the wildcard skips the check entirely instead of meaning "any registered agent". This is undocumented behavior with side effects.

Discovery

While auditing a peer-agent boundary violation, we noticed sessions_spawn could target an arbitrary peer agent name (e.g., graybeard) even though that agent doesn't exist in the requester's agent registry — only peer-agent gateways host it. Two probes confirmed:

// Probe 1
sessions_spawn({ agentId: "nopagent", task: "...", mode: "run" })
// → status: "accepted", childSessionKey: "agent:nopagent:subagent:..."

// Probe 2
sessions_spawn({ agentId: "bogus_test_agent_xyz", task: "diagnostic..." })
// → status: "accepted", model: "claude-opus-4-7" (defaults), no agent config

The bogus session ran successfully and replied: "I am the bogus_test_agent_xyz subagent, running on the anthropic/claude-opus-4-7 model."

Both child workspaces were materialized on disk: ~/.openclaw/agents/nopagent/ and ~/.openclaw/agents/bogus_test_agent_xyz/.

Root Cause

src/agents/subagent-target-policy.ts short-circuits to ok: true when allowAgents contains *:

// resolveSubagentTargetPolicy
const allowed = resolveSubagentAllowedTargetIds({ requesterAgentId, allowAgents });
if (allowed.allowAny || allowed.allowedIds.includes(targetAgentId)) {
  return { ok: true };
}

resolveSubagentAllowedTargetIds does populate configuredIds from params.configuredAgentIds, but those ids are only used to build the display allowed-list — the validation path never consults the registry when allowAny is true:

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)),
  };
}

So * literally means "accept any string as a valid agent id and provision a new agent on demand using defaults." There is no callsite that checks targetAgentId ∈ configuredAgentIds.

Behavior When agentId Is Unknown

  1. Spawn gate accepts the arbitrary id (no error, no warning).
  2. Skip agent registry lookup entirely.
  3. Apply agents.defaults.model.primary (no per-agent model config).
  4. Apply default/empty bootstrap (no per-agent SOUL.md, no DB-backed bootstrap rows, no domain context).
  5. Create a fresh agent home directory at ~/.openclaw/agents/<arbitrary_string>/ with subdirs (agent/, sessions/).
  6. Spawn it as a subagent parented to the requester. Full tool access (workspace, exec, etc.).
  7. Persist transcripts under the bogus identity until manually cleaned.

The resulting agent has the requester's workspace and tool surface but inherits no policy or identity guardrails specific to the spoofed name.

Impact

Security / scoping

  • Any caller with sessions_spawn and allowAgents: ["*"] can name-collide or impersonate any agent string — including peer-agent names from other gateways (e.g., newhart, graybeard). The spawn doesn't actually reach those peers (they're separate gateways), but the in-gateway clone runs with the requester's permissions while looking like the peer in logs, transcripts, and channel announces.
  • A simple typo (coderr for coder) produces a silent fallback to a default-configured agent instead of an error, which can mask real configuration drift.
  • The * wildcard's intuitive meaning ("any registered agent in my allowlist") is wildly different from its actual meaning ("auto-provision any string"). Operators are likely to assume the safer semantic.

Filesystem hygiene

~/.openclaw/agents/ accumulates directories for every bogus, typo'd, deprecated, or accidentally-spawned name and never garbage-collects them. Sample from a long-running production gateway:

argus-security/   claude/         claude-code/    default/
docs-agent/       erato/          gemini-cli/     git-agent/
graybeard/        iris-artist/    librarian-agent/ main/
newhart/          nhr-agent/      qa-agent/       ...

Most of these have no corresponding entries in agents.json and represent historical accidental spawns or deprecated agent names. Each has its own sessions/*.jsonl transcripts. Over time this becomes a sprawl problem and an audit headache.

Observability

  • agents_list correctly shows only registered agents, so operators auditing their roster won't see the rogue identities even though they exist on disk.
  • Transcripts under bogus identities aren't surfaced anywhere unless you list the filesystem directly.

Proposed Fix

Primary (default behavior): Validate target agent id against the configured registry even when allowAgents: ["*"] is set. Replace the wildcard semantic with "any agent present in agents.list".

In resolveSubagentTargetPolicy, when allowAny is true, still require:

if (allowed.allowAny) {
  if (allowed.allowedIds.includes(targetAgentId)) return { ok: true };
  return {
    ok: false,
    allowedText: allowed.allowedIds.join(", ") || "none",
    error: `agentId "${targetAgentId}" is not in the configured agent registry`,
  };
}

(Make sure configuredAgentIds is actually threaded into resolveSubagentAllowedTargetIds at all resolveSubagentTargetPolicy callsites — right now it's accepted but the policy resolver doesn't pass it through.)

Opt-out flag: Add agents.subagents.strictRegistry: boolean (default true) to let operators preserve the current auto-provision behavior if they've built on top of it. When false, restore the pre-fix lax behavior with a one-time deprecation warning logged at spawn time.

Suggested config schema:

agents: {
  subagents: {
    strictRegistry?: boolean; // default true
    // existing fields...
  }
}

Acceptance Criteria

  • sessions_spawn({ agentId: "<unknown>" }) returns an error like agentId "<unknown>" is not in the configured agent registry (allowed: a, b, c) instead of accepting and provisioning.
  • No filesystem artifacts (no new ~/.openclaw/agents/<unknown>/ directory, no transcript file).
  • Existing tests for subagent-target-policy updated; new test covers the * + unknown-id case.
  • Setting agents.subagents.strictRegistry: false restores current behavior (auto-provision) for backward compat, with a WARN log per spawn.
  • Documentation update in docs/concepts/session-tool.md (or multi-agent.md) explicitly stating the registry validation contract and the opt-out flag.

Discovery Context (for changelog)

Found 2026-05-19 by NOVA agent (production) during a peer-agent boundary audit. The trigger was noticing that sessions_spawn(agentId: "graybeard") had succeeded earlier in the day even though Graybeard is a peer agent running in a separate gateway. The follow-up probes with synthetic names (nopagent, bogus_test_agent_xyz) confirmed there is no registry check on the * path.

Related

  • src/agents/subagent-target-policy.ts — primary bug location
  • src/agents/subagent-target-policy.test.ts — needs coverage for unknown-id case
  • docs/concepts/session-tool.md — needs registry-validation contract documented
  • docs/concepts/multi-agent.md — could note the strictRegistry knob

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix sessions_spawn accepts unknown agentId with allowAgents:"*" (auto-provisions default-configured subagent, no registry validation) [1 pull requests, 2 comments, 3 participants]