feat(terminal): Kubernetes execution backend (run commands in isolated session pods)#1
Closed
Tombar wants to merge 1 commit into
Closed
feat(terminal): Kubernetes execution backend (run commands in isolated session pods)#1Tombar wants to merge 1 commit into
Tombar wants to merge 1 commit into
Conversation
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-attribute |
15 |
unresolved-import |
9 |
invalid-assignment |
2 |
invalid-argument-type |
1 |
First entries
tools/environments/kubernetes.py:202: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["activeDeadlineSeconds"]` and value of type `int` on object of type `dict[str, str | bool | dict[str, int] | ... omitted 3 union elements]`
tools/environments/kubernetes.py:313: [unresolved-import] unresolved-import: Cannot resolve imported module `kubernetes.stream`
tests/integration/test_kubernetes_terminal.py:12: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/tools/test_kubernetes_environment.py:19: [unresolved-attribute] unresolved-attribute: Unresolved attribute `config` on type `ModuleType`
tools/environments/kubernetes.py:325: [unresolved-attribute] unresolved-attribute: Attribute `namespace` is not defined on `None` in union `PodRef | None`
tools/environments/kubernetes.py:270: [unresolved-attribute] unresolved-attribute: Attribute `delete_namespaced_pod` is not defined on `None` in union `Unknown | None`
tests/tools/test_kubernetes_environment.py:18: [unresolved-attribute] unresolved-attribute: Unresolved attribute `client` on type `ModuleType`
tools/environments/kubernetes.py:224: [unresolved-attribute] unresolved-attribute: Attribute `create_namespaced_persistent_volume_claim` is not defined on `None` in union `Unknown | None`
tests/tools/test_kubernetes_environment.py:33: [unresolved-attribute] unresolved-attribute: Unresolved attribute `stream` on type `ModuleType`
tools/terminal_tool.py:1328: [unresolved-import] unresolved-import: Cannot resolve imported module `kubernetes`
tools/environments/kubernetes.py:230: [unresolved-attribute] unresolved-attribute: Attribute `create_namespaced_pod` is not defined on `None` in union `Unknown | None`
tests/tools/test_kubernetes_environment.py:49: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/tools/test_kubernetes_environment.py:32: [unresolved-attribute] unresolved-attribute: Unresolved attribute `load_incluster_config` on type `ModuleType`
tools/environments/kubernetes.py:324: [unresolved-attribute] unresolved-attribute: Attribute `pod_name` is not defined on `None` in union `PodRef | None`
tests/tools/test_kubernetes_environment.py:180: [unresolved-import] unresolved-import: Cannot resolve imported module `kubernetes.client.exceptions`
tools/environments/kubernetes.py:248: [unresolved-attribute] unresolved-attribute: Attribute `read_namespaced_pod` is not defined on `None` in union `Unknown | None`
tests/tools/test_kubernetes_environment.py:30: [unresolved-attribute] unresolved-attribute: Unresolved attribute `exceptions` on type `ModuleType`
tools/terminal_tool.py:1329: [unresolved-import] unresolved-import: Cannot resolve imported module `kubernetes.client`
tests/integration/test_kubernetes_terminal.py:42: [unresolved-import] unresolved-import: Cannot resolve imported module `kubernetes`
tools/environments/kubernetes.py:326: [unresolved-attribute] unresolved-attribute: Attribute `container` is not defined on `None` in union `PodRef | None`
tests/tools/test_kubernetes_environment.py:31: [unresolved-attribute] unresolved-attribute: Unresolved attribute `CoreV1Api` on type `ModuleType`
tools/environments/kubernetes.py:314: [unresolved-import] unresolved-import: Cannot resolve imported module `kubernetes.client`
tools/environments/kubernetes.py:150: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["ownerReferences"]` and value of type `list[dict[Unknown, Unknown]]` on object of type `dict[str, str | dict[str, str]]`
tools/environments/kubernetes.py:356: [invalid-argument-type] invalid-argument-type: Argument to bound method `WorkspaceProvisioner.destroy` is incorrect: Expected `PodRef`, found `PodRef | None`
tools/environments/kubernetes.py:218: [unresolved-attribute] unresolved-attribute: Attribute `read_namespaced_persistent_volume_claim` is not defined on `None` in union `Unknown | None`
... and 2 more
✅ Fixed issues: none
Unchanged: 4991 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
…d session pods)
Adds a `kubernetes` execution backend to the terminal / code-execution
tools — a peer of the existing `ssh` / `docker` / `modal` / `daytona`
backends. With `TERMINAL_ENV=kubernetes`, each agent shell command runs
by `exec`-ing into a dedicated session pod in the same cluster, instead
of in the agent's own process/container.
Motivation
----------
When Hermes runs inside Kubernetes, the `local` backend executes commands
in the agent's own container — sharing its filesystem, secrets, and
ServiceAccount token. The Kubernetes backend isolates execution into a
separate pod, so untrusted or heavy commands can't touch the agent's
home, memory, or credentials. It's the K8s-native counterpart to the
Docker/SSH sandboxes.
How it works
------------
New `tools/environments/kubernetes.py`:
- `KubernetesEnvironment(BaseEnvironment)` — runs each command via
`kubernetes.stream` exec into the session pod, wrapped in the existing
`_ThreadedProcessHandle` (same pattern as the Daytona/Modal backends).
Inherits `execute()`, the session snapshot, CWD tracking, and interrupt
handling unchanged.
- `WorkspaceProvisioner` interface + `DirectProvisioner` — create the
session Pod (and a PVC when persistent) via the in-cluster API
(`load_incluster_config()`, with a kubeconfig fallback for
out-of-cluster use).
- Locked-down pod shape: `runAsNonRoot`, non-root `runAsUser`/`fsGroup`,
drop ALL caps, `allowPrivilegeEscalation: false`, no host namespaces,
`automountServiceAccountToken: false`.
- Clean isolated workspace: the session pod starts empty at `/workspace`;
no host file sync — `write_file`/`patch` route through the exec channel.
Lifecycle
---------
- Ephemeral (default) — Pod with an `emptyDir`, deleted on cleanup, with
an `activeDeadlineSeconds` ceiling as a leak backstop.
- Persistent (opt-in, `TERMINAL_KUBERNETES_PERSISTENT=true`) — Pod mounts
a retained PVC `hermes-ws-<task_id>` at `/workspace`; on cleanup the pod
is deleted but the PVC is kept, so the next session resumes the
filesystem (mirrors Daytona's `persistent_filesystem` contract).
- Crash safety — the session pod carries an ownerReference to the agent's
own pod (via downward-API `HERMES_POD_NAME` / `HERMES_POD_UID`), so
Kubernetes garbage-collects it if the agent dies.
Configuration
-------------
TERMINAL_ENV=kubernetes
TERMINAL_KUBERNETES_NAMESPACE # default: in-cluster SA namespace
TERMINAL_KUBERNETES_IMAGE # session-pod image (default: ubuntu:22.04)
TERMINAL_KUBERNETES_POD_SA # no-perms SA for session pods
TERMINAL_KUBERNETES_PERSISTENT # default: false (ephemeral)
TERMINAL_KUBERNETES_ACTIVE_DEADLINE_SECONDS # default: 14400
TERMINAL_KUBERNETES_PULL_SECRETS
The `kubernetes` client ships as a named optional extra plus a
`LAZY_DEPS` entry (kept out of `[all]`, per the existing terminal-backend
policy), and is baked into the image via the Dockerfile.
Testing
-------
- Unit (mocked client): manifest shape / security context, ensure/destroy,
exec loop, factory wiring, requirements gate — 23 tests.
- Integration `tests/integration/test_kubernetes_terminal.py` (kind,
`@pytest.mark.integration`, env-gated): real exec roundtrip + PVC
persistence across pod recreation.
- CI `.github/workflows/test-k8s.yml`: spins up kind and runs the
integration tests.
- Validated end-to-end on a real cluster: real model → terminal tool →
session pod → output, including ownerReference garbage-collection.
Deployment manifests
--------------------
Cluster operators need two things for this backend to be safe: a scoped
RBAC grant for the agent SA, and an admission policy that constrains the
shape of session pods. Both ship in `k8s/`:
- `rbac.yaml` — namespace-scoped Role (`pods`, `pods/exec`, `pods/log`,
`persistentvolumeclaims`) + RoleBinding to the agent SA, plus a no-perms
session ServiceAccount (`automountServiceAccountToken: false`).
- `validatingadmissionpolicy.yaml` — CEL policy (Kubernetes >= 1.30) that
denies session pods running as the default SA, using host namespaces,
with `allowPrivilegeEscalation` true, without `drop: ALL`, or mounting
`hostPath`. Scoped via `matchConditions` to pod-creates by a
ServiceAccount carrying `app.kubernetes.io/managed-by: hermes-agent`.
- `README.md` — apply/verify steps and hardening notes.
Repo docs
---------
- `.env.example` — new `KUBERNETES BACKEND` section with the full env-var
surface.
- `cli-config.yaml.example` — new `OPTION 7: Kubernetes session pod`.
- `README.md` — terminal-backend count bumped with a link to `k8s/`.
Tombar
force-pushed
the
feat/k8s-exec-backend
branch
from
June 2, 2026 05:50
dbacc06 to
7381106
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
kubernetesexecution backend to the terminal / code-execution tools — a peer of the existingssh/docker/modal/daytonabackends. WithTERMINAL_ENV=kubernetes, each agent shell command runs byexec-ing into a dedicated session pod in the same cluster, instead of in the agent's own process/container.Motivation
When Hermes runs inside Kubernetes, the
localbackend executes commands in the agent's own container — sharing its filesystem, secrets, and ServiceAccount token. The Kubernetes backend isolates execution into a separate pod, so untrusted or heavy commands can't touch the agent's home, memory, or credentials. It's the K8s-native counterpart to the Docker/SSH sandboxes.Context
We built this backend to pair with our companion Kubernetes controller, hermes-operator, which manages Hermes deployments as custom resources and can reconcile the RBAC + admission policy automatically. But it's intentionally operator-agnostic — namespace, ServiceAccount, image, persistence, deadline, and image-pull-secrets are all config/env-driven, and the
k8s/samples run it standalone with no operator involved. The operator is an optional convenience, not a dependency.How it works
New
tools/environments/kubernetes.py:KubernetesEnvironment(BaseEnvironment)— runs each command viakubernetes.streamexec into the session pod, wrapped in the existing_ThreadedProcessHandle(same pattern as the Daytona/Modal backends). Inheritsexecute(), the session snapshot, CWD tracking, and interrupt handling unchanged.WorkspaceProvisionerinterface +DirectProvisioner— create the session Pod (and a PVC when persistent) via the in-cluster API (load_incluster_config(), with a kubeconfig fallback for out-of-cluster use).runAsNonRoot, non-rootrunAsUser/fsGroup, drop ALL caps,allowPrivilegeEscalation: false, no host namespaces,automountServiceAccountToken: false./workspace; no host file sync —write_file/patchroute through the exec channel.Lifecycle
emptyDir, deleted on cleanup, with anactiveDeadlineSecondsceiling as a leak backstop.TERMINAL_KUBERNETES_PERSISTENT=true) — Pod mounts a retained PVChermes-ws-<task_id>at/workspace; on cleanup the pod is deleted but the PVC is kept, so the next session resumes the filesystem (mirrors Daytona'spersistent_filesystemcontract).HERMES_POD_NAME/HERMES_POD_UID), so Kubernetes garbage-collects it if the agent dies.Configuration
The
kubernetesclient ships as a named optional extra plus aLAZY_DEPSentry (kept out of[all], per the existing terminal-backend policy), and is baked into the image via the Dockerfile.Testing
23 passedon the rebased branch).tests/integration/test_kubernetes_terminal.py(kind,@pytest.mark.integration, env-gated): real exec roundtrip + PVC persistence across pod recreation..github/workflows/test-k8s.yml: spins up kind and runs the integration tests.Deployment manifests
Cluster operators need two things for this backend to be safe: a scoped RBAC grant for the agent SA, and an admission policy that constrains the shape of session pods (so a granted
pods/createcannot mean "create a privileged pod"). Both ship ink8s/:rbac.yaml— namespace-scopedRole(pods,pods/exec,pods/log,persistentvolumeclaims) +RoleBindingto the agent SA, plus a no-perms sessionServiceAccount(automountServiceAccountToken: false).validatingadmissionpolicy.yaml— CEL policy (Kubernetes ≥ 1.30) that denies session pods running as the default SA, using host namespaces, withallowPrivilegeEscalationtrue, withoutdrop: ALL, or mountinghostPath. Scoped viamatchConditionsto pod-creates by a ServiceAccount carrying the backend'sapp.kubernetes.io/managed-by: hermes-agentlabel.README.md— apply/verify steps, hardening notes, and a pointer to an operator-managed alternative.Repo docs
.env.example— newKUBERNETES BACKENDsection with the full env-var surface.cli-config.yaml.example— newOPTION 7: Kubernetes session pod.README.md— terminal-backend count bumped to "Seven" with a link tok8s/.🤖 Generated with Claude Code