Repository navigation
Expand file tree
/
Copy pathdocker-compose.quickstart.yml
More file actions
232 lines (224 loc) · 12.6 KB
/
Copy pathdocker-compose.quickstart.yml
File metadata and controls
232 lines (224 loc) · 12.6 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
# The family, from published images. No checkout, no build.
#
# curl -fsSLO https://raw.githubusercontent.com/itswl/hookstack/main/docker-compose.quickstart.yml
# docker compose -f docker-compose.quickstart.yml up -d
# bash <(curl -fsSL https://raw.githubusercontent.com/itswl/hookstack/main/scripts/demo.sh)
#
# Everything below runs on ghcr.io/itswl images and needs no key and no bill:
# the model is a canned stub that ships inside the judge's image, and the
# downstream is a readable sink that ships inside the pipe's. That is why this
# file has no `build:` and no bind mount — an image that cannot run its own
# demo makes every trial start by rebuilding what was already published.
#
# The investigator is up from the start, with no key: it runs the `replay`
# rehearsal — a recorded investigation played back through the real read-only
# gate and the real audit, priced at nothing — so the first ten minutes show the
# whole loop: a report, a card whose approve button executes an allowlisted
# observation, and the audit page of the chain. Every number in a rehearsal's
# report was written in advance; its last section says so. To investigate for
# real, hand the node a model:
#
# HOOKPROBE_RUNTIME=claude HOOKPROBE_MODEL=claude-opus-5 ANTHROPIC_API_KEY=sk-ant-... \
# docker compose -f docker-compose.quickstart.yml up -d
#
# Pinned by tag on purpose. `latest` moves under you between two `up`s and
# turns "it worked yesterday" into a question nobody can answer.
#
# 0.3.0 is the release that closed the gap this file used to describe. Up to
# 0.2.0 the pipe rendered the Feishu card itself; the stack.yaml baked into the
# pipe's image now sends a neutral card model to a bridge, so this file grew the
# two things the old comment said the next release would need: a `lark-bridge`
# service in webhook mode (docs/bridge-protocol.md — the pipe models, the
# sidecar dresses, the sink receives) and HOOKRELAY_PLUGINS so the DingTalk
# dialect resolves. Neither needs a Lark account: the bridge posts a rendered
# card at the sink and `docker compose logs -f sink` is what an operator would
# have received. 0.4.0 was the release whose pipe config offers the approve and
# follow-up buttons and whose investigator carries the rehearsal; 0.5.0 was the
# first whose images carry the boards the pictures show — one attention entry
# per alert, the relay inbox, dark and light, two languages; 0.6.0 is the first
# whose boards fit the phone the pictures now show, and whose approve press
# names the version it read; 0.7.0 is the first whose bridge refuses to start
# without its secret (set below), whose pipe answers a replayed request as a
# duplicate, and whose investigator measures its posture with the tools its
# image carries; 0.7.1 is the first whose images ship neither a CA bundle that
# trusts compromised intermediates nor a trace-header parser one request could
# keep busy for seconds; 0.8.0 is the first whose judge takes the in-flight cap
# and the opt-in dollar ceiling set below, and whose investigator refuses an
# unsigned approve, which is why the pipe and the investigator both carry the
# action secret. This file needs all of that, so it cannot point at anything
# older than 0.8.0; it points at 0.11.0, whose boards are laid out as apps on a
# phone, because a stranger should start on the newest.
name: hookstack
services:
hookrelay:
image: ghcr.io/itswl/hookrelay:0.11.0@sha256:540e9cb2938ce2bfc7596ab5b8f7c674de53c12a9483797106b10bf60e037fc9
container_name: hs-hookrelay
restart: unless-stopped
environment:
HOOKRELAY_HOST: 0.0.0.0
HOOKRELAY_PORT: 8100
HOOKRELAY_DB: /data/hookrelay.db
# The demo wiring, from inside the image.
HOOKRELAY_CONFIG: /app/examples/stack.yaml
# `ops-dingtalk` in that config is a plugin channel type, not a built-in.
# Without this the config loads, the channel does not resolve, and one of
# the two dialects the demo exists to show is silently missing.
HOOKRELAY_PLUGINS: /app/examples/plugins
# The baked config declares card buttons (`card_actions`), and a button is
# a signed token: without a key the pipe logs an error at startup and
# every button renders and does nothing. Published since 0.1.0 that way —
# found by actually running this file rather than reading it. Same demo
# values as docker-compose.yml, which has always set both.
HOOKRELAY_ACTION_SECRET: demo-card-secret
# The baked config signs the press it forwards to the investigator's action
# door with this (`to-probe-action`); the investigator below verifies with
# the same variable. Since 0.8.0 an unsigned approve answers 503 while the
# button still renders, so one side without it is a demo that stops at
# the press.
HOOKPROBE_ACTION_SECRET: ${HOOKPROBE_ACTION_SECRET:-demo-card-secret}
HOOKRELAY_PUBLIC_URL: ${HOOKRELAY_PUBLIC_URL:-http://127.0.0.1:8100}
HOOKRELAY_WORKER_INTERVAL: "0.5"
# A captured request replayed inside the signature's freshness window is
# answered as a duplicate instead of buying a second verdict. 0 leaves the
# window bounded rather than closed (security.verify_signature).
HOOKRELAY_SIGNATURE_REPLAY_SECONDS: ${HOOKRELAY_SIGNATURE_REPLAY_SECONDS:-300}
# Where the escalation route delivers: the investigator's door. Point it
# at the sink's /probe-standin instead to see the escalation shape alone.
HOOKPROBE_EVENT_URL: ${HOOKPROBE_EVENT_URL:-http://hookprobe:8088/hooks/event}
# Where a card press on an investigator's report goes back to: the
# approve, follow-up and remember buttons forward here. The retired
# production pipe never named this channel and never enabled those
# buttons, so no report card there ever carried one — see
# .agents/notes/proposed/2026-09-28-pilot-zero-read-back-and-the-order-of-the-next-ninety-days.md.
HOOKPROBE_ACTION_URL: ${HOOKPROBE_ACTION_URL:-http://hookprobe:8088/hooks/action}
ports:
- "127.0.0.1:8100:8100"
volumes:
- relay-data:/data
depends_on:
- sink
hookjudge:
image: ghcr.io/itswl/hookjudge:0.11.0@sha256:37022c233bc9c9538adac7c2af70a8445facc7f14c7b560156184d554a961e9c
container_name: hs-hookjudge
restart: unless-stopped
environment:
HOOKJUDGE_HOST: 0.0.0.0
HOOKJUDGE_PORT: 8200
HOOKJUDGE_DB: /data/hookjudge.db
# One outbound address, and it is the pipe. Fan-out is not its business.
HOOKJUDGE_RETURN_URL: http://hookrelay:8100/hook/judge-notify
HOOKJUDGE_WORKER_INTERVAL: "0.5"
HOOKJUDGE_MAX_INFLIGHT: ${HOOKJUDGE_MAX_INFLIGHT:-8}
HOOKJUDGE_BUDGET_USD: ${HOOKJUDGE_BUDGET_USD:-0}
HOOKJUDGE_BUDGET_WINDOW_HOURS: ${HOOKJUDGE_BUDGET_WINDOW_HOURS:-24}
HOOKJUDGE_REUSE_WINDOW_SECONDS: "3600"
# Defaults to the stub beside it. Leaving these empty instead would send
# every event to the rule floor, and `reuse` — which only ever follows an
# ai verdict — would never happen, hiding half the cost policy.
HOOKJUDGE_AI_BASE_URL: ${HOOKJUDGE_AI_BASE_URL:-http://stub-ai:8300/v1}
HOOKJUDGE_AI_API_KEY: ${HOOKJUDGE_AI_API_KEY:-stub-key}
HOOKJUDGE_AI_MODEL: ${HOOKJUDGE_AI_MODEL:-stub-4o-mini}
HOOKJUDGE_AI_PRICE_IN_PER_1K: ${HOOKJUDGE_AI_PRICE_IN_PER_1K:-0.00015}
HOOKJUDGE_AI_PRICE_OUT_PER_1K: ${HOOKJUDGE_AI_PRICE_OUT_PER_1K:-0.0006}
ports:
- "127.0.0.1:8200:8200"
volumes:
- judge-data:/data
depends_on:
- stub-ai
# The last link, otherwise invisible: `docker compose logs -f sink` is what an
# operator would actually have received. It runs from the pipe's own image
# because that image now carries it — one fewer thing to pull, and no mount.
sink:
image: ghcr.io/itswl/hookrelay:0.11.0@sha256:540e9cb2938ce2bfc7596ab5b8f7c674de53c12a9483797106b10bf60e037fc9
container_name: hs-sink
restart: unless-stopped
command: ["python", "-u", "/app/examples/sink.py"]
healthcheck:
disable: true
# The pipe stopped knowing any chat platform's card schema in 0.3.0: it sends
# a neutral card model and this sidecar renders it. In webhook mode — no Lark
# app, no credential — it dresses the card and posts it at the sink, which is
# exactly the hop a real deployment makes to a custom bot's incoming URL.
# `ops-feishu` in the baked config is `type: bridge` and points here.
lark-bridge:
image: ghcr.io/itswl/lark-bridge:0.11.0@sha256:495f63da3fdaeffebc844be464f527cf8a7c5bceb0f2ccc6ebf291bb14de7610
container_name: hs-lark-bridge
restart: unless-stopped
environment:
LARK_WEBHOOK_URL: ${LARK_WEBHOOK_URL:-http://sink:9000/feishu}
LARK_WEBHOOK_SECRET: ${LARK_WEBHOOK_SECRET:-}
# The value `ops-feishu`'s `secret:` carries in the baked stack.yaml. A
# bridge that accepted an unsigned card would take one from anything that
# could reach the port.
BRIDGE_INBOUND_SECRET: demo-bridge-secret
depends_on:
- sink
# Canned verdicts over a real OpenAI-compatible endpoint, so the client, the
# JSON-in-content convention and the token accounting are all exercised. It is
# not a model: it reports itself as `stub-4o-mini`. Point the judge's AI vars
# at a real provider and this stops being used.
stub-ai:
image: ghcr.io/itswl/hookjudge:0.11.0@sha256:37022c233bc9c9538adac7c2af70a8445facc7f14c7b560156184d554a961e9c
container_name: hs-stub-ai
restart: unless-stopped
command: ["python", "-u", "/app/examples/stub_ai.py"]
healthcheck:
disable: true
# Up by default, on the rehearsal (see the top of this file). High/critical
# alerts get a deep read-only investigation whose report returns through the
# pipe to the same channels; the disk alert in demo.sh also proposes a
# two-step procedure the approve button runs against the allowlist below.
hookprobe:
image: ghcr.io/itswl/hookprobe:0.11.0@sha256:9651427590e5f2f64fa3c44a6baa940ccf98ef8bc75b36a59fcc16fb4d8119b1
container_name: hs-hookprobe
restart: unless-stopped
# The agent executes arbitrary bash, so the container is bounded even in a
# demo. Same numbers as the production compose.
mem_limit: ${HOOKPROBE_MEM_LIMIT:-2g}
pids_limit: 512
cap_drop: [ALL]
security_opt:
- no-new-privileges:true
environment:
HOOKPROBE_TOKEN: ${HOOKPROBE_TOKEN:-quickstart-token}
HOOKPROBE_RETURN_URL: http://hookrelay:8100/hook/probe-notify
# Verifies the press the pipe forwards, with the same variable as the pipe
# above: an unsigned approve is indistinguishable from a run shaping one.
HOOKPROBE_ACTION_SECRET: ${HOOKPROBE_ACTION_SECRET:-demo-card-secret}
# The rehearsal, and the name its runs carry. Set all three of
# HOOKPROBE_RUNTIME, HOOKPROBE_MODEL and a key to investigate for real.
HOOKPROBE_RUNTIME: ${HOOKPROBE_RUNTIME:-replay}
HOOKPROBE_MODEL: ${HOOKPROBE_MODEL:-rehearsal}
# The console as YOUR browser reaches it, so a card can link to the run
# behind it. Empty on the retired production deployment, which is why 3
# of its 741 cards carried any link at all.
HOOKPROBE_PUBLIC_URL: ${HOOKPROBE_PUBLIC_URL:-http://127.0.0.1:8088}
HOOKPROBE_RELAY_UI_URL: ${HOOKPROBE_RELAY_UI_URL:-http://127.0.0.1:8100}
# The allowlist the approve press is checked against. This one ships with
# the rehearsal and admits exactly the two observations its disk script
# proposes; a real deployment writes its own file and points here instead.
HOOKPROBE_REMEDIATION_ALLOWLIST: ${HOOKPROBE_REMEDIATION_ALLOWLIST:-/app/hookprobe/replays/rehearsal-allowlist}
# Where this node says so when its own hourly selftest fails. Empty here
# by default — a quickstart has nobody to page — but carried, because a
# probe node that cannot report its own failure fails silently, and the
# knob check requires every probe node to have it for that reason.
HOOKPROBE_ALARM_URL: ${HOOKPROBE_ALARM_URL:-}
HOOKPROBE_ALARM_MIN_INTERVAL_SECONDS: ${HOOKPROBE_ALARM_MIN_INTERVAL_SECONDS:-}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
# Any Anthropic-dialect provider: base URL, token and model aliases move
# together. See the provider notes in hookprobe/README.md.
ANTHROPIC_BASE_URL: ${ANTHROPIC_BASE_URL:-}
ANTHROPIC_AUTH_TOKEN: ${ANTHROPIC_AUTH_TOKEN:-}
ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-}
# A demo should not be able to run up a bill unattended.
HOOKPROBE_BUDGET_USD: ${HOOKPROBE_BUDGET_USD:-5}
HOOKPROBE_BUDGET_WINDOW_HOURS: ${HOOKPROBE_BUDGET_WINDOW_HOURS:-24}
ports:
- "127.0.0.1:8088:8088"
volumes:
- probe-data:/data
volumes:
relay-data:
judge-data:
probe-data: