Skip to content

Sandbox runtime

Choose between on-cluster OpenSandbox and E2B cloud sandboxes, route browsers to runtimes, and size sandbox pods.

The sandbox runtime is chart configuration, applied by a redeploy. Configure it in a Helm overlay, under Sandbox Runtime in the Admin Console, or with matching KOTS ConfigValues items.

Commands on this page use $NAMESPACE. For Helm, set it to your release namespace. For Embedded Cluster, enter sudo ./dreadnode shell, then set it to kotsadm.

Dreadnode runs agent code in isolated sandboxes. Choose the runtime based on where workloads may execute and whether you need task-based evaluations.

RuntimeWorkload locationNetwork requirementTask evaluations
OpenSandboxYour clusterWildcard DNS to the cluster ingressNot supported
E2BE2B cloudOutbound access and public callbacksSupported

OpenSandbox is the default and runs through the bundled sandbox controller and server:

dreadnode-api:
config:
sandboxProvider: opensandbox

No manually supplied credential or callback configuration is required. The chart generates a shared API key for the platform API and OpenSandbox server; rotate it later from Secret rotation. OpenSandbox is the appropriate choice for air-gapped installs and environments where workload data cannot leave the cluster — the wildcard DNS record below resolves inside your network, so nothing about it requires public DNS.

Sandbox pods run in the release namespace. The chart can pin them elsewhere with dreadnode-sandbox-server.kubernetes.namespace, but that is unsupported on Embedded Cluster and KOTS installs: the license-bound image pull secret exists only in the release namespace, so pods scheduled outside it cannot pull their runtime image.

The first runtime on a node can take longer while Kubernetes fills its image cache. Dreadnode waits up to three minutes for the sandbox pod to become ready by default, which accommodates a cold pull of the runtime image through the Replicated proxy. Later runtimes normally start much faster.

Isolate OpenSandbox pods with a RuntimeClass

Section titled “Isolate OpenSandbox pods with a RuntimeClass”

By default, sandbox pods run on the same container runtime as everything else on the node and share the host kernel with it. Agent code that escapes its container reaches the node. To put a stronger boundary around it, name a Kubernetes RuntimeClass backed by an isolation runtime, and Dreadnode applies it to every sandbox pod it creates.

type names the isolation mechanism and runtimeClassName is the exact class installed on your cluster. Both are required together. Dreadnode never derives the class name from the type, because a RuntimeClass can be called anything. There is no dedicated Admin Console field, so both Embedded Cluster paths use Advanced Helm Values.

dreadnode-sandbox-server:
secureRuntime:
type: kata # gvisor, kata, or firecracker
runtimeClassName: kata-qemu
typeIsolationNested containers
gvisorUser-space kernel; no direct host syscallsNo
kataFull guest kernel in a VM (QEMU)Yes
firecrackerGuest kernel in a Firecracker microVMYes

The chart does not install the isolation runtime. A cluster administrator installs gVisor or Kata on the nodes and creates the RuntimeClass first. Confirm the name before you deploy:

Terminal window
kubectl get runtimeclass

The OpenSandbox server checks the RuntimeClass exists when it starts and refuses to start when it does not, so a typo fails the deployment instead of quietly running every sandbox on the host kernel.

Managed Kubernetes can do the install for you, and each provider offers one mechanism:

PlatformMechanismEnable itResulting class
GKEgVisorGKE Sandbox--sandbox type=gvisor on a node pool using the cos_containerd imagegvisor
AKSKataPod Sandboxing--workload-runtime KataVmIsolationkata-vm-isolation
EKS, k0s, bare metaleitherInstall it yourself, belowYou choose the name

GKE Sandbox applies to a whole node pool, so every pod on those nodes is sandboxed, and it cannot be enabled on a Standard cluster’s default node pool. AKS Pod Sandboxing needs Kubernetes 1.27 or later, the AzureLinux OS SKU, and a generation 2 VM size that supports nested virtualization.

Everywhere else, install the runtime on the nodes yourself:

  • Kata Containers — the kata-deploy Helm chart lays the binaries down on each node and creates the RuntimeClass objects for you.
  • gVisor — follow the containerd quick start to install runsc, then create a RuntimeClass whose handler is runsc.

Both are node-level installs that need root on every node that will run sandboxes, and both must be reapplied when you replace or upgrade node images.

