Repository navigation
Expand file tree
/
Copy pathstart-local.sh
More file actions
executable file
·370 lines (346 loc) · 15.8 KB
/
Copy pathstart-local.sh
File metadata and controls
executable file
·370 lines (346 loc) · 15.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
#!/bin/bash
# grokbot-local — launch the reconstructed Grok Bot in local-admin mode.
#
# start-local.sh [start|stop|status|restart|logs] (default: start)
# GROKBOT_BOX=docker start-local.sh start # computer = Docker VM (default)
#
# Principles baked in:
# idempotent — safe to run repeatedly; seeds settings only when absent,
# reuses the staged host runtime, refuses double launches.
# lifecycle — stop goes through the Apple quit event so before-quit
# reaps the local host; status reports every moving part.
# no blind retry — start waits a bounded time for the gateway, then fails
# with the host log tail instead of spinning.
set -euo pipefail
# Electron runs as plain Node when this leaks in from the calling shell, and the
# binary then rejects --user-data-dir and exits at once. The launcher owns this
# variable for its own child processes, so clear it for everything it starts.
unset ELECTRON_RUN_AS_NODE
APP_BUNDLE="${GROKBOT_APP_PATH:-/Applications/Grok Bot 0.18 Reconstructed.app}"
BIN="$APP_BUNDLE/Contents/MacOS/Grok Bot"
STAMP="$APP_BUNDLE/Contents/Resources/build-stamp.json"
BUNDLE_ID="com.anysphere.sand.reconstructed"
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DATA_ROOT="${GROKBOT_DATA_ROOT:-$HOME/.grokbot-local}"
PROFILE="$DATA_ROOT/profile"
APP_LOG="$DATA_ROOT/app.log"
PID_FILE="$DATA_ROOT/app.pid"
GATEWAY_HEALTH_URL="http://127.0.0.1:1340/health"
READY_TIMEOUT_S="${GROKBOT_READY_TIMEOUT_S:-45}"
say() { printf '%s\n' "$*"; }
die() { printf 'grokbot-local: %s\n' "$*" >&2; exit 1; }
app_pid() {
# The host and exec-daemon also run under $BIN as Electron-as-node; the app
# itself is the one carrying --user-data-dir.
pgrep -f "^${BIN} --user-data-dir=" | head -1 || true
}
host_pid() {
pgrep -f "sand-host/host-main\.cjs" | head -1 || true
}
gateway_token() {
python3 - "$DATA_ROOT/local-docker-vm.json" <<'PY' 2>/dev/null || true
import json, sys
try:
print(json.load(open(sys.argv[1]))["token"])
except Exception:
pass
PY
}
# Docker endpoint discovery lives in scripts/lib/docker-socket.sh so this script
# and zero-remote-live.sh cannot drift apart.
. "$REPO/scripts/lib/docker-socket.sh"
health_ok() {
local token status
token="$(gateway_token)"
[ -n "$token" ] || return 1
status="$(curl --fail -s -o /dev/null --write-out '%{http_code}' --max-time 2 -H "authorization: Bearer $token" \
"${1:-$GATEWAY_HEALTH_URL}" 2>/dev/null)" || return 1
case "$status" in 2??) return 0 ;; *) return 1 ;; esac
}
seed_settings() {
local settings="$DATA_ROOT/settings.json"
if [ -f "$settings" ]; then
node "$REPO/scripts/lib/local-launch-config.mjs" settings "$DATA_ROOT" >/dev/null
say "settings: kept existing $settings"
return
fi
local temporary status=0
temporary="$(umask 077; mktemp "$DATA_ROOT/.launch-settings.XXXXXX")"
node "$REPO/scripts/lib/local-launch-config.mjs" settings "$DATA_ROOT" >"$temporary" || status=$?
if [ "$status" -ne 0 ]; then rm -f "$temporary"; return "$status"; fi
mv -n "$temporary" "$settings"
rm -f "$temporary"
say "settings: seeded $settings (claude-code + local box)"
}
load_launch_environment() {
mkdir -p "$DATA_ROOT"
local temporary key value status=0
temporary="$(umask 077; mktemp "$DATA_ROOT/.launch-env.XXXXXX")"
node "$REPO/scripts/lib/local-launch-config.mjs" env0 "$DATA_ROOT" >"$temporary" || status=$?
if [ "$status" -ne 0 ]; then rm -f "$temporary"; return "$status"; fi
LAUNCH_ENV_ARGS=()
while IFS= read -r -d '' key; do
if ! IFS= read -r -d '' value; then rm -f "$temporary"; die "incomplete launch environment"; fi
export "$key=$value"
LAUNCH_ENV_ARGS+=(--env "$key=$value")
done <"$temporary"
rm -f "$temporary"
DATA_ROOT="$SAND_DATA_ROOT"
PROFILE="$SAND_USER_DATA_DIR"
APP_LOG="$DATA_ROOT/app.log"
PID_FILE="$DATA_ROOT/app.pid"
}
do_start() {
[ -x "$BIN" ] || die "app binary not found: $BIN (run scripts/package-macos.mjs first)"
load_launch_environment
if [ "$(app_pid)" ]; then
local existing_pid
existing_pid="$(app_pid)"
if ps eww -p "$existing_pid" -o command= | grep -Eq '(^|[[:space:]])SAND_LOCAL_ADMIN=1([[:space:]]|$)'; then
say "already running: app pid $existing_pid"
health_ok && say "gateway: healthy" || say "gateway: not ready (host may still be starting)"
exit 0
fi
say "app pid $existing_pid lacks local admin; restarting with the complete launch environment"
do_stop
fi
# Reap our own orphaned host BEFORE classifying the 1340 holder — the
# guard cannot tell an orphan host from a foreign process, but host_pid can;
# with the guard first, our own leftover host killed the boot instead.
if [ "$(host_pid)" ]; then
say "note: leftover host pid $(host_pid); stopping it first"
kill "$(host_pid)" 2>/dev/null || true
sleep 1
fi
# 1340 held by a NON-app listener is fine when it is the Docker computer's
# port forward (ssh/docker-proxy for grok-bot-local-vm) and that gateway is
# healthy — the app connects to it. Anything else holding the port blocks.
# The socket discovery must run in THIS shell so its DOCKER_HOST export
# survives; inside a pipeline it would only affect the subshell and the
# docker call below would fail to reach the daemon.
if lsof -nP -iTCP:1340 -sTCP:LISTEN >/dev/null 2>&1; then
forward_holder=false
if [ "$(cat "$DATA_ROOT/box-mode" 2>/dev/null || echo auto)" != "mac-host" ] && resolve_docker_host 2>/dev/null; then
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^grok-bot-local-vm$' && health_ok; then
forward_holder=true
fi
fi
if [ "$forward_holder" = true ]; then
say "port 1340: held by the Docker computer's forward (healthy) — the app will connect to it"
else
die "port 1340 is held by something that is not our app or computer; refusing to start"
fi
fi
mkdir -p "$DATA_ROOT" "$PROFILE"
seed_settings
# Stale package hint: repo dist newer than the installed bundle.
if [ -d "$REPO/dist/Grok Bot 0.18 Reconstructed.app" ] && \
[ "$REPO/dist/Grok Bot 0.18 Reconstructed.app/Contents/MacOS/Grok Bot" -nt "$BIN" ]; then
say "note: repo dist is newer than /Applications copy — consider re-copying it"
fi
: > "$APP_LOG"
# Stale package guard: the app bundle carries a build stamp; warn loudly when
# it does not match this repository's HEAD (a silently failed rebuild shipped
# as a stale dist exactly once — never again).
if [ -f "$STAMP" ]; then
STAMPED_REV=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["sourceRevision"])' "$STAMP")
HEAD_REV=$(git -C "$REPO" rev-parse HEAD 2>/dev/null || echo unknown)
if [ "$STAMPED_REV" != "$HEAD_REV" ]; then
say "WARNING: installed app was built from ${STAMPED_REV:0:7} but the repo is at ${HEAD_REV:0:7} — repackage and re-copy before trusting runtime behavior"
fi
else
say "WARNING: installed app has no build stamp (pre-stamp package); freshness cannot be verified"
fi
# 本地管理员模式只在容器执行。
if [ "${GROKBOT_BOX:-docker}" = "docker" ]; then
resolve_docker_host || die "no Docker socket found ($(docker_unreachable_hint))"
node "$REPO/scripts/lib/local-runtime-cli.mjs" check "$STAMP" || die "selected Docker runtime or execution image is not ready"
echo docker > "$DATA_ROOT/box-mode"
say "computer: Docker VM"
else
die "unsupported GROKBOT_BOX: $GROKBOT_BOX (expected docker)"
fi
if [ -n "${GROKBOT_IMAGE:-}" ]; then
say "image: $GROKBOT_IMAGE (pinned)"
fi
# Agent turns execute in the container host.
say "turns: in-box execution plane"
# The desktop plane is the default computer (complete bot; both gate
# profiles green). GROKBOT_DESKTOP=0 opts back to the headless exec plane.
if [ "${GROKBOT_DESKTOP:-}" = "0" ]; then
say "desktop: off (headless exec plane, GROKBOT_DESKTOP=0)"
else
say "desktop: on (default; GROKBOT_DESKTOP=0 for headless exec)"
fi
# Launch Services 在脚本退出后继续管理进程;显式传入全部非敏感运行参数。
load_launch_environment
local launch_args=(-n -a "$APP_BUNDLE" --stdout "$APP_LOG" --stderr "$APP_LOG")
launch_args+=("${LAUNCH_ENV_ARGS[@]}")
open "${launch_args[@]}" --args "--user-data-dir=$PROFILE"
local pid=""
local launch_waited=0
while [ -z "$pid" ] && [ "$launch_waited" -lt 20 ]; do
pid="$(app_pid)"
if [ -z "$pid" ]; then sleep 0.5; launch_waited=$((launch_waited + 1)); fi
done
[ -n "$pid" ] || die "Launch Services did not start the app within 10 seconds; see $APP_LOG"
echo "$pid" > "$PID_FILE"
say "launched: app pid $pid, log $APP_LOG"
local waited=0
while [ "$waited" -lt "$((READY_TIMEOUT_S * 2))" ]; do
health_ok && { say "gateway: healthy on 127.0.0.1:1340 (after ${waited}x500ms)"; exit 0; }
if ! ps -p "$pid" >/dev/null 2>&1; then
say "app exited during startup; last log lines:"
tail -20 "$APP_LOG" || true
exit 1
fi
sleep 0.5
waited=$((waited + 1))
done
say "gateway did not become healthy within ${READY_TIMEOUT_S}s; host log tail:"
docker logs --tail 30 grok-bot-local-vm 2>&1 || \
say "(container host logs unavailable; see $APP_LOG)"
exit 1
}
do_stop() {
local pid
pid="$(app_pid)"
if [ -z "$pid" ]; then
say "not running"
else
osascript -e "tell application id \"$BUNDLE_ID\" to quit" >/dev/null 2>&1 || kill "$pid"
local waited=0
while ps -p "$pid" >/dev/null 2>&1 && [ "$waited" -lt 20 ]; do
sleep 0.5
waited=$((waited + 1))
done
ps -p "$pid" >/dev/null 2>&1 && { kill "$pid" 2>/dev/null || true; sleep 1; }
say "app stopped (pid $pid)"
fi
local hpid
hpid="$(host_pid)"
if [ -n "$hpid" ]; then
say "host still alive (pid $hpid); sending SIGTERM"
kill "$hpid" 2>/dev/null || true
fi
rm -f "$PID_FILE"
# The Docker computer outlives the app by design: desktop session, browser
# state, and the handover URL all survive an app restart (the login profile
# rides the data volume regardless). Removing it on every stop was a
# Mac-host-era reflex — a host-mode start stops the computer itself when
# actually switching (the connector's host branch owns that).
if [ "$(cat "$DATA_ROOT/box-mode" 2>/dev/null || echo auto)" != "mac-host" ]; then
resolve_docker_host || true
if docker info >/dev/null 2>&1; then
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^grok-bot-local-vm$'; then
say "computer: left running by design (use the computer reset action to rebuild it)"
fi
else
say "computer: docker unreachable — container state untouched"
fi
fi
say "note: the detached local-exec-daemon is left running by design; it reattaches on next start"
}
do_status() {
local pid hpid container_state mcp_config
pid="$(app_pid)"
say "data root: $DATA_ROOT"
say "box mode: $(cat "$DATA_ROOT/box-mode" 2>/dev/null || echo docker) (GROKBOT_BOX=docker to switch)"
# 盒内回合要求自建镜像,状态信息给出对应的构建入口。
if [ "$(cat "$DATA_ROOT/box-mode" 2>/dev/null || echo auto)" != "mac-host" ] && [ -z "${GROKBOT_IMAGE:-}" ]; then
if resolve_docker_host && docker info >/dev/null 2>&1 && \
! docker image inspect grok-bot-exec-box:arm64 >/dev/null 2>&1; then
say "image: self-built arm64 image missing — in-box turns require docker/build-arm64-box.sh"
fi
fi
if [ "$(cat "$DATA_ROOT/box-mode" 2>/dev/null || echo auto)" != "mac-host" ] && [ -d "$DATA_ROOT/box-workspace" ]; then
say "workspace: $DATA_ROOT/box-workspace (Mac side of the container's /workspace)"
fi
mcp_config="$DATA_ROOT/mcp-config/shared/mcp-servers.json"
[ -d "$DATA_ROOT/mcp-config/shared" ] || mcp_config="$DATA_ROOT/mcp-servers.json"
if [ -f "$mcp_config" ]; then
say "mcp plugins: $(python3 -c 'import json,sys; print(len(json.load(open(sys.argv[1])).get("mcpServers", {})))' "$mcp_config") defined in $mcp_config"
fi
if [ -n "$pid" ]; then say "app: running (pid $pid)"; else say "app: not running"; fi
if ! resolve_docker_host 2>/dev/null || ! docker info >/dev/null 2>&1; then
say "computer: docker unreachable — cannot inspect the container ($(docker_unreachable_hint))"
say "host: unknown (Docker unreachable)"
say "box-exec: unknown (Docker unreachable)"
else
container_state="$(docker inspect --format '{{.State.Status}}' grok-bot-local-vm 2>/dev/null || true)"
if [ "$container_state" = "running" ]; then
if [ "$(docker inspect --format '{{index .Config.Labels "com.grok-bot.local-vm.desktop"}}' grok-bot-local-vm 2>/dev/null)" = "1" ]; then
say "computer: desktop plane (box-init-exec, opt-in)"
if [ -f "$DATA_ROOT/box-workspace/.grokbot/novnc-url" ]; then
say "handover: $(cat "$DATA_ROOT/box-workspace/.grokbot/novnc-url")"
fi
else
say "computer: exec plane (headless)"
fi
hpid="$(docker exec grok-bot-local-vm pgrep -f '[s]and-host/host-main[.]cjs' 2>/dev/null | head -1 || true)"
if [ -n "$hpid" ]; then say "host: running in container (pid $hpid)"; else say "host: not running in container"; fi
if docker exec grok-bot-local-vm node -e '
const token = process.env.SAND_BOX_EXEC_DAEMON_AUTH_TOKEN || process.env.SAND_GATEWAY_TOKEN;
if (!token) process.exit(1);
fetch("http://127.0.0.1:1337/agent.v1.ControlService/Ping", {
method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
body: "{}", signal: AbortSignal.timeout(2000),
}).then(response => process.exit(response.ok ? 0 : 1), () => process.exit(1));
' >/dev/null 2>&1; then
say "box-exec: healthy (container port 1337)"
else
say "box-exec: not responding (container port 1337)"
fi
else
say "computer: container not running (${container_state:-not found})"
say "host: not running (container ${container_state:-not found})"
say "box-exec: not running (container ${container_state:-not found})"
fi
fi
if pgrep -f "dist/local-exec-daemon/main\.cjs" >/dev/null 2>&1; then
say "Mac local-exec: running (pid $(pgrep -f "dist/local-exec-daemon/main\.cjs" | head -1))"
else
say "Mac local-exec: not running"
fi
if health_ok; then say "gateway: healthy"; else say "gateway: down"; fi
if [ -f "$DATA_ROOT/local-intercept.jsonl" ]; then
say "intercept: $(wc -l < "$DATA_ROOT/local-intercept.jsonl" | tr -d ' ') events"
fi
exit 0
}
do_logs() {
resolve_docker_host 2>/dev/null && docker info >/dev/null 2>&1 || die "Docker unreachable ($(docker_unreachable_hint))"
docker inspect grok-bot-local-vm >/dev/null 2>&1 || die "container grok-bot-local-vm not found"
local app_log_pid="" container_log_pid=""
cleanup_logs() {
[ -z "$app_log_pid" ] || kill "$app_log_pid" 2>/dev/null || true
[ -z "$container_log_pid" ] || kill "$container_log_pid" 2>/dev/null || true
wait 2>/dev/null || true
}
trap cleanup_logs EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
say "tailing $APP_LOG and Docker grok-bot-local-vm logs (Ctrl-C to stop)"
tail -n 40 -F "$APP_LOG" &
app_log_pid=$!
docker logs --tail 40 --follow grok-bot-local-vm &
container_log_pid=$!
wait "$container_log_pid"
cleanup_logs
trap - EXIT INT TERM
}
validate_turn_mode() {
# Reject the retired Mac selection before start or restart changes any state.
[ "${GROKBOT_TURN:-host}" = "host" ] || die "unsupported GROKBOT_TURN: $GROKBOT_TURN (expected host)"
[ "${GROKBOT_BOX:-docker}" = "docker" ] || die "unsupported GROKBOT_BOX: $GROKBOT_BOX (expected docker)"
}
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
case "${1:-start}" in
start) validate_turn_mode; do_start ;;
stop) do_stop ;;
restart) validate_turn_mode; load_launch_environment; do_stop; do_start ;;
status) do_status ;;
logs) do_logs ;;
*) die "usage: $0 [start|stop|status|restart|logs]" ;;
esac
fi