lazaret/docs/specification.md
2026-06-28 15:09:39 +09:00

28 KiB

Lazaret: Unified Sandbox Quarantine & Host Escalation CLI

Architecture & Technical Specification

Lazaret is a secure developer-experience (DX) tool written in Rust. It orchestrates rootless, gVisor-sandboxed container environments for AI agents, isolating them from the host network and system namespace by default, while providing an interactive, human-in-the-loop (HITL) Host Escalation protocol for controlled actions.

Glossary

Term Definition
Agent Workspace The bind-mounted host project directory inside the Agent Sandbox (at /workspace).
Agent Sandbox The gVisor (runsc) container running a profile.
Network Sidecar The shell-less gateway container (from scratch) running the Netlink router under runc.
Git Shadow The tmpfs masking + read-only shadow mount of the host .git + selective write-back binds + symlink redirection for config and HEAD.
Host Escalation The secure UDS channel (with ephemeral mTLS) from the Escalation Client to the host TUI for approved host actions.
Escalation Client The statically linked lazaret-escalate binary.
Escalation Socket The profile-specific Unix domain socket.
Profile Root <data_root>/<profile_name>.
Storage Root The configured data_root for persistent profile data.

Fixed In-Container Paths

Lazaret-managed files and sockets inside every Agent Sandbox live under /run/lazaret/:

Path Description
/run/lazaret/escalate The Escalation Client binary (volume-mounted, read-only).
/run/lazaret/escalate.sock The Escalation Socket.
/run/lazaret/git Target directory for the Git Shadow (read-only bind).
/run/lazaret/certs Ephemeral mTLS client certificates (memory-backed tmpfs, read-only).

The Agent Workspace is mounted at /workspace.


1. Problem Statement & Goals

1.1 The Security vs. Capability Dilemma

Modern AI agents require powerful execution capabilities (e.g., running code, interacting with git, testing microservices). However, running untrusted agent-generated code directly on a developer's host machine poses severe security risks:

  • Host Privilege Escalation: Untrusted code can access SSH keys, local credentials, or modify system files.
  • Network Escapes: A rogue agent can scan local subnets, exfiltrate data, or interact with sensitive internal services (RFC 1918 ranges).
  • Git Hook Injection: An agent can write a malicious git hook (e.g., .git/hooks/post-checkout) that executes code on the host machine when the developer runs simple git commands.
  • Git Config Exploitation: An agent can write a malicious git pager config (e.g., core.pager = malicious_command) which executes on the host when the developer runs git log or git diff.

1.2 The Developer Experience (DX) Friction

Existing container-based isolation solutions (like standard Docker/Podman running on runc) introduce high friction:

  • Terminal Degradation: Terminfo mismatches (e.g., running Ghostty, Kitty, or Alacritty) cause terminal applications to crash inside the container.
  • Transient State Loss: Re-authenticating with developer tools, subscriptions, or package manager registries (npm, cargo, etc.) on every sandbox restart is highly disruptive.
  • Impoverished Git Integration: Masking the entire .git directory prevents agents from running basic queries (like git diff or git status), severely crippling their capabilities.

1.3 Core Objectives

  • Deep Virtualization-Based Isolation: Run AI agent workspaces in rootless Podman containers utilizing the gVisor (runsc) sandbox runtime, establishing a strong kernel boundary.
  • Network Sidecar: Route container traffic through a statically compiled, shell-less network gateway container (scratch) running a custom Netlink router that dynamically applies egress policies, bypassing gVisor Netlink capabilities limits.
  • Persistent State & Subscriptions: Provide configurable persistence folders so that packages, tools, and developer authentications (OAuth, CLI subscriptions) survive container restarts via host-persistent directory mapping.
  • Git Hook & Config Protection with Full Git DX: Neutralize git hook injection and config pager-script exploits to protect the host, while maintaining complete local git write (commits, staging, branch checking) and read functionality inside the sandbox, ensuring real-time configuration visibility.
  • Host Escalation: Provide a secure Escalation Socket (UDS) channel with ephemeral mTLS that allows the Escalation Client inside the Agent Sandbox to request host-bound actions, displaying an interactive terminal prompt to the user for approval or modification.

