Agent Sandbox - Kubernetes-native sandboxes για εκτέλεση κώδικα από AI agents
~/posts/agent-sandbox.el.md21 min · 4433 words

Agent Sandbox - Kubernetes-native sandboxes για εκτέλεση κώδικα από AI agents

// Μια hands-on περιήγηση στο agent-sandbox, το project του Kubernetes SIG Apps για isolated, stateful, singleton workloads, φτιαγμένο για τα runtimes των AI agents. Με έναν Go agent με sandboxed tools σε Kind, warm pools που κάνουν claim σε 53ms, Flux GitOps, και τις παγίδες που δεν αναφέρει το documentation.

$ date

Πριν από μερικούς μήνες έφτιαξα έναν Kubernetes agent με το ADK της Google για Go. Εκείνος ο agent μιλούσε με το cluster: έτρεχε kubectl πάνω στο kind cluster μου και εξηγούσε τι έβλεπε. Αυτό το post πιάνει το ανάποδο πρόβλημα, που είναι και αυτό που πραγματικά δεν με αφήνει να κοιμηθώ: όταν ένα LLM γράφει κώδικα, πού τρέχει αυτός ο κώδικας;

Για τα περισσότερα agent stacks σήμερα, η ειλικρινής απάντηση είναι «σε όποιο μηχάνημα έτυχε να τρέχει το process του agent». Το LLM παράγει ένα shell command ή ένα Python script, το framework το κάνει exec, και όλοι ελπίζουν ότι το μοντέλο δεν θα βγάλει ποτέ ένα rm -rf ή κάτι που θα κάνει exfiltration τα credentials που κάθονται στο environment. Τα hosted sandboxes όπως το E2B και το Modal υπάρχουν ακριβώς γι’ αυτό. Αν όμως τρέχεις ήδη Kubernetes, το να στέλνεις το code execution των agents σου σε ένα third-party SaaS μοιάζει ανάποδο. Έχεις scheduler, έχεις network policy, έχεις runtimes με kernel isolation. Αυτό που σου έλειπε ήταν το workload abstraction.

Αυτό το κενό καλύπτει το agent-sandbox. Είναι ένα project του Kubernetes SIG Apps που φέρνει ένα Sandbox CRD και έναν controller για isolated, stateful, singleton workloads (το σχήμα «lightweight single-container VM», που ούτε τα Deployments ούτε τα StatefulSets το μοντελοποιούν καλά), μαζί με warm pools, ώστε ένας agent να παίρνει sandbox σε milliseconds αντί για δεκάδες δευτερόλεπτα. Χθες έφτασε στο v0.5.6, έχει SDK και για Go και για Python, και τρέχει σε ένα σκέτο kind cluster. Έφτιαξα έναν Go agent που κάθε tool call του εκτελείται μέσα σε ένα sandbox pod, τα έδεσα όλα με Flux, και στον δρόμο σκόνταψα σε αρκετές κοφτερές γωνίες ώστε να αξίζει τον χρόνο σου αυτό το κείμενο. Το demo βρίσκεται στο srekubecraft-demo/agent-sandbox/.

Ποιον αφορά αυτό το post;#

Το post απευθύνεται σε:

  • Platform Engineers που τους ζητούν «κάπου ασφαλές για να τρέχουν κώδικα οι agents» και το θέλουν μέσα στο cluster, όχι σε κάποιο SaaS
  • SREs που έμαθαν με τον δύσκολο τρόπο ότι το subprocess.run() ενός agent framework εκτελείται πάνω στο node, με το service account token του pod mounted
  • Go developers που φτιάχνουν agents με τα SDK της Anthropic ή της OpenAI και χρειάζονται ένα execution backend με πραγματικό isolation
  • Όποιον αξιολογεί E2B, Modal ή Daytona, τρέχει ήδη Kubernetes και θέλει να δει πώς μοιάζει η self-hosted επιλογή από τη γειτονιά του CNCF
  • Ομάδες που τρέχουν coding agents σε κλίμακα (CI bots, code interpreters, RL environments) και χρειάζονται εκατοντάδες βραχύβια, stateful environments με εγγυημένο cleanup

TL;DR#

  • Το agent-sandbox είναι project του Kubernetes SIG Apps: ένα Sandbox CRD + controller για long-running, stateful, singleton containers με σταθερή ταυτότητα, μαζί με extension CRDs (SandboxTemplate, SandboxWarmPool, SandboxClaim) για pooling και self-service
  • Είναι sandbox orchestrator, όχι sandbox runtime: το isolation σε επίπεδο kernel το αναθέτει στο gVisor ή στα Kata Containers μέσω του runtimeClassName. Σε kind παίρνεις σκέτο container isolation
  • Το Go SDK (sigs.k8s.io/agent-sandbox/clients/go/sandbox) σου δίνει CreateSandbox, Run, Read/Write/List, τρία connection modes και ένα Handle interface για mocking
  • Τα warm pools δουλεύουν: το claim ενός προθερμασμένου sandbox πήρε 53ms στο kind cluster μου, απέναντι σε ~25s για να φτιαχτεί ένα από το μηδέν (image pull + scheduling + readiness)
  • Το garbage collection με βάση το TTL, μέσω του spec.lifecycle.shutdownTime, σημαίνει ότι ένα process agent που κράσαρε δεν μπορεί να αφήσει πίσω του sandboxes: τα σβήνει ο controller
  • Το documentation έχει κενά: το k8s/ overlay του source tree έρχεται με ko:// image refs που δεν γίνονται resolve, ο οδηγός εγκατάστασης και το Go SDK διαφωνούν για το namespace στο οποίο ζει ο router, και το NetworkPolicy που δημιουργεί αυτόματα ο controller θα σου γυρίσει 504 χωρίς καμία εξήγηση. Και τα τρία καλύπτονται παρακάτω.

