Skip to the content.

Design

Architecture Overview

AgentBox layers three isolation mechanisms between the AI agent and the host system. Each layer is enforced by the Linux kernel or OpenShell supervisor — none are advisory.

┌─────────────────────────────────────────────────────────┐
│ Host (Fedora 44)                                        │
│  goose-sandbox CLI  ──── OpenShell Gateway (gRPC)       │
│                            │                            │
│  ┌─────────────────────────┼──────────────────────────┐ │
│  │ Podman Container (bridge network)                  │ │
│  │  Supervisor (PID 1, UID 0)                         │ │
│  │    ├── Landlock LSM (filesystem)                   │ │
│  │    ├── seccomp BPF (syscalls)                      │ │
│  │    ├── veth0 10.200.0.1 ← CONNECT proxy ← OPA     │ │
│  │    │                                               │ │
│  │  ┌─┤── Nested Network Namespace ──────────────────┐│ │
│  │  │ │  veth1 10.200.0.2                            ││ │
│  │  │ │  Agent process (UID 1000)                    ││ │
│  │  │ │    └── goose run / session                   ││ │
│  │  │ │         HTTP_PROXY → veth0:port              ││ │
│  │  └─┤─────────────────────────────────────────────┘│ │
│  └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘

Key Design Decisions

D1: Shim Image (BYOC without baking)

Decision: The container image contains only runtime dependencies (bash, git, python3, curl, etc.). The Goose binary, config, and AgentFS are bind-mounted from the host at runtime.

Rationale:

Trade-off: Requires enable_bind_mounts = true in gateway config, which is a security-relevant setting. Mitigated by Landlock policy restricting which paths the agent can actually access.

D2: Provider URL dual-resolution

Decision: All provider base_url values use host.containers.internal instead of localhost.

Rationale:

Prerequisite: base_hosts_file = "none" in ~/.config/containers/containers.conf to prevent the host’s /etc/hosts entry from being copied into containers and shadowing Podman’s injection.

D3: UID mapping with keep-id

Decision: OPENSHELL_PODMAN_USERNS=keep-id:uid=1000,gid=1000

Rationale: Default rootless Podman maps container UID 0 → host user, making UID 1000 map to a subordinate UID that cannot write to bind-mounted host directories. keep-id maps container UID 1000 → host user, enabling write access to the project directory bind-mount. The supervisor (UID 0 inside container) retains container-scoped capabilities for namespace setup.

D4: Config read/write split

Decision: Goose config directory is bind-mounted read-only. The wrapper copies essential files to /tmp/goose-config/ inside the sandbox and sets XDG_CONFIG_HOME=/tmp/goose-config.

Rationale: Goose writes a permission.yaml.lock file on startup. Mounting config as read-write would allow the sandboxed agent to modify its own config (provider keys, extensions). The copy-to-tmp approach allows the lock file write without exposing host config.

D5: Sandbox-local session DB

Decision: Session database lives at /tmp/goose-data/ inside the sandbox, not shared with the host.

Rationale: SQLite WAL mode uses mmap and shared memory for concurrent access. These mechanisms do not work across the Podman user namespace boundary (SQLITE_CANTOPEN error code 14). Sharing a single DB file via bind-mount causes database corruption or lock failures.

Trade-off: Sessions are lost when a sandbox is deleted. Mitigated by session resume (--history flag replays previous messages) and named sandboxes that persist across multiple goose-sandbox run invocations.

D6: Unique sandbox names

Decision: Sandbox name = session name. The wrapper enforces uniqueness by reusing existing sandboxes or deleting errored ones.

Rationale: OpenShell returns AlreadyExists on duplicate sandbox names. Goose session names are not unique. Mapping 1:1 gives a consistent lifecycle: create → use → delete.

D7: Background delete

Decision: goose-sandbox delete runs the OpenShell delete asynchronously and returns immediately.

Rationale: OpenShell supervisor has a hardcoded ~45-second graceful shutdown timeout. Blocking the terminal for 45 seconds on every delete is unacceptable for interactive use. The user can verify deletion via goose-sandbox list.

D8: Factory pattern for skill artifacts

Decision: The goose-openshell skill owns source artifacts (goose-sandbox wrapper, policy.yaml, Containerfile). setup.sh installs them to standard locations. The wrapper is callable from any project directory.

Rationale: Skill-as-factory means one source of truth with installable outputs. Updates flow through setup.sh with md5sum change detection. No symlinks, no PATH manipulation beyond ~/.local/bin/.

Security Boundaries

Layer Mechanism Enforced By Revocable?
Filesystem Landlock LSM Linux kernel No — locked at sandbox creation, inherited by all children
Network OPA/Rego policy via CONNECT proxy OpenShell supervisor No — proxy is sole egress path
Process seccomp BPF + nested namespace Linux kernel + supervisor No
Identity /proc/{pid}/exe matching OpenShell policy engine No — binary path is immutable for running process

Network Policy Model

Deny-by-default. Each allowed endpoint is declared with:

network_policies:
  litellm:
    endpoints:
      - host: host.containers.internal
        port: 4000
    binaries:
      - path: /usr/local/bin/goose

Component Inventory

Component Source Installed At Owner
OpenShell Gateway NVIDIA RPM ~/.local/bin/openshell fedora-openshell skill
OpenShell Supervisor OCI volume image Injected by gateway NVIDIA
Goose Shim Image Containerfile localhost/goose-shim:latest goose-openshell skill
goose-sandbox wrapper goose-sandbox (skill dir) ~/.local/bin/goose-sandbox goose-openshell skill
Sandbox policy policy.yaml (skill dir) ~/.config/openshell/goose-policy.yaml goose-openshell skill
Bash completion goose-sandbox-completion.bash ~/.local/share/bash-completion/completions/ goose-openshell skill