Merge pull request #4084 from pizzav-xyz/feature/keyword-detector-enabled-expansions

feat(keyword-detector): add enabled_expansions config for allowlist control
This commit is contained in:
YeonGyu-Kim
2026-05-21 12:58:18 +09:00
committed by GitHub
5 changed files with 127 additions and 3 deletions
+14
View File
@@ -6026,6 +6026,20 @@
"keyword_detector": {
"type": "object",
"properties": {
"enabled_expansions": {
"type": "array",
"items": {
"type": "string",
"enum": [
"ultrawork",
"search",
"analyze",
"team",
"hyperplan",
"hyperplan-ultrawork"
]
}
},
"disabled_keywords": {
"type": "array",
"items": {
+1
View File
@@ -4,6 +4,7 @@ export const KeywordTypeSchema = z.enum(["ultrawork", "search", "analyze", "team
export type KeywordType = z.infer<typeof KeywordTypeSchema>
export const KeywordDetectorConfigSchema = z.object({
enabled_expansions: z.array(KeywordTypeSchema).optional(),
disabled_keywords: z.array(KeywordTypeSchema).optional(),
})
+11 -2
View File
@@ -34,8 +34,9 @@ export function detectKeywords(
agentName?: string,
modelID?: string,
disabledKeywords?: ReadonlyArray<KeywordType>,
enabledExpansions?: ReadonlyArray<KeywordType>,
): string[] {
return detectKeywordsWithType(text, agentName, modelID, disabledKeywords).map(
return detectKeywordsWithType(text, agentName, modelID, disabledKeywords, enabledExpansions).map(
({ message }) => message,
)
}
@@ -45,6 +46,7 @@ export function detectKeywordsWithType(
agentName?: string,
modelID?: string,
disabledKeywords?: ReadonlyArray<KeywordType>,
enabledExpansions?: ReadonlyArray<KeywordType>,
): DetectedKeyword[] {
const textWithoutCode = removeCodeBlocks(text)
const disabled = new Set<KeywordType>(disabledKeywords ?? [])
@@ -52,12 +54,19 @@ export function detectKeywordsWithType(
if (disabled.has("ultrawork") || disabled.has("hyperplan")) {
disabled.add("hyperplan-ultrawork")
}
// Allowlist: if enabledExpansions is set, only those types fire
const allowlist = enabledExpansions ? new Set<KeywordType>(enabledExpansions) : null
return KEYWORD_DETECTORS.map(({ type, pattern, message }) => ({
matches: pattern.test(textWithoutCode),
type,
message: resolveMessage(message, agentName, modelID),
}))
.filter((result) => result.matches && !disabled.has(result.type))
.filter((result) => {
if (!result.matches) return false
if (allowlist && !allowlist.has(result.type)) return false
if (disabled.has(result.type)) return false
return true
})
.map(({ type, message }) => ({ type, message }))
}
+2 -1
View File
@@ -37,6 +37,7 @@ export function createKeywordDetectorHook(
defaultMode?: DefaultModeConfig,
) {
const disabledKeywords = config?.disabled_keywords
const enabledExpansions = config?.enabled_expansions
function getRuntimeVariant(input: { variant?: string }, message: Record<string, unknown>): string | undefined {
if (typeof message.variant === "string") {
return message.variant
@@ -85,7 +86,7 @@ export function createKeywordDetectorHook(
const cleanText = removeSystemReminders(promptText)
const modelID = input.model?.modelID
let detectedKeywords = detectKeywordsWithType(cleanText, currentAgent, modelID, disabledKeywords)
let detectedKeywords = detectKeywordsWithType(cleanText, currentAgent, modelID, disabledKeywords, enabledExpansions)
detectedKeywords = suppressComboStandalones(detectedKeywords)
if (isPlannerAgent(currentAgent)) {
+99
View File
@@ -181,6 +181,105 @@ describe("keyword-detector message transform", () => {
expect(textPart?.text).toBe(peerText)
expect(textPart?.text).not.toContain("[search-mode]")
})
test("should only fire ultrawork when enabled_expansions is set to [ultrawork]", async () => {
// given - allowlist configured to only enable ultrawork
const collector = new ContextCollector()
const hook = createKeywordDetectorHook(
createMockPluginInput(),
collector,
undefined,
{ enabled_expansions: ["ultrawork"] }
)
const sessionID = "enabled-expansions-ultrawork-only"
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "search for the bug" }],
}
// when - keyword detection runs with enabled_expansions restricting to ultrawork
await hook["chat.message"]({ sessionID }, output)
// then - search should be blocked by allowlist even though it matches
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).toBe("search for the bug") // no search-mode injection
})
test("should fire only allowed expansions from allowlist", async () => {
// given - allowlist configured to only enable analyze
const collector = new ContextCollector()
const hook = createKeywordDetectorHook(
createMockPluginInput(),
collector,
undefined,
{ enabled_expansions: ["analyze"] }
)
const sessionID = "enabled-expansions-analyze-only"
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "investigate the bug" }],
}
// when - keyword detection runs with enabled_expansions restricting to analyze
await hook["chat.message"]({ sessionID }, output)
// then - analyze should fire because it's in the allowlist
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).toContain("[analyze-mode]")
})
test("should block all expansions when enabled_expansions is empty array", async () => {
// given - empty allowlist (effectively disable all)
const collector = new ContextCollector()
const hook = createKeywordDetectorHook(
createMockPluginInput(),
collector,
undefined,
{ enabled_expansions: [] }
)
const sessionID = "enabled-expansions-empty"
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "ultrawork fix the bug" }],
}
// when - keyword detection runs with empty enabled_expansions
await hook["chat.message"]({ sessionID }, output)
// then - ultrawork should not fire
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).toBe("ultrawork fix the bug") // no mode injection
})
test("should allow both allowlist and denylist to coexist", async () => {
// given - allowlist enables ultrawork and search, but denylist also blocks search
const collector = new ContextCollector()
const hook = createKeywordDetectorHook(
createMockPluginInput(),
collector,
undefined,
{ enabled_expansions: ["ultrawork", "search"], disabled_keywords: ["search"] }
)
const sessionID = "enabled-and-disabled-coexist"
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "search for the bug" }],
}
// when - both config fields are set
await hook["chat.message"]({ sessionID }, output)
// then - search blocked by both allowlist (allowed) AND denylist (blocked)
// Actually search is in enabled_expansions so it would fire, but disabled_keywords blocks it
// Wait, let me reconsider: with enabled_expansions=["ultrawork", "search"], search passes the allowlist.
// Then disabled_keywords=["search"] blocks it. So no injection.
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).toBe("search for the bug")
})
})
describe("keyword-detector session filtering", () => {