Τι είναι το Agent Sandbox;#

Το Agent Sandbox είναι ένα CRD και ένας controller που αναπτύσσονται υπό το Kubernetes SIG Apps, με τον κώδικα στο kubernetes-sigs/agent-sandbox. Το βασικό resource είναι παραπλανητικά απλό:

apiVersion: agents.x-k8s.io/v1beta1
kind: Sandbox
metadata:
  name: my-sandbox
spec:
  podTemplate:
    spec:
      containers:
        - name: workspace
          image: python:3.13-slim

Ένα Sandbox, ένα pod, σταθερό hostname, προαιρετικό persistent storage, και lifecycle management (προγραμματισμένο shutdown, pause/resume) που το αναλαμβάνει ο controller. Η ενότητα motivation του documentation ονομάζει τα use cases ευθέως: runtimes για AI agents που εκτελούν μη έμπιστο κώδικα γραμμένο από LLM, cloud development environments, notebooks, και stateful services του ενός pod που δεν δικαιολογούν ένα StatefulSet.

Το project είναι ξεκάθαρο για το scope του, και αυτή είναι η πιο σημαντική πρόταση του documentation:

«Agent Sandbox is a sandbox orchestrator. It delegates low-level container isolation to secure “Sandbox Runtimes” (like gVisor or Kata Containers) by managing Pods configured to use these runtimes (via RuntimeClass).»

Το agent-sandbox λοιπόν σου δίνει το API, το pooling και το lifecycle. Το όριο στα syscalls είναι ό,τι παρέχει το runtimeClassName σου. Σε ένα kind cluster πάνω σε laptop αυτό είναι σκέτο runc: μια χαρά για demo, όχι για εχθρικό multi-tenancy.

Γιατί όχι απλώς Pods, Jobs ή ένα StatefulSet;#

Μπορείς κάλλιστα να το στήσεις με το χέρι: ένα StatefulSet με size 1, ένα Service και ένα PVC. Το κάνει κόσμος. Η σύγκριση εξηγεί γιατί επικράτησε ένα ξεχωριστό abstraction:

ΚριτήριοΣκέτο Pod / JobStatefulSet (size 1)Sandbox CRD
Σταθερή ταυτότηταΌχιΝαιΝαι
Persistent storagePVC που το δένεις με το χέριvolumeClaimTemplatesvolumeClaimTemplates
Προγραμματισμένη λήξη (TTL)Μόνο στα Jobs, μετά την ολοκλήρωσηΌχιshutdownTime + shutdownPolicy
Pause / resumeΌχιScale στο 0 (χάνεται η σημασιολογία)operatingMode: Suspended
Προθερμασμένο poolΤο φτιάχνεις μόνος σουΌχιSandboxWarmPool + SandboxClaim
Exec/file API για agentsΤο στήνεις μόνος σου με kubectl execΤο ίδιοRouter + Go/Python SDK
Ασφαλή defaultsΔικό σου πρόβλημαΔικό σου πρόβλημαSA token automount κλειστό, NetworkPolicy ανά template

Γιατί όχι E2B ή Modal;#

ΚριτήριοE2B / Modal / Daytonaagent-sandbox
Πού τρέχειΣτο cloud του vendorΣτα δικά σου clusters
Data localityΚώδικας + δεδομένα βγαίνουν από την περίμετρό σουΜένουν μέσα στο VPC σου
IsolationFirecracker microVMs (ισχυρό)Ανατίθεται αλλού: gVisor / Kata / runc, διαλέγεις εσύ
Cold startΚάτω από ένα δευτερόλεπτο (αυτό ακριβώς πουλάνε)~50ms από warm pool, δευτερόλεπτα όταν ξεκινάει κρύο
Λειτουργικό βάροςΜηδένΕίναι ένας controller που τον τρέχεις και τον αναβαθμίζεις εσύ
Μοντέλο κόστουςΧρέωση ανά δευτερόλεπτοΤα node pools που ήδη έχεις

Με δυο λόγια: αν δεν έχεις καθόλου Kubernetes, ένα hosted sandbox είναι λιγότερη δουλειά. Αν τρέχεις ήδη clusters με GitOps, network policy και διαδικασία security review, το agent-sandbox μετατρέπει το «πού τρέχουν κώδικα οι agents» σε ένα workload type που το διαχειρίζεσαι όπως όλα τα υπόλοιπα.

