Outcall
SpecificationsS008 · Docker Manager

S008 · Docker Manager

Specification module 008-docker-manager

S008: Docker Manager

FieldValue
SpecS008
FeatureDocker Manager
Date2026-04-21
StatusImplemented
Author@marktopper

Overview

The Docker Manager handles the full lifecycle of agent containers within outcalld. It uses the bollard crate to communicate with the Docker Engine API — creating, starting, monitoring, stopping, and removing containers with the correct security constraints enforced at creation time.

Every agent container is created with a deterministic set of constraints:

  • Attached only to an inspected network backed by the daemon's exact Outcall bridge (S002)
  • Optional agent.sock and outcall-agent helper mounts for shim-based workloads (S004/S005)
  • HTTP_PROXY / HTTPS_PROXY env vars pointed at the HTTP proxy (S006)
  • DNS resolver pointed at the DNS filter (S007)
  • Numeric non-root process identity, read-only root, dropped capabilities, no-new-privileges, and fixed PID limit
  • Explicit Docker network mode, Docker init, and daemon-owned DNS options
  • The host API, Docker, and containerd control sockets are never exposed

Control-socket protection is a critical security invariant. Before Bollard issues the Docker API call, Outcall checks lexical and canonical bind-source paths, catches symlinks, and rejects parent mounts that would expose a protected socket. Bind destinations cannot cover the helper shim, agent socket, or resolver configuration. The daemon also inspects the selected network's driver and bridge name; an outcall- prefix alone is not trusted.

Container naming

Callers may supply an exact operator-visible name. Omitted names default to an outcall- prefix plus 8 random hex characters. Discovery and cleanup use the managed-by=outcalld label, not a naming convention. Recipe runtimes provide the higher-level <project>-N naming policy.

Lifecycle

create → start → monitor → stop → remove

The Docker Manager runs as a Tokio task inside outcalld. There is no separate process. The host operator manages containers through the outcall CLI and the outcalld host API. Startup uses a bounded Docker ping so a client endpoint that exists but does not respond is reported as degraded rather than initialized. Lifecycle operations require the managed label and use the inspected immutable Docker ID.

User Scenarios

S008-US-001 [P1] As a host operator, I want to create agent containers with security constraints enforced so that agents cannot access the host surface.

S008-US-002 [P1] As a host operator, I want to list running agent containers so that I can see what is active.

S008-US-003 [P1] As a host operator, I want to stop and remove agent containers so that I can clean up resources.

S008-US-004 [P1] As a host operator, I want the host socket to never be mounted into containers so that agents cannot escalate to the host API.

S008-US-005 [P2] As a host operator, I want to inspect a specific container so that I can debug issues.

S008-US-006 [P1] As a host operator, I want containers to survive daemon restarts so that running workloads are not disrupted.

S008-US-007 [P2] As a host operator, I want to pull images before creating containers so that the create step does not block on network I/O.

Requirements Summary

