Repository navigation
Expand file tree
/
Copy pathcompose.yaml
More file actions
246 lines (242 loc) · 10.8 KB
/
Copy pathcompose.yaml
File metadata and controls
246 lines (242 loc) · 10.8 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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
name: termflow-v020-local
services:
control-plane:
build:
context: ..
dockerfile: deploy/Dockerfile.control-plane
read_only: true
tmpfs:
- /tmp:size=64m,mode=1777
cap_drop:
- ALL
cap_add:
- CHOWN
- DAC_OVERRIDE
- SETUID
- SETGID
- SETPCAP
security_opt:
- no-new-privileges:true
command:
- /opt/termflow/bin/termflow-control
- serve
- --host
- 0.0.0.0
- --port
- "8000"
environment:
TERMFLOW_ADMIN_TOKEN: ${TERMFLOW_ADMIN_TOKEN:?set TERMFLOW_ADMIN_TOKEN}
TERMFLOW_DATABASE_URL: sqlite+aiosqlite:////app/data/termflow.db
TERMFLOW_ALLOW_INSECURE_LOOPBACK: ${TERMFLOW_ALLOW_INSECURE_LOOPBACK:-true}
TERMFLOW_STATIC_DIR: /app/frontend-dist
TERMFLOW_PUBLIC_BASE_URL: ${TERMFLOW_PUBLIC_BASE_URL:-http://127.0.0.1:8765}
TERMFLOW_BROWSER_SESSION_TTL_SECONDS: ${TERMFLOW_BROWSER_SESSION_TTL_SECONDS:-28800}
TERMFLOW_BROWSER_SESSION_CAPACITY: ${TERMFLOW_BROWSER_SESSION_CAPACITY:-4096}
TERMFLOW_TOTP_MASTER_KEY:
TERMFLOW_TOTP_AUTO_MASTER_KEY_FILE: /app/totp-secrets/totp-master-key
TERMFLOW_ENROLLMENT_TOKEN_TTL_SECONDS: ${TERMFLOW_ENROLLMENT_TOKEN_TTL_SECONDS:-60}
TERMFLOW_TERMINAL_MAX_FRAME_BYTES: ${TERMFLOW_TERMINAL_MAX_FRAME_BYTES:-65536}
TERMFLOW_TERMINAL_INPUT_RATE_BYTES_PER_SECOND: ${TERMFLOW_TERMINAL_INPUT_RATE_BYTES_PER_SECOND:-262144}
TERMFLOW_TERMINAL_QUEUE_MAX_MESSAGES: ${TERMFLOW_TERMINAL_QUEUE_MAX_MESSAGES:-256}
TERMFLOW_TERMINAL_QUEUE_MAX_BYTES: ${TERMFLOW_TERMINAL_QUEUE_MAX_BYTES:-1048576}
TERMFLOW_TERMINAL_RESUME_GRACE_SECONDS: ${TERMFLOW_TERMINAL_RESUME_GRACE_SECONDS:-30}
# B-to-OpenCode basic auth pair (mirrors the opencode-agent service's
# OPENCODE_SERVER_* values; both or neither).
TERMFLOW_AGENT_OPENCODE_USERNAME: ${OPENCODE_SERVER_USERNAME:-}
TERMFLOW_AGENT_OPENCODE_PASSWORD: ${OPENCODE_SERVER_PASSWORD:-}
# The hardened reference container has no /workspace mount. Its /tmp
# tmpfs is the isolated, writable session directory for this deployment.
TERMFLOW_AGENT_OPENCODE_DIRECTORY: ${TERMFLOW_AGENT_OPENCODE_DIRECTORY:-/tmp}
# B's simplified supervisor and OpenCode must observe the same
# binding-scoped MCP capability during an epoch restart.
TERMFLOW_AGENT_OPENCODE_MCP_TOKEN: ${OPENCODE_AGENT_MCP_TOKEN:?set OPENCODE_AGENT_MCP_TOKEN}
# Independent credential used only by the cleanup helper callback. Keep
# it separate from the administrator and MCP capabilities.
TERMFLOW_AGENT_CLEANUP_HELPER_TOKEN: ${TERMFLOW_AGENT_CLEANUP_HELPER_TOKEN:-}
# MCP endpoint DNS-rebinding allowlist (plan §10/§16): in-container
# OpenCode reaches B over agent_internal by the service name, so the
# loopback-only defaults would reject every request with 421.
TERMFLOW_AGENT_MCP_ALLOWED_HOSTS: ${TERMFLOW_AGENT_MCP_ALLOWED_HOSTS:-control-plane:8000}
# Provider policy is server-owned. The offline/base profile keeps the
# policy unset (and therefore the catalog fail-closed); the live profile
# below requires every disclosure field before activation.
TERMFLOW_AGENT_PROVIDER_DEEPSEEK_ENDPOINT_ORIGIN: ${TERMFLOW_AGENT_PROVIDER_DEEPSEEK_ENDPOINT_ORIGIN:-https://api.deepseek.com}
TERMFLOW_AGENT_PROVIDER_DEEPSEEK_MODEL_IDS: ${TERMFLOW_AGENT_PROVIDER_DEEPSEEK_MODEL_IDS:-deepseek-v4-flash}
TERMFLOW_AGENT_PROVIDER_DEEPSEEK_REGION: ${TERMFLOW_AGENT_PROVIDER_DEEPSEEK_REGION:-}
TERMFLOW_AGENT_PROVIDER_DEEPSEEK_RETENTION_TERMS: ${TERMFLOW_AGENT_PROVIDER_DEEPSEEK_RETENTION_TERMS:-}
TERMFLOW_AGENT_PROVIDER_DEEPSEEK_RETENTION_VERSION: ${TERMFLOW_AGENT_PROVIDER_DEEPSEEK_RETENTION_VERSION:-}
# An unset policy is represented as false (never as an invalid empty
# boolean) so the base/offline B can boot while ProviderCatalog remains
# fail-closed. The live overlay requires an explicit, verified `true`.
TERMFLOW_AGENT_PROVIDER_DEEPSEEK_NO_TRAINING: ${TERMFLOW_AGENT_PROVIDER_DEEPSEEK_NO_TRAINING:-false}
TERMFLOW_AGENT_PROVIDER_DEEPSEEK_CREDENTIAL_SOURCE: ${TERMFLOW_AGENT_PROVIDER_DEEPSEEK_CREDENTIAL_SOURCE:-DEEPSEEK_API_KEY}
TERMFLOW_AGENT_PROVIDER_DEEPSEEK_POLICY_VERSION: ${TERMFLOW_AGENT_PROVIDER_DEEPSEEK_POLICY_VERSION:-}
ports:
# Bind address is configurable: deployments keep the loopback-only
# default; Docker Desktop (WSL2) verification overrides it to 0.0.0.0
# because its loopback port forwarding resets WSL-side connections.
- ${TERMFLOW_HOST_BIND:-127.0.0.1}:${TERMFLOW_HOST_PORT:-8765}:8000
volumes:
- termflow-data:/app/data
- termflow-totp-key:/app/totp-secrets
networks:
# B serves Web C and the A/C API through the host-published endpoint on
# the normal default bridge, and reaches OpenCode only over the internal
# agent network (plan §16).
- default
- agent_internal
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/healthz"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
# OpenCode reference runtime for the Agent Broker (plan §6.2.1, §16).
#
# One runtime per Agent Binding. B never drives Docker: the supervisor
# connector (plugins.agent_broker.agent.runtime_supervisor) attests
# health/epoch over HTTP while Compose/deployment tooling owns this
# container's lifecycle and configuration.
#
# The TermFlow MCP server entry is kept in the frozen config below and uses
# OpenCode's native {env:} interpolation for the deployment URL/token, so
# no raw shared token is committed to an image or volume.
# A fresh named volume is owned by root. This one-shot, networkless init
# container grants only the pinned guest UID access to the dedicated data
# volume before the long-running non-root service starts.
opencode-init:
image: ghcr.io/anomalyco/opencode:1.18.35@sha256:ae90ec960c871b0ea6df9abc1c2ec1e6b36464e8eca9b0e71e1ed3b553f47aa2
entrypoint:
- /bin/sh
- -ec
command:
- chown 0:0 /data && chmod 0700 /data && chown 405:100 /data
volumes:
- opencode-data:/data
network_mode: none
user: "0:0"
cap_drop:
- ALL
cap_add:
- CHOWN
security_opt:
- no-new-privileges:true
read_only: true
mem_limit: 32m
cpus: "0.25"
pids_limit: 32
restart: "no"
opencode-agent:
# The pinned image, non-root UID 405, and OPENCODE_SERVER_* basic auth are
# exercised by the opt-in M4 container test when Docker is available.
image: ghcr.io/anomalyco/opencode:1.18.35@sha256:ae90ec960c871b0ea6df9abc1c2ec1e6b36464e8eca9b0e71e1ed3b553f47aa2
command:
- serve
- --hostname
- 0.0.0.0
- --port
- "4096"
environment:
# The frozen deny-by-default config (M0.3 fixture) is canonical in
# deploy/ and mounted read-only from /etc/termflow (see volumes); not
# overridable at runtime. The startup drift guard fails closed on any
# tool/config divergence.
OPENCODE_CONFIG: /etc/termflow/opencode-config.yaml
# HTTP Basic auth for the internal B-to-OpenCode connection (official
# OPENCODE_SERVER_* variables, verified live in M4 exit); values must
# come from deployment secrets, never a committed file.
OPENCODE_SERVER_USERNAME: ${OPENCODE_SERVER_USERNAME:?set OPENCODE_SERVER_USERNAME}
OPENCODE_SERVER_PASSWORD: ${OPENCODE_SERVER_PASSWORD:?set OPENCODE_SERVER_PASSWORD}
HOME: /home/opencode
# Keep persistent OpenCode data outside HOME so Docker never creates
# root-owned nested mountpoint parents inside the non-root HOME tmpfs.
XDG_DATA_HOME: /data
# The TermFlow MCP server entry is interpolated from these values by
# OpenCode's native {env:} config substitution (spike-verified on the
# pinned image). The token is a B-issued AgentToken (raw shown once)
# bound to the binding's runtime epoch; rotate it with the binding.
# An empty token keeps the stack bootable while MCP stays unreachable.
TERMFLOW_AGENT_MCP_URL: http://control-plane:8000/api/v1/agent/mcp
TERMFLOW_AGENT_MCP_TOKEN: ${OPENCODE_AGENT_MCP_TOKEN:?set OPENCODE_AGENT_MCP_TOKEN}
depends_on:
opencode-init:
condition: service_completed_successfully
volumes:
# Dedicated data volume for backend conversation persistence (plan §16);
# no docker socket, no A directories, no TermFlow credentials, and no
# arbitrary host workspaces are ever mounted.
- opencode-data:/data
# The frozen M0.3 config fixture (canonical in deploy/) is the only
# host file mounted, read-only.
- type: bind
source: ./opencode-config.yaml
target: /etc/termflow/opencode-config.yaml
read_only: true
bind:
create_host_path: false
expose:
- "4096"
networks:
# Internal network only: never attached to the public A/C network.
# The live/release overlay adds a separate normal bridge for direct
# model-provider egress (see docs/security.md「运行时出网边界」).
- agent_internal
user: "405:100"
# The image's fixed `guest` UID 405 runs the server; /home/opencode is a
# tmpfs (read-only rootfs, so no other HOME can be created) and the data
# volume mounts underneath it.
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp:size=64m,mode=1777,noexec,nosuid
- /home/opencode:size=512m,noexec,nosuid,uid=405,gid=100,mode=0700
mem_limit: ${OPENCODE_MEM_LIMIT:-2g}
cpus: ${OPENCODE_CPUS:-2}
pids_limit: 512
ulimits:
nofile:
soft: 1024
hard: 1024
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
healthcheck:
# The image ships busybox wget (no node/curl); /global/health requires
# the OPENCODE_SERVER_* basic auth (verified live in M4 exit). Default
# Docker seccomp/AppArmor profiles apply (never "unconfined").
test:
- CMD-SHELL
- >-
auth="$$(printf '%s:%s' "$$OPENCODE_SERVER_USERNAME" "$$OPENCODE_SERVER_PASSWORD" | base64 | tr -d '\n')";
wget -Y off -q -O- --header "Authorization: Basic $$auth"
http://127.0.0.1:4096/global/health | grep -q healthy
interval: 10s
timeout: 3s
retries: 3
start_period: 15s
volumes:
termflow-data:
name: termflow-v020-local-data
termflow-totp-key:
name: termflow-v020-local-totp-key
opencode-data:
name: termflow-v020-local-opencode-data
networks:
# Host-facing bridge for B's explicit port publish and same-host A/C.
# Keep capability-specific isolation in the networks below instead.
default:
internal: false
agent_internal:
internal: true