fix(ai): strip deepseek xml tags from chat history & secure git tools

This commit addresses the issue where DeepSeek's raw XML markup (like <tool_calls> and <think>) was leaking into chat history, causing hallucinations in subsequent turns. It also patches a vulnerability in the git commit tool where arbitrary shell injection was possible.

Additionally, it includes UX copy and color contrast adjustments for the marketing homepage breadcrumbs.
This commit is contained in:
2026-05-14 11:34:42 -07:00
parent 5968b98aa7
commit c51c3c21b3
22 changed files with 4559 additions and 667 deletions

View File

@@ -316,7 +316,13 @@ export async function POST(request: Request) {
const history: ChatMessage[] = rows.reverse().map((r: any) => {
const msg = r.data;
if (msg.role === "assistant" && msg.toolCalls?.length) {
return { ...msg, toolCalls: undefined };
msg.toolCalls = undefined;
}
if (typeof msg.content === "string") {
msg.content = msg.content
.replace(/<tool_calls>[\s\S]*?<\/tool_calls>/g, "")
.replace(/<think>[\s\S]*?<\/think>/g, "")
.trim();
}
return msg;
});

View File

@@ -1,13 +1,11 @@
import { NextRequest, NextResponse } from "next/server";
import { authSession } from "@/lib/auth/session-server";
import { query } from "@/lib/db-postgres";
import {
augmentAtlasMessage,
parseContextRefs,
} from "@/lib/chat-context-refs";
import { augmentAtlasMessage, parseContextRefs } from "@/lib/chat-context-refs";
import { formatCreationKickoffForPrompt } from "@/lib/server/creation-kickoff-prompt";
const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333";
const AGENT_RUNNER_URL =
process.env.AGENT_RUNNER_URL ?? "http://localhost:3333";
const ALLOWED_SCOPES = new Set(["overview", "build"]);
@@ -16,8 +14,13 @@ function normalizeScope(raw: string | null | undefined): "overview" | "build" {
return ALLOWED_SCOPES.has(s) ? (s as "overview" | "build") : "overview";
}
function runnerSessionId(projectId: string, scope: "overview" | "build"): string {
return scope === "overview" ? `atlas_${projectId}` : `atlas_${projectId}__build`;
function runnerSessionId(
projectId: string,
scope: "overview" | "build",
): string {
return scope === "overview"
? `atlas_${projectId}`
: `atlas_${projectId}__build`;
}
// ---------------------------------------------------------------------------
@@ -53,12 +56,15 @@ async function ensureLegacyConversationsTable() {
legacyTableChecked = true;
}
async function loadAtlasHistory(projectId: string, scope: "overview" | "build"): Promise<any[]> {
async function loadAtlasHistory(
projectId: string,
scope: "overview" | "build",
): Promise<any[]> {
try {
await ensureThreadsTable();
const rows = await query<{ messages: any[] }>(
`SELECT messages FROM atlas_chat_threads WHERE project_id = $1 AND scope = $2`,
[projectId, scope]
[projectId, scope],
);
if (rows.length > 0) {
const fromThreads = rows[0]?.messages;
@@ -68,7 +74,7 @@ async function loadAtlasHistory(projectId: string, scope: "overview" | "build"):
await ensureLegacyConversationsTable();
const leg = await query<{ messages: any[] }>(
`SELECT messages FROM atlas_conversations WHERE project_id = $1`,
[projectId]
[projectId],
);
const legacyMsgs = leg[0]?.messages ?? [];
if (Array.isArray(legacyMsgs) && legacyMsgs.length > 0) {
@@ -82,7 +88,11 @@ async function loadAtlasHistory(projectId: string, scope: "overview" | "build"):
}
}
async function saveAtlasHistory(projectId: string, scope: "overview" | "build", messages: any[]): Promise<void> {
async function saveAtlasHistory(
projectId: string,
scope: "overview" | "build",
messages: any[],
): Promise<void> {
try {
await ensureThreadsTable();
await query(
@@ -90,7 +100,7 @@ async function saveAtlasHistory(projectId: string, scope: "overview" | "build",
VALUES ($1, $2, $3::jsonb, NOW())
ON CONFLICT (project_id, scope) DO UPDATE
SET messages = $3::jsonb, updated_at = NOW()`,
[projectId, scope, JSON.stringify(messages)]
[projectId, scope, JSON.stringify(messages)],
);
} catch (e) {
console.error("[atlas-chat] Failed to save history:", e);
@@ -104,7 +114,7 @@ async function savePrd(projectId: string, prdContent: string): Promise<void> {
SET data = data || jsonb_build_object('prd', $2::text, 'stage', 'architecture'),
updated_at = NOW()
WHERE id = $1`,
[projectId, prdContent]
[projectId, prdContent],
);
console.log(`[atlas-chat] PRD saved for project ${projectId}`);
} catch (e) {
@@ -113,9 +123,14 @@ async function savePrd(projectId: string, prdContent: string): Promise<void> {
}
/** Replace the latest user message content so DB/UI never show the internal ref prefix. */
function scrubLastUserMessageContent(history: unknown[], cleanText: string): unknown[] {
function scrubLastUserMessageContent(
history: unknown[],
cleanText: string,
): unknown[] {
if (!Array.isArray(history) || history.length === 0) return history;
const h = history.map(m => (m && typeof m === "object" ? { ...(m as object) } : m));
const h = history.map((m) =>
m && typeof m === "object" ? { ...(m as object) } : m,
);
for (let i = h.length - 1; i >= 0; i--) {
const m = h[i] as { role?: string; content?: string };
if (m?.role === "user" && typeof m.content === "string") {
@@ -132,7 +147,7 @@ function scrubLastUserMessageContent(history: unknown[], cleanText: string): unk
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ projectId: string }> }
{ params }: { params: Promise<{ projectId: string }> },
) {
const session = await authSession();
if (!session?.user?.email) {
@@ -146,7 +161,10 @@ export async function GET(
// Filter to only user/assistant messages (no system prompts) for display
const messages = history
.filter((m: any) => m.role === "user" || m.role === "assistant")
.map((m: any) => ({ role: m.role as "user" | "assistant", content: m.content as string }));
.map((m: any) => ({
role: m.role as "user" | "assistant",
content: m.content as string,
}));
return NextResponse.json({ messages });
}
@@ -157,7 +175,7 @@ export async function GET(
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ projectId: string }> }
{ params }: { params: Promise<{ projectId: string }> },
) {
const session = await authSession();
if (!session?.user?.email) {
@@ -180,9 +198,19 @@ export async function POST(
// Strip tool_call / tool_response messages — replaying them across sessions
// causes Gemini to reject the request with a turn-ordering error.
const rawHistory = await loadAtlasHistory(projectId, scope);
const history = rawHistory.filter((m: any) =>
(m.role === "user" || m.role === "assistant") && m.content
);
const history = rawHistory
.filter(
(m: any) => (m.role === "user" || m.role === "assistant") && m.content,
)
.map((m: any) => {
if (typeof m.content === "string") {
m.content = m.content
.replace(/<tool_calls>[\s\S]*?<\/tool_calls>/g, "")
.replace(/<think>[\s\S]*?<\/think>/g, "")
.trim();
}
return m;
});
// __init__ is a special internal trigger used only when there is no existing history.
// If history already exists, ignore the init request (conversation already started).
@@ -197,11 +225,13 @@ export async function POST(
try {
const rows = await query<{ data: Record<string, unknown> }>(
`SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,
[projectId]
[projectId],
);
const kb =
rows[0]?.data != null
? formatCreationKickoffForPrompt(rows[0].data as Record<string, unknown>)
? formatCreationKickoffForPrompt(
rows[0].data as Record<string, unknown>,
)
: null;
if (kb) {
kickoffPrefix = `[Project kickoff from creation wizard]\n${kb}\n\n`;
@@ -236,7 +266,7 @@ export async function POST(
console.error("[atlas-chat] Agent runner error:", text);
return NextResponse.json(
{ error: "Vibn is unavailable. Please try again." },
{ status: 502 }
{ status: 502 },
);
}
@@ -265,7 +295,7 @@ export async function POST(
console.error("[atlas-chat] Error:", err);
return NextResponse.json(
{ error: "Request timed out or failed. Please try again." },
{ status: 500 }
{ status: 500 },
);
}
}
@@ -276,7 +306,7 @@ export async function POST(
export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ projectId: string }> }
{ params }: { params: Promise<{ projectId: string }> },
) {
const session = await authSession();
if (!session?.user?.email) {
@@ -288,21 +318,32 @@ export async function DELETE(
const sessionId = runnerSessionId(projectId, scope);
try {
await fetch(`${AGENT_RUNNER_URL}/atlas/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
} catch { /* runner may be down */ }
await fetch(
`${AGENT_RUNNER_URL}/atlas/sessions/${encodeURIComponent(sessionId)}`,
{ method: "DELETE" },
);
} catch {
/* runner may be down */
}
try {
await ensureThreadsTable();
await query(
`DELETE FROM atlas_chat_threads WHERE project_id = $1 AND scope = $2`,
[projectId, scope]
[projectId, scope],
);
} catch { /* table may not exist yet */ }
} catch {
/* table may not exist yet */
}
if (scope === "overview") {
try {
await query(`DELETE FROM atlas_conversations WHERE project_id = $1`, [projectId]);
} catch { /* legacy */ }
await query(`DELETE FROM atlas_conversations WHERE project_id = $1`, [
projectId,
]);
} catch {
/* legacy */
}
}
return NextResponse.json({ cleared: true });