fireclaw 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +117 -109
- package/bin/fireclaw +11 -3
- package/bin/vm-common.sh +244 -37
- package/bin/vm-ctl +182 -37
- package/bin/vm-provision +152 -18
- package/bin/vm-setup +237 -94
- package/package.json +5 -4
- package/scripts/provision-guest.sh +134 -25
package/bin/vm-setup
CHANGED
|
@@ -8,13 +8,15 @@ CMD_NAME="${VM_SETUP_CMD_NAME:-fireclaw setup}"
|
|
|
8
8
|
|
|
9
9
|
usage() {
|
|
10
10
|
cat <<EOF
|
|
11
|
-
Usage: $CMD_NAME --instance <id>
|
|
11
|
+
Usage: $CMD_NAME --instance <id> [options]
|
|
12
12
|
|
|
13
13
|
Options:
|
|
14
|
-
--telegram-
|
|
15
|
-
--
|
|
14
|
+
--telegram-token <token> (optional; omit for a local-only gateway with Telegram disabled)
|
|
15
|
+
--telegram-users <csv> (required with --telegram-token)
|
|
16
|
+
--model <id> (default: openai/gpt-5.5)
|
|
16
17
|
--skills <csv> (default: github,tmux,coding-agent,session-logs,skill-creator)
|
|
17
18
|
--openclaw-image <image>
|
|
19
|
+
--host-port <n> (default: first free port above BASE_PORT)
|
|
18
20
|
--vm-vcpu <n> (default: 4)
|
|
19
21
|
--vm-mem-mib <n> (default: 8192)
|
|
20
22
|
--disk-size <size> (default: 40G)
|
|
@@ -30,16 +32,48 @@ Options:
|
|
|
30
32
|
EOF
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
require_option_value() {
|
|
36
|
+
local opt="$1"
|
|
37
|
+
local value="${2-}"
|
|
38
|
+
[[ -n "$value" && "$value" != --* ]] || die "Missing value for $opt"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
validate_no_newline() {
|
|
42
|
+
local name="$1"
|
|
43
|
+
local value="$2"
|
|
44
|
+
[[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] || die "$name must not contain newlines"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
validate_positive_int() {
|
|
48
|
+
local name="$1"
|
|
49
|
+
local value="$2"
|
|
50
|
+
[[ "$value" =~ ^[0-9]+$ ]] || die "$name must be numeric"
|
|
51
|
+
(( 10#$value > 0 )) || die "$name must be greater than zero"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
csv_values() {
|
|
55
|
+
printf '%s' "$1" | tr ',' '\n' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//; /^$/d'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
write_kv() {
|
|
59
|
+
local file="$1"
|
|
60
|
+
local key="$2"
|
|
61
|
+
local value="$3"
|
|
62
|
+
validate_no_newline "$key" "$value"
|
|
63
|
+
printf "%s=%s\n" "$key" "$value" >> "$file"
|
|
64
|
+
}
|
|
65
|
+
|
|
33
66
|
INSTANCE=""
|
|
34
67
|
TELEGRAM_TOKEN=""
|
|
35
68
|
TELEGRAM_USERS=""
|
|
36
|
-
MODEL="${MODEL:-
|
|
69
|
+
MODEL="${MODEL:-openai/gpt-5.5}"
|
|
37
70
|
SKILLS="${SKILLS:-github,tmux,coding-agent,session-logs,skill-creator}"
|
|
38
71
|
OPENCLAW_IMAGE="$OPENCLAW_IMAGE_DEFAULT"
|
|
39
72
|
VM_VCPU="${VM_VCPU:-4}"
|
|
40
73
|
VM_MEM_MIB="${VM_MEM_MIB:-8192}"
|
|
41
74
|
DISK_SIZE="${DISK_SIZE:-40G}"
|
|
42
75
|
API_SOCK="${API_SOCK:-}"
|
|
76
|
+
HOST_PORT_OVERRIDE=""
|
|
43
77
|
BASE_IMAGES_DIR="${BASE_IMAGES_DIR:-/srv/firecracker/base/images}"
|
|
44
78
|
BASE_KERNEL="${BASE_KERNEL:-$BASE_IMAGES_DIR/vmlinux}"
|
|
45
79
|
BASE_ROOTFS="${BASE_ROOTFS:-$BASE_IMAGES_DIR/rootfs.ext4}"
|
|
@@ -49,25 +83,63 @@ SKIP_BROWSER_INSTALL="false"
|
|
|
49
83
|
ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-}"
|
|
50
84
|
OPENAI_API_KEY="${OPENAI_API_KEY:-}"
|
|
51
85
|
MINIMAX_API_KEY="${MINIMAX_API_KEY:-}"
|
|
86
|
+
SETUP_COMPLETE="false"
|
|
87
|
+
CLEANUP_ARMED="false"
|
|
88
|
+
|
|
89
|
+
cleanup_failed_setup() {
|
|
90
|
+
local rc="$1"
|
|
91
|
+
[[ "$rc" -eq 0 || "$SETUP_COMPLETE" == "true" ]] && return 0
|
|
92
|
+
[[ "$CLEANUP_ARMED" == "true" ]] || return 0
|
|
93
|
+
set +e
|
|
94
|
+
if [[ -n "${INSTANCE:-}" && "$INSTANCE" =~ ^[a-z0-9_-]+$ ]]; then
|
|
95
|
+
warn "Setup failed; cleaning up partial instance: $INSTANCE"
|
|
96
|
+
local vm_svc proxy_svc
|
|
97
|
+
vm_svc="$(vm_service "$INSTANCE")"
|
|
98
|
+
proxy_svc="$(proxy_service "$INSTANCE")"
|
|
99
|
+
systemctl stop "$proxy_svc" 2>/dev/null || true
|
|
100
|
+
systemctl stop "$vm_svc" 2>/dev/null || true
|
|
101
|
+
systemctl disable "$proxy_svc" 2>/dev/null || true
|
|
102
|
+
systemctl disable "$vm_svc" 2>/dev/null || true
|
|
103
|
+
rm -f "/etc/systemd/system/$proxy_svc" "/etc/systemd/system/$vm_svc"
|
|
104
|
+
systemctl daemon-reload 2>/dev/null || true
|
|
105
|
+
systemctl reset-failed "$proxy_svc" "$vm_svc" 2>/dev/null || true
|
|
106
|
+
if [[ -n "${VM_TAP:-}" ]]; then
|
|
107
|
+
ip link set "$VM_TAP" down 2>/dev/null || true
|
|
108
|
+
ip link del "$VM_TAP" 2>/dev/null || true
|
|
109
|
+
fi
|
|
110
|
+
if [[ -n "${API_SOCK:-}" ]]; then
|
|
111
|
+
rm -f "$API_SOCK"
|
|
112
|
+
fi
|
|
113
|
+
if [[ -n "${inst_dir:-}" ]]; then
|
|
114
|
+
rm -rf "$inst_dir"
|
|
115
|
+
fi
|
|
116
|
+
if [[ -n "${fc_dir:-}" ]]; then
|
|
117
|
+
rm -rf "$fc_dir"
|
|
118
|
+
fi
|
|
119
|
+
fi
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
trap 'rc=$?; cleanup_failed_setup "$rc"; exit "$rc"' EXIT
|
|
52
123
|
|
|
53
124
|
while [[ $# -gt 0 ]]; do
|
|
54
125
|
case "$1" in
|
|
55
|
-
--instance) INSTANCE="$2"; shift 2 ;;
|
|
56
|
-
--telegram-token) TELEGRAM_TOKEN="$2"; shift 2 ;;
|
|
57
|
-
--telegram-users) TELEGRAM_USERS="$2"; shift 2 ;;
|
|
58
|
-
--model) MODEL="$2"; shift 2 ;;
|
|
59
|
-
--skills) SKILLS="$2"; shift 2 ;;
|
|
60
|
-
--openclaw-image) OPENCLAW_IMAGE="$2"; shift 2 ;;
|
|
61
|
-
--
|
|
62
|
-
--vm-
|
|
63
|
-
--
|
|
64
|
-
--
|
|
65
|
-
--
|
|
66
|
-
--base-
|
|
67
|
-
--base-
|
|
68
|
-
--
|
|
69
|
-
--
|
|
70
|
-
--
|
|
126
|
+
--instance) require_option_value "$1" "${2-}"; INSTANCE="$2"; shift 2 ;;
|
|
127
|
+
--telegram-token) require_option_value "$1" "${2-}"; TELEGRAM_TOKEN="$2"; shift 2 ;;
|
|
128
|
+
--telegram-users) require_option_value "$1" "${2-}"; TELEGRAM_USERS="$2"; shift 2 ;;
|
|
129
|
+
--model) require_option_value "$1" "${2-}"; MODEL="$2"; shift 2 ;;
|
|
130
|
+
--skills) require_option_value "$1" "${2-}"; SKILLS="$2"; shift 2 ;;
|
|
131
|
+
--openclaw-image) require_option_value "$1" "${2-}"; OPENCLAW_IMAGE="$2"; shift 2 ;;
|
|
132
|
+
--host-port) require_option_value "$1" "${2-}"; HOST_PORT_OVERRIDE="$2"; shift 2 ;;
|
|
133
|
+
--vm-vcpu) require_option_value "$1" "${2-}"; VM_VCPU="$2"; shift 2 ;;
|
|
134
|
+
--vm-mem-mib) require_option_value "$1" "${2-}"; VM_MEM_MIB="$2"; shift 2 ;;
|
|
135
|
+
--disk-size) require_option_value "$1" "${2-}"; DISK_SIZE="$2"; shift 2 ;;
|
|
136
|
+
--api-sock) require_option_value "$1" "${2-}"; API_SOCK="$2"; shift 2 ;;
|
|
137
|
+
--base-kernel) require_option_value "$1" "${2-}"; BASE_KERNEL="$2"; shift 2 ;;
|
|
138
|
+
--base-rootfs) require_option_value "$1" "${2-}"; BASE_ROOTFS="$2"; shift 2 ;;
|
|
139
|
+
--base-initrd) require_option_value "$1" "${2-}"; BASE_INITRD="$2"; shift 2 ;;
|
|
140
|
+
--anthropic-api-key) require_option_value "$1" "${2-}"; ANTHROPIC_API_KEY="$2"; shift 2 ;;
|
|
141
|
+
--openai-api-key) require_option_value "$1" "${2-}"; OPENAI_API_KEY="$2"; shift 2 ;;
|
|
142
|
+
--minimax-api-key) require_option_value "$1" "${2-}"; MINIMAX_API_KEY="$2"; shift 2 ;;
|
|
71
143
|
--skip-browser-install) SKIP_BROWSER_INSTALL="true"; shift ;;
|
|
72
144
|
-h|--help) usage; exit 0 ;;
|
|
73
145
|
*) die "Unknown option: $1" ;;
|
|
@@ -75,13 +147,47 @@ while [[ $# -gt 0 ]]; do
|
|
|
75
147
|
done
|
|
76
148
|
|
|
77
149
|
[[ -n "$INSTANCE" ]] || die "Missing --instance"
|
|
78
|
-
[[ -n "$TELEGRAM_TOKEN" ]]
|
|
150
|
+
if [[ -n "$TELEGRAM_TOKEN" ]]; then
|
|
151
|
+
[[ -n "$TELEGRAM_USERS" ]] || die "Missing --telegram-users (required with --telegram-token)"
|
|
152
|
+
[[ -n "$(csv_values "$TELEGRAM_USERS")" ]] || die "--telegram-users must include at least one allowed Telegram user ID"
|
|
153
|
+
elif [[ -n "$TELEGRAM_USERS" ]]; then
|
|
154
|
+
die "--telegram-users requires --telegram-token"
|
|
155
|
+
fi
|
|
79
156
|
validate_instance_id "$INSTANCE"
|
|
157
|
+
validate_positive_int "VM_VCPU" "$VM_VCPU"
|
|
158
|
+
validate_positive_int "VM_MEM_MIB" "$VM_MEM_MIB"
|
|
159
|
+
validate_no_newline "TELEGRAM_TOKEN" "$TELEGRAM_TOKEN"
|
|
160
|
+
validate_no_newline "TELEGRAM_USERS" "$TELEGRAM_USERS"
|
|
161
|
+
validate_no_newline "MODEL" "$MODEL"
|
|
162
|
+
validate_no_newline "SKILLS" "$SKILLS"
|
|
163
|
+
validate_no_newline "OPENCLAW_IMAGE" "$OPENCLAW_IMAGE"
|
|
164
|
+
validate_no_newline "DISK_SIZE" "$DISK_SIZE"
|
|
165
|
+
validate_no_newline "API_SOCK" "$API_SOCK"
|
|
166
|
+
validate_no_newline "BASE_KERNEL" "$BASE_KERNEL"
|
|
167
|
+
validate_no_newline "BASE_ROOTFS" "$BASE_ROOTFS"
|
|
168
|
+
validate_no_newline "BASE_INITRD" "$BASE_INITRD"
|
|
169
|
+
validate_no_newline "ANTHROPIC_API_KEY" "$ANTHROPIC_API_KEY"
|
|
170
|
+
validate_no_newline "OPENAI_API_KEY" "$OPENAI_API_KEY"
|
|
171
|
+
validate_no_newline "MINIMAX_API_KEY" "$MINIMAX_API_KEY"
|
|
172
|
+
if [[ -n "$HOST_PORT_OVERRIDE" ]]; then
|
|
173
|
+
validate_no_newline "HOST_PORT" "$HOST_PORT_OVERRIDE"
|
|
174
|
+
fi
|
|
175
|
+
VM_VCPU=$((10#$VM_VCPU))
|
|
176
|
+
VM_MEM_MIB=$((10#$VM_MEM_MIB))
|
|
177
|
+
require_model_provider_key "$MODEL"
|
|
80
178
|
|
|
81
179
|
require_root
|
|
82
|
-
for c in firecracker systemctl ip iptables openssl jq cloud-localds ssh scp socat curl qemu-img; do
|
|
180
|
+
for c in firecracker systemctl ip bridge iptables openssl jq cloud-localds ssh scp socat curl qemu-img install flock; do
|
|
83
181
|
require_cmd "$c"
|
|
84
182
|
done
|
|
183
|
+
FIRECRACKER_BIN="$(command -v firecracker)"
|
|
184
|
+
[[ "$FIRECRACKER_BIN" = /* ]] || die "firecracker must resolve to an absolute executable path"
|
|
185
|
+
[[ -e /dev/kvm && -r /dev/kvm && -w /dev/kvm ]] || die "/dev/kvm is not available (KVM is required to run Firecracker VMs)"
|
|
186
|
+
|
|
187
|
+
mem_avail_mib="$(awk '/^MemAvailable:/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
|
|
188
|
+
if (( mem_avail_mib > 0 && VM_MEM_MIB > mem_avail_mib )); then
|
|
189
|
+
warn "Requested VM memory (${VM_MEM_MIB} MiB) exceeds host MemAvailable (${mem_avail_mib} MiB); the VM may OOM the host"
|
|
190
|
+
fi
|
|
85
191
|
|
|
86
192
|
[[ -f "$BASE_KERNEL" ]] || die "Kernel not found: $BASE_KERNEL"
|
|
87
193
|
[[ -f "$BASE_ROOTFS" ]] || die "Rootfs not found: $BASE_ROOTFS"
|
|
@@ -99,11 +205,30 @@ fc_dir="$(fc_instance_dir "$INSTANCE")"
|
|
|
99
205
|
if [[ -z "$API_SOCK" ]]; then
|
|
100
206
|
API_SOCK="$fc_dir/firecracker.socket"
|
|
101
207
|
fi
|
|
208
|
+
VM_SVC="$(vm_service "$INSTANCE")"
|
|
209
|
+
PROXY_SVC="$(proxy_service "$INSTANCE")"
|
|
102
210
|
[[ "$API_SOCK" = /* ]] || die "--api-sock must be an absolute path"
|
|
103
211
|
[[ ! -e "$inst_dir" ]] || die "Instance already exists: $inst_dir"
|
|
104
|
-
|
|
105
|
-
|
|
212
|
+
[[ ! -e "$fc_dir" ]] || die "Firecracker assets already exist: $fc_dir"
|
|
213
|
+
[[ ! -e "$API_SOCK" ]] || die "Firecracker API socket already exists: $API_SOCK"
|
|
214
|
+
[[ ! -e "/etc/systemd/system/$VM_SVC" ]] || die "VM unit already exists: /etc/systemd/system/$VM_SVC"
|
|
215
|
+
[[ ! -e "/etc/systemd/system/$PROXY_SVC" ]] || die "Proxy unit already exists: /etc/systemd/system/$PROXY_SVC"
|
|
216
|
+
CLEANUP_ARMED="true"
|
|
217
|
+
|
|
218
|
+
# Hold the allocation lock until the reservation is visible in the .env file,
|
|
219
|
+
# so concurrent setups cannot pick the same port/IP.
|
|
220
|
+
exec {ALLOC_LOCK_FD}>"$STATE_ROOT/.alloc.lock"
|
|
221
|
+
flock "$ALLOC_LOCK_FD"
|
|
222
|
+
|
|
223
|
+
if [[ -n "$HOST_PORT_OVERRIDE" ]]; then
|
|
224
|
+
ensure_host_port_available "$HOST_PORT_OVERRIDE"
|
|
225
|
+
HOST_PORT="$((10#$HOST_PORT_OVERRIDE))"
|
|
226
|
+
else
|
|
227
|
+
HOST_PORT="$(next_port)"
|
|
228
|
+
fi
|
|
106
229
|
VM_IP="$(next_ip)"
|
|
230
|
+
SUBNET_MASK_BITS="$(subnet_mask_bits)"
|
|
231
|
+
BRIDGE_GATEWAY_IP="$(bridge_gateway_ip)"
|
|
107
232
|
VM_OCTET="${VM_IP##*.}"
|
|
108
233
|
SHORT_ID="$(echo "$INSTANCE" | tr -cd 'a-z0-9' | cut -c1-6)"
|
|
109
234
|
[[ -n "$SHORT_ID" ]] || SHORT_ID="vm"
|
|
@@ -111,40 +236,50 @@ VM_TAP="t${SHORT_ID}${VM_OCTET}"
|
|
|
111
236
|
VM_MAC="$(printf '06:fc:00:10:00:%02x' "$VM_OCTET")"
|
|
112
237
|
GATEWAY_TOKEN="$(openssl rand -hex 32)"
|
|
113
238
|
|
|
239
|
+
if ip link show "$VM_TAP" >/dev/null 2>&1; then
|
|
240
|
+
die "TAP device already exists: $VM_TAP (leftover from a previous instance? remove it with: sudo ip link del $VM_TAP)"
|
|
241
|
+
fi
|
|
242
|
+
|
|
114
243
|
mkdir -p "$inst_dir/config" "$inst_dir/workspace"
|
|
244
|
+
chmod 700 "$inst_dir" "$inst_dir/config" "$inst_dir/workspace"
|
|
115
245
|
mkdir -p "$fc_dir/images" "$fc_dir/config" "$fc_dir/logs"
|
|
246
|
+
chmod 700 "$fc_dir" "$fc_dir/images" "$fc_dir/config" "$fc_dir/logs"
|
|
116
247
|
mkdir -p "$(dirname "$API_SOCK")"
|
|
117
248
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
249
|
+
ENV_FILE="$(instance_env "$INSTANCE")"
|
|
250
|
+
install -m 600 /dev/null "$ENV_FILE"
|
|
251
|
+
write_kv "$ENV_FILE" FIRECLAW_STATE_FORMAT "plain-v1"
|
|
252
|
+
write_kv "$ENV_FILE" INSTANCE_ID "$INSTANCE"
|
|
253
|
+
write_kv "$ENV_FILE" HOST_PORT "$HOST_PORT"
|
|
254
|
+
write_kv "$ENV_FILE" VM_IP "$VM_IP"
|
|
255
|
+
write_kv "$ENV_FILE" VM_TAP "$VM_TAP"
|
|
256
|
+
write_kv "$ENV_FILE" VM_MAC "$VM_MAC"
|
|
257
|
+
write_kv "$ENV_FILE" GATEWAY_TOKEN "$GATEWAY_TOKEN"
|
|
258
|
+
write_kv "$ENV_FILE" MODEL "$MODEL"
|
|
259
|
+
write_kv "$ENV_FILE" SKILLS "$SKILLS"
|
|
260
|
+
write_kv "$ENV_FILE" TELEGRAM_USERS "$TELEGRAM_USERS"
|
|
261
|
+
write_kv "$ENV_FILE" OPENCLAW_IMAGE "$OPENCLAW_IMAGE"
|
|
262
|
+
write_kv "$ENV_FILE" VM_VCPU "$VM_VCPU"
|
|
263
|
+
write_kv "$ENV_FILE" VM_MEM_MIB "$VM_MEM_MIB"
|
|
264
|
+
write_kv "$ENV_FILE" DISK_SIZE "$DISK_SIZE"
|
|
265
|
+
write_kv "$ENV_FILE" API_SOCK "$API_SOCK"
|
|
266
|
+
write_kv "$ENV_FILE" SSH_KEY_PATH "$SSH_KEY_PATH"
|
|
267
|
+
write_kv "$ENV_FILE" SKIP_BROWSER_INSTALL "$SKIP_BROWSER_INSTALL"
|
|
268
|
+
write_kv "$ENV_FILE" ANTHROPIC_API_KEY "$ANTHROPIC_API_KEY"
|
|
269
|
+
write_kv "$ENV_FILE" OPENAI_API_KEY "$OPENAI_API_KEY"
|
|
270
|
+
write_kv "$ENV_FILE" MINIMAX_API_KEY "$MINIMAX_API_KEY"
|
|
271
|
+
|
|
272
|
+
exec {ALLOC_LOCK_FD}>&-
|
|
273
|
+
|
|
274
|
+
install -m 600 /dev/null "$inst_dir/.token"
|
|
275
|
+
printf '%s\n' "$GATEWAY_TOKEN" > "$inst_dir/.token"
|
|
142
276
|
|
|
143
277
|
log "Preparing VM disk and assets"
|
|
144
278
|
cp --reflink=auto "$BASE_ROOTFS" "$fc_dir/images/rootfs.ext4" 2>/dev/null || cp "$BASE_ROOTFS" "$fc_dir/images/rootfs.ext4"
|
|
279
|
+
chmod 600 "$fc_dir/images/rootfs.ext4"
|
|
145
280
|
if [[ -n "$DISK_SIZE" ]]; then
|
|
146
281
|
log "Resizing rootfs to $DISK_SIZE"
|
|
147
|
-
qemu-img resize "$fc_dir/images/rootfs.ext4" "$DISK_SIZE" >/dev/null
|
|
282
|
+
qemu-img resize -f raw "$fc_dir/images/rootfs.ext4" "$DISK_SIZE" >/dev/null
|
|
148
283
|
fi
|
|
149
284
|
cp "$BASE_KERNEL" "$fc_dir/images/vmlinux"
|
|
150
285
|
|
|
@@ -168,12 +303,8 @@ users:
|
|
|
168
303
|
- $SSH_PUB_KEY
|
|
169
304
|
package_update: true
|
|
170
305
|
packages:
|
|
171
|
-
- docker.io
|
|
172
306
|
- jq
|
|
173
307
|
- curl
|
|
174
|
-
runcmd:
|
|
175
|
-
- [ systemctl, enable, docker ]
|
|
176
|
-
- [ systemctl, start, docker ]
|
|
177
308
|
EOF
|
|
178
309
|
|
|
179
310
|
cat > "$fc_dir/config/meta-data" <<EOF
|
|
@@ -190,10 +321,10 @@ ethernets:
|
|
|
190
321
|
set-name: eth0
|
|
191
322
|
dhcp4: false
|
|
192
323
|
addresses:
|
|
193
|
-
- $VM_IP
|
|
324
|
+
- $VM_IP/$SUBNET_MASK_BITS
|
|
194
325
|
routes:
|
|
195
326
|
- to: 0.0.0.0/0
|
|
196
|
-
via:
|
|
327
|
+
via: $BRIDGE_GATEWAY_IP
|
|
197
328
|
nameservers:
|
|
198
329
|
addresses: [1.1.1.1,8.8.8.8]
|
|
199
330
|
EOF
|
|
@@ -237,21 +368,29 @@ SUBNET_CIDR="$SUBNET_CIDR"
|
|
|
237
368
|
VM_TAP="$VM_TAP"
|
|
238
369
|
API_SOCK="$API_SOCK"
|
|
239
370
|
CONFIG_JSON="$fc_dir/config/vm-config.json"
|
|
371
|
+
FIRECRACKER_BIN="$FIRECRACKER_BIN"
|
|
240
372
|
|
|
241
373
|
if ! ip link show "\$BRIDGE_NAME" >/dev/null 2>&1; then
|
|
242
374
|
ip link add "\$BRIDGE_NAME" type bridge
|
|
243
375
|
fi
|
|
244
|
-
ip addr add "\$BRIDGE_ADDR" dev "\$BRIDGE_NAME"
|
|
376
|
+
ip -4 addr show dev "\$BRIDGE_NAME" | grep -Fq " \$BRIDGE_ADDR " || ip addr add "\$BRIDGE_ADDR" dev "\$BRIDGE_NAME"
|
|
245
377
|
ip link set "\$BRIDGE_NAME" up
|
|
246
378
|
sysctl -w net.ipv4.ip_forward=1 >/dev/null
|
|
247
379
|
iptables -t nat -C POSTROUTING -s "\$SUBNET_CIDR" ! -o "\$BRIDGE_NAME" -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -s "\$SUBNET_CIDR" ! -o "\$BRIDGE_NAME" -j MASQUERADE
|
|
380
|
+
# Defense-in-depth only: matches solely where br_netfilter is enabled.
|
|
381
|
+
iptables -C FORWARD -i "\$BRIDGE_NAME" -o "\$BRIDGE_NAME" -j DROP 2>/dev/null || iptables -I FORWARD 1 -i "\$BRIDGE_NAME" -o "\$BRIDGE_NAME" -j DROP
|
|
248
382
|
|
|
249
|
-
ip
|
|
383
|
+
if ! ip link show "\$VM_TAP" >/dev/null 2>&1; then
|
|
384
|
+
ip tuntap add dev "\$VM_TAP" mode tap user root
|
|
385
|
+
fi
|
|
250
386
|
ip link set "\$VM_TAP" master "\$BRIDGE_NAME"
|
|
387
|
+
# Isolated bridge ports cannot exchange frames (IP or ARP) with each other,
|
|
388
|
+
# only with the bridge/host itself — this is the actual VM-to-VM isolation.
|
|
389
|
+
bridge link set dev "\$VM_TAP" isolated on
|
|
251
390
|
ip link set "\$VM_TAP" up
|
|
252
391
|
|
|
253
392
|
rm -f "\$API_SOCK"
|
|
254
|
-
exec
|
|
393
|
+
exec "\$FIRECRACKER_BIN" --api-sock "\$API_SOCK" --config-file "\$CONFIG_JSON"
|
|
255
394
|
EOF
|
|
256
395
|
chmod +x "$fc_dir/start-vm.sh"
|
|
257
396
|
|
|
@@ -259,14 +398,27 @@ cat > "$fc_dir/stop-vm.sh" <<EOF
|
|
|
259
398
|
#!/usr/bin/env bash
|
|
260
399
|
set -euo pipefail
|
|
261
400
|
VM_TAP="$VM_TAP"
|
|
401
|
+
API_SOCK="$API_SOCK"
|
|
402
|
+
|
|
403
|
+
# Ask the guest to shut down cleanly (systemd maps Ctrl-Alt-Del to a clean
|
|
404
|
+
# reboot, which exits Firecracker) before systemd kills the VMM, so the
|
|
405
|
+
# rootfs ext4 is not torn down mid-write.
|
|
406
|
+
if [[ -S "\$API_SOCK" ]]; then
|
|
407
|
+
if curl -fsS --max-time 2 --unix-socket "\$API_SOCK" -X PUT http://localhost/actions \\
|
|
408
|
+
-H 'Content-Type: application/json' -d '{"action_type": "SendCtrlAltDel"}' >/dev/null 2>&1; then
|
|
409
|
+
for _ in \$(seq 1 20); do
|
|
410
|
+
curl -fsS --max-time 1 --unix-socket "\$API_SOCK" http://localhost/ >/dev/null 2>&1 || break
|
|
411
|
+
sleep 1
|
|
412
|
+
done
|
|
413
|
+
fi
|
|
414
|
+
fi
|
|
415
|
+
|
|
262
416
|
ip link set "\$VM_TAP" down 2>/dev/null || true
|
|
263
417
|
ip link del "\$VM_TAP" 2>/dev/null || true
|
|
418
|
+
rm -f "\$API_SOCK"
|
|
264
419
|
EOF
|
|
265
420
|
chmod +x "$fc_dir/stop-vm.sh"
|
|
266
421
|
|
|
267
|
-
VM_SVC="$(vm_service "$INSTANCE")"
|
|
268
|
-
PROXY_SVC="$(proxy_service "$INSTANCE")"
|
|
269
|
-
|
|
270
422
|
cat > "/etc/systemd/system/$VM_SVC" <<EOF
|
|
271
423
|
[Unit]
|
|
272
424
|
Description=Firecracker VM ($INSTANCE)
|
|
@@ -277,7 +429,8 @@ Wants=network-online.target
|
|
|
277
429
|
Type=simple
|
|
278
430
|
ExecStart=$fc_dir/start-vm.sh
|
|
279
431
|
ExecStop=$fc_dir/stop-vm.sh
|
|
280
|
-
|
|
432
|
+
TimeoutStopSec=45
|
|
433
|
+
Restart=always
|
|
281
434
|
RestartSec=2
|
|
282
435
|
|
|
283
436
|
[Install]
|
|
@@ -289,29 +442,33 @@ cat > "/etc/systemd/system/$PROXY_SVC" <<EOF
|
|
|
289
442
|
Description=Localhost proxy for VM ($INSTANCE)
|
|
290
443
|
After=$VM_SVC
|
|
291
444
|
Requires=$VM_SVC
|
|
445
|
+
PartOf=$VM_SVC
|
|
292
446
|
|
|
293
447
|
[Service]
|
|
294
448
|
Type=simple
|
|
449
|
+
DynamicUser=yes
|
|
295
450
|
ExecStart=/usr/bin/socat TCP-LISTEN:$HOST_PORT,bind=127.0.0.1,reuseaddr,fork TCP:$VM_IP:18789
|
|
451
|
+
SuccessExitStatus=143
|
|
296
452
|
Restart=always
|
|
297
453
|
RestartSec=2
|
|
298
454
|
|
|
299
455
|
[Install]
|
|
300
|
-
WantedBy=multi-user.target
|
|
456
|
+
WantedBy=multi-user.target $VM_SVC
|
|
301
457
|
EOF
|
|
302
458
|
|
|
303
459
|
systemctl daemon-reload
|
|
304
460
|
systemctl enable --now "$VM_SVC"
|
|
305
461
|
|
|
306
462
|
log "Waiting for SSH on $VM_IP"
|
|
307
|
-
if ! wait_for_ssh "$VM_IP" "$SSH_KEY_PATH" 180; then
|
|
463
|
+
if ! wait_for_ssh "$VM_IP" "$SSH_KEY_PATH" 180 "$INSTANCE"; then
|
|
308
464
|
die "VM booted but SSH was not reachable"
|
|
309
465
|
fi
|
|
310
466
|
|
|
311
467
|
PROVISION_VARS="$inst_dir/provision.vars"
|
|
312
|
-
|
|
313
|
-
kv() {
|
|
468
|
+
install -m 600 /dev/null "$PROVISION_VARS"
|
|
469
|
+
kv() { write_kv "$PROVISION_VARS" "$1" "$2"; }
|
|
314
470
|
|
|
471
|
+
kv FIRECLAW_STATE_FORMAT "plain-v1"
|
|
315
472
|
kv INSTANCE_ID "$INSTANCE"
|
|
316
473
|
kv TELEGRAM_TOKEN "$TELEGRAM_TOKEN"
|
|
317
474
|
kv TELEGRAM_USERS "$TELEGRAM_USERS"
|
|
@@ -327,44 +484,30 @@ kv MINIMAX_API_KEY "$MINIMAX_API_KEY"
|
|
|
327
484
|
|
|
328
485
|
[[ -f "$REPO_ROOT/scripts/provision-guest.sh" ]] || die "Missing: $REPO_ROOT/scripts/provision-guest.sh"
|
|
329
486
|
|
|
330
|
-
scp -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile
|
|
487
|
+
scp -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="$(ssh_known_hosts_file "$INSTANCE")" \
|
|
331
488
|
"$REPO_ROOT/scripts/provision-guest.sh" "ubuntu@$VM_IP:/tmp/provision-guest.sh"
|
|
332
|
-
scp -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile
|
|
489
|
+
scp -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="$(ssh_known_hosts_file "$INSTANCE")" \
|
|
333
490
|
"$PROVISION_VARS" "ubuntu@$VM_IP:/tmp/provision.vars"
|
|
334
491
|
|
|
335
|
-
ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile
|
|
492
|
+
ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="$(ssh_known_hosts_file "$INSTANCE")" \
|
|
336
493
|
"ubuntu@$VM_IP" "sudo bash /tmp/provision-guest.sh /tmp/provision.vars"
|
|
337
494
|
|
|
495
|
+
# Guest provisioning succeeded: from here on a failure must not destroy the
|
|
496
|
+
# instance — it is recoverable with `fireclaw provision` / `fireclaw start`.
|
|
497
|
+
CLEANUP_ARMED="false"
|
|
498
|
+
|
|
338
499
|
systemctl enable --now "$PROXY_SVC"
|
|
339
500
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
guest_health_ok="true"
|
|
346
|
-
fi
|
|
347
|
-
if [[ "$host_health_ok" != "true" ]] && curl -fsS "http://127.0.0.1:$HOST_PORT/health" >/dev/null 2>&1; then
|
|
348
|
-
host_health_ok="true"
|
|
349
|
-
fi
|
|
350
|
-
if [[ "$guest_health_ok" == "true" && "$host_health_ok" == "true" ]]; then
|
|
351
|
-
break
|
|
352
|
-
fi
|
|
353
|
-
sleep 2
|
|
354
|
-
done
|
|
501
|
+
if ! wait_for_instance_health "$INSTANCE" "$VM_IP" "$HOST_PORT" "$SSH_KEY_PATH" 30; then
|
|
502
|
+
warn "The instance was kept for inspection: sudo fireclaw status $INSTANCE"
|
|
503
|
+
die "Health checks did not pass for $INSTANCE after provisioning (retry with: sudo fireclaw provision $INSTANCE)"
|
|
504
|
+
fi
|
|
505
|
+
SETUP_COMPLETE="true"
|
|
355
506
|
|
|
356
507
|
echo "✓ VM instance configured"
|
|
357
508
|
echo " Instance: $INSTANCE"
|
|
358
509
|
echo " VM IP: $VM_IP"
|
|
359
510
|
echo " Port: $HOST_PORT"
|
|
360
|
-
echo " Token: $
|
|
361
|
-
|
|
362
|
-
echo " Health: up (guest + proxy)"
|
|
363
|
-
elif [[ "$guest_health_ok" == "true" ]]; then
|
|
364
|
-
echo " Health: guest up, proxy pending"
|
|
365
|
-
elif [[ "$host_health_ok" == "true" ]]; then
|
|
366
|
-
echo " Health: proxy up (guest check pending)"
|
|
367
|
-
else
|
|
368
|
-
echo " Health: pending (guest script: $GUEST_HEALTH_SCRIPT)"
|
|
369
|
-
fi
|
|
511
|
+
echo " Token: (print with: sudo fireclaw token $INSTANCE)"
|
|
512
|
+
echo " Health: up (guest + proxy)"
|
|
370
513
|
echo " Status: $(systemctl is-active "$VM_SVC") / $(systemctl is-active "$PROXY_SVC")"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fireclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Firecracker microVM control plane for isolated OpenClaw instances",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fireclaw": "bin/fireclaw"
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
"README.md"
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
|
-
"lint:shell": "bash -n bin/fireclaw && bash -n bin/vm-common.sh && bash -n bin/vm-setup && bash -n bin/vm-ctl && bash -n scripts/provision-guest.sh",
|
|
15
|
-
"test": "
|
|
14
|
+
"lint:shell": "bash -n bin/fireclaw && bash -n bin/vm-common.sh && bash -n bin/vm-setup && bash -n bin/vm-ctl && bash -n bin/vm-provision && bash -n scripts/provision-guest.sh && bash -n tests/unit/vm-common.test.sh",
|
|
15
|
+
"test:unit": "bash tests/unit/vm-common.test.sh",
|
|
16
|
+
"test": "npm run lint:shell && npm run test:unit",
|
|
16
17
|
"pack:check": "npm pack --dry-run",
|
|
17
18
|
"prepublishOnly": "npm test && npm run pack:check"
|
|
18
19
|
},
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
"bugs": {
|
|
32
33
|
"url": "https://github.com/bchewy/fireclaw/issues"
|
|
33
34
|
},
|
|
34
|
-
"license": "
|
|
35
|
+
"license": "MIT",
|
|
35
36
|
"author": "bchewy",
|
|
36
37
|
"engines": {
|
|
37
38
|
"node": ">=18"
|