sandbox — potente, ma inquadrata
Una sandbox può essere restrittiva — al limite impedire tutto. In quel
caso protegge, ma myclaw non agisce. Una sandbox può essere permissiva —
al limite subprocess.run() diretto. In quel caso l'agente è
potente, ma un bug LLM può causare disastri. Il punto giusto è intermedio,
e questo doc lo fissa esplicitamente.
Tre principi che guidano ogni decisione qui:
"Abbastanza potente" significa: myclaw può fare i compiti domestici concreti, non finti. Concretamente, tutte queste operazioni devono riuscire nel profilo di default (Supervised):
| Operazione | Profilo di default |
|---|---|
| Leggere qualunque file che Roberto possa leggere (tranne forbidden paths) | sì |
Scrivere file in workspace/ e in /tmp/myclaw/ | sì, senza approvazione |
Scrivere file in ~/Downloads/, ~/Documents/ | sì, con approvazione singola (batching attivabile) |
Eseguire df, du, journalctl, systemctl status | sì, senza approvazione |
Eseguire apt update, docker pull, altri con effetto globale | sì, con approvazione |
Git operations in una repo qualsiasi di /opt (eccetto myclaw stessa) | read liberi, write con approvazione |
| Chiamate HTTP outbound verso qualunque URL | sì (con timeout e body cap) |
| Usare llama.cpp locale su porta 8080, supra su 8801, searxng su 8090 | sì |
Toccare /etc, ~/.ssh, ~/.aws, ~/.gnupg, /var/backups, ~/.config/claude | no, mai, in nessun profilo |
Scrivere in /opt/myclaw/src/ (auto-modifica codice) | no, mai (forbidden) |
| Installare pacchetti Python fuori dalla venv di sandbox | no |
| Raw socket, ptrace, kexec, syscall esotici | no (seccomp) |
Questa tabella è il contratto operativo. Tutto il resto del documento è come ottenerla.
bubblewrap (bwrap) è lo strumento scelto: leggero,
user-space, no privilegi necessari, compone namespace in stile container.
Tre profili, uno per livello di autonomy. Ogni tool eredita il
profilo del livello corrente, con override opzionali dichiarati nel tool
stesso.
readonly (autonomy=ReadOnly)bwrap \
--unshare-all --share-net \
--die-with-parent --new-session \
--ro-bind / / \
--tmpfs /tmp --tmpfs /home/roberto/.cache \
--bind /opt/myclaw/workspace/.audit /opt/myclaw/workspace/.audit \
# niente scrittura da nessun'altra parte
--proc /proc --dev /dev \
--unsetenv TMPDIR \
--setenv HOME /home/roberto \
--seccomp /opt/myclaw/sandbox-profiles/seccomp-readonly.bpf \
-- "$@"
Filesystem read-only ovunque; scrittura solo su /tmp (tmpfs) e su .audit/ per registrare la trace.
supervised (autonomy=Supervised, default)bwrap \
--unshare-all --share-net \
--die-with-parent --new-session \
--ro-bind / / \
--tmpfs /tmp --tmpfs /home/roberto/.cache \
--bind /opt/myclaw/workspace /opt/myclaw/workspace \
--bind /tmp/myclaw /tmp/myclaw \
# forbidden paths blinded
--tmpfs /etc/ssh \
--tmpfs /home/roberto/.ssh --tmpfs /home/roberto/.aws \
--tmpfs /home/roberto/.gnupg --tmpfs /home/roberto/.config/claude \
--proc /proc --dev /dev \
--setenv HOME /home/roberto \
--seccomp /opt/myclaw/sandbox-profiles/seccomp-supervised.bpf \
-- "$@"
Scrittura abilitata in workspace/ e /tmp/myclaw/.
Le forbidden paths sono sovrapposte con tmpfs vuoti — l'agente vede una
directory vuota al posto dei file reali (non una permission error: non sa
nemmeno cosa c'è sotto).
full (autonomy=Full, session breve)bwrap \
--unshare-all --share-net --unshare-ipc --unshare-uts --unshare-pid \
--die-with-parent --new-session \
--bind / / # default writable per TUTTO ciò che l'utente può scrivere
# forbidden paths ancora blinded
--tmpfs /etc/ssh \
--tmpfs /home/roberto/.ssh --tmpfs /home/roberto/.aws \
--tmpfs /home/roberto/.gnupg --tmpfs /home/roberto/.config/claude \
--tmpfs /var/backups --tmpfs /opt/myclaw/src \
--proc /proc --dev /dev \
--setenv HOME /home/roberto \
--seccomp /opt/myclaw/sandbox-profiles/seccomp-full.bpf \
-- "$@"
full è per sessioni esplicite e brevi (myclaw session
--level full --for 10m). I forbidden paths restano non negoziabili.
La rete resta aperta (stesso profilo seccomp), ma il logging audit è
esteso: ogni syscall rilevante viene tracciata.
| Capacità | ReadOnly | Supervised | Full |
|---|---|---|---|
Leggere /opt/myclaw/workspace/ | sì | sì | sì |
| Leggere home di Roberto (eccetto forbidden) | sì | sì | sì |
Scrivere in workspace/, /tmp/myclaw/ | no | sì | sì |
Scrivere in ~/Downloads, ~/Documents | no | approve | sì |
Scrivere in /opt/* (escluso myclaw/src) | no | approve | approve |
Scrivere in /opt/myclaw/src (codice propria) | no | no | no |
| Leggere forbidden paths (ssh, aws, etc.) | no | no | no |
| Shell allowlist (df, du, journalctl, git-read, ...) | sì | sì | sì |
| Shell non-allowlist | no | approve | sì |
| HTTP outbound | sì (GET) | sì (GET, POST) | sì (qualunque) |
| Raw socket, ptrace, syscall esotici | no (seccomp) | no (seccomp) | no (seccomp) |
| Creare processi figli | sì (max 10) | sì (max 20) | sì (max 50) |
| Tempo CPU per tool call | 10s | 30s | 120s |
| RAM per tool call | 256 MB | 512 MB | 1 GB |
La regola: tutto è read-only tranne quello che serve. "Serve" è dichiarato per-profilo in §3, non lasciato al caso.
| Path host | Modalità | Motivo |
|---|---|---|
/ | read-only bind | Base per leggere binari, librerie, config pubbliche |
/opt/myclaw/workspace | read-write | L'agente scrive qui liberamente |
/tmp/myclaw | read-write | Scratch area per tool |
/tmp | tmpfs (nuovo) | Isolato dall'host; scratch volatile |
~/.cache | tmpfs (nuovo) | Eventuali cache non persistono cross-call |
/etc/ssh | tmpfs vuoto | Maschera i file di sistema con una dir vuota |
~/.ssh, ~/.aws, ~/.gnupg, ~/.config/claude | tmpfs vuoti | Forbidden paths — §8 |
/opt/myclaw/src | tmpfs vuoto | L'agente non vede il proprio codice, non può modificarlo |
/var/backups | tmpfs vuoto | Mai toccabile |
/proc | proc | Standard bwrap |
/dev | dev minimo | No /dev/mem, no /dev/kmem, no /dev/sd* |
L'agente deve poter chiamare LLM, fare web fetch, usare SearXNG. Un firewall totale lo castra (direttiva di Roberto). Strategia: outbound concesso, con dichiarazione dei destinatari e rate-limit.
--share-net). Il controllo è
a livello tool: web_fetch valida il dominio contro
l'outbound whitelist; altri tool che fanno rete hanno la loro whitelist.
outbound_allowlist:
# LLM providers (via supra, già controllati lì)
- api.anthropic.com
- api.openai.com
- generativelanguage.googleapis.com
# Servizi locali di casa
- 127.0.0.1:*
- 192.168.1.0/24:* # LAN
- localhost
# Telegram
- api.telegram.org
# Web fetch general purpose (si allarga con permesso)
- "*" # catch-all: tool web_fetch può verso chiunque, ma con audit
outbound_denylist:
# Niente cloud personale di Roberto
- "*.dropbox.com"
- "*.gdrive.google.com" # tranne lettura via API autenticata, futuro
- "*.icloud.com"
# Niente SMTP / mail server diretti
- "*:25"
- "*:465"
- "*:587"
# Niente botnet ports suspicious
- "*:6667"
- "*:6697"
Per i tool "verso esterno" (es. telegram_send, futuro
email_send) l'outbound è sempre via un API specifico: il tool
stesso incapsula il destinatario, non l'LLM.
Via systemd del gateway (già in gateway §8): zero capability. Il bwrap non aggiunge nulla. Eredità pulita.
Tre profili seccomp in /opt/myclaw/sandbox-profiles/seccomp-{readonly,supervised,full}.bpf.
Partono dalla allowlist di systemd's @system-service, aggiungono
esplicitamente:
allowed_syscalls:
# I/O base
- read, write, pread64, pwrite64, readv, writev
- open, openat, close, fstat, stat, lstat, newfstatat
- lseek, fsync, fdatasync
# Process/thread
- clone, fork, execve, execveat
- wait4, waitid, exit, exit_group
- kill, tgkill (solo su proprio gruppo)
- getpid, getppid, gettid
# Memory
- mmap, munmap, mprotect, brk, madvise
# Signals
- rt_sigaction, rt_sigprocmask, rt_sigreturn
# Net (solo AF_INET/INET6/UNIX)
- socket (filtered), connect, bind, accept, listen
- send, sendto, recv, recvfrom, sendmsg, recvmsg
- setsockopt, getsockopt, getsockname, getpeername
# Misc utili
- nanosleep, clock_gettime, clock_nanosleep
- pipe, pipe2, dup, dup2, dup3
denied_syscalls:
- ptrace # no debugging processi esterni
- kexec_load, kexec_file_load # no kernel swap
- init_module, delete_module # no kernel module
- reboot, sethostname
- mount, umount2 # no filesystem mount arbitrario
- iopl, ioperm # no I/O diretto hardware
- bpf # no eBPF program loading
- userfaultfd # no user-space page fault tricks
- perf_event_open # no perf instrumentation
- keyctl, add_key, request_key # no kernel keyring
- io_uring_setup, io_uring_register # surface asincrona che preferiamo evitare
- pivot_root # no chroot escape tricks
- unshare, setns (filtered) # bwrap li usa, nested no
Il profilo readonly ha inoltre tutte le syscall di scrittura filtrate a EPERM, oltre al filesystem ro.
Questi path non sono configurabili via YAML. Vivono come costante
Python in src/myclaw/sandbox/forbidden.py. Una modifica richiede
cambio di codice + riavvio del gateway + entry esplicita in audit log
("constitution modified").
FORBIDDEN_PATHS = [
# Sistema
"/etc",
"/root",
"/boot",
"/sys",
"/var/log", # leggibili via journalctl allowlist, non tramite fs_read
"/var/backups",
"/var/spool/cron",
# Credenziali utente
"~/.ssh",
"~/.gnupg",
"~/.aws",
"~/.azure",
"~/.gcloud",
"~/.kube",
"~/.docker",
"~/.config/claude", # configurazione di Claude Code — off-limits
"~/.config/gh",
"~/.netrc",
"~/.pypirc",
"~/.npmrc",
# Browser / password managers
"~/.mozilla",
"~/.config/google-chrome",
"~/.config/chromium",
"~/.config/Bitwarden",
"~/.password-store",
# Progetti sibling (non sono "miei") -- esempi, da adattare all'ambiente
"/opt/suprastructure", # read OK via fs_read se autorizzato esplicitamente in doc
"/opt/", # es. assistente domotico
"/opt/", # es. bot specializzato
"/opt/",
"/opt/",
# Il codice di myclaw stesso
"/opt/myclaw/src",
"/opt/myclaw/.claude",
"/opt/myclaw/config/secrets.env",
]
/opt/suprastructure/: suprastructure è
un package Python che myclaw consuma via pip install -e.
Il codice suprastructure è leggibile dall'agente via fs_read
solo se esplicitamente autorizzato in una ApprovalRequest
(la classe di azione si chiama "fs_read:/opt/suprastructure/*").
Non è forbidden-hard, ma è guarded by default. Scrittura: forbidden sempre.
Se Roberto vuole aggiungere o rimuovere un forbidden path:
src/myclaw/sandbox/forbidden.py (fuori da myclaw CLI).systemctl --user restart myclaw-gateway."forbidden_paths_modified" con diff e timestamp.Non c'è un endpoint API per modificare questa lista. Non c'è un comando myclaw. È un atto deliberato, low-frequency, tracciato.
Applicate via systemd-run --scope --user wrappando la bwrap. Cosi stringono anche l'albero dei processi figli.
# Wrapper per profilo supervised
systemd-run --scope --user \
--property=CPUQuota=100% \
--property=MemoryMax=512M \
--property=MemoryHigh=384M \
--property=IOWeight=50 \
--property=TasksMax=20 \
--property=RuntimeMaxSec=30 \
bwrap [profile args] \
-- "$@"
| Quota | ReadOnly | Supervised | Full |
|---|---|---|---|
| CPUQuota | 100% | 100% | 200% |
| MemoryMax | 256M | 512M | 1G |
| TasksMax | 10 | 20 | 50 |
| RuntimeMaxSec | 10 | 30 | 120 |
| IOWeight | 20 | 50 | 80 |
Per la maggior parte dei tool, bwrap + seccomp è sufficiente e leggero. Docker è supportato come runtime alternativo per tool che richiedono un isolamento più forte (es. quando si esegue codice arbitrario in un neurone nuovo, fase 6+).
docker run --rm \
--network=host \ # allineato a bwrap; niente bridge custom
--user $(id -u):$(id -g) \
--read-only \
--tmpfs /tmp \
--volume /opt/myclaw/workspace:/workspace:ro \
--security-opt=no-new-privileges \
--security-opt seccomp=/opt/myclaw/sandbox-profiles/seccomp-docker.json \
--cap-drop ALL \
--memory 512m --cpus 1 \
--rm \
myclaw-tool-sandbox:latest <cmd>
from typing import Protocol, Literal
from dataclasses import dataclass
from pathlib import Path
@dataclass
class SandboxRequest:
tool_name: str
tool_module_path: Path # il .py del tool
args: dict # gia validati contro schema
autonomy: Literal["readonly", "supervised", "full"]
timeout_s: int # override del default di profilo
extra_ro_binds: list[Path] = None
extra_rw_binds: list[Path] = None
@dataclass
class SandboxResult:
exit_code: int
stdout: bytes
stderr: bytes
wall_time_ms: int
oom_killed: bool
timed_out: bool
class Sandbox(Protocol):
async def run(self, req: SandboxRequest) -> SandboxResult:
"""Esegue un Tool in un profilo bwrap+systemd-run. Non solleva su errori tool."""
...
async def dry_run_simulate(self, req: SandboxRequest) -> SandboxResult:
"""Esegue in profilo tmpfs totale: qualunque side-effect è temporaneo."""
...
| Alternativa | Perché scartata (o rimandata) |
|---|---|
| Firejail | Più UI-friendly ma richiede installazione suid. Bwrap è user-space puro, preferibile. |
| nsjail | Feature comparabili ma meno diffuso; bwrap è in standard package su Ubuntu/Debian. |
| Docker come default | Overhead (daemon, image pull) sproporzionato per 20 tool call al giorno. Bwrap in process fork è < 10ms setup. |
| gVisor / Kata | Isolamento più forte, overhead molto più alto, richiede runc-compatibile. Overkill per casa. |
| Landlock senza bwrap | Landlock è eccellente per file system ma richiede kernel 5.13+ e non copre PID/UTS/IPC namespace. Bwrap combina Landlock più unshare namespace in un command. |
| Firewall iptables per sandbox | Richiede NET_ADMIN. Whitelist applicativa in user-space è più ergonomica e sufficiente per il modello di minaccia. |
| No sandbox per tool "sicuri" (es. time_now) |
Rifiutato: anche un time_now banale gira in bwrap (overhead
trascurabile). Uniformità nell'invocazione > micro-ottimizzazione.
|
| Invariante | Test |
|---|---|
| Forbidden paths invisibili in ogni profilo | Da dentro bwrap (tutti i profili): ls /etc/ssh, ls ~/.ssh → stringa vuota. cat ~/.aws/credentials → not found. |
| Scrittura bloccata fuori dai rw-binds | Da Supervised: touch /opt/<altro-sibling>/test → EROFS. touch /opt/myclaw/workspace/test → OK. |
| Timeout applicato | Tool che dorme 60s con timeout_s=5 → timed_out=True, exit_code diverso da 0 entro 6s. |
| OOM applicato | Tool che alloca 2GB con memoria 512M → oom_killed=True. |
| Seccomp blocca ptrace | strace -p $$ da dentro bwrap → EPERM. |
| Seccomp blocca io_uring | Programma di test che chiama io_uring_setup → EPERM. |
| ReadOnly davvero read-only | Da ReadOnly: echo x > /tmp/test → EROFS (ok solo tmpfs). touch workspace/foo → EROFS. |
| Network condivisa ma outbound filtrato a livello tool | web_fetch verso dominio fuori allowlist → ToolPermissionError prima della chiamata. Socket raw verso IP → EPERM via seccomp. |
| Quote CPU rispettate | Tool CPU-bound in Supervised con CPUQuota=100% → non supera 1 core anche con forks. |
| Max processi | Fork bomb in Supervised → killato a TasksMax=20, altri processi dell'host non intaccati. |
| Audit log scritto anche su kill | Tool killato da OOM o timeout → audit log contiene record con outcome e oom_killed/timed_out veri. |
| dry_run_simulate senza persistence | Tool fs_write via dry_run_simulate → il file non esiste sull'host al termine. |
| Forbidden modify detection | Al boot, se forbidden.py è stato modificato rispetto all'ultimo commit git (o hash cached), emette record audit "forbidden_paths_modified" + diff. |
| Escape tentativo via path traversal | Tool fs_read("/opt/myclaw/workspace/../src/config.py") → Path.resolve() risolve in /opt/myclaw/src/config.py → forbidden rejected PRIMA del bwrap. |
| Riferimento | Cosa abbiamo preso |
|---|---|
| bubblewrap (GitHub containers/bubblewrap) | Tutte le invocazioni bwrap nei §3 e §5. |
| systemd.resource-control(5) | Direttive cgroup per le quote del §9. |
| seccomp BPF (kernel docs) | Whitelist/blacklist del §7. |
| zeroclaw (repo) | Ispirazione per la distinzione in 3 profili di autonomy e per l'approccio "sandbox a strati". |
| Landlock | Integrato implicitamente in bubblewrap recente; potrebbe essere usato direttamente in una fase 2 di hardening. |
| OpenHands runtime | Docker runtime come opzione per isolamento robusto (§11). |
| Architettura intro §3 | I 4 strati: la sandbox è lo strato 3, interposto tra Policy e Tool. |
| Direttiva di Roberto (questa sessione) | "Sandbox abbastanza potente per azioni concrete". Guida filosofica del §2. |
myclaw — sandbox microprogettazione v1.0 — 2026-04-21
Ultimo dei 4 classici. Fase 1 di microprogettazione completa.