Τι ΔΕΝ είναι το Agent Sandbox#

  • Δεν είναι sandbox runtime. Ενορχηστρώνει pods πάνω σε gVisor/Kata, δεν παρέχει το ίδιο kernel isolation
  • Δεν είναι agent framework. Ούτε LLM loop, ούτε tools, ούτε prompts. Φέρνεις εσύ LangGraph, ADK ή (όπως παρακάτω) έναν σκέτο tool runner του Anthropic SDK
  • Δεν είναι code interpreter service. Ο /execute server στο sandbox image είναι reference runtime, όχι προϊόν
  • Δεν είναι multi-cluster. Ένας controller, ένα cluster. Το scheduling σε επίπεδο fleet είναι δική σου δουλειά
  • Δεν είναι GA. Το API είναι v1beta1 στο v0.5.6, αναπτύσσεται ενεργά και έχει πραγματικό deprecation cycle (το v1alpha1 αφαιρέθηκε με migration guide)

Βασικές έννοιες#

Τέσσερα CRDs και ένα deployment κάνουν όλη τη δουλειά:

  • Sandbox (agents.x-k8s.io/v1beta1): το singleton workload. Κρατάει το pod template, τα volumeClaimTemplates, το operatingMode (Running/Suspended) και τα πεδία του lifecycle: το shutdownTime (απόλυτη στιγμή λήξης) και το shutdownPolicy (με Retain το object μένει, με Delete το μαζεύει το garbage collection)
  • SandboxTemplate (extensions): το blueprint που ανήκει στον admin, με image, resources, runtime class και storage. Όταν ένα sandbox δημιουργείται μέσα από template, ο controller βάζει by default το automountServiceAccountToken σε false. Ο μη έμπιστος κώδικας δεν παίρνει credentials για τον API server, εκτός αν το ζητήσεις ρητά εσύ
  • SandboxWarmPool (extensions): κρατάει replicas sandboxes έτοιμα, φτιαγμένα από ένα template, ώστε τα ακριβά κομμάτια (scheduling, image pull, readiness) να έχουν πληρωθεί πριν ζητήσει κανείς οτιδήποτε
  • SandboxClaim (extensions): το request που βλέπει ο χρήστης. Ένα claim πάνω σε ένα pool υιοθετεί ακαριαία ένα warm sandbox, και το pool το αναπληρώνει στο background
  • sandbox-router: ένα deployment που κάνει proxy τα exec και file operations του SDK (/execute, upload, download) προς το σωστό sandbox pod
flowchart TB
    subgraph client["Agent process (your machine or in-cluster)"]
        sdk["Go SDK<br/>sigs.k8s.io/agent-sandbox/clients/go/sandbox"]
    end

    subgraph cluster["Kubernetes cluster"]
        api["kube-apiserver"]
        ctrl["agent-sandbox-controller<br/>(agent-sandbox-system)"]
        router["sandbox-router"]

        subgraph pool["SandboxWarmPool"]
            tmpl["SandboxTemplate"]
            warm1["Sandbox (warm)"]
            warm2["Sandbox (warm)"]
        end

        claim["SandboxClaim"]
    end

    sdk -->|"1- create SandboxClaim"| api
    api --> ctrl
    ctrl -->|"2- adopt warm sandbox"| pool
    ctrl -->|"3- claim bound"| claim
    sdk -->|"4- exec / file ops<br/>(port-forward or Gateway)"| router
    router -->|"HTTP :8888"| warm1
    tmpl -.-> warm1
    tmpl -.-> warm2

Το SDK μιλάει με τον router με έναν από τρεις τρόπους: Gateway (για production, μέσω του Gateway API), port-forward (για development: ένα native SPDY tunnel από το client-go, χωρίς να χρειάζεται kubectl) ή direct URL (για agents μέσα στο cluster που χτυπάνε το DNS του router Service). Το demo χρησιμοποιεί port-forward.

Το Go SDK#

Εγκατάσταση:

go get sigs.k8s.io/agent-sandbox/clients/go/sandbox@v0.5.6

Το Go SDK δημοσιεύεται από το root module του repository, οπότε τα release tags του repo είναι και οι εκδόσεις του SDK. Θέλει Go 1.26+. Η βασική του επιφάνεια είναι μικρή:

client, err := sandbox.NewClient(ctx, sandbox.Options{Namespace: "default"})
// port-forward mode is the default when GatewayName and APIURL are unset

sb, err := client.CreateSandbox(ctx, "python-sandbox-pool", "default")

res, err := sb.Run(ctx, "python3 --version")   // ExecutionResult{Stdout, Stderr, ExitCode}
err = sb.Write(ctx, "script.py", []byte(src))  // plain filenames only
data, err := sb.Read(ctx, "script.py")
entries, err := sb.List(ctx, ".")

