Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
59bdc19
db: Plan 04c spec — Simple Query path + DBEvent emission
erans May 11, 2026
e8b78e7
db: Plan 04c implementation plan — Simple Query + DBEvent emission
erans May 11, 2026
71ed971
db: effects — add SourceStart/SourceEnd to ClassifiedStatement
erans May 11, 2026
72a1093
db: classify/postgres — populate ClassifiedStatement source spans
erans May 11, 2026
5baf6f8
db: classify/postgres — UTF-8-safe whitespace skip + trailing-stmt test
erans May 11, 2026
99489ef
db: classify/postgres — add Normalize to Parser interface
erans May 11, 2026
93b079d
db: events — extend DBEvent with §8 sub-structs
erans May 11, 2026
6b407d4
db: proxy — MaxQueryBytes, dialect map, atomic policy on Server
erans May 11, 2026
5094216
docs: spec — Docker Sandboxes mixin kit for AgentSH
erans May 11, 2026
85431bd
docs: spec — align kit design with verified agentsh paths/CLI
erans May 11, 2026
42e3a77
docs: plan — Docker Sandboxes mixin kit for AgentSH
erans May 11, 2026
df440a5
policy: add coding-agent template baked into the Docker Sandboxes mix…
erans May 11, 2026
df87cb5
policy: cover /root paths in coding-agent deny-credentials and allow-…
erans May 11, 2026
1ea78b7
policy: address coding-agent template review feedback
erans May 11, 2026
624f6b2
policy: cosmetic cleanup of coding-agent rule description and test fi…
erans May 11, 2026
9ade755
policy: add MergeOverlay helper for sbx bootstrap policy stacking
erans May 11, 2026
88c34dd
policy: merge DNS+connect redirects, expand merge tests and doc comment
erans May 11, 2026
a0fb733
bootstrap: cmd/agentsh-sbx-bootstrap with policy merge step
erans May 11, 2026
9bbdff0
bootstrap: use errors.Is(err, os.ErrNotExist) to match codebase conve…
erans May 11, 2026
7507d6f
bootstrap: spawn agentsh server and wait for socket
erans May 11, 2026
6a2dd41
bootstrap: flag-overridable daemon log, synchronous FD close, stat-er…
erans May 11, 2026
2186ed0
bootstrap: shim-tier probe + /run/agentsh/tier writer
erans May 11, 2026
179f8e7
bootstrap: errors.As for ExitError, hoist defaultShimDir, tighten pro…
erans May 11, 2026
a91e037
docs: policy-reference.md packaged with the kit for in-sandbox use
erans May 11, 2026
03c41d2
release: package sbx-bootstrap binary, shim symlinks, policy template
erans May 11, 2026
d2f89a3
scripts: install-agentsh.sh for the Docker Sandboxes mixin kit
erans May 11, 2026
0a070fd
release+scripts: add apk to nfpms formats, revert install.sh apk bran…
erans May 11, 2026
3442349
sbx: Docker Sandboxes mixin kit at docker/sbx-kit/
erans May 11, 2026
2a5705b
release: publish install.sh as a release asset for the sbx mixin kit
erans May 11, 2026
935f7e7
sbx: container-simulated E2E test + bootstrap probe POSIX-mode fix
erans May 11, 2026
781417e
docs: spec — auto-wrap agent harness under `agentsh wrap`
erans May 11, 2026
2876524
docs: plan — auto-wrap agent harness implementation
erans May 11, 2026
5c2ddaf
packaging: agent-wrap.sh — engage \`agentsh wrap\` on launch (fail-cl…
erans May 11, 2026
8054f28
packaging: gate FAKE_ROOT behind AGENTSH_TEST; harden test isolation
erans May 11, 2026
2842cfd
packaging: install-agent-wrappers.sh — symlink /usr/local/bin/<agent>…
erans May 11, 2026
83b60d5
packaging: silent-skip when already wrapped; add foreign-symlink test…
erans May 11, 2026
bea2c6c
release: package agent-wrap.sh and install-agent-wrappers.sh
erans May 11, 2026
6d49697
sbx: wire install-agent-wrappers.sh into spec.yaml install step
erans May 11, 2026
26481d8
sbx: extend spec_test.go to assert the second install command
erans May 11, 2026
aa44c36
sbx: extend e2e to verify agent-wrap engagement
erans May 11, 2026
1413905
docs: document agent-wrap behavior + fail-closed deviation
erans May 11, 2026
261a8c5
docs: amend spec — move-aside-and-replace (v2) after real-agent probe
erans May 11, 2026
0c7242c
packaging: redesign agent-wrap.sh — move-aside-and-replace (${0}.real)
erans May 11, 2026
39fa1e5
packaging: redesign install-agent-wrappers.sh — discover via command …
erans May 11, 2026
0c11094
sbx: replace stub engagement check with real-agent E2E against openco…
erans May 11, 2026
ee360a0
docs: update README + policy-reference for move-aside-and-replace
erans May 11, 2026
5a1976c
sbx: extend real-agent E2E to cover opencode, gemini, and codex templ…
erans May 11, 2026
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
84 changes: 84 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,19 @@ builds:
ldflags:
- -s -w

- id: sbx-bootstrap-linux
main: ./cmd/agentsh-sbx-bootstrap
binary: agentsh-sbx-bootstrap
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
- arm64
ldflags:
- -s -w -X main.version={{.Version}}

# unixwrap: seccomp wrapper for Linux amd64 (requires CGO + libseccomp)
- id: unixwrap-linux-amd64
main: ./cmd/agentsh-unixwrap
Expand Down Expand Up @@ -307,10 +320,12 @@ nfpms:
- unixwrap-linux-amd64
- unixwrap-linux-arm64
- stub-linux
- sbx-bootstrap-linux # NEW: bootstrap binary lands in /usr/bin
formats:
- deb
- rpm
- archlinux
- apk
bindir: /usr/bin
vendor: AgentSH
homepage: https://github.com/agentsh/agentsh
Expand Down Expand Up @@ -361,6 +376,72 @@ nfpms:
dst: /usr/lib/agentsh/bash_startup.sh
file_info:
mode: 0755
# Auto-wrap agent harness (paired with the Docker Sandboxes mixin kit).
- src: packaging/agent-wrap.sh
dst: /usr/lib/agentsh/agent-wrap
file_info:
mode: 0755
- src: packaging/install-agent-wrappers.sh
dst: /usr/lib/agentsh/install-agent-wrappers.sh
file_info:
mode: 0755
# Coding-agent policy template for Docker Sandboxes mixin bootstrap.
# Installed read-only — the bootstrap writes the merged result to
# /etc/agentsh/policies/default.yaml on each sandbox start.
- src: configs/policies/coding-agent.yaml
dst: /usr/share/agentsh/coding-agent.template.yaml
file_info:
mode: 0644

# Shim directory + symlinks (Docker Sandboxes mixin support).
# /usr/lib/agentsh/shims is prepended to PATH inside sandboxes via
# /etc/profile.d/agentsh.sh (written by the mixin kit's initFiles).
- dst: /usr/lib/agentsh/shims
type: dir
file_info:
mode: 0755
- dst: /usr/lib/agentsh/shims/bash
src: /usr/bin/agentsh-shell-shim
type: symlink
- dst: /usr/lib/agentsh/shims/sh
src: /usr/bin/agentsh-shell-shim
type: symlink
- dst: /usr/lib/agentsh/shims/curl
src: /usr/bin/agentsh-shell-shim
type: symlink
- dst: /usr/lib/agentsh/shims/wget
src: /usr/bin/agentsh-shell-shim
type: symlink
- dst: /usr/lib/agentsh/shims/pip
src: /usr/bin/agentsh-shell-shim
type: symlink
- dst: /usr/lib/agentsh/shims/pip3
src: /usr/bin/agentsh-shell-shim
type: symlink
- dst: /usr/lib/agentsh/shims/npm
src: /usr/bin/agentsh-shell-shim
type: symlink
- dst: /usr/lib/agentsh/shims/node
src: /usr/bin/agentsh-shell-shim
type: symlink
- dst: /usr/lib/agentsh/shims/git
src: /usr/bin/agentsh-shell-shim
type: symlink
- dst: /usr/lib/agentsh/shims/python
src: /usr/bin/agentsh-shell-shim
type: symlink
- dst: /usr/lib/agentsh/shims/python3
src: /usr/bin/agentsh-shell-shim
type: symlink
- dst: /usr/lib/agentsh/shims/rm
src: /usr/bin/agentsh-shell-shim
type: symlink

# Packaged policy reference (also lives in repo at docs/policy-reference.md).
- src: docs/policy-reference.md
dst: /usr/share/doc/agentsh/policy-reference.md
file_info:
mode: 0644
# Policy files
- src: configs/policies/*.yaml
dst: /etc/agentsh/policies/
Expand Down Expand Up @@ -388,6 +469,9 @@ nfpms:
release:
draft: false
prerelease: auto
extra_files:
- glob: scripts/install-agentsh.sh
name_template: install.sh

# Homebrew cask is published by the publish-homebrew-cask job in release.yml
# (not managed by GoReleaser — the cask installs the signed DMG, not raw binaries)
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ dns-test:
docker build -f Dockerfile.dns-test -t agentsh-dns-test .
docker run --rm --cap-add SYS_PTRACE agentsh-dns-test

sbx-e2e:
bash docker/sbx-kit/tests/run-e2e.sh

seccomp-probe:
mkdir -p build
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o build/seccomp-probe ./cmd/seccomp-probe/
Expand Down
56 changes: 56 additions & 0 deletions cmd/agentsh-sbx-bootstrap/daemon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package main

import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
)

// spawnDaemon fork-execs `bin args...` with stdout/stderr appended to logPath.
// The child is detached; the returned *exec.Cmd lets the caller signal it if
// needed (in normal flow the bootstrap exits after probing and the daemon
// keeps running, reparented to PID 1).
func spawnDaemon(bin string, args []string, logPath string) (*exec.Cmd, error) {
if err := os.MkdirAll(filepath.Dir(logPath), 0o755); err != nil {
return nil, fmt.Errorf("mkdir log dir: %w", err)
}
logF, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return nil, fmt.Errorf("open log: %w", err)
}
cmd := exec.Command(bin, args...)
cmd.Stdout = logF
cmd.Stderr = logF
cmd.Env = os.Environ()
if err := cmd.Start(); err != nil {
logF.Close()
return nil, fmt.Errorf("start %s: %w", bin, err)
}
// Release the parent's reference to the log file FD now that exec(2) has
// dup'd stdio into the child. The child keeps its own dup'd FD.
_ = logF.Close()
return cmd, nil
}

// waitForSocket polls for a filesystem entry at sockPath, returning nil as
// soon as it exists. Returns an error if the deadline elapses first.
//
// We check existence rather than `Dial` because the daemon may use a
// different socket type (gRPC vs HTTP) and a successful Dial isn't required
// to confirm "the daemon has started writing its socket" — only that the
// file exists.
func waitForSocket(sockPath string, deadline time.Duration) error {
end := time.Now().Add(deadline)
for time.Now().Before(end) {
if _, err := os.Stat(sockPath); err == nil {
return nil
} else if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("stat socket %q: %w", sockPath, err)
}
time.Sleep(50 * time.Millisecond)
}
return fmt.Errorf("socket %q did not appear within %s", sockPath, deadline)
}
69 changes: 69 additions & 0 deletions cmd/agentsh-sbx-bootstrap/daemon_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package main

import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)

func TestSpawnDaemonAndWait_SocketAppears(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("unix sockets only")
}
dir := t.TempDir()
sock := filepath.Join(dir, "agentsh.sock")

// Fake "daemon": a shell script that writes the socket file after a small
// delay. The bootstrap should observe it within the 2s window.
fakeBin := filepath.Join(dir, "fake-agentsh")
script := "#!/bin/sh\n(sleep 0.1; touch '" + sock + "') &\nexec sleep 5\n"
if err := os.WriteFile(fakeBin, []byte(script), 0o755); err != nil {
t.Fatal(err)
}

logPath := filepath.Join(dir, "bootstrap.log")
cmd, err := spawnDaemon(fakeBin, []string{"server"}, logPath)
if err != nil {
t.Fatalf("spawnDaemon: %v", err)
}
t.Cleanup(func() { _ = cmd.Process.Kill() })

if err := waitForSocket(sock, 2*time.Second); err != nil {
t.Fatalf("waitForSocket: %v", err)
}
}

func TestWaitForSocket_TimesOut(t *testing.T) {
dir := t.TempDir()
sock := filepath.Join(dir, "nope.sock")
start := time.Now()
err := waitForSocket(sock, 200*time.Millisecond)
if err == nil {
t.Fatal("expected timeout error")
}
if elapsed := time.Since(start); elapsed > 1*time.Second {
t.Errorf("waitForSocket overshot deadline: %v", elapsed)
}
}

func TestWaitForSocket_NonExistError(t *testing.T) {
// A path under a path component that is a regular file (not a directory)
// makes os.Stat return ENOTDIR, not ENOENT — exercises the new
// non-ErrNotExist branch.
dir := t.TempDir()
notADir := filepath.Join(dir, "file")
if err := os.WriteFile(notADir, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
sock := filepath.Join(notADir, "agentsh.sock") // /tmp/.../file/agentsh.sock — ENOTDIR
err := waitForSocket(sock, 200*time.Millisecond)
if err == nil {
t.Fatal("expected non-nil error for non-existent parent")
}
if !strings.Contains(err.Error(), "stat socket") {
t.Errorf("expected wrapped stat error, got: %v", err)
}
}
78 changes: 78 additions & 0 deletions cmd/agentsh-sbx-bootstrap/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// agentsh-sbx-bootstrap is the startup entrypoint installed into Docker
// Sandboxes by the AgentSH mixin kit. It merges the baked coding-agent
// policy with any user override, spawns the agentsh server, then probes
// the active enforcement tier and writes /run/agentsh/tier so the agent's
// SKILL.md can read it.
package main

import (
"flag"
"fmt"
"os"
"time"
)

const (
defaultTemplatePath = "/usr/share/agentsh/coding-agent.template.yaml"
defaultOverlayPath = "/home/agent/.agentsh/policy.yaml"
defaultPolicyPath = "/etc/agentsh/policies/default.yaml"
defaultTierPath = "/run/agentsh/tier"
// defaultBootstrapLog: target for the future bootstrap banner / tier probe
// log. v1 writes those to stderr; the constant reserves the path so
// installers, doc tooling, and Task 5 can reference a single source of truth.
defaultBootstrapLog = "/var/log/agentsh/bootstrap.log"
defaultDaemonLog = "/var/log/agentsh/daemon.log"
defaultAgentshBin = "/usr/bin/agentsh"
defaultServerConfig = "/etc/agentsh/config.yaml"
defaultDaemonSocket = "/run/agentsh/agentsh.sock"
defaultSocketTimeout = 2 * time.Second
defaultShimDir = "/usr/lib/agentsh/shims"
)

func main() {
var (
tmpl = flag.String("template", defaultTemplatePath, "Baked policy template path")
overlay = flag.String("overlay", defaultOverlayPath, "User override fragment path")
policy = flag.String("policy", defaultPolicyPath, "Output merged policy path")
agentshBin = flag.String("agentsh", defaultAgentshBin, "Path to the agentsh binary")
srvConfig = flag.String("server-config", defaultServerConfig, "Path to the agentsh server config")
sock = flag.String("socket", defaultDaemonSocket, "Daemon socket path to poll for readiness")
daemonLog = flag.String("daemon-log", defaultDaemonLog, "Path to daemon log file")
)
flag.Parse()

if err := mergeAndWritePolicy(*tmpl, *overlay, *policy); err != nil {
fmt.Fprintf(os.Stderr, "agentsh-sbx-bootstrap: policy merge failed: %v\n", err)
os.Exit(1)
}

if _, err := spawnDaemon(*agentshBin, []string{"server", "--config", *srvConfig}, *daemonLog); err != nil {
fmt.Fprintf(os.Stderr, "agentsh-sbx-bootstrap: spawn daemon: %v\n", err)
os.Exit(1)
}

if err := waitForSocket(*sock, defaultSocketTimeout); err != nil {
fmt.Fprintf(os.Stderr, "agentsh-sbx-bootstrap: %v (continuing with degraded tier)\n", err)
// Don't exit — tier probe will record tier=none.
}

shimDir := defaultShimDir
if env := os.Getenv("AGENTSH_SHIM_DIR"); env != "" {
shimDir = env
}

tier := "none"
if ok, resolved, probeErr := probeShimTier(shimDir); probeErr != nil {
fmt.Fprintf(os.Stderr, "agentsh-sbx-bootstrap: shim probe failed: %v\n", probeErr)
} else if ok {
tier = "shim"
fmt.Fprintf(os.Stdout, "agentsh-sbx-bootstrap: shim tier active (curl -> %s)\n", resolved)
} else {
fmt.Fprintf(os.Stderr, "agentsh-sbx-bootstrap: shim tier NOT active (PATH did not yield %s)\n", shimDir)
}

if err := writeTierFile(defaultTierPath, tier); err != nil {
fmt.Fprintf(os.Stderr, "agentsh-sbx-bootstrap: write tier file: %v\n", err)
os.Exit(1)
}
}
Loading