From 3a7e5b9e2ed5ca9f90eb4192ec5f2b6186e24f35 Mon Sep 17 00:00:00 2001 From: itswl Date: Mon, 26 Oct 2026 17:51:04 +0800 Subject: [PATCH 1/2] =?UTF-8?q?release:=200.8.0=20=E2=80=94=20the=20quicks?= =?UTF-8?q?tart=20points=20at=20images=20whose=20judge=20takes=20a=20ceili?= =?UTF-8?q?ng,=20and=20both=20sides=20of=20the=20action=20door=20carry=20i?= =?UTF-8?q?ts=20secret?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The six image lines move from 0.7.1 to 0.8.0, pinned by the OCI index digests GHCR serves for the tags, read anonymously after the release run for the tag finished green on every job; :latest carries the same four. 0.8.0's investigator answers an unsigned approve with 503, and the pipe's baked config signs the press it forwards to the action door with HOOKPROBE_ACTION_SECRET. The from-source compose got the variable on both sides on 10-26; the quickstart did not, and the test pinning the pair read only docker-compose.yml. Pinned as it stood, the demo drew the approve button and the investigator refused both presses (checked: two 503s, the procedure stayed proposed). Both services now carry ${HOOKPROBE_ACTION_SECRET:-demo-card-secret}, and a new test parses both demo compose files service by service and requires the pipe's signing value to equal the investigator's; it fails with either side removed from the quickstart. Run the way a stranger does, from an empty directory: pulled by digest, boards up, the demo's nine steps with the press executed, down -v. The judge loads its in-flight cap at 8 and its budget off. --- docker-compose.quickstart.yml | 27 +++++++++++++++------- hookrelay/tests/test_stack_compose.py | 32 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/docker-compose.quickstart.yml b/docker-compose.quickstart.yml index 75a1675..a2f1855 100644 --- a/docker-compose.quickstart.yml +++ b/docker-compose.quickstart.yml @@ -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: @@ -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 @@ -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: @@ -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"] @@ -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: @@ -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"] @@ -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 @@ -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} diff --git a/hookrelay/tests/test_stack_compose.py b/hookrelay/tests/test_stack_compose.py index 52ec5d4..1f9a291 100644 --- a/hookrelay/tests/test_stack_compose.py +++ b/hookrelay/tests/test_stack_compose.py @@ -5,6 +5,10 @@ `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 @@ -12,7 +16,11 @@ 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" @@ -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" + ) From 75738e867a358c18f436e004357e1cad30379f68 Mon Sep 17 00:00:00 2001 From: itswl Date: Mon, 26 Oct 2026 17:51:04 +0800 Subject: [PATCH 2/2] demo: an accepted press that never executes fails the tour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the action door unsigned on one side, 0.8.0's investigator refused both presses with a 503, the procedure stayed proposed, and demo.sh still ended 0 — a release check reading the exit code would have passed a quickstart whose approve never ran. When a press was accepted and the procedure is not executed, the demo says so with the log line to read and exits 1 after step 9, so the audit record still prints. Nothing approved, as on a second run inside the cooldown, stays a normal ending. Proven both ways on the 0.8.0 images: exit 0 with the secret, exit 1 without. --- scripts/demo.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scripts/demo.sh b/scripts/demo.sh index 355009a..3aacb5c 100755 --- a/scripts/demo.sh +++ b/scripts/demo.sh @@ -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 @@ -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'