#!/usr/bin/env bash # seed_consent_version — hash a consent template + add to allowlist. # # Specification: docs/PHASE_1_6_BIPA_GATES.md (consent_versions # allowlist) + docs/policies/consent/biometric_consent_template_v1.md # # Why this exists: when counsel returns a signed consent template, # the operator needs to flip the gateway from permissive mode # (any non-empty hash accepted, v1 compat) to strict mode (only # accepted hashes). That requires: # 1. Compute SHA-256 of the binding text (the markdown source) # 2. Atomically write it into /etc/lakehouse/consent_versions.json # 3. Restart the gateway so the in-memory allowlist re-reads # # This script does all three. Idempotent: re-running with the same # template is a no-op (the hash is already in the allowlist). # # Usage: # seed_consent_version.sh [--no-restart] [--label "v1 signed 2026-06-15 by Counsel-Name"] # # Environment: # ALLOWLIST_FILE — default /etc/lakehouse/consent_versions.json # GATEWAY_SERVICE — default lakehouse.service (used for restart) # # Exit codes: # 0 — hash seeded; allowlist now contains it (whether new or already present) # 1 — file present but allowlist write/restart failed # 2 — script error (missing tools, missing input, bad path) set -uo pipefail if [ "$#" -lt 1 ]; then cat >&2 <<'USAGE' usage: seed_consent_version.sh [--no-restart] [--label "free-text"] Computes SHA-256 of the markdown file and merges it into the gateway's consent_versions allowlist. By default also restarts the gateway so the new entry takes effect immediately. USAGE exit 2 fi TEMPLATE_PATH="$1" shift DO_RESTART=1 LABEL="" while [ "$#" -gt 0 ]; do case "$1" in --no-restart) DO_RESTART=0; shift ;; --label) LABEL="$2"; shift 2 ;; -h|--help) sed -n '2,20p' "$0" | sed 's/^# \?//'; exit 0 ;; *) echo "[seed] unknown flag: $1" >&2; exit 2 ;; esac done ALLOWLIST_FILE="${ALLOWLIST_FILE:-/etc/lakehouse/consent_versions.json}" GATEWAY_SERVICE="${GATEWAY_SERVICE:-lakehouse.service}" for cmd in sha256sum jq install; do if ! command -v "$cmd" >/dev/null 2>&1; then echo "[seed] FAIL: required tool '$cmd' not found in PATH" >&2 exit 2 fi done if [ ! -r "$TEMPLATE_PATH" ]; then echo "[seed] FAIL: cannot read $TEMPLATE_PATH" >&2 exit 2 fi HASH=$(sha256sum "$TEMPLATE_PATH" | awk '{print $1}') SIZE=$(stat -c '%s' "$TEMPLATE_PATH") ABS=$(readlink -f "$TEMPLATE_PATH") NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ) [ -n "$LABEL" ] || LABEL="auto-seeded by seed_consent_version.sh from $ABS" echo "[seed] template: $ABS" echo "[seed] size: $SIZE bytes" echo "[seed] sha256: $HASH" echo "[seed] allowlist: $ALLOWLIST_FILE" echo "[seed] label: $LABEL" echo "[seed] timestamp: $NOW" # Build the new allowlist contents. Two cases: # (a) file absent → create with single entry + audit metadata # (b) file present → parse, check if hash already exists, add if not TMP_OUT=$(mktemp) trap 'rm -f "$TMP_OUT"' EXIT if [ ! -e "$ALLOWLIST_FILE" ]; then echo "[seed] allowlist file absent — creating fresh" jq -n --arg hash "$HASH" --arg label "$LABEL" --arg ts "$NOW" --arg src "$ABS" ' { "_doc": "docs/PHASE_1_6_BIPA_GATES.md consent_versions allowlist. Strict-mode gate for /biometric/subject/{id}/consent. Hashes are SHA-256 of the binding consent template markdown.", versions: [$hash], _meta: { seeded_at: [{ ts: $ts, hash: $hash, label: $label, source_path: $src }] } } ' > "$TMP_OUT" elif [ ! -r "$ALLOWLIST_FILE" ]; then echo "[seed] FAIL: $ALLOWLIST_FILE exists but is not readable" >&2 exit 1 else if ! jq empty "$ALLOWLIST_FILE" 2>/dev/null; then echo "[seed] FAIL: $ALLOWLIST_FILE is not valid JSON" >&2 exit 1 fi ALREADY=$(jq -r --arg h "$HASH" '(.versions // []) | index($h) | tostring' "$ALLOWLIST_FILE") if [ "$ALREADY" != "null" ]; then echo "[seed] hash already in allowlist (idempotent no-op for versions array)" # Still bump the metadata so the audit trail captures the re-attempt. jq --arg hash "$HASH" --arg label "$LABEL" --arg ts "$NOW" --arg src "$ABS" ' .versions = (.versions // []) | ._meta = ((._meta // {}) | .seeded_at = ((.seeded_at // []) + [{ ts: $ts, hash: $hash, label: ($label + " [reseed]"), source_path: $src }])) ' "$ALLOWLIST_FILE" > "$TMP_OUT" else echo "[seed] adding hash to existing allowlist (now $(($(jq -r '(.versions // []) | length' "$ALLOWLIST_FILE") + 1)) entries)" jq --arg hash "$HASH" --arg label "$LABEL" --arg ts "$NOW" --arg src "$ABS" ' .versions = ((.versions // []) + [$hash]) | ._meta = ((._meta // {}) | .seeded_at = ((.seeded_at // []) + [{ ts: $ts, hash: $hash, label: $label, source_path: $src }])) ' "$ALLOWLIST_FILE" > "$TMP_OUT" fi fi # Atomic move so a crashed write can't leave a half-file. PARENT=$(dirname "$ALLOWLIST_FILE") if [ ! -d "$PARENT" ]; then echo "[seed] FAIL: parent dir $PARENT does not exist (run as root after sudo mkdir -p)" >&2 exit 1 fi if ! sudo install -m 0644 -o root -g root "$TMP_OUT" "$ALLOWLIST_FILE"; then echo "[seed] FAIL: install to $ALLOWLIST_FILE failed (run with sudo?)" >&2 exit 1 fi echo "[seed] allowlist written to $ALLOWLIST_FILE" echo "[seed] current allowlist contents:" sudo cat "$ALLOWLIST_FILE" | jq '{ versions, latest_seed: ._meta.seeded_at[-1] }' # Restart unless --no-restart. if [ "$DO_RESTART" = "1" ]; then echo echo "[seed] restarting $GATEWAY_SERVICE so the new allowlist loads…" if ! sudo systemctl restart "$GATEWAY_SERVICE"; then echo "[seed] FAIL: systemctl restart $GATEWAY_SERVICE failed" >&2 exit 1 fi sleep 2 if ! curl -sf http://localhost:3100/biometric/health >/dev/null; then echo "[seed] WARN: /biometric/health not responding 200 after restart" >&2 else echo "[seed] gateway healthy post-restart. allowlist live." fi else echo "[seed] --no-restart set; gateway must be restarted manually for the change to load." fi echo echo "[seed] To verify strict mode is now active, probe with a known-bad hash:" echo echo ' TOKEN=$(cat /etc/lakehouse/legal_audit.token)' echo ' curl -sS -X POST http://localhost:3100/biometric/subject/WORKER-1/consent \' echo ' -H "X-Lakehouse-Legal-Token: $TOKEN" \' echo ' -H "Content-Type: application/json" \' echo " -d '{\"consent_version_hash\":\"deadbeef\",\"consent_collection_method\":\"electronic_signature\",\"operator_of_record\":\"smoke\"}' | jq" echo echo ' Expected: HTTP 400 + error="consent_version_unknown"'