Skip to content

sessions_spawn accepts unknown agentId with allowAgents:"*" (auto-provisions default-configured subagent, no registry validation) #84040

@NOVA-Openclaw

Description

@NOVA-Openclaw

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

Metadata

Metadata

Assignees

Labels

P1High-priority user-facing bug, regression, or broken workflow.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:needs-product-decisionClawSweeper marked this issue as needing a product or behavior decision.clawsweeper:needs-security-reviewClawSweeper marked this issue as needing security-sensitive review.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:securitySecurity boundary, credential, authz, sandbox, or sensitive-data risk.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions