Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions .env.example

This file was deleted.

43 changes: 0 additions & 43 deletions backend/config/config.prod.sample.yml

This file was deleted.

11 changes: 9 additions & 2 deletions backend/controllers/debatevsbot_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type DebateRequest struct {
BotLevel string `json:"botLevel" binding:"required"`
Topic string `json:"topic" binding:"required"`
Stance string `json:"stance" binding:"required"`
Role string `json:"role"`
History []models.Message `json:"history"`
PhaseTimings []PhaseTiming `json:"phaseTimings"`
Context string `json:"context"`
Expand All @@ -43,6 +44,7 @@ type DebateResponse struct {
BotLevel string `json:"botLevel"`
Topic string `json:"topic"`
Stance string `json:"stance"`
Role string `json:"role"`
PhaseTimings []models.PhaseTiming `json:"phaseTimings,omitempty"` // Backend format
}

Expand All @@ -52,6 +54,7 @@ type DebateMessageResponse struct {
BotLevel string `json:"botLevel"`
Topic string `json:"topic"`
Stance string `json:"stance"`
Role string `json:"role"`
Response string `json:"response"`
}

Expand Down Expand Up @@ -97,6 +100,7 @@ func CreateDebate(c *gin.Context) {
BotLevel: req.BotLevel,
Topic: req.Topic,
Stance: req.Stance,
Role: req.Role,
History: req.History,
PhaseTimings: backendPhaseTimings,
CreatedAt: time.Now().Unix(),
Expand All @@ -114,6 +118,7 @@ func CreateDebate(c *gin.Context) {
BotLevel: req.BotLevel,
Topic: req.Topic,
Stance: req.Stance,
Role: req.Role,
PhaseTimings: backendPhaseTimings,
}
c.JSON(200, response)
Expand All @@ -139,8 +144,8 @@ func SendDebateMessage(c *gin.Context) {
return
}

// Generate bot response with the additional context field.
botResponse := services.GenerateBotResponse(req.BotName, req.BotLevel, req.Topic, req.History, req.Stance, req.Context, 150)
// Generate bot response with the additional context and role field.
botResponse := services.GenerateBotResponse(req.BotName, req.BotLevel, req.Topic, req.History, req.Stance, req.Context, req.Role, 150)

// Update debate history with the bot's response.
updatedHistory := append(req.History, models.Message{
Expand All @@ -155,6 +160,7 @@ func SendDebateMessage(c *gin.Context) {
BotLevel: req.BotLevel,
Topic: req.Topic,
Stance: req.Stance,
Role: req.Role,
History: updatedHistory,
CreatedAt: time.Now().Unix(),
}
Expand All @@ -172,6 +178,7 @@ func SendDebateMessage(c *gin.Context) {
BotLevel: req.BotLevel,
Topic: req.Topic,
Stance: req.Stance,
Role: req.Role,
Response: botResponse,
}
c.JSON(200, response)
Expand Down
2 changes: 2 additions & 0 deletions backend/models/debate.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type Debate struct {
PreRD float64 `bson:"preRD" json:"preRD"`
PostRating float64 `bson:"postRating" json:"postRating"`
PostRD float64 `bson:"postRD" json:"postRD"`
Role string `bson:"role" json:"role"` // User's role
OpponentRole string `bson:"opponentRole" json:"opponentRole"` // Opponent's role
Date time.Time `bson:"date" json:"date"`
}

Expand Down
1 change: 1 addition & 0 deletions backend/models/debatevsbot.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ type DebateVsBot struct {
History []Message `json:"history" bson:"history"`
PhaseTimings []PhaseTiming `json:"phaseTimings" bson:"phaseTimings"` // Added for custom timings
Outcome string `json:"outcome" bson:"outcome"` // Result of the debate (e.g., "User wins")
Role string `json:"role" bson:"role"` // User's role (historian, scientist, lawyer, neutral)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's find the backend/models/debatevsbot.go file and examine the struct

echo "=== Checking DebateVsBot struct ==="
cat -n backend/models/debatevsbot.go | head -50

echo -e "\n=== Searching for DebateVsBot creation patterns ==="
ast-grep --pattern $'DebateVsBot{
  $$$
}'

echo -e "\n=== Searching for Role field usage ==="
rg -nP --type=go '\bRole\b' -A2 -B2

echo -e "\n=== Searching for DebateVsBot initialization ==="
rg -nP --type=go 'new\(|DebateVsBot\s*\{|&DebateVsBot' -A5

Repository: AOSSIE-Org/DebateAI

Length of output: 20321


🏁 Script executed:

# Check if there's any validation logic for Role field in controllers or services
rg -nP --type=go 'if.*Role|Role.*!=' -A2 -B2 | head -40

# Check the switch statement in debatevsbot.go service to see all handled cases
rg -nP --type=go 'switch.*role|case.*historian|case.*scientist|case.*lawyer|case.*neutral' -A1 -B1

Repository: AOSSIE-Org/DebateAI

Length of output: 2946


🏁 Script executed:

# Check the full switch statement in debatevsbot.go service to see if "neutral" case exists
sed -n '173,195p' backend/services/debatevsbot.go

# Also check if there's validation elsewhere in the debatevsbot controller
rg -nP --type=go 'CreateDebateVsBot|UpdateDebateVsBot' -A30 | grep -i "valid\|error\|role"

Repository: AOSSIE-Org/DebateAI

Length of output: 1760


🏁 Script executed:

# Check if "neutral" is explicitly handled anywhere
rg -nP --type=go '"neutral"' | head -20

# Get full context of the switch statement and what happens after it
sed -n '173,200p' backend/services/debatevsbot.go

Repository: AOSSIE-Org/DebateAI

Length of output: 1477


Add validation for Role field or implement default "neutral" handling.

The Role field is properly tagged for serialization, but the code lacks validation and doesn't implement the documented "neutral" role. The switch statement in backend/services/debatevsbot.go (lines 173–184) handles only historian, scientist, and lawyer—if Role is empty or invalid, roleInstruction remains empty, producing degraded prompt instructions.

Recommend either:

  • Validating Role values in the controller and rejecting invalid requests, or
  • Defaulting empty/missing Role to "neutral" and adding the corresponding case to the switch statement for backward compatibility with existing debates.
🤖 Prompt for AI Agents
In @backend/models/debatevsbot.go at line 30, The Role field on the DebateVsBot
model can be empty/invalid and roleInstruction in the switch inside
backend/services/debatevsbot.go (the logic handling historian/scientist/lawyer)
ends up empty; either validate Role on incoming requests (reject if not one of
"historian","scientist","lawyer","neutral") in the controller that constructs
the model, or make Role default to "neutral" when missing and add a "neutral"
case to the switch that sets a proper neutral roleInstruction so prompts never
get an empty roleInstruction.

CreatedAt int64 `json:"createdAt" bson:"createdAt"`
}
1 change: 1 addition & 0 deletions backend/routes/rooms.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type Participant struct {
Elo int `json:"elo" bson:"elo"`
AvatarURL string `json:"avatarUrl" bson:"avatarUrl,omitempty"`
Email string `json:"email" bson:"email,omitempty"`
Role string `json:"role" bson:"role,omitempty"` // historian, scientist, lawyer, neutral
}

// generateRoomID creates a random six-digit room ID as a string.
Expand Down
23 changes: 19 additions & 4 deletions backend/services/debatevsbot.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func inferOpponentStyle(message string) string {
// constructPrompt builds a prompt that adjusts based on bot personality, debate topic, history,
// extra context, and uses the provided stance directly. It includes phase-specific instructions
// and leverages InteractionModifiers and PhilosophicalTenets for tailored responses.
func constructPrompt(bot BotPersonality, topic string, history []models.Message, stance, extraContext string, maxWords int) string {
func constructPrompt(bot BotPersonality, topic string, history []models.Message, stance, extraContext, role string, maxWords int) string {
// Level-based instructions
levelInstructions := ""
switch strings.ToLower(bot.Level) {
Expand Down Expand Up @@ -169,6 +169,17 @@ Your responses must reflect this persona consistently, as if you are the charact
limitInstruction = fmt.Sprintf("Limit your response to %d words.", maxWords)
}

// Role-based instructions
roleInstruction := ""
switch strings.ToLower(role) {
case "historian":
roleInstruction = "Act as a Historian: use historical examples, timelines, and precedents to support your arguments."
case "scientist":
roleInstruction = "Act as a Scientist: use empirical evidence, data, and experimental results to support your arguments."
case "lawyer":
roleInstruction = "Act as a Lawyer: use persuasion, airtight logic, and sharp rebuttals to support your arguments."
}

// Base instruction for all responses
baseInstruction := "Provide only your own argument without simulating an opponent’s dialogue. " +
"If the user’s input is unclear, off-topic, or empty, respond with a personality-appropriate clarification request, e.g., for Yoda: 'Clouded, your point is, young one. Clarify, you must.'"
Expand All @@ -182,17 +193,19 @@ Your debating style must strictly adhere to the following guidelines:
- Level Instructions: %s
- Personality Instructions: %s
- Interaction Modifier: %s
- Role Instructions: %s
Your stance is: %s.
%s
%s
%s
Provide an opening statement that embodies your persona and stance.
[Your opening argument]
%s %s`,
%s`,
bot.Name, bot.Level, stance, topic,
levelInstructions,
personalityInstructions,
modifierInstruction,
roleInstruction,
stance,
func() string {
if extraContext != "" {
Expand Down Expand Up @@ -237,6 +250,7 @@ Your debating style must strictly adhere to the following guidelines:
- Level Instructions: %s
- Personality Instructions: %s
- Interaction Modifier: %s
- Role Instructions: %s
Your stance is: %s.
%s
%s
Expand All @@ -250,6 +264,7 @@ Please provide your full argument.`,
levelInstructions,
personalityInstructions,
modifierInstruction,
roleInstruction,
stance,
func() string {
if extraContext != "" {
Expand All @@ -267,14 +282,14 @@ Please provide your full argument.`,

// GenerateBotResponse generates a response from the debate bot using the Gemini client library.
// It uses the bot’s personality to handle errors and responses vividly.
func GenerateBotResponse(botName, botLevel, topic string, history []models.Message, stance, extraContext string, maxWords int) string {
func GenerateBotResponse(botName, botLevel, topic string, history []models.Message, stance, extraContext, role string, maxWords int) string {
if geminiClient == nil {
return personalityErrorResponse(botName, "My systems are offline, it seems.")
}

bot := GetBotPersonality(botName)
// Construct prompt with enhanced personality integration
prompt := constructPrompt(bot, topic, history, stance, extraContext, maxWords)
prompt := constructPrompt(bot, topic, history, stance, extraContext, role, maxWords)

ctx := context.Background()
response, err := generateDefaultModelText(ctx, prompt)
Expand Down
4 changes: 1 addition & 3 deletions backend/services/matchmaking.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,7 @@ func (ms *MatchmakingService) RemoveFromPool(userID string) {
ms.mutex.Lock()
defer ms.mutex.Unlock()

if _, exists := ms.pool[userID]; exists {
delete(ms.pool, userID)
}
delete(ms.pool, userID)
}

// UpdateActivity updates the last activity time for a user
Expand Down
6 changes: 3 additions & 3 deletions backend/structs/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package structs

type SignUpRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
Password string `json:"password" binding:"required,min=6"`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n backend/structs/auth.go

Repository: AOSSIE-Org/DebateAI

Length of output: 1043


🌐 Web query:

NIST SP 800-63B password minimum length requirements user authentication

💡 Result:

Short answer:

  • For memorized secrets (passwords): NIST SP 800-63B requires a minimum of 8 characters, and for single‑factor password authentication it mandates a minimum of 15 characters; verifiers SHOULD permit a maximum of at least 64 characters and SHOULD accept ASCII (including space) and Unicode. [1]. (pages.nist.gov)

If you want the exact text/section references, I can pull and quote them.

Citations:


🌐 Web query:

OWASP password minimum length requirements recommendations

💡 Result:

Short summary of OWASP recommendations for password minimum length and related rules:

  • Minimum length: OWASP AASVS recommends at least 12 characters for user-set passwords. [1] (owasp-aasvs4.readthedocs.io)
  • OWASP Authentication Cheat Sheet references NIST SP 800-63B guidance: with MFA, passwords shorter than 8 chars are weak; without MFA, passwords shorter than 15 chars are weak. (Use this to set minimums appropriate to your threat model.) [2] (cheatsheetseries.owasp.org)
  • Related guidance: allow long passphrases (typical max ≥64–128 chars), permit spaces and Unicode, do not silently truncate, and check passwords against breached/commonly-used lists. [3] (owasp.deteact.com)

If you want, I can produce a short policy (minimum + max + checks + UX text) based on these recommendations.

Citations:


Increase minimum password length to at least 12 characters.

The current 6-character minimum is below security best practices. NIST SP 800-63B recommends a minimum of 8 characters for memorized secrets, or 15 characters for single-factor password authentication without MFA. OWASP AASVS explicitly recommends a minimum of 12 characters for user-set passwords. A 6-character password significantly reduces the keyspace and increases vulnerability to brute-force attacks.

This applies to all password fields: SignUpRequest.Password (line 5), LoginRequest.Password (line 15), and VerifyForgotPasswordRequest.NewPassword (line 25).

🤖 Prompt for AI Agents
In @backend/structs/auth.go at line 5, Update the password validation minimum
from 6 to 12 for all affected request structs: change the struct tags on
SignUpRequest.Password, LoginRequest.Password, and
VerifyForgotPasswordRequest.NewPassword to use `binding:"required,min=12"`, and
update any corresponding manual validation logic or tests that assert the old
minimum so they reflect the new 12-character requirement. Ensure any error
messages returned to clients reference the new minimum length.

}

type VerifyEmailRequest struct {
Expand All @@ -12,7 +12,7 @@ type VerifyEmailRequest struct {

type LoginRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
Password string `json:"password" binding:"required,min=6"`
}

type ForgotPasswordRequest struct {
Expand All @@ -22,5 +22,5 @@ type ForgotPasswordRequest struct {
type VerifyForgotPasswordRequest struct {
Email string `json:"email" binding:"required,email"`
Code string `json:"code" binding:"required"`
NewPassword string `json:"newPassword" binding:"required,min=8"`
NewPassword string `json:"newPassword" binding:"required,min=6"`
}
4 changes: 2 additions & 2 deletions backend/test_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func main() {
// Check pool - should have 2 users now
pool = ms.GetPool()

for _, user := range pool {
for range pool {
}

// Wait a bit for matching
Expand All @@ -44,7 +44,7 @@ func main() {
// Check pool after matching
pool = ms.GetPool()

for _, user := range pool {
for range pool {
}

}
35 changes: 31 additions & 4 deletions backend/websocket/websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type Client struct {
LastActivity time.Time
IsMuted bool // New field to track mute status
Role string // New field to track debate role (for/against)
PersonaRole string // New field to track persona role (historian, etc.)
SpeechText string // New field to store speech text
ConnectionID string
}
Expand Down Expand Up @@ -82,10 +83,11 @@ type Message struct {
Timestamp int64 `json:"timestamp,omitempty"`
Mode string `json:"mode,omitempty"` // 'type' or 'speak'
// Debate-specific fields
Phase string `json:"phase,omitempty"`
Topic string `json:"topic,omitempty"`
Role string `json:"role,omitempty"`
Ready *bool `json:"ready,omitempty"`
Phase string `json:"phase,omitempty"`
Topic string `json:"topic,omitempty"`
Role string `json:"role,omitempty"`
Persona string `json:"persona,omitempty"`
Ready *bool `json:"ready,omitempty"`
// New fields for automatic muting
IsMuted bool `json:"isMuted,omitempty"`
CurrentTurn string `json:"currentTurn,omitempty"` // "for" or "against"
Expand Down Expand Up @@ -173,6 +175,7 @@ func buildParticipantsMessage(room *Room) map[string]interface{} {
"displayName": client.Username,
"email": client.Email,
"role": client.Role,
"persona": client.PersonaRole,
"isMuted": client.IsMuted,
})
}
Expand Down Expand Up @@ -466,6 +469,8 @@ func WebsocketHandler(c *gin.Context) {
handleTopicChange(room, conn, message, roomID)
case "roleSelection":
handleRoleSelection(room, conn, message, roomID)
case "personaSelection":
handlePersonaSelection(room, conn, message, roomID)
case "ready":
handleReadyStatus(room, conn, message, roomID)
case "mute":
Expand Down Expand Up @@ -684,6 +689,28 @@ func handleRoleSelection(room *Room, conn *websocket.Conn, message Message, room
broadcastParticipants(room)
}

// handlePersonaSelection handles persona role selection
func handlePersonaSelection(room *Room, conn *websocket.Conn, message Message, roomID string) {
// Store the persona in the client
room.Mutex.Lock()
defer room.Mutex.Unlock()
if client, exists := room.Clients[conn]; exists {
if client.IsSpectator {
return
}
client.PersonaRole = message.Persona
}

// Broadcast persona selection to other clients
for _, r := range snapshotRecipients(room, conn) {
if err := r.SafeWriteJSON(message); err != nil {
}
}

// Send updated participant snapshot to everyone
broadcastParticipants(room)
}
Comment on lines +692 to +712
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Deadlock: mutex held while calling locking functions.

The function holds room.Mutex via defer while calling snapshotRecipients and broadcastParticipants, both of which attempt to acquire the same mutex. Since Go's sync.Mutex is not reentrant, this causes a deadlock when any client sends a personaSelection message.

Note: The same bug exists in handleRoleSelection (lines 671-690). Compare with handlePhaseChange which correctly releases the lock before broadcasting.

🔒️ Proposed fix to release lock before broadcasting
 func handlePersonaSelection(room *Room, conn *websocket.Conn, message Message, roomID string) {
 	// Store the persona in the client
 	room.Mutex.Lock()
-	defer room.Mutex.Unlock()
 	if client, exists := room.Clients[conn]; exists {
 		if client.IsSpectator {
+			room.Mutex.Unlock()
 			return
 		}
 		client.PersonaRole = message.Persona
 	}
+	room.Mutex.Unlock()
 
 	// Broadcast persona selection to other clients
 	for _, r := range snapshotRecipients(room, conn) {
 		if err := r.SafeWriteJSON(message); err != nil {
 		}
 	}
 
 	// Send updated participant snapshot to everyone
 	broadcastParticipants(room)
 }

Consider also fixing the same issue in handleRoleSelection:

 func handleRoleSelection(room *Room, conn *websocket.Conn, message Message, roomID string) {
 	room.Mutex.Lock()
-	defer room.Mutex.Unlock()
 	if client, exists := room.Clients[conn]; exists {
 		if client.IsSpectator {
+			room.Mutex.Unlock()
 			return
 		}
 		client.Role = message.Role
 	}
+	room.Mutex.Unlock()
 
 	for _, r := range snapshotRecipients(room, conn) {
 		if err := r.SafeWriteJSON(message); err != nil {
 		}
 	}
 
 	broadcastParticipants(room)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// handlePersonaSelection handles persona role selection
func handlePersonaSelection(room *Room, conn *websocket.Conn, message Message, roomID string) {
// Store the persona in the client
room.Mutex.Lock()
defer room.Mutex.Unlock()
if client, exists := room.Clients[conn]; exists {
if client.IsSpectator {
return
}
client.PersonaRole = message.Persona
}
// Broadcast persona selection to other clients
for _, r := range snapshotRecipients(room, conn) {
if err := r.SafeWriteJSON(message); err != nil {
}
}
// Send updated participant snapshot to everyone
broadcastParticipants(room)
}
// handlePersonaSelection handles persona role selection
func handlePersonaSelection(room *Room, conn *websocket.Conn, message Message, roomID string) {
// Store the persona in the client
room.Mutex.Lock()
if client, exists := room.Clients[conn]; exists {
if client.IsSpectator {
room.Mutex.Unlock()
return
}
client.PersonaRole = message.Persona
}
room.Mutex.Unlock()
// Broadcast persona selection to other clients
for _, r := range snapshotRecipients(room, conn) {
if err := r.SafeWriteJSON(message); err != nil {
}
}
// Send updated participant snapshot to everyone
broadcastParticipants(room)
}
🤖 Prompt for AI Agents
In @backend/websocket/websocket.go around lines 692 - 712,
handlePersonaSelection currently defers room.Mutex.Unlock while later calling
snapshotRecipients and broadcastParticipants which reacquire the same mutex,
causing a deadlock; change the locking to only protect the critical update
(lookup and set client.PersonaRole) and release the mutex before calling
snapshotRecipients and broadcastParticipants (remove the deferred unlock and use
a short lock/unlock around the client update), and apply the same pattern to
handleRoleSelection so both functions unlock the room.Mutex before any calls to
snapshotRecipients or broadcastParticipants.


// handleReadyStatus handles ready status
func handleReadyStatus(room *Room, conn *websocket.Conn, message Message, roomID string) {
// Broadcast ready status to other clients
Expand Down
Loading