From 9791019366fa16dc7067e4b4b4d809dc59f617d8 Mon Sep 17 00:00:00 2001 From: ZeyuFu Date: Sat, 16 May 2026 02:12:37 -0400 Subject: [PATCH] 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 (``) 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 --- src/hooks/prometheus-md-only/constants.ts | 13 +++++++- src/hooks/prometheus-md-only/hook.ts | 5 ++- src/hooks/prometheus-md-only/index.test.ts | 38 ++++++++++++++++++---- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/hooks/prometheus-md-only/constants.ts b/src/hooks/prometheus-md-only/constants.ts index 9434a63ea..a206bcf27 100644 --- a/src/hooks/prometheus-md-only/constants.ts +++ b/src/hooks/prometheus-md-only/constants.ts @@ -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 = `` +export const PLANNING_CONTEXT_CLOSE = `` + 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} + --- ` diff --git a/src/hooks/prometheus-md-only/hook.ts b/src/hooks/prometheus-md-only/hook.ts index 57ee04a3d..e52d7959a 100644 --- a/src/hooks/prometheus-md-only/hook.ts +++ b/src/hooks/prometheus-md-only/hook.ts @@ -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, diff --git a/src/hooks/prometheus-md-only/index.test.ts b/src/hooks/prometheus-md-only/index.test.ts index eeeff6f54..9ae3cde46 100644 --- a/src/hooks/prometheus-md-only/index.test.ts +++ b/src/hooks/prometheus-md-only/index.test.ts @@ -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", () => {