Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,41 @@ Type `@` in Claude Code to see available Resources.

You can ask your LLM to add new Resources by providing an `https://` URI to a public webpage or a `beam://` URI to a file in Clay.

## Contributing

This repo requires commits to be signed with a [Groundwire](https://groundwire.network) identity. PRs with unsigned commits will be rejected by CI.

### Setup commit signing

You need an Urbit ship running the `%vitriol` agent.

**Quick install:**

```bash
./hooks/install.sh <your-ship-url>/vitriol "<auth-cookie>"
```

**Manual install:**

```bash
git config gpg.program /path/to/hooks/groundwire-sign
git config commit.gpgsign true
git config groundwire.sign-endpoint <your-ship-url>/vitriol
git config groundwire.sign-token "<auth-cookie>"
```

Once configured, all commits will be automatically signed with your ship's Ed25519 networking key. The CI verifies signatures against on-chain keys via [vitriol.bot](https://vitriol.bot).

### Re-signing existing commits

If you have unsigned commits on a branch:

```bash
git rebase --exec "true" HEAD~N
```

(where N is the number of commits to re-sign)

## Development

### Build Commands
Expand Down
196 changes: 196 additions & 0 deletions hooks/groundwire-sign
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
#!/usr/bin/env bash
#
# groundwire-sign — custom gpg.program for signing git commits with a
# Groundwire key via a remote signing service (an Urbit ship running %vitriol).
#
# Git calls gpg.program with:
# gpg.program --status-fd=2 -bsau <key-id>
# and pipes the commit object (minus gpgsig) to stdin.
#
# This script:
# 1. Reads the commit content from stdin
# 2. Optionally fetches the maintainer's ecash pubkey and sats-per-pr price
# 3. Sends content (+ sats_required if applicable) to the committer's ship
# 4. Returns a structured signature block on stdout
# 5. Emits GPG-compatible status on fd 2
#
# The signature block includes the signer's @p so the CI verifier can
# look up the identity on-chain via its own ship, rather than trusting
# the committer's self-reported pubkey.
#
# Configuration (git config or env vars):
# GROUNDWIRE_SIGN_ENDPOINT — base URL of the committer's ship (e.g. http://localhost:8080/vitriol)
# GROUNDWIRE_SIGN_TOKEN — auth cookie for the ship
# GROUNDWIRE_MAINTAINER_ENDPOINT — (optional) base URL of the maintainer's ship for ecash pubkey
# GROUNDWIRE_MAINTAINER_TOKEN — (optional) auth cookie for the maintainer's ship
#
# Install:
# git config --global gpg.program /path/to/groundwire-sign
# git config --global commit.gpgsign true
#

set -euo pipefail

# --- Configuration -----------------------------------------------------------

ENDPOINT="${GROUNDWIRE_SIGN_ENDPOINT:-$(git config --get groundwire.sign-endpoint 2>/dev/null || echo "")}"
TOKEN="${GROUNDWIRE_SIGN_TOKEN:-$(git config --get groundwire.sign-token 2>/dev/null || echo "")}"
MAINTAINER_ENDPOINT="${GROUNDWIRE_MAINTAINER_ENDPOINT:-$(git config --get groundwire.maintainer-endpoint 2>/dev/null || echo "")}"
MAINTAINER_TOKEN="${GROUNDWIRE_MAINTAINER_TOKEN:-$(git config --get groundwire.maintainer-token 2>/dev/null || echo "")}"

if [ -z "$ENDPOINT" ]; then
echo "groundwire-sign: GROUNDWIRE_SIGN_ENDPOINT not set" >&2
echo " Set via: git config --global groundwire.sign-endpoint <url>" >&2
echo " Or env: export GROUNDWIRE_SIGN_ENDPOINT=<url>" >&2
exit 1
fi

# --- Parse git's gpg arguments -----------------------------------------------

STATUS_FD=""
KEY_ID=""

while [ $# -gt 0 ]; do
case "$1" in
--status-fd=*)
STATUS_FD="${1#--status-fd=}"
;;
--status-fd)
shift
STATUS_FD="$1"
;;
-bsau)
shift
KEY_ID="$1"
;;
*)
;;
esac
shift
done

# --- Read commit content from stdin -------------------------------------------

COMMIT_CONTENT=$(cat)

# --- Fetch maintainer's ecash pubkey and price (optional) --------------------

ECASH_PUBKEY=""
SATS_REQUIRED=""

