The engineering choices behind Accomplish’s agent sandbox
A user can give a coding agent a goal without specifying the whole procedure. The agent can inspect a repository, decide that it needs to install dependencies and run a build, then check the implementation against a specification. It can open a pull request and communicate with a reviewer afterward. The sequence is the agent’s to work out.
That freedom also makes the agent harder to secure.
Things can go wrong along the way. The agent may misunderstand the request. A file or website may contain unsafe instructions that it executes, or an installed dependency may be compromised. The agent harness may have vulnerabilities of its own.
We want the agent to work carefully, and instructions are useful for that, but they are not a security boundary. Accomplish gives the agent a complete working environment, while trusted controls outside the sandbox govern its access to anything beyond it.
Guiding principles #
The principles we set from the start:
- The sandbox and everything running inside it are untrusted. It should not inherit authority over anything outside the boundary.
- Access to resources outside the sandbox (user files, credentials and network access) must be through a controlled and audited gate.
- Autonomous, open-ended work is the goal. The sandbox environment cannot be limited to a closed set of predefined tools and workflows.
This pointed us toward a virtual machine running a separate, general-purpose operating system. For this kind of sandbox, full virtualization provides a strong isolation boundary: the guest runs its own kernel, separated from the host by the platform hypervisor. Inside, the agent can install packages, run arbitrary tools and work in a complete development environment. Access to external resources remains controlled from outside the VM.
Within that boundary, the agent harness, downloaded code, the guest user and guest root are all untrusted.
The trust model remains the same on all platforms. In this post, we’ll cover our macOS desktop implementation and the challenges and tradeoffs we faced building it.
We chose Ubuntu for the guest OS because it is familiar, widely used and broadly supported by developer tools. To the agent, it should feel like a normal dev machine.
Initially, we built on Apple’s Virtualization.framework. A VZVirtualMachineConfiguration defines the Ubuntu disk, network device and shared folder used to create and start a VZVirtualMachine. Once running, the corresponding VZVirtioFileSystemDevice exposes a share the host can update.
Designed for real work #
At the start of a session, the user selects a working folder, typically the repository for the task. The host exposes that folder to the session as its initial workspace. The agent can edit the code however it likes, run tests, build the app and start its development server within the VM.
But most tasks require access to resources outside the initial workspace.
In our model, each type of access passes through a different gate. Network requests pass through our egress proxy. MCP calls pass through a gateway that checks whether the account is connected and the tool call is allowed. CLI tools such as aws and gh use placeholder credentials in the guest, which our broker replaces in transit.
If the agent needs a file stored outside its workspace, it must request access to that path.
Expanding a live sandbox #
Access to a new folder expands the scope of the sandbox for the rest of the session. The agent makes a request through an MCP server we provide. That expansion gives untrusted code access to a new resource, so the user must decide whether to grant it. Only then does our trusted host service add the folder to the session.
Supporting these grants exposed a technical constraint in the macOS implementation: Virtualization.framework cannot attach a new VirtioFS device after startup.
Apple’s VZMultipleDirectoryShare lets us update the directories exposed through an existing VZVirtioFileSystemDevice. We initialize it with the workspace, then replace its directory map as folders are approved.
Every agent session runs inside its own Linux mount namespace: its own view of the filesystem. A shared folder is exposed to the guest at an internal raw path, then bind-mounted inside the relevant session at the same absolute path it has on the host. The agent sees /Users/user/Downloads/feature-spec.md.
Preserving host paths in the guest keeps absolute references working across the agent’s tools and subprocesses. When code spans several granted folders, each appears at the same path it has on the host.
Moving the VMM to libkrun
Later, we moved the macOS runtime to a purpose-built VMM based on libkrun. This gave us more control over that layer while keeping the Ubuntu guest and our security model unchanged. On macOS, libkrun uses Apple’s Hypervisor.framework directly and supports the same Virtio devices the Ubuntu guest relies on.
Unlike VZMultipleDirectoryShare, which we could update while the VM was running, libkrun’s VirtioFS configuration is fixed at startup. Since libkrun is open source, we added an API that lets the host change the backing path of an inactive device.
krun_set_virtiofs_backing(ctx, tag, host_path, read_only);
We boot the libkrun-based machine with a pool of empty, inactive VirtioFS devices. When a user approves a folder grant, the host assigns its path to an unused device, then mounts it inside the relevant session. The guest has file access, but it cannot change the host path behind the device.
Summary #
When strict sandbox restrictions make a task impossible, agents look for workarounds, while users may turn to unmanaged alternatives.
That made usefulness an engineering requirement alongside isolation. The gates we built give the agent the capabilities it needs without full access to external resources from inside the sandbox.
Looking forward
We expect the coming months to continue to surprise us with what autonomous AI agents can do, and how those capabilities can have unintended consequences. A practical starting point is the set of controlled gates described here. Enforcing policy and logging every decision at these boundaries gives us an isolation system that does not depend on any particular harness, vendor or protocol and can adapt as workloads evolve.
The next step is classification and real-time monitoring of activity at these boundaries, so a series of harmless-looking actions does not add up to something destructive.