Skip to content

Repository files navigation

work

A hardened Docker sandbox for "light" agentic development and research tasks, powered by pi. Feel free to fork and adapt for your own needs. Just update package.json with new extensions and skills, and add any necessary OS-level dependencies to the Dockerfile. Merging to main pushes a new docker image to GHCR for easy use.


Architecture overview

graph TB
    subgraph docker["Docker Compose Network"]
        subgraph work["work container (Node 24 LTS)"]
            pi["Pi Server<br/>(user: agent)<br/>bash, fs, tools<br/>extension hooks"]
            squid["Squid Proxy<br/>:3128<br/><br/>Mode A: CONNECT-only<br/>to allowlisted domains<br/><br/>Mode B: GET/HEAD-only<br/>SSL bump, strip headers"]
            dnsmasq["dnsmasq<br/>127.0.0.1<br/><br/>Mode A: default-deny<br/>Mode B: permissive"]
            sudoers["/etc/sudoers<br/>(immutable)<br/>Generated from<br/>sudo-allowlist.txt"]
            
            pi -->|HTTP/HTTPS| squid
            pi -->|DNS| dnsmasq
            pi -.->|sudo calls| sudoers
            squid -->|upstream| dnsmasq
        end
        
        searxng["SearXNG<br/>:8080<br/>Metasearch Engine"]
        llama["llama-swap<br/>:8080<br/>(optional)<br/>Dynamic Model<br/>Discovery"]
        
        pi -.->|search| searxng
        pi -.->|models| llama
    end
    
    squid -->|filtered| internet((Internet))
    dnsmasq -->|filtered| internet
Loading

Security layers

Layer Mechanism Blocks
OS Immutable /etc/sudoers Non-allowlisted sudo commands (generated at startup, validated with visudo -c, protected by chattr +i and root:root 0440 permissions)
Network squid proxy (Mode A) All outbound except allowlisted HTTPS CONNECT
Network squid proxy (Mode B) POST/PUT/PATCH, query strings, sensitive headers
DNS dnsmasq (Mode A) All non-allowlisted hostnames → 0.0.0.0
OS Docker cap_drop NET_RAW, NET_ADMIN, SYS_PTRACE
OS Docker cap_add LINUX_IMMUTABLE (allows chattr +i on sudoers)
OS Docker seccomp Unconfined (allows squid SSL interception)

Quick start

Prerequisites

  • Docker ≥ 24
  • Docker Compose v2

1. Build the image

docker compose build

Or pull the pre-built image:

docker pull ghcr.io/<owner>/work:main

2. Configure

Edit config/proxy-allowlist.txt to add domains the agent needs to reach:

api.openai.com
api.anthropic.com
registry.npmjs.org
github.com

Edit config/sudo-allowlist.txt to allow specific sudo commands (empty by default):

apt-get update
apt-get install -y curl

3. Run

Provide any necessary environment variables (e.g., API keys) and start the container:

LLAMA_SWAP_URL=https://ai.example.com docker compose up
ANTHROPIC_API_KEY=sk-... docker compose up
OPENAI_API_KEY=sk-... docker compose up
GIT_CREDENTIAL_HOST=github.com GIT_CREDENTIAL_USERNAME=oauth2 GIT_CREDENTIAL_PASSWORD=ghp_... docker compose up

The system starts three processes inside the container:

  • dnsmasq — DNS filtering
  • squid — HTTP/HTTPS proxy
  • Pi Web — Web UI and session daemon (ports 8504)
  • Supercronic — Cron scheduler for background tasks

Open the Pi Web UI at http://localhost:8504 and SearXNG at http://localhost:8080.

Git HTTPS credentials

There is not a strong security boundary between git and a credential helper running inside the same agent runtime: if the agent can use the credential for git fetch, it can usually trigger the same helper path directly. This image therefore supports an explicit startup-time fallback instead of claiming a secret-preserving in-container helper.

At container startup, if git credential env vars are provided, the entrypoint writes them into /home/agent/.git-credentials, sets credential.helper=store, and persists the config in /home/agent/.gitconfig.

Use one of these forms:

# Single host credential assembled at startup
GIT_CREDENTIAL_HOST=github.com \
GIT_CREDENTIAL_USERNAME=oauth2 \
GIT_CREDENTIAL_PASSWORD=ghp_... \
docker compose up

# Optional repo/path-specific match
GIT_CREDENTIAL_HOST=github.com \
GIT_CREDENTIAL_PATH=owner/repo.git \
GIT_CREDENTIAL_USERNAME=oauth2 \
GIT_CREDENTIAL_PASSWORD=ghp_... \
docker compose up

