fix(core): limit v2 subagent nesting depth (#37291)

This commit is contained in:
opencode-agent[bot] 2026-07-16 09:28:15 -04:00 committed by GitHub
commit d01dfa57b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 116 additions and 0 deletions

View file

@ -18,6 +18,7 @@ import { ConfigAgent } from "./config/agent"
import { ConfigAttachments } from "./config/attachments"
import { ConfigCompaction } from "./config/compaction"
import { ConfigCommand } from "./config/command"
import { ConfigExperimental } from "./config/experimental"
import { ConfigFormatter } from "./config/formatter"
import { ConfigLSP } from "./config/lsp"
import { ConfigMCP } from "./config/mcp"
@ -109,6 +110,7 @@ export class Info extends Schema.Class<Info>("Config.Info")({
description: "Ordered plugin enablement directives and external package declarations",
}),
providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional),
experimental: ConfigExperimental.Info.pipe(Schema.optional),
}) {}
export class Document extends Schema.Class<Document>("Config.Document")({

View file

@ -0,0 +1,10 @@
export * as ConfigExperimental from "./experimental"
import { Schema } from "effect"
import { NonNegativeInt } from "../schema"
export class Info extends Schema.Class<Info>("ConfigExperimental.Info")({
subagent_depth: NonNegativeInt.pipe(Schema.optional).annotate({
description: "Maximum subagent nesting depth. Defaults to 1.",
}),
}) {}

View file

@ -4,6 +4,7 @@ import { ToolFailure } from "@opencode-ai/ai"
import type { Context as PluginContext } from "@opencode-ai/plugin/v2/effect/plugin"
import { Effect, Schema, Scope } from "effect"
import { AgentV2 } from "../agent"
import { Config } from "../config"
import { PluginRuntime } from "../plugin/runtime"
import { PermissionV2 } from "../permission"
import { SessionSchema } from "../session/schema"
@ -43,6 +44,7 @@ export const Plugin = {
effect: Effect.fn("SubagentTool.Plugin")(function* (ctx: PluginContext) {
const runtime = yield* PluginRuntime.Service
const agents = yield* AgentV2.Service
const config = yield* Config.Service
const permission = yield* PermissionV2.Service
const scope = yield* Scope.Scope
@ -123,6 +125,23 @@ export const Plugin = {
(error) => new ToolFailure({ message: `Parent session not found: ${context.sessionID}`, error }),
),
)
let current = parent
let depth = 0
while (current.parentID) {
depth++
current = yield* runtime.session
.get(current.parentID)
.pipe(
Effect.mapError(
(error) => new ToolFailure({ message: `Parent session not found: ${current.parentID}`, error }),
),
)
}
const limit = Config.latest(yield* config.entries(), "experimental")?.subagent_depth ?? 1
if (depth >= limit)
return yield* new ToolFailure({
message: `Subagent depth limit reached (${limit}). Increase "experimental.subagent_depth" to allow nested subagents.`,
})
const agent = yield* agents.resolve(input.agent)
if (agent === undefined) return yield* new ToolFailure({ message: `Unknown agent: ${input.agent}` })
if (agent.mode === "primary")

View file

@ -175,6 +175,9 @@ export const Info = Schema.Struct({
primary_tools: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
description: "Tools that should only be available to primary agents.",
}),
subagent_depth: Schema.optional(NonNegativeInt).annotate({
description: "Maximum subagent nesting depth. Defaults to 1.",
}),
continue_loop_on_deny: Schema.optional(Schema.Boolean).annotate({
description: "Continue the agent loop when a tool call is denied",
}),

View file

@ -77,6 +77,10 @@ export function migrate(info: typeof ConfigV1.Info.Type) {
commands: commands(info.command),
instructions: info.instructions,
references: info.references ?? info.reference,
experimental:
info.experimental?.subagent_depth === undefined
? undefined
: { subagent_depth: info.experimental.subagent_depth },
plugins: info.plugin?.map((plugin) =>
typeof plugin === "string" ? plugin : { package: plugin[0], options: plugin[1] },
),