hermes: add audited Atlas operator access

This commit is contained in:
jenkins 2026-08-15 13:22:01 -03:00
parent ce8c25ff32
commit 2ae88973be
7 changed files with 220 additions and 1 deletions

View File

@ -274,6 +274,36 @@ data:
recovery, and verification, and must be followed by a matching source-of-
truth change when they alter desired state. Never expose credentials in
chat or logs. Triage belongs at triage.hermes.bstein.dev.
## Atlas engineering access
Atlas repositories are private and canonical at
`https://scm.bstein.dev/atlas/<repo>.git`. HTTPS Git authentication is
already supplied through `GIT_ASKPASS`. Verify the remote and cleanly
separate pre-existing changes, create a task branch, run the repository's
tests, use `git push --dry-run` when proving access, and push a real branch
only when the requested implementation is review-ready. Never force-push.
The terminal PATH contains the pinned operator tools. Start cluster work
with `kubectl config current-context`, read-only status/events/logs, and the
relevant `titan-iac` manifests. Put durable desired-state changes on a
reviewable `titan-iac` branch, validate Kustomize and client dry-run, then
use Flux reconciliation after the tracked change is published. Direct
`kubectl` mutations are for explicit incident recovery or ephemeral
verification, not ordinary delivery.
Node SSH uses a dedicated audited Hermes identity: `ssh titan-04` (or any
current Kubernetes node name). Host keys are pinned and password fallback
is disabled. Use SSH only for host-level evidence or repairs that cannot be
performed through Kubernetes. Read first, identify the exact node and
impact, avoid fleet-wide destructive commands, and reflect persistent host
configuration in the appropriate tracked provisioning source.
After a failed tool attempt, failing test, contradicted claim, rejected
review, or incomplete evidence, do not repeat the same low-capability plan.
Reassess the objective and raise provider capability or effort by at least
one tier, up to xhigh. Consequential independent final review is always
xhigh and must be separated from the implementing worker.
START-HERE.md: |
# Agent Hermes

View File

