-
Notifications
You must be signed in to change notification settings - Fork 199
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
222 lines (213 loc) · 9.45 KB
/
Copy pathdocker-compose.yml
File metadata and controls
222 lines (213 loc) · 9.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
services:
postgres:
image: pgvector/pgvector:pg17
environment:
POSTGRES_DB: openbot
POSTGRES_USER: openbot
POSTGRES_PASSWORD: openbot
ports:
- "${POSTGRES_PORT:-5432}:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U openbot -d openbot"]
interval: 5s
timeout: 5s
retries: 10
migrate:
build:
context: .
dockerfile: server/Dockerfile
command: ["bun", "x", "drizzle-kit", "migrate", "--config=drizzle.config.ts"]
environment:
DATABASE_URL: postgres://openbot:openbot@postgres:5432/openbot
depends_on:
postgres:
condition: service_healthy
restart: "no"
# The Bot's computer is long-lived so browser sessions remain signed in across turns.
agent-computer:
build:
context: .
dockerfile: agent-computer/Dockerfile
# Tagged explicitly because the supervisor starts one computer per Bot from this image by name,
# and a Compose-derived name would change with the project.
image: ${COMPUTER_IMAGE:-openbot-agent-computer:latest}
ports:
# Loopback only. This process drives a browser holding real logins; COMPUTER_TOKEN is the
# request control, and loopback keeps the surface off routed networks.
- "127.0.0.1:${COMPUTER_PORT:-4100}:4100"
environment:
# The secret every caller must present. The container refuses to start without it.
COMPUTER_TOKEN: ${COMPUTER_TOKEN:-}
volumes:
- agent-workspace:/workspace
# Chromium's user-data directory is volume-backed so logins survive container restarts.
- agent-profiles:/profiles
# Gives Chromium time to flush its profile after SIGTERM before Docker sends SIGKILL.
stop_grace_period: 30s
healthcheck:
test: ["CMD-SHELL", "bun -e \"await fetch('http://localhost:4100/health')\""]
interval: 10s
timeout: 5s
retries: 5
# Bot identity is attested by SPIRE when these optional services run.
#
# SPIRE issues each computer an SVID based on the container labels set by the supervisor. A
# computer cannot claim another Bot's identity, and an unregistered container receives no identity.
#
# Optional. Without these two services the product runs without workload identity and reports that
# state explicitly.
# Fresh named volumes arrive owned by root, while SPIRE runs as uid 1000. This one-shot service
# assigns ownership without running the CA or agent as root.
spire-init:
image: busybox:1.37
command: ["sh", "-c", "chown -R 1000:1000 /server /agent"]
user: "0:0"
volumes:
- spire-server-data:/server
- spire-agent-data:/agent
spire-server:
image: ghcr.io/spiffe/spire-server:1.15.1
depends_on:
spire-init:
condition: service_completed_successfully
command: ["-config", "/opt/spire/conf/server.conf"]
volumes:
- ./spire/server.conf:/opt/spire/conf/server.conf:ro
# Mounted at the directory the image already owns so SPIRE can create its datastore as uid 1000.
- spire-server-data:/opt/spire/data
- spire-server-socket:/tmp/spire-server/private
healthcheck:
test: ["CMD", "/opt/spire/bin/spire-server", "healthcheck"]
interval: 10s
timeout: 5s
retries: 5
spire-agent:
image: ghcr.io/spiffe/spire-agent:1.15.1
command: ["-config", "/opt/spire/conf/agent.conf", "-joinToken", "${SPIRE_JOIN_TOKEN:-openbot-dev-token}"]
# The agent must see the caller's process to map the Workload API peer to a Docker container. The
# host PID namespace is required because the attestor reads the peer cgroup from that pid.
pid: host
depends_on:
spire-server:
condition: service_healthy
spire-init:
condition: service_completed_successfully
volumes:
- ./spire/agent.conf:/opt/spire/conf/agent.conf:ro
- spire-agent-socket:/tmp/spire-agent/public
- spire-agent-data:/opt/spire/data
# Read-only, and only so the docker workload attestor can see which container is asking.
- /var/run/docker.sock:/var/run/docker.sock:ro
# One computer per Bot.
#
# Each Bot gets a container with its own files, browser session and processes. The Docker socket is
# root-equivalent on the host, so only the supervisor holds it and only exposes Bot-level verbs.
# The SPIRE agent mounts the socket read-only to attest workloads and creates nothing.
supervisor:
build:
context: .
dockerfile: supervisor/Dockerfile
environment:
PORT: "4300"
# Shared with the API server. The Bot-level verb set is the boundary; this token keeps other
# services on the network from calling those verbs.
SUPERVISOR_TOKEN: ${SUPERVISOR_TOKEN:-}
# Handed to every computer this creates, so the server and the computers share one secret.
COMPUTER_TOKEN: ${COMPUTER_TOKEN:-}
COMPUTER_IMAGE: ${COMPUTER_IMAGE:-openbot-agent-computer:latest}
# Which deployment the computers it creates belong to, so two stacks on one Docker host never
# derive the same container and volume names for the same Bot.
COMPUTER_NAMESPACE: ${COMPUTER_NAMESPACE:-openbot}
# Empty by default because the API server runs on the host in local development and reaches
# computers by published loopback ports. Deployments running the server inside this network set
# the network name so computers publish no host ports.
COMPUTER_NETWORK: ${COMPUTER_NETWORK:-}
# Set to runsc to run every computer under gVisor, when the host has it. Left unset each
# computer is an ordinary container and shares the host kernel.
COMPUTER_RUNTIME: ${COMPUTER_RUNTIME:-}
# Identity, when SPIRE is part of this deployment. The supervisor registers an entry per Bot
# and mounts the agent's socket into that Bot's computer so it can fetch its own SVID.
SPIRE_SOCKET: /tmp/spire-server/private/api.sock
SPIRE_TRUST_DOMAIN: openbot.local
SPIRE_AGENT_ID: spiffe://openbot.local/spire/agent/join_token/${SPIRE_JOIN_TOKEN:-openbot-dev-token}
SPIRE_AGENT_SOCKET_VOLUME: ${COMPOSE_PROJECT_NAME:-openbot}_spire-agent-socket
ports:
# For the server on the host to ask for a Bot's computer.
- "${SUPERVISOR_PORT:-4500}:4300"
volumes:
# Read-only because this service only ever needs to ask; it is still root-equivalent, which is
# the whole reason nothing else here gets it.
- /var/run/docker.sock:/var/run/docker.sock:ro
# To register an entry per Bot as each computer is created.
- spire-server-socket:/tmp/spire-server/private
healthcheck:
test: ["CMD-SHELL", "bun -e \"await fetch('http://localhost:4300/health')\""]
interval: 10s
timeout: 5s
retries: 5
# The built-in Bot is an AG-UI endpoint registered the same way as a customer-provided Bot.
agent-bot:
build:
context: .
dockerfile: agent-bot/Dockerfile
ports:
# Loopback, not every interface. The token is the boundary; this means an attacker needs to be
# on the machine before they can even try it. Nothing legitimate reaches a Bot from another
# host: the server calls it over localhost, and other containers use the compose network.
- "127.0.0.1:${BOT_PORT:-4200}:4200"
environment:
OPENAI_API_KEY: ${OPENAI_API_KEY}
# Server sends this on every call to the managed Bot. It refuses to start without it.
MANAGED_AGENT_TOKEN: ${MANAGED_AGENT_TOKEN:-}
# Unset means OpenAI. Set, it is any endpoint speaking the same API, and BOT_MODEL is sent
# to it verbatim.
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-}
BOT_MODEL: ${BOT_MODEL:-gpt-5.5}
healthcheck:
test: ["CMD-SHELL", "bun -e \"await fetch('http://localhost:4200/health')\""]
interval: 10s
timeout: 5s
retries: 5
# The same Bot behavior on a framework, exposed as another AG-UI endpoint and registry row.
agent-langgraph:
build:
context: .
dockerfile: agent-langgraph/Dockerfile
ports:
# Loopback, for the same reason as agent-bot above.
- "127.0.0.1:${LANGGRAPH_PORT:-4201}:4201"
environment:
# The selected provider reads its own key. Models requiring the Responses API use
# BOT_RESPONSES_API instead of changing the streaming loop here.
BOT_PROVIDER: ${BOT_PROVIDER:-openai}
# Same server-to-Bot request boundary as agent-bot above.
MANAGED_AGENT_TOKEN: ${MANAGED_AGENT_TOKEN:-}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
ANTHROPIC_BASE_URL: ${ANTHROPIC_BASE_URL:-}
GOOGLE_API_KEY: ${GOOGLE_API_KEY:-}
GOOGLE_GENERATIVE_AI_BASE_URL: ${GOOGLE_GENERATIVE_AI_BASE_URL:-}
BOT_MODEL: ${BOT_MODEL:-gpt-5.5}
BOT_RESPONSES_API: ${BOT_RESPONSES_API:-false}
# Where this Bot runs a tool: back through the deployment that granted it, never at the vendor.
# `host.docker.internal` because the API server runs on the host, not in this network.
OPENBOT_TOOL_URL: ${OPENBOT_TOOL_URL:-http://host.docker.internal:3001/api/agent-tools/call}
AGENT_TOOL_TOKEN: ${AGENT_TOOL_TOKEN:-}
extra_hosts:
- "host.docker.internal:host-gateway"
healthcheck:
test: ["CMD-SHELL", "bun -e \"await fetch('http://localhost:4201/health')\""]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres-data:
agent-workspace:
spire-server-data:
spire-server-socket:
spire-agent-socket:
spire-agent-data:
agent-profiles: