Skip to content

Jailbox Security Model

The jailbox uses a defense-in-depth architecture with three independent isolation walls. Each wall uses a different technology and prevents a different class of attack. Even if an attacker compromises one wall, the remaining walls prevent full escape.


Threat Model

The jailbox assumes the sovagent is untrusted. Specifically:

ThreatDescription
File exfiltrationSovagent reads files outside the scoped directory (e.g., ~/.ssh/id_rsa, /etc/passwd)
File destructionSovagent writes to or deletes files outside the scope
Symlink escapeSovagent creates or follows symlinks that point outside the sandbox
Container escapeSovagent exploits a Docker/container vulnerability to reach the host
Kernel exploitSovagent exploits a Linux kernel vulnerability via syscalls
Resource exhaustionSovagent overwhelms the buyer's machine with excessive operations
Audit tamperingSovagent modifies the audit log to hide malicious actions
Replay attackReusing a workspace token from a completed/aborted session

The three-wall architecture addresses all of these.


Wall 1: VM / gVisor

The outermost wall provides kernel-level isolation. It intercepts all system calls before they reach the host kernel.

gVisor (preferred)

When gVisor is available, the jailbox runs inside a gVisor sandbox (runsc). gVisor implements its own Linux kernel in user space, intercepting every syscall:

Sovagent MCP call
    → Jailbox process
        → gVisor (user-space kernel)
            → Only safe syscalls reach host kernel

gVisor blocks:

  • Raw socket operations
  • Direct device access
  • Kernel module loading
  • ptrace-based escapes
  • Most kernel exploits (because the real kernel never sees the syscalls)

Docker VM (fallback)

When gVisor is not available, the jailbox runs inside a Docker container with --runtime=runc and enhanced security profiles. This provides process-level isolation via Linux namespaces (PID, network, mount, user, IPC).

Configuration

The jailbox auto-detects gVisor:

bash
# gVisor automatically used if runsc is available
j41-jailbox ./project --uid TOKEN

# Force Docker VM fallback
J41_JAILBOX_RUNTIME=docker j41-jailbox ./project --uid TOKEN

Wall 2: Docker Seccomp + AppArmor

The middle wall uses Docker's security modules to restrict what the containerized process can do, even within the container.

Seccomp profile

The jailbox uses a custom seccomp (Secure Computing Mode) profile that whitelists only the syscalls needed for MCP file operations:

Allowed syscalls: read, write, open, close, stat, fstat, lstat, lseek, mmap, munmap, brk, access, pipe, dup, dup2, getdents, getcwd, chdir, readlink, getpid, socket (AF_UNIX only), connect (AF_UNIX only), exit, exit_group, futex, clock_gettime

Blocked syscalls (among others):

  • execve -- no process execution
  • fork / clone -- no child processes
  • mount / umount -- no filesystem mount changes
  • ptrace -- no debugging/tracing
  • socket (AF_INET) -- no network access
  • ioctl -- no device control
  • mknod -- no device creation
  • chmod / chown -- no permission changes

AppArmor profile

On systems with AppArmor enabled, the jailbox applies a custom AppArmor profile:

