fix: feat: disinto edge command + SSH-forced-command control plane in tools/edge-control/ (#621)
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed
ci/woodpecker/pr/smoke-init Pipeline failed

This commit is contained in:
Claude 2026-04-10 18:42:41 +00:00
parent f8bb3eea7d
commit a4fe845b9d
7 changed files with 1498 additions and 0 deletions

View file

@ -54,6 +54,12 @@ Usage:
disinto hire-an-agent <agent-name> <role> [--formula <path>] [--local-model <url>] [--model <name>]
Hire a new agent (create user + .profile repo)
disinto agent <subcommand> Manage agent state (enable/disable)
disinto edge <verb> [options] Manage edge tunnel registrations
Edge subcommands:
register [project] Register a new tunnel (generates keypair if needed)
deregister <project> Remove a tunnel registration
status Show registered tunnels
Agent subcommands:
disable <agent> Remove state file to disable agent
@ -1613,6 +1619,230 @@ EOF
esac
}
# ── edge command ──────────────────────────────────────────────────────────────
# Manage edge tunnel registrations (reverse SSH tunnels to edge hosts)
# Usage: disinto edge <verb> [options]
# register [project] Register a new tunnel (generates keypair if needed)
# deregister <project> Remove a tunnel registration
# status Show registered tunnels
disinto_edge() {
local subcmd="${1:-}"
local EDGE_HOST="${EDGE_HOST:-}"
local env_file="${FACTORY_ROOT}/.env"
# Determine edge host (flag > env var > default)
local edge_host="${EDGE_HOST:-edge.disinto.ai}"
shift || true
case "$subcmd" in
register)
local project="${1:-}"
local env_file="${FACTORY_ROOT}/.env"
# Parse flags
while [ $# -gt 0 ]; do
case "$1" in
--edge-host)
edge_host="$2"
shift 2
;;
*)
if [ -z "$project" ]; then
project="$1"
fi
shift
;;
esac
done
if [ -z "$project" ]; then
echo "Error: project name required" >&2
echo "Usage: disinto edge register [project] [--edge-host <fqdn>]" >&2
exit 1
fi
# Validate project name
if ! [[ "$project" =~ ^[a-zA-Z0-9_-]+$ ]]; then
echo "Error: invalid project name (use alphanumeric, hyphens, underscores)" >&2
exit 1
fi
# Determine edge host (flag > env > default)
if [ -z "$edge_host" ]; then
edge_host="${EDGE_HOST:-edge.disinto.ai}"
fi
# Check for tunnel keypair
local secrets_dir="${FACTORY_ROOT}/secrets"
local tunnel_key="${secrets_dir}/tunnel_key"
local tunnel_pubkey="${tunnel_key}.pub"
if [ ! -f "$tunnel_pubkey" ]; then
echo "Generating tunnel keypair..."
mkdir -p "$secrets_dir"
chmod 700 "$secrets_dir"
ssh-keygen -t ed25519 -f "$tunnel_key" -N "" -C "edge-tunnel@${project}" 2>/dev/null
chmod 600 "$tunnel_key" "$tunnel_pubkey"
echo "Generated: ${tunnel_pubkey}"
fi
# Read pubkey (single line, remove trailing newline)
local pubkey
pubkey=$(tr -d '\n' < "$tunnel_pubkey")
# SSH to edge host and register
echo "Registering tunnel for ${project} on ${edge_host}..."
local response
response=$(ssh -o StrictHostKeyChecking=no -o BatchMode=yes \
"disinto-register@${edge_host}" \
"register ${project} ${pubkey}" 2>&1) || {
echo "Error: failed to register tunnel" >&2
echo "Response: ${response}" >&2
exit 1
}
# Parse response and write to .env
local port fqdn
port=$(echo "$response" | jq -r '.port // empty' 2>/dev/null) || port=""
fqdn=$(echo "$response" | jq -r '.fqdn // empty' 2>/dev/null) || fqdn=""
if [ -z "$port" ] || [ -z "$fqdn" ]; then
echo "Error: invalid response from edge host" >&2
echo "Response: ${response}" >&2
exit 1
fi
# Write to .env
echo "EDGE_TUNNEL_HOST=${edge_host}" >> "$env_file"
echo "EDGE_TUNNEL_PORT=${port}" >> "$env_file"
echo "EDGE_TUNNEL_FQDN=${fqdn}" >> "$env_file"
echo "Registered: ${project}"
echo " Port: ${port}"
echo " FQDN: ${fqdn}"
echo " Saved to: ${env_file}"
;;
deregister)
local project="${1:-}"
# Parse flags
while [ $# -gt 0 ]; do
case "$1" in
--edge-host)
edge_host="$2"
shift 2
;;
*)
if [ -z "$project" ]; then
project="$1"
fi
shift
;;
esac
done
if [ -z "$project" ]; then
echo "Error: project name required" >&2
echo "Usage: disinto edge deregister <project> [--edge-host <fqdn>]" >&2
exit 1
fi
# Determine edge host
if [ -z "$edge_host" ]; then
edge_host="${EDGE_HOST:-edge.disinto.ai}"
fi
# SSH to edge host and deregister
echo "Deregistering tunnel for ${project} on ${edge_host}..."
local response
response=$(ssh -o StrictHostKeyChecking=no -o BatchMode=yes \
"disinto-register@${edge_host}" \
"deregister ${project}" 2>&1) || {
echo "Error: failed to deregister tunnel" >&2
echo "Response: ${response}" >&2
exit 1
}
# Remove from .env if present
if [ -f "$env_file" ]; then
local tmp_env
tmp_env=$(mktemp)
grep -v "^EDGE_TUNNEL_HOST=" "$env_file" > "$tmp_env" 2>/dev/null || true
grep -v "^EDGE_TUNNEL_PORT=" "$env_file" >> "$tmp_env" 2>/dev/null || true
grep -v "^EDGE_TUNNEL_FQDN=" "$env_file" >> "$tmp_env" 2>/dev/null || true
mv "$tmp_env" "$env_file"
fi
echo "Deregistered: ${project}"
;;
status)
# Parse flags
while [ $# -gt 0 ]; do
case "$1" in
--edge-host)
edge_host="$2"
shift 2
;;
*)
shift
;;
esac
done
# Determine edge host
if [ -z "$edge_host" ]; then
edge_host="${EDGE_HOST:-edge.disinto.ai}"
fi
# SSH to edge host and get status
echo "Checking tunnel status on ${edge_host}..."
local response
response=$(ssh -o StrictHostKeyChecking=no -o BatchMode=yes \
"disinto-register@${edge_host}" \
"list" 2>&1) || {
echo "Error: failed to get status" >&2
echo "Response: ${response}" >&2
exit 1
}
# Parse and display
local tunnels
tunnels=$(echo "$response" | jq -r '.tunnels // [] | length' 2>/dev/null) || tunnels="0"
if [ "$tunnels" = "0" ]; then
echo "No tunnels registered"
else
echo "Registered tunnels:"
echo "$response" | jq -r '.tunnels[] | " \(.name): port=\(.port) fqdn=\(.fqdn)"'
fi
;;
*)
cat <<EOF >&2
Usage: disinto edge <verb> [options]
Manage edge tunnel registrations:
register [project] Register a new tunnel (generates keypair if needed)
deregister <project> Remove a tunnel registration
status Show registered tunnels
Options:
--edge-host <fqdn> Edge host FQDN (default: edge.disinto.ai or EDGE_HOST env)
Examples:
disinto edge register myproject
disinto edge register myproject --edge-host custom.example.com
disinto edge deregister myproject
disinto edge status
EOF
exit 1
;;
esac
}
# ── Main dispatch ────────────────────────────────────────────────────────────
case "${1:-}" in
@ -1628,6 +1858,7 @@ case "${1:-}" in
release) shift; disinto_release "$@" ;;
hire-an-agent) shift; disinto_hire_an_agent "$@" ;;
agent) shift; disinto_agent "$@" ;;
edge) shift; disinto_edge "$@" ;;
-h|--help) usage ;;
*) usage ;;
esac