Compare commits

...

2 Commits

Author SHA1 Message Date
diegosouzapw
fba2148f62 Merge remote-tracking branch 'origin/release/v3.8.51' into fix/v3851-system-prompt-noimplicit 2026-09-02 08:32:26 -03:00
diegosouzapw
cd9675038a fix(types): type system prompt injection safely 2026-09-02 01:24:36 -03:00
2 changed files with 81 additions and 22 deletions

View File

@@ -20,6 +20,14 @@ interface SystemPromptConfig {
prompt: string;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function isSystemMessage(value: unknown): value is Record<string, unknown> {
return isRecord(value) && (value.role === "system" || value.role === "developer");
}
// Typed accessor for globalThis storage — avoids `as any` casts (#2470)
const _store = globalThis as unknown as Record<string, SystemPromptConfig | undefined>;
@@ -74,45 +82,50 @@ export function getSystemPromptConfig() {
* suffixPrompt is appended after existing system content.
* This ensures: prefix → agent instructions → suffix (#2468).
*
* @param {object} body - Request body
* @returns {object} Modified body
* @param body - Request body
* @returns Modified body
*/
export function injectSystemPrompt(body) {
export function injectSystemPrompt<T>(body: T): T {
const cfg = getConfig();
if (!cfg.enabled) return body;
const prefix = cfg.prefixPrompt || "";
const suffix = cfg.suffixPrompt || "";
if (!prefix && !suffix) return body;
if (!body || typeof body !== "object") return body;
if (!isRecord(body)) return body;
if (body._skipSystemPrompt) return body;
const result = { ...body };
const result: Record<string, unknown> = { ...body };
// OpenAI/Claude format (messages[])
if (result.messages && Array.isArray(result.messages)) {
const sysIdx = result.messages.findIndex((m) => m.role === "system" || m.role === "developer");
result.messages = [...result.messages];
const messages: unknown[] = result.messages;
const sysIdx = messages.findIndex(isSystemMessage);
const nextMessages = [...messages];
if (sysIdx >= 0) {
const msg = { ...result.messages[sysIdx] };
if (Array.isArray(msg.content)) {
const content = [...msg.content];
if (prefix) content.unshift({ type: "text", text: prefix });
if (suffix) content.push({ type: "text", text: suffix });
msg.content = content;
} else {
let content = msg.content || "";
if (prefix) content = prefix + "\n\n" + content;
if (suffix) content = content + "\n\n" + suffix;
msg.content = content;
const existingMessage = nextMessages[sysIdx];
if (isRecord(existingMessage)) {
const msg = { ...existingMessage };
if (Array.isArray(msg.content)) {
const content: unknown[] = [...msg.content];
if (prefix) content.unshift({ type: "text", text: prefix });
if (suffix) content.push({ type: "text", text: suffix });
msg.content = content;
} else {
let content = String(msg.content || "");
if (prefix) content = prefix + "\n\n" + content;
if (suffix) content = content + "\n\n" + suffix;
msg.content = content;
}
nextMessages[sysIdx] = msg;
}
result.messages[sysIdx] = msg;
} else {
// No existing system message — combine both into one
const combined = [prefix, suffix].filter(Boolean).join("\n\n");
if (combined) {
result.messages = [{ role: "system", content: combined }, ...result.messages];
nextMessages.unshift({ role: "system", content: combined });
}
}
result.messages = nextMessages;
}
// Claude format (system field)
@@ -123,14 +136,14 @@ export function injectSystemPrompt(body) {
if (suffix) sys = sys + "\n\n" + suffix;
result.system = sys;
} else if (Array.isArray(result.system)) {
let arr = [...result.system];
let arr: unknown[] = [...result.system];
if (prefix) arr = [{ type: "text", text: prefix }, ...arr];
if (suffix) arr = [...arr, { type: "text", text: suffix }];
result.system = arr;
}
}
return result;
return Object.assign({}, body, result);
}
/**

View File

@@ -120,6 +120,52 @@ test("injectSystemPrompt: null body returns as-is", () => {
assert.equal(injectSystemPrompt(null), null);
});
test("injectSystemPrompt: non-object bodies return as-is", () => {
setSystemPromptConfig({ enabled: true, suffixPrompt: "test" });
for (const body of [undefined, "prompt", 42, true]) {
assert.equal(injectSystemPrompt(body), body);
}
});
test("injectSystemPrompt: skips malformed message entries safely", () => {
setSystemPromptConfig({ enabled: true, prefixPrompt: "PRE", suffixPrompt: "SUF" });
const body = {
messages: [
{ role: "user", content: "hi" },
null,
{ role: "system", content: "Original prompt" },
],
};
const result = injectSystemPrompt(body);
assert.equal(result.messages[2].content, "PRE\n\nOriginal prompt\n\nSUF");
assert.equal(result.messages[1], null);
});
test("injectSystemPrompt: does not mutate the request or nested message content", () => {
setSystemPromptConfig({ enabled: true, prefixPrompt: "PRE", suffixPrompt: "SUF" });
const systemContent = [{ type: "text", text: "Original prompt" }];
const systemMessage = { role: "system", content: systemContent };
const body = {
messages: [systemMessage, { role: "user", content: "hi" }],
};
const result = injectSystemPrompt(body);
assert.notEqual(result, body);
assert.notEqual(result.messages, body.messages);
assert.notEqual(result.messages[0], systemMessage);
assert.notEqual(result.messages[0].content, systemContent);
assert.deepEqual(body, {
messages: [
{ role: "system", content: [{ type: "text", text: "Original prompt" }] },
{ role: "user", content: "hi" },
],
});
});
test("injectSystemPrompt: developer role treated as system", () => {
setSystemPromptConfig({ enabled: true, prefixPrompt: "PRE", suffixPrompt: "SUF" });
const body = {