#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" HTTP_HELPER="${ROOT_DIR}/build/amduatd_http_unix" USE_HTTP_HELPER=0 TMPDIR="${TMPDIR:-/tmp}" mkdir -p "${TMPDIR}" if ! command -v grep >/dev/null 2>&1; then echo "skip: grep not found" >&2 exit 77 fi if ! command -v awk >/dev/null 2>&1; then echo "skip: awk not found" >&2 exit 77 fi if command -v curl >/dev/null 2>&1; then if curl --help 2>/dev/null | grep -q -- '--unix-socket'; then USE_HTTP_HELPER=0 else USE_HTTP_HELPER=1 fi else USE_HTTP_HELPER=1 fi if [[ "${USE_HTTP_HELPER}" -eq 1 && ! -x "${HTTP_HELPER}" ]]; then echo "skip: curl lacks --unix-socket support and helper missing" >&2 exit 77 fi AMDUATD_BIN="${AMDUATD_BIN:-}" if [[ -z "${AMDUATD_BIN}" ]]; then for cand in \ "${ROOT_DIR}/build/amduatd" \ "${ROOT_DIR}/build-asan/amduatd"; do if [[ -x "${cand}" ]]; then AMDUATD_BIN="${cand}" break fi done fi ASL_BIN="${ASL_BIN:-}" if [[ -z "${ASL_BIN}" ]]; then for cand in \ "${ROOT_DIR}/build/vendor/amduat/amduat-asl" \ "${ROOT_DIR}/vendor/amduat/build/amduat-asl"; do if [[ -x "${cand}" ]]; then ASL_BIN="${cand}" break fi done fi if [[ ! -x "${AMDUATD_BIN}" || ! -x "${ASL_BIN}" ]]; then echo "missing binaries; build amduatd and amduat-asl first" >&2 exit 1 fi tmp_root="$(mktemp -d -p "${TMPDIR}" amduatd-fed-ingest-XXXXXX)" tmp_keep="${AMDUATD_FED_INGEST_KEEP_TMP:-0}" last_log="${TMPDIR}/amduatd-fed-ingest.last.log" root_store="${tmp_root}/store" root_ref="${tmp_root}/ref" sock="${tmp_root}/amduatd.sock" log="${tmp_root}/amduatd.log" space_a="alpha" space_b="beta" show_log() { if [[ -n "${log}" && -f "${log}" ]]; then echo "daemon log: ${log}" >&2 cat "${log}" >&2 else echo "daemon log missing: ${log}" >&2 fi if [[ -f "${last_log}" ]]; then echo "last log copy: ${last_log}" >&2 fi } die() { echo "$1" >&2 show_log exit 1 } cleanup() { if [[ -n "${pid:-}" ]]; then kill "${pid}" >/dev/null 2>&1 || true fi if [[ -f "${log}" ]]; then cp -f "${log}" "${last_log}" 2>/dev/null || true fi if [[ "${tmp_keep}" -eq 0 ]]; then rm -rf "${tmp_root}" else echo "kept tmpdir: ${tmp_root}" >&2 fi } trap cleanup EXIT mkdir -p "${root_store}" "${root_ref}" "${ASL_BIN}" index init --root "${root_store}" "${ASL_BIN}" index init --root "${root_ref}" "${AMDUATD_BIN}" --root "${root_store}" --sock "${sock}" \ --store-backend index --space "${space_a}" \ --fed-enable --fed-transport stub \ >"${log}" 2>&1 & pid=$! http_get() { local sock_path="$1" local path="$2" shift 2 if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then "${HTTP_HELPER}" --sock "${sock_path}" --method GET --path "${path}" "$@" else curl --silent --show-error --fail \ --unix-socket "${sock_path}" \ "$@" \ "http://localhost${path}" fi } http_get_allow() { local sock_path="$1" local path="$2" shift 2 if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then "${HTTP_HELPER}" --sock "${sock_path}" --method GET --path "${path}" \ --allow-status "$@" else curl --silent --show-error \ --unix-socket "${sock_path}" \ "$@" \ "http://localhost${path}" fi } http_post() { local sock_path="$1" local path="$2" local data="$3" shift 3 if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then "${HTTP_HELPER}" --sock "${sock_path}" --method POST --path "${path}" \ --data "${data}" \ "$@" else curl --silent --show-error --fail \ --unix-socket "${sock_path}" \ "$@" \ --data-binary "${data}" \ "http://localhost${path}" fi } http_post_allow() { local sock_path="$1" local path="$2" local data="$3" shift 3 if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then "${HTTP_HELPER}" --sock "${sock_path}" --method POST --path "${path}" \ --allow-status --data "${data}" \ "$@" else curl --silent --show-error \ --unix-socket "${sock_path}" \ "$@" \ --data-binary "${data}" \ "http://localhost${path}" fi } http_status() { local sock_path="$1" local method="$2" local path="$3" shift 3 if [[ "${USE_HTTP_HELPER}" -eq 1 ]]; then echo "" return 0 fi curl --silent --show-error --output /dev/null --write-out '%{http_code}' \ --unix-socket "${sock_path}" \ -X "${method}" \ "$@" \ "http://localhost${path}" } wait_for_ready() { local sock_path="$1" local pid_val="$2" local log_path="$3" local i for i in $(seq 1 100); do if ! kill -0 "${pid_val}" >/dev/null 2>&1; then if [[ -f "${log_path}" ]] && grep -q "bind: Operation not permitted" "${log_path}"; then echo "skip: bind not permitted for unix socket" >&2 exit 77 fi if [[ -f "${log_path}" ]]; then cat "${log_path}" >&2 fi return 1 fi if [[ -S "${sock_path}" ]]; then if http_get "${sock_path}" "/v1/meta" >/dev/null 2>&1; then return 0 fi fi sleep 0.1 done return 1 } if ! wait_for_ready "${sock}" "${pid}" "${log}"; then die "daemon not ready" fi payload="fed-ingest" ref="$( printf '%s' "${payload}" | "${ASL_BIN}" put --root "${root_ref}" \ --input - --input-format raw --ref-format hex \ | tail -n 1 | tr -d '\r\n' )" ingest_resp="$( http_post "${sock}" "/v1/fed/ingest?record_type=artifact&ref=${ref}" \ "${payload}" \ --header "Content-Type: application/octet-stream" \ --header "X-Amduat-Space: ${space_a}" )" || { die "ingest artifact failed" } status="$( printf '%s' "${ingest_resp}" \ | tr -d '\r\n' \ | awk 'match($0, /"status":"[^"]+"/) {print substr($0, RSTART+10, RLENGTH-11)}' )" applied="$( printf '%s' "${ingest_resp}" \ | tr -d '\r\n' \ | awk 'match($0, /"applied":[^,}]+/) {print substr($0, RSTART+10, RLENGTH-10)}' )" if [[ "${status}" != "ok" || "${applied}" != "true" ]]; then die "unexpected ingest response: ${ingest_resp}" fi fetched="$(http_get "${sock}" "/v1/artifacts/${ref}")" if [[ "${fetched}" != "${payload}" ]]; then die "artifact fetch mismatch" fi ingest_again="$( http_post "${sock}" "/v1/fed/ingest?record_type=artifact&ref=${ref}" \ "${payload}" \ --header "Content-Type: application/octet-stream" \ --header "X-Amduat-Space: ${space_a}" )" status="$( printf '%s' "${ingest_again}" \ | tr -d '\r\n' \ | awk 'match($0, /"status":"[^"]+"/) {print substr($0, RSTART+10, RLENGTH-11)}' )" applied="$( printf '%s' "${ingest_again}" \ | tr -d '\r\n' \ | awk 'match($0, /"applied":[^,}]+/) {print substr($0, RSTART+10, RLENGTH-10)}' )" if [[ "${status}" != "already_present" && "${applied}" != "false" ]]; then die "unexpected re-ingest response: ${ingest_again}" fi tombstone_resp="$( http_post "${sock}" "/v1/fed/ingest" \ "{\"record_type\":\"tombstone\",\"ref\":\"${ref}\"}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_a}" )" status="$( printf '%s' "${tombstone_resp}" \ | tr -d '\r\n' \ | awk 'match($0, /"status":"[^"]+"/) {print substr($0, RSTART+10, RLENGTH-11)}' )" if [[ "${status}" != "ok" ]]; then die "unexpected tombstone response: ${tombstone_resp}" fi http_get_allow "${sock}" "/v1/artifacts/${ref}" >/dev/null 2>&1 || true tombstone_again="$( http_post "${sock}" "/v1/fed/ingest" \ "{\"record_type\":\"tombstone\",\"ref\":\"${ref}\"}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_a}" )" status="$( printf '%s' "${tombstone_again}" \ | tr -d '\r\n' \ | awk 'match($0, /"status":"[^"]+"/) {print substr($0, RSTART+10, RLENGTH-11)}' )" if [[ "${status}" != "ok" && "${status}" != "already_present" ]]; then die "unexpected tombstone repeat response: ${tombstone_again}" fi cap_resp="$( http_post "${sock}" "/v1/capabilities" \ "{\"kind\":\"pointer_name\",\"target\":{\"name\":\"space/${space_a}/fed/records\"},\"expiry_seconds\":3600}" \ --header "Content-Type: application/json" \ --header "X-Amduat-Space: ${space_a}" )" cap_token="$( printf '%s' "${cap_resp}" \ | tr -d '\r\n' \ | awk 'match($0, /"token":"[^"]+"/) {print substr($0, RSTART+9, RLENGTH-10)}' )" if [[ -z "${cap_token}" ]]; then die "failed to mint capability: ${cap_resp}" fi wrong_space_resp="$( http_post_allow "${sock}" "/v1/fed/ingest?record_type=artifact&ref=${ref}" \ "${payload}" \ --header "Content-Type: application/octet-stream" \ --header "X-Amduat-Space: ${space_b}" \ --header "X-Amduat-Capability: ${cap_token}" )" status="$( printf '%s' "${wrong_space_resp}" \ | tr -d '\r\n' \ | awk 'match($0, /"status":"[^"]+"/) {print substr($0, RSTART+10, RLENGTH-11)}' )" if [[ "${status}" != "invalid" ]]; then die "unexpected wrong-space response: ${wrong_space_resp}" fi if [[ "${USE_HTTP_HELPER}" -eq 0 ]]; then code="$(http_status "${sock}" "POST" "/v1/fed/ingest?record_type=artifact&ref=${ref}" \ --header "Content-Type: application/octet-stream" \ --header "X-Amduat-Space: ${space_b}" \ --header "X-Amduat-Capability: ${cap_token}")" if [[ "${code}" != "403" ]]; then die "expected 403 for wrong-space capability, got ${code}" fi fi kill "${pid}" >/dev/null 2>&1 || true wait "${pid}" >/dev/null 2>&1 || true pid="" rm -f "${sock}" "${AMDUATD_BIN}" --root "${root_store}" --sock "${sock}" \ --store-backend index --space "${space_a}" \ --fed-transport stub \ >"${log}" 2>&1 & pid=$! if ! wait_for_ready "${sock}" "${pid}" "${log}"; then die "daemon (fed disabled) not ready" fi disabled_resp="$( http_post_allow "${sock}" "/v1/fed/ingest?record_type=artifact&ref=${ref}" \ "${payload}" \ --header "Content-Type: application/octet-stream" \ --header "X-Amduat-Space: ${space_a}" )" status="$( printf '%s' "${disabled_resp}" \ | tr -d '\r\n' \ | awk 'match($0, /"status":"[^"]+"/) {print substr($0, RSTART+10, RLENGTH-11)}' )" if [[ "${status}" != "error" ]]; then die "unexpected disabled response: ${disabled_resp}" fi if [[ "${USE_HTTP_HELPER}" -eq 0 ]]; then code="$(http_status "${sock}" "POST" "/v1/fed/ingest?record_type=artifact&ref=${ref}" \ --header "Content-Type: application/octet-stream" \ --header "X-Amduat-Space: ${space_a}")" if [[ "${code}" != "503" ]]; then die "expected 503 for federation disabled, got ${code}" fi fi echo "ok"