The sandbox limits what an executor process can see and modify. Its resources come from the signed manifest and, where needed, the concrete invocation. This layer complements Vaglio, identity checks, and application controls: it does not replace them, nor does it grant permission to perform an action by itself.
When an executor runs on the Linux server, runtime/sandbox.py
can prefix its Python command with bwrap. Bubblewrap builds a
separate filesystem view, provides a private /tmp, separates
selected namespaces, and adds --unshare-net when the executor
does not require network access. There is no separate daemon: a profile is
built for every invocation.
When work is placed on a paired device, the Rust client applies the mechanism available on that operating system. The server selects the device and checks that it belongs to the user; the client runs the command and returns the containment level it actually used.
| Function | Responsibility |
|---|---|
bwrap_available, sandbox_disabled | Detect whether bwrap is present and whether METNOS_SANDBOX explicitly disables it. |
wrap_command | Return a command wrapped by bwrap, or the original command when the wrapper is inactive. |
filesystem_extras | Resolve only arguments named by a signed fs:read capability in the form arg:<name>. The argument may narrow the scope, never widen it. |
undo_history_extras | Make writable only the current turn's managed backup directory when the manifest declares the supported reverse pattern. |
invocation_skills, skill_extras | Identify provider bindings required by this invocation, expose only that integration's home, and enable network access. Declared executors use provider:access; older manifests retain a compatibility path. |
mail_extras | For mail:read, expose the selected accounts' credential files read-only and enable network access. It does not mount the whole credential vault. |
dialog_extras | For dialog.user_input, make writable only the authenticated sender's dialog directory. |
python_package_roots | Find the package roots used by the current interpreter without exposing the whole home directory. |
status | Return the path, availability, explicit-disable flag, and effective state of bwrap. |
The profile mounts existing system paths read-only from among
/usr, /bin, /sbin, the library
directories, /etc, /opt,
/var/lib/python3, and /sys. It also adds:
/proc and a minimal /dev;/tmp;--unshare-user, --unshare-ipc,
--unshare-uts, and --die-with-parent are always
requested. The server profile currently adds neither a PID namespace nor
Landlock or a custom seccomp filter.
| Declaration | Effect on the profile |
|---|---|
fs:read / fs:write | Read-only or read-write mount. Fixed glob-like scopes are reduced to their root; arg: scopes resolve the concrete value only when the path exists. |
metnos:read, metnos:write, metnos:create | Translate closed semantic resources—such as spreadsheets, the persons registry, or proposals—to canonical Metnos-managed paths. |
index:read | Read-only access to the canonical root of the declared index. An unknown name grants nothing. |
systemd:read and system:read | Expose only known resources required for status inspection. Requesting network-interface information keeps the host network available. |
provider:access or skill:<name> | Read-write access to the integration home, including what OAuth refresh needs, plus host network access. |
mail:read | Read-only access to individual credential files for the requested account, plus host network access. |
network:* and net:* capabilities, external
integrations, and selected system-inspection requests prevent
--unshare-net from being added. The process then receives the
host network; bubblewrap does not restrict it to one host or provider.
Application controls and narrowly scoped credentials must enforce that
boundary.
If bwrap is not on PATH, or if
METNOS_SANDBOX is 0, off,
no, or false, wrap_command returns the
command unchanged. There is no third branch that automatically converts a
detection exception into an unavailable result.
The signed invocation-scope preflight still runs before the subprocess, and
Vaglio and executor-specific controls remain in force. They are not
equivalent to operating-system containment. Local executor results do not
automatically carry a field that reports the absence of bwrap;
operators must inspect status() or service diagnostics.
invoke_executor is the common execution point. Its relevant
order is:
bwrap is unavailable;sys.executable—the interpreter of the current Metnos environment—through wrap_command.The public entry point also passes through the central scheduler, which applies concurrency limits without changing placement, arguments, or the result contract.
wrap_command accepts an autonomy parameter, but
it is currently informational and does not select different profiles.
Containment comes from manifest capabilities and resources resolved for the
invocation. Consent, criticality, and admission belong to
policy and Vaglio, not to
the sandbox.
The documentation does not freeze a test count. Dedicated suites cover runtime and Python-environment mounts, i18n databases, provider integrations, mail credentials, and downgrade paths. The main suites are:
tests/runtime/safety/test_sandbox_runtime_bind.py tests/runtime/safety/test_sandbox_i18n_bind.py tests/runtime/safety/test_sandbox_skill_backed.py
The Rust modules also contain tests next to the common, Linux, Windows, and AppContainer implementations. Argument-construction tests can run without starting a real sandbox; system tests separately verify the mechanism available on the target platform.
Call sandbox.status() to inspect the current state. It checks
PATH at call time and returns bwrap_available,
bwrap_path, disabled_via_env, and
active; the module does not cache that result.
# Debian and Ubuntu sudo apt install bubblewrap # Fedora and RHEL sudo dnf install bubblewrap # Arch Linux sudo pacman -S bubblewrap
After installation, verify both status() and a real invocation
in the service environment. A different PATH or
METNOS_SANDBOX value can make the service behave differently
from an administrator's shell.
/etc, /opt, and /sys read-only when they exist. Read-only prevents modification, but does not guarantee confidentiality for every file under those roots.system:undo is an explicit exception. Undo must reconstruct resources known only from the runtime journal at execution time, so wrap_command passes the signed broker through directly. Vaglio and server-only placement remain necessary, but are not an operating-system sandbox.The remote client uses a common result contract, while
containment depends on the platform. The sandbox field names the
level actually applied; sandbox_downgrade_reason, when present,
explains why a weaker level was used.
| Platform | Current implementation | Downgrade behaviour |
|---|---|---|
| Linux | Bubblewrap with mount and PID namespaces, network separation when not requested, a private temporary directory, and a dedicated process group. Custom Landlock and seccomp are not implemented. | If bwrap is missing or disabled, execution is direct and logged; the result reports sandbox:"none". The process group still supports tree termination at the deadline. |
| Windows | AppContainer with capability-derived grants, itself placed inside a Job Object. AppContainer is attempted by default when supported. | System subprocesses, unanchored paths, scopes too broad for safe ACL changes, or an unavailable container cause an explicit downgrade to the Job Object. If the manifest requires AppContainer as its minimum, the invocation must not silently downgrade. |
| macOS | There is currently no native sandbox_macos.rs module. The common Unix path has no operational bubblewrap equivalent on macOS. | The run must not be presented as isolated: its effective level is none, although process-group deadline handling remains. |
The Job Object limits per-process memory, the number of active processes, and the lifetime of the tree. The process is created suspended, assigned to the Job Object, and only then resumed; a deadline terminates the whole job. AppContainer adds filesystem and network isolation through grants computed for the invocation.
Before remote execution, the client verifies the signed package and uses a provisioned, verified Python interpreter. These checks protect code provenance; they do not replace the containment level declared in the result.