Skip to content
Open
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
17 changes: 12 additions & 5 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,13 @@ jobs:
- name: Shell syntax and shellcheck
if: runner.os == 'Linux'
run: |
for f in agent-vm.sh agent-vm.setup.sh release.sh runtime.example.sh tests/*.sh tests/sftp/run tests/sftp/*.sh scripts/*; do
for f in agent-vm.sh provision/agent-vm.setup.sh release.sh runtime.example.sh tests/*.sh tests/sftp/run tests/sftp/*.sh scripts/*; do
echo "bash -n $f"
bash -n "$f"
done
sh -n www/public/install.sh
shellcheck --version
shellcheck -S warning agent-vm.sh agent-vm.setup.sh release.sh runtime.example.sh tests/*.sh tests/sftp/run tests/sftp/*.sh scripts/*
shellcheck -S warning agent-vm.sh provision/agent-vm.setup.sh release.sh runtime.example.sh tests/*.sh tests/sftp/run tests/sftp/*.sh scripts/*
shellcheck -S warning --shell=sh www/public/install.sh

- name: Build, with the guest agent and templates
Expand All @@ -83,7 +83,7 @@ jobs:
run: go build -o _output/bin/agent-vm.exe ./cmd/agent-vm

- name: Vet
run: go vet . ./cmd/... ./internal/...
run: go vet . ./agents/... ./cmd/... ./internal/... ./policy/... ./provision/...

- name: Vet for Windows, amd64 and arm64
if: runner.os == 'Linux'
Expand All @@ -99,15 +99,22 @@ jobs:
GOOS=windows govulncheck ./...

- name: Test
run: go test . ./cmd/... ./internal/...
run: go test . ./agents/... ./cmd/... ./internal/... ./policy/... ./provision/...

- name: Tests of Lima, sshocker and pkg/sftp, patched
if: runner.os != 'Windows'
run: make test-third-party

# A program that embeds the library, built against this checkout.
# Its end-to-end test (go test -tags e2e in _output/example) boots a
# VM: not run here.
- name: Example program that embeds the library
if: runner.os == 'Linux'
run: make test-example

# Where the file system folds names (APFS, NTFS), it is the oracle of
# the read-only names' matching; Linux's matches byte for byte.
- name: Fuzz read-only name matching against the file system
if: runner.os != 'Linux'
shell: bash
run: GOWORK="$PWD/tests/fuzz/go.work" go test -vet=off -run '^$' -fuzz '^FuzzSameNameFS$' -fuzztime 2m github.com/lima-vm/sshocker/pkg/reversesshfs
run: go test -vet=off -run '^$' -fuzz '^FuzzSameNameFS$' -fuzztime 2m ./third_party/sshocker/pkg/reversesshfs
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ dist
# Built by the Makefile, embedded in the binary.
/internal/limaembed/assets/*
!/internal/limaembed/assets/README
# Made by go generate in the example.
/examples/consumer/assets/*
!/examples/consumer/assets/README
74 changes: 73 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,78 @@
# Changelog

## Unreleased
## 0.4.0 (unreleased)

### Added

- Named VMs: `agent-vm --name agent2 claude` runs a second VM on a folder,
called `agent2`, next to the folder's own. It belongs to the folder it is
made in and runs from there only: from another folder it stops, naming the
folder (`agent-vm rm agent2` gives it up, `--reset` makes it again, bound
anew). `stop NAME` and `rm NAME` reach it; `list` marks the ones of the
current folder with `>`. A name is lowercase letters, digits and dashes, at
most 40 characters, not `base` nor `base-…`, nor ending in a dash and 8 hex
digits (which every project's VM does).
- `--no-shares`, with `--name`: a VM that shares nothing, like `--scratch`,
but kept, and usable from any folder (`agent-vm --name sandbox --no-shares
claude`). It never gets shares; the folder's path is made in the VM as an
empty working folder. `--scratch` is still `--no-shares --rm` with a name of
its own; `--scratch --name` is a usage error.
- agent-vm is a Go library too, for programs that run their own agent in a VM
per project: `github.com/sylvinus/agent-vm` (`Open`, `Setup`, `ProjectVM`,
`Ensure`, `Exec`), with `provision` (what the base VM is built from),
`policy` (what the VMs may reach, and the files they may not change
unasked) and `agents`. The command line is built on it. It never writes to a
terminal nor reads an `AGENT_VM_*` variable: what it tells goes to a callback,
what it asks to an `Asker`. Experimental until a program other than
agent-vm uses it. `examples/consumer` is such a program;
`go run github.com/sylvinus/agent-vm/cmd/agent-vm-assets` makes the guest
agent and templates it embeds.

### Changed

- Each VM has a folder of its own, `~/.agent-vm/vms/<name>/`: Lima's files
for it (`lima/`), what agent-vm knows of it (`metadata.json`: the base it
was cloned from, its terminfo, its `--scratch` run), the policies it
booted with (`policy.json`) and the single files of `~/.agent-vm/volumes`
staged for it (`file-mounts/`). Deleting a VM deletes the folder, so
nothing of a VM outlives it. `~/.agent-vm/lima` keeps only what Lima
shares between VMs (`_config`, `_networks`). The `.agent-vm-*` files in
`~/.agent-vm` and `~/.agent-vm/file-mounts` are gone.
- The first command that changes VMs moves 0.3.0's: the VMs are renamed to
their new place (their disks keep sharing blocks with the base), the
markers become `metadata.json`. A running VM cannot move: on a terminal you
are asked whether to stop it, else it moves next time. Until then
`agent-vm list` says some wait. There is no way back: 0.3.0 sees no VM after
the move. `ssh_config` lines in `~/.ssh/config` become
`Include ~/.agent-vm/vms/*/lima/ssh.config`.
- The network and guarded-files policies a VM boots with are written to its
`policy.json`, which its host agent reads: the files in `~/.agent-vm` are
read by the command that starts the VM, not by the host agent.
- Lima, sshocker, pkg/sftp and gvisor-tap-vsock are packages of agent-vm's
module (`third_party/`, upstream plus `patches/`), not replaced modules: a
program that imports agent-vm gets them patched.

## 0.3.1

### Added

- Releases have a `.deb` and an `.rpm` for Linux (amd64, arm64): the binary in
`/usr/bin` and the docs, with QEMU and ssh recommended. Not in a package
repository yet: install the file with `apt install ./agent-vm_X.Y.Z_arm64.deb`
or `dnf install ./agent-vm-X.Y.Z-1.aarch64.rpm`.
- A Nix flake (Linux): `nix run github:sylvinus/agent-vm`. QEMU comes with it,
ssh is the system's.

### Fixed

- Linux: VMs turned Broken seconds to minutes after a start. The end of the
agent-vm command that started a VM (closing its terminal, a kill, and at
random a plain exit) killed Lima's host agent, which left QEMU running
without it, cut off, and failing every later start. The host agent now
outlives that command. On macOS, the same killed the VM.
- A VM left in that state is stopped by force, then started again, on the
next run; `agent-vm stop` stops it too. `stop` no longer says "VM stopped."
for a Broken VM it did not stop.

## 0.3.0

Expand Down
62 changes: 33 additions & 29 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# agent-vm 0.3, the Go binary, with Lima built in from third_party/.
# agent-vm, the Go binary, with Lima built in from third_party/.
#
# make _output/bin/agent-vm, with the guest agent and templates
# make install build it, then link it onto the PATH (AGENT_VM_BIN_DIR
# to choose where; ~/.local/bin by default)
# make limactl Lima's own limactl from third_party/, for debugging
# make test the Go tests
# make test-example the example program that embeds the library, built
# against this checkout
# make check-windows the Windows build and tests compile and vet, from any OS
# make test-third-party the tests of the modules in third_party/, patched
# make check-third-party third_party/ is upstream plus patches/, nothing else
Expand All @@ -31,9 +33,10 @@ EXE := .exe
endif

ASSETS := internal/limaembed/assets
MOD := github.com/sylvinus/agent-vm
LDFLAGS := -s -w \
-X github.com/sylvinus/agent-vm/internal/version.Version=$(VERSION) \
-X github.com/lima-vm/lima/v2/pkg/version.Version=$(LIMA_VERSION)
-X $(MOD)/internal/version.Version=$(VERSION) \
-X $(MOD)/third_party/lima/pkg/version.Version=$(LIMA_VERSION)
# yq only edits Lima's YAML here: without its other formats (Lua, HCL, TOML,
# XML...), 10% smaller. Tests run with the same tags.
TAGS := yq_noxml,yq_nolua,yq_noini,yq_nohcl,yq_notoml,yq_noprops,yq_nocsv,yq_nouri,yq_nobase64,yq_nosh,yq_noshell
Expand All @@ -47,7 +50,7 @@ CGO_ENABLED ?= 0
endif
GO_BUILD := CGO_ENABLED=$(CGO_ENABLED) $(GO) build -trimpath -tags $(TAGS) -ldflags "$(LDFLAGS)"

.PHONY: all agent-vm install limactl assets test test-go check-windows test-third-party check-third-party fuzz clean
.PHONY: all agent-vm install limactl assets test test-go test-example check-windows test-third-party check-third-party fuzz clean

all: agent-vm

Expand All @@ -63,60 +66,61 @@ install: agent-vm
./_output/bin/agent-vm$(EXE) install

limactl:
$(GO_BUILD) -o _output/bin/limactl$(EXE) github.com/lima-vm/lima/v2/cmd/limactl
$(GO_BUILD) -o _output/bin/limactl$(EXE) ./third_party/lima/cmd/limactl
mkdir -p _output/share/lima
CGO_ENABLED=0 GOOS=linux GOARCH=$(GOARCH) $(GO) build -trimpath -ldflags "-s -w" \
-o _output/share/lima/lima-guestagent.Linux-$(GUEST_ARCH) github.com/lima-vm/lima/v2/cmd/lima-guestagent
-o _output/share/lima/lima-guestagent.Linux-$(GUEST_ARCH) ./third_party/lima/cmd/lima-guestagent
rm -rf _output/share/lima/templates && cp -R third_party/lima/templates _output/share/lima/templates
ifeq ($(GOOS),darwin)
codesign -f -v --entitlements third_party/lima/vz.entitlements -s - _output/bin/limactl
endif

# The guest agent runs in the Linux guest: built for it, without cgo.
# The guest agent runs in the Linux guest: built for it, without cgo. The same
# tool a program that embeds agentvm runs (cmd/agent-vm-assets), run for this
# machine whatever GOOS and GOARCH build the binary for.
assets:
CGO_ENABLED=0 GOOS=linux GOARCH=$(GOARCH) $(GO) build -trimpath \
-ldflags "-s -w -X github.com/lima-vm/lima/v2/pkg/version.Version=$(LIMA_VERSION)" \
-o $(ASSETS)/lima-guestagent.Linux-$(GUEST_ARCH) github.com/lima-vm/lima/v2/cmd/lima-guestagent
gzip -9 -n -f $(ASSETS)/lima-guestagent.Linux-$(GUEST_ARCH)
rm -rf $(ASSETS)/templates && cp -R third_party/lima/templates $(ASSETS)/templates
env -u GOOS -u GOARCH $(GO) run ./cmd/agent-vm-assets -o $(ASSETS) -arch $(GUEST_ARCH)

# The example program that embeds agentvm, built against this checkout in a
# copy of its module, to which the replace is added (it has none: it requires
# the released agent-vm), and its test.
test-example:
rm -rf _output/example && mkdir -p _output && cp -R examples/consumer _output/example
cd _output/example && $(GO) mod edit -replace $(MOD)=$(CURDIR) && GOFLAGS=-mod=mod $(GO) mod tidy \
&& env -u GOOS -u GOARCH $(GO) generate ./... \
&& $(GO) vet ./... && $(GO) build -o ../sandbox$(EXE) . && $(GO) test ./...

test: test-go

test-go:
$(GO) test -tags $(TAGS) . ./cmd/... ./internal/...
$(GO) test -tags $(TAGS) . ./agents/... ./cmd/... ./internal/... ./policy/... ./provision/...

# The Windows build and its tests compile and vet, from any OS. The Windows
# paths' logic is tested everywhere (paths.Windows); the tests themselves
# run on Windows in CI (windows-latest).
check-windows:
GOOS=windows GOARCH=amd64 $(GO) vet -tags $(TAGS) . ./cmd/... ./internal/...
GOOS=windows GOARCH=arm64 $(GO) vet -tags $(TAGS) . ./cmd/... ./internal/...
GOOS=windows GOARCH=amd64 $(GO) vet -tags $(TAGS) . ./agents/... ./cmd/... ./internal/... ./policy/... ./provision/...
GOOS=windows GOARCH=arm64 $(GO) vet -tags $(TAGS) . ./agents/... ./cmd/... ./internal/... ./policy/... ./provision/...

# From this module, so that each library is built with the others patched.
# Lima's MCP packages and sshocker's command are not part of agent-vm (-e:
# listing them needs sums nothing built here uses; grep keeps them out of
# the tests).
test-third-party:
$(GO) test -vet=off -race github.com/pkg/sftp/...
$(GO) test -race github.com/lima-vm/sshocker/pkg/...
$(GO) test $$($(GO) list -e github.com/lima-vm/lima/v2/pkg/... | grep -v /mcp)
$(GO) test github.com/containers/gvisor-tap-vsock/pkg/...
$(GO) test -vet=off -race ./third_party/sftp/...
$(GO) test -race ./third_party/sshocker/pkg/...
$(GO) test ./third_party/lima/pkg/...
$(GO) test ./third_party/gvisor-tap-vsock/pkg/...

check-third-party:
scripts/third-party-sync all --check

# Each fuzzer for FUZZTIME (their seeds run with the tests). The vendored
# sshocker's through tests/fuzz/go.work, which makes it fuzzable.
# Each fuzzer for FUZZTIME (their seeds run with the tests).
FUZZTIME ?= 1m
FUZZ = github.com/lima-vm/sshocker/pkg/reversesshfs github.com/pkg/sftp ./internal/env ./internal/gitguard \
FUZZ = ./third_party/sshocker/pkg/reversesshfs ./third_party/sftp ./internal/env ./internal/gitguard \
./internal/mounts ./internal/paths ./internal/runscript ./internal/ui ./internal/vmname
fuzz:
@set -e; for pkg in $(FUZZ); do \
case $$pkg in ./*) work=off ;; *) work=$(CURDIR)/tests/fuzz/go.work ;; esac; \
fs=$$(GOWORK=$$work $(GO) test -vet=off -list '^Fuzz' $$pkg) || { echo "$$fs"; exit 1; }; \
fs=$$($(GO) test -vet=off -list '^Fuzz' $$pkg) || { echo "$$fs"; exit 1; }; \
for f in $$(echo "$$fs" | grep '^Fuzz'); do \
echo "== $$pkg $$f"; \
GOWORK=$$work $(GO) test -vet=off -run '^$$' -fuzz "^$$f$$" -fuzztime $(FUZZTIME) $$pkg; \
$(GO) test -vet=off -run '^$$' -fuzz "^$$f$$" -fuzztime $(FUZZTIME) $$pkg; \
done; \
done

Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,23 @@ The agent is root in its VM and has the network. What it can reach on your machi
- The agent writes the project folder: on your machine, open it in your editor and use git there, and run everything else in the VM. Open agent-vm projects in VS Code's Restricted Mode, and never connect an editor to the VM over SSH (Remote-SSH trusts the VM with your machine: [SSH from your machine](https://www.agent-vm.org/#ssh-from-your-machine)). Editors, agents, commit hooks and commands you run on the host can run code the agent wrote: [What else reads the project](https://www.agent-vm.org/#what-else-on-your-machine-reads-the-project).
- The VM reaches the internet, not your machine nor your local network: what it may reach there, or the only domains it may reach, go in `~/.agent-vm/network`. A port the VM listens on is forwarded to your machine's loopback, unless something there uses it already or it is in 32768-60999, the range the guest picks random ports from.

## As a library

`github.com/sylvinus/agent-vm` is the engine of the command, for programs that run their own agent in a VM per project: `agentvm.Open`, `Setup`, `ProjectVM`, `Ensure`, `Exec`, with `provision`, `policy` and `agents` beside it. It writes to no terminal and reads no `AGENT_VM_*` variable. Experimental until a program other than agent-vm uses it; [`examples/consumer`](examples/consumer) is one, and the package documentation says what a build needs (cgo and a signature on macOS, the guest agent from `cmd/agent-vm-assets`).

## Development

```bash
make # _output/bin/agent-vm
make test-go # no VM; 0.2's recorded behavior where it is compared
go test -tags e2e ./internal/vm/ ./internal/cli/ # real VMs
make test-example # the example program that embeds the library
make check-third-party test-third-party # Lima, sshocker, pkg/sftp, gvisor-tap-vsock: upstream + patches/
make fuzz FUZZTIME=5m # each fuzzer; their seeds run with the tests
```

`third_party/` is part of this module, so a program importing it builds Lima, sshocker, pkg/sftp and gvisor-tap-vsock patched.

Releases: [RELEASING.md](RELEASING.md). The website is in [`www/`](www/README.md). Changes: [CHANGELOG.md](CHANGELOG.md).

## License
Expand Down
9 changes: 9 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ binaries `osslsigncode` (`brew install osslsigncode`).
`AGENT_VM_WINDOWS_PFX_PASS=<its password>`. Without, they go out unsigned, with a warning.
- Homebrew: `AGENT_VM_TAP=<clone of the tap>` updates `Formula/agent-vm.rb` there (each
platform's url and sha256) and prints the commands to commit it.
- The Linux `.deb` and `.rpm` (`packaging/nfpm.yaml`) are made with nfpm, built with Go at the
version pinned in `release.sh` (`NFPM_VERSION`; `AGENT_VM_NFPM=<binary>` uses another). They
are release assets and in `SHA256SUMS`, after the tarballs.
- A run that tagged and then failed is resumed by running it again.
3. In a commit after it: `NEXT_VERSION` to the next version, and a new `## Unreleased` section.