# Multiple preformatted entries (newline-separated)
GIT_CREDENTIAL_URLS=$'https://oauth2:token1@github.com/owner/repo.git\nhttps://user:token2@gitlab.com/group/project.git' \
docker compose up

Notes:

  • GIT_CREDENTIAL_PATH enables credential.useHttpPath=true so git can distinguish per-repo credentials on the same host.
  • GIT_CREDENTIAL_URLS must already be URL-encoded if usernames or passwords contain reserved URL characters.
  • The credentials are persisted on disk inside the container user home; treat this as a convenience fallback, not a secret-isolation mechanism.

4. Using Pi Web

Pi Web provides a browser-based interface for interacting with the agent:

  1. Projects — Create or open a project (folder on the server)
  2. Workspaces — For git repos, create worktrees; for non-git folders, use the project directly
  3. Sessions — Start chat sessions with Pi Coding Agent inside a workspace

All chat history and session data persists in the .pi/sessions directory on the host (bind-mounted into the container).

Use /tools state to see available tools, /tools toggle <name> to enable/disable tools, and other extension commands as needed.

Optional: llama-swap

llama-swap is an optional service for dynamic LLM model swapping. It is disabled by default and can be enabled in two ways:

Option 1: Profile (local llama-swap instance)

docker compose --profile llama-swap up

Option 2: External URL (remote llama-swap service)

LLAMA_SWAP_URL=https://ai.example.com docker compose up

When LLAMA_SWAP_URL is set, the work container will auto-trust the host in the proxy allowlist. Configure your pi models to point to this URL for dynamic model discovery.

Health Monitoring

Docker healthchecks verify that all critical services are running:

Work Container (checked every 30s):

  • ✅ Squid proxy listening on port 3128
  • ✅ dnsmasq DNS resolver listening on port 53
  • ✅ Pi Web session daemon socket exists
  • ✅ Pi Web server listening on port 8504
  • ✅ Supercronic scheduler process running

SearXNG Container (checked every 30s):

  • ✅ HTTP endpoint responding on port 8080

View health status:

docker ps                        # Shows health status in output
docker inspect work --format='{{.State.Health.Status}}'
docker compose ps                # Shows health status for all services

If a service fails its healthcheck after 3 retries, Docker will restart the container automatically.


Configuration reference

Environment variables

Variable Default Description
NETWORK_MODE allowlist allowlist — strict outbound control; open-get — all domains but GET/HEAD only
WORKSPACE_DIR ./agent-workspace Host path mounted as /workspace
PI_WEB_PORT 8504 Host port for the pi web UI
SEARXNG_URL http://searxng:8080 SearXNG endpoint (internal Docker URL); set to a custom URL for external SearXNG
URL_REWRITE_ENABLED false Enable optional URL query-string stripping in Mode B (uses squid-url-rewrite.py)
PROXY_ALLOWLIST Comma-separated domains; appended to config/proxy-allowlist.txt at runtime
SUDO_ALLOWLIST Comma-separated commands (without sudo prefix); appended to config/sudo-allowlist.txt at runtime
LLAMA_SWAP_URL External llama-swap URL for dynamic model discovery (auto-adds host to proxy allowlist)
GIT_CREDENTIAL_URLS Newline-separated full .git-credentials entries written at startup
GIT_CREDENTIAL_PROTOCOL https Protocol used when assembling a single git credential entry
GIT_CREDENTIAL_HOST Hostname for a single git HTTPS credential entry
GIT_CREDENTIAL_PATH Optional repo/path scope; enables credential.useHttpPath=true
GIT_CREDENTIAL_USERNAME Username for a single git HTTPS credential entry
GIT_CREDENTIAL_PASSWORD Password or PAT for a single git HTTPS credential entry
ANTHROPIC_API_KEY Anthropic API key
OPENAI_API_KEY OpenAI API key

config/proxy-allowlist.txt

One domain per line; subdomains are matched automatically. Blank lines and # comments are ignored. Used in Mode A (squid allowlist + dnsmasq default-deny). Can be overridden at runtime via the PROXY_ALLOWLIST env var.

config/sudo-allowlist.txt

One command per line without the sudo prefix. Empty by default. At container startup, the entrypoint converts this file into /etc/sudoers Cmnd_Alias directives, then makes /etc/sudoers immutable with chattr +i so the agent cannot modify sudo permissions. Commands not listed here are blocked by sudo itself. Can be overridden at runtime via the SUDO_ALLOWLIST env var.

config/searxng-settings.yml

SearXNG configuration file. Defines enabled search engines, safe-search level, and server settings. Mounted read-only into the searxng container.

config/llama-swap.yml

llama-swap configuration file. Defines health check timeouts, log levels, server macros, and context-length shortcuts. Mounted read-only into the llama-swap container when running via --profile llama-swap.

config/agent.gitconfig

Default git configuration for the agent user. Copied into the container at /home/agent/.gitconfig. Git credential settings are applied at startup via the GIT_CREDENTIAL_* environment variables.


pi extensions

Local extensions (bundled)

Extension File Purpose
pi-system-prompt extensions/system-prompt.ts Injects sandbox environment details (network mode, proxy behaviour, sudo restrictions) into the agent system prompt
pi-network-mode extensions/network-mode.ts network_mode tool + /network command for runtime sandbox mode switching (allowlist / open-get)
pi-tools extensions/tools.ts /tools command; runtime enable/disable of individual tools; persists selection
pi-scheduler extensions/scheduler.ts /task command and tool; manage scheduled tasks via supercronic (cron for containers); persists to crontab file
pi-todo extensions/todo.ts todo tool; persistent todo list (add / complete / delete / list)
pi-llama-swap extensions/llama-swap.ts Llama-swap dynamic model discovery; field mapping from llama-swap metadata to pi model config
pi-superagent extensions/superagent.ts Weak-model-gathers, strong-model-plans hybrid; single strong-model call for strategic planning

Off-the-shelf extensions (loaded via package.jsonpi install)

Extension Pinned Version Purpose
@earendil-works/pi-coding-agent 0.82.1 Pi Coding Agent core (SDK + runtime)
@jmfederico/pi-web 1.202607.2 Web UI and session daemon
@amartinr/pi-searxng 1.0.3 SearXNG search integration
pi-lens 3.8.61 Code lens / language server integration

Commands

Custom commands provided by local extensions:

Command Extension Usage Description
/network pi-network-mode /network state Show the current runtime network mode and active squid/dnsmasq configs
`/network switch <allowlist open-get>`
/tools pi-tools /tools state Show all tools and their enabled/disabled state
/tools toggle <name> Toggle a specific tool on or off
/tools set <name1,name2,...> Enable only the specified tools, disable all others
/task pi-scheduler /task schedule <name> <prompt> [interval] Create a scheduled task (interval: 5m, 2h, 1d, or cron syntax)
/task list Show all scheduled tasks
/task delete <name> Remove a scheduled task
/superagent pi-superagent /superagent models List all available models for planning
/superagent providers List configured providers and auth status

Session persistence

Session data is stored in .pi/sessions on the host, bind-mounted to /home/agent/.pi/agent/sessions inside the container. Global pi settings live at .pi/agent/settings.json (bind-mounted to /home/agent/.pi/agent/settings.json). Pi Web state lives at .pi/web (bind-mounted to /home/agent/.pi/web). All three directories persist across container rebuilds via bind mounts.

Scheduler state is stored separately in .pi/scheduled on the host, bind-mounted to /home/agent/.pi/scheduled.

Scheduler

The scheduler extension uses supercronic to manage scheduled agent tasks. Scheduler state defaults to /home/agent/.pi/scheduled (intended for a dedicated persistent mount), including the crontab and execution history.

Creating scheduled tasks

Simple tasks (via command):

# Human-readable intervals (converted to cron)
/task schedule hourly-check "Check system status" 1h
/task schedule daily-report "Generate daily report" 1d
/task schedule frequent "Quick check" 5m

# Cron syntax for advanced scheduling
/task schedule nightly "Run backup" "0 2 * * *"  # 2 AM daily
/task schedule weekday "Weekday task" "0 9 * * 1-5"  # 9 AM Mon-Fri

Advanced tasks (via scheduler_task tool):

For tasks requiring prompt files, tool restrictions, skills, custom models, or ephemeral sessions, use the scheduler_task tool:

// Task with prompt file
scheduler_task({
  action: "schedule",
  name: "daily-report",
  promptFile: "tasks/daily_report_prompt.md",
  interval: "1d"
})

// Task with restricted tools and custom model
scheduler_task({
  action: "schedule",
  name: "readonly-audit",
  prompt: "Audit the codebase for security issues",
  tools: ["read", "grep", "find", "ls"],
  model: "sonnet",
  interval: "12h"
})