gVisor runs on any node. Kata and Firecracker need hardware virtualization/dev/kvm — on every node that runs sandboxes, and this is where most Kata deployments stall:

  • Bare metal gives you KVM natively. It is also what AWS recommends when the workload is performance-sensitive or latency-sensitive.
  • AWS EC2 supports nested virtualization on virtual instances, currently C7i, M7i, R7i, I7i, C7i-flex, M7i-flex, C8i, M8i, R8i, X8i, C8id, M8id, R8id, C8i-flex, M8i-flex, and R8i-flex. Check that list against AWS’s own page before you size a node group — it grows, and sibling types you might expect are absent (m7id is not on it, for instance). Nested virtualization is off by default: set NestedVirtualization=enabled in the instance CPU options, which for EKS means the managed node group’s launch template. An existing instance must be stopped to change it.
  • Azure requires a generation 2 VM size that supports nested virtualization, such as Dsv3.

Confirm a node can actually do it before you commit to Kata:

Terminal window
kubectl debug node/<node> -it --image=busybox -- ls -l /host/dev/kvm

To keep the isolation runtime on dedicated nodes, put scheduling.nodeSelector and scheduling.tolerations on the RuntimeClass itself. Kubernetes applies them to every pod that selects the class. Dreadnode does not set pod placement for sandboxes.

Sandbox containers run under the container runtime’s default seccomp profile. It blocks the syscalls a container has no ordinary reason to make, including clone with CLONE_NEWUSER — the step that opens a user namespace, and the start of most kernel privilege-escalation chains.

Kubernetes applies no profile of its own unless the kubelet is started with seccompDefault, which managed distributions generally do not. Dreadnode sets it on every sandbox pod instead of relying on the cluster to do it.

The filter covers the sandbox container. The init container that installs the sandbox agent is unaffected, and this setting is independent of a RuntimeClass — a profile restricts which syscalls reach the kernel, while a RuntimeClass changes which kernel serves them.

Agent tooling that needs a blocked syscall can turn the filter off. There is no dedicated Admin Console field, so both Embedded Cluster paths use Advanced Helm Values, which overrides the dedicated fields:

dreadnode-sandbox-server:
sandboxSeccompProfile: Unconfined

This applies to every sandbox the deployment creates, not to one runtime, so it is better used to identify a syscall a workload needs than left in place.

A RuntimeClass and a seccomp profile both constrain a sandbox from the inside. To constrain what it can reach on the network, see Restrict sandbox egress.

Sandbox pods reach whatever the cluster network allows. Turn on egress enforcement and the sandbox server adds a filtering sidecar to every sandbox pod it creates:

global:
sandbox:
egressEnabled: true

One value switches both halves. The sandbox server injects the sidecar from it, and the API reads the same value to learn whether a policy it composes will actually be enforced. They cannot be set independently, because a server that filters while the API sends no policy fails every sandbox at admission, and an API that assumes enforcement the server is not doing reports containment that does not exist.

This is the OpenSandbox path. On E2B, enforcement rides the sandbox create call and there is nothing to enable — the switch above has no effect, and the rest of this section’s chart values do not apply.

The sidecar shares the sandbox pod’s network namespace and filters with nftables inside it, so enforcement does not depend on your CNI implementing NetworkPolicy. It runs in a different container from the one agent code runs in, and the sandbox container is provisioned without NET_ADMIN or NET_RAW — a process that escalates to root inside the sandbox can neither flush the rules nor mark its own packets to skip them.

Dropping NET_RAW costs the sandbox its raw-socket tooling. ping stops working, nmap’s SYN and UDP scans are unavailable, and an nmap run that does not name a scan type falls back to a TCP connect scan rather than failing — slower, and visible to the target as a completed connection rather than a half-open one.

The capability is dropped because on its own it defeats the filter. The sidecar exempts its own forwarded DNS by marking those packets, and from Linux 5.17 NET_RAW is enough to set that mark — so a sandbox that keeps it marks its own traffic and skips every rule, not one destination. That is a wider hole than any single allowed host, which is why the trade lands this way by default.

Restore raw sockets where the tooling matters more than the filter. There is no dedicated Admin Console field, so both Embedded Cluster paths use Advanced Helm Values:

dreadnode-sandbox-server:
sandboxDropCapabilities: []

NET_ADMIN is dropped whatever you set here, so the rules stay out of reach either way. The setting applies to every sandbox the deployment creates, and it reopens the bypass for all of them — an air-gapped deployment is the worst place to leave it cleared.

Every sandbox gets a floor, and global.airgapped alone decides which one. The floor has no setting of its own: global.airgapped already states whether the deployment has public egress, and a second switch could only disagree with the first.

global.airgappedDefault actionWhat the floor denies
trueDenyEverything not named — a literal-address connection included
falseAllowThe cloud metadata address 169.254.169.254

The two are not symmetric, and the asymmetry is worth understanding before you rely on the connected floor. Deny-by-default refuses a connection to a literal address for free, because such a destination is in no allow list — an air-gapped floor blocks your data stores, the Kubernetes API server and every other pod without the platform knowing a single address. Allow-by-default refuses nothing unless a rule matches it, so each protected destination has to be denied by address, and today only the metadata address is. A connected deployment’s floor is not a substitute for NetworkPolicy on your data stores — see Network trust boundaries for the inbound half.

Nothing above the floor can widen it. A task or a runtime can declare a narrower scope for itself, and a target your floor denies fails that run with the target named, before the sandbox starts — never dropped from the policy and run without. See Scoping what a sandbox can reach.

Most enclave hosts are already reachable, because the platform reads the URL-valued entries you set in SANDBOX_EXTRA_ENVUV_INDEX_URL, GOPROXY, HF_ENDPOINT, proxies — and allows those hosts. Add anything they do not name:

global:
sandbox:
egressExtraAllow:
- artifacts.corp.internal
- '*.mirror.corp.internal'
- registry.corp.internal # a private image registry compose tasks pull from

Each entry is a fully-qualified name, a *.suffix wildcard, an IP, or a CIDR. Name an in-cluster service as <service>.<namespace>.svc.cluster.local: a single-label name cannot be matched by the sidecar and is ignored, and the same rule applies to the hosts read from SANDBOX_EXTRA_ENV. The platform derives the public container registries for a compose task but not a private one, so a registry your mirror serves belongs here. egressExtraAllow is additive: it cannot override a floor deny, and an entry containing a protected destination is rejected rather than quietly widening the floor.

The platform’s own in-cluster URLs are held to the same rule. If you override the LiteLLM or MinIO endpoint with a single-label service name, an air-gapped deployment fails preflight naming the setting, and a connected one logs a warning at boot, because every sandbox that declared a scope would otherwise lose the gateway.

The sidecar is also unsupported inside a pod that already carries a transparent service-mesh sidecar, since both rewrite outbound traffic in the same namespace. Nothing detects that at render, so it is a prerequisite you own.

With the bundled inference proxy enabled, OpenSandbox runtimes receive DREADNODE_LLM_BASE=http://<release>-litellm:4000/v1. This in-cluster route does not depend on the platform’s public ingress accepting traffic from its own pods. E2B runtimes and external clients continue to use the configured LiteLLM public URL.

Leave the OpenSandbox URL empty for a bundled, on-cluster deployment. Set it when OpenSandbox runs in another cluster or your runtime pods need a custom LiteLLM route. The value may include /v1; the API normalizes the injected base to one /v1 suffix. There is no dedicated Admin Console field, so both Embedded Cluster paths use Advanced Helm Values.

dreadnode-api:
config:
opensandbox:
litellmUrl: https://litellm.sandbox.example/v1

Use a URL reachable from the runtime pods. Pointing a remote OpenSandbox runtime at an in-cluster Service name from the Dreadnode cluster will fail DNS resolution.

Point *.sandbox.dreadnode.example.com at the same ingress endpoint as the platform, then configure the base authority without *.. If you leave it empty, Dreadnode derives sandbox.<platform-domain>.

global:
domain: dreadnode.example.com
scheme: https
tls:
secretName: dreadnode-tls
sandbox:
domain: sandbox.dreadnode.example.com
tls:
secretName: dreadnode-sandbox-tls

Create the TLS Secret in the Dreadnode namespace before deployment. Its certificate needs the single wildcard SAN *.sandbox.dreadnode.example.com; each runtime uses one label beneath that authority, such as https://<sandbox-id>-8787.sandbox.dreadnode.example.com. You may reuse the platform TLS Secret when its certificate already includes this SAN.

The gateway routes stable Host names without signing them. Dreadnode protects runtime HTTP endpoints with the runtime bearer token and browser WebSocket handshakes with short-lived, single-use tickets. The OpenSandbox lifecycle API stays on its authenticated ClusterIP Service and is not exposed by the wildcard Ingress.

Each sandbox pod requests 0.25 CPU and 512 MiB by default. The runtime’s configured 2 CPU and 2 GiB remain pod limits, so idle interactive sessions reserve modest capacity but can burst when they are active. These dynamic pod requests are separate from the Resource Preset, which sizes the always-on platform components.

Requests decide how many sandboxes fit on a node. The small preset reserves about 1.76 CPU and 5.1 GiB for always-on components, so subtract that and the kubelet’s own reservation from node allocatable, then divide by the per-sandbox request:

NodeFree after platformSandboxes by CPUSandboxes by memoryEffective limit
8 vCPU / 16 GiB~5.7 CPU, ~8.9 GiB~22~17~17 (memory)
4 vCPU / 32 GiB~1.8 CPU, ~25 GiB~7~50~7 (CPU)

Which resource binds depends on the node’s CPU-to-memory ratio, so check both before assuming a larger box helps.

CPU and memory requests are not equally safe to lower. CPU is compressible: a pod above its request is throttled in proportion to that request, and nothing is killed. Memory is not. A pod above its memory request is a candidate for eviction under node memory pressure, and a pod at its memory limit is OOM-killed. Lower the CPU request to raise density; leave the memory request alone unless you have measured the workload.

dreadnode-api:
config:
opensandbox:
resources:
requests:
cpuCores: 0.25
memoryMb: 512

Raise requests when measurements show a sandbox needs guaranteed capacity. Lower them only when the node is constrained and the workload can tolerate contention. If provisioning times out, see Runtime provisioning returns a scheduling timeout.

Set global.caBundle. Sandbox runtimes receive the same bundle as the API and the bundled LiteLLM proxy, so a private CA needs no custom image:

global:
caBundle:
enabled: true
source: secret
name: corporate-ca
key: ca.crt

A sandbox cannot mount a cluster secret — no sandbox provider exposes a secret volume backend — so the API passes the bundle in bounded, numbered environment chunks. This avoids Linux’s per-value process environment limit for combined public/private bundles. A CA certificate is a public key, so this discloses nothing a mounted file would not.

The runtime rejoins the chunks, merges your bundle with the image’s own system roots, and writes the result to /tmp/dreadnode-ca-bundle.pem, so public TLS keeps working.

Every trust mechanism in the image is pointed at that file from the sandbox’s own environment, not by the runtime process: SSL_CERT_FILE, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE, GIT_SSL_CAINFO, NODE_EXTRA_CA_CERTS, AWS_CA_BUNDLE and GRPC_DEFAULT_SSL_ROOTS_FILE_PATH, plus UV_NATIVE_TLSuv uses its own vendored roots unless told otherwise, and so would keep failing behind an intercepting proxy with everything else configured correctly. Declaring them in the container is what makes curl, wget and an agent’s own scripts inherit your CA, rather than only the processes the runtime happens to start. The image ships that path pre-populated with its stock public roots, so the variables point at a usable bundle from the moment the sandbox starts.

Verify from a running sandbox:

Start a runtime, then find its pod — the OpenSandbox controller names sandbox pods for the sandbox ID, so they are the ones that carry no release name — and check trust from inside it:

Terminal window
kubectl get pods -n $NAMESPACE
kubectl exec -n $NAMESPACE <sandbox-pod> -c sandbox -- \
python3 -c "import ssl; print(len(ssl.create_default_context().get_ca_certs()))"
kubectl exec -n $NAMESPACE <sandbox-pod> -c sandbox -- \
curl -sSf https://your-internal-service.acme.internal >/dev/null && echo OK

The count should exceed the image’s stock root count by the number of certificates in your bundle. Run the same two commands with bash -lc in front of them and the result must not change: the trust comes from the container environment, so it does not depend on which shell reaches it.

Only needed when the runtime needs more than trust — extra tooling, a preinstalled dataset, an internal package your capabilities import. global.caBundle covers certificates on its own, and a wrapper image means you own upgrades: the runtime image otherwise tracks the chart’s appVersion automatically, and pinning it stops that, so the runtime SDK will drift from the API unless you rebuild on each release.

