Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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"
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.env
appdata/
certs/
host/
logs/
node_modules/
runtime/
state/

29 changes: 25 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -41,15 +42,36 @@ elsewhere.

## Quick start

### Linux

```bash
git clone <your-repository-url>
cd grokbot-shim
npm ci
npm run setup
cp .env.example .env # optional; add provider keys if needed
npm run setup
./run-all.sh
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

### 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 <your-repository-url>
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.
Expand Down Expand Up @@ -225,4 +247,3 @@ I build tech businesses through SaaS products across every layer of the internet

Connect here:<br>
Website [www.aashuu.me](https://www.aashuu.me) ✦ 𝕏 [@warrioraashuu](https://x.com/warrioraashuu) ✦ LinkedIn [@warrioraashuu](https://www.linkedin.com/in/warrioraashuu/)

33 changes: 32 additions & 1 deletion computerctl.sh
Original file line number Diff line number Diff line change
@@ -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}"

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)
if [[ -n "${CODEX_AUTH_DIR:-}" ]]; then
EXTRA_ARGS+=(-v "$CODEX_AUTH_DIR:/codex:ro")
fi
fi
docker run -d \
--name "$CONTAINER" \
--restart unless-stopped \
Expand All @@ -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 \
Expand Down Expand Up @@ -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
Expand Down
17 changes: 13 additions & 4 deletions run-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
29 changes: 28 additions & 1 deletion run-host.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions run-recon.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <webview> 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,
Expand Down
2 changes: 2 additions & 0 deletions scripts/doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"
Expand Down
11 changes: 6 additions & 5 deletions scripts/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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/"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
cp -a "$UNPACKED/dist/deps/." "$ROOT/host/dist/deps/"

if [[ ! -s "$ROOT/host/dist/host/host-main.cjs" ]]; then
Expand Down
3 changes: 2 additions & 1 deletion shim/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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}`);
Expand Down
19 changes: 19 additions & 0 deletions shimctl.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand All @@ -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
Expand Down