mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:40:44 +00:00
fix: tighten signed thinking replay sanitization
This commit is contained in:
@@ -418,7 +418,10 @@ export async function sanitizeSessionHistory(params: {
|
||||
: sanitizedImages;
|
||||
const sanitizedToolCalls = sanitizeToolCallInputs(droppedThinking, {
|
||||
allowedToolNames: params.allowedToolNames,
|
||||
preserveImmutableThinkingTurns: policy.validateAnthropicTurns,
|
||||
allowProviderOwnedThinkingReplay:
|
||||
policy.validateAnthropicTurns &&
|
||||
params.provider === "anthropic" &&
|
||||
params.modelApi === "anthropic-messages",
|
||||
});
|
||||
const repairedTools = policy.repairToolUseResultPairing
|
||||
? sanitizeToolUseResultPairing(sanitizedToolCalls, {
|
||||
|
||||
@@ -2,7 +2,10 @@ import type { AgentMessage, StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import { streamSimple } from "@mariozechner/pi-ai";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../../shared/string-coerce.js";
|
||||
import { validateAnthropicTurns, validateGeminiTurns } from "../../pi-embedded-helpers.js";
|
||||
import { sanitizeToolUseResultPairing } from "../../session-transcript-repair.js";
|
||||
import {
|
||||
isRedactedSessionsSpawnAttachment,
|
||||
sanitizeToolUseResultPairing,
|
||||
} from "../../session-transcript-repair.js";
|
||||
import { normalizeToolName } from "../../tool-policy.js";
|
||||
import type { TranscriptPolicy } from "../../transcript-policy.js";
|
||||
|
||||
@@ -251,14 +254,7 @@ function hasUnredactedSessionsSpawnAttachments(block: ReplayToolCallBlock): bool
|
||||
continue;
|
||||
}
|
||||
for (const attachment of attachments) {
|
||||
if (!attachment || typeof attachment !== "object") {
|
||||
continue;
|
||||
}
|
||||
if (!Object.hasOwn(attachment, "content")) {
|
||||
continue;
|
||||
}
|
||||
const content = (attachment as { content?: unknown }).content;
|
||||
if (content !== "__OPENCLAW_REDACTED__") {
|
||||
if (!isRedactedSessionsSpawnAttachment(attachment)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -331,7 +327,7 @@ function resolveReplayToolCallName(
|
||||
function sanitizeReplayToolCallInputs(
|
||||
messages: AgentMessage[],
|
||||
allowedToolNames?: Set<string>,
|
||||
preserveImmutableThinkingTurns?: boolean,
|
||||
allowProviderOwnedThinkingReplay?: boolean,
|
||||
): ReplayToolCallSanitizeReport {
|
||||
let changed = false;
|
||||
let droppedAssistantMessages = 0;
|
||||
@@ -347,7 +343,7 @@ function sanitizeReplayToolCallInputs(
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
preserveImmutableThinkingTurns &&
|
||||
allowProviderOwnedThinkingReplay &&
|
||||
message.content.some((block) => isThinkingLikeReplayBlock(block)) &&
|
||||
message.content.some((block) => isReplayToolCallBlock(block))
|
||||
) {
|
||||
@@ -641,7 +637,8 @@ export function wrapStreamFnSanitizeMalformedToolCalls(
|
||||
const sanitized = sanitizeReplayToolCallInputs(
|
||||
messages as AgentMessage[],
|
||||
allowedToolNames,
|
||||
transcriptPolicy?.validateAnthropicTurns === true,
|
||||
transcriptPolicy?.validateAnthropicTurns === true &&
|
||||
(model as { api?: unknown })?.api === "anthropic-messages",
|
||||
);
|
||||
if (sanitized.messages === messages) {
|
||||
return baseFn(model, context, options);
|
||||
|
||||
@@ -8,6 +8,8 @@ import { extractToolCallsFromAssistant, extractToolResultId } from "./tool-call-
|
||||
|
||||
const TOOL_CALL_NAME_MAX_CHARS = 64;
|
||||
const TOOL_CALL_NAME_RE = /^[A-Za-z0-9_:.-]+$/;
|
||||
const REDACTED_SESSIONS_SPAWN_ATTACHMENT_CONTENT = "__OPENCLAW_REDACTED__";
|
||||
const SESSIONS_SPAWN_ATTACHMENT_METADATA_KEYS = ["name", "encoding", "mimeType"] as const;
|
||||
|
||||
type RawToolCallBlock = {
|
||||
type?: unknown;
|
||||
@@ -94,20 +96,59 @@ function redactSessionsSpawnAttachmentsArgs(value: unknown): unknown {
|
||||
if (!Array.isArray(raw)) {
|
||||
return value;
|
||||
}
|
||||
let changed = false;
|
||||
const next = raw.map((item) => {
|
||||
if (!item || typeof item !== "object") {
|
||||
if (isRedactedSessionsSpawnAttachment(item)) {
|
||||
return item;
|
||||
}
|
||||
const a = item as Record<string, unknown>;
|
||||
if (!Object.hasOwn(a, "content")) {
|
||||
return item;
|
||||
}
|
||||
const { content: _content, ...rest } = a;
|
||||
return { ...rest, content: "__OPENCLAW_REDACTED__" };
|
||||
changed = true;
|
||||
return redactSessionsSpawnAttachment(item);
|
||||
});
|
||||
if (!changed) {
|
||||
return value;
|
||||
}
|
||||
return { ...rec, attachments: next };
|
||||
}
|
||||
|
||||
function redactSessionsSpawnAttachment(item: unknown): Record<string, unknown> {
|
||||
const next: Record<string, unknown> = {
|
||||
content: REDACTED_SESSIONS_SPAWN_ATTACHMENT_CONTENT,
|
||||
};
|
||||
if (!item || typeof item !== "object") {
|
||||
return next;
|
||||
}
|
||||
const attachment = item as Record<string, unknown>;
|
||||
for (const key of SESSIONS_SPAWN_ATTACHMENT_METADATA_KEYS) {
|
||||
const value = attachment[key];
|
||||
if (typeof value === "string" && value.trim().length > 0) {
|
||||
next[key] = value;
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function isRedactedSessionsSpawnAttachment(item: unknown): boolean {
|
||||
if (!item || typeof item !== "object") {
|
||||
return false;
|
||||
}
|
||||
const attachment = item as Record<string, unknown>;
|
||||
if (attachment.content !== REDACTED_SESSIONS_SPAWN_ATTACHMENT_CONTENT) {
|
||||
return false;
|
||||
}
|
||||
for (const key of Object.keys(attachment)) {
|
||||
if (key === "content") {
|
||||
continue;
|
||||
}
|
||||
if (!(SESSIONS_SPAWN_ATTACHMENT_METADATA_KEYS as readonly string[]).includes(key)) {
|
||||
return false;
|
||||
}
|
||||
if (typeof attachment[key] !== "string" || (attachment[key] as string).trim().length === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function sanitizeToolCallBlock(block: RawToolCallBlock): RawToolCallBlock {
|
||||
const rawName = readStringValue(block.name);
|
||||
const trimmedName = rawName?.trim();
|
||||
@@ -232,7 +273,7 @@ export type ToolCallInputRepairReport = {
|
||||
|
||||
export type ToolCallInputRepairOptions = {
|
||||
allowedToolNames?: Iterable<string>;
|
||||
preserveImmutableThinkingTurns?: boolean;
|
||||
allowProviderOwnedThinkingReplay?: boolean;
|
||||
};
|
||||
|
||||
export type ErroredAssistantResultPolicy = "preserve" | "drop";
|
||||
@@ -270,7 +311,7 @@ export function repairToolCallInputs(
|
||||
let changed = false;
|
||||
const out: AgentMessage[] = [];
|
||||
const allowedToolNames = normalizeAllowedToolNames(options?.allowedToolNames);
|
||||
const preserveImmutableThinkingTurns = options?.preserveImmutableThinkingTurns === true;
|
||||
const allowProviderOwnedThinkingReplay = options?.allowProviderOwnedThinkingReplay === true;
|
||||
|
||||
for (const msg of messages) {
|
||||
if (!msg || typeof msg !== "object") {
|
||||
@@ -284,7 +325,7 @@ export function repairToolCallInputs(
|
||||
}
|
||||
|
||||
if (
|
||||
preserveImmutableThinkingTurns &&
|
||||
allowProviderOwnedThinkingReplay &&
|
||||
msg.content.some((block) => isThinkingLikeBlock(block)) &&
|
||||
countRawToolCallBlocks(msg.content) > 0
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user