Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .objgit/hooks/receive-pack
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
#
# Execution environment (see docs/usage/hooks.md):
# * It runs in a kefka virtual shell, NOT a real OS shell. Only kefka's
# built-in commands are available (coreutils: cat, ls, echo, printf, head,
# tail, cut, sort, uniq, wc, sha256sum, grep, ...). There is no PATH to
# registered commands are available (cat, ls, printf, head, tail, cut,
# sort, uniq, wc, sha256sum, jo, jq, rg, ...). There is no PATH to
# system binaries, no network, and no `git`.
# * /src is a READ-ONLY checkout of the pushed commit. The working directory
# starts here.
Expand Down
4 changes: 4 additions & 0 deletions cmd/objgitd/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import (
"mvdan.cc/sh/v3/syntax"
"tangled.org/xeiaso.net/kefka/command/registry"
"tangled.org/xeiaso.net/kefka/command/registry/coreutils"
"tangled.org/xeiaso.net/kefka/command/registry/uutils"
"tangled.org/xeiaso.net/kefka/command/registry/wasmprog"
)

// receivePackHook names the hook that a push runs, and so its script at
Expand Down Expand Up @@ -280,6 +282,8 @@ func newHookShell(tree *object.Tree, changes hookChanges, env []string, stdin io

reg := registry.New()
coreutils.Register(reg)
wasmprog.Register(reg)
uutils.Register(reg)
if err := reg.Chdir(fsys, "/src"); err != nil {
return nil, fmt.Errorf("chdir /src: %w", err)
}
Expand Down
8 changes: 7 additions & 1 deletion cmd/objgitd/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,19 @@ func TestReceivePackHook(t *testing.T) {
// read-only error, so WROTE_SRC must never print.
hook := strings.Join([]string{
"cat README.md",
"cd sub && cat note.txt",
"printf hi | base32",
`printf '{"value":42}' | jq -r '.value'`,
"cp /src/README.md /tmp/copied && cat /tmp/copied",
"if cp /src/README.md /src/copied; then echo WROTE_SRC_CP; fi",
`echo "cwd=$PWD"`,
"echo built > /tmp/out",
"cat /tmp/out",
"echo nope > /src/nope.txt",
"echo WROTE_SRC",
}, "\n") + "\n"
writeFile(t, filepath.Join(work, "README.md"), "hello from repo\n")
writeFile(t, filepath.Join(work, "sub", "note.txt"), "from subdir\n")
writeFile(t, filepath.Join(work, ".objgit", "hooks", "receive-pack"), hook)
runGit(t, work, "add", ".")
runGit(t, work, "commit", "-m", "with hook")
Expand All @@ -102,7 +108,7 @@ func TestReceivePackHook(t *testing.T) {
t.Fatalf("hook did not run; logs:\n%s", logs)
}
// /src is readable and /tmp is writable; their output streamed to the client.
for _, want := range []string{"hello from repo", "cwd=/src", "built"} {
for _, want := range []string{"hello from repo", "from subdir", "NBUQ====", "42", "cwd=/src", "built"} {
if !strings.Contains(pushOut, want) {
t.Errorf("push output missing streamed hook output %q; output:\n%s", want, pushOut)
}
Expand Down
5 changes: 4 additions & 1 deletion docs/architecture/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ carries its own hook.
The script runs in a **kefka** virtual shell
(`tangled.org/xeiaso.net/kefka`). kefka is _not_ an OS sandbox. It is an
`mvdan.cc/sh` interpreter wired to a `billy.Filesystem`, plus a fixed registry
of commands, which here is coreutils only.
of Go coreutils, WASM programs, and WASM-backed uutils.

The sandbox filesystem is an `internal/mountfs` composite of two mounts:

Expand All @@ -71,6 +71,9 @@ A write outside `/tmp` fails, and a redirect into `/src` aborts the script.
`internal/kefkash` vendors the unexported `billysh` handler wiring from kefka.
Its `OpenHandler` is adapted to permit writes, so `/tmp` redirections work.
The filesystem is what enforces the read-only `/src`.
`internal/mountfs` also provides read-only directory handles for the WASI
programs, which need to open `/` and directories below it to resolve paths.
Open files report their full mounted path so WASI can stat them after open.

`newHookShell` builds this sandbox, and `hookEnv` builds its environment.
`loadHookChanges` gives both of them the file changes of the update.
Expand Down
31 changes: 31 additions & 0 deletions docs/plans/kefka-uutils.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Update kefka and add uutils to the hook shell

## Context

`objgitd` pins kefka to a May 2026 commit and registers only its Go
coreutils. Upstream `v0.0.8` moves common
coreutils to a WASM-backed uutils registry and exposes WASM programs through
`wasmprog`. Hooks and SSH `sh` share
`newHookShell`, so that function is the registration point for both.

## Changes

1. Pin kefka to `v0.0.8` and update the module sums.
2. Register wasmprog and uutils after coreutils, following kefka's own CLI.
The uutils registry provides commands such as `cat` and `ls` and adds
`base32`; wasmprog provides `jo`, `jq`, `python3`, `qjs`, and `rg`.
3. Let `mountfs` open read-only directory handles. WASI needs them to
resolve paths beneath `/`, including `/src` and its subdirectories. Return
mount-qualified file names so WASI can stat opened files.
4. Exercise a uutils-only command, `jq`, relative paths after `cd`, and the
read-only `/src` and writable `/tmp` mounts in the existing push test.
5. Update the hook documentation to describe the available commands.

## Verification

Run `go build ./...`, the hook and SSH protocol tests, then `go test ./...`.

The tag points to commit `e1236bb201d5013afcfd3b71ce7259e2d42c7830`.
The public Go module proxy initially returned 404, but Go's normal direct
fallback fetched the tag. `go mod download` passed with an empty module cache
and the `v0.0.8` checksums in `go.sum`.
14 changes: 8 additions & 6 deletions docs/usage/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ Hooks run in [kefka](https://xeiaso.net/blog/2026/dancing-mad-sandboxing/), a
virtual `bash` interpreter. **This is not a container, VM, or OS sandbox.** It is
safe because of what it _cannot_ reach, not because of kernel isolation:

- **No system binaries.** Only kefka's built-in commands exist — roughly the
POSIX coreutils: `cat`, `ls`, `echo`, `printf`, `head`, `tail`, `cut`, `sort`,
`uniq`, `wc`, `tr`, `grep`, `sha256sum`, `base64`, `mkdir`, `cp`, `mv`, `rm`,
`touch`, `date`, `sleep`, `seq`, `expr`, and so on. There is no `git`, no
package manager, no compiler, no `curl`.
- **No system binaries.** Kefka provides Go commands, WASM-backed uutils, and
the WASM programs `jo`, `jq`, `python3` (also `python`), `qjs`, and `rg`. Available
coreutils include `cat`, `ls`, `printf`, `head`, `tail`, `cut`, `sort`,
`uniq`, `wc`, `tr`, `sha256sum`, `base64`, `base32`, `mkdir`, `cp`, `mv`,
`rm`, `touch`, `date`, `sleep`, `seq`, and `expr`. There is no `git`,
package manager, compiler, or `curl`.
- **No network.**
- **No host filesystem.** The only files a hook can see are the two mounts
below.
Expand Down Expand Up @@ -218,5 +219,6 @@ Hook failure is logged, but it cannot undo the accepted push.
- No writable working tree: `/src` is strictly read-only and `/tmp` is the only
scratch space.
- No way to reject a push from a hook (it runs after the fact).
- No system tooling, network, or arbitrary executables — only kefka built-ins.
- No system tooling, network, or arbitrary executables — only kefka's
registered commands.
- Hook output reaches the pusher only when the client negotiated sideband.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ require (
golang.org/x/sync v0.20.0
golang.org/x/term v0.43.0
mvdan.cc/sh/v3 v3.13.1
tangled.org/xeiaso.net/kefka v0.0.6-0.20260528192045-e0a84e40ceb8
tangled.org/xeiaso.net/kefka v0.0.8
)

require (
Expand Down Expand Up @@ -60,13 +60,13 @@ require (
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/tetratelabs/wazero v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/tools v0.45.0 // indirect
google.golang.org/protobuf v1.36.8
honnef.co/go/tools v0.7.0 // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
github.com/tigrisdata/storage-go v0.6.0 h1:1Rxzxp/GAUqwFln8qMXYOQIa47IsT2aBNuxYc55o6D4=
github.com/tigrisdata/storage-go v0.6.0/go.mod h1:l3u7N9LDIhv4lfpkEBJYzolWJ/SBb6WiBexgy/uq6iQ=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
Expand Down Expand Up @@ -164,5 +166,5 @@ honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=
honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=
mvdan.cc/sh/v3 v3.13.1 h1:DP3TfgZhDkT7lerUdnp6PTGKyxxzz6T+cOlY/xEvfWk=
mvdan.cc/sh/v3 v3.13.1/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0=
tangled.org/xeiaso.net/kefka v0.0.6-0.20260528192045-e0a84e40ceb8 h1:R008zBlWio3SXRZUaEDDDzzEf82X6e6KLvqhNddXprI=
tangled.org/xeiaso.net/kefka v0.0.6-0.20260528192045-e0a84e40ceb8/go.mod h1:p474Le4nfwuwnuAWowRhXMsTYfkpOMscchuaFvDa2zM=
tangled.org/xeiaso.net/kefka v0.0.8 h1:RbiOJn+b+zxa4gNVkW1mSIBj0Qjbcji2eo7wdBf5Gu8=
tangled.org/xeiaso.net/kefka v0.0.8/go.mod h1:h2ZITASpfUqC6PzLUfYTTO9AKPAft45UvCyxe/LAN2k=
56 changes: 54 additions & 2 deletions internal/mountfs/mountfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"path"
"sort"
"strings"
"syscall"
"time"

"github.com/go-git/go-billy/v6"
Expand Down Expand Up @@ -69,11 +70,62 @@ func (f *FS) OpenFile(filename string, flag int, perm fs.FileMode) (billy.File,
return nil, pathErr("open", filename, err)
}
if isRoot || isMount {
return nil, pathErr("open", filename, billy.ErrNotSupported)
return f.openDir(filename, flag)
}
return sub.OpenFile(rel, flag, perm)
file, err := sub.OpenFile(rel, flag, perm)
if err == nil {
// The mounted filesystem may return only the basename from Name().
// WASI uses it to stat the open file against this composite filesystem.
return namedFile{File: file, name: path.Clean("/" + filename)}, nil
}
if flag&(os.O_WRONLY|os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC) != 0 {
return nil, err
}
// Some mounted filesystems cannot open directories. WASI needs directory
// handles to resolve paths beneath the preopened root.
if info, statErr := sub.Stat(rel); statErr == nil && info.IsDir() {
return f.openDir(filename, flag)
}
return nil, err
}

type namedFile struct {
billy.File
name string
}

func (f namedFile) Name() string { return f.name }

func (f *FS) openDir(filename string, flag int) (billy.File, error) {
if flag&(os.O_WRONLY|os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC) != 0 {
return nil, pathErr("open", filename, billy.ErrReadOnly)
}
return &dirFile{fsys: f, name: path.Clean("/" + filename)}, nil
}

// dirFile is a read-only handle for a virtual or mounted directory. The WASI
// adapter uses its name to stat and list entries through the composite FS.
type dirFile struct {
fsys *FS
name string
}

func (d *dirFile) Name() string { return d.name }
func (d *dirFile) Stat() (fs.FileInfo, error) { return d.fsys.Stat(d.name) }
func (d *dirFile) Close() error { return nil }
func (d *dirFile) Read([]byte) (int, error) { return 0, syscall.EISDIR }
func (d *dirFile) ReadAt([]byte, int64) (int, error) {
return 0, syscall.EISDIR
}
func (d *dirFile) Write([]byte) (int, error) { return 0, billy.ErrReadOnly }
func (d *dirFile) WriteAt([]byte, int64) (int, error) {
return 0, billy.ErrReadOnly
}
func (d *dirFile) Seek(int64, int) (int64, error) { return 0, syscall.EISDIR }
func (d *dirFile) Truncate(int64) error { return billy.ErrReadOnly }

var _ billy.File = (*dirFile)(nil)

func (f *FS) Stat(filename string) (fs.FileInfo, error) {
sub, rel, isRoot, isMount, err := f.route(filename)
if err != nil {
Expand Down
Loading