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-
|
recovery, and verification, and must be followed by a matching source-of-
|
||||||
truth change when they alter desired state. Never expose credentials in
|
truth change when they alter desired state. Never expose credentials in
|
||||||
chat or logs. Triage belongs at triage.hermes.bstein.dev.
|
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: |
|
START-HERE.md: |
|
||||||
# Agent Hermes
|
# Agent Hermes
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,11 @@ spec:
|
|||||||
{{- with secret "kv/data/atlas/hermes/developer-gitea" -}}
|
{{- with secret "kv/data/atlas/hermes/developer-gitea" -}}
|
||||||
{{ .Data.data.username }}
|
{{ .Data.data.username }}
|
||||||
{{- end }}
|
{{- 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-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" -}}
|
||||||
@ -104,6 +109,7 @@ spec:
|
|||||||
/opt/data/home/.claude \
|
/opt/data/home/.claude \
|
||||||
/opt/data/home/.codex \
|
/opt/data/home/.codex \
|
||||||
/opt/data/home/.kube \
|
/opt/data/home/.kube \
|
||||||
|
/opt/data/home/.ssh \
|
||||||
/opt/data/cli-lanes \
|
/opt/data/cli-lanes \
|
||||||
/opt/data/logs \
|
/opt/data/logs \
|
||||||
/opt/data/provider-health \
|
/opt/data/provider-health \
|
||||||
@ -118,6 +124,15 @@ 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 /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}"
|
touch "${env_file}"
|
||||||
upsert_env() {
|
upsert_env() {
|
||||||
key="$1"
|
key="$1"
|
||||||
@ -158,6 +173,9 @@ spec:
|
|||||||
/opt/data/home/.claude \
|
/opt/data/home/.claude \
|
||||||
/opt/data/home/.codex \
|
/opt/data/home/.codex \
|
||||||
/opt/data/home/.kube \
|
/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/cli-lanes \
|
||||||
/opt/data/logs \
|
/opt/data/logs \
|
||||||
/opt/data/provider-health \
|
/opt/data/provider-health \
|
||||||
@ -172,6 +190,9 @@ 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
|
||||||
|
|||||||
@ -14,6 +14,7 @@ resources:
|
|||||||
- switchyard-configmap.yaml
|
- switchyard-configmap.yaml
|
||||||
- rbac.yaml
|
- rbac.yaml
|
||||||
- agent-rbac.yaml
|
- agent-rbac.yaml
|
||||||
|
- node-ssh-access.yaml
|
||||||
- pvc.yaml
|
- pvc.yaml
|
||||||
- switchyard-pvc.yaml
|
- switchyard-pvc.yaml
|
||||||
- routing-catalog-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,
|
medium even when the user's wording is short. Filesystem, shell,
|
||||||
repository, cluster, browser, and image-generation/edit operations are
|
repository, cluster, browser, and image-generation/edit operations are
|
||||||
tool work; never route those boundaries to Luna, Haiku, or local-low.
|
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
|
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
|
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,
|
medium even when the user's wording is short. Filesystem, shell,
|
||||||
repository, cluster, browser, and image-generation/edit operations are
|
repository, cluster, browser, and image-generation/edit operations are
|
||||||
tool work; never route those boundaries to Luna, Haiku, or local-low.
|
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
|
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
|
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,
|
medium even when the user's wording is short. Filesystem, shell,
|
||||||
repository, cluster, browser, and image-generation/edit operations are
|
repository, cluster, browser, and image-generation/edit operations are
|
||||||
tool work; never route those boundaries to Luna or Haiku.
|
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
|
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
|
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.
|
Those xhigh triggers are mandatory: never answer one with a high target.
|
||||||
Medium and low targets are intentionally unavailable on this route.
|
Medium and low targets are intentionally unavailable on this route.
|
||||||
Never choose below the floor or above xhigh.
|
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
|
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
|
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,
|
- low: only mechanical, reversible, tightly bounded work such as a typo,
|
||||||
formatting, or a simple lookup.
|
formatting, or a simple lookup.
|
||||||
Never choose below the floor and never exceed xhigh.
|
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:
|
2. Determine provider availability before provider preference:
|
||||||
- Anthropic and Claude name the same provider. If either is failed,
|
- 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 (
|
assert (
|
||||||
'"hermes/agent-oidc hermes/agent-tokens hermes/chat-telegram '
|
'"hermes/agent-oidc hermes/agent-tokens hermes/chat-telegram '
|
||||||
'hermes/developer-keycloak hermes/developer-gitea '
|
'hermes/developer-keycloak hermes/developer-gitea '
|
||||||
'hermes/developer-harbor hermes/developer-jenkins"'
|
'hermes/developer-harbor hermes/developer-jenkins '
|
||||||
|
'hermes/developer-ssh"'
|
||||||
in vault_policy
|
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 not any(target.endswith("_medium") for target in maximum_targets)
|
||||||
assert "_medium" not in maximum_selector_targets
|
assert "_medium" not in maximum_selector_targets
|
||||||
assert "absolute high effort floor" in routes["auto_maximum"]["prompt"]
|
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(
|
assert any(
|
||||||
target.startswith("local_")
|
target.startswith("local_")
|
||||||
for target in routes["manual_local_qwen"]["targets"]
|
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():
|
def test_switchyard_has_a_dedicated_non_owner_identity_and_read_only_catalog():
|
||||||
"""Routing must not inherit the owner agent's cluster-admin capability."""
|
"""Routing must not inherit the owner agent's cluster-admin capability."""
|
||||||
service_accounts = [
|
service_accounts = [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user