2. Architecture & Data Flow

graph TD
    subgraph Host Machine
        Orchestrator[Lazaret Host CLI / Daemon]
        Config[~/.config/lazaret/config.toml]
        Storage[Sandbox Storage Root / NVMe]
        TUI[Interactive HITL TUI Prompt]
        UDS_Server[Unix Domain Socket Server]
    end

    subgraph Container Isolation Boundary
        subgraph Gateway Sidecar (scratch - runc)
            RouteDaemon[Netlink Router Daemon]
        end

        subgraph Agent Sandbox (Debian Trixie - runsc/gVisor)
            AgentProcess[Agent Process / CLI]
            EscalateCLI[lazaret-escalate CLI]
        end
    end

    Orchestrator -->|Loads| Config
    Orchestrator -->|Initializes Storage| Storage
    Orchestrator -->|Spins up Sidecar| RouteDaemon
    Orchestrator -->|Launches Sandbox| AgentProcess
    
    RouteDaemon -->|Configures Netlink| GatewaySidecarNetwork
    AgentProcess -->|Binds Network Namespace| RouteDaemon
    
    AgentProcess -->|Requests host command| EscalateCLI
    EscalateCLI -->|JSON Request via UDS| UDS_Server
    UDS_Server -->|Triggers Prompt| TUI
    TUI -->|User Approval / Modification| Orchestrator
    Orchestrator -->|Executes Command on Host| HostEnv[Host Environment]
    HostEnv -->|Sends Stdout / Stderr| UDS_Server
    UDS_Server -->|JSON Response| EscalateCLI

3. Configuration Contract

Configuration files are parsed hierarchically (lowest precedence to highest):

  1. User-wide global default: ~/.config/lazaret/config.toml
  2. Workspace local override: ./lazaret.toml (in the active working directory)
  3. Direct override command-line argument: --config <file>

A concrete template demonstrating all 5 MVP agent profiles is located in the repository as example.toml.

3.1 Schema Contract Specification

Section [storage]

Property Type Required Description
data_root String Yes Absolute path on the host where persistent directories (home, usr, etc.) are initialized and mounted.

Section [network]

Property Type Required Description
default_allow Array of Strings Yes Global whitelist of destination CIDR networks permitted for all containers.
default_deny Array of Strings Yes Global blacklist of destination CIDR networks (e.g., RFC 1918) blackholed for all containers.

Section [escalation]

Property Type Required Description
enabled Boolean Yes Activates or suspends the host-side escalation daemon.
socket_dir String Yes Path on the host filesystem (e.g. /run/user/1000/lazaret/ or /tmp/lazaret) where profile-specific Unix escalation sockets are created.
auto_approve Array of Strings No List of user-defined CLI commands allowed to execute on the host automatically without interactive confirmation.

Section [profiles.<profile_name>]

Property Type Required Description
image String Yes The fully-qualified OCI container image to pull and launch. To prevent background updates and local host state drift, specific image tags or SHA-256 digests should be pinned.
persistent_dirs Array of Strings No Paths inside the container copied out and persisted under the host's storage directory (e.g. /usr, /var).
allow_egress Array of Strings No CIDRs allowed specifically for this agent, merged with the default global network whitelist.
env Map of Key-Value No Environment variables forwarded to the container. Setting a value prefix of env:VAR_NAME pulls dynamically from the host.
volumes Array of Objects No Custom volumes mapped from host to container. Hosts support the {profile_dir} expansion.

Custom Volume Object

  • host: String. Host directory or file path. May use {profile_dir} which resolves to <data_root>/<profile_name>/.
  • container: String. Target path inside the container's directory tree.
  • mode: String. Mount properties (e.g., z for SELinux sharing, ro for read-only bind mounts).

3.2 Variable Expansion, Templating & Sanitation Specification

To maintain a secure sandbox quarantine and provide an ergonomic configuration experience, Lazaret implements a custom, zero-dependency, safe variable substitution engine rather than a heavy template engine like Jinja or Handlebars. This minimizes the attack surface and compilation footprint.

3.2.1 Linux-Only Tilde (~) Expansion

Because the core orchestration dependencies of Lazaret (rootless Podman and gVisor runsc) run exclusively on Linux architectures, the codebase is compiled and optimized solely for Linux systems. Path resolution and tilde expansion are handled using Linux standards:

  • Lazaret retrieves the host user's home directory by reading the $HOME environment variable, falling back to query the host's /etc/passwd entry if $HOME is unpopulated.
  • Strict Validation: Tilde is only expanded if it is the absolute first character of the path string (e.g. ~/config resolves to /home/user/config, but some/path/~ is treated as a literal directory named ~).

3.2.2 Expansion Tokens

Lazaret parses and expands a small, strictly defined whitelist of local variables inside configuration fields:

  • {profile_name}: Evaluates to the lowercase alphanumeric name of the active agent profile.
  • {profile_dir}: Expands dynamically on the host to: {data_root}/{profile_name}.
  • {workspace_dir}: Evaluates to the current host working directory (std::env::current_dir()).

3.2.3 Sanitation & Directory Traversal Protection

Because AI agents might attempt to manipulate configuration templates or profile names to trigger directory escapes on the host machine, Lazaret enforces three strict structural validation boundaries:

  1. Profile Name Sanitation: Profile names must strictly match the regex pattern: ^[a-z0-9_-]{2,32}$. Any profile name containing dots (.), slashes (/, \), or shell metacharacters is immediately rejected, preventing injection attacks when creating paths like {data_root}/{profile_name}.

  2. Path Traversal Interception: Before resolving any expanded path, Lazaret passes the resulting string to a sanitation function that checks for the presence of relative path segments. If a resolved path contains a dot-dot segment (..), the execution is aborted immediately with a security panic, preventing the agent from escaping the storage boundaries.

  3. Absolute Path Verification: All host paths used for mounts, storage, and volumes are canonicalized using Rust's std::fs::canonicalize or resolved to absolute paths before being passed to Podman execution commands. This prevents relative symlink tricks or path obfuscation from bypassing rootless boundaries.


4. Key Security & Functional Mechanisms

4.1 Git Hook & Config Isolation (Git Shadow)

To completely secure the developer's host environment against git hook injection and config pager exploits while preserving complete Git capabilities and real-time configuration visibility, Lazaret implements a strict filesystem mount decoupling strategy.

Direct single-file read-only bind mounts of .git/config are prohibited due to the Unix stale inode problem: when git configuration is updated on the host, Git atomically unlinks and replaces .git/config, causing single-file bind mounts to remain permanently bound to the stale, unlinked file inode.

4.1.1 The Decoupling Protocol

Rather than mounting .git directly as a read-write or simple read-only directory, the host orchestrator constructs a hybrid virtual directory structure inside the container's mount namespace:

  1. Agent Workspace Mount: The active Agent Workspace is bind-mounted read-write (with the standard :z flag for rootless SELinux compatibility).
  2. Git Directory Masking: The orchestrator overlays a memory-backed tmpfs volume directly over the .git directory inside the Agent Workspace namespace, hiding the host's physical .git directory.
  3. Shadow Mount Injection: The host's actual .git directory is bind-mounted read-only to the Git Shadow target inside the container.
  4. Write-Back Bind Mounts: To support standard Git modifications inside the Agent Workspace (such as staging changes, recording commits, and managing branches), the host's writeable Git directories are bind-mounted read-write directly into the memory-backed .git/ inside the Agent Workspace:
    • Host objects directory → write-back object database inside the Agent Workspace.
    • Host refs directory → write-back refs inside the Agent Workspace.
    • Host logs directory → write-back logs inside the Agent Workspace.
  5. Read-Only Symlink Redirection: Critical configurations inside the Agent Workspace are exposed using symbolic links pointing to the secure, read-only Git Shadow target:
    • .git/config is symlinked to the Git Shadow.
    • .git/HEAD is symlinked to the Git Shadow.

Filesystem Mount Mapping Table

