Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,7 @@ jobs:
- 'scripts/tests/test-coroot-application-health-reconciler.sh'
- 'scripts/tests/test-coroot-helper-image-pull-policy.sh'
- 'scripts/tests/test-agentic-engineer-schedule.sh'
- 'scripts/tests/test-agentic-engineer-lane-store.sh'
- 'scripts/tests/test-kagent-claude-harness-wrapper.sh'
- 'scripts/kagent-claude-harness/**'
- 'scripts/tests/test-anthropic-proxy-envoy-config.sh'
Expand Down Expand Up @@ -2858,6 +2859,15 @@ jobs:
shellcheck scripts/tests/test-agentic-engineer-schedule.sh
bash scripts/tests/test-agentic-engineer-schedule.sh

- name: 🗄️ Validate the Agentic Engineer lane store
if: needs.changes.outputs.k8s == 'true'
# Renders the lane and Substrate: the agent reaches the store only through
# the egress gateway with the store credential, and the maintainer reads it
# only through the API server.
run: |
shellcheck scripts/tests/test-agentic-engineer-lane-store.sh
bash scripts/tests/test-agentic-engineer-lane-store.sh

- name: 🧠 Validate the kagent harness plugin wrapper
if: needs.changes.outputs.k8s == 'true'
# Runs the image's real `claude` wrapper against a stub Claude Code: a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# actor's own egress policy (the destinations its kagent EgressPolicies and
# model grant) at layer 7, so this rule does not repeat that allow-list: it
# lets the gateway reach the internet over HTTP(S), the kagent controller
# (agents' task store calls) and the in-cluster credential services.
# (agents' task store calls), the in-cluster credential services and the
# Agentic Engineer lane's store.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
Expand All @@ -26,3 +27,11 @@ spec:
k8s:io.kubernetes.pod.namespace: kagent
- matchLabels:
k8s:io.kubernetes.pod.namespace: kagent-credentials
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: agentic-engineer
app.kubernetes.io/name: lane-store
toPorts:
- ports:
- port: "8080"
protocol: TCP
16 changes: 11 additions & 5 deletions k8s/providers/hetzner/apps/agentic-engineer/agent-template.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Who the agent is and the rules every run keeps. What a run does comes from
# the message the trigger sends (cron-job.yaml). The engineering contract
# itself is devantler-tech/monorepo's AGENTS.md, which the agent reads from its
# own checkout, so it never drifts from the reviewed source.
# the prompt of its kagent scheduled run (job-schedule.yaml). The engineering
# contract itself is devantler-tech/monorepo's AGENTS.md, which the agent reads
# from its own checkout, so it never drifts from the reviewed source.
apiVersion: api.kagent.dev/v1alpha3
kind: AgentTemplate
metadata:
Expand All @@ -17,12 +17,18 @@ spec:
devantler-tech-engineering[bot] and own only `kagent/*` branches.

Credentials never reach you. Substrate's egress gateway replaces a
placeholder Authorization header with the App's token:
placeholder Authorization header with the real credential:
- GitHub API: `gh` works as is. With curl or wget, send
`Authorization: Bearer kagent-credential-injected`.
- Git over HTTPS: run git with
`-c http.extraHeader='Authorization: Basic kagent-credential-injected'`.
Only GitHub and the model are reachable.
- The lane store, your memory between runs and the place each run leaves
its transcript, is a WebDAV server at
http://lane-store.agentic-engineer.svc.cluster.local:8080. Send
`Authorization: Basic kagent-credential-injected`, for example
`curl -fsS -H 'Authorization: Basic kagent-credential-injected' <url>` to
read a file and `curl -fsS -T <file> -H '...' <url>` to write one.
Only GitHub, the model and the lane store are reachable.