// Task with skills and ephemeral session
scheduler_task({
  action: "schedule",
  name: "notification-check",
  promptFile: "tasks/check_and_notify.md",
  skills: ["notify", "scheduled-tasks"],
  ephemeralSession: true,
  interval: "1h"
})

Prompt options:

  • prompt: Inline string (max 500 characters). Newlines are automatically converted to spaces.
  • promptFile: Path to a file containing the prompt (workspace-relative or absolute). Passed to pi via @filename syntax.
  • tools: Array of allowed tool names (e.g., ["read", "grep", "find"])
  • skills: Array of skill names (e.g., ["notify", "scheduled-tasks"]). Skills are loaded from ~/.pi/agent/skills/.
  • model: Model pattern or ID (e.g., "sonnet", "gpt-4o")
  • ephemeralSession: Don't save session to disk (useful for recurring tasks that don't need history)

How it works

  1. Command: Use /task schedule for simple tasks, or scheduler_task tool for advanced features
  2. Storage: Task metadata stored as comments in /home/agent/.pi/scheduled/scheduler.crontab
  3. Execution: Supercronic monitors the crontab and invokes a wrapper that runs pi -p with configured options at scheduled times
  4. Isolation: Each task runs in an isolated agent session

Viewing and managing tasks

/task list                    # Show all tasks with schedules and options
/task delete hourly-check     # Remove a task

The crontab file can also be inspected directly at /home/agent/.pi/scheduled/scheduler.crontab for debugging.

Execution history and debugging

Each scheduled invocation is recorded under scheduler state:

/home/agent/.pi/scheduled/history.jsonl
/home/agent/.pi/scheduled/<run-id>/metadata.json
/home/agent/.pi/scheduled/<run-id>/stdout.log
/home/agent/.pi/scheduled/<run-id>/stderr.log

The local Scheduled runs PI WEB panel shows run status, duration, exit code, a basic failure classification, and captured stdout/stderr. Select a run to inspect its logs. Scheduler output is ignored by git because agent output can contain workspace-derived or prompt-derived content.

The wrapper retains the most recent 200 run directories by default. Set SCHEDULER_HISTORY_MAX_RUNS to change that number, or set it to 0 to disable pruning. The JSONL index is append-only, so rows whose logs have been pruned can remain in the index.

After building the image, reload the PI WEB browser tab so it discovers the seeded scheduler-history local plugin. The entrypoint does not overwrite a user-managed copy in the persistent PI WEB directory.

Skills

Skills are loaded from skills/ (declared in package.jsonpi.skills) and copied into the container at ~/.pi/agent/skills/ for global discovery.

Skill Location Purpose
notify skills/notify/ Send push notifications via ntfy.sh for background-triggered events
superagent skills/superagent/ Guide for invoking the superagent planning workflow with strong models

Network modes in detail

Mode A — Allowlist (default)

  • Squid listens on port 3128, accepts only CONNECT to allowlisted domains.
  • dnsmasq returns 0.0.0.0 for all domains by default; only allowlisted domains receive real DNS lookups (forwarded to upstream resolver from container's original resolv.conf).
  • Designed to prevent bulk data exfiltration and DNS-based exfiltration.

Mode B — Open-GET

  • Squid performs TLS interception (SSL bump) using a build-time self-signed CA injected into the container's trust store.
  • Only GET and HEAD methods are forwarded; all others return 403.
  • All request headers except a small safe set (Host, Accept, Accept-Language, Accept-Encoding, User-Agent, Cache-Control) are stripped.
  • Query strings are removed from all URLs before forwarding (optional, enabled via URL_REWRITE_ENABLED=true).
  • dnsmasq forwards all queries upstream.
  • Designed for read-only browsing/research with reduced header leakage.

Runtime mode switching

The sandbox mode can be changed while the container is running:

  • Tool: network_mode
    • {"action":"status"}
    • {"action":"set","mode":"allowlist"}
    • {"action":"set","mode":"open-get"}
  • Command: /network state and /network switch <allowlist|open-get>

Implementation notes:

  • Privileged changes are delegated to /usr/local/bin/network-mode via sudo (explicitly allowlisted in config/sudo-allowlist.txt).
  • The script re-renders runtime dnsmasq/squid configs, validates them, restarts dnsmasq, and reconfigures squid in place.
  • Current mode is persisted to /run/work/network-mode and /run/work/network-state.json.
  • The system-prompt extension reads runtime mode state on each before_agent_start, so prompt injection always reflects the active mode.

Development

See AGENTS.md for coding conventions and testing checklist.

About

Hardened Docker sandbox for AI agent development and research tasks

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages