igneum/relay/clients/send.sh
igneum-labs 1065b81d05 relay: three auth tiers, signed run tasks, machine secrets, retention; clients on headers; TZ=UTC and curl -K checks (X23 X24 X25 X26 X27 X28 X29 G13 G14)
Relay (X23, X27): the intake key is its own tier (upload and file drops only, RELAY_INTAKE_COMPAT=0 closes it);
a run task needs an Ed25519 signature by the Mac run key over {to, nonce, body sha256, flags} (RELAY_RUN_PUB,
401 without) and an HMAC tag with the target's machine secret that the agent verifies before anything runs;
results and registration are bound to the machine the secret proves (403 on a forged from).
X24: every client and Mac tool sends x-relay-token as a header to /api/relay?fn=; the path token stays for the
phone page only. X25: the agent arms the logon task only for a restart a task asked for and disarms on start
and exit. X26: 30-day retention with blob deletion, feed capped at 100, the dl base as RELAY_DL_BASE held by the
agent, never in a body. X28: GET inbox never acks (POST inbox does), RELAY-REBOOT on its own line and only with a
reboot flag, 120/min and 10 failed auths/min per IP, no username or folder on register, WSL sudo scoped to
apt-get and dpkg with SETENV, no password on a command line. X29: the intake key reaches curl through -K in
upload.sh and both upload-log.bat; tools/ci/curl-header-check.sh fails the class. G14: TZ=UTC in ship-app.mjs
and publish-jobs.sh; tools/ci/commit-tz-check.sh fails the class; history-rewrite.md names the .old-2026-10-05
files as the values in the history. The handler moved to relay/lib/handler.mjs with injected sql and blobs
(relay/lib/blob.mjs holds @vercel/blob) so relay/test/handler.test.mjs drives it without a database:
47 tests across 6 suites, all green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-05 18:46:13 +00:00

55 lines
5.2 KiB
Bash
Executable file

