mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-15 14:22:43 +00:00
fix(session-export): escape html tool call names
This commit is contained in:
parent
b5c655c89e
commit
a4dd08a977
2 changed files with 67 additions and 13 deletions
|
|
@ -8,7 +8,9 @@ Enhanced with UI-UX-PRO-MAX design intelligence.
|
|||
|
||||
import json
|
||||
import datetime
|
||||
import secrets
|
||||
from typing import Any, Dict, List
|
||||
from urllib.parse import quote
|
||||
|
||||
# --- Icons (Lucide-style SVGs) ---
|
||||
ICON_USER = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>'
|
||||
|
|
@ -26,6 +28,7 @@ HTML_TEMPLATE = """<!DOCTYPE html>
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-{script_nonce}'; style-src 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; object-src 'none'">
|
||||
<title>{page_title}</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
|
@ -566,13 +569,13 @@ HTML_TEMPLATE = """<!DOCTYPE html>
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<script nonce="{script_nonce}">
|
||||
// Session Switching
|
||||
function showSession(id) {{
|
||||
// Update Sidebar
|
||||
document.querySelectorAll('.session-item').forEach(el => el.classList.remove('active'));
|
||||
const activeItem = document.querySelector(`.session-item[data-id="${{id}}"]`);
|
||||
if (activeItem) activeItem.classList.add('active');
|
||||
document.querySelectorAll('.session-item').forEach(el => {{
|
||||
el.classList.toggle('active', el.dataset.id === id);
|
||||
}});
|
||||
|
||||
// Update View
|
||||
document.querySelectorAll('.session-view').forEach(el => {{
|
||||
|
|
@ -582,9 +585,16 @@ HTML_TEMPLATE = """<!DOCTYPE html>
|
|||
if (activeView) activeView.classList.add('active');
|
||||
|
||||
// Store in URL hash
|
||||
window.location.hash = id;
|
||||
window.location.hash = encodeURIComponent(id);
|
||||
}}
|
||||
|
||||
document.querySelectorAll('.session-item').forEach(item => {{
|
||||
item.addEventListener('click', (e) => {{
|
||||
e.preventDefault();
|
||||
showSession(item.dataset.id || '');
|
||||
}});
|
||||
}});
|
||||
|
||||
// Search Filter
|
||||
const searchInput = document.getElementById('session-search');
|
||||
if (searchInput) {{
|
||||
|
|
@ -611,7 +621,7 @@ HTML_TEMPLATE = """<!DOCTYPE html>
|
|||
|
||||
// Initialization
|
||||
window.addEventListener('load', () => {{
|
||||
const hash = window.location.hash.slice(1);
|
||||
const hash = decodeURIComponent(window.location.hash.slice(1));
|
||||
if (hash) {{
|
||||
showSession(hash);
|
||||
}} else {{
|
||||
|
|
@ -761,16 +771,17 @@ def generate_multi_session_html_export(sessions: List[Dict[str, Any]]) -> str:
|
|||
if is_multi:
|
||||
sidebar_items = []
|
||||
for s in sessions:
|
||||
sid = s.get("id", "N/A")
|
||||
sid = str(s.get("id", "N/A"))
|
||||
escaped_sid = _escape_html(sid)
|
||||
title = s.get("title") or s.get("preview") or "Untitled Session"
|
||||
if len(title) > 50: title = title[:47] + "..."
|
||||
date = _format_timestamp(s.get("started_at", 0)).split(" ")[0]
|
||||
|
||||
item = f'''
|
||||
<a class="session-item" data-id="{sid}" onclick="showSession('{sid}')">
|
||||
<a class="session-item" data-id="{escaped_sid}" href="#{quote(sid, safe='')}">
|
||||
<div class="session-item-title">{_escape_html(title)}</div>
|
||||
<div class="session-item-meta">
|
||||
<span>{sid[:8]}</span>
|
||||
<span>{_escape_html(sid[:8])}</span>
|
||||
<span>{date}</span>
|
||||
</div>
|
||||
</a>
|
||||
|
|
@ -797,7 +808,8 @@ def generate_multi_session_html_export(sessions: List[Dict[str, Any]]) -> str:
|
|||
# Main Content
|
||||
sessions_html_list = []
|
||||
for s in sessions:
|
||||
sid = s.get("id", "N/A")
|
||||
sid = str(s.get("id", "N/A"))
|
||||
escaped_sid = _escape_html(sid)
|
||||
title = s.get("title") or "Hermes Session"
|
||||
model = s.get("model", "Unknown")
|
||||
started_at = _format_timestamp(s.get("started_at", 0))
|
||||
|
|
@ -808,7 +820,7 @@ def generate_multi_session_html_export(sessions: List[Dict[str, Any]]) -> str:
|
|||
view_class = "session-view"
|
||||
if not is_multi: view_class += " active"
|
||||
|
||||
session_view_id = f"view-{sid}"
|
||||
session_view_id = f"view-{escaped_sid}"
|
||||
|
||||
system_prompt = s.get("system_prompt")
|
||||
system_html = ""
|
||||
|
|
@ -830,7 +842,7 @@ def generate_multi_session_html_export(sessions: List[Dict[str, Any]]) -> str:
|
|||
<header class="fade-in">
|
||||
<h1>{_escape_html(title)}</h1>
|
||||
<div class="meta">
|
||||
<div class="meta-item"><strong>ID:</strong> {sid}</div>
|
||||
<div class="meta-item"><strong>ID:</strong> {escaped_sid}</div>
|
||||
<div class="meta-item"><strong>Model:</strong> {_escape_html(model)}</div>
|
||||
<div class="meta-item"><strong>Started:</strong> {started_at}</div>
|
||||
</div>
|
||||
|
|
@ -843,13 +855,15 @@ def generate_multi_session_html_export(sessions: List[Dict[str, Any]]) -> str:
|
|||
'''
|
||||
sessions_html_list.append(session_html)
|
||||
|
||||
script_nonce = secrets.token_urlsafe(16)
|
||||
return HTML_TEMPLATE.format(
|
||||
page_title="Hermes Session Export" if is_multi else _escape_html(sessions[0].get("title", "Hermes Session")),
|
||||
sidebar_html=sidebar_html,
|
||||
sessions_html="\n".join(sessions_html_list),
|
||||
main_margin="var(--sidebar-width)" if is_multi else "0",
|
||||
layout_class="layout-multi" if is_multi else "layout-single",
|
||||
generated_at=generated_at
|
||||
generated_at=generated_at,
|
||||
script_nonce=script_nonce,
|
||||
)
|
||||
|
||||
def generate_html_export(session_data: Dict[str, Any]) -> str:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import json
|
|||
import sys
|
||||
|
||||
from hermes_cli.session_export import export_record_count, render_sessions_export
|
||||
from hermes_cli.session_export_html import (
|
||||
_generate_messages_html,
|
||||
generate_multi_session_html_export,
|
||||
)
|
||||
|
||||
|
||||
def _sample_session():
|
||||
|
|
@ -113,6 +117,42 @@ def test_full_markdown_renderer_collapses_tool_output_and_filters_system():
|
|||
assert "hidden system context" not in rendered
|
||||
|
||||
|
||||
def test_html_export_escapes_tool_call_names():
|
||||
payload = '<img src=x onerror="alert(document.domain)">'
|
||||
|
||||
rendered = _generate_messages_html(
|
||||
[
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {"name": payload, "arguments": "<b>x</b>"},
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
assert payload not in rendered
|
||||
assert '<img src=x onerror="alert(document.domain)">' in rendered
|
||||
assert "<b>x</b>" in rendered
|
||||
|
||||
|
||||
def test_html_export_uses_csp_without_inline_event_handlers():
|
||||
first = _sample_session()
|
||||
second = {**_sample_session(), "id": "sess-456", "title": "Second session"}
|
||||
|
||||
rendered = generate_multi_session_html_export([first, second])
|
||||
|
||||
assert "Content-Security-Policy" in rendered
|
||||
assert "script-src 'nonce-" in rendered
|
||||
assert "<script nonce=" in rendered
|
||||
assert "onclick=" not in rendered
|
||||
|
||||
|
||||
def test_export_record_count_switches_unit_for_prompt_only_exports():
|
||||
assert export_record_count([_sample_session()]) == (1, "session")
|
||||
assert export_record_count([_sample_session()], only="user-prompts") == (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue