hermes: isolate runtime access and expose activity
This commit is contained in:
parent
d29a5737fe
commit
89b9f1a4c7
@ -30,6 +30,10 @@ spec:
|
|||||||
kind: Deployment
|
kind: Deployment
|
||||||
name: hermes-agent
|
name: hermes-agent
|
||||||
namespace: hermes
|
namespace: hermes
|
||||||
|
- apiVersion: apps/v1
|
||||||
|
kind: DaemonSet
|
||||||
|
name: hermes-node-ssh-access
|
||||||
|
namespace: hermes
|
||||||
- apiVersion: apps/v1
|
- apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
name: hermes
|
name: hermes
|
||||||
|
|||||||
6
dockerfiles/Dockerfile.hermes-agent.dockerignore
Normal file
6
dockerfiles/Dockerfile.hermes-agent.dockerignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
**
|
||||||
|
!dockerfiles/hermes-python-sandbox-tool.py
|
||||||
|
!dockerfiles/hermes-public-extract/
|
||||||
|
!dockerfiles/hermes-public-extract/**
|
||||||
|
!dockerfiles/hermes-session-activity-panel.tsx
|
||||||
|
!dockerfiles/hermes-session-migrate.py
|
||||||
@ -104,8 +104,9 @@ function ActivityMessage({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Show every persisted message and tool event without relying on xterm's
|
* Show every persisted message and tool event without relying on xterm's
|
||||||
* canvas. API workers default to this view; the normal terminal remains one
|
* canvas. Resumed sessions default to this view so an unknown or delayed
|
||||||
* click away and continues running underneath it.
|
* backend source classification cannot hide activity; the normal terminal
|
||||||
|
* remains one click away and continues running underneath it.
|
||||||
*/
|
*/
|
||||||
export function SessionActivityPanel({
|
export function SessionActivityPanel({
|
||||||
sessionId,
|
sessionId,
|
||||||
@ -129,7 +130,7 @@ export function SessionActivityPanel({
|
|||||||
api.getSessionMessages(sessionId, profile ?? ""),
|
api.getSessionMessages(sessionId, profile ?? ""),
|
||||||
]);
|
]);
|
||||||
setDetail(nextDetail);
|
setDetail(nextDetail);
|
||||||
setMessages(response.messages);
|
setMessages(Array.isArray(response.messages) ? response.messages : []);
|
||||||
setError(null);
|
setError(null);
|
||||||
setPollTime(Date.now());
|
setPollTime(Date.now());
|
||||||
if (stickToBottom.current) {
|
if (stickToBottom.current) {
|
||||||
@ -169,9 +170,6 @@ export function SessionActivityPanel({
|
|||||||
? "Working"
|
? "Working"
|
||||||
: "Run finished";
|
: "Run finished";
|
||||||
|
|
||||||
// Ordinary interactive TUI sessions keep their established terminal UI.
|
|
||||||
if (detail && detail.source !== "api_server") return null;
|
|
||||||
|
|
||||||
if (!expanded) {
|
if (!expanded) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@ -235,6 +233,11 @@ export function SessionActivityPanel({
|
|||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
className="min-h-0 flex-1 space-y-2 overflow-y-auto p-3"
|
className="min-h-0 flex-1 space-y-2 overflow-y-auto p-3"
|
||||||
|
role="log"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-relevant="additions text"
|
||||||
|
aria-busy={running && messages.length === 0}
|
||||||
|
aria-label="Chronological Hermes worker activity"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onScroll={(event) => {
|
onScroll={(event) => {
|
||||||
const node = event.currentTarget;
|
const node = event.currentTarget;
|
||||||
|
|||||||
@ -3,6 +3,6 @@
|
|||||||
"helmrelease_host_hints": 23,
|
"helmrelease_host_hints": 23,
|
||||||
"http_endpoints": 61,
|
"http_endpoints": 61,
|
||||||
"services": 91,
|
"services": 91,
|
||||||
"workloads": 124
|
"workloads": 125
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -892,6 +892,19 @@
|
|||||||
"wger/server@sha256:710588b78af4e0aa0b4d8a8061e4563e16eae80eeaccfe7f9e0d9cbdd7f0cbc5"
|
"wger/server@sha256:710588b78af4e0aa0b4d8a8061e4563e16eae80eeaccfe7f9e0d9cbdd7f0cbc5"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "DaemonSet",
|
||||||
|
"namespace": "hermes",
|
||||||
|
"name": "hermes-node-ssh-access",
|
||||||
|
"labels": {
|
||||||
|
"app": "hermes-node-ssh-access"
|
||||||
|
},
|
||||||
|
"serviceAccountName": "hermes-node-ssh-access",
|
||||||
|
"nodeSelector": {},
|
||||||
|
"images": [
|
||||||
|
"busybox:1.37"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "Deployment",
|
"kind": "Deployment",
|
||||||
"namespace": "hermes",
|
"namespace": "hermes",
|
||||||
@ -902,7 +915,7 @@
|
|||||||
"serviceAccountName": "hermes-triage",
|
"serviceAccountName": "hermes-triage",
|
||||||
"nodeSelector": {},
|
"nodeSelector": {},
|
||||||
"images": [
|
"images": [
|
||||||
"registry.bstein.dev/bstein/hermes-agent@sha256:3154e1df5f6941583f4a369fcc9581bbf9cb9d502676e91b557e90cc576923e1",
|
"registry.bstein.dev/bstein/hermes-agent@sha256:cce1f65dc7d30fdce4b1748cc7d3434b9761ae9946a8c8be6049204c503d60a8",
|
||||||
"registry.bstein.dev/bstein/hermes-webui@sha256:9c2fe8341c7b650e08d10acead3151b19e2af737863268bafb39b3d9517575b1"
|
"registry.bstein.dev/bstein/hermes-webui@sha256:9c2fe8341c7b650e08d10acead3151b19e2af737863268bafb39b3d9517575b1"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -917,7 +930,7 @@
|
|||||||
"nodeSelector": {},
|
"nodeSelector": {},
|
||||||
"images": [
|
"images": [
|
||||||
"quay.io/oauth2-proxy/oauth2-proxy:v7.15.3@sha256:10a1165743a192e1940b4708fb9647027185ce11a681a1c5519b442ff7f1f561",
|
"quay.io/oauth2-proxy/oauth2-proxy:v7.15.3@sha256:10a1165743a192e1940b4708fb9647027185ce11a681a1c5519b442ff7f1f561",
|
||||||
"registry.bstein.dev/bstein/hermes-agent@sha256:3154e1df5f6941583f4a369fcc9581bbf9cb9d502676e91b557e90cc576923e1"
|
"registry.bstein.dev/bstein/hermes-agent@sha256:cce1f65dc7d30fdce4b1748cc7d3434b9761ae9946a8c8be6049204c503d60a8"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1164,7 +1177,7 @@
|
|||||||
"serviceAccountName": "hermes-chat",
|
"serviceAccountName": "hermes-chat",
|
||||||
"nodeSelector": {},
|
"nodeSelector": {},
|
||||||
"images": [
|
"images": [
|
||||||
"registry.bstein.dev/bstein/hermes-agent@sha256:3154e1df5f6941583f4a369fcc9581bbf9cb9d502676e91b557e90cc576923e1",
|
"registry.bstein.dev/bstein/hermes-agent@sha256:cce1f65dc7d30fdce4b1748cc7d3434b9761ae9946a8c8be6049204c503d60a8",
|
||||||
"registry.bstein.dev/bstein/hermes-webui@sha256:9c2fe8341c7b650e08d10acead3151b19e2af737863268bafb39b3d9517575b1"
|
"registry.bstein.dev/bstein/hermes-webui@sha256:9c2fe8341c7b650e08d10acead3151b19e2af737863268bafb39b3d9517575b1"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -1515,7 +1528,7 @@
|
|||||||
"kubernetes.io/os": "linux"
|
"kubernetes.io/os": "linux"
|
||||||
},
|
},
|
||||||
"images": [
|
"images": [
|
||||||
"registry.bstein.dev/bstein/metis-sentinel:0.1.0-292-amd64"
|
"registry.bstein.dev/bstein/metis-sentinel:0.1.0-293-amd64"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1531,7 +1544,7 @@
|
|||||||
"kubernetes.io/os": "linux"
|
"kubernetes.io/os": "linux"
|
||||||
},
|
},
|
||||||
"images": [
|
"images": [
|
||||||
"registry.bstein.dev/bstein/metis-sentinel:0.1.0-291-arm64"
|
"registry.bstein.dev/bstein/metis-sentinel:0.1.0-293-arm64"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1648,7 +1661,7 @@
|
|||||||
"serviceAccountName": "metis",
|
"serviceAccountName": "metis",
|
||||||
"nodeSelector": {},
|
"nodeSelector": {},
|
||||||
"images": [
|
"images": [
|
||||||
"registry.bstein.dev/bstein/metis:0.1.0-291-arm64"
|
"registry.bstein.dev/bstein/metis:0.1.0-293-arm64"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -596,6 +596,15 @@ workloads:
|
|||||||
images:
|
images:
|
||||||
- nginx:1.27.5-alpine@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10
|
- nginx:1.27.5-alpine@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10
|
||||||
- wger/server@sha256:710588b78af4e0aa0b4d8a8061e4563e16eae80eeaccfe7f9e0d9cbdd7f0cbc5
|
- wger/server@sha256:710588b78af4e0aa0b4d8a8061e4563e16eae80eeaccfe7f9e0d9cbdd7f0cbc5
|
||||||
|
- kind: DaemonSet
|
||||||
|
namespace: hermes
|
||||||
|
name: hermes-node-ssh-access
|
||||||
|
labels:
|
||||||
|
app: hermes-node-ssh-access
|
||||||
|
serviceAccountName: hermes-node-ssh-access
|
||||||
|
nodeSelector: {}
|
||||||
|
images:
|
||||||
|
- busybox:1.37
|
||||||
- kind: Deployment
|
- kind: Deployment
|
||||||
namespace: hermes
|
namespace: hermes
|
||||||
name: hermes
|
name: hermes
|
||||||
@ -604,7 +613,7 @@ workloads:
|
|||||||
serviceAccountName: hermes-triage
|
serviceAccountName: hermes-triage
|
||||||
nodeSelector: {}
|
nodeSelector: {}
|
||||||
images:
|
images:
|
||||||
- registry.bstein.dev/bstein/hermes-agent@sha256:3154e1df5f6941583f4a369fcc9581bbf9cb9d502676e91b557e90cc576923e1
|
- registry.bstein.dev/bstein/hermes-agent@sha256:cce1f65dc7d30fdce4b1748cc7d3434b9761ae9946a8c8be6049204c503d60a8
|
||||||
- registry.bstein.dev/bstein/hermes-webui@sha256:9c2fe8341c7b650e08d10acead3151b19e2af737863268bafb39b3d9517575b1
|
- registry.bstein.dev/bstein/hermes-webui@sha256:9c2fe8341c7b650e08d10acead3151b19e2af737863268bafb39b3d9517575b1
|
||||||
- kind: Deployment
|
- kind: Deployment
|
||||||
namespace: hermes
|
namespace: hermes
|
||||||
@ -615,7 +624,7 @@ workloads:
|
|||||||
nodeSelector: {}
|
nodeSelector: {}
|
||||||
images:
|
images:
|
||||||
- quay.io/oauth2-proxy/oauth2-proxy:v7.15.3@sha256:10a1165743a192e1940b4708fb9647027185ce11a681a1c5519b442ff7f1f561
|
- quay.io/oauth2-proxy/oauth2-proxy:v7.15.3@sha256:10a1165743a192e1940b4708fb9647027185ce11a681a1c5519b442ff7f1f561
|
||||||
- registry.bstein.dev/bstein/hermes-agent@sha256:3154e1df5f6941583f4a369fcc9581bbf9cb9d502676e91b557e90cc576923e1
|
- registry.bstein.dev/bstein/hermes-agent@sha256:cce1f65dc7d30fdce4b1748cc7d3434b9761ae9946a8c8be6049204c503d60a8
|
||||||
- kind: Deployment
|
- kind: Deployment
|
||||||
namespace: hermes
|
namespace: hermes
|
||||||
name: hermes-chat-router
|
name: hermes-chat-router
|
||||||
@ -788,7 +797,7 @@ workloads:
|
|||||||
serviceAccountName: hermes-chat
|
serviceAccountName: hermes-chat
|
||||||
nodeSelector: {}
|
nodeSelector: {}
|
||||||
images:
|
images:
|
||||||
- registry.bstein.dev/bstein/hermes-agent@sha256:3154e1df5f6941583f4a369fcc9581bbf9cb9d502676e91b557e90cc576923e1
|
- registry.bstein.dev/bstein/hermes-agent@sha256:cce1f65dc7d30fdce4b1748cc7d3434b9761ae9946a8c8be6049204c503d60a8
|
||||||
- registry.bstein.dev/bstein/hermes-webui@sha256:9c2fe8341c7b650e08d10acead3151b19e2af737863268bafb39b3d9517575b1
|
- registry.bstein.dev/bstein/hermes-webui@sha256:9c2fe8341c7b650e08d10acead3151b19e2af737863268bafb39b3d9517575b1
|
||||||
- kind: Deployment
|
- kind: Deployment
|
||||||
namespace: jellyfin
|
namespace: jellyfin
|
||||||
@ -1028,7 +1037,7 @@ workloads:
|
|||||||
kubernetes.io/arch: amd64
|
kubernetes.io/arch: amd64
|
||||||
kubernetes.io/os: linux
|
kubernetes.io/os: linux
|
||||||
images:
|
images:
|
||||||
- registry.bstein.dev/bstein/metis-sentinel:0.1.0-292-amd64
|
- registry.bstein.dev/bstein/metis-sentinel:0.1.0-293-amd64
|
||||||
- kind: DaemonSet
|
- kind: DaemonSet
|
||||||
namespace: maintenance
|
namespace: maintenance
|
||||||
name: metis-sentinel-arm64
|
name: metis-sentinel-arm64
|
||||||
@ -1039,7 +1048,7 @@ workloads:
|
|||||||
kubernetes.io/arch: arm64
|
kubernetes.io/arch: arm64
|
||||||
kubernetes.io/os: linux
|
kubernetes.io/os: linux
|
||||||
images:
|
images:
|
||||||
- registry.bstein.dev/bstein/metis-sentinel:0.1.0-291-arm64
|
- registry.bstein.dev/bstein/metis-sentinel:0.1.0-293-arm64
|
||||||
- kind: DaemonSet
|
- kind: DaemonSet
|
||||||
namespace: maintenance
|
namespace: maintenance
|
||||||
name: node-image-sweeper
|
name: node-image-sweeper
|
||||||
@ -1118,7 +1127,7 @@ workloads:
|
|||||||
serviceAccountName: metis
|
serviceAccountName: metis
|
||||||
nodeSelector: {}
|
nodeSelector: {}
|
||||||
images:
|
images:
|
||||||
- registry.bstein.dev/bstein/metis:0.1.0-291-arm64
|
- registry.bstein.dev/bstein/metis:0.1.0-293-arm64
|
||||||
- kind: Deployment
|
- kind: Deployment
|
||||||
namespace: maintenance
|
namespace: maintenance
|
||||||
name: oauth2-proxy-metis
|
name: oauth2-proxy-metis
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -135,11 +135,10 @@ alert fires instead.
|
|||||||
|
|
||||||
## Credentials
|
## Credentials
|
||||||
|
|
||||||
- Ariadne → Hermes: `Authorization: Bearer` key shared via the
|
- Ariadne → Hermes: `Authorization: Bearer` key from
|
||||||
`hermes-api-server-key` Secret present in both `hermes` and `maintenance`
|
`kv/atlas/hermes/triage-api`. Vault Agent injects it into both workloads at
|
||||||
namespaces (Hermes's init container seeds it into the persistent `.env`).
|
runtime; it is not stored in either persistent Hermes state or a manually
|
||||||
NOTE: currently manually created (Vault migration pending — see plan
|
maintained Kubernetes Secret. Rotate the Vault value and roll both tracked
|
||||||
handoff); rotating = write new value to both Secrets, restart hermes
|
deployments through Flux.
|
||||||
deployment and ariadne deployment.
|
|
||||||
- Ariadne → Jenkins: existing `JENKINS_API_USER/TOKEN` from Vault
|
- Ariadne → Jenkins: existing `JENKINS_API_USER/TOKEN` from Vault
|
||||||
(`atlas/maintenance/ariadne-db`).
|
(`atlas/maintenance/ariadne-db`).
|
||||||
|
|||||||
@ -3,15 +3,26 @@ set -euo pipefail
|
|||||||
|
|
||||||
# === CONFIG ===
|
# === CONFIG ===
|
||||||
STYX_USER="styx"
|
STYX_USER="styx"
|
||||||
STYX_PASS="TempPass#123" # change at first login
|
STYX_PASS="${STYX_PASS:-}"
|
||||||
STYX_HOSTNAME="styx"
|
STYX_HOSTNAME="styx"
|
||||||
SSH_PUBKEY="" # e.g., 'ssh-ed25519 AAAA... your@host' (optional)
|
SSH_PUBKEY="${SSH_PUBKEY:-}"
|
||||||
|
|
||||||
# === helpers ===
|
# === helpers ===
|
||||||
require_root() {
|
require_root() {
|
||||||
if [[ $EUID -ne 0 ]]; then exec sudo -E "$0" "$@"; fi
|
if [[ $EUID -ne 0 ]]; then exec sudo -E "$0" "$@"; fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
require_runtime_access() {
|
||||||
|
[[ -n "$STYX_PASS" ]] || {
|
||||||
|
echo "STYX_PASS must be injected at runtime from Vault." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[[ -n "$SSH_PUBKEY" ]] || {
|
||||||
|
echo "SSH_PUBKEY must be injected at runtime from Vault." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ensure_binfmt_arm64() {
|
ensure_binfmt_arm64() {
|
||||||
# If binfmt for arm64 isn't registered, register it via Docker (idempotent).
|
# If binfmt for arm64 isn't registered, register it via Docker (idempotent).
|
||||||
if [[ ! -e /proc/sys/fs/binfmt_misc/qemu-aarch64 ]]; then
|
if [[ ! -e /proc/sys/fs/binfmt_misc/qemu-aarch64 ]]; then
|
||||||
@ -176,6 +187,7 @@ cleanup() {
|
|||||||
|
|
||||||
main() {
|
main() {
|
||||||
require_root
|
require_root
|
||||||
|
require_runtime_access
|
||||||
ensure_binfmt_arm64
|
ensure_binfmt_arm64
|
||||||
find_parts
|
find_parts
|
||||||
trap 'echo "ERROR at line $LINENO" >&2; cleanup' ERR INT
|
trap 'echo "ERROR at line $LINENO" >&2; cleanup' ERR INT
|
||||||
@ -185,7 +197,7 @@ main() {
|
|||||||
boot_tweaks
|
boot_tweaks
|
||||||
cleanup
|
cleanup
|
||||||
echo "✅ Done. Move the NVMe to the Pi and boot."
|
echo "✅ Done. Move the NVMe to the Pi and boot."
|
||||||
echo " Login: user '${STYX_USER}' pass '${STYX_PASS}' (change with 'passwd')."
|
echo " Login: user '${STYX_USER}' (change the injected bootstrap password with 'passwd')."
|
||||||
echo " Quick checks on the Pi:"
|
echo " Quick checks on the Pi:"
|
||||||
echo " sudo i2cdetect -y 1"
|
echo " sudo i2cdetect -y 1"
|
||||||
echo " rpicam-still -n -o test.jpg # (if rpicam-apps installed)"
|
echo " rpicam-still -n -o test.jpg # (if rpicam-apps installed)"
|
||||||
|
|||||||
@ -15,8 +15,8 @@ TGT_BOOT="/mnt/target-boot"
|
|||||||
|
|
||||||
STYX_USER="styx"
|
STYX_USER="styx"
|
||||||
STYX_HOSTNAME="titan-ag"
|
STYX_HOSTNAME="titan-ag"
|
||||||
STYX_PASS="TempPass#123" # will be forced to change on first login via cloud-init
|
STYX_PASS="${STYX_PASS:-}"
|
||||||
SSH_PUBKEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion"
|
SSH_PUBKEY="${SSH_PUBKEY:-}"
|
||||||
|
|
||||||
# Video / input prefs
|
# Video / input prefs
|
||||||
DSI_FLAGS="video=DSI-1:800x480@60D video=HDMI-A-1:off video=HDMI-A-2:off"
|
DSI_FLAGS="video=DSI-1:800x480@60D video=HDMI-A-1:off video=HDMI-A-2:off"
|
||||||
@ -27,6 +27,11 @@ need(){ command -v "$1" >/dev/null || fatal "Missing tool: $1"; }
|
|||||||
|
|
||||||
require_root(){ [[ $EUID -eq 0 ]] || exec sudo -E "$0" "$@"; }
|
require_root(){ [[ $EUID -eq 0 ]] || exec sudo -E "$0" "$@"; }
|
||||||
|
|
||||||
|
require_runtime_access() {
|
||||||
|
[[ -n "$STYX_PASS" ]] || fatal "STYX_PASS must be injected at runtime from Vault."
|
||||||
|
[[ -n "$SSH_PUBKEY" ]] || fatal "SSH_PUBKEY must be injected at runtime from Vault."
|
||||||
|
}
|
||||||
|
|
||||||
part() {
|
part() {
|
||||||
local n="$1"
|
local n="$1"
|
||||||
if [[ "$NVME" =~ [0-9]$ ]]; then
|
if [[ "$NVME" =~ [0-9]$ ]]; then
|
||||||
@ -544,6 +549,7 @@ cleanup(){
|
|||||||
|
|
||||||
main(){
|
main(){
|
||||||
require_root
|
require_root
|
||||||
|
require_runtime_access
|
||||||
need losetup; need parted; need rsync
|
need losetup; need parted; need rsync
|
||||||
auto_detect_target_disk
|
auto_detect_target_disk
|
||||||
echo "Target disk: $NVME"
|
echo "Target disk: $NVME"
|
||||||
|
|||||||
@ -3,6 +3,6 @@
|
|||||||
"helmrelease_host_hints": 23,
|
"helmrelease_host_hints": 23,
|
||||||
"http_endpoints": 61,
|
"http_endpoints": 61,
|
||||||
"services": 91,
|
"services": 91,
|
||||||
"workloads": 124
|
"workloads": 125
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -892,6 +892,19 @@
|
|||||||
"wger/server@sha256:710588b78af4e0aa0b4d8a8061e4563e16eae80eeaccfe7f9e0d9cbdd7f0cbc5"
|
"wger/server@sha256:710588b78af4e0aa0b4d8a8061e4563e16eae80eeaccfe7f9e0d9cbdd7f0cbc5"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "DaemonSet",
|
||||||
|
"namespace": "hermes",
|
||||||
|
"name": "hermes-node-ssh-access",
|
||||||
|
"labels": {
|
||||||
|
"app": "hermes-node-ssh-access"
|
||||||
|
},
|
||||||
|
"serviceAccountName": "hermes-node-ssh-access",
|
||||||
|
"nodeSelector": {},
|
||||||
|
"images": [
|
||||||
|
"busybox:1.37"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "Deployment",
|
"kind": "Deployment",
|
||||||
"namespace": "hermes",
|
"namespace": "hermes",
|
||||||
@ -902,7 +915,7 @@
|
|||||||
"serviceAccountName": "hermes-triage",
|
"serviceAccountName": "hermes-triage",
|
||||||
"nodeSelector": {},
|
"nodeSelector": {},
|
||||||
"images": [
|
"images": [
|
||||||
"registry.bstein.dev/bstein/hermes-agent@sha256:3154e1df5f6941583f4a369fcc9581bbf9cb9d502676e91b557e90cc576923e1",
|
"registry.bstein.dev/bstein/hermes-agent@sha256:cce1f65dc7d30fdce4b1748cc7d3434b9761ae9946a8c8be6049204c503d60a8",
|
||||||
"registry.bstein.dev/bstein/hermes-webui@sha256:9c2fe8341c7b650e08d10acead3151b19e2af737863268bafb39b3d9517575b1"
|
"registry.bstein.dev/bstein/hermes-webui@sha256:9c2fe8341c7b650e08d10acead3151b19e2af737863268bafb39b3d9517575b1"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -917,7 +930,7 @@
|
|||||||
"nodeSelector": {},
|
"nodeSelector": {},
|
||||||
"images": [
|
"images": [
|
||||||
"quay.io/oauth2-proxy/oauth2-proxy:v7.15.3@sha256:10a1165743a192e1940b4708fb9647027185ce11a681a1c5519b442ff7f1f561",
|
"quay.io/oauth2-proxy/oauth2-proxy:v7.15.3@sha256:10a1165743a192e1940b4708fb9647027185ce11a681a1c5519b442ff7f1f561",
|
||||||
"registry.bstein.dev/bstein/hermes-agent@sha256:3154e1df5f6941583f4a369fcc9581bbf9cb9d502676e91b557e90cc576923e1"
|
"registry.bstein.dev/bstein/hermes-agent@sha256:cce1f65dc7d30fdce4b1748cc7d3434b9761ae9946a8c8be6049204c503d60a8"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1164,7 +1177,7 @@
|
|||||||
"serviceAccountName": "hermes-chat",
|
"serviceAccountName": "hermes-chat",
|
||||||
"nodeSelector": {},
|
"nodeSelector": {},
|
||||||
"images": [
|
"images": [
|
||||||
"registry.bstein.dev/bstein/hermes-agent@sha256:3154e1df5f6941583f4a369fcc9581bbf9cb9d502676e91b557e90cc576923e1",
|
"registry.bstein.dev/bstein/hermes-agent@sha256:cce1f65dc7d30fdce4b1748cc7d3434b9761ae9946a8c8be6049204c503d60a8",
|
||||||
"registry.bstein.dev/bstein/hermes-webui@sha256:9c2fe8341c7b650e08d10acead3151b19e2af737863268bafb39b3d9517575b1"
|
"registry.bstein.dev/bstein/hermes-webui@sha256:9c2fe8341c7b650e08d10acead3151b19e2af737863268bafb39b3d9517575b1"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -1515,7 +1528,7 @@
|
|||||||
"kubernetes.io/os": "linux"
|
"kubernetes.io/os": "linux"
|
||||||
},
|
},
|
||||||
"images": [
|
"images": [
|
||||||
"registry.bstein.dev/bstein/metis-sentinel:0.1.0-292-amd64"
|
"registry.bstein.dev/bstein/metis-sentinel:0.1.0-293-amd64"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1531,7 +1544,7 @@
|
|||||||
"kubernetes.io/os": "linux"
|
"kubernetes.io/os": "linux"
|
||||||
},
|
},
|
||||||
"images": [
|
"images": [
|
||||||
"registry.bstein.dev/bstein/metis-sentinel:0.1.0-291-arm64"
|
"registry.bstein.dev/bstein/metis-sentinel:0.1.0-293-arm64"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1648,7 +1661,7 @@
|
|||||||
"serviceAccountName": "metis",
|
"serviceAccountName": "metis",
|
||||||
"nodeSelector": {},
|
"nodeSelector": {},
|
||||||
"images": [
|
"images": [
|
||||||
"registry.bstein.dev/bstein/metis:0.1.0-291-arm64"
|
"registry.bstein.dev/bstein/metis:0.1.0-293-arm64"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -596,6 +596,15 @@ workloads:
|
|||||||
images:
|
images:
|
||||||
- nginx:1.27.5-alpine@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10
|
- nginx:1.27.5-alpine@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10
|
||||||
- wger/server@sha256:710588b78af4e0aa0b4d8a8061e4563e16eae80eeaccfe7f9e0d9cbdd7f0cbc5
|
- wger/server@sha256:710588b78af4e0aa0b4d8a8061e4563e16eae80eeaccfe7f9e0d9cbdd7f0cbc5
|
||||||
|
- kind: DaemonSet
|
||||||
|
namespace: hermes
|
||||||
|
name: hermes-node-ssh-access
|
||||||
|
labels:
|
||||||
|
app: hermes-node-ssh-access
|
||||||
|
serviceAccountName: hermes-node-ssh-access
|
||||||
|
nodeSelector: {}
|
||||||
|
images:
|
||||||
|
- busybox:1.37
|
||||||
- kind: Deployment
|
- kind: Deployment
|
||||||
namespace: hermes
|
namespace: hermes
|
||||||
name: hermes
|
name: hermes
|
||||||
@ -604,7 +613,7 @@ workloads:
|
|||||||
serviceAccountName: hermes-triage
|
serviceAccountName: hermes-triage
|
||||||
nodeSelector: {}
|
nodeSelector: {}
|
||||||
images:
|
images:
|
||||||
- registry.bstein.dev/bstein/hermes-agent@sha256:3154e1df5f6941583f4a369fcc9581bbf9cb9d502676e91b557e90cc576923e1
|
- registry.bstein.dev/bstein/hermes-agent@sha256:cce1f65dc7d30fdce4b1748cc7d3434b9761ae9946a8c8be6049204c503d60a8
|
||||||
- registry.bstein.dev/bstein/hermes-webui@sha256:9c2fe8341c7b650e08d10acead3151b19e2af737863268bafb39b3d9517575b1
|
- registry.bstein.dev/bstein/hermes-webui@sha256:9c2fe8341c7b650e08d10acead3151b19e2af737863268bafb39b3d9517575b1
|
||||||
- kind: Deployment
|
- kind: Deployment
|
||||||
namespace: hermes
|
namespace: hermes
|
||||||
@ -615,7 +624,7 @@ workloads:
|
|||||||
nodeSelector: {}
|
nodeSelector: {}
|
||||||
images:
|
images:
|
||||||
- quay.io/oauth2-proxy/oauth2-proxy:v7.15.3@sha256:10a1165743a192e1940b4708fb9647027185ce11a681a1c5519b442ff7f1f561
|
- quay.io/oauth2-proxy/oauth2-proxy:v7.15.3@sha256:10a1165743a192e1940b4708fb9647027185ce11a681a1c5519b442ff7f1f561
|
||||||
- registry.bstein.dev/bstein/hermes-agent@sha256:3154e1df5f6941583f4a369fcc9581bbf9cb9d502676e91b557e90cc576923e1
|
- registry.bstein.dev/bstein/hermes-agent@sha256:cce1f65dc7d30fdce4b1748cc7d3434b9761ae9946a8c8be6049204c503d60a8
|
||||||
- kind: Deployment
|
- kind: Deployment
|
||||||
namespace: hermes
|
namespace: hermes
|
||||||
name: hermes-chat-router
|
name: hermes-chat-router
|
||||||
@ -788,7 +797,7 @@ workloads:
|
|||||||
serviceAccountName: hermes-chat
|
serviceAccountName: hermes-chat
|
||||||
nodeSelector: {}
|
nodeSelector: {}
|
||||||
images:
|
images:
|
||||||
- registry.bstein.dev/bstein/hermes-agent@sha256:3154e1df5f6941583f4a369fcc9581bbf9cb9d502676e91b557e90cc576923e1
|
- registry.bstein.dev/bstein/hermes-agent@sha256:cce1f65dc7d30fdce4b1748cc7d3434b9761ae9946a8c8be6049204c503d60a8
|
||||||
- registry.bstein.dev/bstein/hermes-webui@sha256:9c2fe8341c7b650e08d10acead3151b19e2af737863268bafb39b3d9517575b1
|
- registry.bstein.dev/bstein/hermes-webui@sha256:9c2fe8341c7b650e08d10acead3151b19e2af737863268bafb39b3d9517575b1
|
||||||
- kind: Deployment
|
- kind: Deployment
|
||||||
namespace: jellyfin
|
namespace: jellyfin
|
||||||
@ -1028,7 +1037,7 @@ workloads:
|
|||||||
kubernetes.io/arch: amd64
|
kubernetes.io/arch: amd64
|
||||||
kubernetes.io/os: linux
|
kubernetes.io/os: linux
|
||||||
images:
|
images:
|
||||||
- registry.bstein.dev/bstein/metis-sentinel:0.1.0-292-amd64
|
- registry.bstein.dev/bstein/metis-sentinel:0.1.0-293-amd64
|
||||||
- kind: DaemonSet
|
- kind: DaemonSet
|
||||||
namespace: maintenance
|
namespace: maintenance
|
||||||
name: metis-sentinel-arm64
|
name: metis-sentinel-arm64
|
||||||
@ -1039,7 +1048,7 @@ workloads:
|
|||||||
kubernetes.io/arch: arm64
|
kubernetes.io/arch: arm64
|
||||||
kubernetes.io/os: linux
|
kubernetes.io/os: linux
|
||||||
images:
|
images:
|
||||||
- registry.bstein.dev/bstein/metis-sentinel:0.1.0-291-arm64
|
- registry.bstein.dev/bstein/metis-sentinel:0.1.0-293-arm64
|
||||||
- kind: DaemonSet
|
- kind: DaemonSet
|
||||||
namespace: maintenance
|
namespace: maintenance
|
||||||
name: node-image-sweeper
|
name: node-image-sweeper
|
||||||
@ -1118,7 +1127,7 @@ workloads:
|
|||||||
serviceAccountName: metis
|
serviceAccountName: metis
|
||||||
nodeSelector: {}
|
nodeSelector: {}
|
||||||
images:
|
images:
|
||||||
- registry.bstein.dev/bstein/metis:0.1.0-291-arm64
|
- registry.bstein.dev/bstein/metis:0.1.0-293-arm64
|
||||||
- kind: Deployment
|
- kind: Deployment
|
||||||
namespace: maintenance
|
namespace: maintenance
|
||||||
name: oauth2-proxy-metis
|
name: oauth2-proxy-metis
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -135,11 +135,10 @@ alert fires instead.
|
|||||||
|
|
||||||
## Credentials
|
## Credentials
|
||||||
|
|
||||||
- Ariadne → Hermes: `Authorization: Bearer` key shared via the
|
- Ariadne → Hermes: `Authorization: Bearer` key from
|
||||||
`hermes-api-server-key` Secret present in both `hermes` and `maintenance`
|
`kv/atlas/hermes/triage-api`. Vault Agent injects it into both workloads at
|
||||||
namespaces (Hermes's init container seeds it into the persistent `.env`).
|
runtime; it is not stored in either persistent Hermes state or a manually
|
||||||
NOTE: currently manually created (Vault migration pending — see plan
|
maintained Kubernetes Secret. Rotate the Vault value and roll both tracked
|
||||||
handoff); rotating = write new value to both Secrets, restart hermes
|
deployments through Flux.
|
||||||
deployment and ariadne deployment.
|
|
||||||
- Ariadne → Jenkins: existing `JENKINS_API_USER/TOKEN` from Vault
|
- Ariadne → Jenkins: existing `JENKINS_API_USER/TOKEN` from Vault
|
||||||
(`atlas/maintenance/ariadne-db`).
|
(`atlas/maintenance/ariadne-db`).
|
||||||
|
|||||||
@ -81,6 +81,25 @@ generate_token() {
|
|||||||
--raw 2>/dev/null
|
--raw 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
token_is_valid() {
|
||||||
|
username=$1
|
||||||
|
token=$2
|
||||||
|
[ -n "${token}" ] || return 1
|
||||||
|
|
||||||
|
response_file=$(mktemp)
|
||||||
|
status=$(curl --silent --show-error \
|
||||||
|
--output "${response_file}" \
|
||||||
|
--write-out '%{http_code}' \
|
||||||
|
--header "Authorization: token ${token}" \
|
||||||
|
"${api_url}/user")
|
||||||
|
if [ "${status}" = 200 ] && grep -Eq "\"login\"[[:space:]]*:[[:space:]]*\"${username}\"" "${response_file}"; then
|
||||||
|
rm -f "${response_file}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
rm -f "${response_file}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
api_request() {
|
api_request() {
|
||||||
method=$1
|
method=$1
|
||||||
path=$2
|
path=$2
|
||||||
@ -143,6 +162,31 @@ vault_login() {
|
|||||||
[ -n "${vault_token}" ] || die "Vault login did not return a client token"
|
[ -n "${vault_token}" ] || die "Vault login did not return a client token"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vault_read_field() {
|
||||||
|
path=$1
|
||||||
|
field=$2
|
||||||
|
response_file=$(mktemp)
|
||||||
|
status=$(curl --silent --show-error \
|
||||||
|
--output "${response_file}" \
|
||||||
|
--write-out '%{http_code}' \
|
||||||
|
--header "X-Vault-Token: ${vault_token}" \
|
||||||
|
"${vault_addr}/v1/kv/data/atlas/${path}")
|
||||||
|
case "${status}" in
|
||||||
|
200)
|
||||||
|
value=$(sed -n "s/.*\"${field}\":\"\([^\"]*\)\".*/\1/p" "${response_file}")
|
||||||
|
;;
|
||||||
|
404)
|
||||||
|
value=
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
rm -f "${response_file}"
|
||||||
|
die "read Vault path ${path} returned HTTP ${status}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
rm -f "${response_file}"
|
||||||
|
printf '%s' "${value}"
|
||||||
|
}
|
||||||
|
|
||||||
vault_write() {
|
vault_write() {
|
||||||
path=$1
|
path=$1
|
||||||
payload=$2
|
payload=$2
|
||||||
@ -231,9 +275,16 @@ transfer_managed_repositories() {
|
|||||||
wait_for_gitea
|
wait_for_gitea
|
||||||
ensure_user "${reconciler_user}" "${reconciler_email}" true
|
ensure_user "${reconciler_user}" "${reconciler_email}" true
|
||||||
ensure_user "${hermes_user}" "${hermes_email}" false
|
ensure_user "${hermes_user}" "${hermes_email}" false
|
||||||
|
vault_login
|
||||||
|
|
||||||
reconciler_token=$(generate_token "${reconciler_user}")
|
reconciler_token=$(vault_read_field gitea/atlas-reconciler token)
|
||||||
hermes_token=$(generate_token "${hermes_user}")
|
if ! token_is_valid "${reconciler_user}" "${reconciler_token}"; then
|
||||||
|
reconciler_token=$(generate_token "${reconciler_user}")
|
||||||
|
fi
|
||||||
|
hermes_token=$(vault_read_field hermes/developer-gitea token)
|
||||||
|
if ! token_is_valid "${hermes_user}" "${hermes_token}"; then
|
||||||
|
hermes_token=$(generate_token "${hermes_user}")
|
||||||
|
fi
|
||||||
[ -n "${reconciler_token}" ] || die "reconciler token generation failed"
|
[ -n "${reconciler_token}" ] || die "reconciler token generation failed"
|
||||||
[ -n "${hermes_token}" ] || die "Hermes token generation failed"
|
[ -n "${hermes_token}" ] || die "Hermes token generation failed"
|
||||||
|
|
||||||
@ -267,7 +318,6 @@ expect_status "${status}" 204 "add Hermes to Atlas contributors"
|
|||||||
|
|
||||||
transfer_managed_repositories
|
transfer_managed_repositories
|
||||||
|
|
||||||
vault_login
|
|
||||||
vault_write gitea/atlas-reconciler "{\"data\":{\"username\":\"${reconciler_user}\",\"token\":\"${reconciler_token}\",\"base_url\":\"${public_url}\"}}"
|
vault_write gitea/atlas-reconciler "{\"data\":{\"username\":\"${reconciler_user}\",\"token\":\"${reconciler_token}\",\"base_url\":\"${public_url}\"}}"
|
||||||
vault_write hermes/developer-gitea "{\"data\":{\"username\":\"${hermes_user}\",\"email\":\"${hermes_email}\",\"token\":\"${hermes_token}\",\"base_url\":\"${public_url}\"}}"
|
vault_write hermes/developer-gitea "{\"data\":{\"username\":\"${hermes_user}\",\"email\":\"${hermes_email}\",\"token\":\"${hermes_token}\",\"base_url\":\"${public_url}\"}}"
|
||||||
|
|
||||||
|
|||||||
@ -299,6 +299,19 @@ data:
|
|||||||
impact, avoid fleet-wide destructive commands, and reflect persistent host
|
impact, avoid fleet-wide destructive commands, and reflect persistent host
|
||||||
configuration in the appropriate tracked provisioning source.
|
configuration in the appropriate tracked provisioning source.
|
||||||
|
|
||||||
|
Treat every credential, SSH identity, host-trust record, and
|
||||||
|
credential-bearing access-client state as runtime-only Vault data. Keep
|
||||||
|
non-secret endpoints, RBAC policy, and client behavior in tracked GitOps
|
||||||
|
configuration, but never reconstruct secret or trust values in a manifest,
|
||||||
|
commit, transcript, or command output. Use dedicated Kubernetes-auth roles
|
||||||
|
with Vault Agent or Secrets Store CSI injection for workloads. For an
|
||||||
|
explicitly authorized operator Vault change, obtain the existing admin
|
||||||
|
credential from the `vault` namespace at execution time, keep it only in
|
||||||
|
process scope, suppress value-bearing output, and unset it immediately
|
||||||
|
afterward. Verify public-key fingerprints through an already authenticated
|
||||||
|
path before changing trust or authorization, and rotate an identity if any
|
||||||
|
private material may have escaped its runtime boundary.
|
||||||
|
|
||||||
After a failed tool attempt, failing test, contradicted claim, rejected
|
After a failed tool attempt, failing test, contradicted claim, rejected
|
||||||
review, or incomplete evidence, do not repeat the same low-capability plan.
|
review, or incomplete evidence, do not repeat the same low-capability plan.
|
||||||
Reassess the objective and raise provider capability or effort by at least
|
Reassess the objective and raise provider capability or effort by at least
|
||||||
@ -319,8 +332,9 @@ data:
|
|||||||
Use `/route status` to inspect the current decision, `/route auto`
|
Use `/route status` to inspect the current decision, `/route auto`
|
||||||
for automatic routing, or `/route manual <codex|claude>
|
for automatic routing, or `/route manual <codex|claude>
|
||||||
<low|medium|high|xhigh> [model]` for a persistent override. The first native
|
<low|medium|high|xhigh> [model]` for a persistent override. The first native
|
||||||
Codex worker requires one device-code login; subsequent sessions persist on
|
Codex worker requires one device-code login; refreshed provider credentials
|
||||||
the agent volume. The owner workspace includes cluster-admin Kubernetes
|
persist through Vault while client caches remain disposable. The owner
|
||||||
|
workspace includes cluster-admin Kubernetes
|
||||||
access plus `kubectl`, `flux`, `helm`, `kustomize`, `vault`, `sops`, `age`,
|
access plus `kubectl`, `flux`, `helm`, `kustomize`, `vault`, `sops`, `age`,
|
||||||
`terraform`, `k9s`, `jq`, `yq`, `gh`, Git, SSH, Python, Node, the
|
`terraform`, `k9s`, `jq`, `yq`, `gh`, Git, SSH, Python, Node, the
|
||||||
browser/computer tools, and the native provider CLIs.
|
browser/computer tools, and the native provider CLIs.
|
||||||
|
|||||||
@ -25,13 +25,23 @@ spec:
|
|||||||
ai.bstein.dev/execution: Hermes Kanban with durable direct Codex and Claude Code CLI workers
|
ai.bstein.dev/execution: Hermes Kanban with durable direct Codex and Claude Code CLI workers
|
||||||
ai.bstein.dev/model-policy: Jetson-assisted AUTO routing, low through xhigh, cross-provider fallback
|
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/placement: rpi5 preferred; Jetson deferred until state storage is available
|
||||||
ai.bstein.dev/config-rev: "20260815-worker-restart-recovery"
|
ai.bstein.dev/config-rev: "20260815-runtime-access-boundary"
|
||||||
vault.hashicorp.com/agent-inject: "true"
|
vault.hashicorp.com/agent-inject: "true"
|
||||||
vault.hashicorp.com/role: hermes-agent
|
vault.hashicorp.com/role: hermes-agent
|
||||||
vault.hashicorp.com/agent-inject-secret-anthropic-token: kv/data/atlas/hermes/agent-tokens
|
vault.hashicorp.com/agent-inject-secret-agent-api-key: kv/data/atlas/hermes/agent-tokens
|
||||||
vault.hashicorp.com/agent-inject-template-anthropic-token: |
|
vault.hashicorp.com/agent-inject-template-agent-api-key: |
|
||||||
{{- with secret "kv/data/atlas/hermes/agent-tokens" -}}
|
{{- with secret "kv/data/atlas/hermes/agent-tokens" -}}
|
||||||
{{ .Data.data.anthropic_oauth_token }}
|
{{ .Data.data.agent_api_key }}
|
||||||
|
{{- end }}
|
||||||
|
vault.hashicorp.com/agent-inject-secret-claude-credentials: kv/data/atlas/hermes/agent-tokens
|
||||||
|
vault.hashicorp.com/agent-inject-template-claude-credentials: |
|
||||||
|
{{- with secret "kv/data/atlas/hermes/agent-tokens" -}}
|
||||||
|
{{ .Data.data.claude_credentials_json }}
|
||||||
|
{{- end }}
|
||||||
|
vault.hashicorp.com/agent-inject-secret-codex-auth: kv/data/atlas/hermes/agent-tokens
|
||||||
|
vault.hashicorp.com/agent-inject-template-codex-auth: |
|
||||||
|
{{- with secret "kv/data/atlas/hermes/agent-tokens" -}}
|
||||||
|
{{ .Data.data.codex_auth_json }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
vault.hashicorp.com/agent-inject-secret-gitea-token: kv/data/atlas/hermes/developer-gitea
|
vault.hashicorp.com/agent-inject-secret-gitea-token: kv/data/atlas/hermes/developer-gitea
|
||||||
vault.hashicorp.com/agent-inject-template-gitea-token: |
|
vault.hashicorp.com/agent-inject-template-gitea-token: |
|
||||||
@ -140,15 +150,16 @@ spec:
|
|||||||
cp /config/SOUL.md /opt/data/SOUL.md
|
cp /config/SOUL.md /opt/data/SOUL.md
|
||||||
cp /config/AGENTS.md /opt/data/workspace/AGENTS.md
|
cp /config/AGENTS.md /opt/data/workspace/AGENTS.md
|
||||||
cp /config/START-HERE.md /opt/data/workspace/START-HERE.md
|
cp /config/START-HERE.md /opt/data/workspace/START-HERE.md
|
||||||
cp /vault/secrets/node-ssh-config /opt/data/home/.ssh/config
|
rm -f \
|
||||||
cp /vault/secrets/node-ssh-known-hosts /opt/data/home/.ssh/known_hosts
|
/opt/data/auth.json \
|
||||||
if [ -s /vault/secrets/node-ssh-private-key ]; then
|
/opt/data/home/.claude/.credentials.json \
|
||||||
cp /vault/secrets/node-ssh-private-key /opt/data/home/.ssh/id_ed25519_atlas_nodes
|
/opt/data/home/.codex/auth.json \
|
||||||
chmod 0600 /opt/data/home/.ssh/id_ed25519_atlas_nodes
|
|
||||||
fi
|
|
||||||
chmod 0600 \
|
|
||||||
/opt/data/home/.ssh/config \
|
/opt/data/home/.ssh/config \
|
||||||
/opt/data/home/.ssh/known_hosts
|
/opt/data/home/.ssh/known_hosts \
|
||||||
|
/opt/data/home/.ssh/id_ed25519_atlas_nodes
|
||||||
|
ln -s /runtime-access/node-ssh-config /opt/data/home/.ssh/config
|
||||||
|
ln -s /runtime-access/node-ssh-known-hosts /opt/data/home/.ssh/known_hosts
|
||||||
|
ln -s /runtime-access/node-ssh-private-key /opt/data/home/.ssh/id_ed25519_atlas_nodes
|
||||||
chmod 0700 /opt/data/home/.ssh
|
chmod 0700 /opt/data/home/.ssh
|
||||||
touch "${env_file}"
|
touch "${env_file}"
|
||||||
upsert_env() {
|
upsert_env() {
|
||||||
@ -157,26 +168,19 @@ spec:
|
|||||||
{ grep -v "^${key}=" "${env_file}" || true; printf '%s=%s\n' "${key}" "${value}"; } > "${env_file}.tmp"
|
{ grep -v "^${key}=" "${env_file}" || true; printf '%s=%s\n' "${key}" "${value}"; } > "${env_file}.tmp"
|
||||||
mv "${env_file}.tmp" "${env_file}"
|
mv "${env_file}.tmp" "${env_file}"
|
||||||
}
|
}
|
||||||
if ! grep -q '^API_SERVER_KEY=' "${env_file}"; then
|
for key in ANTHROPIC_API_KEY API_SERVER_KEY CLAUDE_API_KEY CLAUDE_CODE_OAUTH_TOKEN GITEA_TOKEN GITEA_USERNAME HERMES_IMAGE_BROKER_KEY OPENAI_API_KEY; do
|
||||||
api_key="$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')"
|
grep -v "^${key}=" "${env_file}" > "${env_file}.tmp" || true
|
||||||
upsert_env API_SERVER_KEY "${api_key}"
|
mv "${env_file}.tmp" "${env_file}"
|
||||||
fi
|
done
|
||||||
if [ -s /vault/secrets/anthropic-token ]; then
|
for profile_env in /opt/data/profiles/*/.env; do
|
||||||
token="$(tr -d '\r\n' < /vault/secrets/anthropic-token)"
|
[ -f "${profile_env}" ] || continue
|
||||||
[ -z "${token}" ] || upsert_env CLAUDE_CODE_OAUTH_TOKEN "${token}"
|
for key in ANTHROPIC_API_KEY API_SERVER_KEY CLAUDE_API_KEY CLAUDE_CODE_OAUTH_TOKEN GITEA_TOKEN GITEA_USERNAME HERMES_IMAGE_BROKER_KEY OPENAI_API_KEY; do
|
||||||
fi
|
grep -v "^${key}=" "${profile_env}" > "${profile_env}.tmp" || true
|
||||||
if [ -s /vault/secrets/gitea-token ]; then
|
mv "${profile_env}.tmp" "${profile_env}"
|
||||||
token="$(tr -d '\r\n' < /vault/secrets/gitea-token)"
|
done
|
||||||
case "${token}" in ""|"<no value>"|"<nil>") ;; *) upsert_env GITEA_TOKEN "${token}" ;; esac
|
chmod 0600 "${profile_env}"
|
||||||
fi
|
chown 10000:10000 "${profile_env}"
|
||||||
if [ -s /vault/secrets/gitea-username ]; then
|
done
|
||||||
username="$(tr -d '\r\n' < /vault/secrets/gitea-username)"
|
|
||||||
case "${username}" in ""|"<no value>"|"<nil>") ;; *) upsert_env GITEA_USERNAME "${username}" ;; esac
|
|
||||||
fi
|
|
||||||
if [ -s /vault/secrets/chat-relay-key ]; then
|
|
||||||
token="$(tr -d '\r\n' < /vault/secrets/chat-relay-key)"
|
|
||||||
[ -z "${token}" ] || upsert_env HERMES_IMAGE_BROKER_KEY "${token}"
|
|
||||||
fi
|
|
||||||
upsert_env GIT_ASKPASS /opt/coordinator/gitea_askpass.sh
|
upsert_env GIT_ASKPASS /opt/coordinator/gitea_askpass.sh
|
||||||
upsert_env GIT_TERMINAL_PROMPT 0
|
upsert_env GIT_TERMINAL_PROMPT 0
|
||||||
chmod 0600 "${env_file}"
|
chmod 0600 "${env_file}"
|
||||||
@ -191,8 +195,6 @@ spec:
|
|||||||
/opt/data/home/.codex \
|
/opt/data/home/.codex \
|
||||||
/opt/data/home/.kube \
|
/opt/data/home/.kube \
|
||||||
/opt/data/home/.ssh \
|
/opt/data/home/.ssh \
|
||||||
/opt/data/home/.ssh/config \
|
|
||||||
/opt/data/home/.ssh/known_hosts \
|
|
||||||
/opt/data/cli-lanes \
|
/opt/data/cli-lanes \
|
||||||
/opt/data/logs \
|
/opt/data/logs \
|
||||||
/opt/data/provider-health \
|
/opt/data/provider-health \
|
||||||
@ -207,9 +209,6 @@ spec:
|
|||||||
/opt/data/workspace/AGENTS.md \
|
/opt/data/workspace/AGENTS.md \
|
||||||
/opt/data/workspace/START-HERE.md \
|
/opt/data/workspace/START-HERE.md \
|
||||||
"${env_file}"
|
"${env_file}"
|
||||||
if [ -f /opt/data/home/.ssh/id_ed25519_atlas_nodes ]; then
|
|
||||||
chown 10000:10000 /opt/data/home/.ssh/id_ed25519_atlas_nodes
|
|
||||||
fi
|
|
||||||
securityContext:
|
securityContext:
|
||||||
allowPrivilegeEscalation: false
|
allowPrivilegeEscalation: false
|
||||||
runAsUser: 0
|
runAsUser: 0
|
||||||
@ -225,6 +224,26 @@ spec:
|
|||||||
resources:
|
resources:
|
||||||
requests: {cpu: 25m, memory: 32Mi}
|
requests: {cpu: 25m, memory: 32Mi}
|
||||||
limits: {cpu: 100m, memory: 64Mi}
|
limits: {cpu: 100m, memory: 64Mi}
|
||||||
|
- name: stage-runtime-access
|
||||||
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- /opt/hermes/.venv/bin/python
|
||||||
|
- /opt/coordinator/stage_runtime_access.py
|
||||||
|
- agent
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
runAsUser: 0
|
||||||
|
runAsGroup: 0
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
volumeMounts:
|
||||||
|
- {name: home, mountPath: /opt/data}
|
||||||
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
|
- {name: runtime-access, mountPath: /runtime-access}
|
||||||
|
resources:
|
||||||
|
requests: {cpu: 25m, memory: 32Mi}
|
||||||
|
limits: {cpu: 100m, memory: 64Mi}
|
||||||
- name: install-agent-tools
|
- name: install-agent-tools
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
@ -380,6 +399,52 @@ spec:
|
|||||||
resources:
|
resources:
|
||||||
requests: {cpu: 25m, memory: 64Mi}
|
requests: {cpu: 25m, memory: 64Mi}
|
||||||
limits: {cpu: 100m, memory: 128Mi}
|
limits: {cpu: 100m, memory: 128Mi}
|
||||||
|
- name: patch-web-session-activity
|
||||||
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- /opt/hermes/.venv/bin/python
|
||||||
|
- /opt/coordinator/patch_web_session_activity.py
|
||||||
|
- /opt/hermes/hermes_cli/web_server.py
|
||||||
|
- /patched/web_server.py
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
runAsUser: 10000
|
||||||
|
runAsGroup: 10000
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
volumeMounts:
|
||||||
|
- name: coordinator
|
||||||
|
mountPath: /opt/coordinator
|
||||||
|
readOnly: true
|
||||||
|
- name: web-server-patch
|
||||||
|
mountPath: /patched
|
||||||
|
resources:
|
||||||
|
requests: {cpu: 25m, memory: 64Mi}
|
||||||
|
limits: {cpu: 100m, memory: 128Mi}
|
||||||
|
- name: patch-subprocess-secret-boundary
|
||||||
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -ec
|
||||||
|
- |
|
||||||
|
/opt/hermes/.venv/bin/python /opt/coordinator/patch_subprocess_secret_boundary.py \
|
||||||
|
/opt/hermes/tools/environments/local.py /patched/local.py
|
||||||
|
/opt/hermes/.venv/bin/python /opt/coordinator/patch_process_output_redaction.py \
|
||||||
|
/opt/hermes/tools/process_registry.py /patched/process_registry.py
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
runAsUser: 10000
|
||||||
|
runAsGroup: 10000
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
volumeMounts:
|
||||||
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
|
- {name: subprocess-secret-patch, mountPath: /patched}
|
||||||
|
resources:
|
||||||
|
requests: {cpu: 25m, memory: 64Mi}
|
||||||
|
limits: {cpu: 100m, memory: 128Mi}
|
||||||
- name: patch-codex-runtime
|
- name: patch-codex-runtime
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
@ -422,10 +487,10 @@ spec:
|
|||||||
- --once
|
- --once
|
||||||
env:
|
env:
|
||||||
- {name: HERMES_HOME, value: /opt/data}
|
- {name: HERMES_HOME, value: /opt/data}
|
||||||
- {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json}
|
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
|
||||||
- {name: HOME, value: /opt/data/home}
|
- {name: HOME, value: /opt/data/home}
|
||||||
- {name: CODEX_HOME, value: /opt/data/home/.codex}
|
- {name: CODEX_HOME, value: /runtime-access/codex}
|
||||||
- {name: CLAUDE_CONFIG_DIR, value: /opt/data/home/.claude}
|
- {name: CLAUDE_CONFIG_DIR, value: /runtime-access/claude}
|
||||||
- {name: PYTHONPATH, value: /opt/hermes}
|
- {name: PYTHONPATH, value: /opt/hermes}
|
||||||
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
|
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
|
||||||
- {name: HERMES_CASSANDRA_ACTIVE_WORKTREE, value: /opt/data/workspace/projects/cassandra-hermes-v69}
|
- {name: HERMES_CASSANDRA_ACTIVE_WORKTREE, value: /opt/data/workspace/projects/cassandra-hermes-v69}
|
||||||
@ -438,7 +503,7 @@ spec:
|
|||||||
type: RuntimeDefault
|
type: RuntimeDefault
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- {name: home, mountPath: /opt/data}
|
- {name: home, mountPath: /opt/data}
|
||||||
- {name: provider-auth, mountPath: /shared-auth}
|
- {name: runtime-access, mountPath: /runtime-access}
|
||||||
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
||||||
- {name: routing-catalog, mountPath: /routing-catalog}
|
- {name: routing-catalog, mountPath: /routing-catalog}
|
||||||
@ -464,10 +529,10 @@ spec:
|
|||||||
/opt/hermes/.venv/bin/python /opt/coordinator/hermes_coordinator.py --once
|
/opt/hermes/.venv/bin/python /opt/coordinator/hermes_coordinator.py --once
|
||||||
env:
|
env:
|
||||||
- {name: HERMES_HOME, value: /opt/data}
|
- {name: HERMES_HOME, value: /opt/data}
|
||||||
- {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json}
|
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
|
||||||
- {name: HOME, value: /opt/data/home}
|
- {name: HOME, value: /opt/data/home}
|
||||||
- {name: CODEX_HOME, value: /opt/data/home/.codex}
|
- {name: CODEX_HOME, value: /runtime-access/codex}
|
||||||
- {name: CLAUDE_CONFIG_DIR, value: /opt/data/home/.claude}
|
- {name: CLAUDE_CONFIG_DIR, value: /runtime-access/claude}
|
||||||
- {name: PYTHONPATH, value: /opt/hermes}
|
- {name: PYTHONPATH, value: /opt/hermes}
|
||||||
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
|
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
|
||||||
- {name: HERMES_CASSANDRA_ACTIVE_WORKTREE, value: /opt/data/workspace/projects/cassandra-hermes-v69}
|
- {name: HERMES_CASSANDRA_ACTIVE_WORKTREE, value: /opt/data/workspace/projects/cassandra-hermes-v69}
|
||||||
@ -480,7 +545,7 @@ spec:
|
|||||||
type: RuntimeDefault
|
type: RuntimeDefault
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- {name: home, mountPath: /opt/data}
|
- {name: home, mountPath: /opt/data}
|
||||||
- {name: provider-auth, mountPath: /shared-auth}
|
- {name: runtime-access, mountPath: /runtime-access}
|
||||||
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
||||||
- {name: routing-catalog, mountPath: /routing-catalog}
|
- {name: routing-catalog, mountPath: /routing-catalog}
|
||||||
@ -518,16 +583,19 @@ spec:
|
|||||||
set -a
|
set -a
|
||||||
. /opt/data/.env
|
. /opt/data/.env
|
||||||
set +a
|
set +a
|
||||||
|
API_SERVER_KEY="$(tr -d '\r\n' < /runtime-access/agent-api-key)"
|
||||||
|
test -n "${API_SERVER_KEY}"
|
||||||
|
export API_SERVER_KEY
|
||||||
exec /init /opt/hermes/docker/main-wrapper.sh gateway run
|
exec /init /opt/hermes/docker/main-wrapper.sh gateway run
|
||||||
ports:
|
ports:
|
||||||
- {name: api, containerPort: 8642, protocol: TCP}
|
- {name: api, containerPort: 8642, protocol: TCP}
|
||||||
- {name: dashboard, containerPort: 9119, protocol: TCP}
|
- {name: dashboard, containerPort: 9119, protocol: TCP}
|
||||||
env:
|
env:
|
||||||
- {name: HERMES_HOME, value: /opt/data}
|
- {name: HERMES_HOME, value: /opt/data}
|
||||||
- {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json}
|
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
|
||||||
- {name: HOME, value: /opt/data/home}
|
- {name: HOME, value: /opt/data/home}
|
||||||
- {name: CODEX_HOME, value: /opt/data/home/.codex}
|
- {name: CODEX_HOME, value: /runtime-access/codex}
|
||||||
- {name: CLAUDE_CONFIG_DIR, value: /opt/data/home/.claude}
|
- {name: CLAUDE_CONFIG_DIR, value: /runtime-access/claude}
|
||||||
- {name: KUBECONFIG, value: /opt/data/home/.kube/config}
|
- {name: KUBECONFIG, value: /opt/data/home/.kube/config}
|
||||||
- {name: PATH, value: /opt/coordinator:/opt/data/tools/bin:/opt/data/home/.local/bin:/opt/hermes/.venv/bin:/opt/hermes/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin}
|
- {name: PATH, value: /opt/coordinator:/opt/data/tools/bin:/opt/data/home/.local/bin:/opt/hermes/.venv/bin:/opt/hermes/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin}
|
||||||
- {name: PLAYWRIGHT_BROWSERS_PATH, value: /opt/hermes/.playwright}
|
- {name: PLAYWRIGHT_BROWSERS_PATH, value: /opt/hermes/.playwright}
|
||||||
@ -551,11 +619,13 @@ spec:
|
|||||||
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
|
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
|
||||||
- {name: HERMES_CODEX_HEALTH_PATH, value: /opt/data/provider-health/codex.json}
|
- {name: HERMES_CODEX_HEALTH_PATH, value: /opt/data/provider-health/codex.json}
|
||||||
- {name: HERMES_CLAUDE_HEALTH_PATH, value: /opt/data/provider-health/claude.json}
|
- {name: HERMES_CLAUDE_HEALTH_PATH, value: /opt/data/provider-health/claude.json}
|
||||||
|
- {name: HERMES_IMAGE_BROKER_KEY_FILE, value: /runtime-access/chat-relay-key}
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- {name: home, mountPath: /opt/data}
|
- {name: home, mountPath: /opt/data}
|
||||||
- {name: provider-auth, mountPath: /shared-auth}
|
- {name: runtime-access, mountPath: /runtime-access}
|
||||||
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
||||||
|
- {name: web-server-patch, mountPath: /opt/hermes/hermes_cli/web_server.py, subPath: web_server.py}
|
||||||
- {name: codex-runtime-patch, mountPath: /opt/hermes/hermes_cli/runtime_provider.py, subPath: runtime_provider.py}
|
- {name: codex-runtime-patch, mountPath: /opt/hermes/hermes_cli/runtime_provider.py, subPath: runtime_provider.py}
|
||||||
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/transports/codex_app_server_session.py, subPath: codex_app_server_session.py}
|
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/transports/codex_app_server_session.py, subPath: codex_app_server_session.py}
|
||||||
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/codex_runtime.py, subPath: codex_runtime.py}
|
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/codex_runtime.py, subPath: codex_runtime.py}
|
||||||
@ -564,6 +634,8 @@ spec:
|
|||||||
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/auxiliary_client.py, subPath: auxiliary_client.py}
|
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/auxiliary_client.py, subPath: auxiliary_client.py}
|
||||||
- {name: tui-gateway-patch, mountPath: /opt/hermes/tui_gateway/server.py, subPath: server.py}
|
- {name: tui-gateway-patch, mountPath: /opt/hermes/tui_gateway/server.py, subPath: server.py}
|
||||||
- {name: api-server-patch, mountPath: /opt/hermes/gateway/platforms/api_server.py, subPath: api_server.py}
|
- {name: api-server-patch, mountPath: /opt/hermes/gateway/platforms/api_server.py, subPath: api_server.py}
|
||||||
|
- {name: subprocess-secret-patch, mountPath: /opt/hermes/tools/environments/local.py, subPath: local.py}
|
||||||
|
- {name: subprocess-secret-patch, mountPath: /opt/hermes/tools/process_registry.py, subPath: process_registry.py}
|
||||||
- {name: kubeconfig, mountPath: /opt/data/home/.kube/config, subPath: config, readOnly: true}
|
- {name: kubeconfig, mountPath: /opt/data/home/.kube/config, subPath: config, readOnly: true}
|
||||||
- {name: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true}
|
- {name: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true}
|
||||||
- {name: routing-catalog, mountPath: /routing-catalog, readOnly: true}
|
- {name: routing-catalog, mountPath: /routing-catalog, readOnly: true}
|
||||||
@ -699,9 +771,9 @@ spec:
|
|||||||
env:
|
env:
|
||||||
- {name: HOME, value: /opt/data/home}
|
- {name: HOME, value: /opt/data/home}
|
||||||
- {name: HERMES_HOME, value: /opt/data}
|
- {name: HERMES_HOME, value: /opt/data}
|
||||||
- {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json}
|
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
|
||||||
- {name: CODEX_HOME, value: /opt/data/home/.codex}
|
- {name: CODEX_HOME, value: /runtime-access/codex}
|
||||||
- {name: CLAUDE_CONFIG_DIR, value: /opt/data/home/.claude}
|
- {name: CLAUDE_CONFIG_DIR, value: /runtime-access/claude}
|
||||||
- {name: KUBECONFIG, value: /opt/data/home/.kube/config}
|
- {name: KUBECONFIG, value: /opt/data/home/.kube/config}
|
||||||
- {name: PYTHONPATH, value: /opt/hermes}
|
- {name: PYTHONPATH, value: /opt/hermes}
|
||||||
- {name: PATH, value: /opt/coordinator:/opt/data/tools/bin:/opt/data/home/.local/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin}
|
- {name: PATH, value: /opt/coordinator:/opt/data/tools/bin:/opt/data/home/.local/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin}
|
||||||
@ -715,7 +787,7 @@ spec:
|
|||||||
- {name: HERMES_CLAUDE_HEALTH_PATH, value: /opt/data/provider-health/claude.json}
|
- {name: HERMES_CLAUDE_HEALTH_PATH, value: /opt/data/provider-health/claude.json}
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- {name: home, mountPath: /opt/data}
|
- {name: home, mountPath: /opt/data}
|
||||||
- {name: provider-auth, mountPath: /shared-auth}
|
- {name: runtime-access, mountPath: /runtime-access}
|
||||||
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
||||||
- {name: codex-runtime-patch, mountPath: /opt/hermes/hermes_cli/runtime_provider.py, subPath: runtime_provider.py}
|
- {name: codex-runtime-patch, mountPath: /opt/hermes/hermes_cli/runtime_provider.py, subPath: runtime_provider.py}
|
||||||
@ -725,6 +797,8 @@ spec:
|
|||||||
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/conversation_loop.py, subPath: conversation_loop.py}
|
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/conversation_loop.py, subPath: conversation_loop.py}
|
||||||
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/auxiliary_client.py, subPath: auxiliary_client.py}
|
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/auxiliary_client.py, subPath: auxiliary_client.py}
|
||||||
- {name: tui-gateway-patch, mountPath: /opt/hermes/tui_gateway/server.py, subPath: server.py}
|
- {name: tui-gateway-patch, mountPath: /opt/hermes/tui_gateway/server.py, subPath: server.py}
|
||||||
|
- {name: subprocess-secret-patch, mountPath: /opt/hermes/tools/environments/local.py, subPath: local.py}
|
||||||
|
- {name: subprocess-secret-patch, mountPath: /opt/hermes/tools/process_registry.py, subPath: process_registry.py}
|
||||||
- {name: kubeconfig, mountPath: /opt/data/home/.kube/config, subPath: config, readOnly: true}
|
- {name: kubeconfig, mountPath: /opt/data/home/.kube/config, subPath: config, readOnly: true}
|
||||||
- {name: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true}
|
- {name: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true}
|
||||||
- {name: routing-catalog, mountPath: /routing-catalog, readOnly: true}
|
- {name: routing-catalog, mountPath: /routing-catalog, readOnly: true}
|
||||||
@ -769,10 +843,10 @@ spec:
|
|||||||
exec /opt/hermes/.venv/bin/python /opt/coordinator/cli_lane_runner.py
|
exec /opt/hermes/.venv/bin/python /opt/coordinator/cli_lane_runner.py
|
||||||
env:
|
env:
|
||||||
- {name: HERMES_HOME, value: /opt/data}
|
- {name: HERMES_HOME, value: /opt/data}
|
||||||
- {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json}
|
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
|
||||||
- {name: HOME, value: /opt/data/home}
|
- {name: HOME, value: /opt/data/home}
|
||||||
- {name: CODEX_HOME, value: /opt/data/home/.codex}
|
- {name: CODEX_HOME, value: /runtime-access/codex}
|
||||||
- {name: CLAUDE_CONFIG_DIR, value: /opt/data/home/.claude}
|
- {name: CLAUDE_CONFIG_DIR, value: /runtime-access/claude}
|
||||||
- {name: KUBECONFIG, value: /opt/data/home/.kube/config}
|
- {name: KUBECONFIG, value: /opt/data/home/.kube/config}
|
||||||
- {name: PYTHONPATH, value: /opt/hermes}
|
- {name: PYTHONPATH, value: /opt/hermes}
|
||||||
- {name: HERMES_CLI_LANE_CONCURRENCY, value: "4"}
|
- {name: HERMES_CLI_LANE_CONCURRENCY, value: "4"}
|
||||||
@ -790,7 +864,7 @@ spec:
|
|||||||
type: RuntimeDefault
|
type: RuntimeDefault
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- {name: home, mountPath: /opt/data}
|
- {name: home, mountPath: /opt/data}
|
||||||
- {name: provider-auth, mountPath: /shared-auth}
|
- {name: runtime-access, mountPath: /runtime-access}
|
||||||
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
||||||
- {name: kubeconfig, mountPath: /opt/data/home/.kube/config, subPath: config, readOnly: true}
|
- {name: kubeconfig, mountPath: /opt/data/home/.kube/config, subPath: config, readOnly: true}
|
||||||
@ -805,10 +879,10 @@ spec:
|
|||||||
command: [/opt/hermes/.venv/bin/python, /opt/coordinator/hermes_coordinator.py, --loop, --interval, "3600"]
|
command: [/opt/hermes/.venv/bin/python, /opt/coordinator/hermes_coordinator.py, --loop, --interval, "3600"]
|
||||||
env:
|
env:
|
||||||
- {name: HERMES_HOME, value: /opt/data}
|
- {name: HERMES_HOME, value: /opt/data}
|
||||||
- {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json}
|
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
|
||||||
- {name: HOME, value: /opt/data/home}
|
- {name: HOME, value: /opt/data/home}
|
||||||
- {name: CODEX_HOME, value: /opt/data/home/.codex}
|
- {name: CODEX_HOME, value: /runtime-access/codex}
|
||||||
- {name: CLAUDE_CONFIG_DIR, value: /opt/data/home/.claude}
|
- {name: CLAUDE_CONFIG_DIR, value: /runtime-access/claude}
|
||||||
- {name: PYTHONPATH, value: /opt/hermes}
|
- {name: PYTHONPATH, value: /opt/hermes}
|
||||||
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
|
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
|
||||||
- {name: HERMES_CASSANDRA_ACTIVE_WORKTREE, value: /opt/data/workspace/projects/cassandra-hermes-v69}
|
- {name: HERMES_CASSANDRA_ACTIVE_WORKTREE, value: /opt/data/workspace/projects/cassandra-hermes-v69}
|
||||||
@ -821,33 +895,57 @@ spec:
|
|||||||
type: RuntimeDefault
|
type: RuntimeDefault
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- {name: home, mountPath: /opt/data}
|
- {name: home, mountPath: /opt/data}
|
||||||
- {name: provider-auth, mountPath: /shared-auth}
|
- {name: runtime-access, mountPath: /runtime-access}
|
||||||
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
||||||
- {name: routing-catalog, mountPath: /routing-catalog}
|
- {name: routing-catalog, mountPath: /routing-catalog}
|
||||||
resources:
|
resources:
|
||||||
requests: {cpu: 25m, memory: 64Mi}
|
requests: {cpu: 25m, memory: 64Mi}
|
||||||
limits: {cpu: 250m, memory: 512Mi}
|
limits: {cpu: 250m, memory: 512Mi}
|
||||||
|
- name: credential-sync
|
||||||
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- /opt/hermes/.venv/bin/python
|
||||||
|
- /opt/coordinator/sync_runtime_credentials.py
|
||||||
|
env:
|
||||||
|
- {name: HERMES_CREDENTIAL_SYNC_VAULT_ROLE, value: hermes-credential-sync}
|
||||||
|
- {name: HERMES_CREDENTIAL_SYNC_INTERVAL, value: "60"}
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 10000
|
||||||
|
runAsGroup: 10000
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
volumeMounts:
|
||||||
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
|
- {name: runtime-access, mountPath: /runtime-access/claude, subPath: claude, readOnly: true}
|
||||||
|
- {name: runtime-access, mountPath: /runtime-access/codex, subPath: codex, readOnly: true}
|
||||||
|
resources:
|
||||||
|
requests: {cpu: 10m, memory: 32Mi}
|
||||||
|
limits: {cpu: 100m, memory: 128Mi}
|
||||||
- name: image-broker
|
- name: image-broker
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command: [/bin/sh, -ec]
|
command: [/bin/sh, -ec]
|
||||||
args:
|
args:
|
||||||
- |
|
- |
|
||||||
set -a
|
|
||||||
. /opt/data/.env
|
|
||||||
set +a
|
|
||||||
exec /opt/hermes/.venv/bin/python /opt/coordinator/image_broker.py
|
exec /opt/hermes/.venv/bin/python /opt/coordinator/image_broker.py
|
||||||
ports:
|
ports:
|
||||||
- {name: image-broker, containerPort: 9002, protocol: TCP}
|
- {name: image-broker, containerPort: 9002, protocol: TCP}
|
||||||
env:
|
env:
|
||||||
- {name: HERMES_HOME, value: /opt/data}
|
- {name: HERMES_HOME, value: /opt/data}
|
||||||
- {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json}
|
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
|
||||||
- {name: HOME, value: /opt/data/home}
|
- {name: HOME, value: /opt/data/home}
|
||||||
- {name: CODEX_HOME, value: /opt/data/home/.codex}
|
- {name: CODEX_HOME, value: /runtime-access/codex}
|
||||||
- {name: PYTHONPATH, value: /opt/hermes}
|
- {name: PYTHONPATH, value: /opt/hermes}
|
||||||
- {name: HERMES_IMAGE_BROKER_DEFAULT_MODEL, value: atlas-image-auto-high}
|
- {name: HERMES_IMAGE_BROKER_DEFAULT_MODEL, value: atlas-image-auto-high}
|
||||||
- {name: HERMES_IMAGE_BROKER_LISTEN_PORT, value: "9002"}
|
- {name: HERMES_IMAGE_BROKER_LISTEN_PORT, value: "9002"}
|
||||||
|
- {name: HERMES_IMAGE_BROKER_KEY_FILE, value: /runtime-access/chat-relay-key}
|
||||||
- {name: HERMES_LOCAL_IMAGE_URL, value: http://hermes-local-image.hermes.svc.cluster.local:9004}
|
- {name: HERMES_LOCAL_IMAGE_URL, value: http://hermes-local-image.hermes.svc.cluster.local:9004}
|
||||||
- {name: HERMES_IMAGE_POLICY_PATH, value: /etc/hermes-image-policy/policy.json}
|
- {name: HERMES_IMAGE_POLICY_PATH, value: /etc/hermes-image-policy/policy.json}
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
@ -870,7 +968,7 @@ spec:
|
|||||||
type: RuntimeDefault
|
type: RuntimeDefault
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- {name: home, mountPath: /opt/data}
|
- {name: home, mountPath: /opt/data}
|
||||||
- {name: provider-auth, mountPath: /shared-auth}
|
- {name: runtime-access, mountPath: /runtime-access}
|
||||||
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/auxiliary_client.py, subPath: auxiliary_client.py}
|
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/auxiliary_client.py, subPath: auxiliary_client.py}
|
||||||
- {name: image-policy, mountPath: /etc/hermes-image-policy, readOnly: true}
|
- {name: image-policy, mountPath: /etc/hermes-image-policy, readOnly: true}
|
||||||
@ -884,19 +982,17 @@ spec:
|
|||||||
command: [/bin/sh, -ec]
|
command: [/bin/sh, -ec]
|
||||||
args:
|
args:
|
||||||
- |
|
- |
|
||||||
set -a
|
|
||||||
. /opt/data/.env
|
|
||||||
set +a
|
|
||||||
exec /opt/hermes/.venv/bin/python /opt/coordinator/codex_broker.py
|
exec /opt/hermes/.venv/bin/python /opt/coordinator/codex_broker.py
|
||||||
ports:
|
ports:
|
||||||
- {name: codex-broker, containerPort: 9003, protocol: TCP}
|
- {name: codex-broker, containerPort: 9003, protocol: TCP}
|
||||||
env:
|
env:
|
||||||
- {name: HERMES_HOME, value: /opt/data}
|
- {name: HERMES_HOME, value: /opt/data}
|
||||||
- {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json}
|
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
|
||||||
- {name: HOME, value: /opt/data/home}
|
- {name: HOME, value: /opt/data/home}
|
||||||
- {name: CODEX_HOME, value: /opt/data/home/.codex}
|
- {name: CODEX_HOME, value: /runtime-access/codex}
|
||||||
- {name: PYTHONPATH, value: /opt/hermes}
|
- {name: PYTHONPATH, value: /opt/hermes}
|
||||||
- {name: HERMES_CODEX_BROKER_LISTEN_PORT, value: "9003"}
|
- {name: HERMES_CODEX_BROKER_LISTEN_PORT, value: "9003"}
|
||||||
|
- {name: HERMES_CODEX_BROKER_KEY_FILE, value: /runtime-access/chat-relay-key}
|
||||||
- {name: HERMES_CODEX_HEALTH_PATH, value: /opt/data/provider-health/codex.json}
|
- {name: HERMES_CODEX_HEALTH_PATH, value: /opt/data/provider-health/codex.json}
|
||||||
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
|
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
@ -919,7 +1015,7 @@ spec:
|
|||||||
type: RuntimeDefault
|
type: RuntimeDefault
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- {name: home, mountPath: /opt/data}
|
- {name: home, mountPath: /opt/data}
|
||||||
- {name: provider-auth, mountPath: /shared-auth}
|
- {name: runtime-access, mountPath: /runtime-access}
|
||||||
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/auxiliary_client.py, subPath: auxiliary_client.py}
|
- {name: codex-runtime-patch, mountPath: /opt/hermes/agent/auxiliary_client.py, subPath: auxiliary_client.py}
|
||||||
- {name: tmp, mountPath: /tmp}
|
- {name: tmp, mountPath: /tmp}
|
||||||
@ -933,23 +1029,21 @@ spec:
|
|||||||
command: [/bin/sh, -ec]
|
command: [/bin/sh, -ec]
|
||||||
args:
|
args:
|
||||||
- |
|
- |
|
||||||
set -a
|
|
||||||
. /opt/data/.env
|
|
||||||
set +a
|
|
||||||
unset ANTHROPIC_API_KEY CLAUDE_API_KEY
|
unset ANTHROPIC_API_KEY CLAUDE_API_KEY
|
||||||
exec /opt/hermes/.venv/bin/python /opt/coordinator/claude_oauth_broker.py
|
exec /opt/hermes/.venv/bin/python /opt/coordinator/claude_oauth_broker.py
|
||||||
ports:
|
ports:
|
||||||
- {name: claude-broker, containerPort: 9006, protocol: TCP}
|
- {name: claude-broker, containerPort: 9006, protocol: TCP}
|
||||||
env:
|
env:
|
||||||
- {name: HERMES_HOME, value: /opt/data}
|
- {name: HERMES_HOME, value: /opt/data}
|
||||||
- {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json}
|
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
|
||||||
- {name: HOME, value: /opt/data/home}
|
- {name: HOME, value: /opt/data/home}
|
||||||
- {name: CODEX_HOME, value: /opt/data/home/.codex}
|
- {name: CODEX_HOME, value: /runtime-access/codex}
|
||||||
- {name: CLAUDE_CONFIG_DIR, value: /opt/data/home/.claude}
|
- {name: CLAUDE_CONFIG_DIR, value: /runtime-access/claude}
|
||||||
- {name: PYTHONPATH, value: /opt/hermes}
|
- {name: PYTHONPATH, value: /opt/hermes}
|
||||||
- {name: PATH, value: /opt/coordinator:/opt/data/tools/bin:/opt/data/home/.local/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin}
|
- {name: PATH, value: /opt/coordinator:/opt/data/tools/bin:/opt/data/home/.local/bin:/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin}
|
||||||
- {name: HERMES_CLAUDE_BIN, value: /opt/coordinator/claude}
|
- {name: HERMES_CLAUDE_BIN, value: /opt/coordinator/claude}
|
||||||
- {name: HERMES_CLAUDE_BROKER_PORT, value: "9006"}
|
- {name: HERMES_CLAUDE_BROKER_PORT, value: "9006"}
|
||||||
|
- {name: HERMES_CLAUDE_BROKER_KEY_FILE, value: /runtime-access/chat-relay-key}
|
||||||
- {name: HERMES_CLAUDE_BROKER_READ_TIMEOUT, value: "1800"}
|
- {name: HERMES_CLAUDE_BROKER_READ_TIMEOUT, value: "1800"}
|
||||||
- {name: HERMES_CLAUDE_HEALTH_PATH, value: /opt/data/provider-health/claude.json}
|
- {name: HERMES_CLAUDE_HEALTH_PATH, value: /opt/data/provider-health/claude.json}
|
||||||
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
|
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
|
||||||
@ -973,7 +1067,7 @@ spec:
|
|||||||
type: RuntimeDefault
|
type: RuntimeDefault
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- {name: home, mountPath: /opt/data}
|
- {name: home, mountPath: /opt/data}
|
||||||
- {name: provider-auth, mountPath: /shared-auth}
|
- {name: runtime-access, mountPath: /runtime-access}
|
||||||
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
||||||
- {name: tmp, mountPath: /tmp}
|
- {name: tmp, mountPath: /tmp}
|
||||||
@ -985,9 +1079,10 @@ spec:
|
|||||||
- name: home
|
- name: home
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: hermes-agent-home
|
claimName: hermes-agent-home
|
||||||
- name: provider-auth
|
- name: runtime-access
|
||||||
persistentVolumeClaim:
|
emptyDir:
|
||||||
claimName: hermes-provider-auth
|
medium: Memory
|
||||||
|
sizeLimit: 128Mi
|
||||||
- name: routing-catalog
|
- name: routing-catalog
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: hermes-routing-catalog
|
claimName: hermes-routing-catalog
|
||||||
@ -1008,6 +1103,10 @@ spec:
|
|||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
- name: api-server-patch
|
- name: api-server-patch
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
|
- name: web-server-patch
|
||||||
|
emptyDir: {}
|
||||||
|
- name: subprocess-secret-patch
|
||||||
|
emptyDir: {}
|
||||||
- name: codex-runtime-patch
|
- name: codex-runtime-patch
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
- name: auto-router-plugin
|
- name: auto-router-plugin
|
||||||
|
|||||||
@ -20,7 +20,7 @@ spec:
|
|||||||
app: hermes-chat-router
|
app: hermes-chat-router
|
||||||
annotations:
|
annotations:
|
||||||
ai.bstein.dev/role: privacy-preserving-chat-tenant-router
|
ai.bstein.dev/role: privacy-preserving-chat-tenant-router
|
||||||
ai.bstein.dev/config-rev: "20260813-telegram-token-v3"
|
ai.bstein.dev/config-rev: "20260815-runtime-access-boundary"
|
||||||
vault.hashicorp.com/agent-inject: "true"
|
vault.hashicorp.com/agent-inject: "true"
|
||||||
vault.hashicorp.com/agent-pre-populate-only: "true"
|
vault.hashicorp.com/agent-pre-populate-only: "true"
|
||||||
vault.hashicorp.com/agent-init-first: "true"
|
vault.hashicorp.com/agent-init-first: "true"
|
||||||
|
|||||||
@ -29,14 +29,9 @@ spec:
|
|||||||
ai.bstein.dev/router-wire-contract: ollama-numeric-keepalive
|
ai.bstein.dev/router-wire-contract: ollama-numeric-keepalive
|
||||||
ai.bstein.dev/isolation: one Hermes process and PVC per Keycloak subject
|
ai.bstein.dev/isolation: one Hermes process and PVC per Keycloak subject
|
||||||
ai.bstein.dev/model-policy: uniform automatic policy with per-user overrides
|
ai.bstein.dev/model-policy: uniform automatic policy with per-user overrides
|
||||||
ai.bstein.dev/config-rev: "20260812-stream-recovery"
|
ai.bstein.dev/config-rev: "20260815-runtime-access-boundary"
|
||||||
vault.hashicorp.com/agent-inject: "true"
|
vault.hashicorp.com/agent-inject: "true"
|
||||||
vault.hashicorp.com/role: hermes-chat
|
vault.hashicorp.com/role: hermes-chat
|
||||||
vault.hashicorp.com/agent-inject-secret-anthropic-token: kv/data/atlas/hermes/agent-tokens
|
|
||||||
vault.hashicorp.com/agent-inject-template-anthropic-token: |
|
|
||||||
{{- with secret "kv/data/atlas/hermes/agent-tokens" -}}
|
|
||||||
{{ .Data.data.anthropic_oauth_token }}
|
|
||||||
{{- end }}
|
|
||||||
vault.hashicorp.com/agent-inject-secret-chat-relay-key: kv/data/atlas/hermes/chat-telegram
|
vault.hashicorp.com/agent-inject-secret-chat-relay-key: kv/data/atlas/hermes/chat-telegram
|
||||||
vault.hashicorp.com/agent-inject-template-chat-relay-key: |
|
vault.hashicorp.com/agent-inject-template-chat-relay-key: |
|
||||||
{{- with secret "kv/data/atlas/hermes/chat-telegram" -}}
|
{{- with secret "kv/data/atlas/hermes/chat-telegram" -}}
|
||||||
@ -111,26 +106,11 @@ spec:
|
|||||||
cp /config/SOUL.md /opt/data/SOUL.md
|
cp /config/SOUL.md /opt/data/SOUL.md
|
||||||
cp /config/AGENTS.md /opt/data/workspace/AGENTS.md
|
cp /config/AGENTS.md /opt/data/workspace/AGENTS.md
|
||||||
touch /opt/data/.env
|
touch /opt/data/.env
|
||||||
relay_key=""
|
for key in ANTHROPIC_API_KEY API_SERVER_KEY CLAUDE_API_KEY CLAUDE_CODE_OAUTH_TOKEN GITEA_TOKEN GITEA_USERNAME HERMES_IMAGE_BROKER_KEY OPENAI_API_KEY; do
|
||||||
if [ -s /vault/secrets/chat-relay-key ]; then
|
grep -v "^${key}=" /opt/data/.env > /opt/data/.env.tmp || true
|
||||||
relay_key="$(tr -d '\r\n' < /vault/secrets/chat-relay-key)"
|
mv /opt/data/.env.tmp /opt/data/.env
|
||||||
fi
|
done
|
||||||
if [ -z "${relay_key}" ]; then
|
rm -f /opt/data/auth.json
|
||||||
api_key="$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')"
|
|
||||||
relay_key="${api_key}"
|
|
||||||
fi
|
|
||||||
{ grep -v '^API_SERVER_KEY=' /opt/data/.env || true; printf 'API_SERVER_KEY=%s\n' "${relay_key}"; } > /opt/data/.env.tmp
|
|
||||||
mv /opt/data/.env.tmp /opt/data/.env
|
|
||||||
{ grep -v '^HERMES_IMAGE_BROKER_KEY=' /opt/data/.env || true; printf 'HERMES_IMAGE_BROKER_KEY=%s\n' "${relay_key}"; } > /opt/data/.env.tmp
|
|
||||||
mv /opt/data/.env.tmp /opt/data/.env
|
|
||||||
if [ -s /vault/secrets/anthropic-token ]; then
|
|
||||||
token="$(tr -d '\r\n' < /vault/secrets/anthropic-token)"
|
|
||||||
if [ -n "${token}" ]; then
|
|
||||||
{ grep -v '^CLAUDE_CODE_OAUTH_TOKEN=' /opt/data/.env || true; printf 'CLAUDE_CODE_OAUTH_TOKEN=%s\n' "${token}"; } > /opt/data/.env.tmp
|
|
||||||
mv /opt/data/.env.tmp /opt/data/.env
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
rm -f /vault/secrets/anthropic-token /vault/secrets/chat-relay-key
|
|
||||||
chmod 0600 /opt/data/.env
|
chmod 0600 /opt/data/.env
|
||||||
chown 10000:10000 \
|
chown 10000:10000 \
|
||||||
/opt/data \
|
/opt/data \
|
||||||
@ -157,6 +137,25 @@ spec:
|
|||||||
resources:
|
resources:
|
||||||
requests: {cpu: 25m, memory: 32Mi}
|
requests: {cpu: 25m, memory: 32Mi}
|
||||||
limits: {cpu: 100m, memory: 64Mi}
|
limits: {cpu: 100m, memory: 64Mi}
|
||||||
|
- name: stage-runtime-access
|
||||||
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- /opt/hermes/.venv/bin/python
|
||||||
|
- /opt/coordinator/stage_runtime_access.py
|
||||||
|
- chat
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
runAsUser: 0
|
||||||
|
runAsGroup: 0
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
volumeMounts:
|
||||||
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
|
- {name: runtime-access, mountPath: /runtime-access}
|
||||||
|
resources:
|
||||||
|
requests: {cpu: 25m, memory: 32Mi}
|
||||||
|
limits: {cpu: 100m, memory: 64Mi}
|
||||||
- name: patch-auth
|
- name: patch-auth
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
@ -197,6 +196,29 @@ spec:
|
|||||||
resources:
|
resources:
|
||||||
requests: {cpu: 25m, memory: 64Mi}
|
requests: {cpu: 25m, memory: 64Mi}
|
||||||
limits: {cpu: 100m, memory: 128Mi}
|
limits: {cpu: 100m, memory: 128Mi}
|
||||||
|
- name: patch-subprocess-secret-boundary
|
||||||
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -ec
|
||||||
|
- |
|
||||||
|
/opt/hermes/.venv/bin/python /opt/coordinator/patch_subprocess_secret_boundary.py \
|
||||||
|
/opt/hermes/tools/environments/local.py /patched/local.py
|
||||||
|
/opt/hermes/.venv/bin/python /opt/coordinator/patch_process_output_redaction.py \
|
||||||
|
/opt/hermes/tools/process_registry.py /patched/process_registry.py
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
runAsUser: 10000
|
||||||
|
runAsGroup: 10000
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
volumeMounts:
|
||||||
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
|
- {name: subprocess-secret-patch, mountPath: /patched}
|
||||||
|
resources:
|
||||||
|
requests: {cpu: 25m, memory: 64Mi}
|
||||||
|
limits: {cpu: 100m, memory: 128Mi}
|
||||||
containers:
|
containers:
|
||||||
- name: hermes
|
- name: hermes
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
||||||
@ -209,12 +231,15 @@ spec:
|
|||||||
set -a
|
set -a
|
||||||
. /opt/data/.env
|
. /opt/data/.env
|
||||||
set +a
|
set +a
|
||||||
|
API_SERVER_KEY="$(tr -d '\r\n' < /runtime-access/chat-relay-key)"
|
||||||
|
test -n "${API_SERVER_KEY}"
|
||||||
|
export API_SERVER_KEY
|
||||||
exec /opt/hermes/.venv/bin/hermes gateway run
|
exec /opt/hermes/.venv/bin/hermes gateway run
|
||||||
ports:
|
ports:
|
||||||
- {name: api, containerPort: 8642, protocol: TCP}
|
- {name: api, containerPort: 8642, protocol: TCP}
|
||||||
env:
|
env:
|
||||||
- {name: HERMES_HOME, value: /opt/data}
|
- {name: HERMES_HOME, value: /opt/data}
|
||||||
- {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json}
|
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
|
||||||
- {name: HOME, value: /opt/data/home}
|
- {name: HOME, value: /opt/data/home}
|
||||||
- {name: TERMINAL_CWD, value: /opt/data/workspace}
|
- {name: TERMINAL_CWD, value: /opt/data/workspace}
|
||||||
- {name: HERMES_WRITE_SAFE_ROOT, value: /opt/data/workspace}
|
- {name: HERMES_WRITE_SAFE_ROOT, value: /opt/data/workspace}
|
||||||
@ -228,16 +253,16 @@ spec:
|
|||||||
- {name: API_SERVER_PORT, value: "8642"}
|
- {name: API_SERVER_PORT, value: "8642"}
|
||||||
- {name: API_SERVER_CORS_ORIGINS, value: https://chat.hermes.bstein.dev}
|
- {name: API_SERVER_CORS_ORIGINS, value: https://chat.hermes.bstein.dev}
|
||||||
- {name: HERMES_IMAGE_BROKER_URL, value: http://hermes-image-broker.hermes.svc.cluster.local:9002}
|
- {name: HERMES_IMAGE_BROKER_URL, value: http://hermes-image-broker.hermes.svc.cluster.local:9002}
|
||||||
|
- {name: HERMES_IMAGE_BROKER_KEY_FILE, value: /runtime-access/chat-relay-key}
|
||||||
- {name: HERMES_AUTO_ROUTER_PROFILE, value: chat}
|
- {name: HERMES_AUTO_ROUTER_PROFILE, value: chat}
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- {name: home, mountPath: /opt/data}
|
- {name: home, mountPath: /opt/data}
|
||||||
- {name: workspace, mountPath: /opt/data/workspace}
|
- {name: workspace, mountPath: /opt/data/workspace}
|
||||||
# The shared provider pool uses auth.lock to serialize token refresh
|
- {name: runtime-access, mountPath: /runtime-access}
|
||||||
# across tenant gateways. Tenant files and conversations remain on
|
|
||||||
# their own PVCs; only provider credentials are shared here.
|
|
||||||
- {name: provider-auth, mountPath: /shared-auth}
|
|
||||||
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
- {name: auth-patch, mountPath: /opt/hermes/hermes_cli/auth.py, subPath: auth.py}
|
||||||
- {name: stream-recovery-patch, mountPath: /opt/hermes/agent/conversation_loop.py, subPath: conversation_loop.py}
|
- {name: stream-recovery-patch, mountPath: /opt/hermes/agent/conversation_loop.py, subPath: conversation_loop.py}
|
||||||
|
- {name: subprocess-secret-patch, mountPath: /opt/hermes/tools/environments/local.py, subPath: local.py}
|
||||||
|
- {name: subprocess-secret-patch, mountPath: /opt/hermes/tools/process_registry.py, subPath: process_registry.py}
|
||||||
- {name: image-plugin, mountPath: /opt/hermes/plugins/image_gen/atlas-broker, readOnly: true}
|
- {name: image-plugin, mountPath: /opt/hermes/plugins/image_gen/atlas-broker, readOnly: true}
|
||||||
- {name: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true}
|
- {name: auto-router-plugin, mountPath: /opt/data/plugins/auto-router, readOnly: true}
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
@ -265,7 +290,7 @@ spec:
|
|||||||
command: [/bin/sh, -ec]
|
command: [/bin/sh, -ec]
|
||||||
args:
|
args:
|
||||||
- |
|
- |
|
||||||
api_key="$(sed -n 's/^API_SERVER_KEY=//p' /opt/data/.env | tail -n 1)"
|
api_key="$(tr -d '\r\n' < /runtime-access/chat-relay-key)"
|
||||||
test -n "${api_key}"
|
test -n "${api_key}"
|
||||||
export API_SERVER_KEY="${api_key}"
|
export API_SERVER_KEY="${api_key}"
|
||||||
export HERMES_WEBUI_GATEWAY_API_KEY="${api_key}"
|
export HERMES_WEBUI_GATEWAY_API_KEY="${api_key}"
|
||||||
@ -274,7 +299,7 @@ spec:
|
|||||||
- {name: webui, containerPort: 8787, protocol: TCP}
|
- {name: webui, containerPort: 8787, protocol: TCP}
|
||||||
env:
|
env:
|
||||||
- {name: HERMES_HOME, value: /opt/data}
|
- {name: HERMES_HOME, value: /opt/data}
|
||||||
- {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json}
|
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
|
||||||
- {name: HOME, value: /opt/data/home}
|
- {name: HOME, value: /opt/data/home}
|
||||||
- {name: HERMES_WEBUI_AGENT_DIR, value: /opt/hermes}
|
- {name: HERMES_WEBUI_AGENT_DIR, value: /opt/hermes}
|
||||||
- {name: HERMES_WEBUI_HOST, value: 0.0.0.0}
|
- {name: HERMES_WEBUI_HOST, value: 0.0.0.0}
|
||||||
@ -302,7 +327,7 @@ spec:
|
|||||||
volumeMounts:
|
volumeMounts:
|
||||||
- {name: home, mountPath: /opt/data}
|
- {name: home, mountPath: /opt/data}
|
||||||
- {name: workspace, mountPath: /opt/data/workspace}
|
- {name: workspace, mountPath: /opt/data/workspace}
|
||||||
- {name: provider-auth, mountPath: /shared-auth, readOnly: true}
|
- {name: runtime-access, mountPath: /runtime-access, readOnly: true}
|
||||||
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
- {name: tmp, mountPath: /tmp}
|
- {name: tmp, mountPath: /tmp}
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
@ -338,11 +363,12 @@ spec:
|
|||||||
valueFrom:
|
valueFrom:
|
||||||
fieldRef:
|
fieldRef:
|
||||||
fieldPath: metadata.name
|
fieldPath: metadata.name
|
||||||
- {name: HERMES_MEDIA_RELAY_KEY_FILE, value: /opt/data/.env}
|
- {name: HERMES_MEDIA_RELAY_KEY_FILE, value: /runtime-access/chat-relay-key}
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- {name: home, mountPath: /opt/data, readOnly: true}
|
- {name: home, mountPath: /opt/data, readOnly: true}
|
||||||
- {name: workspace, mountPath: /opt/data/workspace, readOnly: true}
|
- {name: workspace, mountPath: /opt/data/workspace, readOnly: true}
|
||||||
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
|
- {name: runtime-access, mountPath: /runtime-access, readOnly: true}
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet: {path: /healthz, port: telegram-media}
|
httpGet: {path: /healthz, port: telegram-media}
|
||||||
initialDelaySeconds: 2
|
initialDelaySeconds: 2
|
||||||
@ -364,9 +390,10 @@ spec:
|
|||||||
requests: {cpu: 10m, memory: 24Mi}
|
requests: {cpu: 10m, memory: 24Mi}
|
||||||
limits: {cpu: 100m, memory: 64Mi}
|
limits: {cpu: 100m, memory: 64Mi}
|
||||||
volumes:
|
volumes:
|
||||||
- name: provider-auth
|
- name: runtime-access
|
||||||
persistentVolumeClaim:
|
emptyDir:
|
||||||
claimName: hermes-provider-auth
|
medium: Memory
|
||||||
|
sizeLimit: 2Mi
|
||||||
- name: config
|
- name: config
|
||||||
configMap:
|
configMap:
|
||||||
name: hermes-chat-config
|
name: hermes-chat-config
|
||||||
@ -378,6 +405,8 @@ spec:
|
|||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
- name: stream-recovery-patch
|
- name: stream-recovery-patch
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
|
- name: subprocess-secret-patch
|
||||||
|
emptyDir: {}
|
||||||
- name: auto-router-plugin
|
- name: auto-router-plugin
|
||||||
configMap:
|
configMap:
|
||||||
name: hermes-auto-router-plugin
|
name: hermes-auto-router-plugin
|
||||||
|
|||||||
@ -25,17 +25,13 @@ spec:
|
|||||||
ai.bstein.dev/model: anthropic/claude-opus-5, falling back to openai-codex/gpt-5.6-terra then titan-20 Qwen 14B
|
ai.bstein.dev/model: anthropic/claude-opus-5, falling back to openai-codex/gpt-5.6-terra then titan-20 Qwen 14B
|
||||||
ai.bstein.dev/role: testing-triage
|
ai.bstein.dev/role: testing-triage
|
||||||
ai.bstein.dev/placement: titan-21 preferred, Jetson preferred, arm64 fallback
|
ai.bstein.dev/placement: titan-21 preferred, Jetson preferred, arm64 fallback
|
||||||
ai.bstein.dev/config-rev: "20260813-api-lineage-cleanup"
|
ai.bstein.dev/config-rev: "20260815-runtime-access-boundary"
|
||||||
# The Anthropic credential comes from Vault rather than a manually
|
|
||||||
# created Secret. The role is declared in
|
|
||||||
# services/vault/scripts/vault_k8s_auth_configure.sh and bound to
|
|
||||||
# this pod's service account.
|
|
||||||
vault.hashicorp.com/agent-inject: "true"
|
vault.hashicorp.com/agent-inject: "true"
|
||||||
vault.hashicorp.com/role: hermes
|
vault.hashicorp.com/role: hermes
|
||||||
vault.hashicorp.com/agent-inject-secret-anthropic-token: kv/data/atlas/hermes/agent-tokens
|
vault.hashicorp.com/agent-inject-secret-triage-api-key: kv/data/atlas/hermes/triage-api
|
||||||
vault.hashicorp.com/agent-inject-template-anthropic-token: |
|
vault.hashicorp.com/agent-inject-template-triage-api-key: |
|
||||||
{{- with secret "kv/data/atlas/hermes/agent-tokens" -}}
|
{{- with secret "kv/data/atlas/hermes/triage-api" -}}
|
||||||
{{ .Data.data.anthropic_oauth_token }}
|
{{ .Data.data.api_key }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
vault.hashicorp.com/agent-pre-populate-only: "true"
|
vault.hashicorp.com/agent-pre-populate-only: "true"
|
||||||
# Without this the agent init container is appended after the
|
# Without this the agent init container is appended after the
|
||||||
@ -99,24 +95,6 @@ spec:
|
|||||||
- name: init-config
|
- name: init-config
|
||||||
image: busybox:1.37
|
image: busybox:1.37
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
env:
|
|
||||||
# When the Flux/Vault-managed shared key Secret exists, it becomes
|
|
||||||
# the API_SERVER_KEY in the persistent .env (which overrides pod
|
|
||||||
# env at runtime). Optional: absent Secret keeps the old behavior
|
|
||||||
# of generating a random key on first boot.
|
|
||||||
- name: API_SERVER_KEY_SEED
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: hermes-api-server-key
|
|
||||||
key: api-key
|
|
||||||
optional: true
|
|
||||||
# Fallback only; Vault is preferred when its file is present.
|
|
||||||
- name: CLAUDE_CODE_OAUTH_TOKEN_SEED
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: hermes-anthropic-token
|
|
||||||
key: token
|
|
||||||
optional: true
|
|
||||||
command:
|
command:
|
||||||
- sh
|
- sh
|
||||||
- -c
|
- -c
|
||||||
@ -135,37 +113,13 @@ spec:
|
|||||||
cp /guide/OPERATOR-RUNBOOK.md /opt/data/HERMES-OPERATOR-RUNBOOK.md
|
cp /guide/OPERATOR-RUNBOOK.md /opt/data/HERMES-OPERATOR-RUNBOOK.md
|
||||||
cp /config/ATLAS-TRIAGE-PROOFS.md /opt/data/ATLAS-TRIAGE-PROOFS.md
|
cp /config/ATLAS-TRIAGE-PROOFS.md /opt/data/ATLAS-TRIAGE-PROOFS.md
|
||||||
touch /opt/data/.env
|
touch /opt/data/.env
|
||||||
if [ -n "${API_SERVER_KEY_SEED:-}" ]; then
|
for key in ANTHROPIC_API_KEY API_SERVER_KEY CLAUDE_API_KEY CLAUDE_CODE_OAUTH_TOKEN GITEA_TOKEN GITEA_USERNAME HERMES_IMAGE_BROKER_KEY OPENAI_API_KEY; do
|
||||||
grep -v '^API_SERVER_KEY=' /opt/data/.env > /opt/data/.env.tmp || true
|
grep -v "^${key}=" /opt/data/.env > /opt/data/.env.tmp || true
|
||||||
printf 'API_SERVER_KEY=%s\n' "${API_SERVER_KEY_SEED}" >> /opt/data/.env.tmp
|
|
||||||
mv /opt/data/.env.tmp /opt/data/.env
|
mv /opt/data/.env.tmp /opt/data/.env
|
||||||
fi
|
done
|
||||||
if ! grep -q '^API_SERVER_KEY=' /opt/data/.env; then
|
rm -f /opt/data/auth.json
|
||||||
api_key="$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')"
|
|
||||||
printf '\nAPI_SERVER_KEY=%s\n' "${api_key}" >> /opt/data/.env
|
|
||||||
fi
|
|
||||||
# Anthropic credential: Vault first, then the manual Secret. The
|
|
||||||
# Secret is retained only as a rollback path while the migration
|
|
||||||
# settles; delete it once Vault has been serving for a while.
|
|
||||||
anthropic=""
|
|
||||||
if [ -r /vault/secrets/anthropic-token ]; then
|
|
||||||
anthropic="$(cat /vault/secrets/anthropic-token)"
|
|
||||||
elif [ -n "${CLAUDE_CODE_OAUTH_TOKEN_SEED:-}" ]; then
|
|
||||||
anthropic="${CLAUDE_CODE_OAUTH_TOKEN_SEED}"
|
|
||||||
fi
|
|
||||||
if [ -n "${anthropic}" ]; then
|
|
||||||
grep -v '^CLAUDE_CODE_OAUTH_TOKEN=' /opt/data/.env > /opt/data/.env.tmp || true
|
|
||||||
printf 'CLAUDE_CODE_OAUTH_TOKEN=%s\n' "${anthropic}" >> /opt/data/.env.tmp
|
|
||||||
mv /opt/data/.env.tmp /opt/data/.env
|
|
||||||
fi
|
|
||||||
chmod 0600 /opt/data/.env
|
chmod 0600 /opt/data/.env
|
||||||
mkdir -p /shared-auth
|
|
||||||
if [ ! -s /shared-auth/auth.json ] && [ -s /opt/data/auth.json ]; then
|
|
||||||
cp /opt/data/auth.json /shared-auth/auth.json
|
|
||||||
chmod 0600 /shared-auth/auth.json
|
|
||||||
fi
|
|
||||||
chown -R 10000:10000 /opt/data
|
chown -R 10000:10000 /opt/data
|
||||||
chown -R 10000:10000 /shared-auth
|
|
||||||
securityContext:
|
securityContext:
|
||||||
runAsUser: 0
|
runAsUser: 0
|
||||||
runAsGroup: 0
|
runAsGroup: 0
|
||||||
@ -176,8 +130,6 @@ spec:
|
|||||||
mountPath: /config
|
mountPath: /config
|
||||||
- name: operator-guide
|
- name: operator-guide
|
||||||
mountPath: /guide
|
mountPath: /guide
|
||||||
- name: provider-auth
|
|
||||||
mountPath: /shared-auth
|
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: 25m
|
cpu: 25m
|
||||||
@ -185,6 +137,25 @@ spec:
|
|||||||
limits:
|
limits:
|
||||||
cpu: 100m
|
cpu: 100m
|
||||||
memory: 64Mi
|
memory: 64Mi
|
||||||
|
- name: stage-runtime-access
|
||||||
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- /opt/hermes/.venv/bin/python
|
||||||
|
- /opt/coordinator/stage_runtime_access.py
|
||||||
|
- triage
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
runAsUser: 0
|
||||||
|
runAsGroup: 0
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
volumeMounts:
|
||||||
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
|
- {name: runtime-access, mountPath: /runtime-access}
|
||||||
|
resources:
|
||||||
|
requests: {cpu: 25m, memory: 32Mi}
|
||||||
|
limits: {cpu: 100m, memory: 64Mi}
|
||||||
- name: patch-auth
|
- name: patch-auth
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
@ -239,6 +210,29 @@ spec:
|
|||||||
resources:
|
resources:
|
||||||
requests: {cpu: 25m, memory: 64Mi}
|
requests: {cpu: 25m, memory: 64Mi}
|
||||||
limits: {cpu: 100m, memory: 128Mi}
|
limits: {cpu: 100m, memory: 128Mi}
|
||||||
|
- name: patch-subprocess-secret-boundary
|
||||||
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -ec
|
||||||
|
- |
|
||||||
|
/opt/hermes/.venv/bin/python /opt/coordinator/patch_subprocess_secret_boundary.py \
|
||||||
|
/opt/hermes/tools/environments/local.py /patched/local.py
|
||||||
|
/opt/hermes/.venv/bin/python /opt/coordinator/patch_process_output_redaction.py \
|
||||||
|
/opt/hermes/tools/process_registry.py /patched/process_registry.py
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
runAsUser: 10000
|
||||||
|
runAsGroup: 10000
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
volumeMounts:
|
||||||
|
- {name: coordinator, mountPath: /opt/coordinator, readOnly: true}
|
||||||
|
- {name: subprocess-secret-patch, mountPath: /patched}
|
||||||
|
resources:
|
||||||
|
requests: {cpu: 25m, memory: 64Mi}
|
||||||
|
limits: {cpu: 100m, memory: 128Mi}
|
||||||
- name: install-kubectl
|
- name: install-kubectl
|
||||||
image: bitnami/kubectl@sha256:554ab88b1858e8424c55de37ad417b16f2a0e65d1607aa0f3fe3ce9b9f10b131
|
image: bitnami/kubectl@sha256:554ab88b1858e8424c55de37ad417b16f2a0e65d1607aa0f3fe3ce9b9f10b131
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
@ -267,11 +261,13 @@ spec:
|
|||||||
- name: hermes
|
- name: hermes
|
||||||
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
image: registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command: [/opt/hermes/.venv/bin/hermes]
|
command: [/bin/sh, -ec]
|
||||||
args:
|
args:
|
||||||
- gateway
|
- |
|
||||||
- run
|
API_SERVER_KEY="$(tr -d '\r\n' < /runtime-access/triage-api-key)"
|
||||||
- --no-supervise
|
test -n "${API_SERVER_KEY}"
|
||||||
|
export API_SERVER_KEY
|
||||||
|
exec /opt/hermes/.venv/bin/hermes gateway run --no-supervise
|
||||||
ports:
|
ports:
|
||||||
- name: api
|
- name: api
|
||||||
containerPort: 8642
|
containerPort: 8642
|
||||||
@ -280,7 +276,7 @@ spec:
|
|||||||
- name: HERMES_HOME
|
- name: HERMES_HOME
|
||||||
value: /opt/data
|
value: /opt/data
|
||||||
- name: HERMES_AUTH_FILE
|
- name: HERMES_AUTH_FILE
|
||||||
value: /shared-auth/auth.json
|
value: /runtime-access/hermes-auth.json
|
||||||
- name: HOME
|
- name: HOME
|
||||||
value: /opt/data/home
|
value: /opt/data/home
|
||||||
- name: PATH
|
- name: PATH
|
||||||
@ -313,14 +309,6 @@ spec:
|
|||||||
value: automated-triage
|
value: automated-triage
|
||||||
- name: HERMES_API_DEFAULT_PARENT_MATCH_PREFIXES
|
- name: HERMES_API_DEFAULT_PARENT_MATCH_PREFIXES
|
||||||
value: "A static-analysis finding, not a build failure.||Use $triage-titan-test-failures."
|
value: "A static-analysis finding, not a build failure.||Use $triage-titan-test-failures."
|
||||||
# Claude subscription OAuth token (sk-ant-oat01...). The anthropic
|
|
||||||
# provider accepts ANTHROPIC_API_KEY, ANTHROPIC_TOKEN, or this, in
|
|
||||||
# that order; an OAuth token is not an API key, so it must arrive
|
|
||||||
# under this name. Optional, so Hermes still starts without it and
|
|
||||||
# falls back to openai-codex.
|
|
||||||
# TODO: migrate to Vault alongside the Gitea token; this manual
|
|
||||||
# Secret was created in the interest of demo time.
|
|
||||||
|
|
||||||
securityContext:
|
securityContext:
|
||||||
allowPrivilegeEscalation: false
|
allowPrivilegeEscalation: false
|
||||||
runAsNonRoot: true
|
runAsNonRoot: true
|
||||||
@ -333,14 +321,20 @@ spec:
|
|||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: home
|
- name: home
|
||||||
mountPath: /opt/data
|
mountPath: /opt/data
|
||||||
- name: provider-auth
|
- name: runtime-access
|
||||||
mountPath: /shared-auth
|
mountPath: /runtime-access
|
||||||
- name: auth-patch
|
- name: auth-patch
|
||||||
mountPath: /opt/hermes/hermes_cli/auth.py
|
mountPath: /opt/hermes/hermes_cli/auth.py
|
||||||
subPath: auth.py
|
subPath: auth.py
|
||||||
- name: api-server-patch
|
- name: api-server-patch
|
||||||
mountPath: /opt/hermes/gateway/platforms/api_server.py
|
mountPath: /opt/hermes/gateway/platforms/api_server.py
|
||||||
subPath: api_server.py
|
subPath: api_server.py
|
||||||
|
- name: subprocess-secret-patch
|
||||||
|
mountPath: /opt/hermes/tools/environments/local.py
|
||||||
|
subPath: local.py
|
||||||
|
- name: subprocess-secret-patch
|
||||||
|
mountPath: /opt/hermes/tools/process_registry.py
|
||||||
|
subPath: process_registry.py
|
||||||
- name: tools
|
- name: tools
|
||||||
mountPath: /usr/local/bin/kubectl
|
mountPath: /usr/local/bin/kubectl
|
||||||
subPath: kubectl
|
subPath: kubectl
|
||||||
@ -390,7 +384,7 @@ spec:
|
|||||||
command: [/bin/sh, -ec]
|
command: [/bin/sh, -ec]
|
||||||
args:
|
args:
|
||||||
- |
|
- |
|
||||||
api_key="$(sed -n 's/^API_SERVER_KEY=//p' /opt/data/.env | tail -n 1)"
|
api_key="$(tr -d '\r\n' < /runtime-access/triage-api-key)"
|
||||||
test -n "${api_key}"
|
test -n "${api_key}"
|
||||||
export API_SERVER_KEY="${api_key}"
|
export API_SERVER_KEY="${api_key}"
|
||||||
export HERMES_WEBUI_GATEWAY_API_KEY="${api_key}"
|
export HERMES_WEBUI_GATEWAY_API_KEY="${api_key}"
|
||||||
@ -399,7 +393,7 @@ spec:
|
|||||||
- {name: dashboard, containerPort: 8787, protocol: TCP}
|
- {name: dashboard, containerPort: 8787, protocol: TCP}
|
||||||
env:
|
env:
|
||||||
- {name: HERMES_HOME, value: /opt/data}
|
- {name: HERMES_HOME, value: /opt/data}
|
||||||
- {name: HERMES_AUTH_FILE, value: /shared-auth/auth.json}
|
- {name: HERMES_AUTH_FILE, value: /runtime-access/hermes-auth.json}
|
||||||
- {name: HOME, value: /opt/data/home}
|
- {name: HOME, value: /opt/data/home}
|
||||||
- {name: HERMES_WEBUI_AGENT_DIR, value: /opt/hermes}
|
- {name: HERMES_WEBUI_AGENT_DIR, value: /opt/hermes}
|
||||||
- {name: HERMES_WEBUI_HOST, value: 0.0.0.0}
|
- {name: HERMES_WEBUI_HOST, value: 0.0.0.0}
|
||||||
@ -417,7 +411,7 @@ spec:
|
|||||||
- {name: HERMES_ROUTER_PROFILE, value: triage}
|
- {name: HERMES_ROUTER_PROFILE, value: triage}
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- {name: home, mountPath: /opt/data}
|
- {name: home, mountPath: /opt/data}
|
||||||
- {name: provider-auth, mountPath: /shared-auth, readOnly: true}
|
- {name: runtime-access, mountPath: /runtime-access, readOnly: true}
|
||||||
- {name: tmp, mountPath: /tmp}
|
- {name: tmp, mountPath: /tmp}
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet: {path: /health, port: dashboard}
|
httpGet: {path: /health, port: dashboard}
|
||||||
@ -446,9 +440,10 @@ spec:
|
|||||||
- name: home
|
- name: home
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: hermes-home
|
claimName: hermes-home
|
||||||
- name: provider-auth
|
- name: runtime-access
|
||||||
persistentVolumeClaim:
|
emptyDir:
|
||||||
claimName: hermes-provider-auth
|
medium: Memory
|
||||||
|
sizeLimit: 2Mi
|
||||||
- name: config
|
- name: config
|
||||||
configMap:
|
configMap:
|
||||||
name: hermes-config
|
name: hermes-config
|
||||||
@ -468,6 +463,8 @@ spec:
|
|||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
- name: api-server-patch
|
- name: api-server-patch
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
|
- name: subprocess-secret-patch
|
||||||
|
emptyDir: {}
|
||||||
- name: tmp
|
- name: tmp
|
||||||
emptyDir:
|
emptyDir:
|
||||||
sizeLimit: 256Mi
|
sizeLimit: 256Mi
|
||||||
|
|||||||
@ -4,7 +4,7 @@ kind: Kustomization
|
|||||||
namespace: hermes
|
namespace: hermes
|
||||||
images:
|
images:
|
||||||
- name: registry.bstein.dev/bstein/hermes-agent
|
- name: registry.bstein.dev/bstein/hermes-agent
|
||||||
digest: sha256:cce1f65dc7d30fdce4b1748cc7d3434b9761ae9946a8c8be6049204c503d60a8
|
digest: sha256:0d9faa7970e4166eb3d06772decc570a695e36ca27327e4a0e8074aa05030b78
|
||||||
resources:
|
resources:
|
||||||
- namespace.yaml
|
- namespace.yaml
|
||||||
- vault-serviceaccount.yaml
|
- vault-serviceaccount.yaml
|
||||||
@ -76,6 +76,11 @@ configMapGenerator:
|
|||||||
- migrate_herdr_state.py=scripts/migrate_herdr_state.py
|
- migrate_herdr_state.py=scripts/migrate_herdr_state.py
|
||||||
- migrate_api_session_lineage.py=scripts/migrate_api_session_lineage.py
|
- migrate_api_session_lineage.py=scripts/migrate_api_session_lineage.py
|
||||||
- patch_api_server_sessions.py=scripts/patch_api_server_sessions.py
|
- patch_api_server_sessions.py=scripts/patch_api_server_sessions.py
|
||||||
|
- patch_web_session_activity.py=scripts/patch_web_session_activity.py
|
||||||
|
- patch_subprocess_secret_boundary.py=scripts/patch_subprocess_secret_boundary.py
|
||||||
|
- patch_process_output_redaction.py=scripts/patch_process_output_redaction.py
|
||||||
|
- stage_runtime_access.py=scripts/stage_runtime_access.py
|
||||||
|
- sync_runtime_credentials.py=scripts/sync_runtime_credentials.py
|
||||||
- patch_hermes_auth.py=scripts/patch_hermes_auth.py
|
- patch_hermes_auth.py=scripts/patch_hermes_auth.py
|
||||||
- patch_codex_runtime.py=scripts/patch_codex_runtime.py
|
- patch_codex_runtime.py=scripts/patch_codex_runtime.py
|
||||||
- patch_stream_recovery.py=scripts/patch_stream_recovery.py
|
- patch_stream_recovery.py=scripts/patch_stream_recovery.py
|
||||||
|
|||||||
@ -159,15 +159,17 @@ HOSTED_EDIT_SCHEMA = {
|
|||||||
|
|
||||||
|
|
||||||
def _broker_key() -> str:
|
def _broker_key() -> str:
|
||||||
"""Load the internal relay key from process env or the private .env."""
|
"""Load the internal relay key from its runtime-only Vault file."""
|
||||||
value = os.environ.get("HERMES_IMAGE_BROKER_KEY", "").strip()
|
secret_path = Path(
|
||||||
if value:
|
os.environ.get(
|
||||||
return value
|
"HERMES_IMAGE_BROKER_KEY_FILE",
|
||||||
home = Path(os.environ.get("HERMES_HOME", "/opt/data"))
|
"/runtime-access/chat-relay-key",
|
||||||
|
)
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
for line in (home / ".env").read_text(encoding="utf-8").splitlines():
|
value = secret_path.read_text(encoding="utf-8").strip()
|
||||||
if line.startswith("HERMES_IMAGE_BROKER_KEY="):
|
if value:
|
||||||
return line.split("=", 1)[1].strip()
|
return value
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@ -31,21 +31,6 @@ spec:
|
|||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
metadata:
|
|
||||||
name: hermes-provider-auth
|
|
||||||
namespace: hermes
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/part-of: hermes
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteMany
|
|
||||||
storageClassName: astreae
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 1Gi
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
metadata:
|
metadata:
|
||||||
name: hermes-chat-router-state
|
name: hermes-chat-router-state
|
||||||
namespace: hermes
|
namespace: hermes
|
||||||
|
|||||||
@ -5,5 +5,5 @@ set -eu
|
|||||||
exec /opt/data/tools/bin/claude \
|
exec /opt/data/tools/bin/claude \
|
||||||
--dangerously-skip-permissions \
|
--dangerously-skip-permissions \
|
||||||
--autocompact auto \
|
--autocompact auto \
|
||||||
--settings /opt/data/home/.claude/settings.json \
|
--settings "${CLAUDE_CONFIG_DIR:-/runtime-access/claude}/settings.json" \
|
||||||
"$@"
|
"$@"
|
||||||
|
|||||||
@ -66,11 +66,8 @@ _auth_probe_at = 0.0
|
|||||||
_auth_probe_value: dict[str, Any] = {}
|
_auth_probe_value: dict[str, Any] = {}
|
||||||
|
|
||||||
|
|
||||||
def _read_secret(env_name: str, file_env_name: str) -> str:
|
def _read_secret(file_env_name: str) -> str:
|
||||||
"""Read a secret from an environment value or mounted file."""
|
"""Read a secret only from its runtime-mounted file."""
|
||||||
value = os.environ.get(env_name, "").strip()
|
|
||||||
if value:
|
|
||||||
return value
|
|
||||||
path = os.environ.get(file_env_name, "").strip()
|
path = os.environ.get(file_env_name, "").strip()
|
||||||
if not path:
|
if not path:
|
||||||
return ""
|
return ""
|
||||||
@ -82,10 +79,7 @@ def _read_secret(env_name: str, file_env_name: str) -> str:
|
|||||||
|
|
||||||
def _relay_key() -> str:
|
def _relay_key() -> str:
|
||||||
"""Return the internal relay key shared with Switchyard."""
|
"""Return the internal relay key shared with Switchyard."""
|
||||||
return (
|
return _read_secret("HERMES_CLAUDE_BROKER_KEY_FILE")
|
||||||
_read_secret("HERMES_CLAUDE_BROKER_KEY", "HERMES_CLAUDE_BROKER_KEY_FILE")
|
|
||||||
or os.environ.get("HERMES_IMAGE_BROKER_KEY", "").strip()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _authorized(authorization: str | None, api_key: str | None) -> bool:
|
def _authorized(authorization: str | None, api_key: str | None) -> bool:
|
||||||
|
|||||||
@ -23,7 +23,22 @@ from routing_catalog import resolve_route
|
|||||||
|
|
||||||
HOST = os.environ.get("HERMES_CODEX_BROKER_LISTEN_HOST", "0.0.0.0")
|
HOST = os.environ.get("HERMES_CODEX_BROKER_LISTEN_HOST", "0.0.0.0")
|
||||||
PORT = int(os.environ.get("HERMES_CODEX_BROKER_LISTEN_PORT", "9003"))
|
PORT = int(os.environ.get("HERMES_CODEX_BROKER_LISTEN_PORT", "9003"))
|
||||||
TOKEN = os.environ.get("HERMES_IMAGE_BROKER_KEY", "").strip()
|
|
||||||
|
|
||||||
|
def _relay_token() -> str:
|
||||||
|
"""Read relay auth without exporting it to Codex subprocesses."""
|
||||||
|
path = Path(
|
||||||
|
os.environ.get(
|
||||||
|
"HERMES_CODEX_BROKER_KEY_FILE", "/runtime-access/chat-relay-key"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
return path.read_text(encoding="utf-8").strip()
|
||||||
|
except OSError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
TOKEN = _relay_token()
|
||||||
UPSTREAM = os.environ.get(
|
UPSTREAM = os.environ.get(
|
||||||
"HERMES_CODEX_BROKER_UPSTREAM",
|
"HERMES_CODEX_BROKER_UPSTREAM",
|
||||||
"https://chatgpt.com/backend-api/codex",
|
"https://chatgpt.com/backend-api/codex",
|
||||||
@ -613,7 +628,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Serve until Kubernetes terminates the sidecar."""
|
"""Serve until Kubernetes terminates the sidecar."""
|
||||||
if not TOKEN:
|
if not TOKEN:
|
||||||
raise SystemExit("HERMES_IMAGE_BROKER_KEY is required")
|
raise SystemExit("runtime relay key is unavailable")
|
||||||
try:
|
try:
|
||||||
token = _access_token()
|
token = _access_token()
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
|
|||||||
@ -2,7 +2,18 @@
|
|||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
*Username*) printf '%s\n' "${GITEA_USERNAME:-hermes-automation}" ;;
|
*Username*)
|
||||||
*Password*) printf '%s\n' "${GITEA_TOKEN:-}" ;;
|
if [ -s /runtime-access/gitea-username ]; then
|
||||||
|
tr -d '\r\n' </runtime-access/gitea-username
|
||||||
|
else
|
||||||
|
printf '%s' hermes-automation
|
||||||
|
fi
|
||||||
|
printf '\n'
|
||||||
|
;;
|
||||||
|
*Password*)
|
||||||
|
test -s /runtime-access/gitea-token
|
||||||
|
tr -d '\r\n' </runtime-access/gitea-token
|
||||||
|
printf '\n'
|
||||||
|
;;
|
||||||
*) exit 1 ;;
|
*) exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@ -25,6 +25,7 @@ from hermes_model_routing import (
|
|||||||
|
|
||||||
CASSANDRA_BASE_PATH = Path("/opt/data/workspace/projects/cassandra")
|
CASSANDRA_BASE_PATH = Path("/opt/data/workspace/projects/cassandra")
|
||||||
CASSANDRA_REMOTE = "https://scm.bstein.dev/atlas/cassandra.git"
|
CASSANDRA_REMOTE = "https://scm.bstein.dev/atlas/cassandra.git"
|
||||||
|
DEFAULT_GITEA_TOKEN_PATH = Path("/runtime-access/gitea-token")
|
||||||
|
|
||||||
|
|
||||||
def cassandra_workspace() -> Path:
|
def cassandra_workspace() -> Path:
|
||||||
@ -138,13 +139,44 @@ def bootstrap_cassandra_state(root: Path) -> dict[str, str]:
|
|||||||
return {"state": "ready"}
|
return {"state": "ready"}
|
||||||
|
|
||||||
|
|
||||||
|
def _gitea_token_path() -> Path:
|
||||||
|
"""Return the runtime-only Gitea credential path without reading it."""
|
||||||
|
configured = os.environ.get("HERMES_GITEA_TOKEN_FILE", "").strip()
|
||||||
|
return Path(configured) if configured else DEFAULT_GITEA_TOKEN_PATH
|
||||||
|
|
||||||
|
|
||||||
|
def _gitea_token_available() -> bool:
|
||||||
|
"""Check credential readiness without loading it into process memory."""
|
||||||
|
try:
|
||||||
|
path = _gitea_token_path()
|
||||||
|
return path.is_file() and path.stat().st_size > 0
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def sync_cassandra_repo(env_values: dict[str, str]) -> str:
|
def sync_cassandra_repo(env_values: dict[str, str]) -> str:
|
||||||
"""Clone or fetch Cassandra when the optional Gitea token is available."""
|
"""Clone or fetch Cassandra through the runtime-only askpass credential."""
|
||||||
token = env_values.get("GITEA_TOKEN", "").strip()
|
token_available = _gitea_token_available()
|
||||||
if shutil.which("git") is None:
|
if shutil.which("git") is None:
|
||||||
return "git-unavailable"
|
return "git-unavailable"
|
||||||
child_env = os.environ.copy()
|
child_env = os.environ.copy()
|
||||||
child_env.update(env_values)
|
child_env.update(
|
||||||
|
{
|
||||||
|
key: value
|
||||||
|
for key, value in env_values.items()
|
||||||
|
if key
|
||||||
|
not in {
|
||||||
|
"API_SERVER_KEY",
|
||||||
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
||||||
|
"GITEA_TOKEN",
|
||||||
|
"HERMES_IMAGE_BROKER_KEY",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
child_env["GIT_ASKPASS"] = env_values.get(
|
||||||
|
"GIT_ASKPASS", "/opt/coordinator/gitea_askpass.sh"
|
||||||
|
)
|
||||||
|
child_env["GIT_TERMINAL_PROMPT"] = "0"
|
||||||
if (CASSANDRA_BASE_PATH / ".git").exists():
|
if (CASSANDRA_BASE_PATH / ".git").exists():
|
||||||
try:
|
try:
|
||||||
current_remote = subprocess.run(
|
current_remote = subprocess.run(
|
||||||
@ -200,7 +232,7 @@ def sync_cassandra_repo(env_values: dict[str, str]) -> str:
|
|||||||
return f"remote-repair-failed-{repaired.returncode}"
|
return f"remote-repair-failed-{repaired.returncode}"
|
||||||
except (OSError, subprocess.TimeoutExpired):
|
except (OSError, subprocess.TimeoutExpired):
|
||||||
return "remote-repair-failed"
|
return "remote-repair-failed"
|
||||||
if not token:
|
if not token_available:
|
||||||
return "ready; fetch skipped until Gitea token is configured"
|
return "ready; fetch skipped until Gitea token is configured"
|
||||||
command = [
|
command = [
|
||||||
"git",
|
"git",
|
||||||
@ -215,7 +247,7 @@ def sync_cassandra_repo(env_values: dict[str, str]) -> str:
|
|||||||
CASSANDRA_BASE_PATH.mkdir(parents=True, exist_ok=True)
|
CASSANDRA_BASE_PATH.mkdir(parents=True, exist_ok=True)
|
||||||
if any(CASSANDRA_BASE_PATH.iterdir()):
|
if any(CASSANDRA_BASE_PATH.iterdir()):
|
||||||
return "unmanaged-nonempty-directory"
|
return "unmanaged-nonempty-directory"
|
||||||
if not token:
|
if not token_available:
|
||||||
return "awaiting-gitea-token"
|
return "awaiting-gitea-token"
|
||||||
command = [
|
command = [
|
||||||
"git",
|
"git",
|
||||||
@ -244,8 +276,6 @@ def sync_cassandra_repo(env_values: dict[str, str]) -> str:
|
|||||||
def refresh_once(root: Path) -> dict[str, Any]:
|
def refresh_once(root: Path) -> dict[str, Any]:
|
||||||
"""Refresh provider catalogs and all managed coordinator state once."""
|
"""Refresh provider catalogs and all managed coordinator state once."""
|
||||||
env_values = _read_env(root / ".env")
|
env_values = _read_env(root / ".env")
|
||||||
if env_values.get("CLAUDE_CODE_OAUTH_TOKEN"):
|
|
||||||
os.environ["CLAUDE_CODE_OAUTH_TOKEN"] = env_values["CLAUDE_CODE_OAUTH_TOKEN"]
|
|
||||||
codex = discover_codex_models()
|
codex = discover_codex_models()
|
||||||
claude = discover_claude_models()
|
claude = discover_claude_models()
|
||||||
routes = configure_routes(root, codex, claude)
|
routes = configure_routes(root, codex, claude)
|
||||||
|
|||||||
@ -39,11 +39,18 @@ SWITCHYARD_API = "http://hermes-switchyard.hermes.svc.cluster.local:9005/v1"
|
|||||||
SWITCHYARD_AUTO_ROUTE = "atlas/auto/maximum"
|
SWITCHYARD_AUTO_ROUTE = "atlas/auto/maximum"
|
||||||
ROUTING_CATALOG_PATH = os.environ.get("HERMES_ROUTING_CATALOG_PATH", "").strip()
|
ROUTING_CATALOG_PATH = os.environ.get("HERMES_ROUTING_CATALOG_PATH", "").strip()
|
||||||
MANAGED_ENV_KEYS = {
|
MANAGED_ENV_KEYS = {
|
||||||
|
"GIT_ASKPASS",
|
||||||
|
"GIT_TERMINAL_PROMPT",
|
||||||
|
}
|
||||||
|
RUNTIME_SECRET_ENV_KEYS = {
|
||||||
|
"ANTHROPIC_API_KEY",
|
||||||
|
"API_SERVER_KEY",
|
||||||
|
"CLAUDE_API_KEY",
|
||||||
"CLAUDE_CODE_OAUTH_TOKEN",
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
||||||
"GITEA_TOKEN",
|
"GITEA_TOKEN",
|
||||||
"GITEA_USERNAME",
|
"GITEA_USERNAME",
|
||||||
"GIT_ASKPASS",
|
"HERMES_IMAGE_BROKER_KEY",
|
||||||
"GIT_TERMINAL_PROMPT",
|
"OPENAI_API_KEY",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -359,7 +366,7 @@ def _read_env(path: Path) -> dict[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
def _update_profile_env(path: Path, source: dict[str, str]) -> None:
|
def _update_profile_env(path: Path, source: dict[str, str]) -> None:
|
||||||
"""Refresh managed credentials while preserving user-owned environment keys."""
|
"""Refresh non-secret managed settings and remove stale credential copies."""
|
||||||
try:
|
try:
|
||||||
old_lines = path.read_text(encoding="utf-8").splitlines()
|
old_lines = path.read_text(encoding="utf-8").splitlines()
|
||||||
except OSError:
|
except OSError:
|
||||||
@ -367,7 +374,10 @@ def _update_profile_env(path: Path, source: dict[str, str]) -> None:
|
|||||||
kept = [
|
kept = [
|
||||||
line
|
line
|
||||||
for line in old_lines
|
for line in old_lines
|
||||||
if not any(line.lstrip().startswith(f"{key}=") for key in MANAGED_ENV_KEYS)
|
if not any(
|
||||||
|
line.lstrip().startswith(f"{key}=")
|
||||||
|
for key in MANAGED_ENV_KEYS | RUNTIME_SECRET_ENV_KEYS
|
||||||
|
)
|
||||||
]
|
]
|
||||||
kept.extend(
|
kept.extend(
|
||||||
f"{key}={source[key]}" for key in sorted(MANAGED_ENV_KEYS) if source.get(key)
|
f"{key}={source[key]}" for key in sorted(MANAGED_ENV_KEYS) if source.get(key)
|
||||||
@ -494,25 +504,13 @@ def discover_claude_models() -> Catalog:
|
|||||||
)
|
)
|
||||||
except (OSError, subprocess.SubprocessError, ValueError, json.JSONDecodeError):
|
except (OSError, subprocess.SubprocessError, ValueError, json.JSONDecodeError):
|
||||||
pass
|
pass
|
||||||
token = str(os.environ.get("CLAUDE_CODE_OAUTH_TOKEN") or "").strip()
|
|
||||||
live: list[str] = []
|
|
||||||
if token:
|
|
||||||
try:
|
|
||||||
from hermes_cli.models import _fetch_anthropic_models
|
|
||||||
|
|
||||||
live = _unique_models(_fetch_anthropic_models(timeout=10.0) or [])
|
|
||||||
except Exception:
|
|
||||||
live = []
|
|
||||||
if live:
|
|
||||||
return Catalog("anthropic", live, True, True, "connected")
|
|
||||||
try:
|
try:
|
||||||
from hermes_cli.models import provider_model_ids
|
from hermes_cli.models import provider_model_ids
|
||||||
|
|
||||||
known = _unique_models(provider_model_ids("anthropic", force_refresh=True))
|
known = _unique_models(provider_model_ids("anthropic", force_refresh=True))
|
||||||
except Exception:
|
except Exception:
|
||||||
known = []
|
known = []
|
||||||
state = "degraded" if token else "not-configured"
|
return Catalog("anthropic", known, False, False, "not-configured")
|
||||||
return Catalog("anthropic", known, False, bool(token), state)
|
|
||||||
|
|
||||||
|
|
||||||
def _profile_config(
|
def _profile_config(
|
||||||
@ -626,8 +624,6 @@ def configure_routes(
|
|||||||
env_values = _read_env(root / ".env")
|
env_values = _read_env(root / ".env")
|
||||||
routes: dict[str, list[str]] = {}
|
routes: dict[str, list[str]] = {}
|
||||||
for effort in EFFORTS:
|
for effort in EFFORTS:
|
||||||
codex_model = codex_models[effort]
|
|
||||||
claude_model = claude_models[effort]
|
|
||||||
codex_name = f"codex-{effort}"
|
codex_name = f"codex-{effort}"
|
||||||
claude_name = f"claude-{effort}"
|
claude_name = f"claude-{effort}"
|
||||||
codex_route = {
|
codex_route = {
|
||||||
|
|||||||
@ -20,7 +20,22 @@ HOST = os.environ.get("HERMES_IMAGE_BROKER_LISTEN_HOST", "0.0.0.0")
|
|||||||
# Kubernetes reserves <SERVICE>_PORT for service-link discovery and injects a
|
# Kubernetes reserves <SERVICE>_PORT for service-link discovery and injects a
|
||||||
# tcp://... value. Keep listener configuration out of that namespace.
|
# tcp://... value. Keep listener configuration out of that namespace.
|
||||||
PORT = int(os.environ.get("HERMES_IMAGE_BROKER_LISTEN_PORT", "9002"))
|
PORT = int(os.environ.get("HERMES_IMAGE_BROKER_LISTEN_PORT", "9002"))
|
||||||
TOKEN = os.environ.get("HERMES_IMAGE_BROKER_KEY", "").strip()
|
|
||||||
|
|
||||||
|
def _relay_token() -> str:
|
||||||
|
"""Read relay auth without exporting it to provider subprocesses."""
|
||||||
|
path = Path(
|
||||||
|
os.environ.get(
|
||||||
|
"HERMES_IMAGE_BROKER_KEY_FILE", "/runtime-access/chat-relay-key"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
return path.read_text(encoding="utf-8").strip()
|
||||||
|
except OSError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
TOKEN = _relay_token()
|
||||||
PROVIDER_PATH = Path(
|
PROVIDER_PATH = Path(
|
||||||
os.environ.get(
|
os.environ.get(
|
||||||
"HERMES_IMAGE_PROVIDER_PATH",
|
"HERMES_IMAGE_PROVIDER_PATH",
|
||||||
@ -329,7 +344,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Serve until Kubernetes terminates the sidecar."""
|
"""Serve until Kubernetes terminates the sidecar."""
|
||||||
if not TOKEN:
|
if not TOKEN:
|
||||||
raise SystemExit("HERMES_IMAGE_BROKER_KEY is required")
|
raise SystemExit("runtime relay key is unavailable")
|
||||||
server = ThreadingHTTPServer((HOST, PORT), Handler)
|
server = ThreadingHTTPServer((HOST, PORT), Handler)
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
|
|
||||||
|
|||||||
@ -129,11 +129,20 @@ RUN_CLOSE_AFTER = ''' finally:
|
|||||||
# distinguish a long model wait from completed or failed work.
|
# distinguish a long model wait from completed or failed work.
|
||||||
if parent_session_id:
|
if parent_session_id:
|
||||||
try:
|
try:
|
||||||
|
terminal_status = self._run_statuses.get(run_id, {}).get(
|
||||||
|
"status", "failed"
|
||||||
|
)
|
||||||
|
activity_status = terminal_status if terminal_status in {
|
||||||
|
"completed",
|
||||||
|
"failed",
|
||||||
|
"cancelled",
|
||||||
|
} else "failed"
|
||||||
|
self._record_run_activity(
|
||||||
|
session_id,
|
||||||
|
f"run.{activity_status}",
|
||||||
|
)
|
||||||
db = self._ensure_session_db()
|
db = self._ensure_session_db()
|
||||||
if db is not None:
|
if db is not None:
|
||||||
terminal_status = self._run_statuses.get(run_id, {}).get(
|
|
||||||
"status", "failed"
|
|
||||||
)
|
|
||||||
db.end_session(session_id, f"api_run_{terminal_status}")
|
db.end_session(session_id, f"api_run_{terminal_status}")
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
@ -145,6 +154,156 @@ RUN_CLOSE_AFTER = ''' finally:
|
|||||||
# If the asyncio wrapper is cancelled (for example via
|
# If the asyncio wrapper is cancelled (for example via
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
EVENT_CALLBACK_SIGNATURE_BEFORE = ''' def _make_run_event_callback(self, run_id: str, loop: "asyncio.AbstractEventLoop"):
|
||||||
|
'''
|
||||||
|
|
||||||
|
EVENT_CALLBACK_SIGNATURE_AFTER = ''' _RUN_ACTIVITY_BYTES = 512_000
|
||||||
|
_RUN_ACTIVITY_FILES = 256
|
||||||
|
|
||||||
|
def _record_run_activity(
|
||||||
|
self,
|
||||||
|
session_id: str,
|
||||||
|
event_type: str,
|
||||||
|
*,
|
||||||
|
tool_name: str = None,
|
||||||
|
preview: str = None,
|
||||||
|
is_error: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""Append a bounded, display-only event without changing agent history."""
|
||||||
|
labels = {
|
||||||
|
"run.started": "Worker started",
|
||||||
|
"run.completed": "Worker completed",
|
||||||
|
"run.failed": "Worker failed",
|
||||||
|
"run.cancelled": "Worker cancelled",
|
||||||
|
"_thinking": "Hermes is reasoning",
|
||||||
|
"reasoning.available": "Hermes finished a reasoning step",
|
||||||
|
"tool.started": "Tool started",
|
||||||
|
"tool.completed": "Tool failed" if is_error else "Tool finished",
|
||||||
|
"subagent.tool": "Nested worker tool",
|
||||||
|
"subagent.progress": "Nested worker progress",
|
||||||
|
"subagent_progress": "Nested worker progress",
|
||||||
|
"subagent.complete": "Nested worker finished",
|
||||||
|
"subagent.text": "Nested worker produced an update",
|
||||||
|
"subagent.thinking": "Nested worker is reasoning",
|
||||||
|
}
|
||||||
|
label = labels.get(event_type)
|
||||||
|
if not session_id or not label:
|
||||||
|
return
|
||||||
|
# Tool names are bounded identifiers. All previews are arbitrary model
|
||||||
|
# or tool text and can contain prompts, arguments, paths, or credentials,
|
||||||
|
# so the activity projection must never persist them.
|
||||||
|
detail = tool_name if event_type in {
|
||||||
|
"tool.started",
|
||||||
|
"tool.completed",
|
||||||
|
"subagent.tool",
|
||||||
|
} else ""
|
||||||
|
try:
|
||||||
|
detail = redact_sensitive_text(str(detail)).strip()[:480]
|
||||||
|
content = f"{label}: {detail}" if detail else label
|
||||||
|
entry = {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": f"Activity · {content}",
|
||||||
|
"timestamp": time.time(),
|
||||||
|
"activity_event": event_type,
|
||||||
|
}
|
||||||
|
root = Path(
|
||||||
|
os.environ.get("HERMES_HOME", "~/.hermes")
|
||||||
|
).expanduser() / "run-activity"
|
||||||
|
if root.is_symlink():
|
||||||
|
return
|
||||||
|
root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
|
root.chmod(0o700)
|
||||||
|
digest = hashlib.sha256(session_id.encode("utf-8")).hexdigest()
|
||||||
|
path = root / f"{digest}.jsonl"
|
||||||
|
previous = root / f"{digest}.previous.jsonl"
|
||||||
|
if path.is_symlink() or previous.is_symlink():
|
||||||
|
return
|
||||||
|
new_file = not path.exists()
|
||||||
|
if path.exists() and path.stat().st_size >= self._RUN_ACTIVITY_BYTES:
|
||||||
|
previous.unlink(missing_ok=True)
|
||||||
|
path.replace(previous)
|
||||||
|
line = (json.dumps(entry, separators=(",", ":")) + "\\n").encode()
|
||||||
|
flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND
|
||||||
|
flags |= getattr(os, "O_NOFOLLOW", 0)
|
||||||
|
fd = os.open(path, flags, 0o600)
|
||||||
|
try:
|
||||||
|
os.fchmod(fd, 0o600)
|
||||||
|
os.write(fd, line)
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
|
if new_file:
|
||||||
|
files = sorted(
|
||||||
|
root.glob("*.jsonl"),
|
||||||
|
key=lambda item: item.stat().st_mtime,
|
||||||
|
)
|
||||||
|
for stale in files[:-self._RUN_ACTIVITY_FILES]:
|
||||||
|
if not stale.is_symlink():
|
||||||
|
stale.unlink(missing_ok=True)
|
||||||
|
except Exception:
|
||||||
|
logger.debug("Could not record run activity", exc_info=True)
|
||||||
|
|
||||||
|
def _make_run_event_callback(
|
||||||
|
self,
|
||||||
|
run_id: str,
|
||||||
|
loop: "asyncio.AbstractEventLoop",
|
||||||
|
*,
|
||||||
|
session_id: str,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
|
||||||
|
EVENT_CALLBACK_BODY_BEFORE = ''' def _callback(event_type: str, tool_name: str = None, preview: str = None, args=None, **kwargs):
|
||||||
|
ts = time.time()
|
||||||
|
if event_type == "tool.started":
|
||||||
|
'''
|
||||||
|
|
||||||
|
EVENT_CALLBACK_BODY_AFTER = ''' def _callback(event_type: str, tool_name: str = None, preview: str = None, args=None, **kwargs):
|
||||||
|
ts = time.time()
|
||||||
|
self._record_run_activity(
|
||||||
|
session_id,
|
||||||
|
event_type,
|
||||||
|
tool_name=tool_name,
|
||||||
|
preview=preview,
|
||||||
|
is_error=bool(kwargs.get("is_error", False)),
|
||||||
|
)
|
||||||
|
if event_type == "tool.started":
|
||||||
|
'''
|
||||||
|
|
||||||
|
EVENT_CALLBACK_END_BEFORE = ''' # _thinking and subagent_progress are intentionally not forwarded
|
||||||
|
'''
|
||||||
|
|
||||||
|
EVENT_CALLBACK_END_AFTER = ''' elif event_type in {
|
||||||
|
"subagent.tool",
|
||||||
|
"subagent.progress",
|
||||||
|
"subagent_progress",
|
||||||
|
"subagent.complete",
|
||||||
|
"subagent.text",
|
||||||
|
"subagent.thinking",
|
||||||
|
}:
|
||||||
|
safe_preview = (
|
||||||
|
redact_sensitive_text(str(tool_name or ""))[:480]
|
||||||
|
if event_type == "subagent.tool"
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
_push({
|
||||||
|
"event": event_type,
|
||||||
|
"run_id": run_id,
|
||||||
|
"timestamp": ts,
|
||||||
|
"tool": tool_name if event_type == "subagent.tool" else None,
|
||||||
|
"preview": safe_preview,
|
||||||
|
})
|
||||||
|
'''
|
||||||
|
|
||||||
|
EVENT_CALLBACK_CALL_BEFORE = ''' event_cb = self._make_run_event_callback(run_id, loop)
|
||||||
|
'''
|
||||||
|
|
||||||
|
EVENT_CALLBACK_CALL_AFTER = ''' event_cb = self._make_run_event_callback(
|
||||||
|
run_id,
|
||||||
|
loop,
|
||||||
|
session_id=session_id,
|
||||||
|
)
|
||||||
|
self._record_run_activity(session_id, "run.started")
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
def patch(source: Path, destination: Path) -> None:
|
def patch(source: Path, destination: Path) -> None:
|
||||||
"""Apply the narrow session-lineage extension and fail on upstream drift."""
|
"""Apply the narrow session-lineage extension and fail on upstream drift."""
|
||||||
@ -155,11 +314,27 @@ def patch(source: Path, destination: Path) -> None:
|
|||||||
raise RuntimeError("Hermes API runs patch context changed")
|
raise RuntimeError("Hermes API runs patch context changed")
|
||||||
if RUN_CLOSE_BEFORE not in content:
|
if RUN_CLOSE_BEFORE not in content:
|
||||||
raise RuntimeError("Hermes API run-close patch context changed")
|
raise RuntimeError("Hermes API run-close patch context changed")
|
||||||
|
for marker, message in (
|
||||||
|
(EVENT_CALLBACK_SIGNATURE_BEFORE, "event callback signature"),
|
||||||
|
(EVENT_CALLBACK_BODY_BEFORE, "event callback body"),
|
||||||
|
(EVENT_CALLBACK_END_BEFORE, "event callback end"),
|
||||||
|
(EVENT_CALLBACK_CALL_BEFORE, "event callback call"),
|
||||||
|
):
|
||||||
|
if marker not in content:
|
||||||
|
raise RuntimeError(f"Hermes API {message} patch context changed")
|
||||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||||
content = content.replace(BEFORE, AFTER, 1)
|
content = content.replace(BEFORE, AFTER, 1)
|
||||||
content = content.replace(RUNS_BEFORE, RUNS_AFTER, 1)
|
content = content.replace(RUNS_BEFORE, RUNS_AFTER, 1)
|
||||||
|
content = content.replace(RUN_CLOSE_BEFORE, RUN_CLOSE_AFTER, 1)
|
||||||
|
content = content.replace(
|
||||||
|
EVENT_CALLBACK_SIGNATURE_BEFORE,
|
||||||
|
EVENT_CALLBACK_SIGNATURE_AFTER,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
content = content.replace(EVENT_CALLBACK_BODY_BEFORE, EVENT_CALLBACK_BODY_AFTER, 1)
|
||||||
|
content = content.replace(EVENT_CALLBACK_END_BEFORE, EVENT_CALLBACK_END_AFTER, 1)
|
||||||
destination.write_text(
|
destination.write_text(
|
||||||
content.replace(RUN_CLOSE_BEFORE, RUN_CLOSE_AFTER, 1),
|
content.replace(EVENT_CALLBACK_CALL_BEFORE, EVENT_CALLBACK_CALL_AFTER, 1),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
39
services/hermes/scripts/patch_process_output_redaction.py
Normal file
39
services/hermes/scripts/patch_process_output_redaction.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Redact credential assignments from every background-process result."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
BEFORE = ''' result[field] = redact_terminal_output(value, command)
|
||||||
|
'''
|
||||||
|
|
||||||
|
AFTER = ''' result[field] = redact_sensitive_text(
|
||||||
|
redact_terminal_output(value, command),
|
||||||
|
code_file=False,
|
||||||
|
)
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def patch(source: Path, destination: Path) -> None:
|
||||||
|
"""Apply unconditional result redaction and fail on upstream drift."""
|
||||||
|
content = source.read_text(encoding="utf-8")
|
||||||
|
if content.count(BEFORE) != 1:
|
||||||
|
raise RuntimeError("Hermes process-result redaction context changed")
|
||||||
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
destination.write_text(content.replace(BEFORE, AFTER, 1), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("source", type=Path)
|
||||||
|
parser.add_argument("destination", type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
patch(args.source, args.destination)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
45
services/hermes/scripts/patch_subprocess_secret_boundary.py
Normal file
45
services/hermes/scripts/patch_subprocess_secret_boundary.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Keep Atlas runtime credentials out of Hermes-spawned subprocesses."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
BEFORE = ''' upper = key.upper()
|
||||||
|
if upper.startswith("AUXILIARY_") and (
|
||||||
|
'''
|
||||||
|
|
||||||
|
AFTER = ''' upper = key.upper()
|
||||||
|
if upper in {
|
||||||
|
"API_SERVER_KEY",
|
||||||
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
||||||
|
"GITEA_TOKEN",
|
||||||
|
"HERMES_IMAGE_BROKER_KEY",
|
||||||
|
}:
|
||||||
|
return True
|
||||||
|
if upper.startswith("AUXILIARY_") and (
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def patch(source: Path, destination: Path) -> None:
|
||||||
|
"""Apply the runtime-credential boundary and fail on upstream drift."""
|
||||||
|
content = source.read_text(encoding="utf-8")
|
||||||
|
if content.count(BEFORE) != 1:
|
||||||
|
raise RuntimeError("Hermes subprocess secret boundary context changed")
|
||||||
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
destination.write_text(content.replace(BEFORE, AFTER, 1), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("source", type=Path)
|
||||||
|
parser.add_argument("destination", type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
patch(args.source, args.destination)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
96
services/hermes/scripts/patch_web_session_activity.py
Normal file
96
services/hermes/scripts/patch_web_session_activity.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Merge display-only worker activity into dashboard session transcripts."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
HELPER_MARKER = '''@app.get("/api/sessions/{session_id}/messages")
|
||||||
|
async def get_session_messages(session_id: str, profile: Optional[str] = None):
|
||||||
|
'''
|
||||||
|
|
||||||
|
HELPER_REPLACEMENT = '''def _run_activity_messages(session_id: str) -> List[Dict[str, Any]]:
|
||||||
|
"""Read a bounded activity journal without treating it as agent history."""
|
||||||
|
digest = hashlib.sha256(session_id.encode("utf-8")).hexdigest()
|
||||||
|
root = get_hermes_home() / "run-activity"
|
||||||
|
if root.is_symlink():
|
||||||
|
return []
|
||||||
|
paths = (
|
||||||
|
root / f"{digest}.previous.jsonl",
|
||||||
|
root / f"{digest}.jsonl",
|
||||||
|
)
|
||||||
|
entries: List[Dict[str, Any]] = []
|
||||||
|
for path in paths:
|
||||||
|
try:
|
||||||
|
if path.is_symlink() or not path.is_file():
|
||||||
|
continue
|
||||||
|
if path.stat().st_size > 600_000:
|
||||||
|
continue
|
||||||
|
for line in path.read_text(encoding="utf-8").splitlines():
|
||||||
|
item = json.loads(line)
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
content = item.get("content")
|
||||||
|
timestamp = item.get("timestamp")
|
||||||
|
if not isinstance(content, str) or not isinstance(
|
||||||
|
timestamp, (int, float)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
entries.append(
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": content[:1_000],
|
||||||
|
"timestamp": float(timestamp),
|
||||||
|
"observed": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except (OSError, UnicodeError, ValueError, json.JSONDecodeError):
|
||||||
|
continue
|
||||||
|
return entries[-1_000:]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/sessions/{session_id}/messages")
|
||||||
|
async def get_session_messages(session_id: str, profile: Optional[str] = None):
|
||||||
|
'''
|
||||||
|
|
||||||
|
MESSAGES_BEFORE = ''' messages = db.get_messages(sid)
|
||||||
|
return {"session_id": sid, "messages": messages}
|
||||||
|
'''
|
||||||
|
|
||||||
|
MESSAGES_AFTER = ''' messages = [
|
||||||
|
*db.get_messages(sid),
|
||||||
|
*_run_activity_messages(sid),
|
||||||
|
]
|
||||||
|
messages.sort(key=lambda item: float(item.get("timestamp") or 0.0))
|
||||||
|
return {"session_id": sid, "messages": messages}
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def patch(source: Path, destination: Path) -> None:
|
||||||
|
"""Apply the activity projection and fail closed on upstream drift."""
|
||||||
|
content = source.read_text(encoding="utf-8")
|
||||||
|
if HELPER_MARKER not in content:
|
||||||
|
raise RuntimeError("Hermes dashboard activity helper context changed")
|
||||||
|
if MESSAGES_BEFORE not in content:
|
||||||
|
raise RuntimeError("Hermes dashboard messages context changed")
|
||||||
|
content = content.replace(HELPER_MARKER, HELPER_REPLACEMENT, 1)
|
||||||
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
destination.write_text(
|
||||||
|
content.replace(MESSAGES_BEFORE, MESSAGES_AFTER, 1),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("source", type=Path)
|
||||||
|
parser.add_argument("destination", type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
patch(args.source, args.destination)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
162
services/hermes/scripts/stage_runtime_access.py
Normal file
162
services/hermes/scripts/stage_runtime_access.py
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Stage Vault-projected access material into a pod-lifetime filesystem."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
VAULT_ROOT = Path("/vault/secrets")
|
||||||
|
RUNTIME_ROOT = Path("/runtime-access")
|
||||||
|
PERSISTENT_HOME = Path("/opt/data/home")
|
||||||
|
OWNER_UID = 10000
|
||||||
|
OWNER_GID = 10000
|
||||||
|
|
||||||
|
|
||||||
|
def _owned_directory(path: Path) -> None:
|
||||||
|
"""Create one private directory owned by the unprivileged Hermes user."""
|
||||||
|
path.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
|
path.chmod(0o700)
|
||||||
|
os.chown(path, OWNER_UID, OWNER_GID)
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_secret(source_name: str, destination: Path) -> str:
|
||||||
|
"""Copy a non-empty Vault projection without logging its value."""
|
||||||
|
value = (VAULT_ROOT / source_name).read_text(encoding="utf-8").strip()
|
||||||
|
if not value:
|
||||||
|
raise RuntimeError(f"Vault projection {source_name} is empty")
|
||||||
|
destination.write_text(value + "\n", encoding="utf-8")
|
||||||
|
destination.chmod(0o600)
|
||||||
|
os.chown(destination, OWNER_UID, OWNER_GID)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _validated_json(source_name: str, destination: Path, required: tuple[str, ...]) -> None:
|
||||||
|
"""Stage one credential document after checking its expected shape."""
|
||||||
|
value = _copy_secret(source_name, destination)
|
||||||
|
try:
|
||||||
|
document = json.loads(value)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
destination.unlink(missing_ok=True)
|
||||||
|
raise
|
||||||
|
current = document
|
||||||
|
for key in required:
|
||||||
|
if not isinstance(current, dict) or key not in current:
|
||||||
|
destination.unlink(missing_ok=True)
|
||||||
|
raise RuntimeError(f"Vault projection {source_name} has an invalid shape")
|
||||||
|
current = current[key]
|
||||||
|
if not isinstance(current, str) or not current:
|
||||||
|
destination.unlink(missing_ok=True)
|
||||||
|
raise RuntimeError(f"Vault projection {source_name} has an empty credential")
|
||||||
|
|
||||||
|
|
||||||
|
def _link_noncredential_state(
|
||||||
|
source: Path,
|
||||||
|
destination: Path,
|
||||||
|
allowed: tuple[str, ...],
|
||||||
|
) -> None:
|
||||||
|
"""Expose only an explicit allowlist of persistent, non-credential state."""
|
||||||
|
if not source.is_dir():
|
||||||
|
return
|
||||||
|
for name in allowed:
|
||||||
|
item = source / name
|
||||||
|
if not item.exists() or item.is_symlink():
|
||||||
|
continue
|
||||||
|
target = destination / name
|
||||||
|
if target.exists() or target.is_symlink():
|
||||||
|
continue
|
||||||
|
target.symlink_to(item)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_empty_auth_store() -> None:
|
||||||
|
"""Create a private, non-credential Hermes auth store for Switchyard routes."""
|
||||||
|
path = RUNTIME_ROOT / "hermes-auth.json"
|
||||||
|
path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{"version": 1, "providers": {}, "credential_pool": {}},
|
||||||
|
separators=(",", ":"),
|
||||||
|
)
|
||||||
|
+ "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
path.chmod(0o600)
|
||||||
|
os.chown(path, OWNER_UID, OWNER_GID)
|
||||||
|
|
||||||
|
|
||||||
|
def stage_agent() -> None:
|
||||||
|
"""Stage the owner agent's complete runtime access set."""
|
||||||
|
for path in (RUNTIME_ROOT, RUNTIME_ROOT / "claude", RUNTIME_ROOT / "codex"):
|
||||||
|
_owned_directory(path)
|
||||||
|
for name in (
|
||||||
|
"agent-api-key",
|
||||||
|
"chat-relay-key",
|
||||||
|
"gitea-token",
|
||||||
|
"gitea-username",
|
||||||
|
"node-ssh-private-key",
|
||||||
|
"node-ssh-config",
|
||||||
|
"node-ssh-known-hosts",
|
||||||
|
):
|
||||||
|
_copy_secret(name, RUNTIME_ROOT / name)
|
||||||
|
_validated_json(
|
||||||
|
"claude-credentials",
|
||||||
|
RUNTIME_ROOT / "claude" / ".credentials.json",
|
||||||
|
("claudeAiOauth", "refreshToken"),
|
||||||
|
)
|
||||||
|
_validated_json(
|
||||||
|
"codex-auth",
|
||||||
|
RUNTIME_ROOT / "codex" / "auth.json",
|
||||||
|
("tokens", "refresh_token"),
|
||||||
|
)
|
||||||
|
_link_noncredential_state(
|
||||||
|
PERSISTENT_HOME / ".claude",
|
||||||
|
RUNTIME_ROOT / "claude",
|
||||||
|
("settings.json", "hooks", "plugins"),
|
||||||
|
)
|
||||||
|
_link_noncredential_state(
|
||||||
|
PERSISTENT_HOME / ".codex",
|
||||||
|
RUNTIME_ROOT / "codex",
|
||||||
|
(
|
||||||
|
"config.toml",
|
||||||
|
"hooks.json",
|
||||||
|
"herdr-agent-state.sh",
|
||||||
|
"rules",
|
||||||
|
"skills",
|
||||||
|
"plugins",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
_write_empty_auth_store()
|
||||||
|
|
||||||
|
|
||||||
|
def stage_chat() -> None:
|
||||||
|
"""Stage only the relay credential needed by an isolated chat tenant."""
|
||||||
|
_owned_directory(RUNTIME_ROOT)
|
||||||
|
_copy_secret("chat-relay-key", RUNTIME_ROOT / "chat-relay-key")
|
||||||
|
_write_empty_auth_store()
|
||||||
|
|
||||||
|
|
||||||
|
def stage_triage() -> None:
|
||||||
|
"""Stage the automated-triage API credential without a persistent copy."""
|
||||||
|
_owned_directory(RUNTIME_ROOT)
|
||||||
|
_copy_secret("triage-api-key", RUNTIME_ROOT / "triage-api-key")
|
||||||
|
_write_empty_auth_store()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("mode", choices=("agent", "chat", "triage"))
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.mode == "agent":
|
||||||
|
stage_agent()
|
||||||
|
elif args.mode == "chat":
|
||||||
|
stage_chat()
|
||||||
|
else:
|
||||||
|
stage_triage()
|
||||||
|
print("Runtime access staged from Vault.", flush=True)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
141
services/hermes/scripts/sync_runtime_credentials.py
Normal file
141
services/hermes/scripts/sync_runtime_credentials.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Persist refreshed coding-client OAuth documents back into Vault."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
from urllib.error import HTTPError, URLError
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
|
||||||
|
VAULT_ADDR = os.environ.get(
|
||||||
|
"VAULT_ADDR", "http://vault.vault.svc.cluster.local:8200"
|
||||||
|
).rstrip("/")
|
||||||
|
VAULT_ROLE = os.environ.get(
|
||||||
|
"HERMES_CREDENTIAL_SYNC_VAULT_ROLE", "hermes-credential-sync"
|
||||||
|
)
|
||||||
|
SECRET_ENDPOINT = "/v1/kv/data/atlas/hermes/agent-tokens"
|
||||||
|
JWT_PATH = Path("/var/run/secrets/kubernetes.io/serviceaccount/token")
|
||||||
|
CREDENTIALS = {
|
||||||
|
"claude_credentials_json": (
|
||||||
|
Path("/runtime-access/claude/.credentials.json"),
|
||||||
|
("claudeAiOauth", "refreshToken"),
|
||||||
|
),
|
||||||
|
"codex_auth_json": (
|
||||||
|
Path("/runtime-access/codex/auth.json"),
|
||||||
|
("tokens", "refresh_token"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _request(
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
payload: dict[str, Any] | None = None,
|
||||||
|
*,
|
||||||
|
token: str = "",
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Issue one bounded Vault JSON request."""
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
if token:
|
||||||
|
headers["X-Vault-Token"] = token
|
||||||
|
request = Request(
|
||||||
|
VAULT_ADDR + path,
|
||||||
|
data=(json.dumps(payload).encode("utf-8") if payload is not None else None),
|
||||||
|
headers=headers,
|
||||||
|
method=method,
|
||||||
|
)
|
||||||
|
with urlopen(request, timeout=10) as response:
|
||||||
|
body = response.read(4 << 20)
|
||||||
|
value = json.loads(body)
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise RuntimeError("Vault returned a non-object response")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _login() -> str:
|
||||||
|
"""Exchange the pod identity for the narrow credential-sync policy."""
|
||||||
|
response = _request(
|
||||||
|
"POST",
|
||||||
|
"/v1/auth/kubernetes/login",
|
||||||
|
{"role": VAULT_ROLE, "jwt": JWT_PATH.read_text(encoding="utf-8").strip()},
|
||||||
|
)
|
||||||
|
token = response.get("auth", {}).get("client_token")
|
||||||
|
if not isinstance(token, str) or not token:
|
||||||
|
raise RuntimeError("Vault login returned no client token")
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def _credential_document(path: Path, required: tuple[str, ...]) -> str | None:
|
||||||
|
"""Return one complete credential JSON document or ignore a partial write."""
|
||||||
|
try:
|
||||||
|
value = path.read_text(encoding="utf-8").strip()
|
||||||
|
document = json.loads(value)
|
||||||
|
except (OSError, UnicodeError, json.JSONDecodeError):
|
||||||
|
return None
|
||||||
|
current = document
|
||||||
|
for key in required:
|
||||||
|
if not isinstance(current, dict) or key not in current:
|
||||||
|
return None
|
||||||
|
current = current[key]
|
||||||
|
if not isinstance(current, str) or not current:
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def sync_once(token: str) -> list[str]:
|
||||||
|
"""CAS-update only credential fields whose runtime documents changed."""
|
||||||
|
response = _request("GET", SECRET_ENDPOINT, token=token)
|
||||||
|
envelope = response.get("data")
|
||||||
|
if not isinstance(envelope, dict):
|
||||||
|
raise RuntimeError("Vault KV response has no data envelope")
|
||||||
|
data = envelope.get("data")
|
||||||
|
metadata = envelope.get("metadata")
|
||||||
|
if not isinstance(data, dict) or not isinstance(metadata, dict):
|
||||||
|
raise RuntimeError("Vault KV response has an invalid shape")
|
||||||
|
version = metadata.get("version")
|
||||||
|
if not isinstance(version, int) or version < 1:
|
||||||
|
raise RuntimeError("Vault KV response has no version")
|
||||||
|
updated = dict(data)
|
||||||
|
changed: list[str] = []
|
||||||
|
for field, (path, required) in CREDENTIALS.items():
|
||||||
|
value = _credential_document(path, required)
|
||||||
|
if value is not None and value != data.get(field):
|
||||||
|
updated[field] = value
|
||||||
|
changed.append(field)
|
||||||
|
if changed:
|
||||||
|
_request(
|
||||||
|
"POST",
|
||||||
|
SECRET_ENDPOINT,
|
||||||
|
{"options": {"cas": version}, "data": updated},
|
||||||
|
token=token,
|
||||||
|
)
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Continuously persist refresh-token rotation without exposing values."""
|
||||||
|
interval = max(30, int(os.environ.get("HERMES_CREDENTIAL_SYNC_INTERVAL", "60")))
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
token = _login()
|
||||||
|
changed = sync_once(token)
|
||||||
|
if changed:
|
||||||
|
print(
|
||||||
|
"Persisted refreshed runtime credentials: " + ", ".join(changed),
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
except (OSError, RuntimeError, HTTPError, URLError, ValueError) as error:
|
||||||
|
print(
|
||||||
|
f"Runtime credential sync deferred: {type(error).__name__}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
time.sleep(interval)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@ -23,12 +23,11 @@ class MediaPathError(ValueError):
|
|||||||
|
|
||||||
|
|
||||||
def read_relay_key(path: Path) -> str:
|
def read_relay_key(path: Path) -> str:
|
||||||
"""Read the shared relay key from the tenant's private environment file."""
|
"""Read the shared relay key from one runtime-only secret file."""
|
||||||
for line in path.read_text(encoding="utf-8").splitlines():
|
value = path.read_text(encoding="utf-8").strip()
|
||||||
key, separator, value = line.partition("=")
|
if not value:
|
||||||
if separator and key.strip() == "API_SERVER_KEY" and value.strip():
|
raise RuntimeError("tenant relay key is unavailable")
|
||||||
return value.strip()
|
return value
|
||||||
raise RuntimeError("tenant relay key is unavailable")
|
|
||||||
|
|
||||||
|
|
||||||
def tenant_slot(pod_name: str) -> str:
|
def tenant_slot(pod_name: str) -> str:
|
||||||
@ -179,7 +178,12 @@ class TenantMediaHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Start the tenant-local media handoff server."""
|
"""Start the tenant-local media handoff server."""
|
||||||
relay_key_path = Path(os.getenv("HERMES_MEDIA_RELAY_KEY_FILE", "/opt/data/.env"))
|
relay_key_path = Path(
|
||||||
|
os.getenv(
|
||||||
|
"HERMES_MEDIA_RELAY_KEY_FILE",
|
||||||
|
"/runtime-access/chat-relay-key",
|
||||||
|
)
|
||||||
|
)
|
||||||
relay_key = read_relay_key(relay_key_path)
|
relay_key = read_relay_key(relay_key_path)
|
||||||
slot = tenant_slot(os.getenv("POD_NAME", ""))
|
slot = tenant_slot(os.getenv("POD_NAME", ""))
|
||||||
server = TenantMediaServer(("0.0.0.0", 8788), relay_key, slot)
|
server = TenantMediaServer(("0.0.0.0", 8788), relay_key, slot)
|
||||||
|
|||||||
@ -22,7 +22,7 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: hermes-switchyard
|
app: hermes-switchyard
|
||||||
annotations:
|
annotations:
|
||||||
ai.bstein.dev/config-rev: "20260815-quality-escalation"
|
ai.bstein.dev/config-rev: "20260815-runtime-access-boundary"
|
||||||
prometheus.io/scrape: "true"
|
prometheus.io/scrape: "true"
|
||||||
prometheus.io/port: "9005"
|
prometheus.io/port: "9005"
|
||||||
prometheus.io/path: /metrics
|
prometheus.io/path: /metrics
|
||||||
|
|||||||
@ -38,6 +38,12 @@ spec:
|
|||||||
export GAME_MODE_HOOK_TOKEN="{{ .Data.data.game_mode_hook_token }}"
|
export GAME_MODE_HOOK_TOKEN="{{ .Data.data.game_mode_hook_token }}"
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ with secret "kv/data/atlas/hermes/triage-api" }}
|
||||||
|
export ARIADNE_HERMES_API_KEY="{{ .Data.data.api_key }}"
|
||||||
|
{{ end }}
|
||||||
|
{{ with secret "kv/data/atlas/hermes/developer-gitea" }}
|
||||||
|
export ARIADNE_HERMES_GITEA_TOKEN="{{ .Data.data.token }}"
|
||||||
|
{{ end }}
|
||||||
{{ with secret "kv/data/atlas/portal/atlas-portal-db" }}
|
{{ with secret "kv/data/atlas/portal/atlas-portal-db" }}
|
||||||
export PORTAL_DATABASE_URL="{{ .Data.data.PORTAL_DATABASE_URL }}"
|
export PORTAL_DATABASE_URL="{{ .Data.data.PORTAL_DATABASE_URL }}"
|
||||||
{{ end }}
|
{{ end }}
|
||||||
@ -489,12 +495,6 @@ spec:
|
|||||||
value: http://hermes.hermes.svc.cluster.local:8642
|
value: http://hermes.hermes.svc.cluster.local:8642
|
||||||
- name: ARIADNE_HERMES_RUN_TIMEOUT_SECONDS
|
- name: ARIADNE_HERMES_RUN_TIMEOUT_SECONDS
|
||||||
value: "420"
|
value: "420"
|
||||||
- name: ARIADNE_HERMES_API_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: hermes-api-server-key
|
|
||||||
key: api-key
|
|
||||||
optional: true
|
|
||||||
- name: ARIADNE_HERMES_CODE_ENABLED
|
- name: ARIADNE_HERMES_CODE_ENABLED
|
||||||
value: "true"
|
value: "true"
|
||||||
- name: ARIADNE_HERMES_CODE_JOB
|
- name: ARIADNE_HERMES_CODE_JOB
|
||||||
@ -605,12 +605,6 @@ spec:
|
|||||||
value: metis=cmd/|pkg/,lesavka=server/|client/|common/,soteria=cmd/|internal/,bstein-dev-home=backend/|frontend/,ariadne=ariadne/,ananke=cmd/|internal/,pegasus=backend/|frontend/,atlasbot=atlasbot/
|
value: metis=cmd/|pkg/,lesavka=server/|client/|common/,soteria=cmd/|internal/,bstein-dev-home=backend/|frontend/,ariadne=ariadne/,ananke=cmd/|internal/,pegasus=backend/|frontend/,atlasbot=atlasbot/
|
||||||
- name: ARIADNE_HERMES_CODE_SUFFIXES
|
- name: ARIADNE_HERMES_CODE_SUFFIXES
|
||||||
value: metis=.go,lesavka=.rs,soteria=.go,bstein-dev-home=.py|.ts|.tsx|.js,ariadne=.py,ananke=.go,pegasus=.go|.ts|.tsx|.js,atlasbot=.py
|
value: metis=.go,lesavka=.rs,soteria=.go,bstein-dev-home=.py|.ts|.tsx|.js,ariadne=.py,ananke=.go,pegasus=.go|.ts|.tsx|.js,atlasbot=.py
|
||||||
- name: ARIADNE_HERMES_GITEA_TOKEN
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: hermes-gitea-token
|
|
||||||
key: token
|
|
||||||
optional: true
|
|
||||||
- name: ARIADNE_TESTING_TRIAGE_MODEL_URL
|
- name: ARIADNE_TESTING_TRIAGE_MODEL_URL
|
||||||
value: http://hermes-model-gate.hermes.svc.cluster.local:11434
|
value: http://hermes-model-gate.hermes.svc.cluster.local:11434
|
||||||
- name: ARIADNE_TESTING_TRIAGE_MODEL
|
- name: ARIADNE_TESTING_TRIAGE_MODEL
|
||||||
|
|||||||
@ -254,9 +254,11 @@ write_policy_and_role "health" "health" "health-vault-sync" \
|
|||||||
write_policy_and_role "game-stream" "game-stream" "game-stream-vault" \
|
write_policy_and_role "game-stream" "game-stream" "game-stream-vault" \
|
||||||
"game-stream/*" ""
|
"game-stream/*" ""
|
||||||
write_policy_and_role "hermes" "hermes" "hermes-vault,hermes-triage" \
|
write_policy_and_role "hermes" "hermes" "hermes-vault,hermes-triage" \
|
||||||
"hermes/triage-oidc hermes/agent-tokens" ""
|
"hermes/triage-oidc hermes/agent-tokens hermes/triage-api" ""
|
||||||
write_policy_and_role "hermes-agent" "hermes" "hermes-agent,hermes-switchyard" \
|
write_policy_and_role "hermes-agent" "hermes" "hermes-agent,hermes-switchyard" \
|
||||||
"hermes/agent-oidc hermes/agent-tokens hermes/chat-telegram hermes/developer-keycloak hermes/developer-gitea hermes/developer-harbor hermes/developer-jenkins hermes/developer-ssh" ""
|
"hermes/agent-oidc hermes/agent-tokens hermes/chat-telegram hermes/developer-keycloak hermes/developer-gitea hermes/developer-harbor hermes/developer-jenkins hermes/developer-ssh" ""
|
||||||
|
write_policy_and_role "hermes-credential-sync" "hermes" "hermes-agent" \
|
||||||
|
"" "hermes/agent-tokens"
|
||||||
write_policy_and_role "hermes-node-ssh" "hermes" "hermes-node-ssh-access" \
|
write_policy_and_role "hermes-node-ssh" "hermes" "hermes-node-ssh-access" \
|
||||||
"hermes/developer-ssh" ""
|
"hermes/developer-ssh" ""
|
||||||
write_policy_and_role "hermes-chat" "hermes" "hermes-chat" \
|
write_policy_and_role "hermes-chat" "hermes" "hermes-chat" \
|
||||||
@ -278,7 +280,7 @@ write_policy_and_role "cassandra-secrets" "cassandra" "cassandra-secrets-ensure"
|
|||||||
write_policy_and_role "cassandra-migration" "cassandra" "cassandra-db-migration" \
|
write_policy_and_role "cassandra-migration" "cassandra" "cassandra-db-migration" \
|
||||||
"veles/veles-db cassandra/cassandra-db" ""
|
"veles/veles-db cassandra/cassandra-db" ""
|
||||||
write_policy_and_role "maintenance" "maintenance" "ariadne,maintenance-vault-sync,metis" \
|
write_policy_and_role "maintenance" "maintenance" "ariadne,maintenance-vault-sync,metis" \
|
||||||
"maintenance/ariadne-db maintenance/metis-oidc maintenance/soteria-oidc maintenance/metis-ssh-keys maintenance/metis-runtime portal/atlas-portal-db portal/bstein-dev-home-keycloak-admin mailu/mailu-db-secret mailu/mailu-initial-account-secret nextcloud/nextcloud-db nextcloud/nextcloud-admin health/wger-admin finance/firefly-secrets comms/mas-admin-client-runtime comms/atlasbot-credentials-runtime comms/synapse-db comms/synapse-admin vault/vault-oidc-config shared/harbor-pull shared/soteria-restic harbor/harbor-core" "" \
|
"maintenance/ariadne-db maintenance/metis-oidc maintenance/soteria-oidc maintenance/metis-ssh-keys maintenance/metis-runtime portal/atlas-portal-db portal/bstein-dev-home-keycloak-admin mailu/mailu-db-secret mailu/mailu-initial-account-secret nextcloud/nextcloud-db nextcloud/nextcloud-admin health/wger-admin finance/firefly-secrets comms/mas-admin-client-runtime comms/atlasbot-credentials-runtime comms/synapse-db comms/synapse-admin vault/vault-oidc-config shared/harbor-pull shared/soteria-restic harbor/harbor-core hermes/triage-api hermes/developer-gitea" "" \
|
||||||
'
|
'
|
||||||
path "kv/data/atlas/nodes/*" {
|
path "kv/data/atlas/nodes/*" {
|
||||||
capabilities = ["read"]
|
capabilities = ["read"]
|
||||||
|
|||||||
@ -173,6 +173,9 @@ def test_gateway_image_honors_ui_model_and_caps_reasoning():
|
|||||||
assert "Live worker activity" in activity
|
assert "Live worker activity" in activity
|
||||||
assert "api.getSessionMessages" in activity
|
assert "api.getSessionMessages" in activity
|
||||||
assert "aria-live=\"polite\"" in activity
|
assert "aria-live=\"polite\"" in activity
|
||||||
|
assert "role=\"log\"" in activity
|
||||||
|
assert "aria-busy={running && messages.length === 0}" in activity
|
||||||
|
assert 'detail.source !== "api_server"' not in activity
|
||||||
assert "Poll-backed transcript; terminal rendering is not required." in activity
|
assert "Poll-backed transcript; terminal rendering is not required." in activity
|
||||||
assert "Open Telegram setup" in dockerfile
|
assert "Open Telegram setup" in dockerfile
|
||||||
|
|
||||||
@ -418,7 +421,8 @@ def test_chat_image_generation_uses_private_owner_broker():
|
|||||||
assert mounts["image-plugin"]["mountPath"] == (
|
assert mounts["image-plugin"]["mountPath"] == (
|
||||||
"/opt/hermes/plugins/image_gen/atlas-broker"
|
"/opt/hermes/plugins/image_gen/atlas-broker"
|
||||||
)
|
)
|
||||||
assert mounts["provider-auth"].get("readOnly") is not True
|
assert mounts["runtime-access"]["mountPath"] == "/runtime-access"
|
||||||
|
assert "provider-auth" not in mounts
|
||||||
assert not any(mount["name"] == "home" and "agent" in str(mount) for mount in hermes["volumeMounts"])
|
assert not any(mount["name"] == "home" and "agent" in str(mount) for mount in hermes["volumeMounts"])
|
||||||
env = {item["name"]: item["value"] for item in hermes["env"]}
|
env = {item["name"]: item["value"] for item in hermes["env"]}
|
||||||
assert env["HERMES_IMAGE_BROKER_URL"].startswith("http://hermes-image-broker.")
|
assert env["HERMES_IMAGE_BROKER_URL"].startswith("http://hermes-image-broker.")
|
||||||
@ -597,7 +601,7 @@ def test_chat_reasoning_uses_switchyard_without_owner_credentials():
|
|||||||
statefulset = _documents(HERMES / "chat-statefulset.yaml")[0]
|
statefulset = _documents(HERMES / "chat-statefulset.yaml")[0]
|
||||||
assert statefulset["spec"]["template"]["metadata"]["annotations"][
|
assert statefulset["spec"]["template"]["metadata"]["annotations"][
|
||||||
"ai.bstein.dev/config-rev"
|
"ai.bstein.dev/config-rev"
|
||||||
] == "20260812-stream-recovery"
|
] == "20260815-runtime-access-boundary"
|
||||||
pod_spec = statefulset["spec"]["template"]["spec"]
|
pod_spec = statefulset["spec"]["template"]["spec"]
|
||||||
patch_init = next(
|
patch_init = next(
|
||||||
item for item in pod_spec["initContainers"]
|
item for item in pod_spec["initContainers"]
|
||||||
@ -1044,6 +1048,12 @@ def test_api_session_patch_accepts_parent_lineage(tmp_path: Path):
|
|||||||
+ module.RUNS_BEFORE
|
+ module.RUNS_BEFORE
|
||||||
+ "run body\n"
|
+ "run body\n"
|
||||||
+ module.RUN_CLOSE_BEFORE
|
+ module.RUN_CLOSE_BEFORE
|
||||||
|
+ module.EVENT_CALLBACK_SIGNATURE_BEFORE
|
||||||
|
+ "callback docstring and push helper\n"
|
||||||
|
+ module.EVENT_CALLBACK_BODY_BEFORE
|
||||||
|
+ "tool start body\n"
|
||||||
|
+ module.EVENT_CALLBACK_END_BEFORE
|
||||||
|
+ module.EVENT_CALLBACK_CALL_BEFORE
|
||||||
+ "suffix\n",
|
+ "suffix\n",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
@ -1059,6 +1069,48 @@ def test_api_session_patch_accepts_parent_lineage(tmp_path: Path):
|
|||||||
assert "session_parent_conflict" in patched
|
assert "session_parent_conflict" in patched
|
||||||
assert "db.reopen_session(session_id)" in patched
|
assert "db.reopen_session(session_id)" in patched
|
||||||
assert 'db.end_session(session_id, f"api_run_{terminal_status}")' in patched
|
assert 'db.end_session(session_id, f"api_run_{terminal_status}")' in patched
|
||||||
|
assert "def _record_run_activity(" in patched
|
||||||
|
assert '"_thinking": "Hermes is reasoning"' in patched
|
||||||
|
assert '"run.started": "Worker started"' in patched
|
||||||
|
assert '"run.completed": "Worker completed"' in patched
|
||||||
|
assert '"reasoning.available": "Hermes finished a reasoning step"' in patched
|
||||||
|
assert '"subagent.progress": "Nested worker progress"' in patched
|
||||||
|
assert "redact_sensitive_text" in patched
|
||||||
|
assert 'getattr(os, "O_NOFOLLOW", 0)' in patched
|
||||||
|
assert "os.fchmod(fd, 0o600)" in patched
|
||||||
|
assert "session_id=session_id" in patched
|
||||||
|
assert 'self._record_run_activity(session_id, "run.started")' in patched
|
||||||
|
assert 'detail = tool_name if event_type in {' in patched
|
||||||
|
assert 'if event_type == "subagent.tool"' in patched
|
||||||
|
|
||||||
|
|
||||||
|
def test_web_session_activity_patch_projects_bounded_events(tmp_path: Path):
|
||||||
|
"""The DOM transcript includes events without polluting agent history."""
|
||||||
|
module_path = HERMES / "scripts" / "patch_web_session_activity.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("patch_web_activity", module_path)
|
||||||
|
assert spec and spec.loader
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
source = tmp_path / "web_server.py"
|
||||||
|
destination = tmp_path / "patched.py"
|
||||||
|
source.write_text(
|
||||||
|
"prefix\n"
|
||||||
|
+ module.HELPER_MARKER
|
||||||
|
+ " db = object()\n"
|
||||||
|
+ module.MESSAGES_BEFORE
|
||||||
|
+ "suffix\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
module.patch(source, destination)
|
||||||
|
patched = destination.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "def _run_activity_messages(" in patched
|
||||||
|
assert 'root = get_hermes_home() / "run-activity"' in patched
|
||||||
|
assert "path.stat().st_size > 600_000" in patched
|
||||||
|
assert "entries[-1_000:]" in patched
|
||||||
|
assert "*_run_activity_messages(sid)" in patched
|
||||||
|
assert "messages.sort(" in patched
|
||||||
|
|
||||||
|
|
||||||
def test_legacy_api_sessions_are_nested_idempotently(tmp_path: Path):
|
def test_legacy_api_sessions_are_nested_idempotently(tmp_path: Path):
|
||||||
@ -1723,40 +1775,41 @@ def test_local_flux_uses_low_vram_offload_without_reducing_resolution():
|
|||||||
assert '"square": (1024, 1024)' in source
|
assert '"square": (1024, 1024)' in source
|
||||||
|
|
||||||
|
|
||||||
def test_chat_auth_file_mount_survives_atomic_provider_refresh():
|
def test_chat_auth_and_relay_are_pod_lifetime_only():
|
||||||
statefulset = _documents(HERMES / "chat-statefulset.yaml")[0]
|
statefulset = _documents(HERMES / "chat-statefulset.yaml")[0]
|
||||||
|
pod = statefulset["spec"]["template"]["spec"]
|
||||||
containers = statefulset["spec"]["template"]["spec"]["containers"]
|
containers = statefulset["spec"]["template"]["spec"]["containers"]
|
||||||
|
|
||||||
for name in ("hermes", "webui"):
|
for name in ("hermes", "webui"):
|
||||||
container = next(item for item in containers if item["name"] == name)
|
container = next(item for item in containers if item["name"] == name)
|
||||||
env = {item["name"]: item["value"] for item in container["env"]}
|
env = {item["name"]: item["value"] for item in container["env"]}
|
||||||
assert env["HERMES_AUTH_FILE"] == "/shared-auth/auth.json"
|
assert env["HERMES_AUTH_FILE"] == "/runtime-access/hermes-auth.json"
|
||||||
mount = next(
|
mount = next(
|
||||||
item for item in container["volumeMounts"] if item["name"] == "provider-auth"
|
item for item in container["volumeMounts"] if item["name"] == "runtime-access"
|
||||||
)
|
)
|
||||||
assert mount["mountPath"] == "/shared-auth"
|
assert mount["mountPath"] == "/runtime-access"
|
||||||
assert "subPath" not in mount
|
assert "subPath" not in mount
|
||||||
|
|
||||||
hermes_env = {
|
hermes_env = {
|
||||||
item["name"]: item["value"]
|
item["name"]: item["value"]
|
||||||
for item in next(item for item in containers if item["name"] == "hermes")["env"]
|
for item in next(item for item in containers if item["name"] == "hermes")["env"]
|
||||||
}
|
}
|
||||||
hermes_mount = next(
|
runtime = next(item for item in pod["volumes"] if item["name"] == "runtime-access")
|
||||||
item
|
assert runtime["emptyDir"] == {"medium": "Memory", "sizeLimit": "2Mi"}
|
||||||
for item in next(
|
assert not any(item["name"] == "provider-auth" for item in pod["volumes"])
|
||||||
item for item in containers if item["name"] == "hermes"
|
init_command = next(
|
||||||
)["volumeMounts"]
|
item for item in pod["initContainers"] if item["name"] == "init-config"
|
||||||
if item["name"] == "provider-auth"
|
)["command"][2]
|
||||||
)
|
for key in (
|
||||||
assert hermes_mount.get("readOnly") is not True
|
"ANTHROPIC_API_KEY",
|
||||||
webui_mount = next(
|
"API_SERVER_KEY",
|
||||||
item
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
||||||
for item in next(
|
"GITEA_TOKEN",
|
||||||
item for item in containers if item["name"] == "webui"
|
"HERMES_IMAGE_BROKER_KEY",
|
||||||
)["volumeMounts"]
|
"OPENAI_API_KEY",
|
||||||
if item["name"] == "provider-auth"
|
):
|
||||||
)
|
assert key in init_command
|
||||||
assert webui_mount["readOnly"] is True
|
assert "printf 'API_SERVER_KEY=%s" not in init_command
|
||||||
assert hermes_env["AGENT_BROWSER_EXECUTABLE_PATH"].endswith("/chrome-linux/headless_shell")
|
assert hermes_env["AGENT_BROWSER_EXECUTABLE_PATH"].endswith("/chrome-linux/headless_shell")
|
||||||
assert "--no-sandbox" in hermes_env["AGENT_BROWSER_ARGS"]
|
assert "--no-sandbox" in hermes_env["AGENT_BROWSER_ARGS"]
|
||||||
|
|
||||||
|
|||||||
@ -802,16 +802,21 @@ def test_agent_refreshes_routes_after_restoring_cli_logins():
|
|||||||
"hermes_coordinator.py --once"
|
"hermes_coordinator.py --once"
|
||||||
)
|
)
|
||||||
env = {item["name"]: item["value"] for item in configure["env"]}
|
env = {item["name"]: item["value"] for item in configure["env"]}
|
||||||
assert env["HERMES_AUTH_FILE"] == "/shared-auth/auth.json"
|
assert env["HERMES_AUTH_FILE"] == "/runtime-access/hermes-auth.json"
|
||||||
assert env["PYTHONPATH"] == "/opt/hermes"
|
assert env["PYTHONPATH"] == "/opt/hermes"
|
||||||
for name in ("bootstrap-coordinator", "configure-agent-clients"):
|
for name in ("bootstrap-coordinator", "configure-agent-clients"):
|
||||||
route_env = {
|
route_env = {
|
||||||
item["name"]: item["value"]
|
item["name"]: item["value"]
|
||||||
for item in init_containers[name]["env"]
|
for item in init_containers[name]["env"]
|
||||||
}
|
}
|
||||||
assert route_env["CODEX_HOME"] == "/opt/data/home/.codex"
|
assert route_env["CODEX_HOME"] == "/runtime-access/codex"
|
||||||
|
assert route_env["CLAUDE_CONFIG_DIR"] == "/runtime-access/claude"
|
||||||
assert "/opt/data/tools/bin" in route_env["PATH"]
|
assert "/opt/data/tools/bin" in route_env["PATH"]
|
||||||
|
|
||||||
|
assert "patch-web-session-activity" in init_containers
|
||||||
|
web_patch = init_containers["patch-web-session-activity"]
|
||||||
|
assert "/opt/coordinator/patch_web_session_activity.py" in web_patch["command"]
|
||||||
|
|
||||||
containers = {
|
containers = {
|
||||||
item["name"]: item
|
item["name"]: item
|
||||||
for item in deployment["spec"]["template"]["spec"]["containers"]
|
for item in deployment["spec"]["template"]["spec"]["containers"]
|
||||||
@ -820,8 +825,18 @@ def test_agent_refreshes_routes_after_restoring_cli_logins():
|
|||||||
item["name"]: item["value"]
|
item["name"]: item["value"]
|
||||||
for item in containers["model-steward"]["env"]
|
for item in containers["model-steward"]["env"]
|
||||||
}
|
}
|
||||||
assert steward_env["CODEX_HOME"] == "/opt/data/home/.codex"
|
assert steward_env["CODEX_HOME"] == "/runtime-access/codex"
|
||||||
|
assert steward_env["CLAUDE_CONFIG_DIR"] == "/runtime-access/claude"
|
||||||
assert "/opt/data/tools/bin" in steward_env["PATH"]
|
assert "/opt/data/tools/bin" in steward_env["PATH"]
|
||||||
|
hermes_mounts = {
|
||||||
|
(item["name"], item["mountPath"], item.get("subPath"))
|
||||||
|
for item in containers["hermes"]["volumeMounts"]
|
||||||
|
}
|
||||||
|
assert (
|
||||||
|
"web-server-patch",
|
||||||
|
"/opt/hermes/hermes_cli/web_server.py",
|
||||||
|
"web_server.py",
|
||||||
|
) in hermes_mounts
|
||||||
|
|
||||||
|
|
||||||
def test_flux_health_checks_follow_the_owner_oauth_sidecar():
|
def test_flux_health_checks_follow_the_owner_oauth_sidecar():
|
||||||
@ -941,14 +956,17 @@ def test_owner_agent_has_pinned_dedicated_node_ssh_access():
|
|||||||
if item["name"] == "init-config"
|
if item["name"] == "init-config"
|
||||||
)
|
)
|
||||||
command = init["command"][2]
|
command = init["command"][2]
|
||||||
assert "cp /vault/secrets/node-ssh-config /opt/data/home/.ssh/config" in command
|
assert "ln -s /runtime-access/node-ssh-config /opt/data/home/.ssh/config" in command
|
||||||
assert (
|
assert (
|
||||||
"cp /vault/secrets/node-ssh-known-hosts /opt/data/home/.ssh/known_hosts"
|
"ln -s /runtime-access/node-ssh-known-hosts /opt/data/home/.ssh/known_hosts"
|
||||||
in command
|
in command
|
||||||
)
|
)
|
||||||
assert "ln -s home/.ssh /opt/data/.ssh" in command
|
assert "ln -s home/.ssh /opt/data/.ssh" in command
|
||||||
assert "chmod 0700 /opt/data/home/.ssh" in command
|
assert "chmod 0700 /opt/data/home/.ssh" in command
|
||||||
assert "chmod 0600 /opt/data/home/.ssh/id_ed25519_atlas_nodes" in command
|
assert (
|
||||||
|
"ln -s /runtime-access/node-ssh-private-key "
|
||||||
|
"/opt/data/home/.ssh/id_ed25519_atlas_nodes"
|
||||||
|
) in command
|
||||||
|
|
||||||
config = yaml.safe_load((HERMES / "agent-configmap.yaml").read_text())["data"]
|
config = yaml.safe_load((HERMES / "agent-configmap.yaml").read_text())["data"]
|
||||||
assert "ssh_config" not in config
|
assert "ssh_config" not in config
|
||||||
|
|||||||
@ -207,6 +207,15 @@ def test_configure_routes_keeps_every_profile_on_switchyard(tmp_path: Path):
|
|||||||
"GIT_ASKPASS=/opt/coordinator/gitea_askpass.sh\n",
|
"GIT_ASKPASS=/opt/coordinator/gitea_askpass.sh\n",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
stale_profile = tmp_path / "profiles/codex-high"
|
||||||
|
stale_profile.mkdir(parents=True)
|
||||||
|
(stale_profile / ".env").write_text(
|
||||||
|
"CLAUDE_CODE_OAUTH_TOKEN=stale-claude\n"
|
||||||
|
"GITEA_TOKEN=stale-gitea\n"
|
||||||
|
"HERMES_IMAGE_BROKER_KEY=stale-relay\n"
|
||||||
|
"USER_SETTING=preserve\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
codex = routing.Catalog(
|
codex = routing.Catalog(
|
||||||
"openai-codex", ["gpt-5.6-sol", "gpt-5.6-terra"], True, True, "connected"
|
"openai-codex", ["gpt-5.6-sol", "gpt-5.6-terra"], True, True, "connected"
|
||||||
)
|
)
|
||||||
@ -253,9 +262,11 @@ def test_configure_routes_keeps_every_profile_on_switchyard(tmp_path: Path):
|
|||||||
)
|
)
|
||||||
|
|
||||||
profile_env = (tmp_path / "profiles/codex-high/.env").read_text(encoding="utf-8")
|
profile_env = (tmp_path / "profiles/codex-high/.env").read_text(encoding="utf-8")
|
||||||
assert "CLAUDE_CODE_OAUTH_TOKEN=claude-secret" in profile_env
|
assert "CLAUDE_CODE_OAUTH_TOKEN" not in profile_env
|
||||||
assert "GITEA_TOKEN=gitea-secret" in profile_env
|
assert "GITEA_TOKEN" not in profile_env
|
||||||
|
assert "HERMES_IMAGE_BROKER_KEY" not in profile_env
|
||||||
assert "API_SERVER_KEY" not in profile_env
|
assert "API_SERVER_KEY" not in profile_env
|
||||||
|
assert "USER_SETTING=preserve" in profile_env
|
||||||
assert "claude-secret" not in json.dumps(routes)
|
assert "claude-secret" not in json.dumps(routes)
|
||||||
assert (tmp_path / "profiles/codex-high/.env").stat().st_mode & 0o777 == 0o600
|
assert (tmp_path / "profiles/codex-high/.env").stat().st_mode & 0o777 == 0o600
|
||||||
|
|
||||||
@ -411,10 +422,15 @@ def test_cassandra_sync_repairs_existing_worktree(
|
|||||||
(workspace / ".git").mkdir(parents=True)
|
(workspace / ".git").mkdir(parents=True)
|
||||||
monkeypatch.setattr(coordinator, "CASSANDRA_BASE_PATH", workspace)
|
monkeypatch.setattr(coordinator, "CASSANDRA_BASE_PATH", workspace)
|
||||||
monkeypatch.setattr(coordinator.shutil, "which", lambda _name: "/usr/bin/git")
|
monkeypatch.setattr(coordinator.shutil, "which", lambda _name: "/usr/bin/git")
|
||||||
|
token_path = tmp_path / "gitea-token"
|
||||||
|
token_path.write_text("configured\n", encoding="utf-8")
|
||||||
|
monkeypatch.setenv("HERMES_GITEA_TOKEN_FILE", str(token_path))
|
||||||
commands: list[list[str]] = []
|
commands: list[list[str]] = []
|
||||||
|
environments: list[dict[str, str]] = []
|
||||||
|
|
||||||
def run(command, **_kwargs):
|
def run(command, **kwargs):
|
||||||
commands.append(command)
|
commands.append(command)
|
||||||
|
environments.append(kwargs.get("env", {}))
|
||||||
if command[-2:] == ["get-url", "origin"]:
|
if command[-2:] == ["get-url", "origin"]:
|
||||||
return subprocess.CompletedProcess(
|
return subprocess.CompletedProcess(
|
||||||
command, 0 if current_remote else 2, stdout=f"{current_remote or ''}\n"
|
command, 0 if current_remote else 2, stdout=f"{current_remote or ''}\n"
|
||||||
@ -422,10 +438,11 @@ def test_cassandra_sync_repairs_existing_worktree(
|
|||||||
return subprocess.CompletedProcess(command, 0)
|
return subprocess.CompletedProcess(command, 0)
|
||||||
|
|
||||||
monkeypatch.setattr(coordinator.subprocess, "run", run)
|
monkeypatch.setattr(coordinator.subprocess, "run", run)
|
||||||
assert coordinator.sync_cassandra_repo({"GITEA_TOKEN": "configured"}) == "ready"
|
assert coordinator.sync_cassandra_repo({"GITEA_TOKEN": "must-not-pass"}) == "ready"
|
||||||
repair_commands = [command for command in commands if repair_action in command]
|
repair_commands = [command for command in commands if repair_action in command]
|
||||||
assert bool(repair_commands) is bool(repair_action)
|
assert bool(repair_commands) is bool(repair_action)
|
||||||
assert commands[-1][-4:] == ["fetch", "--quiet", "--prune", "origin"]
|
assert commands[-1][-4:] == ["fetch", "--quiet", "--prune", "origin"]
|
||||||
|
assert all("GITEA_TOKEN" not in environment for environment in environments)
|
||||||
|
|
||||||
|
|
||||||
def test_cassandra_sync_repairs_origin_without_token(tmp_path: Path, monkeypatch):
|
def test_cassandra_sync_repairs_origin_without_token(tmp_path: Path, monkeypatch):
|
||||||
|
|||||||
276
testing/tests/test_hermes_runtime_access.py
Normal file
276
testing/tests/test_hermes_runtime_access.py
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
"""Contracts for Hermes' Vault-only runtime access boundary."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).parents[2]
|
||||||
|
HERMES = ROOT / "services" / "hermes"
|
||||||
|
SCRIPTS = HERMES / "scripts"
|
||||||
|
|
||||||
|
|
||||||
|
def _load(name: str):
|
||||||
|
spec = importlib.util.spec_from_file_location(name, SCRIPTS / f"{name}.py")
|
||||||
|
assert spec and spec.loader
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[spec.name] = module
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_runtime_stage_keeps_credentials_in_memory(tmp_path: Path, monkeypatch):
|
||||||
|
stage = _load("stage_runtime_access")
|
||||||
|
vault = tmp_path / "vault"
|
||||||
|
runtime = tmp_path / "runtime"
|
||||||
|
home = tmp_path / "home"
|
||||||
|
vault.mkdir()
|
||||||
|
(home / ".claude").mkdir(parents=True)
|
||||||
|
(home / ".claude" / "backups").mkdir()
|
||||||
|
(home / ".codex" / "skills").mkdir(parents=True)
|
||||||
|
(home / ".codex" / "sessions").mkdir()
|
||||||
|
(home / ".claude" / "settings.json").write_text("{}\n", encoding="utf-8")
|
||||||
|
(home / ".claude" / "backups" / "credentials.json").write_text(
|
||||||
|
"do-not-link\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
values = {
|
||||||
|
"agent-api-key": "agent-key",
|
||||||
|
"chat-relay-key": "relay-key",
|
||||||
|
"gitea-token": "gitea-key",
|
||||||
|
"gitea-username": "hermes-automation",
|
||||||
|
"node-ssh-private-key": "private-key",
|
||||||
|
"node-ssh-config": "host-config",
|
||||||
|
"node-ssh-known-hosts": "known-hosts",
|
||||||
|
"claude-credentials": json.dumps(
|
||||||
|
{"claudeAiOauth": {"refreshToken": "claude-refresh"}}
|
||||||
|
),
|
||||||
|
"codex-auth": json.dumps({"tokens": {"refresh_token": "codex-refresh"}}),
|
||||||
|
}
|
||||||
|
for name, value in values.items():
|
||||||
|
(vault / name).write_text(value + "\n", encoding="utf-8")
|
||||||
|
monkeypatch.setattr(stage, "VAULT_ROOT", vault)
|
||||||
|
monkeypatch.setattr(stage, "RUNTIME_ROOT", runtime)
|
||||||
|
monkeypatch.setattr(stage, "PERSISTENT_HOME", home)
|
||||||
|
monkeypatch.setattr(stage.os, "chown", lambda *_args: None)
|
||||||
|
|
||||||
|
stage.stage_agent()
|
||||||
|
|
||||||
|
assert (runtime / "claude/.credentials.json").stat().st_mode & 0o777 == 0o600
|
||||||
|
assert (runtime / "codex/auth.json").stat().st_mode & 0o777 == 0o600
|
||||||
|
assert (runtime / "claude/settings.json").is_symlink()
|
||||||
|
assert (runtime / "codex/skills").is_symlink()
|
||||||
|
assert not (runtime / "claude/backups").exists()
|
||||||
|
assert not (runtime / "codex/sessions").exists()
|
||||||
|
auth = json.loads((runtime / "hermes-auth.json").read_text(encoding="utf-8"))
|
||||||
|
assert auth == {"version": 1, "providers": {}, "credential_pool": {}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_runtime_json_is_removed(tmp_path: Path, monkeypatch):
|
||||||
|
stage = _load("stage_runtime_access")
|
||||||
|
vault = tmp_path / "vault"
|
||||||
|
runtime = tmp_path / "runtime"
|
||||||
|
vault.mkdir()
|
||||||
|
runtime.mkdir()
|
||||||
|
(vault / "credential").write_text("not-json\n", encoding="utf-8")
|
||||||
|
monkeypatch.setattr(stage, "VAULT_ROOT", vault)
|
||||||
|
monkeypatch.setattr(stage.os, "chown", lambda *_args: None)
|
||||||
|
destination = runtime / "credential.json"
|
||||||
|
|
||||||
|
try:
|
||||||
|
stage._validated_json("credential", destination, ("token",))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise AssertionError("invalid credential JSON should fail staging")
|
||||||
|
|
||||||
|
assert not destination.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_refresh_sync_uses_cas_and_preserves_other_fields(
|
||||||
|
tmp_path: Path, monkeypatch
|
||||||
|
):
|
||||||
|
sync = _load("sync_runtime_credentials")
|
||||||
|
claude = tmp_path / "claude.json"
|
||||||
|
codex = tmp_path / "codex.json"
|
||||||
|
claude.write_text(
|
||||||
|
json.dumps({"claudeAiOauth": {"refreshToken": "new-claude"}}),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
codex.write_text(
|
||||||
|
json.dumps({"tokens": {"refresh_token": "new-codex"}}),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
sync,
|
||||||
|
"CREDENTIALS",
|
||||||
|
{
|
||||||
|
"claude_credentials_json": (
|
||||||
|
claude,
|
||||||
|
("claudeAiOauth", "refreshToken"),
|
||||||
|
),
|
||||||
|
"codex_auth_json": (codex, ("tokens", "refresh_token")),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
writes = []
|
||||||
|
|
||||||
|
def request(method, path, payload=None, *, token=""):
|
||||||
|
assert token == "vault-token"
|
||||||
|
if method == "GET":
|
||||||
|
return {
|
||||||
|
"data": {
|
||||||
|
"data": {
|
||||||
|
"claude_credentials_json": "old",
|
||||||
|
"codex_auth_json": "old",
|
||||||
|
"agent_api_key": "preserve-me",
|
||||||
|
},
|
||||||
|
"metadata": {"version": 7},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writes.append((path, payload))
|
||||||
|
return {}
|
||||||
|
|
||||||
|
monkeypatch.setattr(sync, "_request", request)
|
||||||
|
|
||||||
|
assert sync.sync_once("vault-token") == [
|
||||||
|
"claude_credentials_json",
|
||||||
|
"codex_auth_json",
|
||||||
|
]
|
||||||
|
assert writes[0][1]["options"] == {"cas": 7}
|
||||||
|
assert writes[0][1]["data"]["agent_api_key"] == "preserve-me"
|
||||||
|
|
||||||
|
|
||||||
|
def test_subprocess_patches_strip_and_redact_runtime_credentials(tmp_path: Path):
|
||||||
|
boundary = _load("patch_subprocess_secret_boundary")
|
||||||
|
process = _load("patch_process_output_redaction")
|
||||||
|
local_source = tmp_path / "local.py"
|
||||||
|
local_output = tmp_path / "patched-local.py"
|
||||||
|
local_source.write_text("prefix\n" + boundary.BEFORE + "suffix\n", encoding="utf-8")
|
||||||
|
boundary.patch(local_source, local_output)
|
||||||
|
patched_local = local_output.read_text(encoding="utf-8")
|
||||||
|
for name in (
|
||||||
|
"API_SERVER_KEY",
|
||||||
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
||||||
|
"GITEA_TOKEN",
|
||||||
|
"HERMES_IMAGE_BROKER_KEY",
|
||||||
|
):
|
||||||
|
assert f'"{name}"' in patched_local
|
||||||
|
|
||||||
|
process_source = tmp_path / "process.py"
|
||||||
|
process_output = tmp_path / "patched-process.py"
|
||||||
|
process_source.write_text(
|
||||||
|
"prefix\n" + process.BEFORE + "suffix\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
process.patch(process_source, process_output)
|
||||||
|
assert "code_file=False" in process_output.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_manifests_never_seed_access_material_into_persistent_env():
|
||||||
|
agent = yaml.safe_load((HERMES / "agent-deployment.yaml").read_text())
|
||||||
|
chat = yaml.safe_load((HERMES / "chat-statefulset.yaml").read_text())
|
||||||
|
triage = yaml.safe_load((HERMES / "deployment.yaml").read_text())
|
||||||
|
for workload in (agent, chat, triage):
|
||||||
|
pod = workload["spec"]["template"]["spec"]
|
||||||
|
runtime = next(item for item in pod["volumes"] if item["name"] == "runtime-access")
|
||||||
|
assert runtime["emptyDir"]["medium"] == "Memory"
|
||||||
|
assert not any(item["name"] == "provider-auth" for item in pod["volumes"])
|
||||||
|
init = next(item for item in pod["initContainers"] if item["name"] == "init-config")
|
||||||
|
command = init["command"][2]
|
||||||
|
for key in (
|
||||||
|
"ANTHROPIC_API_KEY",
|
||||||
|
"API_SERVER_KEY",
|
||||||
|
"CLAUDE_API_KEY",
|
||||||
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
||||||
|
"GITEA_TOKEN",
|
||||||
|
"HERMES_IMAGE_BROKER_KEY",
|
||||||
|
"OPENAI_API_KEY",
|
||||||
|
):
|
||||||
|
assert f"printf '{key}=" not in command
|
||||||
|
assert f"upsert_env {key}" not in command
|
||||||
|
|
||||||
|
annotations = agent["spec"]["template"]["metadata"]["annotations"]
|
||||||
|
agent_runtime = next(
|
||||||
|
item
|
||||||
|
for item in agent["spec"]["template"]["spec"]["volumes"]
|
||||||
|
if item["name"] == "runtime-access"
|
||||||
|
)
|
||||||
|
assert agent_runtime["emptyDir"] == {
|
||||||
|
"medium": "Memory",
|
||||||
|
"sizeLimit": "128Mi",
|
||||||
|
}
|
||||||
|
assert "vault.hashicorp.com/agent-inject-secret-anthropic-token" not in annotations
|
||||||
|
assert annotations["vault.hashicorp.com/agent-inject-secret-claude-credentials"] == (
|
||||||
|
"kv/data/atlas/hermes/agent-tokens"
|
||||||
|
)
|
||||||
|
assert annotations["vault.hashicorp.com/agent-inject-secret-codex-auth"] == (
|
||||||
|
"kv/data/atlas/hermes/agent-tokens"
|
||||||
|
)
|
||||||
|
credential_sync = next(
|
||||||
|
item
|
||||||
|
for item in agent["spec"]["template"]["spec"]["containers"]
|
||||||
|
if item["name"] == "credential-sync"
|
||||||
|
)
|
||||||
|
assert {
|
||||||
|
(item["mountPath"], item.get("subPath"))
|
||||||
|
for item in credential_sync["volumeMounts"]
|
||||||
|
if item["name"] == "runtime-access"
|
||||||
|
} == {
|
||||||
|
("/runtime-access/claude", "claude"),
|
||||||
|
("/runtime-access/codex", "codex"),
|
||||||
|
}
|
||||||
|
triage_annotations = triage["spec"]["template"]["metadata"]["annotations"]
|
||||||
|
assert "vault.hashicorp.com/agent-inject-secret-anthropic-token" not in (
|
||||||
|
triage_annotations
|
||||||
|
)
|
||||||
|
assert triage_annotations[
|
||||||
|
"vault.hashicorp.com/agent-inject-secret-triage-api-key"
|
||||||
|
] == "kv/data/atlas/hermes/triage-api"
|
||||||
|
askpass = (SCRIPTS / "gitea_askpass.sh").read_text(encoding="utf-8")
|
||||||
|
assert "/runtime-access/gitea-token" in askpass
|
||||||
|
assert "GITEA_TOKEN" not in askpass
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_media_reads_one_raw_runtime_secret(tmp_path: Path):
|
||||||
|
module = _load("telegram_media_server")
|
||||||
|
secret = tmp_path / "relay"
|
||||||
|
secret.write_text("relay-value\n", encoding="utf-8")
|
||||||
|
assert module.read_relay_key(secret) == "relay-value"
|
||||||
|
|
||||||
|
|
||||||
|
def test_ariadne_receives_the_triage_key_directly_from_vault():
|
||||||
|
maintenance = ROOT / "services" / "maintenance" / "apps" / "ariadne-deployment.yaml"
|
||||||
|
deployment = yaml.safe_load(maintenance.read_text(encoding="utf-8"))
|
||||||
|
annotations = deployment["spec"]["template"]["metadata"]["annotations"]
|
||||||
|
template = annotations["vault.hashicorp.com/agent-inject-template-ariadne-env.sh"]
|
||||||
|
assert 'secret "kv/data/atlas/hermes/triage-api"' in template
|
||||||
|
assert 'export ARIADNE_HERMES_API_KEY="{{ .Data.data.api_key }}"' in template
|
||||||
|
assert 'secret "kv/data/atlas/hermes/developer-gitea"' in template
|
||||||
|
assert 'export ARIADNE_HERMES_GITEA_TOKEN="{{ .Data.data.token }}"' in template
|
||||||
|
container = deployment["spec"]["template"]["spec"]["containers"][0]
|
||||||
|
assert not any(item["name"] == "ARIADNE_HERMES_API_KEY" for item in container["env"])
|
||||||
|
assert not any(
|
||||||
|
item["name"] == "ARIADNE_HERMES_GITEA_TOKEN" for item in container["env"]
|
||||||
|
)
|
||||||
|
|
||||||
|
vault_policy = (
|
||||||
|
ROOT / "services" / "vault" / "scripts" / "vault_k8s_auth_configure.sh"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
assert '"hermes/triage-oidc hermes/agent-tokens hermes/triage-api"' in vault_policy
|
||||||
|
assert 'hermes-credential-sync' in vault_policy
|
||||||
|
assert 'hermes/triage-api hermes/developer-gitea' in vault_policy
|
||||||
|
|
||||||
|
|
||||||
|
def test_gitea_bootstrap_reuses_valid_vault_tokens():
|
||||||
|
bootstrap = (
|
||||||
|
ROOT / "services" / "gitea" / "scripts" / "gitea_atlas_identity_ensure.sh"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "vault_login\n\nreconciler_token=$(vault_read_field" in bootstrap
|
||||||
|
assert 'if ! token_is_valid "${reconciler_user}"' in bootstrap
|
||||||
|
assert 'if ! token_is_valid "${hermes_user}"' in bootstrap
|
||||||
|
assert bootstrap.count('generate_token "${reconciler_user}"') == 1
|
||||||
|
assert bootstrap.count('generate_token "${hermes_user}"') == 1
|
||||||
Loading…
x
Reference in New Issue
Block a user