#!/usr/bin/env bash
# Igneum relay client for Mac/Linux/WSL. Needs curl and jq (python3 fallback for jq).
# send.sh "<text>" note to everyone send.sh <file> file (up to 50 MB)
# send.sh inbox unread tasks for this machine, marked read send.sh peek same, not marked
# send.sh get <id> print an item, download its file here send.sh done <id>
# send.sh result "<text>" [--task-id N] [--file path] (needs this machine's secret)
# RELAY_TO=PC1 RELAY_TITLE="..." RELAY_MACHINE=Mac override the defaults.
# The token, key and machine secret go to curl through a header file (-K), never on the command line or in the URL
# (X24, X29). The URL, key and token are written in by make-clients.sh (the repo copy holds placeholders);
# machine-secret.txt next to this file (or RELAY_MACHINE_SECRET) names this machine on every result (X27).
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
RELAY_URL='__RELAY_URL__'; RELAY_KEY='__RELAY_KEY__'; RELAY_TOKEN='__RELAY_TOKEN__'
API="$RELAY_URL/api/relay?fn="
STATE="${XDG_STATE_HOME:-$HOME/.local/state}/igneum-relay"; mkdir -p "$STATE"; chmod 700 "$STATE"
SECRET="${RELAY_MACHINE_SECRET:-}"; [ -n "$SECRET" ] || { [ -f "$HERE/machine-secret.txt" ] && SECRET="$(tr -d '[:space:]' < "$HERE/machine-secret.txt")" || SECRET=""; }
HDR="$STATE/headers.cfg"
( umask 077; { printf 'header = "x-relay-token: %s"\nheader = "x-igneum-key: %s"\n' "$RELAY_TOKEN" "$RELAY_KEY"; [ -n "$SECRET" ] && printf 'header = "x-machine-secret: %s"\n' "$SECRET"; } > "$HDR" )
machine() { if [ -n "${RELAY_MACHINE:-}" ]; then echo "$RELAY_MACHINE"; elif [ -f "$STATE/machine.txt" ]; then cat "$STATE/machine.txt"; else hostname -s; fi; }
jqq() { if command -v jq >/dev/null; then jq "$@"; else python3 -c 'import json,sys; q=sys.argv[1]; d=json.load(sys.stdin); print(d[q.strip(".")])' "$@"; fi; }
post() { curl -sS --max-time 120 -K "$HDR" -X POST "$API$1" -H 'Content-Type: application/json' --data-binary "$2"; }
get() { curl -sS --max-time 60 -K "$HDR" "$API${1/\?/&}"; }
jstr() { python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' ; }
upload() {
local path="$1" name size tok
name="$(basename "$path")"; size=$(stat -f%z "$path" 2>/dev/null || stat -c%s "$path")
tok="$(post upload "{\"name\":$(printf '%s' "$name" | jstr),\"size\":$size}")"
local put url token ver
put="$(printf '%s' "$tok" | jqq -r .put_url)"; token="$(printf '%s' "$tok" | jqq -r .token)"; ver="$(printf '%s' "$tok" | jqq -r .api_version)"
url="$(curl -sS --max-time 600 -X PUT "$put" -H "authorization: Bearer $token" -H "x-api-version: $ver" -H 'x-add-random-suffix: 1' -H 'x-content-type: application/octet-stream' --data-binary "@$path" | jqq -r .url)"
[ -n "$url" ] && [ "$url" != "null" ] || { echo "blob upload failed" >&2; return 1; }
printf '"file_name":%s,"file_url":"%s","size":%s' "$(printf '%s' "$name" | jstr)" "$url" "$size"
}
show() { python3 -c '
import json,sys; it=json.load(sys.stdin)
for x in (it if isinstance(it,list) else [it]):
print("===== #%s %s from %s to %s at %s%s =====" % (x["id"],x["kind"],x["from"],x["to"],x["ts"], (" | "+x["title"]) if x.get("title") else ""))
if x.get("body"): print(x["body"])
if x.get("has_file"): print("file: %s (%s bytes); send.sh get %s downloads it" % (x["file_name"],x["size"],x["id"]))'; }
cmd="${1:-}"; M="$(machine)"; TO="${RELAY_TO:-all}"; TITLE="${RELAY_TITLE:-}"
case "$cmd" in
inbox) post inbox "{\"machine\":$(printf '%s' "$M" | jstr),\"kind\":\"task\",\"ack\":true}" | jqq .items | show ;; # marking read is a POST (X28)
peek) get "inbox?machine=$M&kind=task" | jqq .items | show ;;
get) [ -n "${2:-}" ] || { echo 'get <id>' >&2; exit 1; }; j="$(get "item?id=$2")"; printf '%s' "$j" | jqq .item | show
if [ "$(printf '%s' "$j" | jqq -r .item.has_file)" = "true" ]; then out="$2-$(printf '%s' "$j" | jqq -r .item.file_name)"; curl -sSL --max-time 600 -K "$HDR" "${API}file&id=$2" -o "$out"; echo "downloaded: $out"; fi ;;
done) [ -n "${2:-}" ] || { echo "done <id>" >&2; exit 1; }; post done "{\"id\":$2}" >/dev/null; echo "#$2 done" ;;
result) shift; text="${1:-}"; shift || true; task=0; file=""
while [ $# -gt 0 ]; do case "$1" in --task-id) task="$2"; shift 2;; --file) file="$2"; shift 2;; *) shift;; esac; done
[ -n "$SECRET" ] || echo "note: no machine-secret.txt next to send.sh; the relay refuses results from a bound machine without it" >&2
extra=""; [ -n "$file" ] && extra=",$(upload "$file")"
post drop "{\"from\":$(printf '%s' "$M" | jstr),\"to\":\"$TO\",\"kind\":\"result\",\"title\":$(printf '%s' "$TITLE" | jstr),\"body\":$(printf '%s' "$text" | jstr),\"task_id\":$task$extra}"; echo ;;
"") echo 'send.sh "<text>" | send.sh <file> | send.sh inbox | send.sh result "<text>" | send.sh get <id> | send.sh done <id>' >&2; exit 1 ;;
*) if [ -f "$cmd" ]; then post drop "{\"from\":$(printf '%s' "$M" | jstr),\"to\":\"$TO\",\"kind\":\"file\",\"title\":$(printf '%s' "${TITLE:-$(basename "$cmd")}" | jstr),\"body\":$(printf '%s' "${2:-}" | jstr),$(upload "$cmd")}"
else post drop "{\"from\":$(printf '%s' "$M" | jstr),\"to\":\"$TO\",\"kind\":\"text\",\"title\":$(printf '%s' "$TITLE" | jstr),\"body\":$(printf '%s' "$cmd" | jstr)}"; fi; echo ;;
esac