Expand All @@ -34,5 +37,11 @@ binaries `osslsigncode` (`brew install osslsigncode`).
`on_arm`/`on_intel`), installing the tarball's `agent-vm`: that is the shape `AGENT_VM_TAP` updates.
Then, in the tap: `brew audit --strict sylvinus/tap/agent-vm`, `brew reinstall` and `brew test`
before pushing.
- `flake.nix` builds from source, with its `vendorHash` for the Go dependencies. When go.mod or
go.sum change, `release.sh` stops until it is updated: on a machine with Nix, set
`pkgs.lib.fakeHash`, run `nix build .#agent-vm` (Linux), put the hash it prints back, and update
the `go.mod+go.sum:` checksum line next to it (the error gives the value). The hash is of the
downloaded modules only (`proxyVendor`), so patches to `third_party/` leave it as it is. Do the
same after `nix flake update`: `flake.lock` pins nixpkgs, and with it the Go that downloads them.
- `--not-latest` publishes a release without making it GitHub's latest: the installer's default
stays the previous release, and the Homebrew formula is left alone, `AGENT_VM_TAP` or not.
53 changes: 53 additions & 0 deletions agents/agents.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Package agents lists the coding agents agent-vm runs, each with the flag
// that lets it work unattended, which is safe in a VM. A program that embeds
// agentvm can run these, or its own.
//
// This package is experimental until a program other than agent-vm has used
// it.
package agents

// Agent is a command that works on a project, in a VM.
type Agent struct {
// Name is what the command line calls it: `agent-vm claude`.
Name string
// Command is the program and the flags that let it work without asking
// permission.
Command []string
// TTY says the agent draws a full-screen terminal interface: it needs
// one even when its output is not a terminal.
TTY bool
}

// Defaults are agent-vm's agents.
func Defaults() []Agent {
return []Agent{
// The VM also enforces bypass mode with managed settings: Claude Code
// drops the flag when it relaunches itself.
{Name: "claude", Command: []string{"claude", "--dangerously-skip-permissions"}},
// Approves every prompt that is not explicitly denied.
{Name: "opencode", Command: []string{"opencode", "--auto"}, TTY: true},
{Name: "codex", Command: []string{"codex", "--dangerously-bypass-approvals-and-sandbox"}},
{Name: "vibe", Command: []string{"vibe", "--agent", "auto-approve"}, TTY: true},
// No permission prompts: it runs every tool as asked.
{Name: "pi", Command: []string{"pi"}, TTY: true},
}
}

// Lookup is the default agent called name.
func Lookup(name string) (Agent, bool) {
for _, a := range Defaults() {
if a.Name == name {
return a, true
}
}
return Agent{}, false
}

// Names are the names of the default agents, in the order they are listed.
func Names() []string {
var out []string
for _, a := range Defaults() {
out = append(out, a.Name)
}
return out
}
Loading
Loading