fix(prometheus-md-only): replace SYSTEM DIRECTIVE marker with XML tag in external prompts (#4036)
PLANNING_CONSULT_WARNING was prepended to the prompt forwarded to subagent LLMs via task(). Its leading bracket-enclosed marker `[SYSTEM DIRECTIVE: OH-MY-OPENCODE - PROMETHEUS READ-ONLY]` is exactly the indirect-prompt-injection signature that Azure OpenAI Prompt Shield flags in user-role content; on GPT-5.4 through Azure, the model returns "I'm sorry, but I cannot assist with that request." before any planning work runs, making Prometheus non-functional on Azure. The bracket marker was designed for internal hook-to-hook filtering, but PLANNING_CONSULT_WARNING leaks it to external LLM payloads. Replace the header with a neutral XML-tag wrapper (`<planning-context source="prometheus-read-only">`) that Azure's filter does not match while preserving the human-readable warning body. Internal isSystemDirective() consumers are unaffected. Regression test asserts the post-hook task() prompt does not begin with the flagged bracket sequence. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,11 +11,20 @@ export const ALLOWED_PATH_PREFIX = ".omo"
|
|||||||
|
|
||||||
export const BLOCKED_TOOLS = ["Write", "Edit", "write", "edit"]
|
export const BLOCKED_TOOLS = ["Write", "Edit", "write", "edit"]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XML-tag wrapper used to mark the planning-context boundary in prompts
|
||||||
|
* forwarded to external LLMs via task(). This format intentionally avoids
|
||||||
|
* the `[SYSTEM DIRECTIVE: ...]` bracket syntax that Azure OpenAI Prompt Shield
|
||||||
|
* flags as indirect prompt injection in user-role content (#4036).
|
||||||
|
*/
|
||||||
|
export const PLANNING_CONTEXT_OPEN = `<planning-context source="prometheus-read-only">`
|
||||||
|
export const PLANNING_CONTEXT_CLOSE = `</planning-context>`
|
||||||
|
|
||||||
export const PLANNING_CONSULT_WARNING = `
|
export const PLANNING_CONSULT_WARNING = `
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
${createSystemDirective(SystemDirectiveTypes.PROMETHEUS_READ_ONLY)}
|
${PLANNING_CONTEXT_OPEN}
|
||||||
|
|
||||||
You are being invoked by ${getAgentDisplayName("prometheus")}, a planning agent restricted to .omo/*.md plan files only.
|
You are being invoked by ${getAgentDisplayName("prometheus")}, a planning agent restricted to .omo/*.md plan files only.
|
||||||
|
|
||||||
@@ -28,6 +37,8 @@ You are being invoked by ${getAgentDisplayName("prometheus")}, a planning agent
|
|||||||
**YOUR ROLE**: Provide consultation, research, and analysis to assist with planning.
|
**YOUR ROLE**: Provide consultation, research, and analysis to assist with planning.
|
||||||
Return your findings and recommendations. The actual implementation will be handled separately after planning is complete.
|
Return your findings and recommendations. The actual implementation will be handled separately after planning is complete.
|
||||||
|
|
||||||
|
${PLANNING_CONTEXT_CLOSE}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import { HOOK_NAME, BLOCKED_TOOLS, PLANNING_CONSULT_WARNING, PROMETHEUS_WORKFLOW_REMINDER } from "./constants"
|
import { HOOK_NAME, BLOCKED_TOOLS, PLANNING_CONSULT_WARNING, PLANNING_CONTEXT_OPEN, PROMETHEUS_WORKFLOW_REMINDER } from "./constants"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
import { replaceToolArgs } from "../../shared/replace-tool-args"
|
import { replaceToolArgs } from "../../shared/replace-tool-args"
|
||||||
import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive"
|
|
||||||
import { getAgentDisplayName } from "../../shared/agent-display-names"
|
import { getAgentDisplayName } from "../../shared/agent-display-names"
|
||||||
import { getAgentFromSession } from "./agent-resolution"
|
import { getAgentFromSession } from "./agent-resolution"
|
||||||
import { isPrometheusAgent } from "./agent-matcher"
|
import { isPrometheusAgent } from "./agent-matcher"
|
||||||
@@ -27,7 +26,7 @@ export function createPrometheusMdOnlyHook(ctx: PluginInput) {
|
|||||||
// Inject planning-only warning for task tools called by Prometheus
|
// Inject planning-only warning for task tools called by Prometheus
|
||||||
if (TASK_TOOLS.includes(toolName)) {
|
if (TASK_TOOLS.includes(toolName)) {
|
||||||
const prompt = output.args.prompt as string | undefined
|
const prompt = output.args.prompt as string | undefined
|
||||||
if (prompt && !prompt.includes(SYSTEM_DIRECTIVE_PREFIX)) {
|
if (prompt && !prompt.includes(PLANNING_CONTEXT_OPEN)) {
|
||||||
replaceToolArgs(output, { prompt: PLANNING_CONSULT_WARNING + prompt })
|
replaceToolArgs(output, { prompt: PLANNING_CONSULT_WARNING + prompt })
|
||||||
log(`[${HOOK_NAME}] Injected planning warning to ${toolName}`, {
|
log(`[${HOOK_NAME}] Injected planning warning to ${toolName}`, {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { join } from "node:path"
|
|||||||
import { tmpdir } from "node:os"
|
import { tmpdir } from "node:os"
|
||||||
import { randomUUID } from "node:crypto"
|
import { randomUUID } from "node:crypto"
|
||||||
import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive"
|
import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive"
|
||||||
|
import { PLANNING_CONTEXT_OPEN } from "./constants"
|
||||||
import { clearSessionAgent, setSessionAgent } from "../../features/claude-code-session-state"
|
import { clearSessionAgent, setSessionAgent } from "../../features/claude-code-session-state"
|
||||||
// Force stable (JSON) mode for tests that rely on message file storage
|
// Force stable (JSON) mode for tests that rely on message file storage
|
||||||
mock.module("../../shared/opencode-storage-detection", () => ({
|
mock.module("../../shared/opencode-storage-detection", () => ({
|
||||||
@@ -411,12 +412,13 @@ describe("prometheus-md-only", () => {
|
|||||||
// when
|
// when
|
||||||
await hook["tool.execute.before"](input, output)
|
await hook["tool.execute.before"](input, output)
|
||||||
|
|
||||||
// then
|
// then — XML tag used, not bracket directive (#4036)
|
||||||
expect(output.args.prompt).toContain(SYSTEM_DIRECTIVE_PREFIX)
|
expect(output.args.prompt).toContain(PLANNING_CONTEXT_OPEN)
|
||||||
|
expect(output.args.prompt).not.toContain("[SYSTEM DIRECTIVE:")
|
||||||
expect(output.args.prompt).toContain("DO NOT modify any files")
|
expect(output.args.prompt).toContain("DO NOT modify any files")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should inject planning warning when Prometheus calls task", async () => {
|
test("should inject planning warning when Prometheus calls task (research)", async () => {
|
||||||
// given
|
// given
|
||||||
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
|
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
|
||||||
const input = {
|
const input = {
|
||||||
@@ -432,7 +434,8 @@ describe("prometheus-md-only", () => {
|
|||||||
await hook["tool.execute.before"](input, output)
|
await hook["tool.execute.before"](input, output)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(output.args.prompt).toContain(SYSTEM_DIRECTIVE_PREFIX)
|
expect(output.args.prompt).toContain(PLANNING_CONTEXT_OPEN)
|
||||||
|
expect(output.args.prompt).not.toContain("[SYSTEM DIRECTIVE:")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should inject planning warning when Prometheus calls call_omo_agent", async () => {
|
test("should inject planning warning when Prometheus calls call_omo_agent", async () => {
|
||||||
@@ -451,7 +454,8 @@ describe("prometheus-md-only", () => {
|
|||||||
await hook["tool.execute.before"](input, output)
|
await hook["tool.execute.before"](input, output)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(output.args.prompt).toContain(SYSTEM_DIRECTIVE_PREFIX)
|
expect(output.args.prompt).toContain(PLANNING_CONTEXT_OPEN)
|
||||||
|
expect(output.args.prompt).not.toContain("[SYSTEM DIRECTIVE:")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should not double-inject warning if already present", async () => {
|
test("should not double-inject warning if already present", async () => {
|
||||||
@@ -462,7 +466,7 @@ describe("prometheus-md-only", () => {
|
|||||||
sessionID: TEST_SESSION_ID,
|
sessionID: TEST_SESSION_ID,
|
||||||
callID: "call-1",
|
callID: "call-1",
|
||||||
}
|
}
|
||||||
const promptWithWarning = `Some prompt ${SYSTEM_DIRECTIVE_PREFIX} already here`
|
const promptWithWarning = `Some prompt ${PLANNING_CONTEXT_OPEN} already here`
|
||||||
const output = {
|
const output = {
|
||||||
args: { prompt: promptWithWarning },
|
args: { prompt: promptWithWarning },
|
||||||
}
|
}
|
||||||
@@ -471,9 +475,29 @@ describe("prometheus-md-only", () => {
|
|||||||
await hook["tool.execute.before"](input, output)
|
await hook["tool.execute.before"](input, output)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const occurrences = (output.args.prompt as string).split(SYSTEM_DIRECTIVE_PREFIX).length - 1
|
const occurrences = (output.args.prompt as string).split(PLANNING_CONTEXT_OPEN).length - 1
|
||||||
expect(occurrences).toBe(1)
|
expect(occurrences).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("regression #4036: task() prompt must not contain [SYSTEM DIRECTIVE: for Azure Prompt Shield safety", async () => {
|
||||||
|
// given
|
||||||
|
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
|
||||||
|
const input = {
|
||||||
|
tool: "task",
|
||||||
|
sessionID: TEST_SESSION_ID,
|
||||||
|
callID: "call-1",
|
||||||
|
}
|
||||||
|
const output = {
|
||||||
|
args: { prompt: "Analyze the codebase architecture" },
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
await hook["tool.execute.before"](input, output)
|
||||||
|
|
||||||
|
// then — the bracket-enclosed directive must NOT appear in the forwarded prompt
|
||||||
|
// because Azure OpenAI Prompt Shield flags it as indirect prompt injection
|
||||||
|
expect(output.args.prompt as string).not.toContain("[SYSTEM DIRECTIVE:")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("with non-Prometheus agent in message storage", () => {
|
describe("with non-Prometheus agent in message storage", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user