Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,38 @@ DEFAULT_LLM_MODEL=gpt-4o
# CUSTOM_PROVIDER_BASE_URL=http://localhost:8080/v1
# CUSTOM_PROVIDER_DEFAULT_MODEL=llama-3.1-70b
# CUSTOM_PROVIDER_API_KEY=

# --- Browser tools ---
BROWSER_PROVIDER=local
BROWSER_SIDECAR_URL=ws://clawix-browser:3000
# Auto-generated by scripts/install.mjs on first install. If you set this manually,
# use a 32-byte hex secret (e.g. `openssl rand -hex 32`) and ensure the same value
# reaches the clawix-browser sidecar (it reads it as TOKEN).
BROWSER_AUTH_TOKEN=
BROWSER_INTERNAL_ALLOWLIST=
BROWSER_QUEUE_TIMEOUT_MS=30000
BROWSER_NAVIGATE_TIMEOUT_MS=30000
BROWSER_OP_TIMEOUT_MS=10000
BROWSER_SIDECAR_MAX_SESSIONS=25

# --- Browser tools: alternate providers (opt-in) ---
# BROWSERBASE_API_KEY=
# BROWSERBASE_PROJECT_ID=
# BROWSER_CDP_URL=

# --- Python tools (clawix-pypi-proxy + sibling python-runner) ---
PYTHON_PROXY_URL=http://clawix-pypi-proxy:3141
# Auto-generated by scripts/install.mjs on first install. If you set this manually,
# use a 32-byte hex secret (e.g. `openssl rand -hex 32`) and ensure the same value
# reaches the clawix-pypi-proxy sidecar.
PYTHON_PROXY_AUTH_TOKEN=
PYTHON_RUNNER_IMAGE=clawix-python-runner:latest
PYTHON_POOL_IDLE_TIMEOUT_SEC=300
PYTHON_POOL_MAX_SIZE=20
PYTHON_NET_NETWORK_NAME=clawix-python-net-egress
# Comma-separated host[:port] entries permitted to bypass the RFC1918 block.
# Example: PYTHON_INTERNAL_ALLOWLIST=admin.internal,grafana.internal:3000
PYTHON_INTERNAL_ALLOWLIST=
# Which Plan-tier allowlist file the clawix-pypi-proxy mounts (prod compose only).
# Values: standard | extended | unrestricted. Defaults to extended.
PYTHON_ALLOWLIST_TIER=extended
88 changes: 88 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ services:
interval: 5s
timeout: 5s
retries: 5
networks:
- clawix-internal

redis:
image: redis:7-alpine
Expand All @@ -29,6 +31,8 @@ services:
interval: 5s
timeout: 5s
retries: 5
networks:
- clawix-internal

api-server:
image: node:22-slim
Expand Down Expand Up @@ -74,6 +78,8 @@ services:
SKILLS_BUILTIN_HOST_DIR: ${CLAWIX_HOST_SKILLS_DIR:-${PWD}/skills/builtin}
SKILLS_CUSTOM_DIR: /app/skills/custom
SKILLS_CUSTOM_HOST_DIR: ${CLAWIX_HOST_SKILLS_DIR:-${PWD}/skills/custom}
# Public memory lives under the persistent /data bind mount so it
# survives `docker compose down` and image rebuilds.
command: >
sh -c "apt-get update && apt-get install -y --no-install-recommends docker.io && rm -rf /var/lib/apt/lists/* &&
corepack enable &&
Expand All @@ -97,6 +103,9 @@ services:
condition: service_healthy
redis:
condition: service_healthy
networks:
- clawix-internal
- clawix-browser-egress

web-server:
image: node:22-slim
Expand Down Expand Up @@ -133,6 +142,61 @@ services:
pnpm install --frozen-lockfile &&
pnpm --filter @clawix/shared build &&
pnpm --filter @clawix/web dev"
networks:
- clawix-internal

clawix-pypi-proxy:
build:
context: ./infra/docker/pypi-proxy
container_name: clawix-pypi-proxy
restart: unless-stopped
environment:
ALLOWLIST_FILE: /etc/clawix/python-allowlist.txt
volumes:
- clawix-pypi-cache:/home/devpi/server
- ./infra/python-allowlist/extended.txt:/etc/clawix/python-allowlist.txt:ro
networks:
- clawix-internal
- clawix-python-net-egress
healthcheck:
test: ['CMD', 'curl', '-fsSL', 'http://localhost:3141/+api']
interval: 10s
timeout: 5s
retries: 3
start_period: 30s

