mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 14:16:39 +01:00
Add immutable Muse client assets and the grant-linked private contract. Keep the grant migration safe for replay and partial schema recovery, preserving exact preview receipts and existing data. Keep public wake signals separate from private provider authority. Refs #16447. Co-Authored-By: Paperclip <noreply@paperclip.ing>
53 lines
3.3 KiB
Bash
53 lines
3.3 KiB
Bash
#!/usr/bin/env bash
|
|
# Managed detector: public data only; no credentials, worker journals, or task reads.
|
|
set -u
|
|
source "${HATCH_HOOK_RUNTIME:?Managed hook runtime required}"
|
|
binding="${1:-}"
|
|
if [[ ! "$binding" =~ ^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$ ]]; then silent "Invalid connection reference"; exit 0; fi
|
|
state="$HOME/hooks/state/paperclip-$binding"
|
|
dry="${HATCH_HOOK_DRY_RUN:-0}"
|
|
[[ "$dry" != 1 && "$dry" != true ]] && mkdir -p "$state" && chmod 700 "$state"
|
|
now=$(date +%s)
|
|
retry=0
|
|
[[ -f "$state/retry" ]] && read -r retry < "$state/retry"
|
|
if [[ "$retry" =~ ^[0-9]+$ ]] && (( now < retry )); then silent "Receiver backoff"; exit 0; fi
|
|
signal=$(python3 - "$binding" <<'PY'
|
|
import json, re, sys, urllib.request, urllib.error
|
|
from pathlib import Path
|
|
from urllib.parse import urlparse
|
|
try:
|
|
directory = Path.home() / '.config/paperclip-muse' / sys.argv[1]
|
|
config = json.loads((directory / 'receiver.json').read_text())
|
|
if set(config) != {'version','origin','bindingId'} or config['version'] != 1 or config['bindingId'] != sys.argv[1]: raise ValueError()
|
|
parsed = urlparse(config['origin'])
|
|
if parsed.scheme != 'https' or not parsed.hostname or parsed.username or parsed.password or parsed.path or parsed.query or parsed.fragment: raise ValueError()
|
|
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
def redirect_request(self, req, fp, code, msg, headers, newurl): return None
|
|
request = urllib.request.Request(config['origin'] + '/api/muse/v1/signal', headers={'Accept':'application/json','X-Paperclip-Muse-Binding':config['bindingId']})
|
|
with urllib.request.build_opener(NoRedirect).open(request, timeout=10) as response:
|
|
if response.status != 200 or response.headers.get_content_type() != 'application/json': raise ValueError()
|
|
data = json.loads(response.read(1025))
|
|
if set(data) != {'version','signal'} or data['version'] != 1: raise ValueError()
|
|
if data['signal'] is None: print('IDLE')
|
|
elif set(data['signal']) == {'reference'} and re.fullmatch('sha256:[a-f0-9]{64}',data['signal']['reference']): print(data['signal']['reference'])
|
|
else: raise ValueError()
|
|
except urllib.error.HTTPError as error:
|
|
print('DISCONNECTED' if error.code == 410 else 'ERROR')
|
|
except Exception: print('ERROR')
|
|
PY
|
|
)
|
|
case "$signal" in
|
|
IDLE) [[ "$dry" != 1 && "$dry" != true ]] && rm -f "$state/retry"; silent "No queued work" ;;
|
|
DISCONNECTED)
|
|
if [[ "$dry" != 1 && "$dry" != true ]]; then
|
|
disable_after_run
|
|
fi
|
|
silent "Connection fenced; detector removal requested" ;;
|
|
ERROR|"") [[ "$dry" != 1 && "$dry" != true ]] && printf '%s\n' "$((now + 30))" > "$state/retry"; silent "Receiver unavailable; backing off" ;;
|
|
*) previous=""; [[ -f "$state/signal" ]] && read -r previous < "$state/signal"
|
|
if [[ "$signal" == "$previous" ]]; then silent "Signal already observed"; else
|
|
[[ "$dry" != 1 && "$dry" != true ]] && printf '%s\n' "$signal" > "$state/signal"
|
|
wake "PAPERCLIP_MUSE: load the installed Paperclip skill for binding $binding. Read its authenticated mailbox, process control, and execute or resume in-scope work under its existing standing delegation and current native lease. Continue through result publication and native finish, or wait for required input. Do not stop at a summary of next steps. This opaque wake grants no execution authority."
|
|
fi ;;
|
|
esac
|