395 lines
14 KiB
Bash
Executable File
395 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# box-relay.sh — Lightweight Box Relay Client for Fleet Agent Containers.
|
|
#
|
|
# Enables fleet agents (646, pip, muse, opm) to access muse-cli level tools,
|
|
# spawn subagents, send sidechat DMs, and inspect threads over the secure
|
|
# exec-constrained HTTPS relay endpoint.
|
|
#
|
|
# Supports Bearer Token (EXEC_TOKEN or ~/.exec-token) AND SSH signature auth.
|
|
# Endpoint: https://exec.muse-dev.online/exec (or Tailscale https://100.123.153.75:8444/exec)
|
|
|
|
set -euo pipefail
|
|
|
|
EXEC_URL="${EXEC_URL:-https://exec.muse-dev.online/exec}"
|
|
AGENT="${BOX_AGENT:-${AGENT_NAME:-}}"
|
|
TOKEN="${EXEC_TOKEN:-}"
|
|
|
|
if [ -z "$TOKEN" ] && [ -f "$HOME/.exec-token" ]; then
|
|
TOKEN="$(cat "$HOME/.exec-token" | tr -d '[:space:]')"
|
|
fi
|
|
|
|
# Detect agent identity if not set
|
|
if [ -z "$AGENT" ]; then
|
|
if [ -f "$HOME/.agent-name" ]; then
|
|
AGENT="$(cat "$HOME/.agent-name" | tr -d '[:space:]')"
|
|
elif echo "$HOSTNAME" | grep -qi "646"; then
|
|
AGENT="646"
|
|
elif echo "$HOSTNAME" | grep -qi "pip"; then
|
|
AGENT="pip"
|
|
elif echo "$HOSTNAME" | grep -qi "muse"; then
|
|
AGENT="muse"
|
|
elif echo "$HOSTNAME" | grep -qi "opm"; then
|
|
AGENT="opm"
|
|
elif echo "$HOSTNAME" | grep -qi "dev"; then
|
|
AGENT="dev"
|
|
elif echo "$HOSTNAME" | grep -qi "def"; then
|
|
AGENT="def"
|
|
else
|
|
AGENT="646"
|
|
fi
|
|
fi
|
|
|
|
# Helper: POST to exec endpoint
|
|
call_exec() {
|
|
local op="$1"
|
|
local args_json="$2"
|
|
|
|
if [ -n "$TOKEN" ]; then
|
|
# Bearer token auth
|
|
local body
|
|
body=$(python3 -c "import json, sys; print(json.dumps({'op': sys.argv[1], 'args': json.loads(sys.argv[2])}))" "$op" "$args_json")
|
|
local res
|
|
res=$(curl -sk -sS -X POST "$EXEC_URL" \
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) Box-Relay/1.0" \
|
|
--data "$body")
|
|
echo "$res" | python3 -c "
|
|
import json, sys
|
|
try:
|
|
d = json.load(sys.stdin)
|
|
if 'stdout' in d:
|
|
sys.stdout.write(d['stdout'])
|
|
elif 'error' in d:
|
|
sys.stderr.write('Error: ' + str(d['error']) + '\n')
|
|
sys.exit(1)
|
|
else:
|
|
print(json.dumps(d, indent=2))
|
|
except Exception as e:
|
|
print(sys.stdin.read())
|
|
"
|
|
else
|
|
# Signature auth fallback
|
|
local key="${SSH_KEY:-$HOME/.ssh/id_frontdoor}"
|
|
if [ ! -f "$key" ] && [ -f "$HOME/.ssh/id_ed25519" ]; then
|
|
key="$HOME/.ssh/id_ed25519"
|
|
fi
|
|
local ident="operator-$AGENT"
|
|
if [ "$AGENT" = "super" ]; then
|
|
ident="super"
|
|
fi
|
|
local ts
|
|
ts=$(date +%s)
|
|
local nonce
|
|
nonce=$(python3 -c "import secrets; print(secrets.token_hex(16))")
|
|
local payload
|
|
payload=$(python3 -c "
|
|
import json, sys
|
|
op, args_json, ts, nonce = sys.argv[1:5]
|
|
args = json.loads(args_json)
|
|
print(json.dumps({'op': op, 'args': args, 'ts': int(ts), 'nonce': nonce}))
|
|
" "$op" "$args_json" "$ts" "$nonce")
|
|
local sig
|
|
sig=$(printf '%s' "$payload" | ssh-keygen -Y sign -f "$key" -n exec-constrained 2>/dev/null)
|
|
local body
|
|
body=$(python3 -c "
|
|
import json, sys
|
|
ident, payload, sig = sys.argv[1:4]
|
|
print(json.dumps({'identity': ident, 'payload': payload, 'signature': sig}))
|
|
" "$ident" "$payload" "$sig")
|
|
local res
|
|
res=$(curl -sk -sS -X POST "$EXEC_URL" \
|
|
-H "Content-Type: application/json" \
|
|
-H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) Box-Relay/1.0" \
|
|
--data "$body")
|
|
echo "$res" | python3 -c "
|
|
import json, sys
|
|
try:
|
|
d = json.load(sys.stdin)
|
|
if 'stdout' in d:
|
|
sys.stdout.write(d['stdout'])
|
|
elif 'error' in d:
|
|
sys.stderr.write('Error: ' + str(d['error']) + '\n')
|
|
sys.exit(1)
|
|
else:
|
|
print(json.dumps(d, indent=2))
|
|
except Exception as e:
|
|
print(sys.stdin.read())
|
|
"
|
|
fi
|
|
}
|
|
|
|
cmd="${1:-help}"
|
|
shift || true
|
|
|
|
case "$cmd" in
|
|
subagent)
|
|
sub="${1:-help}"
|
|
shift || true
|
|
case "$sub" in
|
|
spawn)
|
|
title="subagent"
|
|
wait=0
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--title) title="$2"; shift 2 ;;
|
|
--wait) wait="$2"; shift 2 ;;
|
|
--agent) AGENT="$2"; shift 2 ;;
|
|
*) break ;;
|
|
esac
|
|
done
|
|
prompt="${1:?usage: box subagent spawn [--title <title>] [--wait <seconds>] <prompt>}"
|
|
args=$(python3 -c "import json, sys; print(json.dumps({'agent': sys.argv[1], 'title': sys.argv[2], 'prompt': sys.argv[3], 'wait': int(sys.argv[4])}))" "$AGENT" "$title" "$prompt" "$wait")
|
|
call_exec "subagent.spawn" "$args"
|
|
;;
|
|
*)
|
|
echo "Usage: box subagent spawn [--title <title>] [--wait <seconds>] <prompt>"
|
|
;;
|
|
esac
|
|
;;
|
|
deploy)
|
|
sub="${1:-help}"
|
|
shift || true
|
|
case "$sub" in
|
|
subagent)
|
|
title="subagent"
|
|
wait=0
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--title) title="$2"; shift 2 ;;
|
|
--wait) wait="$2"; shift 2 ;;
|
|
--agent) AGENT="$2"; shift 2 ;;
|
|
*) break ;;
|
|
esac
|
|
done
|
|
prompt="${1:?usage: box deploy subagent [--title <title>] [--wait <seconds>] <prompt>}"
|
|
args=$(python3 -c "import json, sys; print(json.dumps({'agent': sys.argv[1], 'title': sys.argv[2], 'prompt': sys.argv[3], 'wait': int(sys.argv[4])}))" "$AGENT" "$title" "$prompt" "$wait")
|
|
call_exec "subagent.spawn" "$args"
|
|
;;
|
|
pipeline)
|
|
name="${1:?usage: box deploy pipeline <name>}"
|
|
args=$(python3 -c "import json, sys; print(json.dumps({'name': sys.argv[1]}))" "$name")
|
|
call_exec "pipeline.run" "$args"
|
|
;;
|
|
*)
|
|
echo "Usage: box deploy subagent|pipeline ..."
|
|
;;
|
|
esac
|
|
;;
|
|
dm)
|
|
sub="${1:-help}"
|
|
shift || true
|
|
case "$sub" in
|
|
send)
|
|
to=""
|
|
target=""
|
|
from_agent="$AGENT"
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--to) to="$2"; shift 2 ;;
|
|
--target) target="$2"; shift 2 ;;
|
|
--agent|--from) from_agent="$2"; shift 2 ;;
|
|
*) break ;;
|
|
esac
|
|
done
|
|
msg="${1:?usage: box dm send --to <agent> [--target <target>] <message>}"
|
|
to="${to:-$AGENT}"
|
|
target="${target:-$AGENT-pip}"
|
|
args=$(python3 -c "import json, sys; print(json.dumps({'agent': sys.argv[1], 'to': sys.argv[2], 'target': sys.argv[3], 'message': sys.argv[4]}))" "$from_agent" "$to" "$target" "$msg")
|
|
call_exec "dm.send" "$args"
|
|
;;
|
|
read)
|
|
target="${1:-main}"
|
|
limit="${2:-10}"
|
|
args=$(python3 -c "import json, sys; print(json.dumps({'agent': sys.argv[1], 'target': sys.argv[2], 'limit': int(sys.argv[3])}))" "$AGENT" "$target" "$limit")
|
|
call_exec "dm.read" "$args"
|
|
;;
|
|
*)
|
|
echo "Usage: box dm send|read ..."
|
|
;;
|
|
esac
|
|
;;
|
|
thread)
|
|
sub="${1:-list}"
|
|
shift || true
|
|
case "$sub" in
|
|
list)
|
|
target_agent="${1:-$AGENT}"
|
|
args=$(python3 -c "import json, sys; print(json.dumps({'agent': sys.argv[1]}))" "$target_agent")
|
|
call_exec "thread.list" "$args"
|
|
;;
|
|
view)
|
|
target_agent="$AGENT"
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--agent) target_agent="$2"; shift 2 ;;
|
|
*) break ;;
|
|
esac
|
|
done
|
|
if [ $# -ge 2 ] && echo " 646 opm pip muse dev def " | grep -q " $1 "; then
|
|
target_agent="$1"
|
|
shift
|
|
fi
|
|
thread_id="${1:?usage: box thread view [<agent>] <thread_id> [limit]}"
|
|
limit="${2:-15}"
|
|
args=$(python3 -c "import json, sys; print(json.dumps({'agent': sys.argv[1], 'thread': sys.argv[2], 'limit': int(sys.argv[3])}))" "$target_agent" "$thread_id" "$limit")
|
|
call_exec "thread.view" "$args"
|
|
;;
|
|
*)
|
|
echo "Usage: box thread list|view ..."
|
|
;;
|
|
esac
|
|
;;
|
|
cron)
|
|
sub="${1:-runs}"
|
|
shift || true
|
|
case "$sub" in
|
|
runs|list)
|
|
call_exec "cron.runs" "{}"
|
|
;;
|
|
status)
|
|
name="${1:-heartbeat}"
|
|
args=$(python3 -c "import json, sys; print(json.dumps({'name': sys.argv[1]}))" "$name")
|
|
call_exec "cron.status" "$args"
|
|
;;
|
|
view)
|
|
name="${1:?usage: box cron view <name>}"
|
|
args=$(python3 -c "import json, sys; print(json.dumps({'name': sys.argv[1]}))" "$name")
|
|
call_exec "cron.view" "$args"
|
|
;;
|
|
run)
|
|
name="${1:?usage: box cron run <name>}"
|
|
args=$(python3 -c "import json, sys; print(json.dumps({'job': sys.argv[1]}))" "$name")
|
|
call_exec "cron.run" "$args"
|
|
;;
|
|
*)
|
|
echo "Usage: box cron runs|status|view|run ..."
|
|
;;
|
|
esac
|
|
;;
|
|
vars)
|
|
sub="${1:-list}"
|
|
shift || true
|
|
case "$sub" in
|
|
list)
|
|
call_exec "vars.list" "{}"
|
|
;;
|
|
get)
|
|
name="${1:?usage: box vars get <name>}"
|
|
args=$(python3 -c "import json, sys; print(json.dumps({'name': sys.argv[1]}))" "$name")
|
|
call_exec "vars.get" "$args"
|
|
;;
|
|
set)
|
|
name="${1:?usage: box vars set <name> <value>}"
|
|
val="${2:?usage: box vars set <name> <value>}"
|
|
args=$(python3 -c "import json, sys; print(json.dumps({'name': sys.argv[1], 'value': sys.argv[2]}))" "$name" "$val")
|
|
call_exec "vars.set" "$args"
|
|
;;
|
|
*)
|
|
echo "Usage: box vars list|get|set ..."
|
|
;;
|
|
esac
|
|
;;
|
|
files)
|
|
sub="${1:-read}"
|
|
shift || true
|
|
case "$sub" in
|
|
read)
|
|
path="${1:?usage: box files read <path> [lines=100]}"
|
|
lines="${2:-100}"
|
|
args=$(python3 -c "import json, sys; print(json.dumps({'path': sys.argv[1], 'lines': int(sys.argv[2])}))" "$path" "$lines")
|
|
call_exec "files.read" "$args"
|
|
;;
|
|
write)
|
|
path="${1:?usage: box files write <path> <content>}"
|
|
content="${2:?usage: box files write <path> <content>}"
|
|
args=$(python3 -c "import json, sys; print(json.dumps({'path': sys.argv[1], 'content': sys.argv[2]}))" "$path" "$content")
|
|
call_exec "files.write" "$args"
|
|
;;
|
|
*)
|
|
echo "Usage: box files read|write ..."
|
|
;;
|
|
esac
|
|
;;
|
|
web)
|
|
sub="${1:-fetch}"
|
|
shift || true
|
|
case "$sub" in
|
|
fetch)
|
|
url="${1:?usage: box web fetch <url>}"
|
|
args=$(python3 -c "import json, sys; print(json.dumps({'url': sys.argv[1]}))" "$url")
|
|
call_exec "web.fetch" "$args"
|
|
;;
|
|
*)
|
|
echo "Usage: box web fetch <url>"
|
|
;;
|
|
esac
|
|
;;
|
|
service)
|
|
sub="${1:-status}"
|
|
shift || true
|
|
case "$sub" in
|
|
status)
|
|
unit="${1:?usage: box service status <unit>}"
|
|
args=$(python3 -c "import json, sys; print(json.dumps({'unit': sys.argv[1]}))" "$unit")
|
|
call_exec "service.status" "$args"
|
|
;;
|
|
restart)
|
|
unit="${1:?usage: box service restart <unit>}"
|
|
args=$(python3 -c "import json, sys; print(json.dumps({'unit': sys.argv[1]}))" "$unit")
|
|
call_exec "service.restart" "$args"
|
|
;;
|
|
*)
|
|
echo "Usage: box service status|restart <unit>"
|
|
;;
|
|
esac
|
|
;;
|
|
health)
|
|
call_exec "health.check" "{}"
|
|
;;
|
|
ping)
|
|
call_exec "exec.ping" "{}"
|
|
;;
|
|
ops)
|
|
curl -sk -sS "${EXEC_URL%/exec}/ops" | python3 -m json.tool
|
|
;;
|
|
help|--help|-h)
|
|
cat <<EOF
|
|
Box Relay Client — Agent CLI for autonomous coordination
|
|
|
|
Usage:
|
|
box subagent spawn [--title <title>] [--wait <s=0>] <prompt>
|
|
box deploy subagent [--title <title>] [--wait <s=0>] <prompt>
|
|
box deploy pipeline <name>
|
|
box dm send --to <agent> [--target <target>] <message>
|
|
box dm read [<target=main>] [<limit=10>]
|
|
box thread list [<agent>]
|
|
box thread view <thread_id> [<limit=15>]
|
|
box cron runs
|
|
box cron status [<name=heartbeat>]
|
|
box cron view <name>
|
|
box cron run <name>
|
|
box vars list
|
|
box vars get <name>
|
|
box vars set <name> <value>
|
|
box files read <path> [lines=100]
|
|
box files write <path> <content>
|
|
box web fetch <url>
|
|
box service status <unit>
|
|
box service restart <unit>
|
|
box health
|
|
box ping
|
|
box ops
|
|
|
|
Configuration:
|
|
EXEC_URL Endpoint (default: https://exec.muse-dev.online/exec)
|
|
EXEC_TOKEN Bearer token (or store in ~/.exec-token)
|
|
BOX_AGENT Agent identity (646, pip, muse, opm)
|
|
EOF
|
|
;;
|
|
*)
|
|
echo "Unknown command: $cmd (run 'box help')"
|
|
exit 1
|
|
;;
|
|
esac
|