Merge pull request #3813 from code-yeongyu/fix/agent-name-backslash-sanitize

fix(agent): sanitize backslash/quote from agent name
This commit is contained in:
YeonGyu-Kim
2026-05-06 18:14:39 +09:00
committed by GitHub
7 changed files with 140 additions and 24 deletions
@@ -23,6 +23,12 @@ mock.module("../../shared/connected-providers-cache", () => ({
writeProviderModelsCache: () => {},
updateConnectedProvidersCache: () => {},
}))
mock.module("../../shared/frontmatter", () => ({
parseFrontmatter: () => ({ frontmatter: {}, content: "" }),
}))
mock.module("js-yaml", () => ({
load: () => ({}),
}))
mock.restore()
@@ -2447,6 +2453,69 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => {
expect(task.sessionId).toBeUndefined()
})
test("should sanitize wrapped agent names before task creation and queueing", async () => {
// given
const input = {
description: "Test task",
prompt: "Do something",
agent: "\\hephaestus\\",
parentSessionId: "parent-session",
parentMessageId: "parent-message",
}
// when
const task = await manager.launch(input)
const queueItem = getQueuesByKey(manager).values().next().value?.[0]
// then
expect(task.agent).toBe("hephaestus")
expect(getTaskMap(manager).get(task.id)?.agent).toBe("hephaestus")
// queueItem may be undefined if the queue was immediately processed
if (queueItem) {
expect(queueItem.input.agent).toBe("hephaestus")
}
})
test("should sanitize slash and quote wrapped agent names before task creation and queueing", async () => {
// given
const input = {
description: "Test task",
prompt: "Do something",
agent: "\"/hephaestus/\"",
parentSessionId: "parent-session",
parentMessageId: "parent-message",
}
// when
const task = await manager.launch(input)
const queueItem = getQueuesByKey(manager).values().next().value?.[0]
// then
expect(task.agent).toBe("hephaestus")
expect(getTaskMap(manager).get(task.id)?.agent).toBe("hephaestus")
// queueItem may be undefined if the queue was immediately processed
if (queueItem) {
expect(queueItem.input.agent).toBe("hephaestus")
}
})
test("should reject wrapper-only agent names after sanitization", async () => {
// given
const input = {
description: "Test task",
prompt: "Do something",
agent: "\\\"/'\\\"/",
parentSessionId: "parent-session",
parentMessageId: "parent-message",
}
// when
const result = manager.launch(input)
// then
await expect(result).rejects.toThrow("Agent parameter is required after sanitization")
})
test("should initialize attempt state for a newly launched task", async () => {
// given
const input = {
+6
View File
@@ -383,6 +383,12 @@ export class BackgroundManager {
throw new Error("Agent parameter is required")
}
input = { ...input, agent: input.agent.trim().replace(/^[\\/"']+|[\\/"']+$/g, "").trim() }
if (!input.agent) {
throw new Error("Agent parameter is required after sanitization")
}
const spawnReservation = await this.reserveSubagentSpawn(input.parentSessionId)
try {
+4
View File
@@ -214,6 +214,10 @@ describe("stripAgentListSortPrefix", () => {
it("strips legacy zero-width sort prefixes baked into v3.14.0v3.16.0 sessions", () => {
expect(stripAgentListSortPrefix("\u200B\u200BHephaestus - Deep Agent")).toBe("Hephaestus - Deep Agent")
})
it("strips leading and trailing wrapper characters after sort prefix removal", () => {
expect(stripAgentListSortPrefix("\\Hephaestus - Deep Agent\\")).toBe("Hephaestus - Deep Agent")
})
})
describe("normalizeAgentForPrompt", () => {
+2 -1
View File
@@ -28,13 +28,14 @@ export const AGENT_DISPLAY_NAMES: Record<string, string> = {
const INVISIBLE_AGENT_CHARACTERS_REGEX = /[\u200B\u200C\u200D\uFEFF]/g
const VISIBLE_AGENT_LIST_SORT_PREFIX_REGEX = /^\d+\|/
const AGENT_WRAPPER_CHARS_REGEX = /^[\\/"']+|[\\/"']+$/g
export function stripInvisibleAgentCharacters(agentName: string): string {
return agentName.replace(INVISIBLE_AGENT_CHARACTERS_REGEX, "")
}
export function stripAgentListSortPrefix(agentName: string): string {
return stripInvisibleAgentCharacters(agentName).replace(VISIBLE_AGENT_LIST_SORT_PREFIX_REGEX, "")
return stripInvisibleAgentCharacters(agentName).replace(VISIBLE_AGENT_LIST_SORT_PREFIX_REGEX, "").replace(AGENT_WRAPPER_CHARS_REGEX, "")
}
/**
@@ -1,5 +1,11 @@
/// <reference types="bun-types" />
import { describe, test, expect, mock } from "bun:test"
mock.module("../../shared/frontmatter", () => ({
parseFrontmatter: () => ({ frontmatter: {}, content: "" }),
}))
mock.module("js-yaml", () => ({
load: () => ({}),
}))
import type { BackgroundManager } from "../../features/background-agent"
import type { PluginInput } from "@opencode-ai/plugin"
import { executeBackground } from "./background-executor"
@@ -100,6 +106,35 @@ describe("executeBackground", () => {
expect(launchArgs.fallbackChain).toEqual(fallbackChain)
})
test("sanitizes subagent_type before passing to background manager launch", async () => {
//#given
const wrappedArgs = {
...testArgs,
subagent_type: "\\hephaestus\\",
}
launchMock.mockResolvedValueOnce({
id: "test-task-id",
sessionId: "sub-session",
description: "Test task",
agent: "hephaestus",
status: "pending",
})
//#when
await executeBackground(wrappedArgs, testContext, mockManager, mockClient)
//#then
const latestCall = [...launchMock.mock.calls].pop()
if (!latestCall) {
throw new Error("Expected background manager launch to be called")
}
const launchArgs = latestCall[0]
if (!launchArgs) {
throw new Error("Expected launch arguments")
}
expect(launchArgs.agent).toBe("hephaestus")
})
test("keeps launched background task alive when parent aborts before session id resolves", async () => {
//#given - parent abort after launch should stop waiting, not fail the background task
const abortController = new AbortController()
@@ -8,6 +8,7 @@ import { resolveMessageContext } from "../../features/hook-message-injector"
import { getSessionAgent } from "../../features/claude-code-session-state"
import { getMessageDir } from "./message-dir"
import { getSessionTools } from "../../shared/session-tools-store"
import { sanitizeSubagentType } from "../delegate-task/subagent-discovery"
export async function executeBackground(
args: CallOmoAgentArgs,
@@ -47,7 +48,7 @@ export async function executeBackground(
const task = await manager.launch({
description: args.description,
prompt: args.prompt,
agent: args.subagent_type,
agent: sanitizeSubagentType(args.subagent_type),
parentSessionId: toolContext.sessionID,
parentMessageId: toolContext.messageID,
parentAgent,