Skip to content
Registry StackDocsv0.46.0

Review an application and send an approval notice

For the operator

View as Markdown

Run one Registry Coordinator workflow, reviewed-notice, from an application in Base Registry Engine (BReg), through your decision as reviewer in Registry Casework, to an approval email from Registry Messaging. Coordinator does the rest, even across a restart.

Outcome
One approved, unchanged application produces one finished run and one email in local Mailpit.
Time
About 20 minutes
Level
Local evaluation only
Prerequisites
Docker 24 or later with host networkingOpenSSLPython 3.11 or later and uvcurljqLinux amd64 or macOS arm64Verified native tools, a matching source archive, and the unified Python client

Prepare verified candidate or release assets

Section titled “Prepare verified candidate or release assets”

Verify the selected release’s manifest and checksums using the release operations procedure. For a local candidate, use the maintainer’s reviewed source and checksum-bound artifact handoff. The asset directory must contain its verified SHA256SUMS, the host’s unified wheel, and all six native assets named below. Do not use v0.43.0 downloads as substitutes for missing Coordinator assets.

Get GitHub’s source-code archive for that verified source tag or commit, or the maintainer’s checksum-bound git archive export of the same reviewed commit. These are ordinary Git source archives, not a separate published support asset. Extract it and run from its root. Rust is not required. Use a fresh working directory; the maintained macOS extractor refuses an existing destination.

Set REGISTRY_STACK_VERSION to the candidate or release version and REGISTRY_STACK_ASSETS to the absolute directory containing its verified local artifacts. The preparation checks the selected file checksums, renames raw Linux assets to executable tool names, and uses the existing strict macOS archive extractor while retaining its adjacent FIPS libraries.

Terminal window
: "${REGISTRY_STACK_VERSION:?set the verified candidate or release version}"
: "${REGISTRY_STACK_ASSETS:?set the absolute verified artifact directory}"
mkdir -p tutorial-work
python3 - "$REGISTRY_STACK_VERSION" "$REGISTRY_STACK_ASSETS" >tutorial-work/prepared-tools.json <<'PY'
import hashlib, json, platform, shutil, sys
from pathlib import Path
sys.path.insert(0, "release/scripts")
from macos_fips_packaging import extract_macos_binary_archive
version, assets = sys.argv[1], Path(sys.argv[2])
if not assets.is_absolute():
raise SystemExit("Use an absolute artifact directory.")
host = (platform.system(), platform.machine())
if host == ("Darwin", "arm64"):
target, suffix = "macos-arm64", "_arm64.whl"
elif host in (("Linux", "x86_64"), ("Linux", "amd64")):
target, suffix = "linux-amd64", "_x86_64.manylinux2014_x86_64.whl"
else:
raise SystemExit("This tutorial supports macOS arm64 and Linux amd64.")
tools = ("breg", "bregctl", "casework", "caseworkctl", "coordinator", "coordinatorctl")
checksums = {}
for line in (assets / "SHA256SUMS").read_text().splitlines():
if not line.strip():
continue
digest, name = line.split(maxsplit=1)
checksums[name.removeprefix("*")] = digest
wheels = [path for path in assets.glob(f"registry_stack_client-{version}-*.whl") if path.name.endswith(suffix)]
if len(wheels) != 1:
raise SystemExit("Supply exactly one verified host wheel.")
files = [assets / (f"{tool}-v{version}-{target}" + (".tar.gz" if target == "macos-arm64" else "")) for tool in tools]
for path in [*files, wheels[0]]:
if checksums.get(path.name) != hashlib.sha256(path.read_bytes()).hexdigest():
raise SystemExit(f"Checksum mismatch: {path.name}")
output = Path("tutorial-work/native-tools").resolve()
output.mkdir(mode=0o700)
for tool, path in zip(tools, files):
stem = f"{tool}-v{version}-{target}"
if target == "macos-arm64":
extracted = Path("tutorial-work/unpacked") / tool
extract_macos_binary_archive(path, extracted, stem)
source = extracted / stem
for library in extracted.glob("libaws_lc_fips_*.dylib"):
destination = output / library.name
if destination.exists() and destination.read_bytes() != library.read_bytes():
raise SystemExit("Conflicting FIPS library bytes; stop and inspect the candidate.")
if not destination.exists():
shutil.copy2(library, destination)
else:
source = path
shutil.copy2(source, output / tool)
(output / tool).chmod(0o755)
print(json.dumps({"tools": str(output), "clientWheel": str(wheels[0])}))
PY

