<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Padmaja Kotoky</title>
    <description>The latest articles on DEV Community by Padmaja Kotoky (@xenaarchdev).</description>
    <link>https://dev.to/xenaarchdev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4093712%2Fe86e988f-a80b-4fe3-be3b-f53aa418156b.jpg</url>
      <title>DEV Community: Padmaja Kotoky</title>
      <link>https://dev.to/xenaarchdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xenaarchdev"/>
    <language>en</language>
    <item>
      <title>xenarchos: a confirmed, sandboxed, auditable skill runner built on two open-source dependencies</title>
      <dc:creator>Padmaja Kotoky</dc:creator>
      <pubDate>Tue, 25 Aug 2026 08:21:56 +0000</pubDate>
      <link>https://dev.to/xenaarchdev/xenarchos-a-confirmed-sandboxed-auditable-skill-runner-built-on-two-open-source-dependencies-efp</link>
      <guid>https://dev.to/xenaarchdev/xenarchos-a-confirmed-sandboxed-auditable-skill-runner-built-on-two-open-source-dependencies-efp</guid>
      <description>&lt;p&gt;Most agent frameworks conflate three separate problems: what the agent does, whether it's allowed to do it, and whether it did it correctly. xenarchos separates these explicitly — using two already-shipped tools to handle the second and third, so the project itself only has to solve the first.&lt;/p&gt;

&lt;p&gt;What it is&lt;/p&gt;

&lt;p&gt;xenarchos runs YAML-defined skills: ordered lists of shell commands that execute through a confirm gate, confined by xenarch-guard, and recorded as agentkit run artifacts.&lt;/p&gt;

&lt;p&gt;bash&lt;br&gt;
xenarchos init myskills&lt;br&gt;
xenarchos run myskills/setup.yaml --out runs/baseline.json&lt;/p&gt;

&lt;h1&gt;
  
  
  edit the skill or let the environment drift
&lt;/h1&gt;

&lt;p&gt;xenarchos run myskills/setup.yaml --out runs/new.json&lt;br&gt;
agentkit diff runs/baseline.json runs/new.json&lt;/p&gt;

&lt;p&gt;agentkit diff is not xenarchos code. It's agentkit's own diff command, running unmodified against xenarchos's output. That's the point.&lt;/p&gt;

&lt;p&gt;A skill file&lt;br&gt;
yaml&lt;br&gt;
name: backup-notes&lt;br&gt;
default_timeout_s: 30&lt;br&gt;
steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;id: create-backup&lt;br&gt;
command: "cp -r ./notes ./backup"&lt;br&gt;
guard:&lt;br&gt;
  allow_read: [./notes]&lt;br&gt;
  allow_write: [.]&lt;br&gt;
target: 0&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;id: verify-backup&lt;br&gt;
command: "diff -r ./notes ./backup"&lt;br&gt;
guard:&lt;br&gt;
  allow_read: [./notes, ./backup]&lt;br&gt;
target: 0&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each step declares exactly what filesystem access it needs. xenarch-guard enforces that at the kernel level — the command cannot reach anything not listed, regardless of what it tries.&lt;/p&gt;

&lt;p&gt;The confirm gate&lt;/p&gt;

&lt;p&gt;Before each step runs (unless --yes), xenarchos prints the resolved command and its guard policy and requires a typed y:&lt;/p&gt;

&lt;p&gt;[xenarchos] step 1/2: create-backup&lt;br&gt;
  command:  cp -r ./notes ./backup&lt;br&gt;
  guard:    allow-read: ./notes&lt;br&gt;
            allow-write: .&lt;br&gt;
            network: connect blocked, bind blocked&lt;br&gt;
Run this step? [y/N]&lt;/p&gt;

&lt;p&gt;Bare Enter defaults to no — security-tool convention, not general CLI convention. Declining stops the walk immediately: the remaining steps never run and never appear in the written artifact. --yes skips the prompt but still prints every step's command and policy before it runs, so an unattended run leaves the same audit trail an attended one does.&lt;/p&gt;

&lt;p&gt;How the two dependencies are actually used&lt;/p&gt;

&lt;p&gt;xenarch-guard is invoked as a subprocess wrapping each confirmed step:&lt;/p&gt;

&lt;p&gt;xenarch-guard --allow-read ./notes --allow-write . -- /bin/sh -c "cp -r ./notes ./backup"&lt;/p&gt;

&lt;p&gt;xenarchos never touches Landlock or seccomp itself. It builds the argv from the skill's guard: block and runs it. The exit code it observes is the step's real exit code — xenarch-guard execve()s into the command rather than forking, so there's no translation layer.&lt;/p&gt;

&lt;p&gt;agentkit is used as a Python library, not a CLI. xenarchos constructs agentkit.Suite and agentkit.Task objects directly from the parsed YAML and calls agentkit.run_suite() — subprocess isolation, per-task timeout enforcement, and scoring all come from agentkit unchanged. The run artifact xenarchos writes is a byte-for-byte valid agentkit Run — agentkit diff and agentkit view work against it with zero xenarchos-specific code.&lt;/p&gt;

&lt;p&gt;One correctness problem worth naming&lt;/p&gt;

&lt;p&gt;agentkit's task filter is if task_ids: — a falsy check. Passing an empty list (every step declined) is treated as "no filter, run everything." This is the opposite of the intended behavior. The fix: when confirmed_ids is empty, xenarchos builds an empty Run directly rather than calling run_suite at all. This only shows up by actually reading agentkit's filter logic, not by reading its README.&lt;/p&gt;

&lt;p&gt;Timeout is layered, deliberately&lt;/p&gt;

&lt;p&gt;agentkit kills the adapter's subprocess on timeout. That subprocess is xenarchos's adapter, which started xenarch-guard as a child. Killing the adapter doesn't kill xenarch-guard's process tree — those orphan and keep running. xenarchos closes this gap by running the guard command in its own process group (start_new_session=True) and installing a SIGTERM handler that calls os.killpg before the adapter process dies. When agentkit tries to terminate the adapter, the handler kills the whole guarded process tree first.&lt;/p&gt;

&lt;p&gt;What's explicitly not in v0.1&lt;/p&gt;

&lt;p&gt;No persistent context store — agentkit's run.json is already the history. No scheduled or unattended execution — that's v0.2, and only after the confirm-gate loop has been proven with a human first. No natural-language intent resolution — steps are literal commands, same reasoning agentkit uses for not bundling an agent.&lt;/p&gt;

&lt;p&gt;Result&lt;/p&gt;

&lt;p&gt;17/17 tests passing, including three integration tests against a real xenarch-guard binary that verify confinement is load-bearing (not decorative), timeout kills the whole process tree, and agentkit diff catches a real regression. The flagship test: a step with allow_write: [./workspace] that attempts to write outside that path fails when guard is enforced and passes with --unsafe-no-guard — proving confinement, not the command, changed the outcome.&lt;/p&gt;

&lt;p&gt;github.com/xenaarch-dev/xenarchos&lt;/p&gt;

</description>
      <category>python</category>
      <category>linux</category>
      <category>agents</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building a framework-agnostic eval harness for LLM agents</title>
      <dc:creator>Padmaja Kotoky</dc:creator>
      <pubDate>Tue, 25 Aug 2026 08:17:28 +0000</pubDate>
      <link>https://dev.to/xenaarchdev/building-a-framework-agnostic-eval-harness-for-llm-agents-f5j</link>
      <guid>https://dev.to/xenaarchdev/building-a-framework-agnostic-eval-harness-for-llm-agents-f5j</guid>
      <description>&lt;p&gt;Agent evaluation tooling is surprisingly thin. Most frameworks ship a way to run agents but not a rigorous way to tell whether they're getting better or worse across changes. You end up eyeballing outputs, or writing one-off scripts that don't compose, or skipping evals entirely because the setup cost is too high.&lt;/p&gt;

&lt;p&gt;agentkit is my answer to this: a lightweight eval harness that works with any agent framework, records every run as a structured artifact, and lets you diff two runs to catch regressions automatically.&lt;/p&gt;

&lt;p&gt;The core idea&lt;br&gt;
bash&lt;br&gt;
agentkit run --suite tasks/ --adapter my_agent.adapter:run --out baseline.json&lt;/p&gt;

&lt;h1&gt;
  
  
  change something
&lt;/h1&gt;

&lt;p&gt;agentkit run --suite tasks/ --adapter my_agent.adapter:run --out new.json&lt;br&gt;
agentkit diff baseline.json new.json&lt;/p&gt;

&lt;p&gt;diff exits 0 if nothing regressed, exits 1 if anything did. That's a CI check. You don't need a hosted dashboard or a proprietary eval platform — just a JSON file and a diff command.&lt;/p&gt;

&lt;p&gt;Design decisions worth explaining&lt;/p&gt;

&lt;p&gt;No bundled agent. agentkit has no opinion about what model you use, what framework you use, or how your agent works. You supply an adapter — a single Python function that takes a Task and returns an AdapterOutput. That's the entire interface. This keeps the harness honest: it measures whatever you bring to it, not a strawman it controls.&lt;/p&gt;

