Skip to content

Conversation

@standujar
Copy link
Collaborator

@standujar standujar commented Jun 26, 2025

Server Architecture Refactoring - Factory Pattern Implementation

Risks

Medium

  • Breaking Changes: Server API structure has been modified
  • Frontend Serving: Web UI is no longer served by default (API-only server)

What could be affected:

  • Existing server deployments
  • Custom integrations using server package
  • Frontend access (now requires separate client serving)

Background

What does this PR do?

This PR implements a comprehensive refactoring of the ElizaOS server architecture:

  1. Layer Pattern Implementation: Breaks down the 1000+ line AgentServer class into focused services:

    • DatabaseService - Database operations and migrations
    • AgentService - Agent lifecycle and plugin management
    • MiddlewareService - Express middleware configuration
    • HttpService - HTTP server, routing, and Socket.IO
  2. Factory Pattern: Introduces createElizaServer() factory for clean server instantiation with dependency injection

  3. Plugin Loading: Extracts and implements complete plugin loading logic from CLI to server, enabling dynamic plugin loading based on character configuration

  4. Architecture Cleanup: Removes inappropriate cross-package dependencies (server no longer depends on CLI)

  5. API-Only Server: Converts server to pure API backend, removing static file serving for better separation of concerns

What kind of change is this?

Features (non-breaking change which adds functionality)

  • ✅ Factory pattern for server creation
  • ✅ Service-based architecture with dependency injection
  • ✅ Dynamic plugin loading system

Improvements (misc. changes to existing features)

  • ✅ Better separation of concerns
  • ✅ Cleaner file organization following TypeScript conventions
  • ✅ Removed circular dependencies

Documentation changes needed?

My changes require a change to the project documentation.

Documentation updates needed:

  • Update server package README with new architecture
  • Document factory pattern usage
  • Update plugin loading documentation
  • Add service layer documentation
  • Update deployment guides for API-only server

Testing

