Skip to content

Commit 72e9f0f

Browse files
committed
fix(kefka): review feedback
Signed-off-by: Xe Iaso <xe@tigrisdata.com>
1 parent fe6045b commit 72e9f0f

1 file changed

Lines changed: 34 additions & 21 deletions

File tree

lume/src/blog/2026/dancing-mad-sandboxing.mdx

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ System calls are injected into each operating system process via a process kinda
4242

4343
## Bashing your head into the wall
4444

45-
A while ago a new JavaScript package got into the meme sphere at work: [just-bash](https://github.com/vercel-labs/just-bash/tree/main/packages/just-bash). It's a sandboxed environment with a shell interpreter that was originally intended for use with AI agents after its author observed that AI agents know how to use a tool called `bash` a lot better than a tool called `search_documentation`. This is backed by a "fake" shell with "fake" core utilities (`cat`, `ls`, etc, hereinafter coreutils) so that when an agent decides to `rm -rf /`, nothing important actually leaves the room.
45+
A while ago a new JavaScript package got into the meme sphere at work: [just-bash](https://github.com/vercel-labs/just-bash/tree/main/packages/just-bash). It's a sandboxed environment with a shell interpreter that was originally intended for use with AI agents after its author observed that AI agents know how to use a tool called `bash` a lot better than a tool called `search_documentation`. This is backed by a "fake" shell with "fake" core utilities (`cat`, `ls`, etc, hereinafter coreutils) so that when an agent decides to `rm -rf /`, nothing important actually leaves the room. One of my coworkers made [@tigrisdata/agent-shell](https://www.npmjs.com/package/@tigrisdata/agent-shell) on top of this that uses Tigris as its storage layer.
4646

4747
This is great for people in the JavaScript ecosystem, but I am not mainly a JavaScript developer. I really wanted to play with it so I started thinking what it would take to have something like this in Go. mvdan's [shell package](https://pkg.go.dev/mvdan.cc/sh/v3) makes this a heck of a lot easier, meaning that this "fake" shell would be powered by a real shell instead of either porting half of bash to JavaScript or making up hopefully-compatible behaviour.
4848

@@ -131,14 +131,18 @@ This is where I started vibe coding things, mostly via a [skill that ports a jus
131131
>
132132
> None.
133133
134-
I would link to the exact page in the POSIX spec, but they use a legacy frameset layout so you have to [dig through the Shell and Utilities section](https://pubs.opengroup.org/onlinepubs/9799919799/).
134+
Really, check out [the POSIX spec for `true`](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/true.html). It's trivial to implement, here's a oneliner to implement it in Linux:
135+
136+
```
137+
touch ./true && chmod +x ./true
138+
```
135139

136140
</Conv>
137141
</ConvP>
138142

139143
### I made an operating system\*
140144

141-
This is basically an operating system: it provides interfaces for programs (functions) to get input from a user, send output to a user, interact with a filesystem, and more. Eventually I want to add networking a network stack on ExecContext, probably tsnet or wireguard-go for the user-level side.
145+
This is basically an operating system: it provides interfaces for programs (well, in this case functions) to get input from a user, send output to a user, interact with a filesystem, and more. Eventually I want to add networking via a network stack on ExecContext, probably with [tsnet](https://pkg.go.dev/tailscale.com/tsnet) or [wireguard-go's netstack package](https://pkg.go.dev/golang.zx2c4.com/wireguard/tun/netstack) for the user-level side. Maybe there's room for adding [CEL](https://cel.dev/) based network filters there too.
142146

143147
## Porting applications with WebAssembly
144148

@@ -148,27 +152,32 @@ Once I got basic coreutils working, I thought it would be fun to get Python, jq,
148152

149153
At some point I hit a wall and had to switch from io/fs#FS to [billy](https://github.com/go-git/go-billy), another filesystem interface that I think predates the standard library one. This gives you [a bunch more methods](https://pkg.go.dev/github.com/go-git/go-billy/v5@v5.9.0#Filesystem) that map a lot closer to filesystem semantics in ways that coreutils crave. The interface was also mostly compatible with io/fs#FS so most of the hard part was really changing out the type and then chasing down compiler errors until I found enough of a pattern to have Opus automate the rest of it.
150154

151-
From there it was a matter of adapting billy's filesystem to wazero's [experimental sys interface](https://pkg.go.dev/github.com/tetratelabs/wazero@v1.11.0/experimental/sys). Mostly glue code, except where I had to translate Go errors into POSIX errno values. I had to read both the POSIX spec and the wazero source to figure out how to map errors between the two worlds.
155+
From there it was a matter of adapting billy's filesystem to wazero's [experimental sys interface](https://pkg.go.dev/github.com/tetratelabs/wazero@v1.11.0/experimental/sys). Mostly glue code, except where I had to translate Go errors into POSIX errno values. I had to read both the POSIX spec, the WASI spec, and the wazero source to figure out how to map errors between the two worlds. I think I'm at least 95% correct, which is likely within the margin of porting error.
152156

153157
Adapting that codeinterpreter/python library to the new interface was mostly straightforward, and I ended up with a flow like this:
154158

155159
```go
156160
// from https://tangled.org/xeiaso.net/kefka/blob/main/command/internal/python3/python3.go
157161

158162
func (Impl) Exec(ctx context.Context, ec *command.ExecContext, args []string) error {
159-
fsConfig := wazero.NewFSConfig().(sysfs.FSConfig).
163+
fsConfig := wazero.NewFSConfig().
164+
(sysfs.FSConfig).
160165
WithSysFSMount(billyfs.New(ec.FS), "/")
161166

162167
config := wazero.NewModuleConfig().
168+
// Pipe ExecContext stdio
163169
WithStdin(ec.Stdin).WithStdout(ec.Stdout).WithStderr(ec.Stderr).
170+
// Pipe argv
164171
WithArgs(append([]string{"python3"}, args...)...).
165172
WithName("python3").
173+
// Pipe filesystem
166174
WithFSConfig(fsConfig).
175+
// Pipe system time
167176
WithSysNanosleep().WithSysNanotime().WithSysWalltime()
168177

169178
mod, err := runtime.InstantiateModule(ctx, compiled, config)
170179
if err != nil {
171-
// Fit the square peg into the round hole
180+
// Fit the square peg into the round hole
172181
if exitErr, ok := errors.AsType[*wsys.ExitError](err); ok {
173182
if code := exitErr.ExitCode(); code != 0 {
174183
return interp.ExitStatus(uint8(code))
@@ -182,14 +191,14 @@ func (Impl) Exec(ctx context.Context, ec *command.ExecContext, args []string) er
182191
```
183192

184193
<Conv name="Mara" mood="aha">
185-
See? The dependencies such as stdin, stdout, and stderr get _injected_ into
186-
the WebAssembly guest. Wazero also makes you inject the implementation of time
187-
for boring reasons involving deterministic computing, but I'm sure you can see
188-
the ways things hook in. This basic dependency injection flow is how things
189-
like the [linuxulator](https://wiki.freebsd.org/Linuxulator) in FreeBSD or the
190-
old version of the Windows Subsystem for Linux work (WSL1 before it was made
191-
into a Linux VM with WSL2). The table of system calls and filesystem context
192-
is effectively an argument to the process.
194+
See? The dependencies such as `stdin`, `stdout`, and `stderr` get _injected_
195+
into the WebAssembly guest. Wazero also makes you inject the implementation of
196+
time for boring reasons involving deterministic computing, but I'm sure you
197+
can see the ways things hook in. This basic dependency injection flow is how
198+
things like the [linuxulator](https://wiki.freebsd.org/Linuxulator) in FreeBSD
199+
or the old version of the Windows Subsystem for Linux work (WSL1 before it was
200+
made into a Linux VM with WSL2). The table of system calls and filesystem
201+
context is effectively an argument to the process.
193202
</Conv>
194203

195204
Same trick got me ripgrep and jq. jq was annoying — wasi-sdk doesn't love jq's (ab)use of cmake — but 30 or so minutes of tweaking compiler flags got me a binary that works enough.
@@ -206,9 +215,9 @@ OK, that handles filesystems that (arguably) exist, like the btrfs volume on my
206215
like forks and snapshots. I'll be writing more about it soon.
207216
</Conv>
208217

209-
After finding [a basic implementation of an S3 -> Billy adapter](https://github.com/a-poor/s3fs), I vendored it into the Kefka repo and swapped out the "real" filesystem in cmd/kefka for an s3fs implementation pointed at a sample Tigris bucket. From there it was down to an iterative process of running commands, finding feature gaps when errors showed up, implementing them, and making sure things work the same against Tigris as they do against a local filesystem.
218+
After finding [a basic implementation of an S3 -> Billy adapter](https://github.com/a-poor/s3fs), I vendored it into the Kefka repo and swapped out the "real" filesystem in cmd/kefka for an s3fs implementation pointed at a sample Tigris bucket. From there it was down to an iterative process of running commands, finding feature gaps when errors showed up, implementing them, fuzzing, and making sure things work mostly the same against Tigris as they do against a local filesystem.
210219

211-
WASI is cursed: it has no process-level "current working directory," which most programs assume exists. You patch around it by passing a `CWD` envvar, or — easier — just use absolute paths. I haven't hit anything broken in casual use, but expect rough edges. Here be dragons and this code may be known by the state of California to cause cancer.
220+
WASI is cursed: it has no process-level "current working directory," which most programs assume exists. You patch around it by passing a `CWD` envvar, or just use absolute paths. I haven't hit anything broken in casual use, but expect rough edges. Here be dragons and this code may be known by the state of California to cause cancer.
212221

213222
## Why does it have to use the command line?
214223

@@ -293,12 +302,12 @@ func HandleSSH(sess ssh.Session) error {
293302
}
294303
```
295304

296-
[The real handler](https://tangled.org/xeiaso.net/kefka/blob/main/cmd/sophia/main.go#L87) is much messier because Python's REPL needs careful buffering, Ctrl-C has to actually cancel things, pty wiring is its own swamp. None of that shows up if it's working. Tab completion and readline polish are easy enough; I'll let you wire those up as an exercise for the reader.
305+
[The real handler](https://tangled.org/xeiaso.net/kefka/blob/main/cmd/sophia/main.go#L87) is much messier because Python's REPL needs careful buffering, Ctrl-C has to actually cancel things, and [pty](https://en.wikipedia.org/wiki/Pseudoterminal) wiring is its own can of cans of worms. None of that shows up if it's working. Tab completion and readline polish are easy enough; I'll let you wire those up as an exercise for the reader.
297306

298-
If you want to try it today, you can ssh into `sophia.xeiaso.net` on port 2222:
307+
If you want to try it today, you can ssh into `sophia.xeiaso.net`:
299308

300309
```
301-
$ ssh sophia.xeiaso.net -p 2222
310+
$ ssh sophia.xeiaso.net
302311
```
303312

304313
You'll get an isolated sandbox in your own [bucket fork/branch](https://www.tigrisdata.com/docs/forks/). Every `ls` is a `ListObjectsV2` against the bucket. Every `qjs` or `python3` runs WebAssembly on the server, wired to that same bucket.
@@ -312,12 +321,16 @@ Hello, world!
312321

313322
The demo bucket is seeded with examples. You'll probably have to poke around to find everything. Worst case, run `help`.
314323

324+
<Conv name="Cadey" mood="coffee">
325+
I should really hook up session recording to this.
326+
</Conv>
327+
315328
I want more experimental WebAssembly hacks like this to exist. I'll keep poking at it.
316329

317330
## Put your programs in clown jail
318331

319332
This has legs. [yeet](https://github.com/TecharoHQ/yeet) could run [Anubis](https://anubis.techaro.lol) builds on Windows; and if management ever makes you babysit AI agents, clown jail is a decent answer.
320333

321-
Code lives [on Tangled](https://tangled.org/xeiaso.net/kefka). I'm wiring it into [an agent harness](https://github.com/tigrisdata-community/mithras) so I can automate small tools against a local model (Qwen3-30B-A3B right now, good enough for what I'm doing).
334+
The code lives [on Tangled](https://tangled.org/xeiaso.net/kefka). I'm wiring it into [an agent harness](https://github.com/tigrisdata-community/mithras) so I can automate small tools against a local model (I'm loving Qwen3-36B-A3B).
322335

323-
There's a sister post on the Tigris blog that goes deeper into the AI-agent angle and the porting work using Claude Code. If you want, you can check it out here: [Give your agents disposable environments in Go](https://tigrisdata.com/blog/agent-sandbox-go)
336+
There's a sister post on the Tigris blog that goes deeper into the AI-agent angle and the porting work using Claude Code. If you want, you can check it out here: [Give your agents disposable environments in Go](https://tigrisdata.com/blog/agent-sandbox-go).

0 commit comments

Comments
 (0)