@ -43,6 +43,11 @@ spec:
{{- with secret "kv/data/atlas/hermes/developer-gitea" -}}
{{ .Data.data.username }}
{{- end }}
vault.hashicorp.com/agent-inject-secret-node-ssh-private-key: kv/data/atlas/hermes/developer-ssh
vault.hashicorp.com/agent-inject-template-node-ssh-private-key: |
{{- with secret "kv/data/atlas/hermes/developer-ssh" -}}
{{ .Data.data.private_key }}
{{- end }}
vault.hashicorp.com/agent-inject-secret-chat-relay-key: kv/data/atlas/hermes/chat-telegram
vault.hashicorp.com/agent-inject-template-chat-relay-key: |
{{- with secret "kv/data/atlas/hermes/chat-telegram" -}}
@ -104,6 +109,7 @@ spec:
/opt/data/home/.claude \
/opt/data/home/.codex \
/opt/data/home/.kube \
/opt/data/home/.ssh \
/opt/data/cli-lanes \
/opt/data/logs \
/opt/data/provider-health \
@ -118,6 +124,15 @@ spec:
cp /config/SOUL.md /opt/data/SOUL.md
cp /config/AGENTS.md /opt/data/workspace/AGENTS.md
cp /config/START-HERE.md /opt/data/workspace/START-HERE.md
cp /config/ssh_config /opt/data/home/.ssh/config
cp /config/ssh_known_hosts /opt/data/home/.ssh/known_hosts
if [ -s /vault/secrets/node-ssh-private-key ]; then
cp /vault/secrets/node-ssh-private-key /opt/data/home/.ssh/id_ed25519_atlas_nodes
chmod 0600 /opt/data/home/.ssh/id_ed25519_atlas_nodes
fi
chmod 0600 \
/opt/data/home/.ssh/config \
/opt/data/home/.ssh/known_hosts
touch "${env_file}"
upsert_env() {
key="$1"
@ -158,6 +173,9 @@ spec:
/opt/data/home/.claude \
/opt/data/home/.codex \
/opt/data/home/.kube \
/opt/data/home/.ssh \
/opt/data/home/.ssh/config \
/opt/data/home/.ssh/known_hosts \
/opt/data/cli-lanes \
/opt/data/logs \
/opt/data/provider-health \
@ -172,6 +190,9 @@ spec:
/opt/data/workspace/AGENTS.md \
/opt/data/workspace/START-HERE.md \
"${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:
allowPrivilegeEscalation: false
runAsUser: 0

View File

@ -14,6 +14,7 @@ resources:
- switchyard-configmap.yaml
- rbac.yaml
- agent-rbac.yaml
- node-ssh-access.yaml
- pvc.yaml
- switchyard-pvc.yaml
- routing-catalog-pvc.yaml

View File

@ -0,0 +1,89 @@
# services/hermes/node-ssh-access.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: hermes-node-ssh-access
namespace: hermes
labels:
app: hermes-node-ssh-access
data:
authorized_key: >-
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: hermes-node-ssh-access
namespace: hermes
labels:
app: hermes-node-ssh-access
spec:
selector:
matchLabels:
app: hermes-node-ssh-access
template:
metadata:
labels:
app: hermes-node-ssh-access
spec:
automountServiceAccountToken: false
tolerations:
- operator: Exists
containers:
- name: key-reconciler
image: busybox:1.37
imagePullPolicy: IfNotPresent
command: [/bin/sh, -ec]
args:
- |
reconcile() {
key="$(cat /config/authorized_key)"
found=0
for user in atlas oceanus; do
home="/host-home/${user}"
[ -d "${home}" ] || continue
found=1
install -d -m 0700 -o "$(stat -c %u "${home}")" -g "$(stat -c %g "${home}")" "${home}/.ssh"
touch "${home}/.ssh/authorized_keys"
grep -qxF "${key}" "${home}/.ssh/authorized_keys" || printf '%s\n' "${key}" >> "${home}/.ssh/authorized_keys"
chown "$(stat -c %u "${home}"):$(stat -c %g "${home}")" "${home}/.ssh/authorized_keys"
chmod 0600 "${home}/.ssh/authorized_keys"
done
[ "${found}" = 1 ] || { echo "no supported node SSH account found" >&2; return 1; }
}
while true; do
reconcile
sleep 300
done
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsUser: 0
runAsGroup: 0
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: access-config
mountPath: /config
readOnly: true
- name: host-home
mountPath: /host-home
- name: tmp
mountPath: /tmp
resources:
requests:
cpu: 5m
memory: 8Mi
limits:
cpu: 50m
memory: 32Mi
volumes:
- name: access-config
configMap:
name: hermes-node-ssh-access
- name: host-home
hostPath:
path: /home
type: Directory
- name: tmp
emptyDir:
sizeLimit: 8Mi

View File

@ -376,6 +376,11 @@ data:
medium even when the user's wording is short. Filesystem, shell,
repository, cluster, browser, and image-generation/edit operations are
tool work; never route those boundaries to Luna, Haiku, or local-low.
A failing test, failed tool plan, contradicted result, rejected review, or
incomplete evidence means the previous quality mark was missed. Raise the
next boundary by at least one effort tier and prefer a stronger family or
the other hosted provider; repeated misses require xhigh. Never repeat a
failed lower-capability plan unchanged.
2. Treat "think hard", "deeply", "carefully", and equivalent intent as a
request to raise capability by at least one tier when the safety floor is
@ -450,6 +455,11 @@ data:
medium even when the user's wording is short. Filesystem, shell,
repository, cluster, browser, and image-generation/edit operations are
tool work; never route those boundaries to Luna, Haiku, or local-low.
A failing test, failed tool plan, contradicted result, rejected review, or
incomplete evidence means the previous quality mark was missed. Raise the
next boundary by at least one effort tier and prefer a stronger family or
the other hosted provider; repeated misses require xhigh. Never repeat a
failed lower-capability plan unchanged.
2. Treat "think hard", "deeply", "carefully", and equivalent intent as a
request to raise capability by at least one tier when the safety floor is
@ -524,6 +534,11 @@ data:
medium even when the user's wording is short. Filesystem, shell,
repository, cluster, browser, and image-generation/edit operations are
tool work; never route those boundaries to Luna or Haiku.
A failing test, failed tool plan, contradicted result, rejected review, or
incomplete evidence means the previous quality mark was missed. Raise the
next boundary by at least one effort tier and prefer a stronger family or
the other hosted provider; repeated misses require xhigh. Never repeat a
failed lower-capability plan unchanged.
2. Treat "think hard", "deeply", "carefully", and equivalent intent as a
request to raise capability by at least one tier when the safety floor is
@ -583,6 +598,10 @@ data:
Those xhigh triggers are mandatory: never answer one with a high target.
Medium and low targets are intentionally unavailable on this route.
Never choose below the floor or above xhigh.
A failing test, failed tool plan, contradicted result, rejected review, or
incomplete evidence raises the next boundary to xhigh and should switch to
the strongest suitable family or other hosted provider. Never repeat a
failed high-capability plan unchanged.
2. Treat "think hard", "deeply", "carefully", and equivalent intent as a
request to raise capability by at least one tier when the safety floor is
@ -643,6 +662,11 @@ data:
- low: only mechanical, reversible, tightly bounded work such as a typo,
formatting, or a simple lookup.
Never choose below the floor and never exceed xhigh.
If the objective records a failing test, failed attempt, contradicted
result, rejected review, or incomplete evidence from a previous worker,
raise effort by at least one tier and prefer a stronger family or the other
hosted provider. Repeated quality misses require xhigh; never launch the
same failed lower-capability plan unchanged.
2. Determine provider availability before provider preference:
- Anthropic and Claude name the same provider. If either is failed,

View File

@ -462,7 +462,8 @@ def test_chat_image_generation_uses_private_owner_broker():
assert (
'"hermes/agent-oidc hermes/agent-tokens hermes/chat-telegram '
'hermes/developer-keycloak hermes/developer-gitea '
'hermes/developer-harbor hermes/developer-jenkins"'
'hermes/developer-harbor hermes/developer-jenkins '
'hermes/developer-ssh"'
in vault_policy
)
@ -1488,6 +1489,11 @@ def test_local_flux_runtime_and_gpu_handoff_are_flux_managed():
assert not any(target.endswith("_medium") for target in maximum_targets)
assert "_medium" not in maximum_selector_targets
assert "absolute high effort floor" in routes["auto_maximum"]["prompt"]
assert "quality mark was missed" in routes["auto_balanced"]["prompt"]
assert "raises the next boundary to xhigh" in routes["auto_maximum"]["prompt"]
assert "Repeated quality misses require xhigh" in routes[
"worker_auto_maximum"
]["prompt"]
assert any(
target.startswith("local_")
for target in routes["manual_local_qwen"]["targets"]

View File

@ -913,6 +913,54 @@ def test_owner_agent_has_cluster_admin_kubernetes_context():
]
def test_owner_agent_has_pinned_dedicated_node_ssh_access():
deployment = _agent_deployment()
annotations = deployment["spec"]["template"]["metadata"]["annotations"]
assert annotations[
"vault.hashicorp.com/agent-inject-secret-node-ssh-private-key"
] == "kv/data/atlas/hermes/developer-ssh"
init = next(
item
for item in deployment["spec"]["template"]["spec"]["initContainers"]
if item["name"] == "init-config"
)
command = init["command"][2]
assert "cp /config/ssh_config /opt/data/home/.ssh/config" in command
assert "chmod 0600 /opt/data/home/.ssh/id_ed25519_atlas_nodes" in command
config = yaml.safe_load((HERMES / "agent-configmap.yaml").read_text())["data"]
assert "IdentityFile /opt/data/home/.ssh/id_ed25519_atlas_nodes" in config[
"ssh_config"
]
assert "StrictHostKeyChecking yes" in config["ssh_config"]
assert "PasswordAuthentication no" in config["ssh_config"]
pinned_hosts = [
line for line in config["ssh_known_hosts"].splitlines() if line.strip()
]
assert len(pinned_hosts) == 21
assert all(" ssh-ed25519 " in line for line in pinned_hosts)
assert any(line.startswith("[titan-23]:2277 ") for line in pinned_hosts)
resources = yaml.safe_load((HERMES / "kustomization.yaml").read_text())[
"resources"
]
assert "node-ssh-access.yaml" in resources
access = [
item
for item in yaml.safe_load_all((HERMES / "node-ssh-access.yaml").read_text())
if item
]
daemonset = next(item for item in access if item["kind"] == "DaemonSet")
pod = daemonset["spec"]["template"]["spec"]
assert pod["automountServiceAccountToken"] is False
host_home = next(item for item in pod["volumes"] if item["name"] == "host-home")
assert host_home["hostPath"] == {"path": "/home", "type": "Directory"}
reconciler = pod["containers"][0]["args"][0]
assert "grep -qxF" in reconciler
assert "for user in atlas oceanus" in reconciler
def test_switchyard_has_a_dedicated_non_owner_identity_and_read_only_catalog():
"""Routing must not inherit the owner agent's cluster-admin capability."""
service_accounts = [