Skip to main content

Skill & MCP Container Sandboxing

Every MCP server in AgentArea runs in its own isolated container, scoped to a workspace. No two workspaces share network access, filesystem state, or container processes.

Overview

AgentArea’s skill system exposes MCP (Model Context Protocol) server capabilities to agents. Each MCP server instance runs inside a dedicated container managed by the MCP Manager — a Go service responsible for the full container lifecycle: creation, health monitoring, automatic restart, and cleanup. The security model mirrors a VPC architecture: workspace boundaries are enforced at the network, filesystem, and process layers, so a compromised or misbehaving skill container cannot affect other workspaces or the platform control plane.

Isolation Architecture

┌──────────────────────────────────────────────────────────┐
│                     AgentArea Platform                   │
│                                                          │
│  ┌─────────────┐        ┌──────────────────────────────┐ │
│  │  Python API  │──────>│       MCP Manager (Go)       │ │
│  │  (FastAPI)   │ Redis │  ┌────────────────────────┐  │ │
│  └─────────────┘ events │  │   Container Manager    │  │ │
│                          │  │  ┌──────────────────┐  │  │ │
│  ┌─────────────┐         │  │  │  ContainerValidator│ │  │ │
│  │  Traefik    │<────────│  │  └──────────────────┘  │  │ │
│  │  (Proxy)    │  labels │  │  ┌──────────────────┐  │  │ │
│  └─────────────┘         │  │  │   HealthChecker  │  │  │ │
│         │                │  │  └──────────────────┘  │  │ │
│         │                │  └────────────────────────┘  │ │
│         ▼                └──────────────────────────────┘ │
│  ┌──────────────────────────────────────────────────────┐ │
│  │              Docker / Podman Runtime                 │ │
│  │                                                      │ │
│  │  ┌─────────────────────┐  ┌─────────────────────┐   │ │
│  │  │   Workspace A net   │  │   Workspace B net   │   │ │
│  │  │  ┌───┐  ┌───┐      │  │  ┌───┐  ┌───┐      │   │ │
│  │  │  │MCP│  │MCP│      │  │  │MCP│  │MCP│      │   │ │
│  │  │  └───┘  └───┘      │  │  └───┘  └───┘      │   │ │
│  │  └─────────────────────┘  └─────────────────────┘   │ │
│  └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘

Security Model

Network Isolation

Each workspace gets its own Docker network. Containers in workspace A cannot reach containers in workspace B.

No Privileged Mode

Containers never run with --privileged. Resource limits and security defaults follow the principle of least privilege.

Resource Limits

Every container has a configurable memory cap (default 512m) and CPU quota (default 1.0). Runaway processes cannot starve the host.

Workspace Scoping

Containers are labeled with workspace_id. The manager enforces scoping at both creation time and cleanup — orphaned containers from crashed workspaces are reclaimed on restart.

Container Lifecycle

1

Skill Activation

When an agent activates a skill that references an MCP server instance, the Python API publishes an MCPServerInstanceCreated domain event to Redis.
2

Pre-launch Validation

The MCP Manager receives the event and the ContainerValidator performs a dry-run check before any container is started: image existence, port availability, container limits, and resource requirement validation.
3

Container Creation

The Manager calls the container runtime with security-hardened arguments — network assignment, resource limits, Traefik routing labels, and workspace labels.
4

Health Monitoring

The HealthChecker begins continuous polling. It checks both the runtime process status and the container’s HTTP endpoint. On failure the container is automatically restarted.
5

Skill Deactivation

When the skill is detached from an agent or the workspace is deleted, the MCPServerInstanceDeleted event triggers container teardown and network cleanup.

Container Runtime Arguments

The MCP Manager builds the container run command programmatically. The following flags are applied to every MCP container:
// internal/container/manager.go — buildContainerRunArgs()
args := []string{"run", "-d"}

// Identity
args = append(args, "--name", container.Name)

// Network isolation — each workspace gets its own network
args = append(args, "--network", m.config.Container.Network)

// Environment variables (secrets resolved from secret manager)
for key, value := range container.Environment {
    args = append(args, "-e", fmt.Sprintf("%s=%s", key, value))
}

// Resource limits
args = append(args, "--memory", m.config.Container.DefaultMemoryLimit) // default: 512m
args = append(args, "--cpus",   m.config.Container.DefaultCPULimit)    // default: 1.0

// Traefik routing labels (auto-discovery, no static config)
args = append(args,
    "--label", "traefik.enable=true",
    "--label", fmt.Sprintf("traefik.http.routers.%s.rule=PathPrefix(`/mcp/%s`)", slug, slug),
    "--label", fmt.Sprintf("traefik.http.routers.%s.entrypoints=mcp", slug),
    "--label", fmt.Sprintf("traefik.http.services.%s.loadbalancer.server.port=%d", slug, port),
)
No container is ever started with --privileged. Privilege escalation from inside an MCP container is blocked by default.

MCP Server Types

AgentArea supports two MCP server deployment types, both subject to the same container sandboxing rules.
A pre-built container image serves the MCP protocol directly over HTTP. You supply the image and port; the manager handles the rest.
{
  "type": "docker",
  "image": "mcp/filesystem:latest",
  "port": 8001,
  "environment": {
    "ALLOWED_DIRECTORIES": "/tmp,/var/tmp",
    "API_KEY": "secret_ref:instance_123:API_KEY"
  },
  "resources": {
    "memory_limit": "256m",
    "cpu_limit": "0.5"
  }
}
The ContainerValidator checks that the image exists locally or can be pulled before the container is created. Images that are neither local nor pullable are rejected at validation time, before any runtime resources are allocated.

Pre-launch Validation

Before any container starts, ContainerValidator.DryRunValidation() runs a series of checks:
CheckWhat it does
JSON spec structureValidates required fields (image+port for docker, command for command type)
Image existenceVerifies the image is local or can be pulled; rejects unknown images
Container limitEnforces MAX_CONTAINERS (default 50) per manager instance
Resource requirementsValidates memory_limit and cpu_limit format if provided
Name collisionRejects specs that would create a container with a duplicate name
// ContainerValidator — DryRunValidationWithLimits()
if currentRunningCount >= maxContainers {
    result.Errors = append(result.Errors,
        fmt.Sprintf("Container limit reached: %d/%d", currentRunningCount, maxContainers))
    result.Valid = false
}
A ValidationResult is returned with valid, errors, warnings, image_exists, can_pull, and estimated_size. The container is only created when valid == true.

Health Monitoring

The HealthChecker runs continuously for every managed container. Each check combines a runtime-level status query and an HTTP liveness probe.
// internal/container/health.go — PerformHealthCheck()
type HealthCheckResult struct {
    ContainerID   string
    Healthy       bool
    Status        ContainerStatus   // running | starting | stopping | stopped | error
    HTTPReachable bool
    ResponseTime  time.Duration
    Error         string
    Timestamp     time.Time
    Details       map[string]interface{}
}

Health Check Flow

HTTP status codes below 500 are treated as healthy. MCP servers commonly return 403 or 405 on a bare GET / — these are valid responses that confirm the server is up and listening.

Automatic Restart

The manager’s autoRestartContainers routine scans for containers that should be running but have stopped. Any container with status stopped or error that is still registered in the manager is automatically restarted:
// internal/container/manager.go — restartContainer()
func (m *Manager) restartContainer(ctx context.Context, container *models.Container) error {
    // Rebuild the run args and re-execute the container runtime
    args := m.buildContainerRunArgs(container)
    cmd := exec.CommandContext(ctx, m.config.Container.Runtime, args...)
    // ...
}
Status transitions are published back to Redis so the Python API and any connected SSE clients see up-to-date health state.

Workspace Isolation Details

Network Scoping

All MCP containers for a workspace are placed on the same Docker network (MCP_NETWORK, default agentarea_default). Network policies prevent cross-workspace container communication:
  • Containers in workspace A cannot initiate connections to containers in workspace B.
  • The Traefik reverse proxy is the only ingress point; direct container-to-container access from outside the workspace network is not possible.

Label-based Ownership

Every container is labeled at creation time with management metadata:
traefik.enable=true
traefik.http.routers.<slug>.rule=PathPrefix(`/mcp/<slug>`)
traefik.http.routers.<slug>.entrypoints=mcp
traefik.http.services.<slug>.loadbalancer.server.port=<port>
The manager uses the CONTAINER_MANAGED_BY_LABEL label (default mcp-manager) to identify and reclaim its own containers. On startup, containers with this label that are not in the in-memory registry are reconciled — either reattached or cleaned up.

Orphan Cleanup

When the MCP Manager restarts, it queries the container runtime for all containers bearing its management label. Containers with no corresponding database record are treated as orphans and removed. This prevents runaway containers from accumulating across service restarts.

Warm Pool Integration

The warm pool pre-allocates containers so that skill activation is near-instant. See the Warm Pool page for timing details.
Pre-warmed containers follow the same security profile as on-demand containers. The pool manager assigns a workspace label and network at activation time, not at pre-creation time, so a warm container carries no workspace identity until it is claimed.

Configuration Reference

Environment Variables

VariableDefaultDescription
CONTAINER_RUNTIMEdockerContainer runtime binary (docker or podman)
MCP_NETWORKagentarea_defaultDocker network for MCP containers
CONTAINER_NAME_PREFIXmcp-Prefix applied to all managed container names
CONTAINER_MANAGED_BY_LABELmcp-managerLabel used to identify managed containers
MAX_CONTAINERS50Maximum concurrent containers per manager instance
DEFAULT_MEMORY_LIMIT512mDefault container memory cap
DEFAULT_CPU_LIMIT1.0Default container CPU quota (cores)
STARTUP_TIMEOUT120sTime allowed for a container to reach running state
SHUTDOWN_TIMEOUT30sGraceful shutdown window before forced stop

Per-instance Resource Override

Resource limits set in json_spec.resources override the manager-level defaults for that specific instance:
{
  "type": "docker",
  "image": "mcp/heavyweight-tool:latest",
  "port": 8080,
  "resources": {
    "memory_limit": "2g",
    "cpu_limit": "2.0"
  }
}

Skills System Integration

Skills are the user-facing abstraction over MCP server instances. The connection between them follows a simple lifecycle:
  1. A Skill is created and associated with an MCP server definition.
  2. When a skill is attached to an agent, the underlying MCPServerInstance is activated — triggering container creation via the event pipeline.
  3. The skill membership model (agent_skills table) controls which agents can invoke which skills, enforcing access at the API layer before any container interaction occurs.
  4. When the skill is detached or the workspace is deleted, the container is stopped and removed.
Agent ──attaches──> Skill ──references──> MCPServerInstance

                                           Redis event

                                          MCP Manager

                                        Docker container
                                   (workspace-isolated network)

Security Summary

ControlMechanism
No privileged containers--privileged is never passed to the runtime
Network isolationPer-workspace Docker networks; cross-workspace routing is blocked
Resource limitsHard --memory and --cpus caps on every container
Image validationPre-launch check rejects unknown or unpullable images
Container quotasMAX_CONTAINERS limit enforced before creation
Secret isolationSecrets stored as references; resolved to actual values only at container runtime
Orphan cleanupManager-labeled containers without a DB record are removed on startup
Reverse proxy onlyTraefik is the sole ingress; MCP container ports are not exposed to the host

Next Steps