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:
ZeyuFu
2026-05-16 02:12:37 -04:00
parent fcb96841f8
commit 9791019366
3 changed files with 45 additions and 11 deletions
+12 -1
View File
@@ -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}
---
`
+2 -3
View File
@@ -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,
+31 -7
View File
@@ -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", () => {