Skip to content

feat(copilot): comprehensive copilot improvements#2285

Open
thesocialdev wants to merge 8 commits intostagefrom
copilot-improvements
Open

feat(copilot): comprehensive copilot improvements#2285
thesocialdev wants to merge 8 commits intostagefrom
copilot-improvements

Conversation

@thesocialdev
Copy link
Collaborator

Summary

  • Layout & UI polish: improved copilot chat panel layout, CSS fixes, responsive design
  • Session persistence: MongoDB-backed copilot sessions with message history and editor report survival across page refreshes
  • Richer claim context: personality names, content model, and topics passed to LLM agent
  • Source handling: Agencia sources (both web search objects and gazette plain-string formats) are persisted as Source documents and appended as a "Fontes" reference list in the report editor
  • Execution store integration: session_id sent to Agencia, execution_id parsed from NDJSON stream and stored in session, query endpoints for executions
  • Code scanning fixes: SSRF validation on Agencia URLs, input sanitization on session queries

Test plan

  • Verify copilot chat opens with correct layout and no CSS artifacts
  • Verify sessions persist across page refreshes (messages + editor report)
  • Verify Agencia web search sources appear as references in the report form
  • Verify Agencia gazette search sources (plain strings) are parsed and appear in the report form
  • Verify execution_id is stored and queryable via GET endpoints
  • Run yarn build-ts — passes
  • Run editor-parse unit tests — 26/26 pass

🤖 Generated with Claude Code

thesocialdev and others added 7 commits February 28, 2026 18:16
…ditor reports

Phase 1 - Layout & UI polish:
- Widen copilot panel from 350px to 50vw with 350px min-width
- Fix CSS bugs (margin-left, word-break, z-index casing, debug border)
- Add toolbar title with i18n, reduce input height, fix flex layout

Phase 2 - Session persistence:
- Add CopilotSession MongoDB schema and CRUD service
- Add session endpoints (GET, POST, POST clear) to controller
- Refactor CopilotChatService to load/persist messages from sessions
- Fix concurrency bug: use local editorReportRef instead of instance variable
- Persist editorReport in session so it survives page refresh
- Refactor frontend API, types, and CopilotDrawer for session-based flow
- Pass dataHash from ClaimReviewEditor to CopilotDrawer

Also includes prior session fixes:
- NDJSON streaming parser for Agencia responses
- GPT-5-mini model upgrade and temperature adjustment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Expand the context stored in copilot sessions with richer claim data:
- All personality names (not just the first)
- Content model type (Speech, Image, Debate, Unattributed)
- Related topics from the claim's content (pulled from Redux store)
- Claim title

Restructure the system prompt with clear markdown sections so the
LLM has full awareness of the claim being fact-checked.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Process sources returned by Agencia (url, title, type) and persist
them as Source documents in MongoDB via SourceService, with md5
deduplication. Normalizes Agencia's `url` field to `href` for
compatibility with the Source schema and editor-parser.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Send session_id to Agencia on POST /invoke so executions are tracked.
Parse the new "started" NDJSON line to extract execution_id and store
it in the CopilotSessionMessage alongside the assistant response.

Add GET endpoints to query Agencia executions by session and by
specific execution ID for future frontend use.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rom user-controlled sources

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…rgery

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
CopilotSourceService now parses both source formats returned by Agencia:
- Objects with url/title/type (web search) — existing behavior
- Plain strings like "Porto Alegre (2024-06-11): https://..." (gazette search) — new

