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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions docker-compose.quickstart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,16 @@
# 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. This file needs all of it, so it cannot point at an
# older tag.
# 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 it, so it cannot point at an older tag.

name: hookstack

services:
hookrelay:
image: ghcr.io/itswl/hookrelay:0.7.1@sha256:5e3e20c37905d0bf82e4f6086b8eb5d68677461919c78a683028b02ee9861a53
image: ghcr.io/itswl/hookrelay:0.8.0@sha256:934d9353508d77033e7d9683f03c5da76adbec5ff155bbdfd6f097ced639e2ff
container_name: hs-hookrelay
restart: unless-stopped
environment:
Expand All @@ -68,6 +70,12 @@ services:
# 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
Expand All @@ -91,7 +99,7 @@ services:
- sink

hookjudge:
image: ghcr.io/itswl/hookjudge:0.7.1@sha256:d9f617024cbb4b2baee2e318a0ba87ca5d8436163fea4ec450df69f4733927b1
image: ghcr.io/itswl/hookjudge:0.8.0@sha256:28f45f38a045da139cf65d970e7b7a9c8efe835dcd327c2afd529bc4b4a7b5da
container_name: hs-hookjudge
restart: unless-stopped
environment:
Expand Down Expand Up @@ -124,7 +132,7 @@ services:
# 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.7.1@sha256:5e3e20c37905d0bf82e4f6086b8eb5d68677461919c78a683028b02ee9861a53
image: ghcr.io/itswl/hookrelay:0.8.0@sha256:934d9353508d77033e7d9683f03c5da76adbec5ff155bbdfd6f097ced639e2ff
container_name: hs-sink
restart: unless-stopped
command: ["python", "-u", "/app/examples/sink.py"]
Expand All @@ -137,7 +145,7 @@ services:
# 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.7.1@sha256:a2cb299043a983a59de1f60fb8fb20b2c1769255c9295091b0a555b4505f281f
image: ghcr.io/itswl/lark-bridge:0.8.0@sha256:b1a06bd785ed4d0de99d8dbf49d01b2f258ba63267a2f452b1182d559d385b1d
container_name: hs-lark-bridge
restart: unless-stopped
environment:
Expand All @@ -155,7 +163,7 @@ services:
# 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.7.1@sha256:d9f617024cbb4b2baee2e318a0ba87ca5d8436163fea4ec450df69f4733927b1
image: ghcr.io/itswl/hookjudge:0.8.0@sha256:28f45f38a045da139cf65d970e7b7a9c8efe835dcd327c2afd529bc4b4a7b5da
container_name: hs-stub-ai
restart: unless-stopped
command: ["python", "-u", "/app/examples/stub_ai.py"]
Expand All @@ -167,7 +175,7 @@ services:
# 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.7.1@sha256:9a4f7d26c0457df35f643cb083f325dd1cb5e78ad6035f29b6045fc5ced23ef0
image: ghcr.io/itswl/hookprobe:0.8.0@sha256:f49d4fc36cf18c31456403076c203c54e7d12cee93fac130019dda8c10614920
container_name: hs-hookprobe
restart: unless-stopped
# The agent executes arbitrary bash, so the container is bounded even in a
Expand All @@ -180,6 +188,9 @@ services:
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}
Expand Down
32 changes: 32 additions & 0 deletions hookrelay/tests/test_stack_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@
`HOOKPROBE_ACTION_SECRET` at all: the pipe drew a button and the investigator
answered 503. A service that looks healthy while its buttons cannot land is the
shape this test exists to catch, without needing Docker.

The quickstart compose carries the same pair since 0.8.0: pinned to images whose
investigator refuses an unsigned approve, it would have drawn the button and
stopped the demo at the press, and nothing here read that file.
"""

from __future__ import annotations

import re
from pathlib import Path

import pytest
import yaml

COMPOSE = Path(__file__).resolve().parents[2] / "docker-compose.yml"
QUICKSTART = Path(__file__).resolve().parents[2] / "docker-compose.quickstart.yml"
STACK_CONFIG = Path(__file__).resolve().parents[2] / "hookrelay" / "examples" / "stack.yaml"


Expand All @@ -38,3 +46,27 @@ def test_the_pipe_signs_the_forwarded_press_with_the_door_it_verifies_with() ->
assert "secret: ${HOOKPROBE_ACTION_SECRET}" in block, (
"to-probe-action is unsigned while the action door requires a signature; every approve press is a 401"
)


def _resolved(value: object) -> str:
"""A compose value as a run from an empty directory resolves it: ${VAR:-default} is the default."""
text = str(value or "")
match = re.fullmatch(r"\$\{(\w+):-([^}]*)\}", text)
return match.group(2) if match else text


def _environment(compose: Path, service: str) -> dict[str, str]:
env = yaml.safe_load(compose.read_text(encoding="utf-8"))["services"][service].get("environment") or {}
if isinstance(env, list):
env = dict(item.split("=", 1) for item in env)
return {str(key): _resolved(value) for key, value in env.items()}


@pytest.mark.parametrize("compose", [COMPOSE, QUICKSTART], ids=lambda path: path.name)
def test_the_pipe_signs_the_forwarded_press_with_the_value_the_investigator_verifies(compose: Path) -> None:
signed = _environment(compose, "hookrelay").get("HOOKPROBE_ACTION_SECRET", "")
verified = _environment(compose, "hookprobe").get("HOOKPROBE_ACTION_SECRET", "")
assert signed and signed == verified, (
f"{compose.name}: the pipe forwards approve signed with {signed!r} and the investigator verifies with "
f"{verified!r}; an unsigned or mismatched press is refused while the button still renders"
)
15 changes: 15 additions & 0 deletions scripts/demo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,16 @@ for res in row.get("results", []):
first = (res.get("output") or "").strip().splitlines()[:1]
print(f" exit {res['exit']:>2} {res['command']:<22} {first[0][:66] if first else ''}")
PY
# An accepted press that never executes is the loop not closing, and the tour
# must say so rather than finish as if it had: with the action door unsigned on
# one side, 0.8.0's investigator refused both presses with a 503 and this script
# still ended 0. Nothing approved (the cooldown on a second run) is not a failure.
press_failed=""
if [ "$approved" = "1" ] && [ "$pstatus" != "executed" ]; then
press_failed="the press was accepted but the procedure is '${pstatus:-unknown}', not executed"
echo "!! $press_failed. The investigator's log says why: docker logs hs-hookprobe 2>&1 | grep hooks/action"
echo " (a 503 there means the pipe and the investigator do not share HOOKPROBE_ACTION_SECRET)."
fi

say "9. the condition ends: the recovery verifies the work, and the pipe's audit record holds the chain"
post '{"title":"Disk /data at 92% on db-1","message":"back to 15% after the slot was dropped","state":"resolved","env":"prod"}' >/dev/null
Expand Down Expand Up @@ -225,6 +235,11 @@ print(f"the page: {os.environ['RELAY_URL']}/audit/{rec.get('operation')}")
print(f"the alert's story, top to bottom: {os.environ['RELAY_URL']}/#/alert/{rec.get('operation')}")
PY

if [ -n "$press_failed" ]; then
printf '\nThe loop did not close: %s.\n' "$press_failed" >&2
exit 1
fi

printf '\nThis was a rehearsal: no model was called, and nothing ran except the two observations a person approved.\n'
printf 'The boards: %s (pipe) %s (judge) %s/ui (investigator)\n' "$base" "$judge" "$probe"
printf 'What the operator would have received: docker compose logs -f sink\n'
Expand Down
Loading