The runtime image reference is per-install, so read the authoritative value out of the API ConfigMap rather than composing one by hand:

Terminal window
export NAMESPACE=dreadnode # kotsadm for Embedded Cluster
RUNTIME_IMAGE=$(kubectl -n "$NAMESPACE" get cm \
-l app.kubernetes.io/name=dreadnode-api \
-o jsonpath='{.items[0].data.DOCKER_RUNTIME_IMAGE}')
echo "$RUNTIME_IMAGE"
# proxy.enterprise.dreadnode.io/proxy/dreadnode/index.docker.io/dreadnode/dreadnode:<version>

Self-hosted installs pull that image through proxy.enterprise.dreadnode.io, authenticated by your license. docker.io/dreadnode/dreadnode is not anonymously pullable — a build host that reaches for it gets a 401. Channels apply to charts and releases, not to images: there is no beta or unstable path for an image reference the way there is for oci://registry.replicated.com/dreadnode/beta/dreadnode.

Your cluster already holds the credential the build host needs, in enterprise-pull-secret:

Terminal window
REGISTRY="${RUNTIME_IMAGE%%/*}"
AUTH=$(kubectl -n "$NAMESPACE" get secret enterprise-pull-secret \
-o jsonpath='{.data.\.dockerconfigjson}' | base64 -d |
jq -r --arg registry "$REGISTRY" '.auths[$registry].auth' | base64 -d)
printf '%s' "${AUTH#*:}" |
docker login "$REGISTRY" --username "${AUTH%%:*}" --password-stdin

An air-gapped install that mirrors images into its own registry has no enterprise-pull-secret. Build from your mirror’s copy of the runtime image and authenticate to that registry instead.

