Skip to content

Commit eae25df

Browse files
committed
fix(webui): redact tool-title inputs before truncation
1 parent 3560594 commit eae25df

2 files changed

Lines changed: 27 additions & 3 deletions

File tree

src/gateway/chat-tool-titles.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,29 @@ describe("generateToolCallTitles", () => {
110110
expect(call.context.messages[0]?.content).not.toContain(token);
111111
});
112112

113+
it("redacts secrets that straddle the prompt truncation boundary", async () => {
114+
mockPreparedModel();
115+
mockCompletionTitles({ "0": "Pushed with credentials" });
116+
const token = ["ghp", "a1b2c3d4e5f6a1b2c3d4e5f6"].join("_");
117+
// Place the secret so a slice-before-redact would cut it mid-token and
118+
// leave an unmatchable fragment in the prompt.
119+
const padding = "x".repeat(1_990);
120+
const input = `${padding} Authorization: Bearer ${token}`;
121+
122+
await generateToolCallTitles({
123+
cfg: {} satisfies OpenClawConfig,
124+
agentId: AGENT_ID,
125+
items: [{ id: "item-1", name: "bash", input }],
126+
});
127+
128+
const call = completeWithPreparedSimpleCompletionModel.mock.calls[0]?.[0] as {
129+
context: { messages: Array<{ content: string }> };
130+
};
131+
const content = call.context.messages[0]?.content ?? "";
132+
expect(content).not.toContain(token);
133+
expect(content).not.toContain(token.slice(0, 12));
134+
});
135+
113136
it("serves repeated items from the SQLite cache without a second completion", async () => {
114137
mockPreparedModel();
115138
mockCompletionTitles({ "0": "Checked repo status" });

src/gateway/chat-tool-titles.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,10 @@ function normalizeItems(items: readonly ToolTitleRequestItem[]): ToolTitleReques
7171
const name = item.name.trim();
7272
// Redact before anything downstream (cache key + model prompt): transcript
7373
// args can carry raw tokens/signed URLs, and titles must not become a new
74-
// secret egress path. Redaction runs in tools mode regardless of logging
75-
// config.
76-
const input = redactToolPayloadText(item.input.slice(0, TOOL_TITLE_INPUT_MAX_CHARS));
74+
// secret egress path. Redaction runs on the full schema-bounded input and
75+
// only then truncates — slicing first could bisect a secret so its
76+
// fragment no longer matches any redaction pattern.
77+
const input = redactToolPayloadText(item.input).slice(0, TOOL_TITLE_INPUT_MAX_CHARS);
7778
if (!id || !name || !input.trim() || seen.has(id)) {
7879
continue;
7980
}

0 commit comments

Comments
 (0)