diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6e45d96 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Optional provider keys belong here. Never put them in models.json. +# OPENROUTER_API_KEY= + +# macOS: point the UI at the native app and the host at an extracted Linux +# Grok Bot runtime. The Linux runtime runs inside the Computer container. +# GROKBOT_APP="/Applications/Grok Bot.app/Contents/MacOS/Grok Bot" +# GROKBOT_RUNTIME_DIR="/absolute/path/to/grokbot-shim/runtime/Grok Bot" +# GROKBOT_RESOURCES="/absolute/path/to/grokbot-shim/runtime/Grok Bot/resources" +# GROKBOT_HOST_APP="/opt/Grok Bot/grok-bot" +# CODEX_AUTH_DIR="$HOME/.codex" + +# Colima users can select a profile socket instead of Docker Desktop. +# DOCKER_HOST="unix:///absolute/path/to/.colima/default/docker.sock" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca0eeb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.env +appdata/ +certs/ +host/ +logs/ +node_modules/ +runtime/ +state/ + diff --git a/README.md b/README.md index 5bc3eb4..0c37222 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ required runtime files from the user's installed copy of Grok Bot ## Status -Working on Linux: +Working on Linux and Intel macOS: - local Grok Bot login and agent lifecycle; - streamed text, reasoning state, and tool calls; @@ -28,7 +28,8 @@ the desktop application changes. ## Requirements -- Linux with the Grok Bot desktop application installed; +- Linux with Grok Bot installed, or Intel macOS with Grok Bot installed and an + extracted Linux x64 Grok Bot runtime; - Node.js 22.12 or newer; - Docker with a running daemon; - OpenSSL and curl; @@ -41,15 +42,36 @@ elsewhere. ## Quick start +### Linux + ```bash git clone cd grokbot-shim npm ci -npm run setup cp .env.example .env # optional; add provider keys if needed +npm run setup ./run-all.sh ``` +### macOS + +On macOS, extract an official Linux x64 build that contains `resources/app.asar` +into an ignored local directory such as `runtime/Grok Bot`, then uncomment and +adjust the macOS paths in `.env.example` before running setup: + +```bash +git clone +cd grokbot-shim +npm ci +cp .env.example .env +npm run setup +./run-all.sh +``` + +The native macOS application remains the UI. The Linux host runtime and backend +shim run inside the Computer container because the macOS application bundle +does not include the host executable or its Linux native dependencies. + `npm run setup` generates a local TLS certificate and extracts the required host runtime from your installed copy of Grok Bot. Those generated files stay outside Git. @@ -225,4 +247,3 @@ I build tech businesses through SaaS products across every layer of the internet Connect here:
Website [www.aashuu.me](https://www.aashuu.me) ✦ 𝕏 [@warrioraashuu](https://x.com/warrioraashuu) ✦ LinkedIn [@warrioraashuu](https://www.linkedin.com/in/warrioraashuu/) - diff --git a/computerctl.sh b/computerctl.sh index e8567dc..ab9c104 100755 --- a/computerctl.sh +++ b/computerctl.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$ROOT/scripts/load-env.sh" CONTAINER="${GROKBOT_COMPUTER_CONTAINER:-grokbot-computer}" IMAGE="${GROKBOT_COMPUTER_IMAGE:-public.ecr.aws/k0i0n2g5/cursorenvironments/universal@sha256:dcac90cba36653f261988b1c88d11b7655493d455c4af0a18c5991ddaa5da020}" @@ -23,6 +25,16 @@ is_running() { [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null || true)" = "true" ] } +has_runtime_config() { + [[ -z "${GROKBOT_RUNTIME_DIR:-}" ]] && return 0 + local mounts ports + mounts="$(docker inspect -f '{{range .Mounts}}{{println .Destination}}{{end}}' "$CONTAINER")" + ports="$(docker inspect -f '{{json .HostConfig.PortBindings}}' "$CONTAINER")" + grep -Fxq '/grokbot-shim' <<<"$mounts" && + grep -Fxq '/opt/Grok Bot' <<<"$mounts" && + [[ "$ports" == *'"8443/tcp"'* && "$ports" == *'"8550/tcp"'* ]] +} + is_ready() { curl -fsS --max-time 2 http://127.0.0.1:6080/vnc.html >/dev/null 2>&1 && curl -sS --max-time 2 http://127.0.0.1:1337/health >/dev/null 2>&1 @@ -43,6 +55,11 @@ wait_ready() { start() { require_docker if exists; then + if ! has_runtime_config; then + echo "computer container lacks the configured runtime mounts or ports: $CONTAINER" >&2 + echo "Recreate it, then run start again: $0 stop && docker rm $CONTAINER" >&2 + exit 1 + fi if is_running; then echo "computer container already running: $CONTAINER" else @@ -54,6 +71,19 @@ start() { echo "pulling Grok Bot computer image (first run is several GB)..." docker pull "$IMAGE" fi + EXTRA_ARGS=() + if [[ -n "${GROKBOT_RUNTIME_DIR:-}" ]]; then + [[ -d "$GROKBOT_RUNTIME_DIR" ]] || { echo "Grok Bot runtime not found: $GROKBOT_RUNTIME_DIR" >&2; exit 1; } + EXTRA_ARGS+=( + -p 127.0.0.1:8443:8443 + -p 127.0.0.1:8550:8550 + -v "$ROOT:/grokbot-shim" + -v "$GROKBOT_RUNTIME_DIR:/opt/Grok Bot:ro" + ) + if [[ -n "${CODEX_AUTH_DIR:-}" ]]; then + EXTRA_ARGS+=(-v "$CODEX_AUTH_DIR:/codex:ro") + fi + fi docker run -d \ --name "$CONTAINER" \ --restart unless-stopped \ @@ -63,6 +93,7 @@ start() { -p 127.0.0.1:1339:1339 \ -p 127.0.0.1:6080:6080 \ -p 127.0.0.1:6081:6081 \ + "${EXTRA_ARGS[@]}" \ -v grokbot-computer-workspace:/workspace \ -v grokbot-computer-chrome:/home/box/chrome-profile \ -v grokbot-computer-data:/home/box/sand-data \ @@ -113,7 +144,7 @@ case "${1:-status}" in ;; open) status >/dev/null - xdg-open http://127.0.0.1:6080/vnc.html + if [[ "$(uname -s)" == "Darwin" ]]; then open http://127.0.0.1:6080/vnc.html; else xdg-open http://127.0.0.1:6080/vnc.html; fi ;; *) echo "usage: $0 {start|stop|restart|status|logs|open}" >&2 diff --git a/run-all.sh b/run-all.sh index 9b5acba..ba631a3 100755 --- a/run-all.sh +++ b/run-all.sh @@ -6,6 +6,12 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$ROOT/scripts/load-env.sh" +port_up() { + curl -fsS --max-time 1 \ + -H "Authorization: Bearer ${SAND_HOST_GATEWAY_TOKEN:-shim-gateway-token}" \ + http://127.0.0.1:8550/health >/dev/null 2>&1 +} + if [[ ! -s "$ROOT/host/dist/host/host-main.cjs" || ! -f "$ROOT/certs/localhost.pem" ]]; then echo "local runtime files are missing; run: npm run setup" >&2 exit 1 @@ -16,13 +22,16 @@ if "$ROOT/computerctl.sh" status >/dev/null 2>&1; then COMPUTER_WAS_RUNNING=1 fi HOST_WAS_RUNNING=0 -if ss -ltn 2>/dev/null | grep -q '127.0.0.1:8550'; then +if port_up; then HOST_WAS_RUNNING=1 fi HOST_PID="" cleanup() { - [ -n "$HOST_PID" ] && kill "$HOST_PID" 2>/dev/null || true + if [ -n "$HOST_PID" ]; then + kill "$HOST_PID" 2>/dev/null || true + "$ROOT/run-host.sh" --stop >/dev/null 2>&1 || true + fi if [ "$HOST_WAS_RUNNING" -eq 0 ]; then "$ROOT/shimctl.sh" stop >/dev/null 2>&1 || true fi @@ -42,10 +51,10 @@ else HOST_PID=$! echo "host gateway starting (pid $HOST_PID), log: $ROOT/logs/host.out" for _ in $(seq 1 40); do - ss -ltn 2>/dev/null | grep -q '127.0.0.1:8550' && break + port_up && break sleep 0.25 done - if ! ss -ltn 2>/dev/null | grep -q '127.0.0.1:8550'; then + if ! port_up; then echo "host gateway did not come up; tail $ROOT/logs/host.out" >&2 exit 1 fi diff --git a/run-host.sh b/run-host.sh index 5420d69..a38feab 100755 --- a/run-host.sh +++ b/run-host.sh @@ -3,6 +3,7 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$ROOT/scripts/load-env.sh" GROKBOT_APP="${GROKBOT_APP:-/opt/Grok Bot/sand}" +GROKBOT_HOST_APP="${GROKBOT_HOST_APP:-$GROKBOT_APP}" export SAND_BACKEND_URL="${SAND_BACKEND_URL:-https://localhost:8443}" export NODE_EXTRA_CA_CERTS="$ROOT/certs/rootCA.pem" @@ -14,9 +15,35 @@ export SAND_HOST_LOG_FILE="${SAND_HOST_LOG_FILE:-$ROOT/logs/host.log}" export SAND_BOX_MAX_WINDOWS="${SAND_BOX_MAX_WINDOWS:-1}" export ELECTRON_RUN_AS_NODE=1 -mkdir -p "$ROOT/state/host-workdir" +mkdir -p "$ROOT/state/home" "$ROOT/state/host-workdir" cd "$ROOT/state/host-workdir" +if [[ "${1:-}" == "--stop" ]]; then + if [[ "$(uname -s)" == "Darwin" && -n "${GROKBOT_RUNTIME_DIR:-}" ]]; then + docker exec "${GROKBOT_COMPUTER_CONTAINER:-grokbot-computer}" \ + pkill -f '/grokbot-shim/host/dist/host/host-main.cjs' 2>/dev/null || true + fi + exit 0 +fi + +if [[ "$(uname -s)" == "Darwin" && -n "${GROKBOT_RUNTIME_DIR:-}" ]]; then + CONTAINER="${GROKBOT_COMPUTER_CONTAINER:-grokbot-computer}" + exec docker exec \ + -u "$(id -u):$(id -g)" \ + -w /grokbot-shim/state/host-workdir \ + -e HOME=/grokbot-shim/state/home \ + -e ELECTRON_RUN_AS_NODE=1 \ + -e NODE_EXTRA_CA_CERTS=/grokbot-shim/certs/rootCA.pem \ + -e SAND_BACKEND_URL=https://localhost:8443 \ + -e SAND_HOST_PORT="$SAND_HOST_PORT" \ + -e SAND_GATEWAY_BIND_HOST=0.0.0.0 \ + -e SAND_GATEWAY_TOKEN="$SAND_GATEWAY_TOKEN" \ + -e SAND_DEV_INFERENCE_TOKEN_FILE=/grokbot-shim/state/host-token.json \ + -e SAND_HOST_LOG_FILE=/grokbot-shim/logs/host.log \ + -e SAND_BOX_MAX_WINDOWS="$SAND_BOX_MAX_WINDOWS" \ + "$CONTAINER" "$GROKBOT_HOST_APP" /grokbot-shim/host/dist/host/host-main.cjs "$@" +fi + if [[ ! -x "$GROKBOT_APP" ]]; then echo "Grok Bot executable not found: $GROKBOT_APP" >&2 exit 1 diff --git a/run-recon.sh b/run-recon.sh index 4836f9a..d68d1c8 100755 --- a/run-recon.sh +++ b/run-recon.sh @@ -28,6 +28,7 @@ fi echo "backend: $SAND_BACKEND_URL dev-login: $SAND_DEV_LOGIN ($SAND_DEV_LOGIN_EMAIL)" echo "NOTE: using isolated user-data-dir at $ROOT/appdata (real login untouched)" +unset ELECTRON_RUN_AS_NODE # --no-sandbox has to be on the command line for the Computer preview to draw. # The preview is a on the box's noVNC page and the app marks that guest # sandboxed, but the app's own in-process no-sandbox switch lands too late for it, diff --git a/scripts/doctor.sh b/scripts/doctor.sh index 53a750c..4c55654 100755 --- a/scripts/doctor.sh +++ b/scripts/doctor.sh @@ -2,6 +2,7 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "$ROOT/scripts/load-env.sh" GROKBOT_APP="${GROKBOT_APP:-/opt/Grok Bot/sand}" failed=0 @@ -33,6 +34,7 @@ check_file "$ROOT/certs/rootCA.pem" check_file "$ROOT/certs/localhost.pem" check_file "$ROOT/certs/localhost.key" check_file "$ROOT/host/dist/host/host-main.cjs" +check_file "$ROOT/host/dist/host/agent-isolation/agent-store-worker.cjs" if command -v docker >/dev/null 2>&1 && ! docker info >/dev/null 2>&1; then echo "unavailable Docker daemon" diff --git a/scripts/setup.sh b/scripts/setup.sh index af2cb21..05ca403 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -2,6 +2,7 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "$ROOT/scripts/load-env.sh" GROKBOT_RESOURCES="${GROKBOT_RESOURCES:-/opt/Grok Bot/resources}" ASAR="$GROKBOT_RESOURCES/app.asar" UNPACKED="$GROKBOT_RESOURCES/app.asar.unpacked" @@ -56,11 +57,11 @@ if [[ ! -f "$ROOT/certs/localhost.pem" || ! -f "$ROOT/certs/localhost.key" ]]; t fi echo "extracting the host runtime from the local Grok Bot installation..." -( - cd "$ROOT/host/dist/host" - "$ROOT/node_modules/.bin/asar" extract-file "$ASAR" dist/host/host-main.cjs - "$ROOT/node_modules/.bin/asar" extract-file "$ASAR" dist/host/host-main.cjs.map -) +EXTRACTED="$(mktemp -d "${TMPDIR:-/tmp}/grokbot-shim.XXXXXX")" +trap 'find "$EXTRACTED" -depth -delete' EXIT +"$ROOT/node_modules/.bin/asar" extract "$ASAR" "$EXTRACTED" +find "$ROOT/host/dist/host" -mindepth 1 -depth -delete +cp -a "$EXTRACTED/dist/host/." "$ROOT/host/dist/host/" cp -a "$UNPACKED/dist/deps/." "$ROOT/host/dist/deps/" if [[ ! -s "$ROOT/host/dist/host/host-main.cjs" ]]; then diff --git a/shim/server.mjs b/shim/server.mjs index 038ee30..ba0e396 100644 --- a/shim/server.mjs +++ b/shim/server.mjs @@ -38,6 +38,7 @@ function envelope(payload) { const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const PORT = Number(process.env.PORT ?? 8443); +const BIND_HOST = process.env.BIND_HOST ?? "127.0.0.1"; const MODE = process.env.MODE ?? "stub"; // stub | forward const UPSTREAM = process.env.UPSTREAM ?? "https://api2.cursor.sh"; const LOG_DIR = path.join(ROOT, "logs"); @@ -286,7 +287,7 @@ const server = https.createServer( }, ); -server.listen(PORT, "127.0.0.1", () => { +server.listen(PORT, BIND_HOST, () => { console.log(`grokbot-shim recon server: https://localhost:${PORT} (mode=${MODE})`); console.log(`capture log: ${path.join(LOG_DIR, `capture-${stamp}.jsonl`)}`); if (MODE === "forward") console.log(`upstream: ${UPSTREAM}`); diff --git a/shimctl.sh b/shimctl.sh index 1c89053..a609676 100755 --- a/shimctl.sh +++ b/shimctl.sh @@ -7,8 +7,14 @@ PIDFILE="$ROOT/state/shim.pid" mkdir -p "$ROOT/logs" "$ROOT/state" is_up() { curl -sk --max-time 2 https://localhost:8443/health >/dev/null 2>&1; } +container_mode() { [[ "$(uname -s)" == "Darwin" && -n "${GROKBOT_RUNTIME_DIR:-}" ]]; } stop() { + if container_mode; then + docker exec "${GROKBOT_COMPUTER_CONTAINER:-grokbot-computer}" pkill -f '/grokbot-shim/shim/server.mjs' 2>/dev/null || true + for _ in $(seq 1 20); do is_up || break; sleep 0.25; done + return + fi local pid="" if [ -f "$PIDFILE" ]; then pid="$(cat "$PIDFILE" 2>/dev/null || true)" @@ -26,6 +32,19 @@ stop() { } start() { + if container_mode; then + docker exec -d \ + -u "$(id -u):$(id -g)" \ + -w /grokbot-shim \ + -e ELECTRON_RUN_AS_NODE=1 \ + -e BIND_HOST=0.0.0.0 \ + -e CODEX_AUTH_FILE=/codex/auth.json \ + "${GROKBOT_COMPUTER_CONTAINER:-grokbot-computer}" \ + "${GROKBOT_HOST_APP:-/opt/Grok Bot/sand}" /grokbot-shim/shim/server.mjs + for _ in $(seq 1 20); do is_up && break; sleep 0.25; done + if is_up; then echo "shim up on https://localhost:8443"; else echo "shim FAILED to start; inspect container logs" >&2; exit 1; fi + return + fi (setsid node "$ROOT/shim/server.mjs" >>"$ROOT/logs/shim.out" 2>&1 < /dev/null & echo $! >"$PIDFILE") for _ in $(seq 1 20); do is_up && break; sleep 0.25; done if is_up; then echo "shim up on https://localhost:8443"; else echo "shim FAILED to start; tail $ROOT/logs/shim.out" >&2; exit 1; fi