Rules for every run:
- Do only what this run's message asks, then stop.
Expand Down
1 change: 1 addition & 0 deletions k8s/providers/hetzner/apps/agentic-engineer/agent.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ spec:
name: claude
egressPolicyRefs:
- name: github
- name: lane-store
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Worker pods talk only to Substrate (its egress gateway carries the agent's
# traffic; its router and API server reach the workers). The schedule Job is
# excluded: its only path, to kagent's API, is
# cilium-network-policy-schedule.yaml.
# traffic; its router and API server reach the workers). The schedule Job and
# the lane store are excluded: their paths are cilium-network-policy-schedule.yaml
# and cilium-network-policy-lane-store.yaml.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
Expand All @@ -13,6 +13,9 @@ spec:
- key: app
operator: NotIn
values: [agentic-engineer-schedule]
- key: app.kubernetes.io/name
operator: NotIn
values: [lane-store]
ingress:
- fromEndpoints:
- matchLabels:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# The lane store takes the agent's traffic only from Substrate's egress gateway,
# which adds the store credential, and its read-only view only from the
# Kubernetes API server, which checks the reader's services/proxy grant. The
# agent's sandbox has no direct path to either, and the store reaches nothing.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: allow-lane-store
namespace: agentic-engineer
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: lane-store
# Deny everything not allowed below, including all egress: the store calls
# nothing.
enableDefaultDeny:
ingress: true
egress: true
ingress:
- fromEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: ate-system
app: atenet-egress
toPorts:
- ports:
- port: "8080"
protocol: TCP
# The API server runs on the control-plane nodes' host network, so its
# proxied requests can arrive as the node, as for the platform's webhooks.
# Only host-network system pods share that identity.
- fromEntities:
- kube-apiserver
- remote-node
- host
toPorts:
- ports:
- port: "8081"
protocol: TCP
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# The lane store (devantler-tech/monorepo#4869, #4870). A run starts in a fresh
# sandbox, so the lane keeps its memory here between runs, and each run leaves
# its transcript here for the Agent Improver. Two rclone listeners share one
# volume:
#
# - webdav (8080): read and write, behind the lane-store credential. Only
# Substrate's egress gateway reaches it, sending that credential in place of
# the agent's placeholder (egress-policy-lane-store.yaml).
# - reader (8081): read only, with no credential of its own. Only the
# Kubernetes API server reaches it, so a reader needs the
# services/proxy grant in role-lane-store-reader.yaml.
#
# One replica on a ReadWriteOnce volume: the lane runs once a day.
apiVersion: apps/v1
kind: Deployment
metadata:
name: lane-store
namespace: agentic-engineer
labels:
app.kubernetes.io/name: lane-store
platform.devantler.tech/replica-floor: exempt
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: lane-store
template:
metadata:
labels:
app.kubernetes.io/name: lane-store
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: webdav
image: docker.io/rclone/rclone:1.75.2@sha256:2687085f718d3c628f7fdfb77c52a1d344332aed543110bb88088f2c70d43eb5
imagePullPolicy: IfNotPresent
args:
- serve
- webdav
- /data
- --addr=:8080
- --htpasswd=/etc/lane-store/htpasswd
- --config=/tmp/rclone.conf
- --cache-dir=/tmp/cache
ports:
- name: webdav
containerPort: 8080
protocol: TCP
readinessProbe:
tcpSocket:
port: webdav
periodSeconds: 10
livenessProbe:
tcpSocket:
port: webdav
periodSeconds: 30
resources:
requests:
cpu: 5m
memory: 32Mi
limits:
cpu: 200m
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
volumeMounts:
- name: data
mountPath: /data
- name: credential
mountPath: /etc/lane-store
readOnly: true
- name: tmp
mountPath: /tmp
- name: reader
image: docker.io/rclone/rclone:1.75.2@sha256:2687085f718d3c628f7fdfb77c52a1d344332aed543110bb88088f2c70d43eb5
imagePullPolicy: IfNotPresent
args:
- serve
- http
- /data
- --addr=:8081
- --read-only
- --config=/tmp/rclone.conf
- --cache-dir=/tmp/cache
ports:
- name: reader
containerPort: 8081
protocol: TCP
readinessProbe:
httpGet:
path: /
port: reader
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: reader
periodSeconds: 30
resources:
requests:
cpu: 5m
memory: 32Mi
limits:
cpu: 200m
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
volumeMounts:
- name: data
mountPath: /data
readOnly: true
- name: tmp
mountPath: /tmp
volumes:
- name: data
persistentVolumeClaim:
claimName: lane-store
- name: credential
secret:
secretName: lane-store
items:
- key: htpasswd
path: htpasswd
- name: tmp
emptyDir:
sizeLimit: 64Mi
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# The lane store for the agent (deployment-lane-store.yaml). The gateway
# replaces the Authorization header with the store credential, so the agent
# sends only `Authorization: Basic kagent-credential-injected`.
apiVersion: api.kagent.dev/v1alpha3
kind: EgressPolicy
metadata:
name: lane-store
namespace: agentic-engineer
spec:
destinations:
- origin: http://lane-store.agentic-engineer.svc.cluster.local:8080
authentication:
header: Authorization
prefix: "Basic "
secretKeyRef:
name: lane-store
key: basic
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# The lane store's one credential, generated once in-cluster. The store checks
# it from the htpasswd key, and Substrate's egress gateway sends it from the
# basic key, so the agent only ever sends a placeholder
# (egress-policy-lane-store.yaml).
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: lane-store
namespace: agentic-engineer
spec:
refreshInterval: "0"
target:
name: lane-store
creationPolicy: Owner
template:
engineVersion: v2
data:
htpasswd: '{{ htpasswd "agentic-engineer" .password "bcrypt" }}'
basic: '{{ printf "agentic-engineer:%s" .password | b64enc }}'
dataFrom:
- sourceRef:
generatorRef:
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
name: lane-store
14 changes: 12 additions & 2 deletions k8s/providers/hetzner/apps/agentic-engineer/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# The Agentic Engineer's kagent lane (devantler-tech/monorepo#4866): the agent,
# its Substrate workers, its GitHub and Anthropic credentials, and the daily
# schedule. Prod-only, on the kagent and Substrate infrastructure.
# its Substrate workers, its GitHub and Anthropic credentials, its store and
# the daily schedule. Prod-only, on the kagent and Substrate infrastructure.
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
Expand All @@ -18,14 +18,24 @@ resources:
- external-secret-github-token.yaml
- password-model-placeholder.yaml
- external-secret-model-placeholder.yaml
- password-lane-store.yaml
- external-secret-lane-store.yaml
- persistent-volume-claim-lane-store.yaml
- deployment-lane-store.yaml
- service-lane-store.yaml
- service-lane-store-reader.yaml
- role-lane-store-reader.yaml
- role-binding-lane-store-reader.yaml
- worker-pool.yaml
- model-config.yaml
- harness.yaml
- egress-policy-github.yaml
- egress-policy-lane-store.yaml
- agent-template.yaml
- agent.yaml
- job-schedule.yaml
- cilium-network-policy-agents.yaml
- cilium-network-policy-credentials.yaml
- cilium-network-policy-schedule.yaml
- cilium-network-policy-lane-store.yaml
- cilium-network-policy-external-secrets.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
metadata:
name: lane-store
namespace: agentic-engineer
spec:
length: 32
digits: 8
symbols: 0
noUpper: false
allowRepeat: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# The lane store's files: the lane's memory between runs and each run's
# transcript. Kept if the store is ever removed from Git, so a mistaken
# removal cannot delete them.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: lane-store
namespace: agentic-engineer
annotations:
kustomize.toolkit.fluxcd.io/prune: disabled
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
Loading
Loading