For the tested candidate evaluation, set MESSAGING_IMAGE to the exact public Messaging v0.43.0 digest above, prefixed by ghcr.io/registrystack/messaging@. Future published journeys must use the combination recorded for their release, including the supported binary platform roster. The helper accepts the public Messaging repository only; it does not accept a private candidate-image reference.

Terminal window
: "${REGISTRY_STACK_VERSION:?set the verified release or candidate version}"
: "${COORDINATOR_TUTORIAL_BIN_DIR:=$(jq -r .tools tutorial-work/prepared-tools.json)}"
: "${REGISTRY_STACK_CLIENT:=$(jq -r .clientWheel tutorial-work/prepared-tools.json)}"
: "${MESSAGING_IMAGE:?set the published Messaging image digest}"
export COORDINATOR_TUTORIAL_BIN_DIR REGISTRY_STACK_CLIENT
for tool in breg bregctl casework caseworkctl coordinator coordinatorctl; do
test -x "$COORDINATOR_TUTORIAL_BIN_DIR/$tool"
done
export PATH="$COORDINATOR_TUTORIAL_BIN_DIR:$PATH"
docker pull --platform linux/amd64 "$MESSAGING_IMAGE"

The session’s --released-clients option reads BReg and admits the run through the installed unified Python client. It uses the active workflow revision and repeats one identical start key to confirm the same run identity. No Rust input helper or Rust toolchain is needed. The preparation selects the exact local wheel through REGISTRY_STACK_CLIENT. When that variable is unset, the start command instead selects the named published version. Keep this terminal open.

A workflow is a finite list of named steps. Ask coordinatorctl to explain this one:

Terminal window
coordinatorctl explain --project products/coordinator/examples/reviewed-notice

The output ends with the steps from start, among them:

verify-review-subject: choose by review-subject-route(): changed -> changed-before-review, current -> submit-review
changed-before-review: finish application-changed
submit-review: call review review-submit-review-request (casework mutation, recovery same-command) with review-request(), then await-review
await-review: wait for the result of submit-review, at most 1209600 seconds, then decide; on timeout withdraw-timeout; on cancel withdraw-operator

Each line is one step. A call reaches a product through a named connection, here review for Casework; a choose picks the next step; a finish ends with an outcome. await-review is a wait: it holds up to fourteen days for the review result. Later steps read the application again before calling Messaging.

The session script starts BReg, Casework, and Coordinator from the selected native directory, with Messaging, databases, and the Mailpit mail sink in Docker. It seeds one application for Amina Example and starts one run of the workflow for it with POST /v1/runs: one admitted execution that Coordinator keeps in its database. The registry project, review policy, and notice template it uses are in products/coordinator/scripts/real_service_fixtures.py. Docker host networking lets Messaging reach the owned loopback services. This is local evaluation configuration. Every service listens on loopback HTTP (tlsTermination: development-loopback), and Coordinator applies its schema as the database superuser and serves as a separate runtime role: a local setup only.

Terminal window
mkdir -p tutorial-work
uv run --quiet --project products/coordinator/examples/reviewed-notice --locked \
--with "${REGISTRY_STACK_CLIENT:-registry-stack-client==$REGISTRY_STACK_VERSION}" \
products/coordinator/examples/reviewed-notice/session.py start \
--released-clients --messaging-image "$MESSAGING_IMAGE" \
--bin-dir "$COORDINATOR_TUTORIAL_BIN_DIR" \
--session-dir tutorial-work/reviewed-notice > tutorial-work/session.json
jq '{status, workflow}' tutorial-work/session.json
{
"status": "ready",
"workflow": "reviewed-notice"
}

session.json holds service addresses and owner-only token file paths. Keep them in variables, and copy the Coordinator token out of its header file for coordinatorctl:

Terminal window
session=tutorial-work/session.json
run_id="$(jq -r .runId "$session")"
coordinator_url="$(jq -r .coordinatorApi "$session")"
casework_url="$(jq -r .caseworkApi "$session")"
mailpit_url="$(jq -r .mailpit "$session")"
staff_headers="$(jq -r .staffHeaderFile "$session")"
(umask 077 && sed -n 's/^Authorization: Bearer //p' \
"$(jq -r .coordinatorHeaderFile "$session")" > tutorial-work/coordinator.token)
Terminal window
coordinatorctl inspect --run "$run_id" \
--url "$coordinator_url" --token-file tutorial-work/coordinator.token

The run is at await-review, after Casework accepted the review request:

state: running
step: await-review
waits:
- acceptedStep: submit-review
completion: result

Nothing has gone to Messaging. Inspection never shows the application data or the commands sent.

As staff, find the open task in the applications queue and read the facts you decide on. The applicant’s email address stays in BReg.

Terminal window
caseworkctl api '/v1/review-tasks?queue=applications' \
--url "$casework_url" \
--header @"$staff_headers" \
--header 'Registry-Casework-Profile: staff' \
--output tutorial-work/tasks.json
task_id="$(jq -r '.items[] | select(.state == "open") | .taskId' tutorial-work/tasks.json)"
caseworkctl api "/v1/review-tasks/$task_id/context" \
--url "$casework_url" \
--header @"$staff_headers" \
--header 'Registry-Casework-Profile: staff' | jq .context.snapshot
{
"applicantName": "Amina Example",
"benefitProgram": "housing-support",
"requestedAmount": 1200
}

Claim the task at the revision you read:

Terminal window
revision="$(jq -r '.items[] | select(.state == "open") | .revision' tutorial-work/tasks.json)"
caseworkctl api "/v1/review-tasks/$task_id/claim" \
--url "$casework_url" \
--header @"$staff_headers" \
--header 'Registry-Casework-Profile: staff' \
--method POST \
--if-match "\"$revision\"" \
--idempotency-key "$(openssl rand -hex 16)" \
--output /dev/null

The command exits successfully when Casework returns the claimed task.

Before you decide, restart Coordinator. The run is durable state, not a running process:

Terminal window
python3 products/coordinator/examples/reviewed-notice/session.py restart-coordinator \
--session-dir tutorial-work/reviewed-notice > "$session"
coordinator_url="$(jq -r .coordinatorApi "$session")"
jq .coordinatorRestarts "$session"
1

Now approve at the task’s current revision, with the policy’s approved outcome:

Terminal window
caseworkctl api "/v1/review-tasks/$task_id" \
--url "$casework_url" \
--header @"$staff_headers" \
--header 'Registry-Casework-Profile: staff' \
--output tutorial-work/task.json
revision="$(jq -r .revision tutorial-work/task.json)"
cat > tutorial-work/decision.json <<'JSON'
{"decision":{"type":"approve","outcome":"approved"}}
JSON
caseworkctl api "/v1/review-tasks/$task_id/decisions" \
--url "$casework_url" \
--header @"$staff_headers" \
--header 'Registry-Casework-Profile: staff' \
--if-match "\"$revision\"" \
--idempotency-key "$(openssl rand -hex 16)" \
--input tutorial-work/decision.json \
--output /dev/null

The command exits successfully when Casework records the decision and answers 204.

If a request answers 401, the local token expired. Refresh the staff token, and the Coordinator token for coordinatorctl, then repeat:

Terminal window
owner="$(jq -r .ownerProject "$session")"
bregctl dev token staff "$owner"
bregctl dev token coordinator-producer "$owner"
(umask 077 && sed -n 's/^Authorization: Bearer //p' \
"$(jq -r .coordinatorHeaderFile "$session")" > tutorial-work/coordinator.token)

Casework tells the restarted Coordinator a result is ready. The workflow reads the application again, checks it is unchanged and still allows a notice, and asks Messaging to send to the address on that record. Check the run:

Terminal window
coordinatorctl status --run "$run_id" \
--url "$coordinator_url" --token-file tutorial-work/coordinator.token
outcome: notice-accepted
output:
messageId: <message-id>
resultId: <result-id>
state: finished

If it is still running, repeat it after a few seconds. Then look at the local mail server:

Terminal window
curl -fsS "$mailpit_url/api/v1/messages" \
| jq '{count: (.messages | length), to: .messages[0].To[0].Address, subject: .messages[0].Subject}'
{
"count": 1,
"to": "amina@example.invalid",
"subject": "Application approved"
}

One email reached Amina’s address on record. A changed application, a withdrawn noticeAllowed, or a rejection would each have finished without calling Messaging.

The script removes only the containers and sessions it created:

Terminal window
python3 products/coordinator/examples/reviewed-notice/session.py stop \
--session-dir tutorial-work/reviewed-notice

How Registry Coordinator works explains the run, its wait, who owned each effect, and how an operator recovers a run.