Path in Agent Workspace Mount Type Properties / Permissions Purpose
/workspace Bind Mount Read / Write (z shared) Shares the active project folder with the Agent Sandbox.
/workspace/.git tmpfs Memory-only, transient Masks the git folder inside the Agent Workspace, blocking direct host hook writes.
/workspace/.git/objects Bind Mount Read / Write (z shared) Write-back object database (host objects mounted into Agent Workspace).
/workspace/.git/refs Bind Mount Read / Write (z shared) Write-back refs (branches/stashes) into the Agent Workspace.
/workspace/.git/logs Bind Mount Read / Write (z shared) Write-back reflogs into the Agent Workspace.
/workspace/.vscode tmpfs Memory-only Masks user's host VSCode preferences inside the Agent Workspace.
/workspace/.idea tmpfs Memory-only Masks user's host IntelliJ configurations inside the Agent Workspace.
/run/lazaret/git Bind Mount Read-Only (ro, z shared) Git Shadow target directory (read-only).

4.1.2 Security & Operational Guarantees

  • Immutable Target Integrity: Any direct write or append attempt targeting .git/config inside the Agent Workspace fails at the kernel level with EROFS (Read-only file system) because the resolved symlink target resides on the read-only Git Shadow.
  • Container Bypass Neutralization: If a malicious agent process deletes the symlink and writes a new, rogue configuration file at that path, the write remains entirely isolated within the local memory-backed tmpfs. The host's actual .git/config is never touched, and host commands remain completely secure.
  • Instant Configuration Visibility: Because the Git Shadow target is mounted as a directory, any host-side atomic replacements of files within it (such as a developer running git config on the host, which unlinks and replaces .git/config) are instantly visible inside the Git Shadow. The container's symlink immediately resolves to the updated configuration without getting stuck on a stale, unlinked inode.
  • Safe Isolated Branch Switching: The Agent Sandbox can safely execute git checkout to switch branches. Git writes branch reference updates to the write-back refs location (which safely propagates back to the host), but writes the active branch state to HEAD. If the write to the read-only HEAD symlink fails, the container's git CLI simply deletes the symlink and writes a real, local HEAD file to the writeable tmpfs. This isolates the active sandbox branch state, preventing container-side branch switches from disrupting the host developer's active branch workspace.

4.2 Sandbox Git Worktree/Branch Isolation

To prevent the agent from cluttering the developer's working index, Lazaret supports working on isolated branch namespaces:

  • Dedicated Branch: Lazaret can auto-derive a branch lazaret/agent-<name> from the active host commit.
  • Stash Protection: The developer's host working directory remains intact. The developer can inspect changes on the host with git diff lazaret/agent-<name> or merge them explicitly.

4.3 Image-Agnostic State Persistence via Host-Side fuse-overlayfs

When a sandbox initializes, Lazaret provides state persistence (e.g., preserving installed packages, dependencies, and tools) across container deletions and recreations without copying out entire filesystems or bloating host storage. This is achieved by orchestrating a host-side user-space overlay filesystem (fuse-overlayfs) under the rootless container namespace.

4.3.1 Directory Layout on Host

For each profile, the host orchestrator establishes the following directory structure under {data_root}/{profile_name}/ (which host volume {profile_dir} evaluates to):

  • usr_changes/: The persistent, writeable layer (upperdir) that stores user-installed packages, configurations, and modifications. Initially empty.
  • usr_work/: The private metadata workspace directory (workdir) required by overlayfs.
  • usr_merged/: The virtual, merged mount point directory (mergeddir) exposed to the sandbox.

4.3.2 Host-Side Mounting Protocol

Because gVisor (runsc) restricts overlayfs mounts inside the sandbox virtual kernel, and standard host overlays require root privileges, Lazaret runs mounting operations on the host inside the rootless container namespace:

  1. Locate Base Image Mount: The orchestrator creates a lightweight placeholder container and retrieves its base-image filesystem mount path on the host using podman mount (e.g., /home/user/.local/share/containers/storage/overlay/.../merged/).
  2. Mount User-Space Overlay (fuse-overlayfs): The orchestrator executes fuse-overlayfs on the host, setting the base image's system directory as the read-only lowerdir, and the profile-specific usr_changes and usr_work as the writable layers. To support rootless execution and avoid permission errors, this command is executed within the rootless user namespace:
    podman unshare fuse-overlayfs -o lowerdir=<base_image_mount>/usr,upperdir=usr_changes,workdir=usr_work usr_merged
    
  3. Sandbox Mount Injection: The host orchestrator launches the gVisor sandbox, bind-mounting the virtual, merged directory (usr_merged) over the container's /usr directory:
    podman run --runtime=runsc -v <data_root>/<profile_name>/usr_merged:/usr:z ...
    

4.3.3 Resilience to Image Updates, Drift & Podman Compatibility

Unlike static copy-out methods which seal system libraries in time and bypass container patches, this overlay structure naturally inherits base image updates:

  • When the base OCI image is updated (e.g., pulling a new release containing critical CVE patches), the orchestrator mounts the new image's read-only /usr as the lowerdir on the next run.
  • fuse-overlayfs merges this new, patched baseline with the existing usr_changes layer dynamically.
  • Because standard system libraries (such as glibc) maintain strict backwards-compatibility, user-installed packages survive base image updates and execute flawlessly on top of the updated, secure OS.
  • Podman Integration Compatibility Blacklist: When applying user-space overlays over system directories (such as /etc), Lazaret dynamically blacklists and bypasses Podman-critical dynamic files (specifically /etc/resolv.conf and /etc/hosts) to allow Podman to inject standard network configurations seamlessly on startup, ensuring name resolution inside the sandbox. The detailed blacklist rules are managed in the development journal.

5. Agent-to-Host Escalation Protocol

5.1 Defining Escalation Boundaries

AI agents inside the container run as root in their user namespace. They can execute standard operations (such as installing packages with apt-get install or compiling binaries) locally within their sandbox.

Escalation is strictly required when the agent needs access to resources outside the gVisor quarantine:

  • Credential Delegation: Querying host-level credential helpers (e.g. AWS token fetchers) or host keys.
  • Host Resource Modification: Accessing host-level files outside the Agent Workspace or requesting configuration changes to host services.

5.2 JSON Message Schema over UDS with Ephemeral mTLS

The Escalation Client is placed inside the Agent Sandbox. It communicates with the host orchestrator over the Escalation Socket (UDS) utilizing a secure Mutual TLS (mTLS) handshake to cryptographically prove sandbox identity.

To prevent container-based file manipulation or hijacking, the host orchestrator creates the Escalation Socket file inside a secure, host-private directory owned by the host developer (defined by socket_dir in the configuration) and bind-mounts only that socket into the Agent Sandbox, leaving it completely decoupled from the writable Agent Workspace.

5.2.1 Cryptographic Identity Verification (Local Ephemeral CA)

To prevent PID-reuse spoofing vulnerabilities (CWE-377 / CWE-362) and host socket hijacking risks, Lazaret establishes container identity cryptographically rather than relying solely on kernel-level or process-level credentials:

  1. Host CA Generation: On startup, the host orchestrator acts as a private, ephemeral Certificate Authority (CA), generating a self-signed root CA certificate and private key in-memory.
  2. Sandbox Client Certificate: For each active sandbox container, the orchestrator generates a unique, short-lived client certificate and private key. This certificate encodes the active container's profile name and unique sandbox ID inside its Subject Alternative Name (SAN).
  3. Mount Injection: The host orchestrator mounts these certificates into the container namespace using a memory-backed, read-only tmpfs volume (see Fixed In-Container Paths).
  4. mTLS Handshake & Host Credentials Pre-flight Check: When the Escalation Client initiates a request, it performs a complete mutual TLS handshake over the Escalation Socket. The host verifies that the client certificate presented by the connection is signed by the host's active in-memory root CA, and checks the profile SAN to confirm exact container identity before any payloads are parsed. To provide defense-in-depth, the host also runs a lightweight pre-flight credentials check (SO_PEERCRED) on the socket to ensure the socket connection is initiated exclusively by the local developer's rootless user space.

Command Request (Sandbox -> Host JSON payload over mTLS)

  • request_id: Unique identifier (e.g., UUID or hashed string).
  • profile: The current agent profile requesting escalation.
  • command: Raw CLI command to run on the host.
  • justification: Detailed justification explaining why host resources are needed.

Command Response (Host -> Sandbox JSON payload over mTLS)

  • request_id: Matches the original command request ID.
  • approved: Boolean indicating if the user permitted execution.
  • exit_code: Process exit code (only set if approved is true).
  • stdout: Combined standard output text.
  • stderr: Combined standard error text.

5.3 HITL Interactive Console Interface & Whitelisting

When a request is received on the host, the lazaret orchestrator temporarily halts standard TTY forwarding and renders a dedicated interactive console prompt using ratatui. The prompt clearly isolates the request ID, profile, justification, and the requested command, offering the user four distinct actions:

  • [A] Approve & Run: Immediately runs the command on the host.
  • [D] Deny Request: Rejects the escalation, returning an access denied response to the sandbox.
  • [E] Edit Command: Prompts the developer to edit the command in-place before executing it.
  • [Q] Abort: Stops the daemon and suspends container operation.

5.3.1 Whitelisting & Injection Prevention (Argument Vector Prefix Matching)

To provide high developer experience (DX) without introducing command injection vulnerabilities, the orchestrator implements a secure command validation pipeline:

  1. Shell Metacharacter Auto-Approve Bypass: Any requested command containing shell-interpreted metacharacters, control characters, or redirections (such as ;, |, &, $(), ` `, >, <, \n, \r, or backslashes) is strictly ineligible for auto-approval. This bypasses the automatic whitelisting engine and demotes the command to require interactive human-in-the-loop (HITL) approval, ensuring that no command chaining, backgrounding, or output redirection can be executed on the host without explicit developer inspection.

  2. Argument Vector Prefix Whitelisting: A requested command string is parsed into an argument vector utilizing a POSIX-compliant shell words parser (e.g., the shell-words crate in Rust) to resolve quotes and escape spaces correctly. The requested vector is compared against the parsed vectors of the configured auto_approve list. To prevent argument injection vulnerabilities (where an agent appends malicious flags like --exec-path or --pager to hijack general binaries), the auto-approve check verifies that the whitelisted vector is a strict prefix vector of the requested vector.

    • Unsafe / Banned Commands: Broad binaries or commands that support execution redirection (such as raw "git" or any general "git " subcommands) are strictly banned from the auto-approve whitelist.
    • Safe Whitelist Example: Whitelist prefix vector ["aws", "sts", "get-caller-identity"] safely matches requested command vector ["aws", "sts", "get-caller-identity", "--query", "Account"] but rejects any other executable.
  3. Direct Execve Execution: Once verified, the orchestrator passes the literal argument vector directly to the host's execution engine (execve or Rust std::process::Command), launching the binary directly without invoking any intermediate host shell, rendering any argument-based shell escape vectors completely inert.


6. Functional & Non-Functional Requirements

6.1 Functional Requirements

ID Requirement Description
FR-1 TOML Profile Loading Dynamically parses and loads config from ~/.config/lazaret/config.toml or override paths.
FR-2 Podman & gVisor Integration Configures container executions using --runtime=runsc for sandboxes and --runtime=runc for sidecars.
FR-3 Network Sidecar Lifecycle Ensures the shell-less Network Sidecar running in runc is active before spawning any Agent Sandbox.
FR-4 Git Hooks Isolation Automatically isolates the container git environment using the Git Shadow.
FR-5 Host Escalation Establishes private host Escalation Socket listener with ephemeral mTLS certificate verification and SO_PEERCRED pre-flight checks, parses JSON requests, and displays interactive HITL terminal prompt.

6.2 Non-Functional Requirements

ID Requirement Description
NFR-1 Minimal Sidecar Footprint The Network Sidecar binary must compile statically (via musl) with minimal external dependencies to maintain a small footprint suitable for scratch.
NFR-2 Robust Async Cancellation Utilizes Tokio's task cancellation to cleanly close containers, clean up Network Sidecar instances, and close sockets on SIGINT.
NFR-3 Dynamic Terminfo Mapping Locates host terminal terminfo configurations (Ghostty, Alacritty, Kitty) and bind-mounts them read-only into the Agent Sandbox.
NFR-4 Zero Shell Footprint The Network Sidecar container has no shell (sh, bash), preventing any remote execution/hijacking.