hermes: add audited Atlas operator access
This commit is contained in:
parent
ce8c25ff32
commit
2ae88973be
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
89
services/hermes/node-ssh-access.yaml
Normal file
89
services/hermes/node-ssh-access.yaml
Normal 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
|
||||
@ -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,
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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 = [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user