diff --git a/dashboard/pages/skills.js b/dashboard/pages/skills.js
index 84c6122..a41d203 100644
--- a/dashboard/pages/skills.js
+++ b/dashboard/pages/skills.js
@@ -6,7 +6,7 @@ async function renderSkills() {
+
@@ -83,7 +83,7 @@ function filterSkills() {
async function showSkillDetail(name) {
document.getElementById('skillsContainer').style.display = 'none';
document.getElementById('skillTabs').style.display = 'none';
- document.getElementById('skillFilter').style.display = 'none';
+ document.getElementById('skillActions').style.display = 'none';
const detail = document.getElementById('skillDetail');
detail.style.display = 'block';
detail.innerHTML = `
`;
@@ -152,7 +152,7 @@ async function showSkillDetail(name) {
function backToSkills() {
document.getElementById('skillsContainer').style.display = '';
document.getElementById('skillTabs').style.display = '';
- document.getElementById('skillFilter').style.display = '';
+ document.getElementById('skillActions').style.display = '';
document.getElementById('skillDetail').style.display = 'none';
}
diff --git a/server.py b/server.py
index b106ccd..cedef11 100644
--- a/server.py
+++ b/server.py
@@ -166,12 +166,22 @@ def hermes_cli_args(*args: str) -> list:
return ["wsl", "-e", "bash", "-lc", f"hermes {quoted}"]
return ["hermes", *args]
+_hermes_available_cache = {"checked_at": 0.0, "result": False}
+HERMES_AVAILABLE_CACHE_TTL = 60
+
def hermes_available() -> bool:
+ """Cached: this spawns a subprocess (possibly via WSL), and /api/status is polled every 15s."""
+ now = time.time()
+ if now - _hermes_available_cache["checked_at"] < HERMES_AVAILABLE_CACHE_TTL:
+ return _hermes_available_cache["result"]
try:
r = subprocess.run(hermes_cli_args("--version"), capture_output=True, text=True, timeout=10)
- return r.returncode == 0
+ result = r.returncode == 0
except Exception:
- return False
+ result = False
+ _hermes_available_cache["checked_at"] = now
+ _hermes_available_cache["result"] = result
+ return result
def check_agent(name: str) -> dict:
"""Filesystem-based check for opencode/gemini; hermes needs a real subprocess since it may live inside WSL."""
@@ -300,7 +310,9 @@ def create_skill(data: SkillCreate):
if path.exists():
raise HTTPException(409, "Skill already exists")
path.mkdir(parents=True)
+ (path / "context").mkdir()
(path / "SKILL.md").write_text(data.skill_md, encoding="utf-8")
+ (path / "learnings.md").write_text("", encoding="utf-8")
append_audit({"action": "skill_created", "skill": data.name})
return {"name": data.name}
@@ -838,9 +850,20 @@ class PtySession:
os.kill(self.pid, signal.SIGKILL)
except OSError:
pass
+ try:
+ os.waitpid(self.pid, 0) # reap the killed child so it doesn't stay a zombie
+ except ChildProcessError:
+ pass
@app.websocket("/ws/terminal")
async def ws_terminal(websocket: WebSocket):
+ # CORSMiddleware does not protect WebSocket handshakes, so this endpoint - which spawns a
+ # full interactive shell - must check the Origin header itself, or any webpage could open
+ # this socket and get command execution on the machine running the dashboard.
+ origin = websocket.headers.get("origin")
+ if origin not in get_cors_origins():
+ await websocket.close(code=1008)
+ return
await websocket.accept()
session = PtySession()
try: