← Indice documentazione Microprogettazione › sandbox

myclaw

sandbox — potente, ma inquadrata
Microprogettazione v1.0 — 21 aprile 2026
Quarto e ultimo documento classico di fase 1.

Pubblico: chi implementerà l'isolamento e la capability grant. Lettura: 25 min.
Direttiva di Roberto: "la sandbox deve essere abbastanza potente da permettere azioni concrete di myclaw." Questo doc è scritto tenendo quella riga in testa. Qui non si castra l'agente per paura: si decide quale potenza gli serve e come contenerla — non se gliela diamo.

Indice

  1. Il bilancio potenza/sicurezza, esplicitato
  2. La filosofia: "abbastanza potente"
  3. I tre profili bubblewrap
  4. Matrice permessi per autonomy level
  5. La mount strategy
  6. Rete: outbound whitelist, non firewall totale
  7. Capability Linux e seccomp
  8. Forbidden paths: la lista hard-coded
  9. Quote di esecuzione (CPU, RAM, I/O, tempo)
  10. Integrazione con il Tool dispatch
  11. Docker opzionale: quando e perché
  12. Contratto Python
  13. Alternative considerate
  14. Test di conformità
  15. Riferimenti

1. Il bilancio potenza/sicurezza, esplicitato

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:

  1. Nominare le capability, non lo stile Unix. Invece di "posso scrivere file?", "posso scrivere dove?". La granularità è il path + tipo di operazione.
  2. Due perimetri: gateway vs sandbox. Il gateway (vedi gateway §8) è pesantemente ristretto. La sandbox è selettivamente permissiva entro aree espliciti.
  3. Default narrow, allargabile con giustificazione. La prima invocazione di un tool restrittivo apre solo ciò che serve; se la Policy accetta di ampliare, l'ampliamento è loggato e revocabile.

2. La filosofia: "abbastanza potente"

"Abbastanza potente" significa: myclaw può fare i compiti domestici concreti, non finti. Concretamente, tutte queste operazioni devono riuscire nel profilo di default (Supervised):

OperazioneProfilo di default
Leggere qualunque file che Roberto possa leggere (tranne forbidden paths)
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 statussì, senza approvazione
Eseguire apt update, docker pull, altri con effetto globalesì, con approvazione
Git operations in una repo qualsiasi di /opt (eccetto myclaw stessa)read liberi, write con approvazione
Chiamate HTTP outbound verso qualunque URLsì (con timeout e body cap)
Usare llama.cpp locale su porta 8080, supra su 8801, searxng su 8090
Toccare /etc, ~/.ssh, ~/.aws, ~/.gnupg, /var/backups, ~/.config/claudeno, mai, in nessun profilo
Scrivere in /opt/myclaw/src/ (auto-modifica codice)no, mai (forbidden)
Installare pacchetti Python fuori dalla venv di sandboxno
Raw socket, ptrace, kexec, syscall esoticino (seccomp)

Questa tabella è il contratto operativo. Tutto il resto del documento è come ottenerla.

3. I tre profili bubblewrap

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.

Profilo 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.

Profilo 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).

Profilo 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.

4. Matrice permessi per autonomy level