Where should a reviewer start?

  1. Server Package Build: Verify packages/server builds successfully
  2. Factory Pattern: Test server creation via createElizaServer()
  3. Plugin Loading: Verify agents start with correct plugins from character config
  4. API Endpoints: Test all /api/* routes work correctly
  5. CLI Integration: Ensure CLI still works with refactored server

Detailed testing steps

Build and Start Tests

# Test server package builds
cd packages/server && bun run build

# Test CLI with new server architecture  
cd packages/cli && bun start

# Verify server starts and shows correct port
# Expected: "AgentServer is listening on port 3000"

API Testing

# Test health endpoint
curl http://localhost:3000/api/server/health

# Test agents endpoint  
curl http://localhost:3000/api/agents

# Test root returns API info (not HTML)
curl http://localhost:3000/

Plugin Loading Verification

  1. Create test character with plugins specified in character.plugins array
  2. Start agent with character: elizaos start --character test-char.json
  3. Verify logs show: "Loading X plugins for agent Y: plugin1, plugin2..."
  4. Confirm agent starts successfully with all specified plugins

Key Files Changed

New Architecture Files

  • packages/server/src/server/factory.ts - Factory pattern implementation
  • packages/server/src/server/server.ts - Main server class using composition
  • packages/server/src/services/database.ts - Database service layer
  • packages/server/src/services/agent.ts - Agent management service
  • packages/server/src/services/middleware.ts - Express middleware service
  • packages/server/src/services/http.ts - HTTP and Socket.IO service
  • packages/server/src/utils/plugin-loader.ts - Extracted plugin loading logic

File Organization

  • packages/server/src/utils/character-loader.ts (renamed from loader.ts)
  • Moved middleware files to services/middleware/ directory
  • Organized utilities in proper utils/ structure

Updated Entry Points

  • packages/server/src/index.ts - Clean public API exports
  • packages/cli/src/commands/start/actions/server-start.ts - Uses factory pattern

Breaking Changes

  1. Server Package API:

    • Old: Direct AgentServer instantiation
    • New: createElizaServer() factory function
  2. Frontend Serving:

    • Old: Server served static files at /
    • New: API-only server, returns JSON at /
  3. Plugin Loading:

    • Old: Server used minimal plugin loading
    • New: Complete plugin loading matching CLI behavior

Backward Compatibility

  • AgentServer class still exported for compatibility
  • ✅ All existing API endpoints preserved
  • ✅ Database schema unchanged
  • ✅ CLI commands work unchanged
  • ❌ Static file serving removed (web UI requires separate serving)

Testing Results

Successful Tests:

  • ✅ Server package builds without errors
  • ✅ CLI starts agents using new factory pattern
  • ✅ API endpoints respond correctly
  • ✅ Plugin loading works with character configurations
  • ✅ Database operations function properly
  • ✅ Socket.IO connections established successfully

Architecture Validation:

  • ✅ Single Responsibility Principle achieved
  • ✅ Dependency injection working properly
  • ✅ Service layer separation clean
  • ✅ No circular dependencies
  • ✅ TypeScript compilation successful

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jun 26, 2025

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing Touches
🧪 Generate Unit Tests
  • Create PR with Unit Tests
  • Post Copyable Unit Tests in Comment
  • Commit Unit Tests in branch feature/ELIZA-475-server-factory-pattern

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai auto-generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@graphite-app
Copy link

graphite-app bot commented Jun 26, 2025

How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • merge-queue - adds this PR to the back of the merge queue
  • merge-queue-hotfix - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

Comment on lines +138 to +140
app.use('/api', (req, res, next) => {
apiKeyAuthMiddleware(req, res, next);
});

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI 6 months ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

Comment on lines +234 to +267
(req: express.Request, res: express.Response) => {
const agentId = req.params.agentId as string;
const filename = req.params.filename as string;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

if (!uuidRegex.test(agentId)) {
return res.status(400).json({ error: 'Invalid agent ID format' });
}

const sanitizedFilename = basename(filename);
const agentUploadsPath = join(uploadsBasePath, agentId);
const filePath = join(agentUploadsPath, sanitizedFilename);

if (!filePath.startsWith(agentUploadsPath)) {
return res.status(403).json({ error: 'Access denied' });
}

if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'File does not exist!!!!!!!' });
}

res.sendFile(sanitizedFilename, { root: agentUploadsPath }, (err) => {
if (err) {
if (err.message === 'Request aborted') {
logger.warn(`[MEDIA] Download aborted: ${req.originalUrl}`);
} else if (!res.headersSent) {
logger.warn(`[MEDIA] File not found: ${agentUploadsPath}/${sanitizedFilename}`);
res.status(404).json({ error: 'File not found' });
}
} else {
logger.debug(`[MEDIA] Successfully served: ${sanitizedFilename}`);
}
});
}

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
a file system access
, but is not rate-limited.
This route handler performs
a file system access
, but is not rate-limited.

Copilot Autofix

AI 6 months ago

To address the issue, we will introduce rate limiting to the affected route handler using the express-rate-limit package. This package allows us to define a rate-limiting policy, such as limiting the number of requests per minute for the route. The fix involves:

  1. Importing the express-rate-limit package.
  2. Creating a rate limiter instance with appropriate configuration (e.g., maximum requests per minute).
  3. Applying the rate limiter middleware to the affected route handler.

This ensures that the route is protected against DoS attacks while maintaining its functionality.


Suggested changeset 1
packages/server/src/services/middleware.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/packages/server/src/services/middleware.ts b/packages/server/src/services/middleware.ts
--- a/packages/server/src/services/middleware.ts
+++ b/packages/server/src/services/middleware.ts
@@ -230,4 +230,12 @@
   private setupAgentMediaRoute(app: express.Application, uploadsBasePath: string): void {
+    const rateLimit = require('express-rate-limit');
+    const agentMediaRateLimiter = rateLimit({
+      windowMs: 15 * 60 * 1000, // 15 minutes
+      max: 100, // Limit each IP to 100 requests per windowMs
+      message: { error: 'Too many requests, please try again later.' },
+    });
+
     app.get(
       '/media/uploads/agents/:agentId/:filename',
+      agentMediaRateLimiter,
       // @ts-expect-error - this is a valid express route
EOF
@@ -230,4 +230,12 @@
private setupAgentMediaRoute(app: express.Application, uploadsBasePath: string): void {
const rateLimit = require('express-rate-limit');
const agentMediaRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: { error: 'Too many requests, please try again later.' },
});

app.get(
'/media/uploads/agents/:agentId/:filename',
agentMediaRateLimiter,
// @ts-expect-error - this is a valid express route
Copilot is powered by AI and may make mistakes. Always verify output.
return res.status(404).json({ error: 'File does not exist!!!!!!!' });
}

res.sendFile(sanitizedFilename, { root: agentUploadsPath }, (err) => {

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 6 months ago

To fix the issue, we need to ensure that the constructed file path is properly normalized and validated to prevent path traversal attacks. This involves:

  1. Using path.resolve to normalize the constructed path (agentUploadsPath and filePath) and remove any .. segments.
  2. Using fs.realpathSync to resolve symbolic links and ensure the path is contained within the intended directory.
  3. Adding a strict check to verify that the normalized path starts with the intended base directory (uploadsBasePath).

The changes will be made in the setupAgentMediaRoute method, specifically around the construction and validation of agentUploadsPath and filePath.


Suggested changeset 1
packages/server/src/services/middleware.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/packages/server/src/services/middleware.ts b/packages/server/src/services/middleware.ts
--- a/packages/server/src/services/middleware.ts
+++ b/packages/server/src/services/middleware.ts
@@ -243,4 +243,4 @@
         const sanitizedFilename = basename(filename);
-        const agentUploadsPath = join(uploadsBasePath, agentId);
-        const filePath = join(agentUploadsPath, sanitizedFilename);
+        const agentUploadsPath = fs.realpathSync(path.resolve(uploadsBasePath, agentId));
+        const filePath = fs.realpathSync(path.resolve(agentUploadsPath, sanitizedFilename));
         
EOF
@@ -243,4 +243,4 @@
const sanitizedFilename = basename(filename);
const agentUploadsPath = join(uploadsBasePath, agentId);
const filePath = join(agentUploadsPath, sanitizedFilename);
const agentUploadsPath = fs.realpathSync(path.resolve(uploadsBasePath, agentId));
const filePath = fs.realpathSync(path.resolve(agentUploadsPath, sanitizedFilename));

Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +275 to +297
(req: express.Request<{ agentId: string; filename: string }>, res: express.Response) => {
const agentId = req.params.agentId;
const filename = req.params.filename;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

if (!uuidRegex.test(agentId)) {
return res.status(400).json({ error: 'Invalid agent ID format' });
}

const sanitizedFilename = basename(filename);
const agentGeneratedPath = join(generatedBasePath, agentId);
const filePath = join(agentGeneratedPath, sanitizedFilename);

if (!filePath.startsWith(agentGeneratedPath)) {
return res.status(403).json({ error: 'Access denied' });
}

res.sendFile(filePath, (err) => {
if (err) {
res.status(404).json({ error: 'File not found' });
}
});
}

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
a file system access
, but is not rate-limited.

Copilot Autofix

AI 6 months ago

To fix the issue, we will introduce rate limiting to the /media/generated/:agentId/:filename route using the express-rate-limit package. This package allows us to define a maximum number of requests per time window, effectively mitigating the risk of DoS attacks. The rate limiter will be applied specifically to the affected route.

Steps:

  1. Install the express-rate-limit package if not already installed.
  2. Import the package in the file.
  3. Define a rate limiter configuration with appropriate limits (e.g., 100 requests per 15 minutes).
  4. Apply the rate limiter middleware to the /media/generated/:agentId/:filename route.

Suggested changeset 1
packages/server/src/services/middleware.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/packages/server/src/services/middleware.ts b/packages/server/src/services/middleware.ts
--- a/packages/server/src/services/middleware.ts
+++ b/packages/server/src/services/middleware.ts
@@ -7,2 +7,3 @@
 import { apiKeyAuthMiddleware } from './middleware/auth.js';
+import rateLimit from 'express-rate-limit';
 import type { ServerOptions, ServerMiddleware } from '../types/server.js';
@@ -271,4 +272,10 @@
   private setupGeneratedMediaRoute(app: express.Application, generatedBasePath: string): void {
+    const generatedMediaRateLimiter = rateLimit({
+      windowMs: 15 * 60 * 1000, // 15 minutes
+      max: 100, // max 100 requests per windowMs
+    });
+    
     app.get(
       '/media/generated/:agentId/:filename',
+      generatedMediaRateLimiter,
       // @ts-expect-error - this is a valid express route
EOF
@@ -7,2 +7,3 @@
import { apiKeyAuthMiddleware } from './middleware/auth.js';
import rateLimit from 'express-rate-limit';
import type { ServerOptions, ServerMiddleware } from '../types/server.js';
@@ -271,4 +272,10 @@
private setupGeneratedMediaRoute(app: express.Application, generatedBasePath: string): void {
const generatedMediaRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // max 100 requests per windowMs
});

app.get(
'/media/generated/:agentId/:filename',
generatedMediaRateLimiter,
// @ts-expect-error - this is a valid express route
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +304 to +333
(req: express.Request<{ channelId: string; filename: string }>, res: express.Response) => {
const channelId = req.params.channelId as string;
const filename = req.params.filename as string;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

if (!uuidRegex.test(channelId)) {
res.status(400).json({ error: 'Invalid channel ID format' });
return;
}

const sanitizedFilename = basename(filename);
const channelUploadsPath = join(uploadsBasePath, 'channels', channelId);
const filePath = join(channelUploadsPath, sanitizedFilename);

if (!filePath.startsWith(channelUploadsPath)) {
res.status(403).json({ error: 'Access denied' });
return;
}

res.sendFile(filePath, (err) => {
if (err) {
logger.warn(`[STATIC] Channel media file not found: ${filePath}`, err);
if (!res.headersSent) {
res.status(404).json({ error: 'File not found' });
}
} else {
logger.debug(`[STATIC] Served channel media file: ${filePath}`);
}
});
}

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
a file system access
, but is not rate-limited.

Copilot Autofix

AI 6 months ago

To address the issue, we will introduce rate limiting to the /media/uploads/channels/:channelId/:filename route using the express-rate-limit package. This package allows us to define a maximum number of requests per time window, effectively mitigating the risk of DoS attacks.

Steps to fix:

  1. Install the express-rate-limit package if not already installed.
  2. Import the package in the file.
  3. Define a rate limiter configuration with appropriate limits (e.g., 100 requests per 15 minutes).
  4. Apply the rate limiter middleware specifically to the /media/uploads/channels/:channelId/:filename route.

Suggested changeset 1
packages/server/src/services/middleware.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/packages/server/src/services/middleware.ts b/packages/server/src/services/middleware.ts
--- a/packages/server/src/services/middleware.ts
+++ b/packages/server/src/services/middleware.ts
@@ -8,2 +8,3 @@
 import type { ServerOptions, ServerMiddleware } from '../types/server.js';
+import rateLimit from 'express-rate-limit';
 
@@ -301,4 +302,11 @@
   private setupChannelMediaRoute(app: express.Application, uploadsBasePath: string): void {
+    const channelMediaRateLimiter = rateLimit({
+      windowMs: 15 * 60 * 1000, // 15 minutes
+      max: 100, // max 100 requests per windowMs
+      message: { error: 'Too many requests, please try again later.' },
+    });
+
     app.get(
       '/media/uploads/channels/:channelId/:filename',
+      channelMediaRateLimiter,
       (req: express.Request<{ channelId: string; filename: string }>, res: express.Response) => {
EOF
@@ -8,2 +8,3 @@
import type { ServerOptions, ServerMiddleware } from '../types/server.js';
import rateLimit from 'express-rate-limit';

@@ -301,4 +302,11 @@
private setupChannelMediaRoute(app: express.Application, uploadsBasePath: string): void {
const channelMediaRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // max 100 requests per windowMs
message: { error: 'Too many requests, please try again later.' },
});

app.get(
'/media/uploads/channels/:channelId/:filename',
channelMediaRateLimiter,
(req: express.Request<{ channelId: string; filename: string }>, res: express.Response) => {
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Bug: TypeScript Type Safety Violation

The getServersForAgent method uses an incorrect as never type cast when pushing server.id to the serverIds array. This cast is illogical as never represents unreachable values, effectively bypassing TypeScript's type safety. The serverIds array is implicitly typed as any[] while the method returns Promise<UUID[]>, and server.id is expected to be a UUID. This can hide type errors and lead to runtime issues.

packages/server/src/services/database.ts#L177-L178

if (agents.includes(agentId)) {
serverIds.push(server.id as never);

Fix in Cursor


Was this report helpful? Give feedback by reacting with 👍 or 👎

@standujar standujar marked this pull request as draft June 26, 2025 14:00
@standujar
Copy link
Collaborator Author

Aborting this because Next will replace most of this.

@standujar standujar closed this Jun 26, 2025
@standujar standujar deleted the feature/ELIZA-475-server-factory-pattern branch September 1, 2025 15:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants