<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Kernflux]]></title><description><![CDATA[Kernflux]]></description><link>https://kernflux.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 01 Nov 2026 20:44:25 GMT</lastBuildDate><atom:link href="https://kernflux.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Self-Hosting an AI Agent Sandbox with OpenDsec: A Single-Node Walkthrough]]></title><description><![CDATA[An AI agent that writes code needs somewhere to run it. A hosted sandbox service is the quick answer, but sometimes the code, the data or the bill has to…]]></description><link>https://kernflux.hashnode.dev/self-hosting-an-ai-agent-sandbox-with-opendsec-a-single-node-walkthrough</link><guid isPermaLink="true">https://kernflux.hashnode.dev/self-hosting-an-ai-agent-sandbox-with-opendsec-a-single-node-walkthrough</guid><dc:creator><![CDATA[zhou yibo]]></dc:creator><pubDate>Sun, 01 Nov 2026 08:08:42 GMT</pubDate><content:encoded><![CDATA[<p>An AI agent that writes code needs somewhere to run it. A hosted sandbox service is the quick answer, but sometimes the code, the data or the bill has to stay on your own hardware. This walkthrough sets up <a href="https://github.com/Kernflux/opendsec">OpenDsec</a>, an open-source sandbox platform, on one Linux machine, and then goes through what an agent would actually do with it: run code, move files, reach (or fail to reach) the network, pause, fork, snapshot, and call it through E2B's SDK.</p>
<p>Everything below was run for this post on one bare-metal server (384 threads, Ubuntu 26.04), and every output is copied from that run, with noise such as pip's upgrade notice left out. Timings are what the client measured, network and TLS included. They move by tens of milliseconds from one run to the next; an earlier run of the same program forked in 0.23 s instead of the 0.47 s below.</p>
<h2>What you need</h2>
<ul>
<li>A Linux x86_64 machine with systemd and cgroup v2. The project tests on Ubuntu 24.04; this run used 26.04.</li>
<li><code>/dev/kvm</code> for Firecracker microVMs, which is what you want for untrusted code. Bare metal works, and so does a cloud VM with nested virtualization. Without KVM the installer sets up containers only.</li>
<li>Root. The installer sets up system services.</li>
<li>A few GB of free disk. If you can, put the sandbox state on a local NVMe disk formatted as XFS (more on why in the fork section).</li>
</ul>
<h2>Step 1: install</h2>
<pre><code class="language-sh">sudo -i
apt update &amp;&amp; apt install -y make git python3-venv
curl -fsSL https://go.dev/dl/go1.27.3.linux-amd64.tar.gz | tar -C /usr/local -xz
git clone https://github.com/kernflux/opendsec &amp;&amp; cd opendsec
deploy/quickstart.sh
</code></pre>
<p>The script prints what it is doing as it goes:</p>
<pre><code>==&gt; checking this machine
==&gt; building
==&gt; writing the cluster config to /root/opendsec-ops
==&gt; installing (certificates, units, node settings)
==&gt; starting
==&gt; checking the image store
==&gt; importing docker.io/library/python:3.12 as root/py:3.12
==&gt; creating the project root/demo and its caller
==&gt; running a sandbox
  [ok] container sandbox ran python
  [ok] microvm sandbox ran python

OpenDsec is running on this machine.
</code></pre>
<p>It took 379 seconds here, most of it compiling. On an 8-core VM expect about 11 minutes.</p>
<p>What you have afterwards is a small but complete cluster, the same components a multi-machine deployment runs, as systemd services:</p>
<pre><code>opendsec-apiserver@0.service  active running
opendsec-e2b@0.service        active running
opendsec-edge.service         active running
opendsec-iam@0.service        active running
opendsec-watcher@0.service    active running
</code></pre>
<ul>
<li><strong>IAM</strong> holds projects, callers and tokens.</li>
<li><strong>apiserver</strong> is the API your code calls. It keeps no state of its own.</li>
<li><strong>watcher</strong> collects each node's capacity for placement.</li>
<li><strong>edge</strong> runs the sandboxes on this machine: Firecracker microVMs and containers.</li>
<li><strong>e2b</strong> serves E2B's API, for code written against E2B's SDK.</li>
</ul>
<p>The components talk to each other over mutual TLS with a CA the script created. <code>/root/opendsec-ops</code> holds that CA's private key and the admin token, so keep it private. The script also wrote two environment files there:</p>
<ul>
<li><code>env</code> for the admin;</li>
<li><code>demo.env</code> for a caller allowed to create sandboxes in the project <code>root/demo</code>.</li>
</ul>
<h2>Step 2: the Python SDK</h2>
<pre><code class="language-sh">python3 -m venv .venv &amp;&amp; .venv/bin/pip install ./sdk/python
. /root/opendsec-ops/demo.env     # sets OPENDSEC_ADDR, OPENDSEC_TOKEN, OPENDSEC_PROJECT, OPENDSEC_CA
</code></pre>
<p>The SDK is async. The rest of this post is one program, shown in pieces, all inside:</p>
<pre><code class="language-python">import asyncio
from opendsec import DSecClient, DSecRunArgs

async def main():
    async with DSecClient() as client:   # reads the OPENDSEC_* variables
        ...

asyncio.run(main())
</code></pre>
<h2>Step 3: create a microVM</h2>
<pre><code class="language-python">sb = await client.run_sandbox(DSecRunArgs(
    backend="microvm", image="root/py:3.12",
    cpu_cores_limit=1, memory_limit_mb=1024,
    ttl_running_stop=600,              # stop after 10 idle minutes
    network_rules={"pypi": True}))     # outbound traffic is denied unless a rule allows it
r = await sb.run_shell("python3 -V; uname -r; nproc; free -m | head -2")
print(r.stdout)
</code></pre>
<pre><code>[run_sandbox: 0.144s]
Python 3.12.16
6.1.167-opendsec
1
               total        used        free      shared  buff/cache   available
Mem:             985         132         905           0          34         852
</code></pre>
<p>0.144 s for the whole call: placement, booting a VM with its own Linux kernel, and the agent inside it answering. Inside, it is the <code>python:3.12</code> image from Docker Hub with one vCPU and 1 GB, as asked.</p>
<p>Always set <code>backend</code>. The default is <code>sim</code>, which only simulates and runs nothing.</p>
<h2>Step 4: files</h2>
<pre><code class="language-python">await sb.write_file("/tmp/hello.py", "import platform\nprint('hello from', platform.node())\n")
print((await sb.run_shell("python3 /tmp/hello.py")).stdout)
print(await sb.read_file("/tmp/hello.py"))
</code></pre>
<pre><code>hello from sb-wtm44fgei3hc
</code></pre>
<p>There is also <code>list_dir</code>, and <code>run_shell_background</code> for long-running commands.</p>
<h2>Step 5: the network is closed by default</h2>
<p>The sandbox was created with <code>network_rules={"pypi": True}</code>. So installing from PyPI works:</p>
<pre><code>$ pip install -q requests; python3 -c 'import requests; print(requests.__version__)'
2.34.2
</code></pre>
<p>and anything else does not:</p>
<pre><code>$ python3 -c "import requests; requests.get('https://example.com', timeout=5)"
requests.exceptions.ConnectionError: ... Failed to resolve 'example.com' ([Errno -2] Name or service not known)
</code></pre>
<p>The policy is enforced on the host with nftables and a DNS proxy. A name the rules do not allow does not resolve, and an address they do not allow is dropped. Rules can name domains, CIDRs and ports, for example <code>{"github.com:443": True}</code>, or <code>{"*": True}</code> to allow everything.</p>
<h2>Step 6: pause and resume keep memory</h2>
<p>To show that pausing keeps more than files, start a web server inside the sandbox. It lives only in memory:</p>
<pre><code>$ nohup python3 -c "import http.server as h; h.HTTPServer(('127.0.0.1', 8000), h.SimpleHTTPRequestHandler).serve_forever()" &gt;/dev/null 2&gt;&amp;1 &amp;
$ python3 -c "import urllib.request as u; print(u.urlopen('http://127.0.0.1:8000/').status)"
200
</code></pre>
<p>Then pause and resume:</p>
<pre><code class="language-python">await sb.pause()      # [pause: 0.163s]   state PAUSED
await sb.resume()     # [resume: 0.119s]
</code></pre>
<pre><code>server still up: 200
</code></pre>
<p>Pausing a microVM writes a memory snapshot and ends the VM process, so a paused sandbox holds no RAM. Resuming restores it, and the processes carry on where they were.</p>
<h2>Step 7: fork</h2>
<p><code>fork</code> copies a running sandbox, memory included:</p>
<pre><code class="language-python">forks = await sb.fork(count=3)        # [fork(count=3): 0.465s]
</code></pre>
<p>Every copy has the web server already running and the file written earlier:</p>
<pre><code>200
import platform
</code></pre>
<p>The copies are independent from then on. A file written in one copy:</p>
<pre><code>$ echo only-in-fork-0 &gt; /tmp/mark        # in forks[0]
</code></pre>
<p>is not in the source:</p>
<pre><code>$ cat /tmp/mark                          # in sb
cat: /tmp/mark: No such file or directory
</code></pre>
<p>The source is paused for about 10 ms while its state is captured. This is the operation RL rollouts and agent tree search want: bring an environment to an interesting state once, then branch from it several times.</p>
<p>How fast fork is depends on the file system under the sandbox state. Each copy starts from a copy of the memory file. On XFS, as here, that copy is a reflink and costs almost nothing. On ext4 it is a real copy, and forking takes about a second. The installer warns you if the state directory lacks reflink. It uses <code>/data</code> for the state if something is mounted there, so mount an XFS-formatted NVMe disk at <code>/data</code> before installing.</p>
<h2>Step 8: a memory snapshot as an image</h2>
<p>A snapshot turns the sandbox, memory and all, into an image other sandboxes can start from:</p>
<pre><code class="language-python">await sb.create_snapshot("root/demo/walk:1", memory=True)    # [create_snapshot: 0.088s]
for i in range(3):
    await client.run_sandbox(DSecRunArgs(backend="microvm", image="root/demo/walk:1",
                                         cpu_cores_limit=1, memory_limit_mb=1024))
</code></pre>
<pre><code>[run_sandbox from snapshot #1: 0.045s]
[run_sandbox from snapshot #2: 0.037s]
[run_sandbox from snapshot #3: 0.032s]
</code></pre>
<p>A sandbox started from the snapshot already has <code>requests</code> installed and the web server running:</p>
<pre><code>200
2.34.2
</code></pre>
<p>This is how you skip setup time. Install the dependencies, load the model, start the services, snapshot once, then start every sandbox from the snapshot, in 32 to 45 ms each here.</p>
<h2>Step 9: a container, same API</h2>
<p>For trusted work, or where start time and density matter most, use a container. The API stays the same:</p>
<pre><code class="language-python">c = await client.run_sandbox(DSecRunArgs(backend="container", image="root/py:3.12",
                                         cpu_cores_limit=1, memory_limit_mb=512))
</code></pre>
<pre><code>[run_sandbox container: 0.040s]
42
7.0.0-31-generic
</code></pre>
<p>The kernel version gives it away. The microVM ran its own 6.1 guest kernel; the container shares the host's 7.0 kernel. That shared kernel is why containers are the faster choice, and why they are the weaker boundary for code you did not write.</p>
<h2>Step 10: E2B's SDK</h2>
<p>If your agent already uses E2B, the same machine serves E2B's API on port 7400, and <code>demo.env</code> already set <code>E2B_API_URL</code>, <code>E2B_SANDBOX_URL</code> and <code>E2B_API_KEY</code> for it. The key is your OpenDsec token written as <code>e2b_</code> and hex, because several versions of E2B's Python SDK (2.25 to 2.45) refuse any other form before sending a request. The gateway decodes it back into the token. For a token of your own:</p>
<pre><code class="language-sh">export E2B_API_KEY=e2b_$(printf %s "$OPENDSEC_TOKEN" | od -An -tx1 -v | tr -d ' \n')
</code></pre>
<p>Then install the SDK:</p>
<pre><code class="language-sh">.venv/bin/pip install e2b
</code></pre>
<pre><code class="language-python">from e2b import Sandbox

sbx = Sandbox.create()                                   # [Sandbox.create: 0.137s]
print(sbx.commands.run("python3 -c 'print(6*7)'").stdout)   # 42
sbx.files.write("/tmp/a.txt", "hello from e2b")
print(sbx.files.read("/tmp/a.txt"))                      # hello from e2b
sbx.kill()
</code></pre>
<p>That is unchanged E2B code. Fork, snapshots and pause with auto-resume work too, and so does the code interpreter once you build its template. The <a href="https://kernflux.github.io/en/docs/e2b">E2B compatibility page</a> lists what does not, such as directory watching and volumes.</p>
<h2>Step 11: look at it as the admin</h2>
<pre><code class="language-sh">. /root/opendsec-ops/env
opendsecctl status
</code></pre>
<p>prints the cluster as the scheduler sees it. For each node it shows whether it is healthy, the backends it offers (<code>microvm</code>, <code>container</code>), its capacity (here 3072 schedulable CPU cores, the 384 threads with the default 8Ã— CPU overcommit; 1.5 TB of memory; up to 1000 microVMs), and what is in use. <code>opendsecctl --help</code> lists the rest: projects and quotas, callers and tokens, images, sandboxes, and pausing a whole task at once.</p>
<h2>Cleaning up</h2>
<p>Every sandbox above was stopped at the end of the program. They would also have stopped on their own after the 10-minute idle timeout. To remove OpenDsec from the machine:</p>
<pre><code class="language-sh">deploy/quickstart.sh --uninstall
</code></pre>
<p>It removes the services, files and network settings it created. A disk mounted for the state through <code>/etc/fstab</code> is emptied but stays mounted.</p>
<h2>Where to go from here</h2>
<ul>
<li><strong>More machines.</strong> The single node is a real cluster with one member. The <a href="https://kernflux.github.io/en/docs/cluster">cluster guide</a> adds machines, a shared image store and a load balancer. Upgrades roll through the cluster without stopping running sandboxes.</li>
<li><strong>No internet.</strong> An <a href="https://github.com/Kernflux/opendsec/tree/main/deploy/offline">offline bundle</a> installs the same thing on machines with no internet access.</li>
<li><strong>Your own images.</strong> <code>opendsecctl image import</code> pulls any OCI image. <code>pack_diff</code> (containers) and <code>create_snapshot</code> (microVMs) turn a prepared sandbox into a new one.</li>
</ul>
<p>OpenDsec is 0.x, AGPL-3.0 for the servers and Apache-2.0 for the SDKs. It is an independent implementation of the design in DeepSeek's <a href="https://arxiv.org/abs/2609.22978">DSec paper</a>, not affiliated with DeepSeek. The code is at <a href="https://github.com/Kernflux/opendsec">github.com/Kernflux/opendsec</a>. If something in this walkthrough does not work on your machine, an issue there is the fastest way to get it fixed.</p>
]]></content:encoded></item><item><title><![CDATA[What State Should an AI Agent Sandbox Preserve?]]></title><description><![CDATA[Files, memory, and external side effects need different boundaries. How OpenDsec snapshots work, and what to test before branching an AI agent.]]></description><link>https://kernflux.hashnode.dev/what-state-should-an-ai-agent-sandbox-preserve</link><guid isPermaLink="true">https://kernflux.hashnode.dev/what-state-should-an-ai-agent-sandbox-preserve</guid><category><![CDATA[ai agents]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[zhou yibo]]></dc:creator><pubDate>Fri, 30 Oct 2026 14:50:02 GMT</pubDate><content:encoded><![CDATA[<p>An agent has installed its dependencies, loaded a dataset into Python, and started a local service. Now you want to try two different next steps.</p>
<p>Copying the working directory gives both attempts the same files. It does not give them the same Python heap, running service, or execution position. Replaying the setup might work, but that is a different operation from continuing an existing execution.</p>
<p>This is one of the problems behind OpenDsec, the self-hosted sandbox project I’m building for AI agents and RL rollouts. The useful question is not just “can I snapshot it?” It is <strong>“which state does that snapshot actually preserve?”</strong></p>
<h2>There are at least three kinds of state</h2>
<h3>1. Filesystem state</h3>
<p>Source code, installed packages, generated files, and local configuration live here. A files-only image is often exactly what you want: build an environment once, then start fresh processes for every attempt.</p>
<p>For an agent that edits a repository and runs tests, this may be sufficient. There is no reason to capture a live process if recreating it is cheap and reliable.</p>
<p>But a saved directory is not a saved interpreter. A variable created in a long-running Python session does not become a file just because the filesystem was copied.</p>
<h3>2. Runtime state</h3>
<p>This includes process memory and the execution state needed to continue running: loaded data, initialized libraries, in-memory caches, and live local processes.</p>
<p>Suppose a worker has already parsed a large dataset. A filesystem snapshot can preserve the dataset file. A memory-aware snapshot can preserve the worker after parsing it. Those starting points have different costs and different semantics.</p>
<p>That distinction matters when branching an execution. “Start two new environments with these files” is not the same promise as “continue twice from this point.”</p>
<h3>3. External state</h3>
<p>A remote database, an API request, or a message already sent to another service lives outside the sandbox’s snapshot boundary.</p>
<p>Restoring local memory does not undo a database commit. Forking an agent does not create two independent copies of a payment service. Even if local socket state is captured, the remote peer has not necessarily paused or been cloned with it.</p>
<p><strong>A sandbox snapshot is not a transaction over the outside world.</strong> Applications still need reconnect logic, idempotency keys, isolated test services, or explicit checkpoints around external side effects.</p>
<h2>How this maps to OpenDsec</h2>
<p>OpenDsec has multiple execution backends, and they do not all make the same snapshot promise. According to the current <a href="https://kernflux.github.io/en/docs/concepts">concepts documentation</a> and <a href="https://kernflux.github.io/en/docs/images">images and snapshots guide</a>:</p>
<ul>
<li><strong>microvm</strong> uses Firecracker and supports snapshots and forks with memory. A sandbox created from a memory snapshot resumes with its processes running.</li>
<li><strong>container</strong> supports files-only snapshots and forks. It does not preserve a running Python heap through those operations.</li>
<li><strong>fullvm</strong> uses QEMU, but the current backend capability table does not list fork or snapshot support. Having a VM backend does not automatically mean every lifecycle operation is implemented.</li>
</ul>
<p>There is also a useful distinction between <code>pack_diff</code> and <code>create_snapshot</code>.</p>
<p><code>pack_diff</code> builds a reusable image layer from filesystem changes on supported container backends. Think “dependencies installed, repository prepared.” Starting from that image starts fresh processes.</p>
<p>A microVM snapshot with memory is for a different starting point: “the environment is prepared and the processes are already initialized.” The docs expose that choice explicitly:</p>
<pre><code class="language-python"># Fragment: sb is an existing, initialized microVM sandbox.
# Connection, image import, and sandbox creation are omitted.
img = await sb.create_snapshot("root/rl/warm:v1", memory=True)
</code></pre>
<p>This is a documented API fragment, not a benchmark or a complete runnable example. The linked snapshot guide covers setup and creating a sandbox from the saved image.</p>
<h2>Where should an agent branch?</h2>
<p>Consider a coding agent with this workflow:</p>
<ol>
<li>Prepare a repository and install dependencies.</li>
<li>Start a local helper service and initialize a persistent worker.</li>
<li>Try one of several candidate patches.</li>
<li>Run tests and compare results.</li>
</ol>
<p>If step 2 is cheap, a files-only image after step 1 may be the simplest choice. Each candidate gets new processes and a clean startup.</p>
<p>If step 2 is expensive, or the worker contains useful state that is awkward to serialize, a memory-aware branch after step 2 becomes worth evaluating.</p>
<p>I would still place the branch <strong>before</strong> sending writes to shared external services. If each candidate talks to the same mutable database, the experiments are no longer independent just because their local files and memory are isolated.</p>
<p>A useful checkpoint is therefore an application decision, not merely a runtime feature. It should define both what is captured and what each branch is allowed to affect.</p>
<h2>What I would test before relying on it</h2>
<p>A successful restore is only the first test. For a real agent workload, I would check:</p>
<ul>
<li><strong>Memory continuity:</strong> does an in-memory value survive without rerunning initialization?</li>
<li><strong>File continuity:</strong> are the expected files present, and do writes in one branch stay out of another?</li>
<li><strong>Process continuity:</strong> is the local service actually ready after restore, rather than merely having a process entry?</li>
<li><strong>External behavior:</strong> what happens to open connections, expired credentials, and requests that were in flight?</li>
<li><strong>Repeatability:</strong> do clocks, random-number state, and concurrent activity affect the comparison between branches?</li>
<li><strong>Resource use:</strong> how much memory and storage do several active branches consume under the actual workload?</li>
</ul>
<p>These are evaluation questions, not a claim that every application can be resumed transparently. Preserving more state can avoid repeated setup, but it also preserves things you might prefer to reset, including cached credentials or stale connections. Treat memory snapshots as sensitive artifacts, not just convenient images.</p>
<h2>Choose the smallest state boundary that works</h2>
<p>For short, stateless tasks, a fresh environment is often easier to reason about. For repository edits, a files-only image may be enough. For long-lived workers and branching agent execution, preserving runtime state can make a meaningful difference.</p>
<p>The important part is making that boundary explicit. “Snapshot supported” is too vague unless you know whether it means files, memory, or both—and what remains outside it.</p>
<p>If you want to evaluate this with OpenDsec:</p>
<ul>
<li><a href="https://kernflux.github.io/en/docs/quickstart">Start with the English quickstart</a>.</li>
<li><a href="https://kernflux.github.io/en/docs/images">Read the snapshot semantics and examples</a>.</li>
<li><a href="https://github.com/Kernflux/opendsec">Inspect the code or open an issue on GitHub</a>.</li>
</ul>
<p>The microVM backend requires access to <code>/dev/kvm</code>; a container-only setup will not demonstrate memory-aware snapshots.</p>
<p>For your agent workload, what is hardest to recreate: the files, the live process state, or the external dependencies?</p>
<hr />
<p><em>Disclosure: I’m building OpenDsec. This article was prepared with AI assistance and checked against the linked project documentation; it does not report a new benchmark.</em></p>
]]></content:encoded></item></channel></rss>