CapacitàReadOnlySupervisedFull
Leggere /opt/myclaw/workspace/
Leggere home di Roberto (eccetto forbidden)
Scrivere in workspace/, /tmp/myclaw/no
Scrivere in ~/Downloads, ~/Documentsnoapprove
Scrivere in /opt/* (escluso myclaw/src)noapproveapprove
Scrivere in /opt/myclaw/src (codice propria)nonono
Leggere forbidden paths (ssh, aws, etc.)nonono
Shell allowlist (df, du, journalctl, git-read, ...)
Shell non-allowlistnoapprove
HTTP outboundsì (GET)sì (GET, POST)sì (qualunque)
Raw socket, ptrace, syscall esoticino (seccomp)no (seccomp)no (seccomp)
Creare processi figlisì (max 10)sì (max 20)sì (max 50)
Tempo CPU per tool call10s30s120s
RAM per tool call256 MB512 MB1 GB

5. La mount strategy

La regola: tutto è read-only tranne quello che serve. "Serve" è dichiarato per-profilo in §3, non lasciato al caso.

Mount per il profilo Supervised (annotato)

Path hostModalitàMotivo
/read-only bindBase per leggere binari, librerie, config pubbliche
/opt/myclaw/workspaceread-writeL'agente scrive qui liberamente
/tmp/myclawread-writeScratch area per tool
/tmptmpfs (nuovo)Isolato dall'host; scratch volatile
~/.cachetmpfs (nuovo)Eventuali cache non persistono cross-call
/etc/sshtmpfs vuotoMaschera i file di sistema con una dir vuota
~/.ssh, ~/.aws, ~/.gnupg, ~/.config/claudetmpfs vuotiForbidden paths — §8
/opt/myclaw/srctmpfs vuotoL'agente non vede il proprio codice, non può modificarlo
/var/backupstmpfs vuotoMai toccabile
/procprocStandard bwrap
/devdev minimoNo /dev/mem, no /dev/kmem, no /dev/sd*
Pattern "tmpfs vuoto sopra forbidden": invece di denegare l'accesso con permission error (che può essere diagnosticato e aggirato), si sovrappone una directory vuota. L'agente vede "non c'è niente" e non sospetta che ci sia qualcosa di nascosto sotto. È sia sicurezza che UX.

6. Rete: outbound whitelist, non firewall totale

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.

Decisione: user-space, non iptables

DECISIONE v1: nessun firewall kernel-level per la sandbox. La rete è condivisa con l'host (--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.
Motivo: iptables per-user richiede capability NET_ADMIN, che non vogliamo dare al gateway. Una whitelist in user-space è meno rigida ma sufficiente per il modello di minaccia domestico.

Outbound whitelist default

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.

7. Capability Linux e seccomp

Capability Bounding Set

Via systemd del gateway (già in gateway §8): zero capability. Il bwrap non aggiunge nulla. Eredità pulita.

Seccomp filter

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.

8. Forbidden paths: la lista hard-coded

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",
]
Caveat su /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.

Modifica dei forbidden paths (il "rito")

Se Roberto vuole aggiungere o rimuovere un forbidden path:

  1. Edit manuale di src/myclaw/sandbox/forbidden.py (fuori da myclaw CLI).
  2. Restart del gateway: systemctl --user restart myclaw-gateway.
  3. Al boot, il runtime scrive in audit log un record "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.

9. Quote di esecuzione (CPU, RAM, I/O, tempo)

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] \
  -- "$@"
QuotaReadOnlySupervisedFull
CPUQuota100%100%200%
MemoryMax256M512M1G
TasksMax102050
RuntimeMaxSec1030120
IOWeight205080

10. Integrazione con il Tool dispatch

Policy OK decisione: allow Sandbox dispatcher 1. scegli profilo (da autonomy) 2. compone cmd bwrap + systemd-run 3. scrive stdin (args JSON) 4. aspetta output + exit code 5. parse + enforce quote wall-time Tool process dentro bwrap + cgroup esegue Python / shell emette JSON su stdout Parse result o ToolError wrap untrusted se returns_untrusted Il Tool process non conosce il resto del sistema: riceve args, risponde, muore.
Figura 1 — Dispatch sandbox. Il tool Python gira in un processo figlio dedicato dentro bwrap + systemd-run, privo di qualsiasi contesto del gateway. Comunica solo via stdin/stdout JSON.

11. Docker opzionale: quando e perché

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+).

Quando Docker

Docker runtime config

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>
Docker è opt-in: richiede docker installato + permessi di docker per roberto. Non è pre-requisito di fase 1. I tool di fase 1 girano tutti in bwrap.

12. Contratto Python

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."""
        ...

13. Alternative considerate

AlternativaPerché 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.

14. Test di conformità

InvarianteTest
Forbidden paths invisibili in ogni profiloDa dentro bwrap (tutti i profili): ls /etc/ssh, ls ~/.ssh → stringa vuota. cat ~/.aws/credentials → not found.
Scrittura bloccata fuori dai rw-bindsDa Supervised: touch /opt/<altro-sibling>/test → EROFS. touch /opt/myclaw/workspace/test → OK.
Timeout applicatoTool che dorme 60s con timeout_s=5timed_out=True, exit_code diverso da 0 entro 6s.
OOM applicatoTool che alloca 2GB con memoria 512M → oom_killed=True.
Seccomp blocca ptracestrace -p $$ da dentro bwrap → EPERM.
Seccomp blocca io_uringProgramma di test che chiama io_uring_setup → EPERM.
ReadOnly davvero read-onlyDa ReadOnly: echo x > /tmp/test → EROFS (ok solo tmpfs). touch workspace/foo → EROFS.
Network condivisa ma outbound filtrato a livello toolweb_fetch verso dominio fuori allowlist → ToolPermissionError prima della chiamata. Socket raw verso IP → EPERM via seccomp.
Quote CPU rispettateTool CPU-bound in Supervised con CPUQuota=100% → non supera 1 core anche con forks.
Max processiFork bomb in Supervised → killato a TasksMax=20, altri processi dell'host non intaccati.
Audit log scritto anche su killTool killato da OOM o timeout → audit log contiene record con outcome e oom_killed/timed_out veri.
dry_run_simulate senza persistenceTool fs_write via dry_run_simulate → il file non esiste sull'host al termine.
Forbidden modify detectionAl 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 traversalTool fs_read("/opt/myclaw/workspace/../src/config.py")Path.resolve() risolve in /opt/myclaw/src/config.py → forbidden rejected PRIMA del bwrap.

15. Riferimenti

RiferimentoCosa 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".
LandlockIntegrato implicitamente in bubblewrap recente; potrebbe essere usato direttamente in una fase 2 di hardening.
OpenHands runtimeDocker runtime come opzione per isolamento robusto (§11).
Architettura intro §3I 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.

Continua a leggere

fase 1 completa
Landing microprogettazione
3 trasversali + 4 classici = 7 doc approvati. Fase 1 di microprogettazione è completa.
microprogettazione · 20 min
tool
I Tool che la Sandbox esegue. I due doc sono strettamente accoppiati.
chiusura fase 0
Prospettive & Giudizio
Il giudizio che ha aperto questa fase. Rileggilo: ogni critica bloccante è ora coperta da un doc.
home
← Indice documentazione
Tutti i documenti.

myclaw — sandbox microprogettazione v1.0 — 2026-04-21
Ultimo dei 4 classici. Fase 1 di microprogettazione completa.