Sources are appended as a numbered "Fontes:" reference list at the end of
the report text, since Agencia doesn't provide the field/textRange metadata
needed for inline editor citations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The GitHub code scanning autofix (32f0583) placed validation statements
outside the method body, causing TypeScript compilation errors. Moved
validateSessionId/validateExecutionId calls inside the try block.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
async getSessionById(
sessionId: string
): Promise<CopilotSessionDocument | null> {
return this.copilotSessionModel.findById(sessionId).exec();

Check failure

Code scanning / CodeQL

Database query built from user-controlled sources High

This query object depends on a
user-provided value
.

Copilot Autofix

AI 2 days ago

General approach: Ensure that user-controlled values used in MongoDB queries are validated and/or explicitly treated as literal identifiers, not arbitrary query objects. For IDs, we can (a) validate that the value is a string representing a valid ObjectId and cast it with Mongoose’s Types.ObjectId, or (b) at least enforce that it is a primitive string and reject any non-string/object-like values. This prevents NoSQL-style operator injection and also avoids unexpected behavior if malformed IDs are supplied.

Best fix here: strengthen getSessionById (the sink CodeQL flags) and also the other methods that take sessionId and use it in findByIdAndUpdate. We can:

  • Import Types from mongoose.
  • Add a small helper method private isValidObjectId(id: any): boolean that uses Types.ObjectId.isValid.
  • In getSessionById, addMessage, and deactivateSession, check that sessionId is a string and a valid ObjectId before hitting the database. If invalid, log a warning and either return null (for getSessionById) or throw an error / return null for the update methods.
  • For the actual query, pass the validated string (or cast ObjectId) into findById/findByIdAndUpdate, which is safe once we’ve ensured it’s a primitive in the right format.

This approach keeps the external API the same (still takes a string ID), keeps behavior for valid IDs unchanged, and adds clear, minimal validation to stop tainted, malformed, or object-like input from becoming a query object.

Concretely, in server/copilot/copilot-session.service.ts:

  • Update the import from mongoose to import { Model, Types } from "mongoose";.

  • Add a private helper isValidObjectId to the CopilotSessionService class.

  • Update getSessionById, addMessage, and deactivateSession to:

    • Check typeof sessionId === "string" and this.isValidObjectId(sessionId).
    • Log a warning and return null (or throw) if invalid.
    • Optionally cast: const objectId = new Types.ObjectId(sessionId); and use that in findById / findByIdAndUpdate.

No other files need code changes for this fix.


Suggested changeset 1
server/copilot/copilot-session.service.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/server/copilot/copilot-session.service.ts b/server/copilot/copilot-session.service.ts
--- a/server/copilot/copilot-session.service.ts
+++ b/server/copilot/copilot-session.service.ts
@@ -1,6 +1,6 @@
 import { Injectable, Logger } from "@nestjs/common";
 import { InjectModel } from "@nestjs/mongoose";
-import { Model } from "mongoose";
+import { Model, Types } from "mongoose";
 import {
     CopilotSession,
     CopilotSessionDocument,
@@ -11,6 +11,13 @@
 export class CopilotSessionService {
     private readonly logger = new Logger("CopilotSessionService");
 
+    /**
+     * Ensures the provided id is a valid MongoDB ObjectId string.
+     */
+    private isValidObjectId(id: unknown): id is string {
+        return typeof id === "string" && Types.ObjectId.isValid(id);
+    }
+
     constructor(
         @InjectModel(CopilotSession.name)
         private copilotSessionModel: Model<CopilotSessionDocument>
@@ -56,16 +63,32 @@
     async getSessionById(
         sessionId: string
     ): Promise<CopilotSessionDocument | null> {
-        return this.copilotSessionModel.findById(sessionId).exec();
+        if (!this.isValidObjectId(sessionId)) {
+            this.logger.warn(
+                `Invalid sessionId provided to getSessionById: ${sessionId}`
+            );
+            return null;
+        }
+
+        const objectId = new Types.ObjectId(sessionId);
+        return this.copilotSessionModel.findById(objectId).exec();
     }
 
     async addMessage(
         sessionId: string,
         message: CopilotSessionMessage
-    ): Promise<CopilotSessionDocument> {
+    ): Promise<CopilotSessionDocument | null> {
+        if (!this.isValidObjectId(sessionId)) {
+            this.logger.warn(
+                `Invalid sessionId provided to addMessage: ${sessionId}`
+            );
+            return null;
+        }
+
+        const objectId = new Types.ObjectId(sessionId);
         return this.copilotSessionModel
             .findByIdAndUpdate(
-                sessionId,
+                objectId,
                 { $push: { messages: message } },
                 { new: true }
             )
@@ -74,10 +90,18 @@
 
     async deactivateSession(
         sessionId: string
-    ): Promise<CopilotSessionDocument> {
+    ): Promise<CopilotSessionDocument | null> {
+        if (!this.isValidObjectId(sessionId)) {
+            this.logger.warn(
+                `Invalid sessionId provided to deactivateSession: ${sessionId}`
+            );
+            return null;
+        }
+
+        const objectId = new Types.ObjectId(sessionId);
         return this.copilotSessionModel
             .findByIdAndUpdate(
-                sessionId,
+                objectId,
                 { isActive: false },
                 { new: true }
             )
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
): Promise<CopilotSessionDocument> {
return this.copilotSessionModel
.findByIdAndUpdate(
sessionId,

Check failure

Code scanning / CodeQL

Database query built from user-controlled sources High

This query object depends on a
user-provided value
.

Copilot Autofix

AI 2 days ago

In general, to fix this kind of issue with MongoDB/Mongoose, ensure that any user-controlled input used in a query is either: (1) strictly validated/typed before use (e.g., must be a string matching an ObjectId pattern), or (2) passed through a safe comparison operator like $eq when used inside a query object so that it cannot be interpreted as a query object itself.

For this codebase, the narrowest fix without changing behavior is to validate sessionId in addMessage and deactivateSession similarly to how getActiveSession validates claimReviewDataHash. Specifically:

  • Check that sessionId is a string.
  • Optionally, log a warning and return null if it is not valid.
  • Use this.copilotSessionModel.findOneAndUpdate({ _id: { $eq: sessionId } }, ...) instead of findByIdAndUpdate(sessionId, ...). This ensures that even if an attacker managed to send a non-literal value, MongoDB will treat it as a literal via $eq, closing off NoSQL injection vectors.

Concretely:

  • In server/copilot/copilot-session.service.ts, modify addMessage to:
    • Validate sessionId’s type.
    • Use findOneAndUpdate with a query { _id: { $eq: sessionId } }.
  • Similarly modify deactivateSession to:
    • Validate sessionId’s type.
    • Use findOneAndUpdate with $eq.
      No new imports are required; we just reuse the existing Logger and copilotSessionModel. The rest of the application code calling addMessage/deactivateSession does not need to change, since the methods’ signatures and return types remain the same.

Suggested changeset 1
server/copilot/copilot-session.service.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/server/copilot/copilot-session.service.ts b/server/copilot/copilot-session.service.ts
--- a/server/copilot/copilot-session.service.ts
+++ b/server/copilot/copilot-session.service.ts
@@ -63,9 +63,17 @@
         sessionId: string,
         message: CopilotSessionMessage
     ): Promise<CopilotSessionDocument> {
+        // Ensure sessionId is treated as a literal value and not a query object
+        if (typeof sessionId !== "string") {
+            this.logger.warn(
+                `Invalid sessionId type in addMessage: ${typeof sessionId}`
+            );
+            return null;
+        }
+
         return this.copilotSessionModel
-            .findByIdAndUpdate(
-                sessionId,
+            .findOneAndUpdate(
+                { _id: { $eq: sessionId } },
                 { $push: { messages: message } },
                 { new: true }
             )
@@ -75,9 +82,17 @@
     async deactivateSession(
         sessionId: string
     ): Promise<CopilotSessionDocument> {
+        // Ensure sessionId is treated as a literal value and not a query object
+        if (typeof sessionId !== "string") {
+            this.logger.warn(
+                `Invalid sessionId type in deactivateSession: ${typeof sessionId}`
+            );
+            return null;
+        }
+
         return this.copilotSessionModel
-            .findByIdAndUpdate(
-                sessionId,
+            .findOneAndUpdate(
+                { _id: { $eq: sessionId } },
                 { isActive: false },
                 { new: true }
             )
EOF
@@ -63,9 +63,17 @@
sessionId: string,
message: CopilotSessionMessage
): Promise<CopilotSessionDocument> {
// Ensure sessionId is treated as a literal value and not a query object
if (typeof sessionId !== "string") {
this.logger.warn(
`Invalid sessionId type in addMessage: ${typeof sessionId}`
);
return null;
}

return this.copilotSessionModel
.findByIdAndUpdate(
sessionId,
.findOneAndUpdate(
{ _id: { $eq: sessionId } },
{ $push: { messages: message } },
{ new: true }
)
@@ -75,9 +82,17 @@
async deactivateSession(
sessionId: string
): Promise<CopilotSessionDocument> {
// Ensure sessionId is treated as a literal value and not a query object
if (typeof sessionId !== "string") {
this.logger.warn(
`Invalid sessionId type in deactivateSession: ${typeof sessionId}`
);
return null;
}

return this.copilotSessionModel
.findByIdAndUpdate(
sessionId,
.findOneAndUpdate(
{ _id: { $eq: sessionId } },
{ isActive: false },
{ new: true }
)
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant