Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@elizaos/plugin-knowledge",
"description": "Plugin for Knowledge",
"version": "1.2.2",
"version": "1.2.3",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
Expand Down
47 changes: 36 additions & 11 deletions src/frontend/ui/knowledge-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ import { Card } from './card';
import { Input } from './input';
import { MemoryGraph } from './memory-graph';

// Declare global window extension for TypeScript
declare global {
interface Window {
ELIZA_CONFIG?: {
agentId: string;
apiBase: string;
};
}
}

// Local utility function instead of importing from client
const cn = (...classes: (string | undefined | null | false)[]) => {
return classes.filter(Boolean).join(' ');
Expand Down Expand Up @@ -238,18 +248,28 @@ const getCorrectMimeType = (file: File): string => {
return file.type || 'application/octet-stream';
};

// Get the API base path from the injected configuration
const getApiBase = () => {
// Check if we have the ELIZA_CONFIG from the server
if (window.ELIZA_CONFIG?.apiBase) {
return window.ELIZA_CONFIG.apiBase;
}
return '/api';
};

const apiClient = {
getKnowledgeDocuments: async (
agentId: UUID,
options?: { limit?: number; before?: number; includeEmbedding?: boolean }
) => {
const params = new URLSearchParams();
params.append('agentId', agentId);
// Don't append agentId to params if it's already in the URL path
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: API Parameter Mismatch Causes Filtering Issues

The getKnowledgeDocuments API call no longer appends the agentId parameter to its query, but its server-side handler still expects it. This creates an inconsistency with other API calls in the file and may lead to incorrect document filtering or API failures.

Fix in Cursor Fix in Web

if (options?.limit) params.append('limit', options.limit.toString());
if (options?.before) params.append('before', options.before.toString());
if (options?.includeEmbedding) params.append('includeEmbedding', 'true');

const response = await fetch(`/api/documents?${params.toString()}`);
const apiBase = getApiBase();
const response = await fetch(`${apiBase}/documents?${params.toString()}`);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to fetch knowledge documents: ${response.status} ${errorText}`);
Expand All @@ -273,7 +293,8 @@ const apiClient = {
if (options?.documentId) params.append('documentId', options.documentId);
if (options?.documentsOnly) params.append('documentsOnly', 'true');

const response = await fetch(`/api/knowledges?${params.toString()}`);
const apiBase = getApiBase();
const response = await fetch(`${apiBase}/knowledges?${params.toString()}`);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to fetch knowledge chunks: ${response.status} ${errorText}`);
Expand All @@ -285,7 +306,8 @@ const apiClient = {
const params = new URLSearchParams();
params.append('agentId', agentId);

const response = await fetch(`/api/documents/${knowledgeId}?${params.toString()}`, {
const apiBase = getApiBase();
const response = await fetch(`${apiBase}/documents/${knowledgeId}?${params.toString()}`, {
method: 'DELETE',
});
if (!response.ok) {
Expand All @@ -307,7 +329,8 @@ const apiClient = {
}
formData.append('agentId', agentId);

const response = await fetch(`/api/documents`, {
const apiBase = getApiBase();
const response = await fetch(`${apiBase}/documents`, {
method: 'POST',
body: formData,
});
Expand All @@ -330,7 +353,8 @@ const apiClient = {
params.append('threshold', threshold.toString());
params.append('limit', limit.toString());

const response = await fetch(`/api/search?${params.toString()}`);
const apiBase = getApiBase();
const response = await fetch(`${apiBase}/search?${params.toString()}`);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to search knowledge: ${response.status} ${errorText}`);
Expand Down Expand Up @@ -673,7 +697,8 @@ export function KnowledgeTab({ agentId }: { agentId: UUID }) {
setUrlError(null);

try {
const result = await fetch(`/api/documents`, {
const apiBase = getApiBase();
const result = await fetch(`${apiBase}/documents`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down Expand Up @@ -729,7 +754,8 @@ export function KnowledgeTab({ agentId }: { agentId: UUID }) {
}
formData.append('agentId', agentId);

const response = await fetch('/api/documents', {
const apiBase = getApiBase();
const response = await fetch(`${apiBase}/documents`, {
method: 'POST',
body: formData,
});
Expand Down Expand Up @@ -1124,9 +1150,8 @@ export function KnowledgeTab({ agentId }: { agentId: UUID }) {
return (
<div className="flex flex-col h-full">
<div
className={`flex flex-col sm:flex-row items-start sm:items-center justify-between border-b gap-3 ${
isDocumentFocused ? 'p-6 pb-4' : 'p-4'
}`}
className={`flex flex-col sm:flex-row items-start sm:items-center justify-between border-b gap-3 ${isDocumentFocused ? 'p-6 pb-4' : 'p-4'
}`}
>
<div className="flex flex-col gap-1">
<h2 className="text-lg font-semibold">Knowledge</h2>
Expand Down
43 changes: 32 additions & 11 deletions src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,14 @@ async function knowledgePanelHandler(req: any, res: any, runtime: IAgentRuntime)

logger.debug(`[Document Processor] 🌐 Serving knowledge panel for agent ${agentId}`);

// Extract the plugin API base path from the request path
// Request path will be like: /api/agents/[uuid]/plugins/knowledge/display
// We need: /api/agents/[uuid]/plugins/knowledge
const requestPath = req.originalUrl || req.url || req.path;
const pluginBasePath = requestPath.replace(/\/display.*$/, '');

logger.debug(`[Document Processor] 🌐 Plugin base path: ${pluginBasePath}`);

try {
const currentDir = path.dirname(new URL(import.meta.url).pathname);
// Serve the main index.html from Vite's build output
Expand All @@ -522,17 +530,22 @@ async function knowledgePanelHandler(req: any, res: any, runtime: IAgentRuntime)

if (fs.existsSync(frontendPath)) {
const html = await fs.promises.readFile(frontendPath, 'utf8');
// Inject config into existing HTML
const injectedHtml = html.replace(
// Inject config into existing HTML with the correct API base path
// Also fix relative asset paths to use absolute paths
let injectedHtml = html.replace(
'<head>',
`<head>
<script>
window.ELIZA_CONFIG = {
agentId: '${agentId}',
apiBase: '/api'
apiBase: '${pluginBasePath}'
};
</script>`
);
Comment on lines +533 to 544
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid XSS in injected config by serializing via JSON

Injecting request-derived strings into a JS literal is fragile. Serialize instead.

-      let injectedHtml = html.replace(
-        '<head>',
-        `<head>
-          <script>
-            window.ELIZA_CONFIG = {
-              agentId: '${agentId}',
-              apiBase: '${pluginBasePath}'
-            };
-          </script>`
-      );
+      const elizaConfig = JSON.stringify({ agentId, apiBase: pluginBasePath });
+      let injectedHtml = html.replace(
+        '<head>',
+        `<head><script>window.ELIZA_CONFIG=${elizaConfig};</script>`
+      );
📝 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
// Inject config into existing HTML with the correct API base path
// Also fix relative asset paths to use absolute paths
let injectedHtml = html.replace(
'<head>',
`<head>
<script>
window.ELIZA_CONFIG = {
agentId: '${agentId}',
apiBase: '/api'
apiBase: '${pluginBasePath}'
};
</script>`
);
// Inject config into existing HTML with the correct API base path
// Also fix relative asset paths to use absolute paths
const elizaConfig = JSON.stringify({ agentId, apiBase: pluginBasePath });
let injectedHtml = html.replace(
'<head>',
`<head><script>window.ELIZA_CONFIG=${elizaConfig};</script>`
);
🤖 Prompt for AI Agents
In src/routes.ts around lines 533 to 544, the code injects request-derived
strings directly into a JS literal which risks XSS; instead serialize the config
object with JSON.stringify and inject that serialized string. Replace the
current template-literal interpolation with code that builds the config =
JSON.stringify({ agentId, apiBase: pluginBasePath }) and then writes
window.ELIZA_CONFIG = <that JSON string>; into the injected script so values are
safely escaped (ensure the serialized string is inserted as raw JSON, not as a
nested template string, and avoid unescaped "</script>" by using JSON.stringify
output).


// Fix relative asset paths to be absolute
injectedHtml = injectedHtml.replace(/src="\.\/assets\//g, `src="${pluginBasePath}/assets/`);
injectedHtml = injectedHtml.replace(/href="\.\/assets\//g, `href="${pluginBasePath}/assets/`);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(injectedHtml);
} else {
Expand Down Expand Up @@ -577,10 +590,10 @@ async function knowledgePanelHandler(req: any, res: any, runtime: IAgentRuntime)
<script>
window.ELIZA_CONFIG = {
agentId: '${agentId}',
apiBase: '/api'
apiBase: '${pluginBasePath}'
};
</script>
<link rel="stylesheet" href="./assets/${cssFile}">
<link rel="stylesheet" href="${pluginBasePath}/assets/${cssFile}">
<style>
body { font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
Expand All @@ -593,7 +606,7 @@ async function knowledgePanelHandler(req: any, res: any, runtime: IAgentRuntime)
<div class="loading">Loading Knowledge Library...</div>
</div>
</div>
<script type="module" src="./assets/${jsFile}"></script>
<script type="module" src="${pluginBasePath}/assets/${jsFile}"></script>
</body>
</html>`;
res.writeHead(200, { 'Content-Type': 'text/html' });
Expand All @@ -608,16 +621,24 @@ async function knowledgePanelHandler(req: any, res: any, runtime: IAgentRuntime)
// Generic handler to serve static assets from the dist/assets directory
async function frontendAssetHandler(req: any, res: any, runtime: IAgentRuntime) {
try {
logger.debug(`[Document Processor] 🌐 Asset request: ${req.path}`);
// Use originalUrl or url first, fallback to path
const fullPath = req.originalUrl || req.url || req.path;
logger.debug(`[Document Processor] 🌐 Asset request: ${fullPath}`);
const currentDir = path.dirname(new URL(import.meta.url).pathname);

const assetRequestPath = req.path; // This is the full path, e.g., /api/agents/X/plugins/knowledge/assets/file.js
// The path will be like: /api/agents/X/plugins/knowledge/assets/file.js
// We need to extract everything after '/assets/'
const assetsMarker = '/assets/';
const assetsStartIndex = assetRequestPath.indexOf(assetsMarker);
const assetsStartIndex = fullPath.lastIndexOf(assetsMarker); // Use lastIndexOf to get the last occurrence

let assetName = null;
if (assetsStartIndex !== -1) {
assetName = assetRequestPath.substring(assetsStartIndex + assetsMarker.length);
assetName = fullPath.substring(assetsStartIndex + assetsMarker.length);
// Remove any query parameters if present
const queryIndex = assetName.indexOf('?');
if (queryIndex !== -1) {
assetName = assetName.substring(0, queryIndex);
}
}

if (!assetName || assetName.includes('..')) {
Expand All @@ -626,7 +647,7 @@ async function frontendAssetHandler(req: any, res: any, runtime: IAgentRuntime)
res,
400,
'BAD_REQUEST',
`Invalid asset name: '${assetName}' from path ${assetRequestPath}`
`Invalid asset name: '${assetName}' from path ${fullPath}`
);
}

Expand Down