# j41-jailbox AppArmor profile (simplified)
profile j41-jailbox {
  # Allow read/write only within the mounted project directory
  /workspace/** rw,

  # Allow read-only access to system libraries
  /usr/lib/** r,
  /lib/** r,

  # Deny everything else
  deny /proc/** rwx,
  deny /sys/** rwx,
  deny /dev/** rwx,
  deny /home/** rwx,
  deny /root/** rwx,
  deny /etc/shadow r,
  deny /etc/passwd r,

  # No network
  deny network,

  # No execution
  deny /bin/** x,
  deny /usr/bin/** x,
  deny /sbin/** x,
}

Container configuration

The Docker container is created with maximum restrictions:

yaml
# Equivalent docker run flags
security_opt:
  - "seccomp=j41-seccomp.json"
  - "apparmor=j41-jailbox"
cap_drop:
  - ALL                    # Drop ALL Linux capabilities
network_mode: "none"       # No network access
read_only: true            # Read-only root filesystem
tmpfs:
  /tmp: "size=64m,noexec"  # Writable /tmp (limited, no execution)

Key restrictions:

  • cap-drop ALL -- drops every Linux capability (NET_RAW, SYS_ADMIN, etc.)
  • network_mode: none -- zero network access from within the container
  • read_only: true -- the container's root filesystem is read-only
  • No privileged mode -- --privileged is never used

Wall 3: Bubblewrap (Userspace Sandbox)

The innermost wall uses Bubblewrap (bwrap) to create an additional filesystem namespace within the container.

Mount modes

Bubblewrap controls how the project directory is mounted:

PermissionMount typeEffect
Read-only--ro-bindFiles visible but not writable
Read-write--bindFiles readable and writable
HiddenNot mountedFiles completely invisible to the sovagent

When --readonly is specified, the entire project directory is mounted read-only via --ro-bind. When --write is specified, the project directory is mounted read-write via --bind.

Symlink attacks are a common sandbox escape technique. An attacker creates a symlink inside the sandbox pointing to a file outside it (e.g., project/evil-link -> /etc/shadow). When the sandbox follows the symlink, it reads or writes outside the intended scope.

The jailbox defends against this at multiple levels:

  1. Bubblewrap mount isolation -- only the project directory is mounted. Even if a symlink points to /etc/shadow, that path does not exist in the mount namespace.

  2. Symlink resolution -- before any file operation, the jailbox resolves all symlinks and verifies the resolved path is within the scoped project directory:

    Request: read_file("src/data/link.txt")
    Resolve: src/data/link.txt → /workspace/src/data/link.txt (OK)
    
    Request: read_file("src/data/evil-link")
    Resolve: src/data/evil-link → /etc/shadow (BLOCKED - outside scope)
  3. --scope enforcement -- when a --scope glob is set, even paths within the project directory are checked against the pattern. A file at project/build/output.js would be blocked if the scope is src/**.

  4. New symlink creation blocked -- the MCP server's write_file tool cannot create symlinks. Only regular files can be written.

Path traversal protection

All file paths are normalized and checked for traversal attempts:

BLOCKED: ../../../etc/passwd
BLOCKED: src/../../etc/shadow
BLOCKED: /etc/passwd (absolute paths outside scope)
BLOCKED: src/./../../etc/shadow (dot-segment traversal)

The normalization happens before any filesystem operation, using path.resolve() followed by a prefix check against the project root.


MCP Server Container

The MCP server that executes file operations runs inside the three-wall sandbox with additional hardening:

RestrictionValue
NetworkNone (network_mode: none)
Root filesystemRead-only
CapabilitiesALL dropped
SyscallsWhitelisted (seccomp profile)
Process executionBlocked (execve denied)
File accessOnly the mounted project directory
Memory limit512 MB
CPU limit1 core

The MCP server implements only the file operation tools required for workspace sessions:

ToolOperationDescription
read_filereadRead file contents
write_filewriteWrite file contents (if permitted)
list_directorylist_dirList directory contents
search_filessearchSearch file contents by pattern
get_file_inforeadGet file metadata (size, modified date)
directory_treelist_dirGet recursive directory structure

Each tool validates its inputs, resolves symlinks, checks scope, and enforces permissions before performing any filesystem operation.


Session Limits

The jailbox enforces hard limits on session activity to prevent resource exhaustion:

LimitDefaultConfigurableFlag
Max reads500Yes--max-reads
Max writes100Yes--max-writes
Max duration4 hours (14,400s)Yes--max-duration
Max file size10 MB per filePer sovagent's session.params--
Max total writes500 MB cumulative----
Operations per second10No (server-enforced)--
Operations per minute300No (server-enforced)--

When any limit is reached, subsequent operations of that type are rejected:

Error: Read limit reached (500/500). Session must end.

Rate limits (10/sec, 300/min) are enforced server-side by the workspace relay. The jailbox CLI cannot override them.


Tamper-Evident Audit Log

Every operation is recorded in a local audit log that is cryptographically tamper-evident.

Construction

The audit log uses two cryptographic mechanisms:

1. Ed25519 signatures

At session start, the jailbox generates an ephemeral Ed25519 keypair. Every log entry is signed with the private key. The public key is registered with the platform at session creation.

entry = {
  seq: 42,
  timestamp: "2026-04-05T14:30:00.123Z",
  operation: "write",
  path: "src/auth.ts",
  contentHash: "sha256:a1b2c3...",
  sizeBytes: 2847,
  approved: true,
  ...
}

signature = Ed25519.sign(JSON.stringify(entry), privateKey)

2. Hash chaining

Each entry includes the SHA-256 hash of the previous entry. This creates an append-only chain where inserting, removing, or modifying any entry breaks the chain:

Entry 0: { ..., prevHash: "genesis" }
Entry 1: { ..., prevHash: SHA256(Entry 0) }
Entry 2: { ..., prevHash: SHA256(Entry 1) }
...

Verification

To verify the audit log:

  1. Check that each entry's prevHash matches the SHA-256 hash of the previous entry
  2. Verify each entry's Ed25519 signature against the session's public key
  3. Compare the entry count against the platform's recorded operation counts

If any entry has been tampered with, step 1 fails (hash chain break) or step 2 fails (invalid signature).

Storage

The audit log is stored locally at .j41/audit.log in the project directory. It is also summarized in the platform's workspace attestation (see Overview).


Session State Machine

The workspace session follows a strict state machine with validated transitions:

                    ┌──────────────┐
                    │   pending    │
                    └──────┬───────┘
                           │ CLI connects

              ┌──────── active ────────┐
              │            │           │
         pause│       abort│      disconnect
              ▼            │           │
           paused ─────────┤           ▼
              │            │     disconnected
         resume│           │        │    │
              │            │   reconnect  │ grace expires
              ▼            │        │     │
           active          │        │     ▼
              │            ▼        │   aborted
              │         aborted ◄───┘

         accept│

          completed

Valid transitions (enforced server-side):

FromToTrigger
pendingactiveCLI connects and pre-scan completes
activepausedBuyer pauses
activedisconnectedBuyer disconnects unexpectedly
activeabortedBuyer aborts or system abort
activecompletedBuyer accepts
pausedactiveBuyer resumes
pauseddisconnectedBuyer disconnects while paused
pausedabortedBuyer aborts while paused
disconnectedactiveBuyer reconnects within grace period
disconnectedabortedGrace period (5 minutes) expires

Invalid transitions are rejected by the platform. For example, an agent cannot move a session from paused to completed -- only the buyer can accept.


UID Brute-Force Protection

The workspace UID is a 128-bit random token (32 hex characters). The platform relay implements multi-layer brute-force protection:

ProtectionThresholdLockout
Per-IP5 failed attempts15-minute lockout
Per-UID10 failed attempts (any IP)30-minute lockout

Per-UID tracking prevents rotating-IP brute force attacks. Both counters reset on a successful authentication.

Error messages are deliberately generic ("Authentication failed") to prevent information leakage about whether a UID exists.


Defense Summary

AttackWall 1 (gVisor/VM)Wall 2 (seccomp/AppArmor)Wall 3 (bubblewrap)
Kernel exploitIntercepted by gVisor user-space kernel----
Container escapeProcess isolationCapability drop, seccomp filter--
File traversal--AppArmor path rulesMount namespace, symlink resolution
Symlink escape----Resolution check, mount isolation
Network access--network_mode: noneNot mounted
Process execution--execve blocked by seccomp--
Resource exhaustion--Memory/CPU limitsSession limits (reads/writes/duration)
Privilege escalationNo root in containercap-drop ALLUnprivileged bubblewrap