chore: import upstream snapshot with attribution
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Backend release / release (push) Has been cancelled
Bandit Security Scan / bandit_scan (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / manifest (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / manifest (push) Has been cancelled
Python linting / ruff (push) Has been cancelled
Run python tests with pytest / Run tests and count coverage (3.12) (push) Has been cancelled
React Widget Build / build (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Backend release / release (push) Has been cancelled
Bandit Security Scan / bandit_scan (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push multi-arch DocsGPT Docker image / manifest (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Has been cancelled
Build and push DocsGPT FE Docker image for development / manifest (push) Has been cancelled
Python linting / ruff (push) Has been cancelled
Run python tests with pytest / Run tests and count coverage (3.12) (push) Has been cancelled
React Widget Build / build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
services:
|
||||
frontend:
|
||||
build: ../frontend
|
||||
environment:
|
||||
- VITE_API_HOST=http://localhost:7091
|
||||
- VITE_API_STREAMING=$VITE_API_STREAMING
|
||||
ports:
|
||||
- "5173:5173"
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
backend:
|
||||
build: ../application
|
||||
env_file:
|
||||
- ../.env
|
||||
environment:
|
||||
# Override URLs to use docker service names
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/1
|
||||
- POSTGRES_URI=postgresql://docsgpt:docsgpt@postgres:5432/docsgpt
|
||||
ports:
|
||||
- "7091:7091"
|
||||
volumes:
|
||||
- ../application/indexes:/app/application/indexes
|
||||
- ../application/inputs:/app/application/inputs
|
||||
- ../application/vectors:/app/application/vectors
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_started
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
worker:
|
||||
build: ../application
|
||||
# `parsing` queue carries read_document/parse_document; required for its await to resolve.
|
||||
command: celery -A application.app.celery worker -l INFO -Q docsgpt,parsing
|
||||
env_file:
|
||||
- ../.env
|
||||
environment:
|
||||
# Override URLs to use docker service names
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/1
|
||||
- API_URL=http://backend:7091
|
||||
- POSTGRES_URI=postgresql://docsgpt:docsgpt@postgres:5432/docsgpt
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_started
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
# NOTE: sandboxed code execution (the `code_executor` / `artifact_generator`
|
||||
# tools) is an OPT-IN feature and is intentionally NOT part of this default
|
||||
# stack. To run it, layer the sandbox overlay on top of this file:
|
||||
# docker compose -f docker-compose-azure.yaml \
|
||||
# -f optional/docker-compose.optional.sandbox.yaml up
|
||||
# See deployment/sandbox/README.md.
|
||||
|
||||
redis:
|
||||
image: redis:6-alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=docsgpt
|
||||
- POSTGRES_PASSWORD=docsgpt
|
||||
- POSTGRES_DB=docsgpt
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U docsgpt -d docsgpt"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -0,0 +1,26 @@
|
||||
name: docsgpt-oss
|
||||
services:
|
||||
|
||||
redis:
|
||||
image: redis:6-alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=docsgpt
|
||||
- POSTGRES_PASSWORD=docsgpt
|
||||
- POSTGRES_DB=docsgpt
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U docsgpt -d docsgpt"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -0,0 +1,91 @@
|
||||
name: docsgpt-oss
|
||||
services:
|
||||
|
||||
frontend:
|
||||
image: arc53/docsgpt-fe:develop
|
||||
environment:
|
||||
- VITE_API_HOST=http://localhost:7091
|
||||
- VITE_API_STREAMING=${VITE_API_STREAMING:-true}
|
||||
- VITE_GOOGLE_CLIENT_ID=${VITE_GOOGLE_CLIENT_ID:-}
|
||||
ports:
|
||||
- "5173:5173"
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
|
||||
backend:
|
||||
user: root
|
||||
image: arc53/docsgpt:develop
|
||||
env_file:
|
||||
- ../.env
|
||||
environment:
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/1
|
||||
- CACHE_REDIS_URL=redis://redis:6379/2
|
||||
- POSTGRES_URI=postgresql://docsgpt:docsgpt@postgres:5432/docsgpt
|
||||
ports:
|
||||
- "7091:7091"
|
||||
volumes:
|
||||
- ../application/indexes:/app/indexes
|
||||
- ../application/inputs:/app/inputs
|
||||
- ../application/vectors:/app/vectors
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_started
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
|
||||
worker:
|
||||
user: root
|
||||
image: arc53/docsgpt:develop
|
||||
# `parsing` queue carries read_document/parse_document; required for its await to resolve.
|
||||
command: celery -A application.app.celery worker -l INFO -B -Q docsgpt,parsing
|
||||
env_file:
|
||||
- ../.env
|
||||
environment:
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/1
|
||||
- API_URL=http://backend:7091
|
||||
- CACHE_REDIS_URL=redis://redis:6379/2
|
||||
- POSTGRES_URI=postgresql://docsgpt:docsgpt@postgres:5432/docsgpt
|
||||
volumes:
|
||||
- ../application/indexes:/app/indexes
|
||||
- ../application/inputs:/app/inputs
|
||||
- ../application/vectors:/app/vectors
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_started
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
# NOTE: sandboxed code execution (the `code_executor` / `artifact_generator`
|
||||
# tools) is an OPT-IN feature and is intentionally NOT part of this default
|
||||
# stack. To run it, layer the sandbox overlay on top of this file:
|
||||
# docker compose -f docker-compose-hub.yaml \
|
||||
# -f optional/docker-compose.optional.sandbox.yaml up
|
||||
# See deployment/sandbox/README.md.
|
||||
|
||||
redis:
|
||||
image: redis:6-alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=docsgpt
|
||||
- POSTGRES_PASSWORD=docsgpt
|
||||
- POSTGRES_DB=docsgpt
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U docsgpt -d docsgpt"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -0,0 +1,36 @@
|
||||
services:
|
||||
frontend:
|
||||
build: ../frontend
|
||||
volumes:
|
||||
- ../frontend/src:/app/src
|
||||
environment:
|
||||
- VITE_API_HOST=http://localhost:7091
|
||||
- VITE_API_STREAMING=$VITE_API_STREAMING
|
||||
- VITE_EMBEDDINGS_NAME=$EMBEDDINGS_NAME
|
||||
- DOCKER=true
|
||||
ports:
|
||||
- "5173:5173"
|
||||
|
||||
redis:
|
||||
image: redis:6-alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=docsgpt
|
||||
- POSTGRES_PASSWORD=docsgpt
|
||||
- POSTGRES_DB=docsgpt
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U docsgpt -d docsgpt"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -0,0 +1,87 @@
|
||||
name: docsgpt-oss
|
||||
services:
|
||||
frontend:
|
||||
build: ../frontend
|
||||
volumes:
|
||||
- ../frontend/src:/app/src
|
||||
environment:
|
||||
- VITE_API_HOST=http://localhost:7091
|
||||
- VITE_API_STREAMING=$VITE_API_STREAMING
|
||||
- VITE_GOOGLE_CLIENT_ID=$VITE_GOOGLE_CLIENT_ID
|
||||
ports:
|
||||
- "5173:5173"
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
backend:
|
||||
user: root
|
||||
build: ../application
|
||||
env_file:
|
||||
- ../.env
|
||||
environment:
|
||||
# Override URLs to use docker service names
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/1
|
||||
- CACHE_REDIS_URL=redis://redis:6379/2
|
||||
- POSTGRES_URI=postgresql://docsgpt:docsgpt@postgres:5432/docsgpt
|
||||
ports:
|
||||
- "7091:7091"
|
||||
volumes:
|
||||
- ../application/indexes:/app/indexes
|
||||
- ../application/inputs:/app/inputs
|
||||
- ../application/vectors:/app/vectors
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_started
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
worker:
|
||||
user: root
|
||||
build: ../application
|
||||
# Consumes the default queue AND the dedicated `parsing` queue (read_document /
|
||||
# parse_document). Without `parsing` here the read_document await never resolves.
|
||||
# For heavy/OCR parsing run a separate worker with `-Q parsing`.
|
||||
command: celery -A application.app.celery worker -l INFO -B -Q docsgpt,parsing
|
||||
env_file:
|
||||
- ../.env
|
||||
environment:
|
||||
# Override URLs to use docker service names
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/1
|
||||
- API_URL=http://backend:7091
|
||||
- CACHE_REDIS_URL=redis://redis:6379/2
|
||||
- POSTGRES_URI=postgresql://docsgpt:docsgpt@postgres:5432/docsgpt
|
||||
volumes:
|
||||
- ../application/indexes:/app/indexes
|
||||
- ../application/inputs:/app/inputs
|
||||
- ../application/vectors:/app/vectors
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_started
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
redis:
|
||||
image: redis:6-alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=docsgpt
|
||||
- POSTGRES_PASSWORD=docsgpt
|
||||
- POSTGRES_DB=docsgpt
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U docsgpt -d docsgpt"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -0,0 +1,147 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: docsgpt-api
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: docsgpt-api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: docsgpt-api
|
||||
spec:
|
||||
initContainers:
|
||||
# Block pod start until Postgres accepts connections. The `postgres-init`
|
||||
# Job is responsible for running alembic migrations; this container only
|
||||
# waits for the server to be reachable.
|
||||
- name: wait-for-postgres
|
||||
image: postgres:16-alpine
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
until pg_isready -h postgres -p 5432 -U docsgpt -d docsgpt; do
|
||||
echo "Waiting for postgres..."; sleep 2;
|
||||
done
|
||||
containers:
|
||||
- name: docsgpt-api
|
||||
image: arc53/docsgpt
|
||||
ports:
|
||||
- containerPort: 7091
|
||||
resources:
|
||||
limits:
|
||||
memory: "4Gi"
|
||||
cpu: "2"
|
||||
requests:
|
||||
memory: "2Gi"
|
||||
cpu: "1"
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: docsgpt-secrets
|
||||
env:
|
||||
- name: FLASK_APP
|
||||
value: "application/app.py"
|
||||
- name: DEPLOYMENT_TYPE
|
||||
value: "cloud"
|
||||
- name: POSTGRES_URI
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: docsgpt-secrets
|
||||
key: POSTGRES_URI
|
||||
# Disable in-app auto-bootstrap. The `postgres-init` Job under
|
||||
# deployment/k8s/jobs/ owns schema creation and Alembic migrations,
|
||||
# so application pods must not race with it on rollout.
|
||||
- name: AUTO_MIGRATE
|
||||
value: "false"
|
||||
- name: AUTO_CREATE_DB
|
||||
value: "false"
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: docsgpt-worker
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: docsgpt-worker
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: docsgpt-worker
|
||||
spec:
|
||||
initContainers:
|
||||
- name: wait-for-postgres
|
||||
image: postgres:16-alpine
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
until pg_isready -h postgres -p 5432 -U docsgpt -d docsgpt; do
|
||||
echo "Waiting for postgres..."; sleep 2;
|
||||
done
|
||||
containers:
|
||||
- name: docsgpt-worker
|
||||
image: arc53/docsgpt
|
||||
# `parsing` queue carries read_document/parse_document; required for its await to resolve.
|
||||
# For heavy/OCR parsing, run a separate deployment with `-Q parsing` (and GPU env).
|
||||
command: ["celery", "-A", "application.app.celery", "worker", "-l", "INFO", "-n", "worker.%h", "-Q", "docsgpt,parsing"]
|
||||
resources:
|
||||
limits:
|
||||
memory: "4Gi"
|
||||
cpu: "2"
|
||||
requests:
|
||||
memory: "2Gi"
|
||||
cpu: "1"
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: docsgpt-secrets
|
||||
env:
|
||||
- name: API_URL
|
||||
value: "http://<your-api-endpoint>"
|
||||
- name: POSTGRES_URI
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: docsgpt-secrets
|
||||
key: POSTGRES_URI
|
||||
# Disable in-app auto-bootstrap. The `postgres-init` Job under
|
||||
# deployment/k8s/jobs/ owns schema creation and Alembic migrations,
|
||||
# so application pods must not race with it on rollout.
|
||||
- name: AUTO_MIGRATE
|
||||
value: "false"
|
||||
- name: AUTO_CREATE_DB
|
||||
value: "false"
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: docsgpt-frontend
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: docsgpt-frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: docsgpt-frontend
|
||||
spec:
|
||||
containers:
|
||||
- name: docsgpt-frontend
|
||||
image: arc53/docsgpt-fe
|
||||
ports:
|
||||
- containerPort: 5173
|
||||
resources:
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1"
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
env:
|
||||
- name: VITE_API_HOST
|
||||
value: "http://<your-api-endpoint>"
|
||||
- name: VITE_API_STREAMING
|
||||
value: "true"
|
||||
@@ -0,0 +1,79 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: postgres-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi # Adjust size as needed
|
||||
|
||||
---
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: postgres
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
env:
|
||||
- name: POSTGRES_USER
|
||||
value: "docsgpt"
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: "docsgpt"
|
||||
- name: POSTGRES_DB
|
||||
value: "docsgpt"
|
||||
- name: PGDATA
|
||||
value: "/var/lib/postgresql/data/pgdata"
|
||||
resources:
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1"
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
volumeMounts:
|
||||
- name: postgres-data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- pg_isready
|
||||
- -U
|
||||
- docsgpt
|
||||
- -d
|
||||
- docsgpt
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 6
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- pg_isready
|
||||
- -U
|
||||
- docsgpt
|
||||
- -d
|
||||
- docsgpt
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 15
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
volumes:
|
||||
- name: postgres-data
|
||||
persistentVolumeClaim:
|
||||
claimName: postgres-pvc
|
||||
@@ -0,0 +1,46 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: qdrant-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
|
||||
---
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: qdrant
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: qdrant
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: qdrant
|
||||
spec:
|
||||
containers:
|
||||
- name: qdrant
|
||||
image: qdrant/qdrant:latest
|
||||
ports:
|
||||
- containerPort: 6333
|
||||
resources:
|
||||
limits:
|
||||
memory: "2Gi" # Adjust based on your needs
|
||||
cpu: "1" # Adjust based on your needs
|
||||
requests:
|
||||
memory: "1Gi" # Adjust based on your needs
|
||||
cpu: "500m" # Adjust based on your needs
|
||||
volumeMounts:
|
||||
- name: qdrant-data
|
||||
mountPath: /qdrant/storage
|
||||
volumes:
|
||||
- name: qdrant-data
|
||||
persistentVolumeClaim:
|
||||
claimName: qdrant-pvc
|
||||
@@ -0,0 +1,26 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:latest
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
resources:
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "0.5"
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "250m"
|
||||
@@ -0,0 +1,112 @@
|
||||
# docsgpt-sandbox runner (Jupyter Kernel Gateway). OPT-IN: this manifest and its
|
||||
# NetworkPolicy are NOT in kustomization.yaml; apply them only when enabling
|
||||
# code execution. Sessions are in-process kernels, never child containers; the
|
||||
# Docker socket is NOT mounted. Network egress is constrained by the NetworkPolicy
|
||||
# under deployment/k8s/network-policies/sandbox-egress-policy.yaml -- apply both.
|
||||
#
|
||||
# REQUIRED before enabling: create the shared gateway-token Secret and add the
|
||||
# runner env to the app pods (docsgpt-api / docsgpt-worker), which the default
|
||||
# docsgpt-deploy.yaml intentionally omits (sandbox is off by default):
|
||||
# kubectl create secret generic docsgpt-sandbox-gateway \
|
||||
# --from-literal=token="$(openssl rand -hex 32)"
|
||||
# # on docsgpt-api and docsgpt-worker containers, add:
|
||||
# # - name: SANDBOX_GATEWAY_URL
|
||||
# # value: "http://docsgpt-sandbox:8888"
|
||||
# # - name: SANDBOX_KERNEL_NAME
|
||||
# # value: "docsgpt-python"
|
||||
# # - name: SANDBOX_GATEWAY_AUTH_TOKEN
|
||||
# # valueFrom: { secretKeyRef: { name: docsgpt-sandbox-gateway, key: token } }
|
||||
# The gateway authenticates every HTTP + WebSocket request with the token, so
|
||||
# kernel code cannot reach the gateway control API over loopback (enumerate/kill
|
||||
# sibling kernels or bypass the session cap). The gateway fails closed if the
|
||||
# token is unset.
|
||||
#
|
||||
# On Linux prod, schedule this onto a gVisor `runsc` RuntimeClass for kernel
|
||||
# isolation (uncomment `runtimeClassName` once the node has it installed).
|
||||
#
|
||||
# SINGLE TRUST DOMAIN: all sessions in this pod share one container/uid and are
|
||||
# isolated by working directory only (per-session cwd) — not by a kernel/OS
|
||||
# boundary. This image ships an env-scrubbing kernelspec under the distinct name
|
||||
# "docsgpt-python"; the app (docsgpt-api / docsgpt-worker) MUST select it via
|
||||
# SANDBOX_KERNEL_NAME=docsgpt-python (already set in docsgpt-deploy.yaml) so the
|
||||
# scrubber runs. The stock "python3" spec stays untouched, so it can never
|
||||
# shadow the scrubbing kernel. Sibling workspaces are still readable under the
|
||||
# shared uid and kernels share one address space. Do NOT inject app secrets
|
||||
# (DB/LLM creds) into this pod's env; the runner needs none. For cross-tenant /
|
||||
# untrusted multi-tenant workloads use a per-session VM via
|
||||
# SANDBOX_BACKEND=daytona instead.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: docsgpt-sandbox
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: docsgpt-sandbox
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: docsgpt-sandbox
|
||||
spec:
|
||||
# runtimeClassName: gvisor
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
# Numeric UID the kubelet can verify as non-root; MUST match the sandbox
|
||||
# image's `USER` (deployment/sandbox/Dockerfile). Without it the pod
|
||||
# fails to start with CreateContainerConfigError.
|
||||
runAsUser: 10001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: docsgpt-sandbox
|
||||
image: arc53/docsgpt-sandbox
|
||||
ports:
|
||||
- containerPort: 8888
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
env:
|
||||
# Shared gateway token; the launcher fails closed if it is unset.
|
||||
# Must match SANDBOX_GATEWAY_AUTH_TOKEN on the app pods.
|
||||
- name: SANDBOX_GATEWAY_AUTH_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: docsgpt-sandbox-gateway
|
||||
key: token
|
||||
- name: JUPYTER_RUNTIME_DIR
|
||||
value: /tmp/jupyter-runtime
|
||||
- name: JUPYTER_DATA_DIR
|
||||
value: /tmp/jupyter-data
|
||||
resources:
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1"
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
volumeMounts:
|
||||
# Per-session workspaces + Jupyter runtime files on an emptyDir;
|
||||
# the root FS stays read-only everywhere else.
|
||||
- name: scratch
|
||||
mountPath: /tmp
|
||||
volumes:
|
||||
- name: scratch
|
||||
emptyDir: {}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: docsgpt-sandbox
|
||||
spec:
|
||||
selector:
|
||||
app: docsgpt-sandbox
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8888
|
||||
targetPort: 8888
|
||||
# Cluster-internal only: the runner must never be exposed outside the cluster.
|
||||
type: ClusterIP
|
||||
@@ -0,0 +1,25 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: docsgpt-secrets
|
||||
type: Opaque
|
||||
# Notes:
|
||||
# - POSTGRES_URI below decodes to:
|
||||
# postgresql://docsgpt:docsgpt@postgres:5432/docsgpt
|
||||
# This matches the default Postgres Deployment/Service in this kustomization
|
||||
# and mirrors the compose-level default (see deployment/docker-compose.yaml).
|
||||
# - If you still need the MongoDB-backed vector store (VECTOR_STORE=mongodb),
|
||||
# manually add a MONGO_URI key below (base64-encoded) and apply the
|
||||
# opt-in manifests under deployment/k8s/optional-mongo/. Example:
|
||||
# MONGO_URI: <base64 of mongodb://mongodb-service:27017/docsgpt?retryWrites=true&w=majority>
|
||||
data:
|
||||
LLM_PROVIDER: ZG9jc2dwdA==
|
||||
INTERNAL_KEY: aW50ZXJuYWw=
|
||||
CELERY_BROKER_URL: cmVkaXM6Ly9yZWRpcy1zZXJ2aWNlOjYzNzkvMA==
|
||||
CELERY_RESULT_BACKEND: cmVkaXM6Ly9yZWRpcy1zZXJ2aWNlOjYzNzkvMA==
|
||||
QDRANT_URL: cmVkaXM6Ly9yZWRpcy1zZXJ2aWNlOjYzNzkvMA==
|
||||
QDRANT_PORT: NjM3OQ==
|
||||
# postgresql://docsgpt:docsgpt@postgres:5432/docsgpt
|
||||
POSTGRES_URI: cG9zdGdyZXNxbDovL2RvY3NncHQ6ZG9jc2dwdEBwb3N0Z3Jlczo1NDMyL2RvY3NncHQ=
|
||||
postgres-user: ZG9jc2dwdA==
|
||||
postgres-password: ZG9jc2dwdA==
|
||||
@@ -0,0 +1,45 @@
|
||||
# One-shot migrator: runs `python scripts/db/init_postgres.py` (alembic
|
||||
# upgrade head), then exits. The backend and worker Deployments rely on
|
||||
# an init container that blocks until Postgres is reachable, but this Job
|
||||
# ensures the schema is migrated before application pods start serving.
|
||||
#
|
||||
# Re-apply after upgrades to run new migrations. Safe to re-run: idempotent.
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: postgres-init
|
||||
spec:
|
||||
backoffLimit: 6
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgres-init
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
initContainers:
|
||||
- name: wait-for-postgres
|
||||
image: postgres:16-alpine
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
until pg_isready -h postgres -p 5432 -U docsgpt -d docsgpt; do
|
||||
echo "Waiting for postgres..."; sleep 2;
|
||||
done
|
||||
containers:
|
||||
- name: postgres-init
|
||||
image: arc53/docsgpt
|
||||
command: ["python", "scripts/db/init_postgres.py"]
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: docsgpt-secrets
|
||||
env:
|
||||
- name: FLASK_APP
|
||||
value: "application/app.py"
|
||||
resources:
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "1"
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
@@ -0,0 +1,20 @@
|
||||
# Default DocsGPT Kubernetes stack.
|
||||
#
|
||||
# Uses Postgres as the user-data store (mirrors
|
||||
# deployment/docker-compose.yaml). MongoDB manifests are opt-in and live
|
||||
# under deployment/k8s/optional-mongo/ — they are NOT included here and
|
||||
# will not be applied by `kubectl apply -k deployment/k8s/`.
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- docsgpt-secrets.yaml
|
||||
- deployments/postgres-deploy.yaml
|
||||
- deployments/qdrant-deploy.yaml
|
||||
- deployments/redis-deploy.yaml
|
||||
- deployments/docsgpt-deploy.yaml
|
||||
- services/postgres-service.yaml
|
||||
- services/qdrant-service.yaml
|
||||
- services/redis-service.yaml
|
||||
- services/docsgpt-service.yaml
|
||||
- jobs/postgres-init-job.yaml
|
||||
@@ -0,0 +1,102 @@
|
||||
# Egress / SSRF NetworkPolicy for the docsgpt-sandbox runner.
|
||||
#
|
||||
# The sandbox executes arbitrary LLM-authored code, so app-level URL checks
|
||||
# cannot contain it -- the code opens its own sockets. The hardened container
|
||||
# runs without NET_ADMIN and cannot self-apply iptables, so egress is enforced
|
||||
# here at the network layer: broad public-internet egress is allowed, but all
|
||||
# private / link-local / metadata ranges are DENIED. Ingress is restricted to
|
||||
# the backend and worker pods only.
|
||||
#
|
||||
# Requires a NetworkPolicy-enforcing CNI (Calico, Cilium, etc.). The default
|
||||
# kube-proxy / flannel setups do NOT enforce these rules. Apply with:
|
||||
# kubectl apply -f deployment/k8s/network-policies/sandbox-egress-policy.yaml
|
||||
#
|
||||
# Pods this selects must carry the label `app: docsgpt-sandbox`.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: docsgpt-sandbox-egress
|
||||
labels:
|
||||
app: docsgpt-sandbox
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app: docsgpt-sandbox
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
# Only the backend/worker may reach the sandbox gateway (TCP 8888).
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app: docsgpt-api
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app: docsgpt-worker
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8888
|
||||
egress:
|
||||
# DNS to cluster resolvers (kube-dns). Restricted to UDP/TCP 53 so the
|
||||
# broad-egress rule below does not need to whitelist the resolver IP.
|
||||
# Kept for clusters WITHOUT NodeLocal DNSCache, where the resolver is a pod.
|
||||
- to:
|
||||
- namespaceSelector: {}
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- protocol: TCP
|
||||
port: 53
|
||||
# NodeLocal DNSCache: on clusters running it, the stub resolver is a
|
||||
# link-local IP (commonly 169.254.20.10), NOT a pod, and it sits inside the
|
||||
# 169.254.0.0/16 range carved out of the broad-egress rule below -- so
|
||||
# without this explicit allow ALL DNS is dropped and every hostname lookup
|
||||
# (pip install, public APIs) times out. NetworkPolicy rules are
|
||||
# additive-allow, so this narrowly re-permits port 53 to that one IP.
|
||||
# Operators whose NodeLocal DNSCache uses a different link-local IP must
|
||||
# adjust this cidr.
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 169.254.20.10/32 # NodeLocal DNSCache link-local resolver
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- protocol: TCP
|
||||
port: 53
|
||||
# Broad public-internet egress, with every private / link-local / ULA /
|
||||
# carrier-grade-NAT / documentation range carved out via `except`. The
|
||||
# cloud metadata IP 169.254.169.254 is inside the 169.254.0.0/16 hole, so
|
||||
# it is unreachable. SSRF to internal services is therefore blocked while
|
||||
# legitimate outbound calls (package installs, public APIs) still work.
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
except:
|
||||
- 0.0.0.0/8 # "this network" (RFC1122)
|
||||
- 10.0.0.0/8 # RFC1918 private
|
||||
- 172.16.0.0/12 # RFC1918 private
|
||||
- 192.168.0.0/16 # RFC1918 private
|
||||
- 169.254.0.0/16 # link-local incl. 169.254.169.254 metadata
|
||||
- 100.64.0.0/10 # RFC6598 carrier-grade NAT
|
||||
- 127.0.0.0/8 # loopback
|
||||
- 192.0.0.0/24 # IETF protocol assignments
|
||||
- 192.0.2.0/24 # TEST-NET-1 (documentation)
|
||||
- 198.18.0.0/15 # benchmarking
|
||||
- 198.51.100.0/24 # TEST-NET-2 (documentation)
|
||||
- 203.0.113.0/24 # TEST-NET-3 (documentation)
|
||||
- 224.0.0.0/4 # multicast
|
||||
- 240.0.0.0/4 # reserved
|
||||
- 255.255.255.255/32 # limited broadcast (RFC1122)
|
||||
# IPv6 public egress with private / ULA / link-local ranges carved out.
|
||||
# The IPv6 metadata endpoint (fd00:ec2::254) sits inside the fc00::/7 ULA
|
||||
# hole and is unreachable. Drop this block on IPv4-only clusters.
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: "::/0"
|
||||
except:
|
||||
- "::1/128" # loopback
|
||||
- "fc00::/7" # unique local (ULA) incl. fd00:ec2::254
|
||||
- "fe80::/10" # link-local
|
||||
- "ff00::/8" # multicast
|
||||
- "2001:db8::/32" # documentation
|
||||
@@ -0,0 +1,23 @@
|
||||
# Optional: MongoDB manifests
|
||||
|
||||
These manifests are **opt-in**. The default DocsGPT install uses Postgres
|
||||
for user data (see `deployment/k8s/deployments/postgres-deploy.yaml`).
|
||||
|
||||
Apply the manifests in this directory only if you run DocsGPT with the
|
||||
MongoDB-backed vector store (`VECTOR_STORE=mongodb`) and need an
|
||||
in-cluster MongoDB, or if you are intentionally running on the legacy
|
||||
MongoDB user-data store during the Postgres migration window.
|
||||
|
||||
Mirrors `deployment/optional/` for compose — not applied by the default
|
||||
`kubectl apply -k deployment/k8s/`.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
kubectl apply -f deployment/k8s/optional-mongo/deployments/mongo-deploy.yaml
|
||||
kubectl apply -f deployment/k8s/optional-mongo/services/mongo-service.yaml
|
||||
```
|
||||
|
||||
Then extend `docsgpt-secrets.yaml` with a base64-encoded `MONGO_URI`
|
||||
pointing at `mongodb://mongodb-service:27017/docsgpt?retryWrites=true&w=majority`
|
||||
(or your Atlas/external URI) before re-applying the secret.
|
||||
@@ -0,0 +1,46 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: mongodb-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi # Adjust size as needed
|
||||
|
||||
---
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: mongodb
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mongodb
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mongodb
|
||||
spec:
|
||||
containers:
|
||||
- name: mongodb
|
||||
image: mongo:latest
|
||||
ports:
|
||||
- containerPort: 27017
|
||||
resources:
|
||||
limits:
|
||||
memory: "1Gi"
|
||||
cpu: "0.5"
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "250m"
|
||||
volumeMounts:
|
||||
- name: mongodb-data
|
||||
mountPath: /data/db
|
||||
volumes:
|
||||
- name: mongodb-data
|
||||
persistentVolumeClaim:
|
||||
claimName: mongodb-pvc
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mongodb-service
|
||||
spec:
|
||||
selector:
|
||||
app: mongodb
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 27017
|
||||
targetPort: 27017
|
||||
type: ClusterIP
|
||||
@@ -0,0 +1,25 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: docsgpt-api-service
|
||||
spec:
|
||||
selector:
|
||||
app: docsgpt-api
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 7091
|
||||
type: LoadBalancer
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: docsgpt-frontend-service
|
||||
spec:
|
||||
selector:
|
||||
app: docsgpt-frontend
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 5173
|
||||
type: LoadBalancer
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
spec:
|
||||
selector:
|
||||
app: postgres
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5432
|
||||
targetPort: 5432
|
||||
type: ClusterIP
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: qdrant
|
||||
spec:
|
||||
selector:
|
||||
app: qdrant
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 6333
|
||||
targetPort: 6333
|
||||
type: ClusterIP
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis-service
|
||||
spec:
|
||||
selector:
|
||||
app: redis
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 6379
|
||||
targetPort: 6379
|
||||
type: ClusterIP
|
||||
@@ -0,0 +1,11 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
ollama:
|
||||
image: ollama/ollama
|
||||
ports:
|
||||
- "11434:11434"
|
||||
volumes:
|
||||
- ollama_data:/root/.ollama
|
||||
|
||||
volumes:
|
||||
ollama_data:
|
||||
@@ -0,0 +1,16 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
ollama:
|
||||
image: ollama/ollama
|
||||
ports:
|
||||
- "11434:11434"
|
||||
volumes:
|
||||
- ollama_data:/root/.ollama
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- capabilities: [gpu]
|
||||
|
||||
volumes:
|
||||
ollama_data:
|
||||
@@ -0,0 +1,121 @@
|
||||
# Optional egress-firewall overlay for the docsgpt-sandbox runner (compose).
|
||||
#
|
||||
# Docker Compose cannot express L3 egress filtering the way a Kubernetes
|
||||
# NetworkPolicy can, so SSRF containment in compose deployments is delivered by
|
||||
# taking away the runner's direct internet route and forcing its outbound
|
||||
# traffic through an egress-gateway sidecar that DENIES private / link-local /
|
||||
# metadata ranges and ALLOWS the public internet.
|
||||
#
|
||||
# HOW THIS OVERLAY WORKS
|
||||
# The sandbox overlay (docker-compose.optional.sandbox.yaml) puts docsgpt-sandbox
|
||||
# on two networks: the internal `sandbox-net` (control plane to backend/worker)
|
||||
# and `sandbox-egress` (its internet route). Layer THIS overlay on top of it to
|
||||
# flip `sandbox-egress` to `internal: true`, which removes the
|
||||
# runner's direct route to the internet / host / RFC1918 / metadata entirely --
|
||||
# so raw sockets in arbitrary sandbox code have no route OFF the host except the
|
||||
# deny-private proxy. The proxy is the ONLY container with an internet route
|
||||
# (on its own `sandbox-egress-out` network) and its ACL denies private
|
||||
# destinations. We flip a network's scalar property by KEY (a well-defined
|
||||
# Compose merge); we do NOT try to remove an item from the service's `networks`
|
||||
# LIST, which Compose cannot express (it unions lists) -- the reason a naive
|
||||
# `networks: [sandbox-net]` override silently leaves the runner on its old net.
|
||||
#
|
||||
# WHAT THIS OVERLAY DOES NOT CONTAIN (read before enabling the sandbox):
|
||||
# The runner STILL shares `sandbox-net` with `backend` and `worker` -- that is
|
||||
# its control path and it cannot be removed without breaking code execution. A
|
||||
# shared Docker network is bidirectional and Compose cannot sever it
|
||||
# one-directionally, so arbitrary sandbox code can still open sockets to
|
||||
# `backend:7091` and reach the worker. The internal flip contains raw sockets to
|
||||
# the internet / host / RFC1918 / metadata, NOT to backend/worker on sandbox-net.
|
||||
# (The Kubernetes NetworkPolicy DOES block this, via its RFC1918 egress
|
||||
# carve-out; compose has no equivalent.)
|
||||
#
|
||||
# MITIGATION -- required when enabling the sandbox (do the first, ideally both):
|
||||
# - Run the backend with real authentication (`AUTH_TYPE` != none / a real auth
|
||||
# provider) so a reachable API rejects unauthenticated requests. Without it,
|
||||
# runner->backend reach is a free control-plane bypass.
|
||||
# - Add a host-firewall DROP for runner->backend/worker on sandbox-net
|
||||
# (approach (1) below).
|
||||
#
|
||||
# Two layers below: (2) is what this overlay wires up (internet egress via the
|
||||
# deny-private proxy); (1) is the host-firewall DROP that closes the
|
||||
# runner->backend/worker gap. Apply both for the strongest posture.
|
||||
#
|
||||
# (1) Host-firewall DROP for runner->backend/worker (closes the gap above). Run
|
||||
# as root on the Docker host AFTER `... up`. The project name is pinned to
|
||||
# `docsgpt-oss` (see `name:` in docker-compose.yaml), so the shared control
|
||||
# net is `docsgpt-oss_sandbox-net`. `docker compose ... ps -q` resolves each
|
||||
# container id regardless of its generated name (there is no fixed
|
||||
# `container_name`), and the `index` function is REQUIRED because a
|
||||
# Go-template dotted key cannot contain hyphens -- the old
|
||||
# `{{.NetworkSettings.Networks.docsgpt-oss_sandbox-egress.IPAddress}}` form
|
||||
# never parsed. Both IPs are read on sandbox-net so source+dest match the
|
||||
# shared path:
|
||||
# CF="-f deployment/docker-compose.yaml -f deployment/optional/docker-compose.optional.sandbox.yaml -f deployment/optional/docker-compose.optional.sandbox-egress.yaml"
|
||||
# NET=docsgpt-oss_sandbox-net
|
||||
# SBX=$(docker inspect -f "{{(index .NetworkSettings.Networks \"$NET\").IPAddress}}" "$(docker compose $CF ps -q docsgpt-sandbox)")
|
||||
# BE=$( docker inspect -f "{{(index .NetworkSettings.Networks \"$NET\").IPAddress}}" "$(docker compose $CF ps -q backend)")
|
||||
# WK=$( docker inspect -f "{{(index .NetworkSettings.Networks \"$NET\").IPAddress}}" "$(docker compose $CF ps -q worker)")
|
||||
# iptables -I DOCKER-USER -s "$SBX" -d "$BE" -j DROP # runner -> backend (incl. :7091)
|
||||
# iptables -I DOCKER-USER -s "$SBX" -d "$WK" -j DROP # runner -> worker
|
||||
# (These container-to-container DROPs require the host's
|
||||
# `bridge-nf-call-iptables=1`, Docker's default, so bridged traffic
|
||||
# traverses DOCKER-USER.) If a lookup is empty (renamed project, or the
|
||||
# container is not up yet), resolve by hand instead of running a broken
|
||||
# command: `docker compose $CF ps` to list ids, then `docker inspect
|
||||
# <runner-id>` / `<backend-id>`, read the sandbox-net
|
||||
# `.NetworkSettings.Networks.*.IPAddress`, and add the same DROP rules.
|
||||
# Re-run after any recreate -- container IPs are not stable across `up`.
|
||||
# (Internet / RFC1918 / metadata egress is already gone via the internal
|
||||
# flip, so no separate SSRF drop is needed here.)
|
||||
#
|
||||
# (2) Egress-gateway sidecar (this overlay): the runner has NO direct internet
|
||||
# route (see above); its only path out is the deny-private forward proxy.
|
||||
# Point the runner's HTTP(S) client at the proxy via the env vars below.
|
||||
# This does NOT stop runner->backend/worker on sandbox-net -- pair it with
|
||||
# backend auth and/or approach (1).
|
||||
#
|
||||
# Apply on top of the base stack AND the sandbox overlay (order matters):
|
||||
# docker compose -f deployment/docker-compose.yaml \
|
||||
# -f deployment/optional/docker-compose.optional.sandbox.yaml \
|
||||
# -f deployment/optional/docker-compose.optional.sandbox-egress.yaml up -d
|
||||
#
|
||||
# NOTE: because `sandbox-egress` is internal here, non-HTTP raw egress TO THE
|
||||
# INTERNET / HOST / RFC1918 / METADATA is blocked at L3 regardless of what the
|
||||
# code does (there is simply no route). HTTP(S) that honors the proxy env is
|
||||
# filtered by the proxy ACL. This does NOT block raw egress to `backend:7091` /
|
||||
# the worker on the shared `sandbox-net` (see WHAT THIS OVERLAY DOES NOT CONTAIN
|
||||
# / MITIGATION above): enable backend auth and/or apply approach (1). On a
|
||||
# multi-tenant or untrusted deployment, prefer the Kubernetes NetworkPolicy,
|
||||
# which blocks the internal path too.
|
||||
|
||||
services:
|
||||
# Forward proxy that denies private/link-local/metadata destinations and
|
||||
# allows the public internet. Replace the image/command with your proxy of
|
||||
# choice (tinyproxy, squid, mitmproxy with a deny rule, etc.); the ACL must
|
||||
# DENY 10/8, 172.16/12, 192.168/16, 169.254/16, 127/8 and ALLOW the rest.
|
||||
sandbox-egress-proxy:
|
||||
image: ghcr.io/example/egress-deny-private:latest # replace with your proxy image
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- sandbox-net # reachable by the runner (control-plane net)
|
||||
- sandbox-egress-out # the ONLY container with an internet route
|
||||
|
||||
docsgpt-sandbox:
|
||||
# No `networks:` override here on purpose: the sandbox overlay already
|
||||
# attaches the runner to sandbox-net + sandbox-egress. Flipping sandbox-egress
|
||||
# to internal (below) removes its direct internet route; its only way out is
|
||||
# this proxy.
|
||||
environment:
|
||||
- HTTP_PROXY=http://sandbox-egress-proxy:8080
|
||||
- HTTPS_PROXY=http://sandbox-egress-proxy:8080
|
||||
- NO_PROXY=localhost,127.0.0.1
|
||||
|
||||
networks:
|
||||
# Flip the runner's egress net to internal: no direct route to the internet,
|
||||
# host, RFC1918, or cloud metadata. (Merged by key over the base definition.)
|
||||
sandbox-egress:
|
||||
internal: true
|
||||
# Internet-facing route for the proxy ONLY.
|
||||
sandbox-egress-out:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,91 @@
|
||||
# Optional code-execution sandbox overlay.
|
||||
#
|
||||
# Sandboxed code execution (the `code_executor` / `artifact_generator` tools) is
|
||||
# an OPT-IN feature and is NOT part of the default stack. This overlay adds the
|
||||
# `docsgpt-sandbox` runner (a Jupyter Kernel Gateway; sessions are in-process
|
||||
# kernels, never child containers, and the Docker socket is NOT mounted) and
|
||||
# wires the app to it.
|
||||
#
|
||||
# Apply on top of the base stack (order matters -- base first):
|
||||
# docker compose -f deployment/docker-compose.yaml \
|
||||
# -f deployment/optional/docker-compose.optional.sandbox.yaml up
|
||||
#
|
||||
# REQUIRED: set a shared gateway token before starting. The runner's gateway
|
||||
# authenticates every HTTP + WebSocket request with it (so kernel code cannot
|
||||
# reach the gateway control API over loopback -- enumerate/kill sibling kernels
|
||||
# or bypass the session cap), and the app sends it as `Authorization: token`.
|
||||
# Compose fails to start if it is unset. Put it in `deployment/.env` or export it:
|
||||
# export SANDBOX_GATEWAY_AUTH_TOKEN=$(openssl rand -hex 32)
|
||||
#
|
||||
# SECURITY POSTURE (single trust domain): all sessions share this one
|
||||
# container/uid and are isolated by working directory only -- sibling workspaces
|
||||
# are readable and kernels share one address space. The kernelspec scrubs secrets
|
||||
# from the kernel env, and the runner sits on the internal `sandbox-net` +
|
||||
# `sandbox-egress` only (NOT the `default` net), so it has no Docker-DNS route to
|
||||
# redis/postgres. Two residual gaps remain, exactly as documented in the egress
|
||||
# overlay: (a) internet/host/RFC1918/metadata egress is open until you ALSO layer
|
||||
# docker-compose.optional.sandbox-egress.yaml; (b) the runner shares sandbox-net
|
||||
# with backend/worker (its control path), so on `AUTH_TYPE=none` it can reach the
|
||||
# API unauthenticated -- run real auth and/or the egress overlay's host-firewall
|
||||
# DROP. For untrusted multi-tenant workloads use the Daytona backend
|
||||
# (SANDBOX_BACKEND=daytona, a per-session VM) or the Kubernetes manifests, whose
|
||||
# NetworkPolicy blocks the internal path too.
|
||||
|
||||
services:
|
||||
backend:
|
||||
environment:
|
||||
# Code-execution runner reached over HTTP + WebSocket (no docker socket).
|
||||
- SANDBOX_GATEWAY_URL=http://docsgpt-sandbox:8888
|
||||
# Select the runner's env-scrubbing kernelspec (distinct name; never
|
||||
# shadowed by the stock "python3" spec).
|
||||
- SANDBOX_KERNEL_NAME=docsgpt-python
|
||||
# Shared secret the app sends as `Authorization: token <...>`. Must match
|
||||
# the runner's token below; compose errors if it is unset.
|
||||
- SANDBOX_GATEWAY_AUTH_TOKEN=${SANDBOX_GATEWAY_AUTH_TOKEN:?set SANDBOX_GATEWAY_AUTH_TOKEN to a shared gateway token}
|
||||
networks:
|
||||
- default
|
||||
- sandbox-net
|
||||
|
||||
worker:
|
||||
environment:
|
||||
- SANDBOX_GATEWAY_URL=http://docsgpt-sandbox:8888
|
||||
- SANDBOX_KERNEL_NAME=docsgpt-python
|
||||
- SANDBOX_GATEWAY_AUTH_TOKEN=${SANDBOX_GATEWAY_AUTH_TOKEN:?set SANDBOX_GATEWAY_AUTH_TOKEN to a shared gateway token}
|
||||
networks:
|
||||
- default
|
||||
- sandbox-net
|
||||
|
||||
docsgpt-sandbox:
|
||||
build: ./sandbox
|
||||
mem_limit: ${SANDBOX_MEMORY:-1g}
|
||||
cpus: ${SANDBOX_CPUS:-1.0}
|
||||
pids_limit: 256
|
||||
read_only: true
|
||||
environment:
|
||||
# The gateway REQUIRES this and fails closed if unset (see gateway-launch.sh).
|
||||
# Do NOT add `env_file: ../.env` here -- the runner needs no other app secret.
|
||||
- SANDBOX_GATEWAY_AUTH_TOKEN=${SANDBOX_GATEWAY_AUTH_TOKEN:?set SANDBOX_GATEWAY_AUTH_TOKEN to a shared gateway token}
|
||||
# Keep Jupyter's runtime/connection files on the writable tmpfs.
|
||||
- JUPYTER_RUNTIME_DIR=/tmp/jupyter-runtime
|
||||
- JUPYTER_DATA_DIR=/tmp/jupyter-data
|
||||
tmpfs:
|
||||
# Per-session workspaces (/tmp/docsgpt-sandbox/<session_id>) and Jupyter
|
||||
# runtime files live on tmpfs; the root FS is read-only everywhere else.
|
||||
- /tmp
|
||||
networks:
|
||||
# Reachable by backend/worker over the internal sandbox-net; internet egress
|
||||
# (runtime pip install, etc.) via the dedicated sandbox-egress net. NOT on
|
||||
# `default`: the egress overlay cuts internet by flipping sandbox-egress to
|
||||
# internal, without severing the control path.
|
||||
- sandbox-net
|
||||
- sandbox-egress
|
||||
|
||||
networks:
|
||||
# Control plane between backend/worker and the runner. internal:true => no
|
||||
# internet route on this net; the runner egresses via sandbox-egress.
|
||||
sandbox-net:
|
||||
internal: true
|
||||
# The runner's outbound internet route. Internet-facing by default so runtime
|
||||
# pip install works; docker-compose.optional.sandbox-egress.yaml flips it to
|
||||
# internal and forces egress through a deny-private proxy for SSRF containment.
|
||||
sandbox-egress: {}
|
||||
@@ -0,0 +1,75 @@
|
||||
# docsgpt-sandbox: the always-on code-execution runner.
|
||||
#
|
||||
# One container runs a Jupyter Kernel Gateway; each sandbox session is an
|
||||
# in-process kernel (a child process), NOT a child container. This image does
|
||||
# NOT mount the Docker socket and never spawns containers — that avoids the
|
||||
# host-root risk of Docker-in-Docker and is the core security win.
|
||||
#
|
||||
# The app (backend/worker) is the CLIENT and reaches this service over
|
||||
# HTTP + WebSocket via SANDBOX_GATEWAY_URL.
|
||||
#
|
||||
# Pre-baked doc libs are all permissive (MIT/BSD/Apache) for speed/reliability;
|
||||
# runtime `pip install` still works because egress is open. PyMuPDF and any
|
||||
# other AGPL lib are intentionally excluded; Docling is deferred to its own slice.
|
||||
#
|
||||
# HARDENING (separate slice — NOT done here): run under the gVisor `runsc`
|
||||
# runtime, add network-layer SSRF blocks (drop RFC1918 / link-local /
|
||||
# 169.254.169.254), seccomp profile, read-only root FS + quota'd scratch dir,
|
||||
# and cgroup CPU/mem/PID caps wired from SANDBOX_MEMORY / SANDBOX_CPUS.
|
||||
FROM python:3.12-slim
|
||||
|
||||
# Non-root user for the runner (untrusted code runs as this UID).
|
||||
RUN useradd --create-home --uid 10001 sandbox
|
||||
|
||||
# Exact pins for reproducible builds and no license drift (all MIT/BSD/Apache).
|
||||
RUN pip install --no-cache-dir \
|
||||
jupyter-kernel-gateway==3.0.1 \
|
||||
ipykernel==6.29.5 \
|
||||
python-pptx==1.0.2 \
|
||||
python-docx==1.1.2 \
|
||||
openpyxl==3.1.5 \
|
||||
reportlab==4.2.5 \
|
||||
pandas==2.2.3 \
|
||||
matplotlib==3.9.2
|
||||
|
||||
# Docling (MIT) — OFF by default because it pulls torch + models and makes the
|
||||
# image multi-GB. NOTE: document parsing now runs on the Celery `parsing` worker
|
||||
# (see read_document / parse_document_worker), NOT in this sandbox, so this
|
||||
# INSTALL_DOCLING build-arg path is effectively unused for the read_document flow;
|
||||
# it remains only for sandbox code that opts into Docling explicitly. The build
|
||||
# mechanics are kept for that case. Docling is MIT and uses its own PDF backend;
|
||||
# PyMuPDF (AGPL) is NOT installed here. The base image stays docling-free.
|
||||
ARG INSTALL_DOCLING=false
|
||||
RUN if [ "$INSTALL_DOCLING" = "true" ]; then \
|
||||
pip install --no-cache-dir docling==2.8.3; \
|
||||
fi
|
||||
|
||||
# Env-scrubbing kernel launcher + custom kernelspec. The launcher re-execs
|
||||
# ipykernel under a minimal allowlisted env (env -i) so NO secret in the
|
||||
# gateway's environment (*_API_KEY, *_TOKEN, POSTGRES_URI, the gateway auth
|
||||
# token, ...) ever reaches kernel code. The kernelspec ships under a DISTINCT
|
||||
# name ("docsgpt-python"), so the app selects it with SANDBOX_KERNEL_NAME and it
|
||||
# is never shadowed by the stock ipykernel "python3" spec regardless of the
|
||||
# python prefix. The stock "python3" spec is left untouched (no overwrite, no
|
||||
# kernelspec-name precedence to rely on). SECURITY: never give this image
|
||||
# `env_file: ../.env` -- the scrubber blocks exfil from the kernel, but the
|
||||
# runner image itself should stay free of app secrets it has no use for.
|
||||
COPY kernel-launch.sh /opt/docsgpt/kernel-launch.sh
|
||||
RUN chmod 0555 /opt/docsgpt/kernel-launch.sh
|
||||
COPY gateway-launch.sh /opt/docsgpt/gateway-launch.sh
|
||||
RUN chmod 0555 /opt/docsgpt/gateway-launch.sh
|
||||
COPY kernels/docsgpt-python/kernel.json /usr/local/share/jupyter/kernels/docsgpt-python/kernel.json
|
||||
|
||||
# Numeric UID (not the name) so a kubelet with `runAsNonRoot: true` can verify
|
||||
# the user is non-root without resolving /etc/passwd. This uid MUST match
|
||||
# `runAsUser` in deployment/k8s/deployments/sandbox-deploy.yaml.
|
||||
USER 10001
|
||||
WORKDIR /home/sandbox
|
||||
|
||||
EXPOSE 8888
|
||||
|
||||
# The launcher REQUIRES SANDBOX_GATEWAY_AUTH_TOKEN and fails closed if it is
|
||||
# unset: the gateway control API is reachable from kernel code over loopback, so
|
||||
# it must never run unauthenticated (see gateway-launch.sh). The token is shared
|
||||
# with the app and is scrubbed from the kernel env by kernel-launch.sh.
|
||||
CMD ["/opt/docsgpt/gateway-launch.sh"]
|
||||
@@ -0,0 +1,284 @@
|
||||
# docsgpt-sandbox runner
|
||||
|
||||
Opt-in Jupyter Kernel Gateway that executes sandboxed LLM code. The DocsGPT
|
||||
backend/worker is the **client** and connects over HTTP + WebSocket via
|
||||
`SANDBOX_GATEWAY_URL`. Each session is an **in-process kernel** (child process),
|
||||
never a child container; the Docker socket is **not** mounted.
|
||||
|
||||
## Enabling code execution (opt-in)
|
||||
|
||||
The runner is **opt-in**. Neither `code_executor` nor `artifact_generator` is a
|
||||
default chat tool (both were removed from `DEFAULT_CHAT_TOOLS`), and the runner
|
||||
is **not** part of the base compose stack — a plain `docker compose up` does
|
||||
**not** start `docsgpt-sandbox`. Enable it by layering the sandbox overlay and
|
||||
setting a shared gateway token:
|
||||
|
||||
```bash
|
||||
export SANDBOX_GATEWAY_AUTH_TOKEN=$(openssl rand -hex 32)
|
||||
docker compose \
|
||||
-f deployment/docker-compose.yaml \
|
||||
-f deployment/optional/docker-compose.optional.sandbox.yaml up
|
||||
```
|
||||
|
||||
The token is **required** — the gateway fails closed if it is unset (see
|
||||
*Gateway authentication* below). Add the egress-firewall overlay for SSRF
|
||||
containment (see *Network egress / SSRF*):
|
||||
|
||||
```bash
|
||||
docker compose \
|
||||
-f deployment/docker-compose.yaml \
|
||||
-f deployment/optional/docker-compose.optional.sandbox.yaml \
|
||||
-f deployment/optional/docker-compose.optional.sandbox-egress.yaml up
|
||||
```
|
||||
|
||||
Then enable `code_executor` / `artifact_generator` **per-agent** in the agent
|
||||
tool picker. Agents without them never call the runner, and the backend/worker
|
||||
degrade gracefully when the runner is absent. The `-hub` and `-azure` compose
|
||||
variants take the same sandbox overlay.
|
||||
|
||||
## Isolation model
|
||||
|
||||
Read this before pointing untrusted or multi-tenant workloads at the runner.
|
||||
|
||||
A single Jupyter runner is **one trust domain**. Every session is an in-process
|
||||
kernel under **one shared uid (10001)** in **one container**; sessions are
|
||||
isolated by **working directory only** — each session's code runs with its cwd
|
||||
set to its own `/tmp/docsgpt-sandbox/<session_id>` directory. That is a
|
||||
convenience boundary, not a security boundary between sessions.
|
||||
|
||||
What this slice does close:
|
||||
|
||||
- **Env-secret exfil is closed.** The custom kernelspec
|
||||
(`kernels/docsgpt-python/kernel.json` → `/opt/docsgpt/kernel-launch.sh`)
|
||||
re-execs ipykernel under a minimal allowlisted env (`env -i` keeping only
|
||||
`PATH`, `HOME`, `LANG`, `JUPYTER_RUNTIME_DIR`, `JUPYTER_DATA_DIR`). The image
|
||||
installs this spec under the **distinct name `docsgpt-python`** and the app
|
||||
selects it via `SANDBOX_KERNEL_NAME=docsgpt-python`; because the name is
|
||||
distinct, it is **never shadowed** by the stock ipykernel `python3` spec
|
||||
(kernelspec name resolution prefers `sys.prefix/share` over
|
||||
`/usr/local/share`, so reusing `python3` would silently fall back to the
|
||||
unscrubbed stock spec on a different python prefix). The stock `python3` spec
|
||||
is left untouched. So even though the gateway process inherits the operator's
|
||||
full environment, **no `*_API_KEY` / `*_TOKEN` / `POSTGRES_URI` / gateway auth
|
||||
token reaches kernel code** via `os.environ`, regardless of how the gateway is
|
||||
launched. Loopback ZMQ reachability is preserved because `{connection_file}`
|
||||
is forwarded untouched.
|
||||
- **Per-session workspace perms.** The workspace root and each session dir are
|
||||
created `0700` (defense-in-depth). Under one shared uid this does **not** stop
|
||||
a sibling session from reading another's files — it only narrows exposure to
|
||||
other uids on the box.
|
||||
|
||||
Residual gaps (treat all sessions in one runner as mutually trusting):
|
||||
|
||||
- **Sibling-workspace reads.** All kernels run as the same uid, so one session's
|
||||
code can read another session's files (and `/tmp`) despite `0700`. Distinct
|
||||
uids / per-session VMs are required to close this.
|
||||
- **In-memory / cross-kernel.** Kernels are child processes of one gateway under
|
||||
one uid; OS-level process isolation is the only boundary, and it is not a
|
||||
sandbox boundary against a determined escape. No gVisor in the base posture.
|
||||
(The gateway's HTTP/WebSocket control API is reachable from kernel code over
|
||||
loopback, but it is **authenticated** — see *Gateway authentication* — and the
|
||||
token is scrubbed from the kernel env, so kernel code cannot drive it to
|
||||
enumerate/kill sibling kernels or spawn kernels past the session cap.)
|
||||
- **Egress.** Outbound is broad by design (so code can `pip install` / call
|
||||
public APIs). Private/link-local/metadata ranges are blocked **only** by the
|
||||
network layer — the k8s NetworkPolicy or a host/cloud firewall (see *Network
|
||||
egress / SSRF* below), never by the runner itself.
|
||||
|
||||
For real per-tenant isolation (cross-tenant or untrusted code), use the
|
||||
**Daytona backend** (`SANDBOX_BACKEND=daytona`), which gives each session its
|
||||
own VM. To harden the self-hosted Jupyter runner as a whole (host protection +
|
||||
egress), layer the **gVisor `runsc` runtime**, the **NetworkPolicy**, and a
|
||||
**host firewall** as documented below — those protect the host and constrain
|
||||
egress; they do **not** create a boundary between sessions inside one runner.
|
||||
|
||||
## Run standalone for dev
|
||||
|
||||
Build and run the runner on its own, then point the app at it:
|
||||
|
||||
```bash
|
||||
docker build -t docsgpt-sandbox deployment/sandbox
|
||||
docker run --rm -p 8888:8888 -e SANDBOX_GATEWAY_AUTH_TOKEN=devtoken docsgpt-sandbox
|
||||
# in the app's .env: SANDBOX_GATEWAY_URL=http://localhost:8888
|
||||
# SANDBOX_GATEWAY_AUTH_TOKEN=devtoken
|
||||
```
|
||||
|
||||
The token is required — the image's entrypoint refuses to start without it (see
|
||||
*Gateway authentication*). Without Docker (matches the test harness) you can run
|
||||
the gateway directly from a venv that has `jupyter-kernel-gateway` installed; set
|
||||
a matching `--KernelGatewayApp.auth_token`:
|
||||
|
||||
```bash
|
||||
jupyter kernelgateway --KernelGatewayApp.ip=0.0.0.0 --KernelGatewayApp.port=8888 \
|
||||
--KernelGatewayApp.auth_token=devtoken \
|
||||
--ZMQChannelsWebsocketConnection.limit_rate=False
|
||||
```
|
||||
|
||||
`--ZMQChannelsWebsocketConnection.limit_rate=False` raises the iopub data-rate
|
||||
limit so large `get_file` base64 payloads aren't silently truncated. (On older
|
||||
gateways the trait may live elsewhere; the client's `get_file` integrity check
|
||||
catches any truncation regardless.)
|
||||
|
||||
A bare-venv gateway uses the **stock** `python3` kernelspec, which inherits the
|
||||
gateway's full env (no secret scrubbing). The default `SANDBOX_KERNEL_NAME` is
|
||||
`python3`, so plain venv dev gets no scrubbing — acceptable for single-trust
|
||||
dev. The Docker image instead ships the env-scrubbing spec under the distinct
|
||||
name `docsgpt-python` (see *Isolation model*) and the runner stack sets
|
||||
`SANDBOX_KERNEL_NAME=docsgpt-python`. To get the scrubbing behavior in a venv,
|
||||
copy `kernels/docsgpt-python/kernel.json` (pointing `argv` at a local copy of
|
||||
`kernel-launch.sh`) into a Jupyter data dir on the kernelspec search path and
|
||||
set `SANDBOX_KERNEL_NAME=docsgpt-python` before launching.
|
||||
|
||||
## Gateway authentication
|
||||
|
||||
The gateway **requires** an auth token and **fails closed** if it is unset — the
|
||||
image's entrypoint (`gateway-launch.sh`) refuses to start an unauthenticated
|
||||
gateway. This matters even on an internal-only network: the gateway and every
|
||||
session kernel share one container, so kernel code can reach the gateway's
|
||||
control API over **loopback** (`http://localhost:8888`). Without auth, that
|
||||
control API would let kernel code enumerate/attach/kill sibling sessions'
|
||||
kernels and spawn kernels without bound (bypassing the app-side session cap).
|
||||
|
||||
Set the same token on the runner and the app via `SANDBOX_GATEWAY_AUTH_TOKEN`
|
||||
(the app sends it as `Authorization: token <...>`; the runner's gateway
|
||||
validates it on every HTTP + WebSocket request). Kernel code cannot read it: the
|
||||
kernelspec launcher scrubs it from the kernel env (see *Isolation model*), so it
|
||||
is present for the gateway process only. The image also does **not** set
|
||||
`--KernelGatewayApp.allow_origin=*`.
|
||||
|
||||
## In docker-compose
|
||||
|
||||
The `docsgpt-sandbox` service lives in the opt-in overlay
|
||||
`deployment/optional/docker-compose.optional.sandbox.yaml` (layered on the base
|
||||
stack; see *Enabling code execution (opt-in)*) on an internal-only network with
|
||||
no published host port. The overlay puts the backend and worker on `sandbox-net`
|
||||
to reach the runner at `http://docsgpt-sandbox:8888`, and sets
|
||||
`SANDBOX_KERNEL_NAME=docsgpt-python` on them (the runner only ships the
|
||||
kernelspec; the app chooses it) plus the shared `SANDBOX_GATEWAY_AUTH_TOKEN`. In
|
||||
k8s these are added to the `docsgpt-api` and `docsgpt-worker` deployments when
|
||||
enabling the opt-in `sandbox-deploy.yaml` (the default `docsgpt-deploy.yaml`
|
||||
omits them); see that manifest's header for the exact env and the token Secret.
|
||||
|
||||
## Artifact rendering on Daytona (snapshot)
|
||||
|
||||
The `artifact` tool renders `presentation` / `document` / `spreadsheet` / `pdf`
|
||||
specs by running a fixed renderer **inside the sandbox** that imports
|
||||
`python-pptx`, `python-docx`, `openpyxl`, and `reportlab` (HTML/markdown need no
|
||||
library). The self-hosted Jupyter runner inherits these from the backend venv,
|
||||
but Daytona's default snapshot is a plain Python image — so under
|
||||
`SANDBOX_BACKEND=daytona` those renders fail with `render failed: ExecutionError`
|
||||
(a `ModuleNotFoundError` raised inside the sandbox). HTML/markdown still work.
|
||||
|
||||
Bake the libraries into a Daytona snapshot once, then point `DAYTONA_SNAPSHOT`
|
||||
at it:
|
||||
|
||||
```bash
|
||||
# Reads DAYTONA_API_KEY / DAYTONA_API_URL / DAYTONA_TARGET from .env:
|
||||
python scripts/build_daytona_snapshot.py # builds "docsgpt-artifacts-py312"
|
||||
# then in .env:
|
||||
# DAYTONA_SNAPSHOT=docsgpt-artifacts-py312
|
||||
```
|
||||
|
||||
The snapshot lives in **your** Daytona account, so each deployment builds its
|
||||
own — the script is idempotent and skips if the name already exists. Keep the
|
||||
pins in `scripts/build_daytona_snapshot.py` in sync with the backend venv so the
|
||||
Daytona render output matches the Jupyter-backend output.
|
||||
|
||||
## Document reading (parsing worker — not the sandbox)
|
||||
|
||||
Document reading no longer runs in this sandbox. The `read_document` tool and the
|
||||
workflow native-file extract branch enqueue a `parse_document` Celery task that
|
||||
parses the document **in the backend** (Docling, already in
|
||||
`application/requirements.txt`) and awaits the result. The task is routed to a
|
||||
dedicated **`parsing` queue** (`settings.DOCUMENT_PARSE_QUEUE`, default
|
||||
`"parsing"`) so a parse enqueued from inside a Celery worker (headless/scheduled
|
||||
agent) is served by a separate worker and never self-deadlocks the awaiting one.
|
||||
|
||||
Run a dedicated parsing worker that consumes the `parsing` queue:
|
||||
|
||||
```bash
|
||||
celery -A application.app.celery worker -Q parsing -l INFO
|
||||
```
|
||||
|
||||
It can be GPU-enabled with its own env (`DOCLING_OCR_ENABLED=true` plus GPU
|
||||
libraries) so OCR-heavy parsing runs on a separate, optionally larger pool.
|
||||
|
||||
**Dev / single-worker setups:** without a dedicated parsing worker the default
|
||||
worker must also consume `parsing`, or the tool's await never resolves:
|
||||
|
||||
```bash
|
||||
celery -A application.app.celery worker -Q docsgpt,parsing -l INFO
|
||||
```
|
||||
|
||||
Tuning settings: `DOCUMENT_PARSE_TIMEOUT` (seconds the tool awaits before
|
||||
degrading to an error), `DOCUMENT_PARSE_MAX_BYTES` (per-document byte cap; 0
|
||||
reuses `SANDBOX_MAX_INPUT_BYTES`).
|
||||
|
||||
## Network egress / SSRF
|
||||
|
||||
The runner allows **broad outbound egress** (so sandboxed code can `pip install`
|
||||
and call public APIs) but private, link-local, and cloud-metadata ranges **MUST
|
||||
be blocked at the network layer**. This is not optional: the sandbox executes
|
||||
arbitrary LLM-authored code, which opens its own sockets — app-level URL checks
|
||||
(the `mcp_tool.py` approach) cannot contain it. Without a network-layer block,
|
||||
sandbox code can reach `169.254.169.254` (cloud instance metadata / credentials)
|
||||
and internal services on the private network.
|
||||
|
||||
The hardened container runs **without `NET_ADMIN`**, so it cannot self-apply
|
||||
`iptables`. Enforcement therefore lives in deployment config:
|
||||
|
||||
- **Kubernetes** — apply
|
||||
[`deployment/k8s/network-policies/sandbox-egress-policy.yaml`](../k8s/network-policies/sandbox-egress-policy.yaml).
|
||||
It allows `0.0.0.0/0` egress with `except` carve-outs for RFC1918
|
||||
(`10/8`, `172.16/12`, `192.168/16`), link-local (`169.254/16`, which contains
|
||||
`169.254.169.254`), loopback, CGNAT, documentation/test ranges, and the IPv6
|
||||
ULA/link-local equivalents — and restricts ingress to the API/worker pods on
|
||||
TCP 8888. It requires a policy-enforcing CNI (Calico, Cilium, …); plain
|
||||
flannel/kube-proxy will silently not enforce it. The matching sandbox pod is
|
||||
[`deployment/k8s/deployments/sandbox-deploy.yaml`](../k8s/deployments/sandbox-deploy.yaml)
|
||||
(label `app: docsgpt-sandbox`).
|
||||
|
||||
```bash
|
||||
kubectl apply -f deployment/k8s/deployments/sandbox-deploy.yaml
|
||||
kubectl apply -f deployment/k8s/network-policies/sandbox-egress-policy.yaml
|
||||
```
|
||||
|
||||
- **docker-compose** — compose cannot express L3 egress filtering natively. The
|
||||
sandbox overlay reaches the runner over an `internal: true` control network
|
||||
(`sandbox-net`, no host port) and gives it internet egress on a dedicated
|
||||
`sandbox-egress` bridge — but that bridge does not by itself block the metadata
|
||||
IP or RFC1918. Apply
|
||||
[`deployment/optional/docker-compose.optional.sandbox-egress.yaml`](../optional/docker-compose.optional.sandbox-egress.yaml),
|
||||
which flips `sandbox-egress` to `internal: true` (removing the runner's direct
|
||||
internet/RFC1918/metadata route entirely) and forces egress through a
|
||||
deny-private **egress-gateway proxy** sidecar. That `internal` flip is what
|
||||
contains **raw sockets to the internet/host/RFC1918/metadata** (a forward proxy
|
||||
only filters code that honors `HTTP(S)_PROXY`).
|
||||
|
||||
**What compose canNOT contain:** the runner stays on `sandbox-net` with the
|
||||
backend and worker — that is its control path, and a shared Docker network is
|
||||
bidirectional, so Compose cannot sever it one-directionally. Sandbox code can
|
||||
therefore still open sockets to `backend:7091` and the worker. This is a real
|
||||
gap the Kubernetes NetworkPolicy closes (via its RFC1918 egress carve-out) but
|
||||
compose cannot. **Mitigate it** when enabling the sandbox: run the backend with
|
||||
real authentication (`AUTH_TYPE` != none / a real auth provider) so a reachable
|
||||
API rejects unauthenticated requests — **required** — and/or add a host-firewall
|
||||
`DROP` for runner→backend/worker on `sandbox-net` (see approach (1) in the
|
||||
overlay file's header comment). The runner is not on the `default` network, so
|
||||
it has no Docker-DNS route to redis/postgres; if the broker/DB publish host
|
||||
ports on a cloud VM, also apply the egress overlay (its `internal` flip removes
|
||||
the runner's route to the host gateway / RFC1918) or bind those ports to
|
||||
`127.0.0.1`.
|
||||
|
||||
## Other hardening (deployment-level)
|
||||
|
||||
The gVisor `runsc` runtime (kernel isolation for untrusted code), seccomp
|
||||
profile, read-only root FS, non-root, and cgroup CPU/mem/PID caps (wired from
|
||||
`SANDBOX_MEMORY` / `SANDBOX_CPUS`) are deployment-level concerns. The compose
|
||||
service in `deployment/optional/docker-compose.optional.sandbox.yaml` already
|
||||
sets `read_only`, `mem_limit`, `cpus`, and `pids_limit`; the k8s
|
||||
`sandbox-deploy.yaml` sets the
|
||||
equivalent `securityContext` + resource limits and has a commented
|
||||
`runtimeClassName: gvisor` to enable on nodes with the `runsc` RuntimeClass
|
||||
installed. These complement — they do not replace — the network egress policy
|
||||
above.
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh
|
||||
# Entrypoint for the docsgpt-sandbox Jupyter Kernel Gateway.
|
||||
#
|
||||
# The gateway and every session kernel share this one container, so LLM-authored
|
||||
# kernel code can reach the gateway's control API over loopback
|
||||
# (http://localhost:8888). WITHOUT authentication that means kernel code could
|
||||
# enumerate/attach/kill sibling sessions' kernels and spawn kernels without bound
|
||||
# (bypassing the app-side session cap). We therefore REQUIRE an auth token and
|
||||
# fail closed if it is missing -- an unauthenticated gateway must never start.
|
||||
#
|
||||
# The token is shared with the app via SANDBOX_GATEWAY_AUTH_TOKEN (the app sends
|
||||
# it as `Authorization: token <...>`). kernel-launch.sh re-execs ipykernel under a
|
||||
# scrubbed `env -i` allowlist that excludes the token, so it is NOT in kernel code's
|
||||
# os.environ. NOTE: this is not a hard secret boundary -- the gateway and kernels
|
||||
# share one container and uid, so kernel code can still read the gateway's env via
|
||||
# /proc/<gateway_pid>/environ. The token limits the blast radius (an outside caller
|
||||
# cannot drive the gateway), but the Jupyter runner is a single trust domain: for
|
||||
# real isolation against untrusted code use the Daytona backend or run under gVisor
|
||||
# (see deployment/sandbox/README.md).
|
||||
set -eu
|
||||
|
||||
TOKEN="${SANDBOX_GATEWAY_AUTH_TOKEN:-}"
|
||||
if [ -z "$TOKEN" ]; then
|
||||
echo "docsgpt-sandbox: refusing to start an unauthenticated gateway." >&2
|
||||
echo "Set SANDBOX_GATEWAY_AUTH_TOKEN (same value on the app and this container)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ip=0.0.0.0 so the backend/worker can reach it over the internal sandbox network.
|
||||
# auth_token gates every HTTP + WebSocket request, including loopback ones from
|
||||
# kernel code. limit_rate=False raises the iopub data-rate cap so large get_file
|
||||
# base64 payloads are not truncated (the get_file integrity check still guards
|
||||
# truncation if this is ever off).
|
||||
exec jupyter kernelgateway \
|
||||
--KernelGatewayApp.ip=0.0.0.0 \
|
||||
--KernelGatewayApp.port=8888 \
|
||||
--KernelGatewayApp.auth_token="$TOKEN" \
|
||||
--ZMQChannelsWebsocketConnection.limit_rate=False
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
# Env-scrubbing launcher for the docsgpt-sandbox ipykernel.
|
||||
#
|
||||
# The gateway process inherits the operator's full environment, which can carry
|
||||
# secrets (*_API_KEY, *_TOKEN, POSTGRES_URI, the gateway auth token, ...). Stock
|
||||
# kernels inherit that env verbatim, so LLM-authored code could read it via
|
||||
# os.environ. This wrapper re-execs ipykernel under a MINIMAL allowlisted env so
|
||||
# NO secret reaches kernel code, regardless of how the gateway was launched.
|
||||
#
|
||||
# Only what ipykernel needs is kept: PATH (find python), HOME (~/.ipython etc),
|
||||
# LANG (encoding), and the Jupyter runtime/data dirs (writable tmpfs paths). The
|
||||
# {connection_file} the gateway passes is forwarded via "$@" so loopback ZMQ
|
||||
# reachability is preserved -- do NOT drop or rewrite those args.
|
||||
exec env -i \
|
||||
PATH="${PATH}" \
|
||||
HOME="${HOME}" \
|
||||
LANG="${LANG}" \
|
||||
JUPYTER_RUNTIME_DIR="${JUPYTER_RUNTIME_DIR}" \
|
||||
JUPYTER_DATA_DIR="${JUPYTER_DATA_DIR}" \
|
||||
python -m ipykernel_launcher "$@"
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"argv": [
|
||||
"/opt/docsgpt/kernel-launch.sh",
|
||||
"-f",
|
||||
"{connection_file}"
|
||||
],
|
||||
"display_name": "Python 3 (docsgpt-sandbox, scrubbed env)",
|
||||
"language": "python",
|
||||
"metadata": {
|
||||
"debugger": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user