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"]
|
||||
|
||||
/**
|
||||
* 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 = `
|
||||
|
||||
---
|
||||
|
||||
${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.
|
||||
|
||||
@@ -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.
|
||||
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 { 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 { replaceToolArgs } from "../../shared/replace-tool-args"
|
||||
import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive"
|
||||
import { getAgentDisplayName } from "../../shared/agent-display-names"
|
||||
import { getAgentFromSession } from "./agent-resolution"
|
||||
import { isPrometheusAgent } from "./agent-matcher"
|
||||
@@ -27,7 +26,7 @@ export function createPrometheusMdOnlyHook(ctx: PluginInput) {
|
||||
// Inject planning-only warning for task tools called by Prometheus
|
||||
if (TASK_TOOLS.includes(toolName)) {
|
||||
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 })
|
||||
log(`[${HOOK_NAME}] Injected planning warning to ${toolName}`, {
|
||||
sessionID: input.sessionID,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { join } from "node:path"
|
||||
import { tmpdir } from "node:os"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { SYSTEM_DIRECTIVE_PREFIX } from "../../shared/system-directive"
|
||||
import { PLANNING_CONTEXT_OPEN } from "./constants"
|
||||
import { clearSessionAgent, setSessionAgent } from "../../features/claude-code-session-state"
|
||||
// Force stable (JSON) mode for tests that rely on message file storage
|
||||
mock.module("../../shared/opencode-storage-detection", () => ({
|
||||
@@ -411,12 +412,13 @@ describe("prometheus-md-only", () => {
|
||||
// when
|
||||
await hook["tool.execute.before"](input, output)
|
||||
|
||||
// then
|
||||
expect(output.args.prompt).toContain(SYSTEM_DIRECTIVE_PREFIX)
|
||||
// then — XML tag used, not bracket directive (#4036)
|
||||
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")
|
||||
})
|
||||
|
||||
test("should inject planning warning when Prometheus calls task", async () => {
|
||||
test("should inject planning warning when Prometheus calls task (research)", async () => {
|
||||
// given
|
||||
const hook = createPrometheusMdOnlyHook(createMockPluginInput())
|
||||
const input = {
|
||||
@@ -432,7 +434,8 @@ describe("prometheus-md-only", () => {
|
||||
await hook["tool.execute.before"](input, output)
|
||||
|
||||
// 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 () => {
|
||||
@@ -451,7 +454,8 @@ describe("prometheus-md-only", () => {
|
||||
await hook["tool.execute.before"](input, output)
|
||||
|
||||
// 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 () => {
|
||||
@@ -462,7 +466,7 @@ describe("prometheus-md-only", () => {
|
||||
sessionID: TEST_SESSION_ID,
|
||||
callID: "call-1",
|
||||
}
|
||||
const promptWithWarning = `Some prompt ${SYSTEM_DIRECTIVE_PREFIX} already here`
|
||||
const promptWithWarning = `Some prompt ${PLANNING_CONTEXT_OPEN} already here`
|
||||
const output = {
|
||||
args: { prompt: promptWithWarning },
|
||||
}
|
||||
@@ -471,9 +475,29 @@ describe("prometheus-md-only", () => {
|
||||
await hook["tool.execute.before"](input, output)
|
||||
|
||||
// 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)
|
||||
})
|
||||
|
||||
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", () => {
|
||||
|
||||
Reference in New Issue
Block a user