fix(security): remove post-compaction audit injection message (#28507)

* fix: remove post-compaction audit injection (Layer 3)

Remove the post-compaction read audit that injects fake system messages
into conversations after context compaction. This audit:

- Hardcodes WORKFLOW_AUTO.md (a file that doesn't exist in standard
  workspaces) as a required read after every compaction
- Leaks raw regex syntax (memory\/\d{4}-\d{2}-\d{2}\.md) in
  user-facing warning messages
- Injects messages via enqueueSystemEvent that appear as user-role
  messages, tricking agents into reading attacker-controlled files
- Creates a persistent prompt injection vector (see #27697)

Layer 1 (compaction summary) and Layer 2 (workspace context refresh
from AGENTS.md via post-compaction-context.ts) remain intact and are
sufficient for post-compaction context recovery.

Deleted files:
- src/auto-reply/reply/post-compaction-audit.ts
- src/auto-reply/reply/post-compaction-audit.test.ts

Modified files:
- src/auto-reply/reply/agent-runner.ts (removed imports, audit map,
  flag setting, and Layer 3 audit block)

Fixes #27697, fixes #26851, fixes #20484, fixes #22339, fixes #25600
Relates to #26461

* fix: resolve lint failures from post-compaction audit removal

* Tests: add regression for removed post-compaction audit warnings

---------

Co-authored-by: Wilfred (OpenClaw Agent) <jay@openclaw.dev>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
fuller-stack-dev
2026-02-27 18:15:59 -07:00
committed by GitHub
parent a509154be5
commit 70a4f25ab1
4 changed files with 85 additions and 341 deletions

View File

@@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { SessionEntry } from "../../config/sessions.js";
import { loadSessionStore, saveSessionStore } from "../../config/sessions.js";
import { onAgentEvent } from "../../infra/agent-events.js";
import { peekSystemEvents, resetSystemEventsForTest } from "../../infra/system-events.js";
import type { TemplateContext } from "../templating.js";
import type { FollowupRun, QueueSettings } from "./queue.js";
import { createMockTypingController } from "./test-helpers.js";
@@ -79,6 +80,7 @@ beforeEach(() => {
runCliAgentMock.mockClear();
runWithModelFallbackMock.mockClear();
runtimeErrorMock.mockClear();
resetSystemEventsForTest();
// Default: no provider switch; execute the chosen provider+model.
runWithModelFallbackMock.mockImplementation(
@@ -92,6 +94,7 @@ beforeEach(() => {
afterEach(() => {
vi.useRealTimers();
resetSystemEventsForTest();
});
describe("runReplyAgent onAgentRunStart", () => {
@@ -328,6 +331,8 @@ describe("runReplyAgent auto-compaction token update", () => {
storePath: string;
sessionEntry: Record<string, unknown>;
config?: Record<string, unknown>;
sessionFile?: string;
workspaceDir?: string;
}) {
const typing = createMockTypingController();
const sessionCtx = {
@@ -347,8 +352,8 @@ describe("runReplyAgent auto-compaction token update", () => {
sessionId: "session",
sessionKey: "main",
messageProvider: "whatsapp",
sessionFile: "/tmp/session.jsonl",
workspaceDir: "/tmp",
sessionFile: params.sessionFile ?? "/tmp/session.jsonl",
workspaceDir: params.workspaceDir ?? "/tmp",
config: params.config ?? {},
skillsSnapshot: {},
provider: "anthropic",
@@ -495,6 +500,84 @@ describe("runReplyAgent auto-compaction token update", () => {
// totalTokens should use lastCallUsage (55k), not accumulated (75k)
expect(stored[sessionKey].totalTokens).toBe(55_000);
});
it("does not enqueue legacy post-compaction audit warnings", async () => {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-no-audit-warning-"));
const workspaceDir = path.join(tmp, "workspace");
await fs.mkdir(workspaceDir, { recursive: true });
const sessionFile = path.join(tmp, "session.jsonl");
await fs.writeFile(
sessionFile,
`${JSON.stringify({ type: "message", message: { role: "assistant", content: [] } })}\n`,
"utf-8",
);
const storePath = path.join(tmp, "sessions.json");
const sessionKey = "main";
const sessionEntry = {
sessionId: "session",
updatedAt: Date.now(),
totalTokens: 10_000,
compactionCount: 0,
};
await seedSessionStore({ storePath, sessionKey, entry: sessionEntry });
runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedRunParams) => {
params.onAgentEvent?.({ stream: "compaction", data: { phase: "start" } });
params.onAgentEvent?.({ stream: "compaction", data: { phase: "end", willRetry: false } });
return {
payloads: [{ text: "done" }],
meta: {
agentMeta: {
usage: { input: 11_000, output: 500, total: 11_500 },
lastCallUsage: { input: 10_500, output: 500, total: 11_000 },
compactionCount: 1,
},
},
};
});
const config = {
agents: { defaults: { compaction: { memoryFlush: { enabled: false } } } },
};
const { typing, sessionCtx, resolvedQueue, followupRun } = createBaseRun({
storePath,
sessionEntry,
config,
sessionFile,
workspaceDir,
});
await runReplyAgent({
commandBody: "hello",
followupRun,
queueKey: "main",
resolvedQueue,
shouldSteer: false,
shouldFollowup: false,
isActive: false,
isStreaming: false,
typing,
sessionCtx,
sessionEntry,
sessionStore: { [sessionKey]: sessionEntry },
sessionKey,
storePath,
defaultModel: "anthropic/claude-opus-4-5",
agentCfgContextTokens: 200_000,
resolvedVerboseLevel: "off",
isNewSession: false,
blockStreamingEnabled: false,
resolvedBlockStreamingBreak: "message_end",
shouldInjectGroupIntro: false,
typingMode: "instant",
});
const queuedSystemEvents = peekSystemEvents(sessionKey);
expect(queuedSystemEvents.some((event) => event.includes("Post-Compaction Audit"))).toBe(false);
expect(queuedSystemEvents.some((event) => event.includes("WORKFLOW_AUTO.md"))).toBe(false);
});
});
describe("runReplyAgent block streaming", () => {