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"] 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}
--- ---
` `
+2 -3
View File
@@ -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,
+31 -7
View File
@@ -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", () => {