mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-04-28 03:30:23 +00:00
Sanitize print logs; refactor popular plugin logic
Ensure printed output and HTML logs are safe by importing and applying sanitize_string, opening log files with utf-8 and errors='replace', and sanitizing text before writing. Add tests to verify lone surrogate characters are replaced and that logging won't crash on invalid Unicode. In the plugin installer UI, introduce POPULAR_PLUGIN_MIN_STARS and centralize popularity checking in _isPopularPlugin, using it for filtering and counts.
This commit is contained in:
parent
430f8479a9
commit
44e008745d
6 changed files with 272 additions and 11 deletions
|
|
@ -3,6 +3,7 @@ import sys
|
|||
from datetime import datetime
|
||||
from collections.abc import Mapping
|
||||
from . import files
|
||||
from .strings import sanitize_string
|
||||
|
||||
_runtime_module = None
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ class PrintStyle:
|
|||
os.makedirs(logs_dir, exist_ok=True)
|
||||
log_filename = datetime.now().strftime("log_%Y%m%d_%H%M%S.html")
|
||||
PrintStyle.log_file_path = os.path.join(logs_dir, log_filename)
|
||||
with open(PrintStyle.log_file_path, "w") as f:
|
||||
with open(PrintStyle.log_file_path, "w", encoding="utf-8", errors="replace") as f:
|
||||
f.write("<html><body style='background-color:black;font-family: Arial, Helvetica, sans-serif;'><pre>\n")
|
||||
|
||||
def _get_rgb_color_code(self, color, is_background=False):
|
||||
|
|
@ -93,13 +94,13 @@ class PrintStyle:
|
|||
self.padding_added = True
|
||||
|
||||
def _log_html(self, html):
|
||||
with open(PrintStyle.log_file_path, "a", encoding='utf-8') as f: # type: ignore # add encoding='utf-8'
|
||||
f.write(html)
|
||||
with open(PrintStyle.log_file_path, "a", encoding="utf-8", errors="replace") as f: # type: ignore[arg-type]
|
||||
f.write(sanitize_string(html))
|
||||
|
||||
@staticmethod
|
||||
def _close_html_log():
|
||||
if PrintStyle.log_file_path:
|
||||
with open(PrintStyle.log_file_path, "a") as f:
|
||||
with open(PrintStyle.log_file_path, "a", encoding="utf-8", errors="replace") as f:
|
||||
f.write("</pre></body></html>")
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -145,6 +146,8 @@ class PrintStyle:
|
|||
# If masking fails, proceed without masking to avoid breaking functionality
|
||||
pass
|
||||
|
||||
text = sanitize_string(text)
|
||||
|
||||
return text, self._get_styled_text(text), self._get_html_styled_text(text)
|
||||
|
||||
def print(self, *args, sep=' ', end='\n', flush=True):
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@
|
|||
<div class="pi-hero-main">
|
||||
<h2 class="pi-hero-title" x-text="$store.pluginInstallStore.selectedPlugin.title || $store.pluginInstallStore.selectedPlugin.key"></h2>
|
||||
<div class="pi-status-badges">
|
||||
<template x-if="$store.pluginInstallStore.selectedPlugin.suspended">
|
||||
<span class="pi-card-suspended-pill">
|
||||
<span class="material-symbols-outlined">priority_high</span>
|
||||
<span>Suspended</span>
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="$store.pluginInstallStore.selectedPlugin.installed">
|
||||
<span class="pi-card-installed-pill">Installed</span>
|
||||
</template>
|
||||
|
|
@ -100,6 +106,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<template x-if="$store.pluginInstallStore.selectedPlugin.suspended">
|
||||
<div class="pi-suspension-banner">
|
||||
<span class="material-symbols-outlined">priority_high</span>
|
||||
<div class="pi-suspension-banner-copy">
|
||||
<div class="pi-suspension-banner-title">Plugin has been suspended for the following reasons:</div>
|
||||
<div class="pi-suspension-banner-explanation"
|
||||
x-text="$store.pluginInstallStore.selectedPlugin.suspended"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="pi-description" x-text="$store.pluginInstallStore.selectedPlugin.description || 'No description available.'"></div>
|
||||
|
||||
<div class="pi-screenshots-section"
|
||||
|
|
@ -435,6 +452,27 @@
|
|||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.pi-card-suspended-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0.24rem 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
background: rgba(245, 158, 11, 0.16);
|
||||
color: #f59e0b;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pi-card-suspended-pill .material-symbols-outlined {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
body.light-mode .pi-card-suspended-pill {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.pi-status-badges .pi-card-installed-pill {
|
||||
position: static;
|
||||
top: auto;
|
||||
|
|
@ -447,6 +485,12 @@
|
|||
right: auto;
|
||||
}
|
||||
|
||||
.pi-status-badges .pi-card-suspended-pill {
|
||||
position: static;
|
||||
top: auto;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.pi-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -466,6 +510,47 @@
|
|||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.pi-suspension-banner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.9rem 1rem;
|
||||
border: 1px solid rgba(245, 158, 11, 0.32);
|
||||
border-radius: 10px;
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.pi-suspension-banner .material-symbols-outlined {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.2;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pi-suspension-banner-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pi-suspension-banner-title {
|
||||
font-size: 0.92rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.pi-suspension-banner-explanation {
|
||||
margin-top: 0.2rem;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.55;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
body.light-mode .pi-suspension-banner {
|
||||
border-color: rgba(217, 119, 6, 0.28);
|
||||
background: rgba(245, 158, 11, 0.14);
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.pi-actions-primary {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
|
|
|
|||
|
|
@ -124,6 +124,12 @@
|
|||
</div>
|
||||
|
||||
<div class="pi-card-bubbles pi-card-bubbles-status">
|
||||
<template x-if="plugin.suspended">
|
||||
<span class="pi-card-bubble pi-card-bubble-suspended">
|
||||
<span class="material-symbols-outlined">priority_high</span>
|
||||
<span class="pi-card-bubble-text">Suspended</span>
|
||||
</span>
|
||||
</template>
|
||||
<template x-if="plugin.installed">
|
||||
<span class="pi-card-bubble pi-card-bubble-installed">
|
||||
<span class="material-symbols-outlined">check_circle</span>
|
||||
|
|
@ -641,6 +647,19 @@
|
|||
border-color: rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.pi-card-bubble-suspended .material-symbols-outlined {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.pi-card-bubble-suspended .pi-card-bubble-text {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.pi-card-bubble-suspended:hover {
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
border-color: rgba(245, 158, 11, 0.35);
|
||||
}
|
||||
|
||||
body.light-mode .pi-card-bubble-installed .material-symbols-outlined,
|
||||
body.light-mode .pi-card-bubble-installed .pi-card-bubble-text {
|
||||
color: #166534;
|
||||
|
|
@ -661,6 +680,16 @@
|
|||
border-color: rgba(59, 130, 246, 0.35);
|
||||
}
|
||||
|
||||
body.light-mode .pi-card-bubble-suspended .material-symbols-outlined,
|
||||
body.light-mode .pi-card-bubble-suspended .pi-card-bubble-text {
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
body.light-mode .pi-card-bubble-suspended:hover {
|
||||
background: rgba(245, 158, 11, 0.16);
|
||||
border-color: rgba(217, 119, 6, 0.35);
|
||||
}
|
||||
|
||||
.pi-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { store as pluginSettingsStore } from "/components/plugins/plugin-setting
|
|||
|
||||
const PLUGIN_API = "plugins/_plugin_installer/plugin_install";
|
||||
const PER_PAGE = 24;
|
||||
const POPULAR_PLUGIN_MIN_STARS = 3;
|
||||
|
||||
const SECURITY_WARNING = {
|
||||
title: "Security Warning",
|
||||
|
|
@ -93,11 +94,23 @@ const model = {
|
|||
.join(" ");
|
||||
},
|
||||
|
||||
_isPopularPlugin(plugin) {
|
||||
return (plugin?.stars || 0) >= POPULAR_PLUGIN_MIN_STARS;
|
||||
},
|
||||
|
||||
_getSuspensionReason(plugin) {
|
||||
return typeof plugin?.suspended === "string" ? plugin.suspended.trim() : "";
|
||||
},
|
||||
|
||||
isPluginSuspended(plugin) {
|
||||
return !!this._getSuspensionReason(plugin);
|
||||
},
|
||||
|
||||
_matchesBrowseFilter(plugin, filterKey) {
|
||||
if (!filterKey || filterKey === "all") return true;
|
||||
if (filterKey === "installed") return !!plugin?.installed;
|
||||
if (filterKey === "update") return !!plugin?.has_update;
|
||||
if (filterKey === "popular") return (plugin?.stars || 0) >= 3;
|
||||
if (filterKey === "popular") return this._isPopularPlugin(plugin);
|
||||
if (filterKey.startsWith("tag:")) {
|
||||
return this._pluginPrimaryTag(plugin) === filterKey.slice(4);
|
||||
}
|
||||
|
|
@ -126,6 +139,22 @@ const model = {
|
|||
return true;
|
||||
},
|
||||
|
||||
_comparePluginsByStars(a, b) {
|
||||
const aSuspended = this.isPluginSuspended(a);
|
||||
const bSuspended = this.isPluginSuspended(b);
|
||||
if (aSuspended !== bSuspended) {
|
||||
return aSuspended ? 1 : -1;
|
||||
}
|
||||
|
||||
const aStars = aSuspended ? 0 : Number(a?.stars) || 0;
|
||||
const bStars = bSuspended ? 0 : Number(b?.stars) || 0;
|
||||
if (aStars !== bStars) {
|
||||
return bStars - aStars;
|
||||
}
|
||||
|
||||
return (a.title || a.key).localeCompare(b.title || b.key);
|
||||
},
|
||||
|
||||
// ── ZIP Install ──────────────────────────────
|
||||
|
||||
handleFileUpload(event) {
|
||||
|
|
@ -341,6 +370,7 @@ const model = {
|
|||
commit: val?.commit || val?.latest_commit || "",
|
||||
updated: val?.updated || val?.latest_commit_timestamp || "",
|
||||
version: val?.version || "",
|
||||
suspended: this._getSuspensionReason(val),
|
||||
installed,
|
||||
};
|
||||
|
||||
|
|
@ -365,7 +395,7 @@ const model = {
|
|||
const updateCount = plugins.filter((plugin) => plugin.has_update).length;
|
||||
filters.push({ key: "update", label: "Update", count: updateCount });
|
||||
|
||||
const popularCount = plugins.filter((plugin) => (plugin.stars || 0) > 0).length;
|
||||
const popularCount = plugins.filter((plugin) => this._isPopularPlugin(plugin)).length;
|
||||
if (popularCount) {
|
||||
filters.push({ key: "popular", label: "Popular", count: popularCount });
|
||||
}
|
||||
|
|
@ -406,7 +436,7 @@ const model = {
|
|||
);
|
||||
}
|
||||
if (this.sortBy === "stars") {
|
||||
list.sort((a, b) => (b.stars || 0) - (a.stars || 0));
|
||||
list.sort((a, b) => this._comparePluginsByStars(a, b));
|
||||
} else {
|
||||
list.sort((a, b) =>
|
||||
(a.title || a.key).localeCompare(b.title || b.key)
|
||||
|
|
|
|||
52
tests/test_print_style.py
Normal file
52
tests/test_print_style.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from helpers.print_style import PrintStyle
|
||||
|
||||
|
||||
class _PassthroughSecretsManager:
|
||||
def mask_values(self, text: str) -> str:
|
||||
return text
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_print_style_state():
|
||||
PrintStyle.log_file_path = None
|
||||
PrintStyle.last_endline = True
|
||||
yield
|
||||
PrintStyle.log_file_path = None
|
||||
PrintStyle.last_endline = True
|
||||
|
||||
|
||||
def test_get_sanitizes_lone_surrogates(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr("helpers.print_style.files.get_abs_path", lambda _: str(tmp_path))
|
||||
|
||||
style = PrintStyle(log_only=True)
|
||||
style.secrets_mgr = _PassthroughSecretsManager()
|
||||
|
||||
plain_text, styled_text, html_text = style.get("bad \ud83d")
|
||||
|
||||
assert plain_text == "bad ?"
|
||||
assert "\ud83d" not in styled_text
|
||||
assert "\ud83d" not in html_text
|
||||
|
||||
|
||||
def test_print_writes_html_log_without_surrogate_crash(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr("helpers.print_style.files.get_abs_path", lambda _: str(tmp_path))
|
||||
|
||||
style = PrintStyle(log_only=True)
|
||||
style.secrets_mgr = _PassthroughSecretsManager()
|
||||
|
||||
style.print("bad \ud83d")
|
||||
|
||||
log_path = Path(PrintStyle.log_file_path)
|
||||
content = log_path.read_text(encoding="utf-8")
|
||||
|
||||
assert "bad ?" in content
|
||||
assert "\ud83d" not in content
|
||||
|
|
@ -6,6 +6,7 @@ from typing import List
|
|||
from helpers.tool import Tool, Response
|
||||
from helpers import projects, files, file_tree
|
||||
from helpers import skills as skills_helper, runtime
|
||||
from helpers.print_style import PrintStyle
|
||||
|
||||
|
||||
DATA_NAME_LOADED_SKILLS = "loaded_skills"
|
||||
|
|
@ -24,6 +25,67 @@ class SkillsTool(Tool):
|
|||
Script execution is handled by code_execution_tool directly.
|
||||
"""
|
||||
|
||||
def _current_method(self) -> str:
|
||||
return (
|
||||
(self.args.get("method") or self.method or "")
|
||||
.strip()
|
||||
.lower()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_skill_name(skill_name: str) -> str:
|
||||
skill_name = skill_name.strip()
|
||||
if skill_name.startswith("**") and skill_name.endswith("**"):
|
||||
skill_name = skill_name[2:-2]
|
||||
return skill_name.strip()
|
||||
|
||||
def get_log_object(self):
|
||||
import uuid
|
||||
|
||||
if self._current_method() == "load":
|
||||
skill_name = self._normalize_skill_name(
|
||||
str(self.args.get("skill_name") or "")
|
||||
)
|
||||
heading = (
|
||||
f"icon://construction Loading skill {skill_name}"
|
||||
if skill_name
|
||||
else "icon://construction Loading skill"
|
||||
)
|
||||
return self.agent.context.log.log(
|
||||
type="tool",
|
||||
heading=heading,
|
||||
content="",
|
||||
kvps={"_tool_name": self.name},
|
||||
id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
return super().get_log_object()
|
||||
|
||||
async def before_execution(self, **kwargs):
|
||||
if self._current_method() != "load":
|
||||
await super().before_execution(**kwargs)
|
||||
return
|
||||
|
||||
skill_name = self._normalize_skill_name(
|
||||
str(kwargs.get("skill_name") or self.args.get("skill_name") or "")
|
||||
)
|
||||
label = f"{self.name}:{self._current_method()}"
|
||||
if skill_name:
|
||||
PrintStyle(
|
||||
font_color="#1B4F72",
|
||||
padding=True,
|
||||
background_color="white",
|
||||
bold=True,
|
||||
).print(f"{self.agent.agent_name}: Loading skill '{skill_name}'")
|
||||
else:
|
||||
PrintStyle(
|
||||
font_color="#1B4F72",
|
||||
padding=True,
|
||||
background_color="white",
|
||||
bold=True,
|
||||
).print(f"{self.agent.agent_name}: Using tool '{label}'")
|
||||
self.log = self.get_log_object()
|
||||
|
||||
async def execute(self, **kwargs) -> Response:
|
||||
method = (
|
||||
(kwargs.get("method") or self.args.get("method") or self.method or "")
|
||||
|
|
@ -38,7 +100,9 @@ class SkillsTool(Tool):
|
|||
# query = str(kwargs.get("query") or "").strip()
|
||||
# return Response(message=self._search(query), break_loop=False)
|
||||
if method == "load":
|
||||
skill_name = str(kwargs.get("skill_name") or "").strip()
|
||||
skill_name = self._normalize_skill_name(
|
||||
str(kwargs.get("skill_name") or "")
|
||||
)
|
||||
return Response(message=self._load(skill_name), break_loop=False)
|
||||
# if method == "read_file":
|
||||
# skill_name = str(kwargs.get("skill_name") or "").strip()
|
||||
|
|
@ -106,9 +170,7 @@ class SkillsTool(Tool):
|
|||
# return "\n".join(lines)
|
||||
|
||||
def _load(self, skill_name: str) -> str:
|
||||
skill_name = skill_name.strip()
|
||||
if skill_name.startswith("**") and skill_name.endswith("**"):
|
||||
skill_name = skill_name[2:-2]
|
||||
skill_name = self._normalize_skill_name(skill_name)
|
||||
|
||||
if not skill_name:
|
||||
return "Error: 'skill_name' is required for method=load."
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue