personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'hopper-ueekj7wv-health-deep-link-fix'

+183 -1
+41
apps/health/routes.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + import re 5 + from pathlib import Path 6 + 7 + from flask import Blueprint, jsonify, request 8 + 9 + from convey import state 10 + 11 + health_bp = Blueprint("app:health", __name__, url_prefix="/app/health") 12 + 13 + 14 + @health_bp.get("/api/log") 15 + def get_log(): 16 + path = request.args.get("path") 17 + if not path: 18 + return jsonify(error="Missing path parameter"), 400 19 + 20 + if not re.fullmatch(r"\d{8}/health/[^/]+\.log", path): 21 + return jsonify(error="Invalid path"), 400 22 + 23 + journal_root = Path(state.journal_root).resolve() 24 + try: 25 + file_path = (Path(state.journal_root) / path).resolve() 26 + except ValueError: 27 + return jsonify(error="Invalid path"), 400 28 + try: 29 + file_path.relative_to(journal_root) 30 + except ValueError: 31 + return jsonify(error="Invalid path"), 400 32 + 33 + if not file_path.exists(): 34 + return jsonify(error="Log file not found"), 404 35 + 36 + try: 37 + content = file_path.read_text(encoding="utf-8") 38 + except IOError: 39 + return jsonify(error="Failed to read log file"), 500 40 + 41 + return jsonify(content=content, path=path)
+1
apps/health/tests/__init__.py
··· 1 +
+42
apps/health/tests/conftest.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """Self-contained fixtures for health app tests.""" 5 + 6 + from __future__ import annotations 7 + 8 + import pytest 9 + 10 + 11 + @pytest.fixture 12 + def health_env(tmp_path, monkeypatch): 13 + """Create a temporary journal for health app testing.""" 14 + 15 + def _create( 16 + log_content="test log content\nline 2\n", 17 + log_path="20260322/health/1774196508583_transcribe.log", 18 + ): 19 + journal = tmp_path / "journal" 20 + journal.mkdir(exist_ok=True) 21 + 22 + # Create sample log file 23 + log_file = journal / log_path 24 + log_file.parent.mkdir(parents=True, exist_ok=True) 25 + log_file.write_text(log_content, encoding="utf-8") 26 + 27 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 28 + 29 + from convey import create_app 30 + 31 + app = create_app(journal=str(journal)) 32 + client = app.test_client() 33 + 34 + class Env: 35 + def __init__(self): 36 + self.journal = journal 37 + self.client = client 38 + self.app = app 39 + 40 + return Env() 41 + 42 + return _create
+55
apps/health/tests/test_routes.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + """Tests for health app routes.""" 5 + 6 + 7 + class TestLogRoute: 8 + """Tests for GET /app/health/api/log.""" 9 + 10 + def test_valid_log_path(self, health_env): 11 + env = health_env() 12 + resp = env.client.get( 13 + "/app/health/api/log?path=20260322/health/1774196508583_transcribe.log" 14 + ) 15 + assert resp.status_code == 200 16 + data = resp.get_json() 17 + assert data["path"] == "20260322/health/1774196508583_transcribe.log" 18 + assert "test log content" in data["content"] 19 + 20 + def test_path_traversal_rejected(self, health_env): 21 + env = health_env() 22 + resp = env.client.get("/app/health/api/log?path=../../../etc/passwd") 23 + assert resp.status_code == 400 24 + 25 + def test_non_log_extension_rejected(self, health_env): 26 + env = health_env() 27 + resp = env.client.get("/app/health/api/log?path=20260322/health/foo.txt") 28 + assert resp.status_code == 400 29 + 30 + def test_path_outside_health_dir_rejected(self, health_env): 31 + env = health_env() 32 + resp = env.client.get("/app/health/api/log?path=20260322/agents/something.log") 33 + assert resp.status_code == 400 34 + 35 + def test_missing_file_returns_404(self, health_env): 36 + env = health_env() 37 + resp = env.client.get("/app/health/api/log?path=20260322/health/nonexistent.log") 38 + assert resp.status_code == 404 39 + 40 + def test_missing_path_param_returns_400(self, health_env): 41 + env = health_env() 42 + resp = env.client.get("/app/health/api/log") 43 + assert resp.status_code == 400 44 + 45 + def test_encoded_traversal_rejected(self, health_env): 46 + env = health_env() 47 + resp = env.client.get( 48 + "/app/health/api/log?path=20260322/health/..%2F..%2Fetc%2Fpasswd.log" 49 + ) 50 + assert resp.status_code == 400 51 + 52 + def test_null_byte_rejected(self, health_env): 53 + env = health_env() 54 + resp = env.client.get("/app/health/api/log?path=20260322/health/foo%00.log") 55 + assert resp.status_code == 400
+43
apps/health/workspace.html
··· 1541 1541 renderLogs(); 1542 1542 }); 1543 1543 1544 + // Deep-link: display log file content if ?log= param is present 1545 + const deepLinkLog = new URLSearchParams(window.location.search).get('log'); 1546 + if (deepLinkLog) { 1547 + const viewport = elements.logsViewport; 1548 + const logsCard = viewport.closest('.logs-card'); 1549 + const logsTitle = logsCard.querySelector('.logs-title'); 1550 + const logsControls = logsCard.querySelector('.logs-controls'); 1551 + 1552 + // Replace header with log file context 1553 + logsTitle.textContent = 'LOG FILE'; 1554 + logsControls.innerHTML = '<button id="logBackBtn">← Back to Dashboard</button>'; 1555 + viewport.innerHTML = '<div class="logs-line log">Loading...</div>'; 1556 + 1557 + fetch('/app/health/api/log?path=' + encodeURIComponent(deepLinkLog)) 1558 + .then(r => r.json().then(data => ({ok: r.ok, data}))) 1559 + .then(({ok, data}) => { 1560 + if (!ok) { 1561 + viewport.innerHTML = '<div class="logs-line stderr">' + 1562 + (data.error || 'Failed to load log file').replace(/&/g, '&amp;').replace(/</g, '&lt;') + 1563 + '</div>'; 1564 + return; 1565 + } 1566 + const escaped = data.content.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); 1567 + const pathHeader = '<div class="logs-service-header">── ' + 1568 + data.path.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') + 1569 + ' ──</div>'; 1570 + viewport.innerHTML = pathHeader + '<div class="logs-line">' + 1571 + escaped.replace(/\n/g, '</div><div class="logs-line">') + '</div>'; 1572 + }) 1573 + .catch(() => { 1574 + viewport.innerHTML = '<div class="logs-line stderr">Network error loading log file</div>'; 1575 + }); 1576 + 1577 + document.addEventListener('click', function(e) { 1578 + if (e.target && e.target.id === 'logBackBtn') { 1579 + window.location.href = '/app/health'; 1580 + } 1581 + }); 1582 + 1583 + // Scroll logs card into view 1584 + logsCard.scrollIntoView({behavior: 'smooth'}); 1585 + } 1586 + 1544 1587 // Listen to all Callosum events 1545 1588 window.appEvents.listen('*', handleEvent); 1546 1589 })();
+1 -1
observe/sense.py
··· 415 415 title=f"{handler_proc.handler_name.capitalize()} Error", 416 416 icon=icon, 417 417 app="sense", 418 - action=f"/health/logs?path={log_rel}", 418 + action=f"/app/health?log={log_rel}", 419 419 ) 420 420 421 421 # Mark file as done so segment can still complete