From 7d98f539a05bc31c8ca54c55453225b50cfdded1 Mon Sep 17 00:00:00 2001
From: jenkins
Date: Sun, 9 Aug 2026 15:52:06 -0300
Subject: [PATCH] fix(hermes): make browser terminal copy reliable
---
services/hermes/agent-deployment.yaml | 28 +++-
services/hermes/kustomization.yaml | 1 +
services/hermes/scripts/patch_ttyd_index.py | 176 ++++++++++++++++++++
testing/tests/test_hermes_herdr.py | 27 +++
4 files changed, 231 insertions(+), 1 deletion(-)
create mode 100644 services/hermes/scripts/patch_ttyd_index.py
diff --git a/services/hermes/agent-deployment.yaml b/services/hermes/agent-deployment.yaml
index 5be29eec9..633e1e5c7 100644
--- a/services/hermes/agent-deployment.yaml
+++ b/services/hermes/agent-deployment.yaml
@@ -24,7 +24,7 @@ spec:
ai.bstein.dev/execution: Herdr-supervised Codex and Claude Code
ai.bstein.dev/model-policy: Jetson-assisted AUTO routing, low through xhigh, cross-provider fallback
ai.bstein.dev/placement: rpi5 preferred; Jetson deferred until state storage is available
- ai.bstein.dev/config-rev: "20260809-cassandra-readonly"
+ ai.bstein.dev/config-rev: "20260809-terminal-clipboard"
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: hermes-agent
vault.hashicorp.com/agent-inject-secret-anthropic-token: kv/data/atlas/hermes/agent-tokens
@@ -269,6 +269,27 @@ spec:
resources:
requests: {cpu: 25m, memory: 32Mi}
limits: {cpu: 250m, memory: 128Mi}
+ - name: prepare-ttyd-index
+ image: registry.bstein.dev/bstein/hermes-agent@sha256:7a1daefae2f068dcf14e7eb1f2f9aab2e1ce79c1b55b4bb0aa1092fbf4019bbc
+ imagePullPolicy: IfNotPresent
+ command:
+ - /opt/hermes/.venv/bin/python
+ - /opt/coordinator/patch_ttyd_index.py
+ - /opt/data/tools/bin/ttyd
+ - /ttyd-index/index.html
+ securityContext:
+ allowPrivilegeEscalation: false
+ runAsUser: 10000
+ runAsGroup: 10000
+ seccompProfile:
+ type: RuntimeDefault
+ volumeMounts:
+ - {name: home, mountPath: /opt/data}
+ - {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
+ - {name: ttyd-index, mountPath: /ttyd-index}
+ resources:
+ requests: {cpu: 25m, memory: 32Mi}
+ limits: {cpu: 250m, memory: 128Mi}
containers:
- name: hermes
image: registry.bstein.dev/bstein/hermes-agent@sha256:7a1daefae2f068dcf14e7eb1f2f9aab2e1ce79c1b55b4bb0aa1092fbf4019bbc
@@ -401,6 +422,7 @@ spec:
--port 7681 \
--cwd /opt/data/workspace \
--terminal-type xterm-256color \
+ --index /ttyd-index/index.html \
--client-option "titleFixed=Hermes Agent - HERDR" \
--client-option fontSize=15 \
/bin/sh -c '
@@ -423,6 +445,7 @@ spec:
volumeMounts:
- {name: home, mountPath: /opt/data}
- {name: tmp, mountPath: /tmp}
+ - {name: ttyd-index, mountPath: /ttyd-index, readOnly: true}
startupProbe:
tcpSocket: {port: herdr-tui}
periodSeconds: 5
@@ -603,3 +626,6 @@ spec:
- name: tmp
emptyDir:
sizeLimit: 256Mi
+ - name: ttyd-index
+ emptyDir:
+ sizeLimit: 2Mi
diff --git a/services/hermes/kustomization.yaml b/services/hermes/kustomization.yaml
index 64740d87d..49251205c 100644
--- a/services/hermes/kustomization.yaml
+++ b/services/hermes/kustomization.yaml
@@ -50,6 +50,7 @@ configMapGenerator:
- hermes_coordinator.py=scripts/hermes_coordinator.py
- hermes_model_routing.py=scripts/hermes_model_routing.py
- patch_hermes_auth.py=scripts/patch_hermes_auth.py
+ - patch_ttyd_index.py=scripts/patch_ttyd_index.py
options:
disableNameSuffixHash: true
- name: hermes-agent-kubeconfig
diff --git a/services/hermes/scripts/patch_ttyd_index.py b/services/hermes/scripts/patch_ttyd_index.py
new file mode 100644
index 000000000..cb4a293c8
--- /dev/null
+++ b/services/hermes/scripts/patch_ttyd_index.py
@@ -0,0 +1,176 @@
+#!/usr/bin/env python3
+"""Add reliable browser clipboard handling to ttyd's pinned client page."""
+
+from __future__ import annotations
+
+import argparse
+import socket
+import subprocess
+import time
+import urllib.request
+from pathlib import Path
+
+
+MARKER = "atlas-ttyd-clipboard"
+UPSTREAM_COPY = 'document.execCommand("copy")'
+CLIPBOARD_ADAPTER = r"""
+
+
+"""
+
+
+def patch_html(content: str) -> str:
+ """Insert the adapter once and disable ttyd's false-success copy call."""
+ if content.count(UPSTREAM_COPY) != 1 or content.count("