IDTypePriorityTitleStatus
S008-FR-001FunctionalP1Docker Engine API onlyImplemented
S008-FR-002FunctionalP1Container creation parametersImplemented
S008-FR-003FunctionalP1Network attachmentImplemented
S008-FR-004FunctionalP1Agent socket bind mountImplemented
S008-FR-005FunctionalP1Shim binary bind mountImplemented
S008-FR-006FunctionalP1Proxy env var injectionImplemented
S008-FR-007FunctionalP1DNS resolver configurationImplemented
S008-FR-008FunctionalP1Host socket deny listImplemented
S008-FR-009FunctionalP1Bind mount validationImplemented
S008-FR-010FunctionalP1Container naming conventionImplemented
S008-FR-011FunctionalP1Container startImplemented
S008-FR-012FunctionalP1Container stopImplemented
S008-FR-013FunctionalP1Container removeImplemented
S008-FR-014FunctionalP1Container listingImplemented
S008-FR-015FunctionalP2Container inspectionImplemented
S008-FR-016FunctionalP2Resource limitsImplemented
S008-FR-017FunctionalP2Image pullingImplemented
S008-FR-018FunctionalP1In-process Tokio taskImplemented
S008-FR-019FunctionalP1Host-only API endpointsImplemented
S008-FR-020FunctionalP1Containers outlive daemonImplemented
S008-FR-021FunctionalP1CLI subcommandsImplemented
S008-FR-022FunctionalP1CLI transport mechanismImplemented
S008-FR-023FunctionalP1CLI error handlingImplemented
S008-FR-024FunctionalP1Constants in outcall-apiImplemented
S008-FR-025FunctionalP2Read-only root filesystemImplemented
S008-FR-026FunctionalP2No privileged modeImplemented
S008-FR-027FunctionalP2Capability droppingImplemented
S008-FR-028FunctionalP1Stop timeoutImplemented
S008-FR-029FunctionalP2Container labelsImplemented
S008-FR-030FunctionalP1Network-up prerequisiteImplemented
S008-FR-031FunctionalP1Container event streamImplemented
S008-FR-032FunctionalP1Outcall-managed network definitionImplemented
S008-FR-033FunctionalP1Protected bind destinationsImplemented
S008-FR-034FunctionalP1Managed-only immutable-ID lifecycleImplemented
S008-FR-035FunctionalP1Partial-create rollbackImplemented
S008-FR-036FunctionalP1Bounded Docker availability probeImplemented
S008-FR-037FunctionalP2Correct image-reference parsingImplemented
S008-FR-038FunctionalP1Explicit network mode and initImplemented
S008-FR-039FunctionalP1Helper source preflightImplemented
S008-AS-001AcceptanceP1Create agent containerImplemented
S008-AS-002AcceptanceP1Host socket rejectedImplemented
S008-AS-003AcceptanceP1List running containersImplemented
S008-AS-004AcceptanceP1Stop containerImplemented
S008-AS-005AcceptanceP1Remove containerImplemented
S008-AS-006AcceptanceP1Network not readyImplemented
S008-AS-007AcceptanceP2Inspect containerImplemented
S008-AS-008AcceptanceP2Pull imageImplemented
S008-AS-009AcceptanceP1Daemon not runningImplemented
S008-AS-010AcceptanceP1Docker not availableImplemented
S008-AS-011AcceptanceP1Containers survive shutdownImplemented
S008-AS-012AcceptanceP1Container with resource limitsImplemented
S008-AS-013AcceptanceP1Verify bind mounts inside containerImplemented
S008-AS-014AcceptanceP1Verify env vars inside containerImplemented
S008-AS-015AcceptanceP1Protected destination rejectedImplemented
S008-AS-016AcceptanceP1Unmanaged lifecycle rejectedImplemented
S008-AS-017AcceptanceP1Hung Docker startup boundedImplemented
S008-AS-018AcceptanceP1Partial create rolled backImplemented
S008-AS-019AcceptanceP2Registry port and digest parsingImplemented
S008-IF-001InterfaceP1POST /api/v1/container/createImplemented
S008-IF-002InterfaceP1POST /api/v1/container/stopImplemented
S008-IF-003InterfaceP1POST /api/v1/container/removeImplemented
S008-IF-004InterfaceP1GET /api/v1/containersImplemented
S008-IF-005InterfaceP2GET /api/v1/containerImplemented
S008-IF-006InterfaceP2POST /api/v1/container/pullImplemented
S008-IF-007InterfaceP1CLI commandsImplemented
S008-IF-008InterfaceP1CLI output formatImplemented
S008-EC-001Edge CaseP1Host socket in bind mountsImplemented
S008-EC-002Edge CaseP1Network does not existImplemented
S008-EC-003Edge CaseP1Docker unreachableImplemented
S008-EC-004Edge CaseP1Container does not exist (stop/remove)Implemented
S008-EC-005Edge CaseP2Image not found locallyImplemented
S008-EC-006Edge CaseP2Image pull failsImplemented
S008-EC-007Edge CaseP1Container already stoppedImplemented
S008-EC-008Edge CaseP2Container already removedImplemented
S008-EC-009Edge CaseP2Rapid create callsImplemented
S008-EC-010Edge CaseP2Daemon shutdown with running containersImplemented
S008-EC-011Edge CaseP2Docker disappears at runtimeImplemented
S008-EC-012Edge CaseP1Symlink traversal in bind mountsImplemented
S008-EC-013Edge CaseP1Duplicate container nameImplemented
S008-EC-014Edge CaseP2Resource limit exceeds host capacityImplemented
S008-EC-015Edge CaseP1Agent socket path missingImplemented
S008-EC-016Edge CaseP1Shim binary path missingImplemented
S008-EC-017Edge CaseP1Invalid helper sourceImplemented
S008-EC-018Edge CaseP1Protected destination shadowingImplemented
S008-EC-019Edge CaseP1Unmanaged lifecycle targetImplemented
S008-EC-020Edge CaseP1Hung Docker pingImplemented
S008-EC-021Edge CaseP2Registry port without tagImplemented
S008-SC-001SuccessP1Container creation with all constraintsImplemented
S008-SC-002SuccessP1Host socket never mountedImplemented
S008-SC-003SuccessP1Network attachment verifiedImplemented
S008-SC-004SuccessP1Bind mounts verified inside containerImplemented
S008-SC-005SuccessP1Env vars verified inside containerImplemented
S008-SC-006SuccessP1DNS resolver verifiedImplemented
S008-SC-007SuccessP1Stop and remove lifecycleImplemented
S008-SC-008SuccessP1Container listing accuracyImplemented
S008-SC-009SuccessP1No docker CLI shellingImplemented
S008-SC-010SuccessP2Graceful shutdown cleanupImplemented
S008-SC-011SuccessP1CLI round-tripImplemented
S008-SC-012SuccessP1Control destinations cannot be shadowedImplemented
S008-SC-013SuccessP1Managed-only immutable-ID lifecycleImplemented
S008-SC-014SuccessP1Bounded Docker startup probeImplemented
S008-SC-015SuccessP1Partial-create rollbackImplemented
S008-SC-016SuccessP2Image references parsed correctlyImplemented