Λεπτομέρειες που μετράνε στο production, κατευθείαν από το documentation του Go client και επιβεβαιωμένες πάνω στον κώδικα του v0.5.6:

  • Τα file ops κάνουν retry, το Run() όχι. Τα Read/Write/List/Exists ξαναδοκιμάζουν μέχρι 6 φορές σε 5xx και σε connection errors. Το Run() κάνει by default μία μόνο προσπάθεια, γιατί η εκτέλεση εντολών δεν είναι idempotent. Για τις εντολές που είναι, το ενεργοποιείς με sandbox.WithMaxAttempts(6)
  • Disconnect() ή Close(): το Disconnect κλείνει το transport αλλά κρατάει το SandboxClaim, οπότε το sandbox μένει ζεστό ανάμεσα στα requests του χρήστη. Το Close σβήνει το claim
  • Ο θάνατος του port-forward εντοπίζεται, δεν περιμένεις κάποιο timeout: ένας monitor στο background γυρίζει αμέσως τον client σε ErrNotReady, και το Open() ξανασυνδέεται
  • Testing χωρίς cluster: το package κάνει export ένα Handle interface (Open, Close, Run, Read, Write, List, Exists, IsReady) ακριβώς για να μπορείς να κάνεις fake το sandbox στα unit tests
  • Το EnableAutoCleanup() εγκαθιστά έναν SIGINT/SIGTERM handler που σβήνει τα claims που παρακολουθεί. Οι agents που πεθαίνουν στο πληκτρολόγιο δεν αφήνουν πίσω τους pods

Το demo: ένας Go agent με sandboxed tools#

Το pattern που προωθούν τα upstream examples, και αυτό που υλοποίησα, είναι το εξής: το agentic loop τρέχει έξω από το sandbox, και μέσα τρέχει μόνο η εκτέλεση των tools. Το LLM δεν βλέπει ποτέ credential, και ο κώδικας που γράφει δεν μπορεί να αγγίξει το μηχάνημα όπου τρέχει το loop.

Ο agent μου είναι ~250 γραμμές Go. Χρησιμοποιεί τον tool runner του Anthropic Go SDK (που οδηγεί για λογαριασμό σου το loop request -> tool -> response) και τέσσερα tools, το καθένα ένα λεπτό wrapper πάνω από το sandbox SDK:

ToolΑντιστοιχεί σεΣημειώσεις
run_commandsb.Run()Επιστρέφει stdout, stderr, exit code
write_filesb.Write()Μόνο σκέτα filenames
read_filesb.Read()Μόνο σκέτα filenames
list_filessb.List()By default το working directory

Το ενδιαφέρον κομμάτι είναι το πώς δένει με το lifecycle. Κάθε tool call σπρώχνει το shutdownTime του claim πιο μπροστά:

func (l *lifecycle) extend(ctx context.Context) error {
	claim, err := l.helper.ExtensionsClient.SandboxClaims(l.namespace).Get(ctx, l.claim, metav1.GetOptions{})
	if err != nil {
		return err
	}
	shutdownAt := metav1.NewTime(time.Now().Add(l.ttl))
	claim.Spec.Lifecycle = &extensionsv1beta1.Lifecycle{
		ShutdownPolicy: extensionsv1beta1.ShutdownPolicyDelete,
		ShutdownTime:   &shutdownAt,
	}
	_, err = l.helper.ExtensionsClient.SandboxClaims(l.namespace).Update(ctx, claim, metav1.UpdateOptions{})
	return err
}

Γιατί έχει σημασία: το cleanup δεν εξαρτάται από το αν θα επιβιώσει το process του agent. Αν κρασάρει το CLI, αν το laptop πέσει σε sleep ή αν κολλήσει το loop, το sandbox το σβήνει ο controller, 5 λεπτά μετά το τελευταίο tool call. Είναι το ίδιο pattern λήξης λόγω αδράνειας που χρησιμοποιεί το upstream example sandboxed-tools, και είναι η διαφορά ανάμεσα στα «sandboxes σαν pets» και στα «sandboxes σαν sessions που τα μαζεύει το garbage collection».

Σημείωση: τύλιξε αυτό το update σε retry.RetryOnConflict. Στο claim γράφει και ο controller, και η πρώτη μου εκδοχή έχασε ένα race από αυτόν στη μέση ενός session (the object has been modified). Τα claims είναι objects που γίνονται ενεργά reconcile, όχι αδρανείς εγγραφές.

Το infrastructure, σε στυλ GitOps#

Όλα γίνονται reconcile μέσω Flux, εκτός από τις δύο εγκαταστάσεις τύπου «η κότα ή το αυγό» (το Cilium ως CNI και το ίδιο το Flux). Ο controller έρχεται από ένα GitRepository καρφωμένο στο tag v0.5.6, μαζί με ένα Kustomization πάνω στο upstream k8s/ overlay:

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: agent-sandbox
  namespace: flux-system
spec:
  interval: 1h
  sourceRef:
    kind: GitRepository
    name: agent-sandbox
  path: ./k8s
  prune: true
  wait: true
  timeout: 5m
  # the source tree ships ko:// image refs that only resolve in the release
  # pipeline; rewrite to the published controller image, pinned to digest
  images:
    - name: ko://sigs.k8s.io/agent-sandbox/cmd/agent-sandbox-controller
      newName: registry.k8s.io/agent-sandbox/agent-sandbox-controller
      digest: sha256:a502cfdbcf550e77509cc56097978458a1ac3d5b59972f21b7ce0e0a84a5c12e

Αυτός ο images: transformer δεν είναι διακοσμητικός: χωρίς αυτόν, το pod του controller καταλήγει σε InvalidImageName. Περισσότερα στις παγίδες.

Από την πλευρά του sandbox υπάρχουν ένα SandboxTemplate που τυλίγει το upstream python-runtime image (έναν FastAPI server στο :8888, προς τον οποίο προωθεί ο router), ένα SandboxWarmPool με 2 replicas και το deployment του router. Όλα τα images είναι pinned με digest.

Πώς το τρέχεις#

git clone https://github.com/nicknikolakakis/srekubecraft-demo
cd srekubecraft-demo/agent-sandbox
task up

export ANTHROPIC_API_KEY="sk-ant-..."
task agent:run -- "write a python script that prints the first 10 fibonacci numbers and run it"

Πραγματικό output από το kind cluster μου:

sandbox ready in 44ms: claim=sandbox-claim-ktrtz sandbox=python-sandbox-pool-4mq8r pod=python-sandbox-pool-4mq8r
[tool] write_file: fib.py (157 bytes)
[tool] run_command: python3 /app/fib.py

Created `/app/fib.py` with a generator-based `fibonacci(n)` and ran it.

Output: `[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]`

Δύο tool calls, ένα warm claim, τέλος. Το καλύτερο demo είναι να ζητήσεις από τον agent να ψάξει το ίδιο του το κλουβί:

task agent:run -- "Check what network access you have from inside your sandbox: \
  try to reach the Kubernetes API server at https://10.96.0.1 with a short timeout, \
  then try fetching https://pypi.org. Report what is reachable and what is blocked."
sandbox ready in 68ms: claim=sandbox-claim-24q62 sandbox=python-sandbox-pool-5qflx pod=python-sandbox-pool-5qflx
[tool] run_command: sh -c "curl -sS -k --max-time 5 -o /dev/null -w 'HTTP:%{http_code} ...' https://10.96.0.1 ..."
[tool] run_command: sh -c "curl -sS --max-time 8 -o /dev/null -w 'HTTP:%{http_code} ...' https://pypi.org ..."
[tool] write_file: netcheck.py (1386 bytes)
[tool] run_command: python3 netcheck.py

Network access results from inside the sandbox:

- Kubernetes API server https://10.96.0.1:443 - BLOCKED. TCP connect times out
  after 5s (no response, no reset). Cluster-internal API access is firewalled off.
- https://pypi.org - REACHABLE. DNS resolves, TCP connects in ~0.1s, HTTPS returns 200.

Notes: curl isn't installed in the sandbox, so I ran the checks with Python
(socket + urllib). Egress to the public internet works; access to the in-cluster
control plane is blocked - the connection silently times out rather than being
refused, which is consistent with a network policy dropping packets to the
cluster service network.

Αυτό είναι το NetworkPolicy του controller από την παγίδα #3 που κάνει τη δουλειά του, επιβεβαιωμένο από μέσα, από τον ίδιο τον κώδικα που περιορίζει. Ο agent πρόσεξε μάλιστα ότι έλειπε το curl και ξανάγραψε τον έλεγχό του σε Python χωρίς να του το πει κανείς, και διάβασε σωστά το «σιωπηλό timeout, όχι connection refused» ως drop από policy.

Το repo έχει και ένα smoke test που περνάει από ολόκληρο το path του sandbox χωρίς LLM στο loop: claim, exec, round-trip ενός αρχείου, patch του TTL:

cd agent && SANDBOX_SMOKE=1 go test -v -run TestSandboxSmoke
=== RUN   TestSandboxSmoke
"msg"="port-forward established" "localPort"=60997 "pod"="sandbox-router-deployment-576794cd5b-7t54n"
"msg"="API URL discovered" "url"="http://127.0.0.1:60997" "mode"="port-forward"
    smoke_test.go:36: sandbox ready in 53ms: claim=sandbox-claim-xvfmz pod=python-sandbox-pool-7s7mc
    smoke_test.go:50: run: "hello from $(hostname)\n"
--- PASS: TestSandboxSmoke (0.13s)

53 milliseconds από το CreateSandbox() μέχρι ένα sandbox bound και έτοιμο. Για σύγκριση, το ίδιο το warm pool χρειάστηκε περίπου 25 δευτερόλεπτα για να φέρει τα sandboxes του σε Ready όταν πρωτοδημιουργήθηκε: scheduling, image pull, probe. Αυτά τα 25s είναι ακριβώς το latency που δεν βλέπουν ποτέ οι χρήστες σου, γιατί το έχει προπληρώσει το pool. Όλη η αξία του warm pool, σε δύο νούμερα.

Και ναι, εκείνο το $(hostname) που τυπώθηκε αυτούσιο είναι πραγματικό output. Είναι η παγίδα #4.

Οι παγίδες#

Αυτή είναι η ενότητα που θα ήθελα να υπήρχε πριν ξεκινήσω. Πέντε ευρήματα, όλα αναπαραγμένα στο v0.5.6.

1. Το k8s/ overlay του source tree δεν γίνεται deploy όπως είναι#

Το documentation λέει ότι μπορείς να κάνεις render την εγκατάσταση «directly from source with kubectl kustomize k8s/». Render γίνεται, να τρέξει δεν γίνεται. Το deployment αναφέρεται στο ko://sigs.k8s.io/agent-sandbox/cmd/agent-sandbox-controller, ένα build directive του ko που γίνεται resolve μόνο μέσα στο release pipeline. Κάν’ το deploy και το pod μένει σε InvalidImageName.

