diff --git a/src/agentkit/server/app.py b/src/agentkit/server/app.py
index 133131b..511dd0a 100644
--- a/src/agentkit/server/app.py
+++ b/src/agentkit/server/app.py
@@ -696,11 +696,13 @@ def create_app(
@app.get("/{path:path}", response_class=HTMLResponse, include_in_schema=False)
async def spa_fallback(path: str):
"""Serve index.html for SPA client-side routing."""
- # Don't intercept API routes
- if path.startswith("api/"):
+ # Don't intercept API routes or docs
+ if path.startswith("api/") or path.startswith("docs") or path == "openapi.json":
return HTMLResponse("
Not Found
", status_code=404)
- # Try to serve a real static file first
- file_path = _static_dir / path
+ # Try to serve a real static file first (with path traversal protection)
+ file_path = (_static_dir / path).resolve()
+ if not str(file_path).startswith(str(_static_dir.resolve())):
+ return HTMLResponse("Forbidden
", status_code=403)
if file_path.is_file():
return FileResponse(str(file_path))
# Fallback to index.html for SPA routing
diff --git a/src/agentkit/server/frontend/package-lock.json b/src/agentkit/server/frontend/package-lock.json
index 09600c8..390f6f7 100644
--- a/src/agentkit/server/frontend/package-lock.json
+++ b/src/agentkit/server/frontend/package-lock.json
@@ -13,12 +13,14 @@
"@vue-flow/controls": "^1.1.0",
"@vue-flow/core": "^1.41.0",
"ant-design-vue": "^4.2.0",
+ "dompurify": "^3.4.10",
"markdown-it": "^14.2.0",
"pinia": "^2.2.0",
"vue": "^3.5.0",
"vue-router": "^4.4.0"
},
"devDependencies": {
+ "@types/dompurify": "^3.0.5",
"@types/markdown-it": "^14.1.2",
"@vitejs/plugin-vue": "^5.1.0",
"echarts": "^6.1.0",
@@ -933,6 +935,16 @@
"nanopop": "^2.1.0"
}
},
+ "node_modules/@types/dompurify": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmmirror.com/@types/dompurify/-/dompurify-3.0.5.tgz",
+ "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/trusted-types": "*"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz",
@@ -965,6 +977,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/@types/web-bluetooth": {
"version": "0.0.20",
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
@@ -1511,6 +1530,15 @@
"integrity": "sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==",
"license": "MIT"
},
+ "node_modules/dompurify": {
+ "version": "3.4.10",
+ "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.10.tgz",
+ "integrity": "sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/echarts": {
"version": "6.1.0",
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.1.0.tgz",
diff --git a/src/agentkit/server/frontend/package.json b/src/agentkit/server/frontend/package.json
index c17e88a..e24e515 100644
--- a/src/agentkit/server/frontend/package.json
+++ b/src/agentkit/server/frontend/package.json
@@ -14,12 +14,14 @@
"@vue-flow/controls": "^1.1.0",
"@vue-flow/core": "^1.41.0",
"ant-design-vue": "^4.2.0",
+ "dompurify": "^3.4.10",
"markdown-it": "^14.2.0",
"pinia": "^2.2.0",
"vue": "^3.5.0",
"vue-router": "^4.4.0"
},
"devDependencies": {
+ "@types/dompurify": "^3.0.5",
"@types/markdown-it": "^14.1.2",
"@vitejs/plugin-vue": "^5.1.0",
"echarts": "^6.1.0",
diff --git a/src/agentkit/server/frontend/src/api/skills.ts b/src/agentkit/server/frontend/src/api/skills.ts
index d3bdbbb..10889cd 100644
--- a/src/agentkit/server/frontend/src/api/skills.ts
+++ b/src/agentkit/server/frontend/src/api/skills.ts
@@ -77,28 +77,21 @@ class SkillsApiClient extends BaseApiClient {
source?: string
): Promise<{ status: string; name: string; path: string }> {
// The install endpoint is on /api/v1/skills/install, not under skill-management
- const url = `/api/v1/skills/install`
- const resp = await fetch(url, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ name, source }),
- })
- if (!resp.ok) {
- const err = await resp.json().catch(() => ({ detail: resp.statusText }))
- throw new Error(err.detail ?? resp.statusText)
- }
- return resp.json()
+ return this.request<{ status: string; name: string; path: string }>(
+ '/api/v1/skills/install',
+ {
+ method: 'POST',
+ body: JSON.stringify({ name, source }),
+ }
+ )
}
/** Uninstall a skill */
async uninstallSkill(skillName: string): Promise<{ status: string; name: string }> {
- const url = `/api/v1/skills/${encodeURIComponent(skillName)}`
- const resp = await fetch(url, { method: 'DELETE' })
- if (!resp.ok) {
- const err = await resp.json().catch(() => ({ detail: resp.statusText }))
- throw new Error(err.detail ?? resp.statusText)
- }
- return resp.json()
+ return this.request<{ status: string; name: string }>(
+ `/api/v1/skills/${encodeURIComponent(skillName)}`,
+ { method: 'DELETE' }
+ )
}
/** Reload a skill */
diff --git a/src/agentkit/server/frontend/src/components/chat/ChatMessage.vue b/src/agentkit/server/frontend/src/components/chat/ChatMessage.vue
index ff31436..296197a 100644
--- a/src/agentkit/server/frontend/src/components/chat/ChatMessage.vue
+++ b/src/agentkit/server/frontend/src/components/chat/ChatMessage.vue
@@ -54,6 +54,7 @@