Out of Scope

  • Network creation/destruction — handled by S002
  • Bridge management — handled by S001
  • Rule evaluation — handled by S003
  • Agent API protocol — handled by S004
  • Shim binary behavior — handled by S005
  • HTTP proxy behavior — handled by S006
  • DNS filter behavior — handled by S007
  • Container orchestration (scheduling, scaling, rolling updates)
  • Image building — operators provide pre-built images
  • Volume management beyond the required bind mounts
  • TLS or authentication on the host socket
  • Inter-container networking beyond outcall network attachment
  • GPU passthrough or device mounting

Cross-Spec Dependencies

  • Depends on: S001 (bridge must be up)
  • Depends on: S002 (network must exist for container attachment)
  • Depends on: S004 (agent.sock path for bind mount)
  • Depends on: S005 (shim binary path for bind mount)
  • Depends on: S006 (proxy address for env var injection)
  • Depends on: S007 (DNS resolver address for container config)
  • Required by: Host operators managing agent workloads

Shared Types (outcall-api)

Constants

pub const CONTAINER_PREFIX: &str = "outcall-";
pub const SHIM_CONTAINER_PATH: &str = "/usr/local/bin/outcall";
pub const AGENT_SOCKET_CONTAINER_PATH: &str = "/run/outcall/agent.sock";
pub const DEFAULT_STOP_TIMEOUT_SECS: i64 = 10;
pub const DEFAULT_MEMORY_LIMIT: i64 = 512 * 1024 * 1024;
pub const MIN_MEMORY_LIMIT: i64 = 6 * 1024 * 1024;
pub const DEFAULT_CPU_SHARES: i64 = 1024;
pub const MIN_CPU_SHARES: i64 = 2;

Types

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerCreateRequest {
    pub image: String,
    pub network: Option<String>,
    pub name: Option<String>,
    pub user: Option<String>,
    pub memory_limit: Option<i64>,
    pub cpu_shares: Option<i64>,
    pub env: Option<Vec<String>>,
    pub cmd: Option<Vec<String>>,
    pub entrypoint: Option<Vec<String>>,
    pub working_dir: Option<String>,
    pub volumes: Option<Vec<String>>,
    pub include_outcall_helper_mounts: Option<bool>,
    pub interactive: Option<bool>,
    pub tty: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerCreateResult {
    pub container_id: String,
    pub name: String,
    pub created: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerInfo {
    pub container_id: String,
    pub name: String,
    pub image: String,
    pub state: String,
    pub network: String,
    pub created_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerStopRequest {
    pub name: String,
    pub timeout: Option<i64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerStopResult {
    pub name: String,
    pub stopped: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerRemoveRequest {
    pub name: String,
    pub force: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerRemoveResult {
    pub name: String,
    pub removed: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerInspectResult {
    pub container_id: String,
    pub name: String,
    pub image: String,
    pub state: String,
    pub network: String,
    pub ip_address: Option<String>,
    pub mounts: Vec<String>,
    pub env: Vec<String>,
    pub created_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImagePullRequest {
    pub image: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImagePullResult {
    pub image: String,
    pub pulled: bool,
}

On this page