Αν εγκαταστήσεις από το release asset (sandbox-with-extensions.yaml), αυτό δεν θα το δεις ποτέ. Αν θέλεις GitOps πάνω στο tagged source (και το θέλεις, γιατί το release asset δεν είναι Flux source), πρόσθεσε τον images: transformer του kustomize που φαίνεται παραπάνω.

2. Δύο components διαφωνούν για το πού ζει ο router#

Ο οδηγός εγκατάστασης κάνει deploy το sandbox-router στο agent-sandbox-system. Το port-forward mode του Go SDK ψάχνει το sandbox-router-svc στο namespace του claim (το tunnel.go κάνει list τα EndpointSlices στο Options.Namespace). Ακολούθησε τον οδηγό εγκατάστασης, τρέξε μετά το Go quickstart, και παίρνεις:

sandbox: no ready endpoints for sandbox-router-svc in namespace default

Η λύση: κάνε deploy τον router στο namespace όπου ζουν τα claims σου. Το documentation του Go client όντως γράφει «target namespace», χαμηλόφωνα, στα prerequisites.

3. Το NetworkPolicy του ίδιου του controller θα σου γυρίσει 504 μόλις διορθώσεις το #2#

Διορθώνοντας το #2, πέφτεις κατευθείαν στον επόμενο τοίχο. Ο controller δημιουργεί αυτόματα ένα NetworkPolicy ανά SandboxTemplate, και το ingress rule του δέχεται router pods μόνο από το agent-sandbox-system:

ingress:
  - from:
      - namespaceSelector:
          matchLabels:
            kubernetes.io/metadata.name: agent-sandbox-system
        podSelector:
          matchLabels:
            app: sandbox-router

Με τον router πλέον στο default, κάθε exec κάνει timeout στο CNI και ο router επιστρέφει 504: Timed out waiting for the backend sandbox. Κανένα log δεν γράφει drop από policy. Με Cilium, αυτό που σε βάζει σε υποψίες είναι το cilium endpoint list να δείχνει POLICY (ingress): Enabled στα sandbox pods.

Μην πειράξεις το policy που ανήκει στον controller: θα γίνει reconcile πίσω, και θα καταλήξεις να παλεύεις με το ίδιο το εργαλείο. Τα NetworkPolicies είναι additive, οπότε δώσε το επιπλέον allow με ένα δικό σου:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-local-router-to-sandboxes
  namespace: default
spec:
  podSelector:
    matchLabels:
      agents.x-k8s.io/created-by: controller
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: sandbox-router
      ports:
        - protocol: TCP
          port: 8888

Μια και είσαι εκεί, διάβασε και την πλευρά του egress στο policy του controller. Επιτρέπει το 0.0.0.0/0 εκτός από τα RFC1918, τα link-local και τα αντίστοιχά τους σε IPv6. Ο sandboxed κώδικας μπορεί να κάνει pip install από το internet, αλλά δεν φτάνει στα services του cluster σου, στο node metadata ή σε οτιδήποτε άλλο βρίσκεται σε private ranges. Είναι ένα πραγματικά καλό default, και αξίζει να ξέρεις ότι υπάρχει πριν αρχίσεις να αναρωτιέσαι γιατί ένας agent δεν μπορεί να καλέσει ένα API μέσα στο cluster.

4. Το /execute δεν είναι shell#

Το /execute endpoint του python-runtime κάνει exec την εντολή σου, δεν την περνάει από shell. Τα pipes, το &&, το redirection και τα $VARS φτάνουν ως αυτούσια argv tokens:

{"command": "echo a | tr a b"}
// {"stdout": "a | tr a b\n", "exit_code": 0}

Τα arguments μέσα σε εισαγωγικά σπάνε με τον τρόπο του shlex, οπότε η λύση είναι να τυλίγεις οτιδήποτε έχει σχήμα shell:

{"command": "sh -c \"echo a | tr a b\""}
// {"stdout": "b\n", "exit_code": 0}

Για έναν LLM agent, αυτό ανήκει στο system prompt και όχι σε error handling εκ των υστέρων. Το prompt του agent μου δηλώνει το working directory (/app), το non-root UID (1000) και τον κανόνα του sh -c, και ο Claude συμμορφώνεται με την πρώτη, αντί να κάψει ένα round trip από tool call για να το ανακαλύψει.

5. Το Write() δέχεται μόνο σκέτα filenames#

Το sb.Write(ctx, "dir/script.py", ...) απορρίπτεται από το SDK: directory separators δεν επιτρέπονται, και τα αρχεία πέφτουν στο working directory του server. Οτιδήποτε έχει σχήμα path περνάει από το run_command. Καλό είναι να το ξέρεις πριν σχεδιάσεις tools που καθρεφτίζουν ένα filesystem.

Bonus: τα snapshots υπάρχουν μόνο σε Python + GKE#

Το σενάριο suspend/resume με διατήρηση του state (PodSnapshotSandboxClient, δηλαδή πάγωμα της μνήμης και του filesystem ενός gVisor container) απαιτεί προς το παρόν τα podsnapshot.gke.io CRDs του GKE και υπάρχει μόνο στο Python SDK. Το Go SDK δεν έχει ακόμα κάτι αντίστοιχο. Το operatingMode: Suspended είναι portable, αλλά εκεί το pod γκρεμίζεται και κρατιούνται τα volumes, δεν είναι memory snapshot.

Υπέρ και κατά#

Υπέρ#

ΠλεονέκτημαΠεριγραφή
Το σωστό abstractionStateful singleton με TTL, pause/resume και σταθερή ταυτότητα: το σχήμα που έχουν στην πράξη τα agent workloads
Τα warm pools κάνουν αυτό που υπόσχονταιClaims στα 53ms, μετρημένα σε kind. Το κόστος του cold path το έχει προπληρώσει το pool
Ασφαλή defaultsSA token automount κλειστό, NetworkPolicy ανά template χωρίς egress σε private ranges, runtime image που τρέχει non-root
GC από την πλευρά του controllerΜε shutdownTime + shutdownPolicy: Delete, οι clients που κράσαραν δεν μπορούν να αφήσουν πίσω τους sandboxes
Πραγματικά SDKsGo και Python, με retries, διαχείριση του reconnect και interface που γίνεται mock. Όχι ένα wrapper γύρω από το kubectl exec
Vendor-neutral isolationΤο ίδιο API πάνω σε runc, gVisor ή Kata. Το hardening είναι αλλαγή μίας γραμμής στο runtimeClassName
Governance του SIG AppsKubernetes-native διαδικασία review, και deprecation policy που έχει ήδη δοκιμαστεί στην πράξη (migration guide v1alpha1 -> v1beta1)

Κατά#

ΠεριορισμόςΠεριγραφή
Beta API που αλλάζει γρήγοραv1beta1, με releases κάθε εβδομάδα. Υπολόγισε ότι θα παρακολουθείς το upstream από κοντά
Το documentation μένει πίσω από τον κώδικαΗ ασυμφωνία στο namespace του router, τα ko:// refs, η συμπεριφορά του netpol: όλα βρέθηκαν με debugging, όχι με διάβασμα
Από μόνο του δεν δίνει kernel isolationΣε clusters χωρίς gVisor/Kata έχεις τα σκέτα όρια ενός container. Το όνομα «sandbox» μπορεί να υπόσχεται περισσότερα από όσα έκανες deploy
Τα snapshots είναι κατακερματισμέναMemory snapshots μόνο με Python SDK + GKE
Ο router είναι ακόμα ανώριμοςΈνα μόνο HTTP proxy, image από staging registry (latest-main, κάνε pin το digest μόνος σου), και το auth είναι ένα bearer token που το δένεις εσύ
Ένα clusterΚαμία σημασιολογία για fleet. Το capacity του pool είναι ανά cluster

Πότε να χρησιμοποιήσεις το Agent Sandbox#

Χρησιμοποίησέ το όταν:

  • Οι agents (ή οι χρήστες) εκτελούν κώδικα γραμμένο από LLM, και θέλεις να φύγει από το host του agent και να μπει μέσα στο δικό σου όριο isolation
  • Χρειάζεσαι πολλά βραχύβια, stateful environments με εγγυημένο cleanup: code interpreters, coding agents δίπλα στο CI, environments για RL rollouts
  • Το latency του claim μετράει: οι interactive agents νιώθουν τη διαφορά ανάμεσα στα 50ms και στα 25s
  • Τρέχεις ήδη GitOps και θέλεις το infrastructure των agents να το διαχειρίζεσαι όπως κάθε άλλο workload

Προσπέρασέ το όταν:

  • Δεν έχεις καθόλου Kubernetes: ένα hosted sandbox (E2B, Modal) είναι ένα API key, αντί για έναν controller που πρέπει να τρέχεις εσύ
  • Χρειάζεσαι αυστηρό multi-tenancy σε cluster που δεν μπορεί να τρέξει gVisor ή Kata: ο orchestrator δεν μπορεί να προσθέσει isolation που δεν έχει το runtime σου
  • Το «sandbox» σου είναι απλώς batch compute χωρίς ανάγκη για state ή ταυτότητα: ένα Job είναι πιο απλό
  • Χρειάζεσαι σήμερα cross-region ή multi-cluster placement για τα sandboxes

Troubleshooting#

ΠρόβλημαΣυμπτώματαΛύση
Pod του controller σε InvalidImageNameΈγινε deploy από το k8s/ overlay του sourceimages: transformer του kustomize προς το registry.k8s.io/agent-sandbox/agent-sandbox-controller (παγίδα #1)
no ready endpoints for sandbox-router-svcGo SDK, port-forward modeΚάνε deploy τον router στο namespace του claim (παγίδα #2)
Ο router επιστρέφει 504, το sandbox είναι ReadyΤο exec κάνει timeout, κανένα error στο sandbox podΤο NetworkPolicy του controller κόβει τον router από άλλο namespace. Πρόσθεσε ένα additive allow policy (παγίδα #3)
Τα pipes/&& γυρίζουν πίσω αυτούσιαΤο output του run_command περιέχει τους operators σουΤο /execute δεν είναι shell. Τύλιξέ το σε sh -c "..." (παγίδα #4)
Το Write απορρίπτει το pathwrite error σε dir/fileΜόνο σκέτα filenames. Για paths χρησιμοποίησε το run_command (παγίδα #5)
Το sandbox εξαφανίστηκε στη μέση του sessionΤο claim χάθηκε, ErrSandboxDeleted στο reconnectΠέρασε το shutdownTime. Παράτεινε το TTL σε κάθε δραστηριότητα, όπως το lifecycle.extend του demo

Το repository του demo#

Όλα όσα είδες σε αυτό το post βρίσκονται στο srekubecraft-demo/agent-sandbox/:

  • Bootstrap για Kind + Cilium 1.20 + Flux μέσω ενός κοινού Taskfile
  • Flux GitRepository/Kustomization για τον controller στο v0.5.6, pinned με digest
  • Router, SandboxTemplate, SandboxWarmPool και το additive NetworkPolicy
  • Ο Go agent (Anthropic tool runner + 4 sandboxed tools + TTL lifecycle)
  • Το smoke test χωρίς LLM
git clone https://github.com/nicknikolakakis/srekubecraft-demo
cd srekubecraft-demo/agent-sandbox
task up
task agent:run -- "write a python script that prints the first 10 fibonacci numbers and run it"

Συμπέρασμα#

Το Agent Sandbox απαντάει σε μια ερώτηση που αργά ή γρήγορα συναντά κάθε platform team που δέχεται αιτήματα του τύπου «θέλουμε να τρέξουμε AI agents»: τι workload type είναι ο κώδικας που μόλις έγραψε ένα LLM; Τα Deployments υποθέτουν stateless replicas. Τα Jobs υποθέτουν batch που τρέχει μέχρι να ολοκληρωθεί. Τα StatefulSets υποθέτουν ένα διατεταγμένο σύνολο. Ένα agent session δεν είναι τίποτα από αυτά: είναι ένα stateful singleton με ταυτότητα, με TTL και με επίπεδο εμπιστοσύνης κάπου κάτω από το «δικός μας κώδικας». Το να το μοντελοποιήσεις ως first-class CRD, με warm pools που κρύβουν το cold start και με garbage collection από την πλευρά του controller που αντέχει τα crashes των clients, είναι η σωστή απόφαση, και τα 53ms στο latency του claim δείχνουν ότι ο σχεδιασμός δικαιολογεί την πολυπλοκότητά του.

Είναι και νωρίς ακόμα. Το API είναι beta, το documentation ακολουθεί τον κώδικα με καθυστέρηση, και τρεις από τις πέντε παγίδες μου ήταν ασυμφωνίες σε namespaces και policies ανάμεσα σε components του ίδιου project. Καμία δεν ήταν δύσκολη στο debugging με τα συνηθισμένα εργαλεία. Και αξίζει να σημειωθεί ότι το security posture απέτυχε closed κάθε φορά: το χειρότερο default που συνάντησα ήταν traffic που κοβόταν πιο επιθετικά από όσο έπρεπε, ποτέ πιο χαλαρά.

Αν τρέχεις Kubernetes και οι agents είναι στο roadmap σου, αυτό είναι το project που αξίζει να παρακολουθείς: εδώ επενδύει ξεκάθαρα ο μηχανισμός του SIG Apps. Ξεκίνα με το flow warm pool + claim σε kind, όπως σε αυτό το demo, βάλε από κάτω gVisor ή Kata πριν εμφανιστεί οποιοσδήποτε μη έμπιστος κώδικας, και αντιμετώπιζε τον router ως το ανώριμο κομμάτι που είναι σήμερα. Και κράτα το agentic loop έξω από το sandbox: το LLM παίρνει tools, τα tools παίρνουν ένα pod, και το pod παίρνει ένα NetworkPolicy που δεν φτάνει στο control plane σου. Αυτή η διαστρωμάτωση είναι όλο το νόημα.


Αν σου φάνηκε χρήσιμο, ίσως σου αρέσουν και τα σχετικά posts μου:

Agent Sandbox

EOF · 21 min · 4433 words
$ continue exploring
← Modelplane - Fleet inference σε πολλά clusters, demo χωρίς ούτε μία GPU // Μια hands-on γνωριμία με το Modelplane, το control plane για AI inference πάνω σε fleet από GPU clusters, χτισμένο σε Crossplane. Δύο regions σε Kind με ψεύτικες DRA GPU, το contract ανάμεσα σε platform team και developers γραμμένο στο λεξιλόγιο του ίδιου του DRA, drain σε επίπεδο fleet, και η παγίδα στο release που σου κοστίζει δύο APIs. #sre #kubernetes #modelplane llm-d - Kubernetes-native distributed LLM inference σε κλίμακα // Μια hands-on περιήγηση στο llm-d, το CNCF Sandbox framework για distributed LLM inference σε Kubernetes: inference-aware routing, prefill/decode disaggregation και KV-cache offload. Με demo χωρίς GPU σε Kind, με το vLLM simulator και Flux GitOps. #sre #kubernetes #llm-d →
$ grep -r --related
// author
Nick Nikolakakis
Nick Nikolakakis Principal SRE, Platform & AI Engineer // Writing about Kubernetes, SRE practices, cloud-native infrastructure, and AI systems
$ exit█ logout connection closed. cd ~/home ↵
ESC
Type to search...