clawix-browser:
image: ghcr.io/browserless/chromium:latest
container_name: clawix-browser
restart: unless-stopped
user: '1000:1000'
# Note: read_only: true breaks Chromium 1217+ — its crashpad handler
# subprocess loses argv when the rootfs is read-only, causing
# `chrome_crashpad_handler: --database is required` and aborting launch.
# Other isolation (non-root user, cap_drop, no-new-privileges, internal
# egress network, resource limits) still applies.
cap_drop: [ALL]
cap_add: [SYS_ADMIN]
security_opt:
- no-new-privileges
mem_limit: 2g
cpus: 2.0
pids_limit: 200
shm_size: 256m
environment:
MAX_CONCURRENT_SESSIONS: '${BROWSER_SIDECAR_MAX_SESSIONS:-25}'
TOKEN: '${BROWSER_AUTH_TOKEN}'
# Disable Browserless' built-in queue; we queue at the API layer.
QUEUED: '0'
HEALTHCHECK: 'true'
healthcheck:
test: ['CMD-SHELL', 'wget -q -O - "http://127.0.0.1:3000/active?token=$$TOKEN"']
interval: 10s
timeout: 3s
retries: 5
networks:
- clawix-browser-egress
- clawix-browser-net

volumes:
postgres_data:
Expand All @@ -143,3 +207,27 @@ volumes:
web_node_modules:
web_pkg_node_modules:
shared_web_pkg_node_modules:
clawix-pypi-cache:

networks:
# `name:` pins the actual Docker network name, bypassing Compose's
# `<project>_<network>` prefix. The API spawns sibling containers via the
# host docker socket and attaches them by these exact names — without
# pinning, attach fails with "network not found".
clawix-internal:
name: clawix-internal
driver: bridge
clawix-browser-egress:
name: clawix-browser-egress
driver: bridge
internal: true # blocks traffic from this network to the host/internet
clawix-browser-net:
name: clawix-browser-net
driver: bridge
# External default: this network reaches the public internet by default.
# RFC1918 blocking is enforced by the deploy environment's firewall rules
# or a sidecar egress proxy. See docs/specs/2026-05-06-web-fetch-and-browser-tools-design.md §Egress.
clawix-python-net-egress:
name: clawix-python-net-egress
driver: bridge
internal: false
107 changes: 107 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ services:
interval: 10s
timeout: 5s
retries: 5
networks:
- clawix-internal

redis:
image: redis:7-alpine
Expand All @@ -38,6 +40,8 @@ services:
interval: 10s
timeout: 5s
retries: 5
networks:
- clawix-internal

api:
build:
Expand Down Expand Up @@ -77,6 +81,8 @@ services:
SKILLS_BUILTIN_HOST_DIR: ${CLAWIX_HOST_SKILLS_BUILTIN_DIR:-${PWD}/skills/builtin}
SKILLS_CUSTOM_DIR: /app/skills/custom
SKILLS_CUSTOM_HOST_DIR: ${CLAWIX_HOST_SKILLS_CUSTOM_DIR:-${PWD}/skills/custom}
# Public memory lives under the persistent /data bind mount so it
# survives `docker compose down` and image rebuilds.
# WhatsApp Baileys auth state — written under the persistent /data mount
# so the QR pairing survives container restarts.
WHATSAPP_AUTH_DIR: /data/whatsapp-auth
Expand All @@ -91,6 +97,9 @@ services:
condition: service_healthy
redis:
condition: service_healthy
networks:
- clawix-internal
- clawix-browser-egress

web:
build:
Expand All @@ -108,7 +117,105 @@ services:
NODE_ENV: production
depends_on:
- api
networks:
- clawix-internal

clawix-browser:
image: ghcr.io/browserless/chromium:latest
container_name: clawix-browser
restart: unless-stopped
user: '1000:1000'
# Note: read_only: true breaks Chromium 1217+ — its crashpad handler
# subprocess loses argv when the rootfs is read-only, causing
# `chrome_crashpad_handler: --database is required` and aborting launch.
# Other isolation (non-root user, cap_drop, no-new-privileges, internal
# egress network, resource limits) still applies.
cap_drop: [ALL]
cap_add: [SYS_ADMIN]
security_opt:
- no-new-privileges
mem_limit: 2g
cpus: 2.0
pids_limit: 200
shm_size: 256m
environment:
MAX_CONCURRENT_SESSIONS: '${BROWSER_SIDECAR_MAX_SESSIONS:-25}'
TOKEN: '${BROWSER_AUTH_TOKEN}'
# Disable Browserless' built-in queue; we queue at the API layer.
QUEUED: '0'
# browserless health endpoint is on /health
HEALTHCHECK: 'true'
healthcheck:
test: ['CMD-SHELL', 'wget -q -O - "http://127.0.0.1:3000/active?token=$$TOKEN"']
interval: 10s
timeout: 3s
retries: 5
networks:
- clawix-browser-egress
- clawix-browser-net

clawix-pypi-proxy:
build:
context: ./infra/docker/pypi-proxy
image: clawix-pypi-proxy:latest
container_name: clawix-pypi-proxy
restart: unless-stopped
# Note: this service intentionally does NOT use cap_drop:[ALL] or
# no-new-privileges. The entrypoint needs to start as root to chown
# the named volume mount (/home/devpi/server) and then SUID-drop to
# the devpi user via gosu. Both mechanisms require capabilities and
# SUID semantics that those hardening flags would block. Defense-in-
# depth still comes from: non-root devpi-server runtime, isolated
# Docker network, read-only allowlist mount, devpi listening only on
# 127.0.0.1 (nginx is the only external entry point).
mem_limit: 512m
cpus: 1.0
pids_limit: 200
environment:
ALLOWLIST_FILE: /etc/clawix/python-allowlist.txt
volumes:
- clawix-pypi-cache:/home/devpi/server
- ./infra/python-allowlist/${PYTHON_ALLOWLIST_TIER:-extended}.txt:/etc/clawix/python-allowlist.txt:ro
networks:
# clawix-internal: API health probe + warm-pool runner pip installs.
# clawix-python-net-egress: ephemeral python_run_net runners reach the proxy.
- clawix-internal
- clawix-python-net-egress
healthcheck:
test: ['CMD', 'curl', '-fsSL', 'http://localhost:3141/+api']
interval: 10s
timeout: 5s
retries: 3
start_period: 30s

volumes:
postgres_data:
redis_data:
clawix-pypi-cache:

networks:
# `name:` pins the actual Docker network name, bypassing Compose's
# `<project>_<network>` prefix. The API spawns sibling containers via the
# host docker socket and attaches them by these exact names — without
# pinning, attach fails with "network not found".
clawix-internal:
name: clawix-internal
driver: bridge
clawix-browser-egress:
name: clawix-browser-egress
driver: bridge
internal: true # blocks traffic from this network to the host/internet
clawix-browser-net:
name: clawix-browser-net
driver: bridge
# External default: this network reaches the public internet by default.
# RFC1918 blocking is enforced by the deploy environment's firewall rules
# or a sidecar egress proxy. See docs/specs/2026-05-06-web-fetch-and-browser-tools-design.md §Egress.
clawix-python-net-egress:
name: clawix-python-net-egress
driver: bridge
# External default: this network reaches the public internet by default.
# RFC1918 blocking is enforced by the deploy environment's firewall rules
# or a sidecar egress proxy. Used by python_run_net ephemeral runners and
# also carries proxy traffic from those runners to clawix-pypi-proxy.
# See docs/specs/2026-05-08-python-run-tool-design.md §Security.
3 changes: 3 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default tseslint.config(
'**/prisma/seed.example.ts',
'**/generated/**',
'scripts/**',
'data/**',
],
},
js.configs.recommended,
Expand All @@ -30,6 +31,8 @@ export default tseslint.config(
'eslint.config.mjs',
'vitest.workspace.ts',
'packages/*/vitest.config.ts',
'packages/*/vitest.integration.config.ts',
'packages/*/test/*/*/*.ts',
],
defaultProject: 'tsconfig.base.json',
},
Expand Down
30 changes: 30 additions & 0 deletions infra/docker/pypi-proxy/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
FROM python:3.12-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
gosu \
nginx \
&& rm -rf /var/lib/apt/lists/*

RUN pip install --no-cache-dir \
"devpi-server==6.*" \
"devpi-client==7.*" \
"devpi-tools>=0.4"

RUN useradd --create-home --shell /bin/bash --uid 1000 devpi

# entrypoint runs as root so it can chown the volume mount, then drops to devpi
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

WORKDIR /home/devpi

# Use the canonical env var name (DEVPI_SERVERDIR is deprecated in 6.x)
ENV DEVPISERVER_SERVERDIR=/home/devpi/server

EXPOSE 3141

HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=3 \
CMD curl -fsSL "http://localhost:3141/+api" || exit 1

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
Loading
Loading