ARG RUNTIME_IMAGE
FROM ${RUNTIME_IMAGE}
USER root
RUN apt-get update && apt-get install -y --no-install-recommends your-tooling && \
rm -rf /var/lib/apt/lists/*
USER user

Do not install certificates here. global.caBundle reaches the runtime at boot and applies to every sandbox without rebuilding anything, and a wrapper that also carries a CA gives you two sources of trust to keep in step.

Build and publish to a registry the OpenSandbox nodes can pull. Carry the Dreadnode version through to the wrapper’s tag so the two never drift:

Terminal window
VERSION="${RUNTIME_IMAGE##*:}"
WRAPPER_IMAGE="registry.acme.internal/dreadnode/runtime-private-ca:$VERSION"
docker build \
--build-arg RUNTIME_IMAGE="$RUNTIME_IMAGE" \
-t "$WRAPPER_IMAGE" \
.
docker push "$WRAPPER_IMAGE"

Substitute the tag you just pushed for <version>. No dedicated Admin Console field sets dockerRuntimeImage, so both Embedded Cluster paths route through Advanced Helm Values, which Dreadnode merges after the dedicated fields and which therefore overrides them.

dreadnode-api:
config:
dockerRuntimeImage: registry.acme.internal/dreadnode/runtime-private-ca:<version>

Pinning dockerRuntimeImage opts the runtime out of automatic version tracking. Without an override, each release ships the matching runtime image and an upgrade moves it for you; with one, the runtime stays where you put it. Rebuild and repoint the wrapper on every upgrade so the API and runtime SDK stay on the same release commit.

Give sandbox pods a credential for your registry

Section titled “Give sandbox pods a credential for your registry”

OpenSandbox creates sandbox pods dynamically, and they carry only the pull secrets the BatchSandbox template injects — by default just enterprise-pull-secret, which authenticates to the Dreadnode proxy and nothing else. A dockerRuntimeImage on your own registry needs your own credential in global.imagePullSecrets. No dedicated Admin Console field sets it either, so both Embedded Cluster paths route through Advanced Helm Values again.

Create the registry credential in the Dreadnode namespace:

Terminal window
kubectl -n "$NAMESPACE" create secret docker-registry acme-registry \
--docker-server=registry.acme.internal \
--docker-username=<username> \
--docker-password=<password> \
--dry-run=client -o yaml | kubectl apply -f -

Then reference it in your values overlay:

global:
imagePullSecrets:
- acme-registry

Skip this and sandbox pods land in ImagePullBackOff while the rest of the platform stays healthy. See Pods in ImagePullBackOff.

Every sandbox pod also runs a second image, opensandbox-execd, which the sandbox server injects and which the wrapper does not replace. A mirroring install needs both:

Terminal window
kubectl -n "$NAMESPACE" get cm \
-l app.kubernetes.io/name=dreadnode-sandbox-server \
-o jsonpath='{.items[0].data.config\.toml}' | grep execd_image
# execd_image = "proxy.enterprise.dreadnode.io/proxy/dreadnode/index.docker.io/dreadnode/opensandbox-execd:<version>"

The Dreadnode runtime’s Python paths — platform API, model gateway, and trace exporter — read the image’s OS trust store, which update-ca-certificates updates. The runtime image also ships Node 22, Bun, and the Claude Code CLI for JavaScript MCP servers and the claude-code agent engine. Those maintain their own CA list and ignore the OS store entirely, which is why the recipe sets NODE_EXTRA_CA_CERTS. Behind a TLS-inspecting proxy, an image built without it passes Python traffic and fails every JavaScript path.

CA rotation requires a new image build and tag, a chart upgrade, and recreation of affected runtime sandboxes. This recipe does not configure arbitrary task images, E2B templates, dynamic CA injection, or provider-mounted CA files. See Trust an internal certificate for the host TUI, CLI, and SDK setup. Use Outbound TLS trust separately for the platform API and bundled LiteLLM pods.

E2B requires outbound access to api.e2b.dev and *.e2b.app. Create an API key in your E2B team and enable custom template builds for the account.

Configure E2B and its API key:

Store the API key in the Dreadnode namespace:

Terminal window
kubectl -n "$NAMESPACE" create secret generic e2b-credentials \
--from-literal=E2B_API_KEY='<your-e2b-key>'

Then select E2B and reference the Secret:

dreadnode-api:
config:
sandboxProvider: e2b
e2b:
apiKey:
existingSecret: e2b-credentials
secretKey: E2B_API_KEY
dreadnode-sandbox-controller:
enabled: false
dreadnode-sandbox-server:
enabled: false

The Embedded Cluster paths protect the key in stored configuration, then inject it as a plain environment variable on the API Deployment. Anyone who can read that Deployment can read the key. Use the Helm path with a pre-created Secret when that exposure is unacceptable. Disabling the on-cluster subcharts in Helm is optional, but reclaims their resources.

The API validates the E2B configuration at startup: it authenticates the key, checks the template aliases, logs any configuration errors, and gives up after five seconds so the API can finish starting. Check the API logs after switching providers:

Terminal window
kubectl -n "$NAMESPACE" logs deploy/dreadnode-api

E2B sandboxes and the E2B control plane call back into Dreadnode for:

  • agent runtime reporting
  • model inference through LiteLLM, when enabled
  • lifecycle webhooks at /api/v1/webhooks/e2b

These URLs must be reachable from E2B’s cloud. The defaults use the configured Dreadnode domain and work only when that domain is publicly reachable. For an internal-only deployment, expose a restricted tunnel, bastion, or reverse proxy and configure both callback URLs:

dreadnode-api:
config:
e2b:
serverUrl: https://dreadnode-bastion.example.com
litellm:
publicUrl: https://dreadnode-bastion.example.com/llm/v1

Leave serverUrl empty when the configured Dreadnode domain is already reachable from E2B. The API registers the lifecycle webhook beneath the same URL; it has no separate setting.

Evaluations without an explicit task timeout run for up to one hour by default. Explicit timeouts can run for up to the configured maximum, which defaults to eight hours:

dreadnode-api:
config:
sandboxRuntime:
defaultRuntimeSeconds: 3600
maxRuntimeSeconds: 28800

Set the default to at least 60 seconds and no higher than the maximum. The API rejects an invalid pair at startup. These values apply to both providers. E2B also enforces your account’s session limit. Raise the default if long unattended evaluations are being cut short.

Dreadnode extends published base templates for task and interactive runtime sandboxes:

dreadnode-api:
config:
e2b:
taskTemplateAlias: brian-3740/dn-task-prod
runtimeTemplateAlias: brian-3740/dn-dreadnode-prod

Override these aliases only when you maintain compatible forks of the base templates. The first evaluation for a task builds a team-local template and is slower than later runs, which reuse it. E2B bills runtime and template builds to your account, and Dreadnode does not automatically prune accumulated templates.