KeiSeiKit-1.0/_primitives/provision-hetzner.sh
Denis Parfionovich 1d958b3587
Some checks are pending
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / preflight (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / vps-smoke (push) Waiting to run
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:frustration-matrix,kei-frustration-loop,kei-skill-importer,kei-projects-index,kei-projects-watcher,kei-gdrive-import,kei-leak-matrix,kei-skills,kei-gateway,kei-cron-scheduler,kei-export-trajectories,kei-backend-daytona,kei-d… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-compute-baremetal,kei-compute-vultr,kei-compute-linode,kei-compute-digitalocean,kei-svc-systemd,kei-llm-bridge-mlx name:hosted-sleep-compute]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-diff,kei-scheduler,kei-watch,kei-prune,kei-discover,kei-brain-view,kei-hibernate,kei-ledger-sign,kei-fork name:wave13-15]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-git-gitea,kei-git-forgejo,kei-git-gitlab,kei-git-bitbucket,kei-memory-sled,kei-memory-redis,kei-memory-postgres,kei-memory-sqlite,kei-auth-google,kei-auth-apple,kei-auth-magiclink,kei-auth-webauthn,kei-notify-slack,kei-n… (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-ledger,kei-migrate,kei-changelog,kei-memory,kei-store,kei-conflict-scan,kei-refactor-engine,kei-graph-check,kei-shared,kei-dna-index,kei-pet name:core]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-machine-probe,kei-llm-ollama,kei-llm-llamacpp,kei-llm-mlx,kei-llm-router,kei-model name:llm-stack]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:kei-router,kei-sage,kei-task,kei-chat-store,kei-crossdomain,kei-search-core,kei-content-store,kei-social-store,kei-curator,kei-auth,kei-artifact name:mcp-lbm]) (push) Blocked by required conditions
CI (Forgejo Actions — self-hosted runner on Mac, host mode) / rust-primitives (map[crates:keisei,kei-forge,kei-runtime,kei-runtime-core,kei-atom-discovery,kei-agent-runtime,kei-capability,kei-provision,kei-entity-store,kei-pipe,kei-cache,kei-spawn,kei-replay name:atom-substrate]) (push) Blocked by required conditions
fix(security): patent-leak + classical-safety audit fixes
PATENT-LEAK (HIGH):
- hooks/no-python-without-approval.sh: genesis-verify пример → my-project
- docs/encyclopedia/rust-crates-H-N.md: убран термин «Genesis IP, ITAR»
PATENT-LEAK (MEDIUM):
- CHANGELOG: project-vortex → reduced scope
- _blocks/registries (submodule bump): убраны имена приватных
  project-specialists из комментария agent-profiles.toml
- docs/encyclopedia/skills-and-agents.md: ML/RL/CfC → ML/RL

CLASSICAL-SAFETY (MEDIUM):
- install/lib-preflight.sh: eval "$version_cmd" → bash -c "..."
  (защита от инъекции если providers.toml расширят)
- _primitives/provision-{vultr,hetzner}.sh: /tmp/$$ → mktemp
  (устраняет symlink TOCTOU race)
- web-install.sh: chmod 600 + umask 077 на ~/.keisei-install.log
  (Forgejo admin creds + токены в логе)
- scripts/regen-counts.sh: eval "$1" → bash -c

NOT FIXED (требуют действий юзера):
- HIGH: @keisei scope не зарегистрирован на npmjs.org — typosquat
  возможен пока не задан NPM_TOKEN и не сделан publish
- HIGH: install.keisei.app DNS не настроен — DNS-hijack возможен
- LOW: parfionovich@keilab.io в SECURITY.md, plugin.json, ~40 Cargo
  файлах — intentional contact, оставлен

Локальный git author установлен на parfionovich@keilab.io вместо
parfionovichd@icloud.com (только для будущих коммитов в этом репо).
2026-05-18 12:05:25 +08:00

172 lines
5.8 KiB
Bash
Executable file

#!/usr/bin/env bash
# [SUPERSEDED v0.24] Prefer the unified `kei-provision hetzner <cmd>` Rust
# binary (_primitives/_rust/kei-provision). This shell remains for deployed
# scripts that haven't migrated yet; functionally identical, retained only
# so existing call sites keep working until the migration sweep lands.
#
# provision-hetzner.sh — idempotent Hetzner Cloud server provisioning.
# Wraps the `hcloud` CLI. Install path:
# $HOME/.claude/agents/_primitives/provision-hetzner.sh
#
# USAGE
# provision-hetzner.sh create <name> [--type cx22|cax11] [--location fsn1] \
# [--image debian-12] [--ssh-key <id>] \
# [--firewall <name>] [--user-data <file>]
# provision-hetzner.sh status <name>
# provision-hetzner.sh destroy <name> [--force]
# provision-hetzner.sh list
#
# ENV (RULE 0.8 — secrets single source)
# HCLOUD_TOKEN — Hetzner API token (REQUIRED). Source:
# $(grep ^HCLOUD_TOKEN ~/.claude/secrets/.env | cut -d= -f2)
#
# EXIT
# 0 ok
# 1 usage / missing args / missing deps / unknown command
# 2 hcloud API error (non-idempotent path — inspect stderr)
#
# IDEMPOTENCY
# `create <name>` on an existing server prints its IP + exits 0.
# `destroy <name>` on a missing server exits 0 (nothing to do).
set -euo pipefail
log() { printf '[%s] [provision-hetzner] %s\n' "$(date '+%H:%M:%S')" "$*" >&2; }
die() { log "ERROR: $*"; exit "${2:-2}"; }
check_deps() {
command -v hcloud >/dev/null 2>&1 || \
die "hcloud CLI missing. Install: brew install hcloud (macOS) | https://github.com/hetznercloud/cli/releases" 1
command -v jq >/dev/null 2>&1 || die "jq missing. brew install jq" 1
[ -n "${HCLOUD_TOKEN:-}" ] || die "HCLOUD_TOKEN not set. Source ~/.claude/secrets/.env first." 1
}
# Print server JSON if it exists, empty string otherwise. Never fails.
server_json() {
local name="$1"
hcloud server describe "$name" -o json 2>/dev/null || true
}
cmd_list() {
check_deps
hcloud server list -o 'columns=id,name,status,ipv4,location,server_type,created'
}
cmd_status() {
check_deps
local name="${1:-}"; [ -n "$name" ] || die "status: <name> required" 1
local json; json=$(server_json "$name")
if [ -z "$json" ]; then
echo "absent"
return 0
fi
printf 'name=%s\nstatus=%s\nipv4=%s\nlocation=%s\ntype=%s\n' \
"$(jq -r .name <<<"$json")" \
"$(jq -r .status <<<"$json")" \
"$(jq -r '.public_net.ipv4.ip // "-"' <<<"$json")" \
"$(jq -r .datacenter.location.name <<<"$json")" \
"$(jq -r .server_type.name <<<"$json")"
}
cmd_create() {
check_deps
local name="${1:-}"; shift || true
[ -n "$name" ] || die "create: <name> required" 1
local type="cx22" location="fsn1" image="debian-12"
local ssh_key="" firewall="" user_data=""
while [ $# -gt 0 ]; do
case "$1" in
--type) type="$2"; shift 2 ;;
--location) location="$2"; shift 2 ;;
--image) image="$2"; shift 2 ;;
--ssh-key) ssh_key="$2"; shift 2 ;;
--firewall) firewall="$2"; shift 2 ;;
--user-data) user_data="$2"; shift 2 ;;
*) die "create: unknown flag '$1'" 1 ;;
esac
done
# Idempotent fast-path: if the server already exists, just print its IP.
local existing; existing=$(server_json "$name")
if [ -n "$existing" ]; then
local ip; ip=$(jq -r '.public_net.ipv4.ip // "-"' <<<"$existing")
log "server '$name' already exists → $ip (no-op)"
echo "$ip"
return 0
fi
local args=(server create
--name "$name"
--type "$type"
--image "$image"
--location "$location"
--label "project=kei"
)
[ -n "$ssh_key" ] && args+=(--ssh-key "$ssh_key")
[ -n "$firewall" ] && args+=(--firewall "$firewall")
[ -n "$user_data" ] && { [ -r "$user_data" ] || die "user-data not readable: $user_data" 1; args+=(--user-data-from-file "$user_data"); }
log "creating '$name' ($type @ $location, image=$image)…"
# mktemp вместо /tmp/$$ — устраняет symlink-race TOCTOU (security MEDIUM
# audit 2026-05-18).
local tmpf; tmpf=$(mktemp /tmp/provision-hetzner.XXXXXX.json) || return 1
hcloud "${args[@]}" -o json >"$tmpf"
local ip; ip=$(jq -r '.server.public_net.ipv4.ip' "$tmpf")
rm -f "$tmpf"
[ "$ip" != "null" ] && [ -n "$ip" ] || die "create returned no IPv4 (check stderr)"
log "created '$name' → $ip"
echo "$ip"
}
cmd_destroy() {
check_deps
local name="${1:-}"; shift || true
[ -n "$name" ] || die "destroy: <name> required" 1
local force=""
[ "${1:-}" = "--force" ] && force=1
local existing; existing=$(server_json "$name")
if [ -z "$existing" ]; then
log "server '$name' absent (no-op)"
return 0
fi
if [ -z "$force" ]; then
printf 'Destroy server "%s"? [y/N] ' "$name" >&2
read -r ans
[ "$ans" = "y" ] || [ "$ans" = "Y" ] || { log "aborted"; return 1; }
fi
log "deleting '$name'…"
hcloud server delete "$name" >&2
log "deleted '$name'"
}
main() {
local cmd="${1:-}"; shift || true
case "$cmd" in
create) cmd_create "$@" ;;
destroy) cmd_destroy "$@" ;;
status) cmd_status "$@" ;;
list) cmd_list "$@" ;;
-h|--help|"") cat <<EOF >&2
provision-hetzner.sh — idempotent Hetzner Cloud server provisioning.
USAGE
provision-hetzner.sh create <name> [--type cx22|cax11] [--location fsn1] \\
[--image debian-12] [--ssh-key <id>] \\
[--firewall <name>] [--user-data <file>]
provision-hetzner.sh status <name>
provision-hetzner.sh destroy <name> [--force]
provision-hetzner.sh list
ENV
HCLOUD_TOKEN (required) — load via: source ~/.claude/secrets/.env
EOF
[ "$cmd" = "-h" ] || [ "$cmd" = "--help" ] && exit 0 || exit 1
;;
*) die "unknown command '$cmd'. Run --help." 1 ;;
esac
}
main "$@"