if [ -n "$MAINTAINER_ENDPOINT" ]; then
# Fetch ecash pubkey
ECASH_RESPONSE=$(curl -s -f \
-H "Cookie: ${MAINTAINER_TOKEN}" \
"${MAINTAINER_ENDPOINT}/ecash-pubkey") || {
echo "groundwire-sign: warning: could not fetch maintainer ecash pubkey" >&2
}
if [ -n "$ECASH_RESPONSE" ]; then
ECASH_CONFIGURED=$(echo "$ECASH_RESPONSE" | jq -r '.configured // empty')
if [ "$ECASH_CONFIGURED" = "true" ]; then
ECASH_PUBKEY=$(echo "$ECASH_RESPONSE" | jq -r '.pubkey // empty')
else
echo "groundwire-sign: warning: maintainer ecash not configured" >&2
fi
fi

# Fetch sats-per-pr price
PRICE_RESPONSE=$(curl -s -f \
-H "Cookie: ${MAINTAINER_TOKEN}" \
"${MAINTAINER_ENDPOINT}/sats-per-pr") || {
echo "groundwire-sign: warning: could not fetch maintainer price" >&2
}
if [ -n "$PRICE_RESPONSE" ]; then
PRICE_CONFIGURED=$(echo "$PRICE_RESPONSE" | jq -r '.configured // empty')
if [ "$PRICE_CONFIGURED" = "true" ]; then
SATS_REQUIRED=$(echo "$PRICE_RESPONSE" | jq -r '.sats // empty')
fi
fi
fi

# --- Send to signing service --------------------------------------------------

PAYLOAD_ARGS=(--arg content "$COMMIT_CONTENT" --arg key_id "$KEY_ID")
PAYLOAD_EXPR='{content: $content, key_id: $key_id'

if [ -n "$SATS_REQUIRED" ] && [ "$SATS_REQUIRED" != "0" ]; then
PAYLOAD_ARGS+=(--argjson sats "$SATS_REQUIRED")
PAYLOAD_EXPR+=', sats_required: $sats'
fi

if [ -n "$ECASH_PUBKEY" ]; then
PAYLOAD_ARGS+=(--arg ecash_pubkey "$ECASH_PUBKEY")
PAYLOAD_EXPR+=', ecash_pubkey: $ecash_pubkey'
fi

PAYLOAD_EXPR+='}'
PAYLOAD=$(jq -n "${PAYLOAD_ARGS[@]}" "$PAYLOAD_EXPR")

RESPONSE=$(curl -s -f \
-X POST \
-H "Content-Type: application/json" \
-H "Cookie: ${TOKEN}" \
"${ENDPOINT}/sign" \
-d "$PAYLOAD") || {
echo "groundwire-sign: signing service request failed" >&2
exit 1
}

# Check for error in response (e.g. insufficient balance)
ERROR=$(echo "$RESPONSE" | jq -r '.error // empty')
if [ -n "$ERROR" ]; then
echo "groundwire-sign: $ERROR" >&2
exit 1
fi

SIGNATURE=$(echo "$RESPONSE" | jq -r '.signature // empty')
SIGNER_ID=$(echo "$RESPONSE" | jq -r '.signer_id // empty')
PASS=$(echo "$RESPONSE" | jq -r '.pass // empty')
ECASH_AMOUNT=$(echo "$RESPONSE" | jq -r '.ecash_amount // empty')
ECASH_ENCRYPTED=$(echo "$RESPONSE" | jq -r '.ecash_encrypted // empty')
ECASH_CIPHERTEXT=$(echo "$RESPONSE" | jq -r '.ecash_ciphertext // empty')
ECASH_EPH_PUBKEY=$(echo "$RESPONSE" | jq -r '.ecash_ephemeral_pubkey // empty')
ECASH_MAC=$(echo "$RESPONSE" | jq -r '.ecash_mac // empty')
ECASH_TOKENS=$(echo "$RESPONSE" | jq -r '.ecash_tokens // empty')

if [ -z "$SIGNATURE" ]; then
echo "groundwire-sign: no signature in response" >&2
exit 1
fi

if [ -z "$SIGNER_ID" ]; then
echo "groundwire-sign: no signer_id in response" >&2
exit 1
fi

# --- Output structured signature to stdout ------------------------------------
# Git captures this verbatim as the gpgsig header.
# The CI verifier parses this to extract the signer's @p and signature.

