#!/bin/bash # Submit a change request from a machine that cannot push. # # A checkout cloned over https cannot publish anything: a change made on such a # machine is lost at the next pull. So the change is written out, signed, and # handed to whoever can apply it -- see "Can this machine push?" in AGENTS.md. # # dot-request --key ~/.config/dot/inbox-key < request.md # some-agent-writing-a-request | dot-request --key ~/.config/dot/inbox-key # # The request is markdown, and its first line is its title (`# ...`). Say which # files change, give the diff or the exact replacement text rather than a # description of it, why, and how to verify it. # # It posts to the request inbox, which is a public endpoint and named here on # purpose -- see "Can this machine push?" in AGENTS.md. Reaching it proves # nothing on its own: a request is stored only if it carries a signature from a # key authorised for that inbox. Set DOT_REQUEST_URL to point somewhere else # when testing. # # --inbox NAME file into another inbox than dot. The key must be # authorised there too, and signs for that inbox only. # --compress send it xz-compressed (xz -9e), whatever its size # --no-compress send it as it is, whatever its size # # By default a signed request over 400 KB goes xz-compressed and a smaller one # as it is: the inbox takes 400 KB on the wire, and more than that only # compressed, up to what the inbox allows once decompressed. Compressing needs # xz; without it, a request that must be compressed is refused, not sent plain. # # Runs under macOS's /bin/bash 3.2. set -euo pipefail key="" inbox="dot" compress="" # empty: decided by size compress_over=$((400 * 1024)) usage() { sed -n '3,32p' "$0" | sed 's/^# \{0,1\}//' exit "${1:-0}" } while [[ $# -gt 0 ]]; do case "$1" in --key) key="${2:-}"; shift 2 ;; --key=*) key="${1#--key=}"; shift ;; --inbox) inbox="${2:-}"; shift 2 ;; --inbox=*) inbox="${1#--inbox=}"; shift ;; --compress) compress=yes; shift ;; --no-compress) compress=no; shift ;; -h|--help) usage 0 ;; *) >&2 echo "dot-request: unknown argument: $1"; usage 1 ;; esac done # The inbox is a path segment there and a signature namespace here: held to # the same names the inbox itself accepts. if [[ ! "${inbox}" =~ ^[A-Za-z0-9_-]{1,32}$ ]]; then >&2 echo "dot-request: --inbox: '${inbox}' is not an inbox name" exit 1 fi url="${DOT_REQUEST_URL:-https://req.soarez.net/inbox/${inbox}}" if [[ -z "${key}" ]]; then >&2 echo "dot-request: --key is required (the ssh key this machine signs with)" exit 1 fi if [[ ! -r "${key}" ]]; then >&2 echo "dot-request: cannot read ${key}" exit 1 fi if [[ -t 0 ]]; then >&2 echo "dot-request: the request is read from stdin" usage 1 fi tmp="$(mktemp -d)" trap 'rm -rf "${tmp}"' EXIT cat > "${tmp}/request.md" if [[ ! -s "${tmp}/request.md" ]]; then >&2 echo "dot-request: empty request" exit 1 fi if ! head -1 "${tmp}/request.md" | grep -q '^# '; then >&2 echo "dot-request: the first line must be a markdown heading, '# something'" exit 1 fi # The namespace names the inbox this request is for, and the store verifies # against the same one. It is what stops a request signed here being replayed # into a different inbox, and what stops this key being made to sign for # anything else. ssh-keygen -Y sign -q -f "${key}" -n "sz-request:${inbox}" "${tmp}/request.md" cat "${tmp}/request.md" "${tmp}/request.md.sig" > "${tmp}/signed.md" # What is signed is the text; compression is only how it travels, undone by # the inbox before it checks anything. size="$(wc -c < "${tmp}/signed.md" | tr -d ' ')" if [[ -z "${compress}" ]]; then if [[ "${size}" -gt "${compress_over}" ]]; then compress=yes; else compress=no; fi fi encoding=() body="${tmp}/signed.md" if [[ "${compress}" == yes ]]; then if ! command -v xz >/dev/null; then >&2 echo "dot-request: ${size} bytes needs compressing, and there is no xz here" exit 1 fi xz -9e -c "${tmp}/signed.md" > "${tmp}/signed.md.xz" body="${tmp}/signed.md.xz" encoding=(-H 'Content-Encoding: xz') fi code="$(curl -sS -o "${tmp}/reply" -w '%{http_code}' \ -X POST -H 'Content-Type: text/markdown' ${encoding[@]+"${encoding[@]}"} \ --data-binary "@${body}" "${url}")" if [[ "${code}" == "201" ]]; then echo "submitted: $(cat "${tmp}/reply")" else >&2 echo "dot-request: rejected (HTTP ${code}): $(cat "${tmp}/reply")" exit 1 fi