@ssdavidai/zoclaw 1.0.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/README.md +79 -0
- package/bin/zoclaw.js +27 -0
- package/package.json +24 -0
- package/scripts/bootstrap.sh +99 -0
- package/scripts/setup.sh +109 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# zoclaw
|
|
2
|
+
|
|
3
|
+
Set up [OpenClaw](https://openclaw.ai) on a [Zo](https://zo.computer) instance with Tailscale access in one command.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
On a fresh Zo instance:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @ssdavidai/zoclaw && zoclaw init
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or via git:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
git clone https://github.com/ssdavidai/zoclaw.git
|
|
17
|
+
cd zoclaw
|
|
18
|
+
./setup.sh
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
That's it. The setup script walks you through everything:
|
|
22
|
+
|
|
23
|
+
1. Prompts for your Tailscale auth key (or uses the one already in zo secrets)
|
|
24
|
+
2. Installs and configures Tailscale via [zotail](https://github.com/ssdavidai/zotail)
|
|
25
|
+
3. Installs OpenClaw
|
|
26
|
+
4. Runs the OpenClaw onboarding wizard
|
|
27
|
+
5. Patches the config for secure Tailscale access
|
|
28
|
+
6. Prints your Control UI URL and offers to launch the TUI
|
|
29
|
+
|
|
30
|
+
### After setup
|
|
31
|
+
|
|
32
|
+
On first browser load, the Control UI will request device pairing. Approve it once from the CLI:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
openclaw devices list
|
|
36
|
+
openclaw devices approve <request-id>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
After that, the browser is permanently paired.
|
|
40
|
+
|
|
41
|
+
## Why this exists
|
|
42
|
+
|
|
43
|
+
After a fresh `openclaw configure` on Zo, the default gateway config doesn't work with Tailscale. You'll hit a series of issues:
|
|
44
|
+
|
|
45
|
+
1. **"requires HTTPS or localhost"** -- Tailscale Serve terminates TLS externally and proxies to the gateway as plain HTTP on loopback. The gateway sees a localhost socket but a non-local `Host` header (your `.ts.net` hostname), so it treats the connection as remote and rejects it.
|
|
46
|
+
|
|
47
|
+
2. **"device identity required"** -- The Control UI in the browser needs to complete device pairing, but the gateway doesn't recognize the browser as a trusted client without proper proxy configuration.
|
|
48
|
+
|
|
49
|
+
3. **CLI pairing scope mismatch** -- The initial onboarding pairs the CLI with read-only scopes (`operator.read`), but the CLI needs full admin scopes (`operator.admin`, `operator.approvals`, `operator.pairing`) to function.
|
|
50
|
+
|
|
51
|
+
4. **Security audit failures** -- The default config ships with invalid `denyCommands` entries and overly permissive credentials directory permissions.
|
|
52
|
+
|
|
53
|
+
## What the bootstrap patches
|
|
54
|
+
|
|
55
|
+
| Issue | Fix |
|
|
56
|
+
|---|---|
|
|
57
|
+
| Gateway doesn't trust Tailscale Serve | Sets `gateway.auth.allowTailscale: true` |
|
|
58
|
+
| `.ts.net` Host header rejected as remote | Sets `gateway.trustedProxies: ["127.0.0.1/32"]` so the gateway trusts Tailscale Serve's forwarded headers |
|
|
59
|
+
| Control UI not enabled | Sets `gateway.controlUi.enabled: true` |
|
|
60
|
+
| CLI paired with read-only scopes | Upgrades paired device scopes to full admin |
|
|
61
|
+
| Invalid `denyCommands` entries | Removes the ineffective default entries |
|
|
62
|
+
| Credentials dir readable by others | `chmod 700 ~/.openclaw/credentials` |
|
|
63
|
+
|
|
64
|
+
The script does **not** set `allowInsecureAuth` or `dangerouslyDisableDeviceAuth` -- those are insecure workarounds. Instead, it configures `trustedProxies` so the gateway properly recognizes Tailscale Serve connections as secure, and the browser goes through proper Ed25519 device pairing.
|
|
65
|
+
|
|
66
|
+
## Scripts
|
|
67
|
+
|
|
68
|
+
| Script | Purpose |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `zoclaw init` | Full setup from scratch (Tailscale + OpenClaw + bootstrap) |
|
|
71
|
+
| `zoclaw bootstrap` | Config patches only (if OpenClaw and Tailscale are already installed) |
|
|
72
|
+
|
|
73
|
+
## Security
|
|
74
|
+
|
|
75
|
+
Running `openclaw security audit` after setup should show **0 critical findings**. The setup uses `trustedProxies` + proper device pairing instead of insecure bypasses.
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
package/bin/zoclaw.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { execFileSync } = require("child_process");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
const command = process.argv[2];
|
|
6
|
+
const scriptsDir = path.join(__dirname, "..", "scripts");
|
|
7
|
+
|
|
8
|
+
const commands = {
|
|
9
|
+
init: "setup.sh",
|
|
10
|
+
bootstrap: "bootstrap.sh",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
if (!command || !commands[command]) {
|
|
14
|
+
console.log("Usage: zoclaw <command>\n");
|
|
15
|
+
console.log("Commands:");
|
|
16
|
+
console.log(" init Full setup (Tailscale + OpenClaw + bootstrap)");
|
|
17
|
+
console.log(" bootstrap Config patches only (if already installed)");
|
|
18
|
+
process.exit(command ? 1 : 0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const script = path.join(scriptsDir, commands[command]);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
execFileSync("bash", [script], { stdio: "inherit" });
|
|
25
|
+
} catch (err) {
|
|
26
|
+
process.exit(err.status ?? 1);
|
|
27
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ssdavidai/zoclaw",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Set up OpenClaw on Zo with Tailscale access in one command",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/ssdavidai/zoclaw"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"zoclaw": "./bin/zoclaw.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/",
|
|
15
|
+
"scripts/",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"openclaw",
|
|
20
|
+
"zo",
|
|
21
|
+
"tailscale",
|
|
22
|
+
"zotail"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# openclaw-tailscale-bootstrap.sh
|
|
3
|
+
# Run AFTER: zotail is configured + `openclaw configure` has been run.
|
|
4
|
+
# Patches the default openclaw config so the gateway is reachable
|
|
5
|
+
# via Tailscale (both TUI and Control UI in browser).
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
CONFIG="${HOME}/.openclaw/openclaw.json"
|
|
9
|
+
PAIRED="${HOME}/.openclaw/devices/paired.json"
|
|
10
|
+
PENDING="${HOME}/.openclaw/devices/pending.json"
|
|
11
|
+
|
|
12
|
+
if [ ! -f "$CONFIG" ]; then
|
|
13
|
+
echo "Error: $CONFIG not found. Run 'openclaw configure' first."
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
echo "Patching openclaw config for Tailscale access..."
|
|
18
|
+
|
|
19
|
+
# Patch gateway config
|
|
20
|
+
node -e "
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const cfg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
|
|
23
|
+
const gw = cfg.gateway ??= {};
|
|
24
|
+
|
|
25
|
+
// Trust Tailscale identity headers on serve connections
|
|
26
|
+
gw.auth ??= {};
|
|
27
|
+
gw.auth.allowTailscale = true;
|
|
28
|
+
|
|
29
|
+
// Enable control UI
|
|
30
|
+
gw.controlUi ??= {};
|
|
31
|
+
gw.controlUi.enabled = true;
|
|
32
|
+
|
|
33
|
+
// Remove invalid denyCommands entries (default config generates
|
|
34
|
+
// names that don't match real command IDs, triggering audit warnings)
|
|
35
|
+
if (gw.nodes?.denyCommands) delete gw.nodes.denyCommands;
|
|
36
|
+
|
|
37
|
+
// Trust localhost as a reverse proxy (Tailscale serve proxies
|
|
38
|
+
// HTTPS -> HTTP on loopback and forwards X-Forwarded-For).
|
|
39
|
+
// This lets the gateway recognize .ts.net Host headers as local.
|
|
40
|
+
gw.trustedProxies = ['127.0.0.1/32'];
|
|
41
|
+
|
|
42
|
+
// Fix credentials dir permissions
|
|
43
|
+
fs.chmodSync(process.env.HOME + '/.openclaw/credentials', 0o700);
|
|
44
|
+
|
|
45
|
+
fs.writeFileSync(process.argv[1], JSON.stringify(cfg, null, 2) + '\n');
|
|
46
|
+
" "$CONFIG"
|
|
47
|
+
|
|
48
|
+
echo " gateway.auth.allowTailscale = true"
|
|
49
|
+
echo " gateway.controlUi.enabled = true"
|
|
50
|
+
echo " gateway.trustedProxies = [\"127.0.0.1/32\"]"
|
|
51
|
+
echo " nodes.denyCommands -> removed (invalid defaults)"
|
|
52
|
+
echo " credentials dir -> 700"
|
|
53
|
+
|
|
54
|
+
# Upgrade any existing paired devices to full admin scopes
|
|
55
|
+
if [ -f "$PAIRED" ]; then
|
|
56
|
+
node -e "
|
|
57
|
+
const fs = require('fs');
|
|
58
|
+
const paired = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
|
|
59
|
+
const scopes = ['operator.read','operator.admin','operator.approvals','operator.pairing'];
|
|
60
|
+
for (const dev of Object.values(paired)) {
|
|
61
|
+
dev.clientId = 'cli';
|
|
62
|
+
dev.clientMode = 'cli';
|
|
63
|
+
dev.scopes = scopes;
|
|
64
|
+
for (const tok of Object.values(dev.tokens ?? {})) tok.scopes = scopes;
|
|
65
|
+
}
|
|
66
|
+
fs.writeFileSync(process.argv[1], JSON.stringify(paired, null, 2) + '\n');
|
|
67
|
+
" "$PAIRED"
|
|
68
|
+
echo " Upgraded paired device scopes to full admin"
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Clear stale pairing requests
|
|
72
|
+
[ -f "$PENDING" ] && echo '{}' > "$PENDING"
|
|
73
|
+
|
|
74
|
+
# Restart gateway
|
|
75
|
+
echo "Restarting gateway..."
|
|
76
|
+
pkill -f openclaw-gateway 2>/dev/null || true
|
|
77
|
+
sleep 2
|
|
78
|
+
openclaw gateway run --force > /dev/null 2>&1 &
|
|
79
|
+
sleep 5
|
|
80
|
+
|
|
81
|
+
# Verify
|
|
82
|
+
if pgrep -f openclaw-gateway > /dev/null 2>&1; then
|
|
83
|
+
TOKEN=$(node -pe "JSON.parse(require('fs').readFileSync('${CONFIG}','utf8')).gateway?.auth?.token ?? ''")
|
|
84
|
+
TS_URL=$(tailscale serve status 2>/dev/null | grep -oP 'https://\S+' | head -1 || true)
|
|
85
|
+
|
|
86
|
+
echo ""
|
|
87
|
+
echo "Ready!"
|
|
88
|
+
echo " TUI: openclaw tui"
|
|
89
|
+
if [ -n "$TS_URL" ] && [ -n "$TOKEN" ]; then
|
|
90
|
+
echo " Browser: ${TS_URL}?token=${TOKEN}"
|
|
91
|
+
echo ""
|
|
92
|
+
echo " On first browser load, the Control UI will request device pairing."
|
|
93
|
+
echo " Approve it from the TUI or CLI:"
|
|
94
|
+
echo " openclaw devices list"
|
|
95
|
+
echo " openclaw devices approve <request-id>"
|
|
96
|
+
fi
|
|
97
|
+
else
|
|
98
|
+
echo "Warning: gateway did not start. Check 'openclaw gateway run --force' manually."
|
|
99
|
+
fi
|
package/scripts/setup.sh
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# setup.sh — Full OpenClaw + Tailscale setup for Zo
|
|
3
|
+
# Installs everything from scratch on a fresh Zo instance.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SECRETS_FILE="${HOME}/.zo_secrets"
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
BOOTSTRAP="${SCRIPT_DIR}/bootstrap.sh"
|
|
9
|
+
|
|
10
|
+
# ─── Helpers ──────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
step() { echo -e "\n\033[1;36m[$1/5]\033[0m \033[1m$2\033[0m"; }
|
|
13
|
+
|
|
14
|
+
ensure_secrets_file() {
|
|
15
|
+
if [ ! -f "$SECRETS_FILE" ]; then
|
|
16
|
+
touch "$SECRETS_FILE"
|
|
17
|
+
chmod 600 "$SECRETS_FILE"
|
|
18
|
+
fi
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# ─── Step 1: Tailscale auth key ──────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
step 1 "Tailscale auth key"
|
|
24
|
+
|
|
25
|
+
ensure_secrets_file
|
|
26
|
+
source "$SECRETS_FILE" 2>/dev/null || true
|
|
27
|
+
|
|
28
|
+
if [ -n "${TAILSCALE_AUTH_KEY:-}" ]; then
|
|
29
|
+
echo " Found TAILSCALE_AUTH_KEY in zo secrets, using existing key."
|
|
30
|
+
else
|
|
31
|
+
echo " No TAILSCALE_AUTH_KEY found in zo secrets."
|
|
32
|
+
echo " Generate one at: https://login.tailscale.com/admin/settings/keys"
|
|
33
|
+
echo ""
|
|
34
|
+
read -rp " Enter your Tailscale auth key: " ts_key
|
|
35
|
+
if [ -z "$ts_key" ]; then
|
|
36
|
+
echo " Error: auth key cannot be empty."
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
echo "export TAILSCALE_AUTH_KEY=\"${ts_key}\"" >> "$SECRETS_FILE"
|
|
40
|
+
export TAILSCALE_AUTH_KEY="$ts_key"
|
|
41
|
+
echo " Saved to zo secrets."
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# ─── Step 2: Install and configure Tailscale via zotail ──────────────
|
|
45
|
+
|
|
46
|
+
step 2 "Tailscale (zotail)"
|
|
47
|
+
|
|
48
|
+
echo " Installing zotail..."
|
|
49
|
+
npm install -g @ssdavidai/zotail 2>&1 | tail -1
|
|
50
|
+
|
|
51
|
+
echo " Running zotail setup..."
|
|
52
|
+
zotail setup
|
|
53
|
+
|
|
54
|
+
# ─── Step 3: Install OpenClaw ────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
step 3 "OpenClaw"
|
|
57
|
+
|
|
58
|
+
echo " Installing openclaw..."
|
|
59
|
+
npm install -g openclaw@latest 2>&1 | tail -1
|
|
60
|
+
|
|
61
|
+
# ─── Step 4: OpenClaw onboarding ─────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
step 4 "OpenClaw onboarding"
|
|
64
|
+
|
|
65
|
+
echo " Running interactive setup..."
|
|
66
|
+
echo " (When the onboarding finishes, setup will continue automatically.)"
|
|
67
|
+
echo ""
|
|
68
|
+
openclaw onboard --install-daemon
|
|
69
|
+
|
|
70
|
+
# ─── Step 5: Bootstrap Tailscale config ──────────────────────────────
|
|
71
|
+
|
|
72
|
+
step 5 "Tailscale bootstrap"
|
|
73
|
+
|
|
74
|
+
if [ ! -f "$BOOTSTRAP" ]; then
|
|
75
|
+
echo " Error: bootstrap.sh not found at ${BOOTSTRAP}"
|
|
76
|
+
exit 1
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
bash "$BOOTSTRAP"
|
|
80
|
+
|
|
81
|
+
# ─── Done ────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
CONFIG="${HOME}/.openclaw/openclaw.json"
|
|
84
|
+
TOKEN=$(node -pe "JSON.parse(require('fs').readFileSync('${CONFIG}','utf8')).gateway?.auth?.token ?? ''" 2>/dev/null || true)
|
|
85
|
+
TS_URL=$(tailscale serve status 2>/dev/null | grep -oP 'https://\S+' | head -1 || true)
|
|
86
|
+
|
|
87
|
+
echo ""
|
|
88
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
89
|
+
echo ""
|
|
90
|
+
echo " Setup complete!"
|
|
91
|
+
echo ""
|
|
92
|
+
if [ -n "$TS_URL" ] && [ -n "$TOKEN" ]; then
|
|
93
|
+
echo " Control UI: ${TS_URL}?token=${TOKEN}"
|
|
94
|
+
echo ""
|
|
95
|
+
echo " (On first browser load, approve device pairing from the CLI:"
|
|
96
|
+
echo " openclaw devices list && openclaw devices approve <id>)"
|
|
97
|
+
fi
|
|
98
|
+
echo ""
|
|
99
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
100
|
+
echo ""
|
|
101
|
+
|
|
102
|
+
read -rp " Launch the TUI now? [Y/n] " launch_tui
|
|
103
|
+
launch_tui="${launch_tui:-Y}"
|
|
104
|
+
|
|
105
|
+
if [[ "$launch_tui" =~ ^[Yy]$ ]]; then
|
|
106
|
+
exec openclaw tui
|
|
107
|
+
else
|
|
108
|
+
echo " Run 'openclaw tui' whenever you're ready."
|
|
109
|
+
fi
|