{
echo "-----BEGIN GROUNDWIRE SIGNATURE-----"
echo "signer:${SIGNER_ID}"
echo "pass:${PASS}"
echo "sig:${SIGNATURE}"
[ -n "$ECASH_PUBKEY" ] && echo "ecash-pubkey:${ECASH_PUBKEY}"
if [ -n "$ECASH_AMOUNT" ] && [ "$ECASH_AMOUNT" != "0" ]; then
echo "ecash-amount:${ECASH_AMOUNT}"
if [ "$ECASH_ENCRYPTED" = "true" ]; then
echo "ecash-ciphertext:${ECASH_CIPHERTEXT}"
echo "ecash-ephemeral-pubkey:${ECASH_EPH_PUBKEY}"
echo "ecash-mac:${ECASH_MAC}"
elif [ -n "$ECASH_TOKENS" ] && [ "$ECASH_TOKENS" != "null" ]; then
echo "ecash-tokens:${ECASH_TOKENS}"
fi
fi
echo "-----END GROUNDWIRE SIGNATURE-----"
}

# --- Emit GPG-compatible status on status-fd ----------------------------------

if [ -n "$STATUS_FD" ]; then
{
echo "[GNUPG:] SIG_CREATED D"
echo "[GNUPG:] BEGIN_SIGNING"
} >&"$STATUS_FD"
fi
85 changes: 85 additions & 0 deletions hooks/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
#
# Install Groundwire commit signing.
#
# Usage:
# ./install.sh <ship-url> [auth-cookie] [--maintainer <url> [cookie]]
#
# This configures git globally to sign all commits with your Groundwire key.
# Your ship must be running the %vitriol agent with a signing key configured.
#
# When --maintainer is provided, the signing hook will fetch the maintainer's
# ecash pubkey and sats-per-PR price. If a price is set, ecash tokens from
# the committer's wallet are automatically included in the signature.
#
# Example:
# ./install.sh http://localhost:8080/vitriol "urbauth-~zod=0v5.abc..."
# ./install.sh http://localhost:8080/vitriol "urbauth-~zod=0v5.abc..." \
# --maintainer http://maintainer:8080/vitriol "urbauth-~nec=0v5.xyz..."
#

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SIGN_PROGRAM="${SCRIPT_DIR}/groundwire-sign"

if [ $# -lt 1 ]; then
echo "Usage: $0 <ship-url> [auth-cookie]"
echo ""
echo " ship-url Base URL of your ship's vitriol endpoint (e.g. http://localhost:8080/vitriol)"
echo " auth-cookie Auth cookie for your ship (e.g. urbauth-~zod=0v5.abc...)"
exit 1
fi

ENDPOINT="$1"
TOKEN="${2:-}"

# Parse optional --maintainer flag
MAINTAINER_ENDPOINT=""
MAINTAINER_TOKEN=""
shift; shift 2>/dev/null || true
while [ $# -gt 0 ]; do
case "$1" in
--maintainer)
shift
MAINTAINER_ENDPOINT="${1:-}"
shift 2>/dev/null || true
# Next arg is maintainer token if it doesn't start with --
if [ $# -gt 0 ] && [[ "$1" != --* ]]; then
MAINTAINER_TOKEN="$1"
shift
fi
;;
*)
shift
;;
esac
done

chmod +x "$SIGN_PROGRAM"

git config --global gpg.program "$SIGN_PROGRAM"
git config --global commit.gpgsign true
git config --global groundwire.sign-endpoint "$ENDPOINT"

if [ -n "$TOKEN" ]; then
git config --global groundwire.sign-token "$TOKEN"
fi

if [ -n "$MAINTAINER_ENDPOINT" ]; then
git config --global groundwire.maintainer-endpoint "$MAINTAINER_ENDPOINT"
echo " groundwire.maintainer-endpoint: $MAINTAINER_ENDPOINT"
if [ -n "$MAINTAINER_TOKEN" ]; then
git config --global groundwire.maintainer-token "$MAINTAINER_TOKEN"
fi
fi

echo "Groundwire commit signing configured."
echo " gpg.program: $SIGN_PROGRAM"
echo " groundwire.sign-endpoint: $ENDPOINT"
echo " commit.gpgsign: true"
if [ -n "$MAINTAINER_ENDPOINT" ]; then
echo " groundwire.maintainer-endpoint: $MAINTAINER_ENDPOINT"
fi
echo ""
echo "All future commits will be signed with your Groundwire key."
Loading