&lt;p&gt;Subprocess isolation per task. Each task runs in its own spawned subprocess with a timeout. A hung task gets killed; it doesn't hang the whole suite. This matters in practice — LLM calls timeout, tool calls block, network calls fail. An eval harness that can't survive a single bad task isn't useful.&lt;/p&gt;

&lt;p&gt;Scoring is explicit, not implicit. Every task names a scorer — exact_match, contains, regex, or your own. The scorer sees the task's target field and the adapter's output and returns a Score. Nothing is inferred from output shape. This means you can score the same output multiple ways and the scores are always reproducible from the artifact alone.&lt;/p&gt;

&lt;p&gt;The run artifact is the source of truth. agentkit run writes a run.json that contains every task, every output, every score, and every trace. agentkit view renders it. agentkit diff compares two of them. Nothing is held in a database or a server — the file is the record.&lt;/p&gt;

&lt;p&gt;The diff output&lt;br&gt;
suite: my-agent-suite&lt;br&gt;
baseline: runs/baseline.json  (2026-08-01)&lt;br&gt;
new:      runs/new.json        (2026-08-02)&lt;/p&gt;

&lt;p&gt;task                  baseline    new         delta&lt;br&gt;
  ──────────────────────────────────────────────────&lt;br&gt;
  summarize-short       pass        pass        —&lt;br&gt;
  summarize-long        pass        FAIL        regressed&lt;br&gt;
  extract-entities      fail        fail        —&lt;br&gt;
  classify-sentiment    fail        pass        improved&lt;/p&gt;

&lt;p&gt;1 regressed, 1 improved, 2 unchanged&lt;br&gt;
exit 1&lt;/p&gt;

&lt;p&gt;Exit code 1 means something regressed. Pipe it into CI and you catch regressions before they ship.&lt;/p&gt;

&lt;p&gt;Two design problems I had to solve&lt;/p&gt;

&lt;p&gt;Timeout that actually kills the process tree. subprocess.terminate() kills the adapter process but not its children — if the adapter spawned a tool call subprocess, that orphan keeps running. The fix: each adapter subprocess runs in its own process group (start_new_session=True), and on timeout the harness kills the whole group via os.killpg. This came up again when building xenarchos on top of agentkit — the layered timeout problem is real, not theoretical.&lt;/p&gt;

&lt;p&gt;The task_ids=[] falsy check. agentkit's task filter is if task_ids: — an empty list is falsy, so passing task_ids=[] is treated as "no filter, run everything." This is the opposite of what you want when you've explicitly selected zero tasks (e.g., the user declined every step in a confirm gate). The fix is to route around run_suite entirely when the confirmed set is empty, building an empty Run directly. I found this while integrating xenarchos's confirm gate with agentkit's runner and the all-decline case silently ran the full suite.&lt;/p&gt;

&lt;p&gt;What it doesn't do&lt;/p&gt;

&lt;p&gt;No flakiness handling, no epoch tracking, no hosted dashboard, no distributed execution. These are all real problems in production eval pipelines — they're just not v0.1 problems. The scope is: run tasks, score outputs, diff runs, view traces. That scope is complete and useful on its own.&lt;/p&gt;

&lt;p&gt;Result&lt;/p&gt;

&lt;p&gt;44 tests passing. Five CLI verbs: run, score, diff, view, init. Works with any adapter you write. Run artifacts are plain JSON — readable by anything.&lt;/p&gt;

&lt;p&gt;github.com/xenaarch-dev/agentkit&lt;/p&gt;

</description>
      <category>python</category>
      <category>agents</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I built a kernel-enforced sandbox for LLM agent tool calls</title>
      <dc:creator>Padmaja Kotoky</dc:creator>
      <pubDate>Tue, 25 Aug 2026 08:12:12 +0000</pubDate>
      <link>https://dev.to/xenaarchdev/how-i-built-a-kernel-enforced-sandbox-for-llm-agent-tool-calls-fm6</link>
      <guid>https://dev.to/xenaarchdev/how-i-built-a-kernel-enforced-sandbox-for-llm-agent-tool-calls-fm6</guid>
      <description>&lt;p&gt;Every team running LLM agents with shell access has the same problem: the agent calls a tool, the tool runs code, and nothing stops that code from reading files it shouldn't, calling home to an unexpected endpoint, or doing something worse. The usual answers are Docker (heavy, requires root or a daemon), chroot (bypassable), or just not solving it and hoping the agent behaves.&lt;/p&gt;

&lt;p&gt;I wanted a chmod-simple wrapper — one binary, no daemon, no root — backed by actual kernel enforcement. This is what I built.&lt;/p&gt;

&lt;p&gt;What it does&lt;br&gt;
bash&lt;br&gt;
xenarch-guard --allow-write ./workspace -- python3 agent.py&lt;/p&gt;

&lt;p&gt;agent.py runs able to write only inside ./workspace. Every other path is unreachable. A fixed set of dangerous syscalls is blocked regardless of what paths are granted. The wrapped process cannot lift these restrictions — they're enforced by the kernel, not by the program's good behavior.&lt;/p&gt;

&lt;p&gt;Why Landlock and seccomp, not Docker&lt;/p&gt;

&lt;p&gt;Docker solves a different problem. It isolates a whole environment — filesystem, network, process namespace — and it requires either root or a daemon holding root. For sandboxing a single tool call inside an already-running agent process, that's the wrong shape. I wanted something the agent runtime could call the same way it calls any subprocess.&lt;/p&gt;

&lt;p&gt;Landlock is a Linux kernel security module (5.13+) that lets an unprivileged process restrict its own filesystem access. Seccomp-bpf lets a process install a syscall filter on itself. Neither requires root. Neither requires a daemon. Together they give you two independent enforcement layers: Landlock controls which paths are reachable, seccomp controls which syscalls are allowed — and the two layers don't interfere with each other.&lt;/p&gt;

&lt;p&gt;The architecture is one execve(), not a fork: xenarch-guard installs the Landlock ruleset and seccomp filter on its own process, then becomes the wrapped command via execve(). No parent process left holding state the child could reach for.&lt;/p&gt;

&lt;p&gt;Three real bugs I hit building this&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Missing PR_SET_NO_NEW_PRIVS. Landlock requires prctl(PR_SET_NO_NEW_PRIVS, 1) before landlock_restrict_self() will succeed on a non-root process — without it, landlock_restrict_self returns EPERM and silently does nothing. The sandbox appeared to work (process ran, exited cleanly) but nothing was actually confined. Found it by running the test suite against a path that should have been denied and watching it pass when it shouldn't.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Missing LANDLOCK_ACCESS_FS_EXECUTE in read grants. An --allow-read grant that didn't include execute rights meant dynamically-linked binaries couldn't load their shared libraries — the dynamic linker needs execute permission on the library files, not just read. Fixed by folding LANDLOCK_ACCESS_FS_EXECUTE into every allow_read and allow_write grant.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Test script not granting /bin and /usr/bin. The integration tests ran commands like python3 without granting the interpreter's actual paths under the Landlock policy — so the tests were testing an unconfined run without knowing it. The fix was explicit path grants in the test harness. This one only showed up by reading the policy summary xenarch-guard prints to stderr before each run.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All three are the category of bug that only shows up when you actually run the sandbox against a real workload, not when you read the code.&lt;/p&gt;

&lt;p&gt;Two Landlock behaviors that aren't in the documentation&lt;/p&gt;

&lt;p&gt;Granted paths must exist when xenarch-guard starts. Landlock builds its ruleset by opening each granted path at startup. If the path doesn't exist yet — because the wrapped command is about to create it — the grant is silently dropped. Grant the parent directory instead.&lt;/p&gt;

&lt;p&gt;--allow-write on a device node fails with errno 22. Landlock rejects the rule outright (not the operation — the rule), so the device stays unreachable. /dev/null hits this. Grant /dev instead of /dev/null.&lt;/p&gt;

&lt;p&gt;Neither of these produces a hard error. Both are only visible in the policy summary on stderr. I found both while building xenarchos, a skill runner that uses xenarch-guard as a dependency, and the first draft of its README example silently failed because of finding #1.&lt;/p&gt;

&lt;p&gt;What it doesn't protect against&lt;/p&gt;

&lt;p&gt;The threat model is in docs/THREAT_MODEL.md and I'd rather link it than summarize it badly. Short version: this is not a container. A sufficiently privileged process can escape Landlock via kernel exploits. The seccomp deny-list blocks the syscalls most commonly used to do that, but "impenetrable" isn't a claim this project makes. The honest use case is confining cooperative-but-untrusted code — an LLM-generated tool call that shouldn't have write access to your home directory — not confining an adversary who knows they're being sandboxed.&lt;/p&gt;

&lt;p&gt;Result&lt;/p&gt;

&lt;p&gt;11/11 integration tests passing on WSL2 (Landlock ABI 7, Linux 6.x). The test suite includes a symlink-escape check and a network confinement test. Ships as a single statically-linkable C++ binary with one dependency (libseccomp).&lt;/p&gt;

&lt;p&gt;github.com/xenaarch-dev/xenarch-guard&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>cpp</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
