Compare commits
22 Commits
feature/we
...
59dbaf8a6c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59dbaf8a6c | ||
|
|
8d5dd046a4 | ||
|
|
fc8e388c0a | ||
|
|
e6dccb5656 | ||
|
|
7d8eb89911 | ||
|
|
60f425a4fc | ||
|
|
647828aa78 | ||
|
|
b4c8d3fdb8 | ||
|
|
09b1c1e37a | ||
|
|
916d8bf95a | ||
|
|
6bc21219a7 | ||
|
|
ca33b2d74a | ||
|
|
d0c50f5d8a | ||
|
|
a8a285b356 | ||
|
|
045cf6aad2 | ||
|
|
78be4fc600 | ||
|
|
d1b4d58c5d | ||
|
|
4a539a33c9 | ||
|
|
b326153d26 | ||
|
|
2612cef1dc | ||
|
|
120721bbc6 | ||
|
|
fe5b4659fe |
@@ -18,6 +18,7 @@ This repository contains practical OpenClaw skills and companion integrations. I
|
||||
|---|---|---|
|
||||
| `elevenlabs-stt` | Transcribe local audio files with ElevenLabs Speech-to-Text, with diarization, language hints, event tags, and JSON output. | `skills/elevenlabs-stt` |
|
||||
| `gitea-api` | Interact with Gitea via REST API (repos, issues, PRs, releases, branches, user info). | `skills/gitea-api` |
|
||||
| `nordvpn-client` | Install, log in to, connect, disconnect, and verify NordVPN sessions across Linux CLI and macOS NordLynx/WireGuard backends. | `skills/nordvpn-client` |
|
||||
| `portainer` | Manage Portainer stacks via API (list, start/stop/restart, update, prune images). | `skills/portainer` |
|
||||
| `searxng` | Search through a local or self-hosted SearXNG instance for web, news, images, and more. | `skills/searxng` |
|
||||
| `web-automation` | One-shot extraction plus broader browsing/scraping with Playwright-compatible CloakBrowser (auth flows, extraction, bot-protected sites). | `skills/web-automation` |
|
||||
|
||||
@@ -6,6 +6,7 @@ This folder contains detailed docs for each skill in this repository.
|
||||
|
||||
- [`elevenlabs-stt`](elevenlabs-stt.md) — Local audio transcription through ElevenLabs Speech-to-Text
|
||||
- [`gitea-api`](gitea-api.md) — REST-based Gitea automation (no `tea` CLI required)
|
||||
- [`nordvpn-client`](nordvpn-client.md) — Cross-platform NordVPN install, login, connect, disconnect, and verification with Linux CLI and macOS NordLynx/WireGuard support
|
||||
- [`portainer`](portainer.md) — Portainer stack management (list, lifecycle, updates, image pruning)
|
||||
- [`searxng`](searxng.md) — Privacy-respecting metasearch via a local or self-hosted SearXNG instance
|
||||
- [`web-automation`](web-automation.md) — One-shot extraction plus Playwright-compatible CloakBrowser browser automation and scraping
|
||||
|
||||
371
docs/nordvpn-client.md
Normal file
371
docs/nordvpn-client.md
Normal file
@@ -0,0 +1,371 @@
|
||||
# nordvpn-client
|
||||
|
||||
Cross-platform NordVPN lifecycle skill for macOS and Linux.
|
||||
|
||||
## Overview
|
||||
|
||||
`nordvpn-client` is the operator-facing VPN control skill for OpenClaw. It can:
|
||||
|
||||
- detect whether the host is ready for NordVPN automation
|
||||
- install or bootstrap the required backend
|
||||
- validate auth
|
||||
- connect to a target country or city
|
||||
- verify the public exit location
|
||||
- disconnect and restore normal local networking state
|
||||
|
||||
The skill uses different backends by platform:
|
||||
|
||||
- Linux: official `nordvpn` CLI
|
||||
- macOS: NordLynx/WireGuard with `wireguard-go` and `wireguard-tools`
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js status
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js install
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js login
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js verify
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js verify --country "Italy"
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js verify --country "Italy" --city "Milan"
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js connect --country "Italy"
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js connect --city "Tokyo"
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js connect --country "Japan" --city "Tokyo"
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js disconnect
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js status --debug
|
||||
```
|
||||
|
||||
## Credentials
|
||||
|
||||
Supported inputs:
|
||||
|
||||
- `NORDVPN_TOKEN`
|
||||
- `NORDVPN_TOKEN_FILE`
|
||||
- `NORDVPN_USERNAME`
|
||||
- `NORDVPN_PASSWORD`
|
||||
- `NORDVPN_PASSWORD_FILE`
|
||||
|
||||
Default OpenClaw credential paths:
|
||||
|
||||
- token: `~/.openclaw/workspace/.clawdbot/credentials/nordvpn/token.txt`
|
||||
- password: `~/.openclaw/workspace/.clawdbot/credentials/nordvpn/password.txt`
|
||||
|
||||
Recommended setup on macOS is a token file with strict permissions:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.openclaw/workspace/.clawdbot/credentials/nordvpn
|
||||
chmod 700 ~/.openclaw/workspace/.clawdbot/credentials/nordvpn
|
||||
printf '%s\n' '<your-nordvpn-token>' > ~/.openclaw/workspace/.clawdbot/credentials/nordvpn/token.txt
|
||||
chmod 600 ~/.openclaw/workspace/.clawdbot/credentials/nordvpn/token.txt
|
||||
```
|
||||
|
||||
Do not commit secrets into the repo or the skill docs.
|
||||
|
||||
## Platform Backends
|
||||
|
||||
### macOS
|
||||
|
||||
Current macOS backend:
|
||||
|
||||
- NordLynx/WireGuard
|
||||
- `wireguard-go`
|
||||
- `wireguard-tools`
|
||||
- NordVPN DNS in the generated WireGuard config:
|
||||
- `103.86.96.100`
|
||||
- `103.86.99.100`
|
||||
|
||||
Important behavior:
|
||||
|
||||
- `NordVPN.app` may remain installed, but the automated backend does not reuse app login state.
|
||||
- The skill automatically suspends Tailscale before connect if Tailscale is active.
|
||||
- The skill resumes Tailscale after disconnect, or after a failed connect, if it stopped it.
|
||||
- The Homebrew NordVPN app does not need to be uninstalled.
|
||||
|
||||
### Linux
|
||||
|
||||
Current Linux backend:
|
||||
|
||||
- official `nordvpn` CLI
|
||||
- official NordVPN installer
|
||||
- token login through `nordvpn login --token ...`
|
||||
|
||||
## Install / Bootstrap
|
||||
|
||||
### macOS
|
||||
|
||||
Bootstrap the automation backend:
|
||||
|
||||
```bash
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js install
|
||||
```
|
||||
|
||||
Equivalent Homebrew command:
|
||||
|
||||
```bash
|
||||
brew install wireguard-go wireguard-tools
|
||||
```
|
||||
|
||||
What `install` does on macOS:
|
||||
|
||||
- checks whether `wireguard-go` is present
|
||||
- checks whether `wg` and `wg-quick` are present
|
||||
- installs missing packages through Homebrew
|
||||
|
||||
### Linux
|
||||
|
||||
```bash
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js install
|
||||
```
|
||||
|
||||
What `install` does on Linux:
|
||||
|
||||
- downloads NordVPN’s official installer script
|
||||
- runs it
|
||||
- leaves subsequent login/connect to the official `nordvpn` CLI
|
||||
|
||||
## macOS sudoers Setup
|
||||
|
||||
Automated macOS connect/disconnect requires passwordless `sudo` for the helper script that invokes `wg-quick`.
|
||||
|
||||
Installed OpenClaw helper path:
|
||||
|
||||
```text
|
||||
/Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh
|
||||
```
|
||||
|
||||
Edit sudoers safely:
|
||||
|
||||
```bash
|
||||
sudo visudo
|
||||
```
|
||||
|
||||
Add this exact rule:
|
||||
|
||||
```sudoers
|
||||
stefano ALL=(root) NOPASSWD: /Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh probe, /Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh up, /Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh down
|
||||
```
|
||||
|
||||
If you run the repo copy directly instead of the installed OpenClaw skill, adjust the helper path accordingly.
|
||||
|
||||
## Common Flows
|
||||
|
||||
### Status
|
||||
|
||||
```bash
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js status
|
||||
```
|
||||
|
||||
Use this first to answer:
|
||||
|
||||
- is the correct backend available?
|
||||
- is the token visible?
|
||||
- is `sudoReady` true?
|
||||
- is the machine currently connected?
|
||||
|
||||
### Login
|
||||
|
||||
```bash
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js login
|
||||
```
|
||||
|
||||
On macOS this validates the token and populates the local auth cache. It does not connect the VPN.
|
||||
|
||||
### Connect
|
||||
|
||||
Country:
|
||||
|
||||
```bash
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js connect --country "Germany"
|
||||
```
|
||||
|
||||
City:
|
||||
|
||||
```bash
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js connect --country "Japan" --city "Tokyo"
|
||||
```
|
||||
|
||||
Expected macOS behavior:
|
||||
|
||||
- stop Tailscale if active
|
||||
- select a NordVPN server for the target
|
||||
- bring up the WireGuard tunnel
|
||||
- verify the public exit location
|
||||
- return JSON describing the chosen server and final verified location
|
||||
|
||||
### Verify
|
||||
|
||||
```bash
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js verify --country "Germany"
|
||||
```
|
||||
|
||||
Use this after connect if you want an explicit location check without changing VPN state.
|
||||
|
||||
### Disconnect
|
||||
|
||||
```bash
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js disconnect
|
||||
```
|
||||
|
||||
Expected macOS behavior:
|
||||
|
||||
- attempt `wg-quick down` whenever there is active or residual NordVPN WireGuard state
|
||||
- remove stale local NordVPN state files after teardown
|
||||
- resume Tailscale if the skill had suspended it
|
||||
|
||||
## Output Model
|
||||
|
||||
Normal JSON is redacted by default.
|
||||
|
||||
Redacted fields in normal mode:
|
||||
|
||||
- `cliPath`
|
||||
- `appPath`
|
||||
- `wireguard.configPath`
|
||||
- `wireguard.helperPath`
|
||||
- `wireguard.authCache.tokenSource`
|
||||
|
||||
Operational fields preserved in normal mode:
|
||||
|
||||
- `connected`
|
||||
- `wireguard.active`
|
||||
- `wireguard.endpoint`
|
||||
- `requestedTarget`
|
||||
- `verification`
|
||||
- public IP and location
|
||||
|
||||
For deeper troubleshooting, use:
|
||||
|
||||
```bash
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js status --debug
|
||||
```
|
||||
|
||||
`--debug` keeps the internal local paths and other low-level metadata in the JSON output.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `Invalid authorization header`
|
||||
|
||||
Meaning:
|
||||
|
||||
- the token file was found
|
||||
- the token value is not valid for NordVPN’s API
|
||||
|
||||
Actions:
|
||||
|
||||
1. generate a fresh NordVPN access token
|
||||
2. replace the contents of `~/.openclaw/workspace/.clawdbot/credentials/nordvpn/token.txt`
|
||||
3. run:
|
||||
|
||||
```bash
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js login
|
||||
```
|
||||
|
||||
### `sudoReady: false`
|
||||
|
||||
Meaning:
|
||||
|
||||
- the helper script is present
|
||||
- the agent cannot run `wg-quick` non-interactively
|
||||
|
||||
Actions:
|
||||
|
||||
1. add the `visudo` rule shown above
|
||||
2. rerun:
|
||||
|
||||
```bash
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js status
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- `wireguard.sudoReady: true`
|
||||
|
||||
### WireGuard tools missing
|
||||
|
||||
Meaning:
|
||||
|
||||
- macOS backend is selected
|
||||
- `wireguard-go`, `wg`, or `wg-quick` is missing
|
||||
|
||||
Actions:
|
||||
|
||||
```bash
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js install
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```bash
|
||||
brew install wireguard-go wireguard-tools
|
||||
```
|
||||
|
||||
### Tailscale interaction
|
||||
|
||||
Expected behavior on macOS:
|
||||
|
||||
- Tailscale is suspended before the NordVPN connect
|
||||
- Tailscale is resumed after disconnect or failed connect
|
||||
|
||||
If a connect succeeds but later traffic is wrong, check:
|
||||
|
||||
```bash
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js status
|
||||
/opt/homebrew/bin/tailscale status --json
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
- `connected: true` and a foreign exit IP while NordVPN is up
|
||||
- `connected: false` and Texas/Garland IP after disconnect
|
||||
|
||||
### Status says disconnected after a verified connect
|
||||
|
||||
This was a previous macOS false-negative path and is now normalized in the connect response.
|
||||
|
||||
Current expectation:
|
||||
|
||||
- if `connect` verifies the target location successfully
|
||||
- the returned `state` snapshot should also show:
|
||||
- `connected: true`
|
||||
- `wireguard.active: true`
|
||||
|
||||
If that regresses, capture:
|
||||
|
||||
- `connect` JSON
|
||||
- `verify` JSON
|
||||
- `status --debug` JSON
|
||||
|
||||
### Disconnect says “no active connection” but traffic is still foreign
|
||||
|
||||
The current macOS disconnect path now treats residual WireGuard state as sufficient reason to attempt teardown.
|
||||
|
||||
Safe operator check:
|
||||
|
||||
```bash
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js disconnect
|
||||
node skills/nordvpn-client/scripts/nordvpn-client.js verify
|
||||
```
|
||||
|
||||
Expected after a good disconnect:
|
||||
|
||||
- Texas/Garland public IP again
|
||||
- `wireguard.configPath: null` in normal status output
|
||||
- `wireguard.lastConnection: null`
|
||||
|
||||
If that regresses, capture:
|
||||
|
||||
- `disconnect` JSON
|
||||
- `verify` JSON
|
||||
- `status --debug` JSON
|
||||
|
||||
## Recommended Agent Workflow
|
||||
|
||||
For VPN-routed work:
|
||||
|
||||
1. `status`
|
||||
2. `install` if backend tooling is missing
|
||||
3. `login` if token validation has not happened yet
|
||||
4. `connect --country ...` or `connect --country ... --city ...`
|
||||
5. `verify`
|
||||
6. run the follow-up skill such as `web-automation`
|
||||
7. `disconnect`
|
||||
8. `verify` again if you need proof the machine returned to the normal exit path
|
||||
40
docs/plans/2026-03-11-nordvpn-client-design.md
Normal file
40
docs/plans/2026-03-11-nordvpn-client-design.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# NordVPN Client Skill Design
|
||||
|
||||
## Goal
|
||||
Create a `nordvpn-client` skill that works on macOS and Linux gateway hosts. It should detect whether NordVPN is already installed, bootstrap it if missing, handle login/auth setup, connect to a requested country or city, verify the VPN state and public IP location, disconnect when requested, and then be usable alongside other skills like `web-automation`.
|
||||
|
||||
## Architecture
|
||||
The skill exposes one logical interface with platform-specific backends. Linux uses the official NordVPN CLI path. macOS probes for a usable CLI first, but falls back to the official app workflow when needed. The skill is responsible only for VPN lifecycle and verification, not for wrapping arbitrary commands inside a VPN session.
|
||||
|
||||
## Interface
|
||||
Single script entrypoint:
|
||||
- `node scripts/nordvpn-client.js install`
|
||||
- `node scripts/nordvpn-client.js login`
|
||||
- `node scripts/nordvpn-client.js connect --country "Italy"`
|
||||
- `node scripts/nordvpn-client.js connect --city "Milan"`
|
||||
- `node scripts/nordvpn-client.js disconnect`
|
||||
- `node scripts/nordvpn-client.js status`
|
||||
|
||||
## Platform Model
|
||||
### Linux
|
||||
- Probe for `nordvpn`
|
||||
- If missing, bootstrap official NordVPN package/CLI
|
||||
- Prefer token-based login for non-interactive auth
|
||||
- Connect/disconnect/status through official CLI
|
||||
|
||||
### macOS
|
||||
- Probe for `nordvpn` CLI if available
|
||||
- Otherwise probe/install the official app
|
||||
- Use CLI when present, otherwise automate the app/login flow
|
||||
- Verify connection using app/CLI state plus external IP/geolocation
|
||||
|
||||
## Auth and Safety
|
||||
- Do not store raw NordVPN secrets in skill docs
|
||||
- Read token/credentials from env vars or a local credential file path
|
||||
- Keep the skill focused on install/login/connect/disconnect/status
|
||||
- After `connect`, verify both local VPN state and external IP/location before the agent proceeds to tasks like `web-automation`
|
||||
|
||||
## Verification
|
||||
- `status` reports platform, install state, auth state, connection state, and public IP/location check
|
||||
- `connect` verifies the requested target as closely as available data allows
|
||||
- Local validation happens first in the OpenClaw workspace, then the proven skill is copied into `stef-openclaw-skills`, documented, committed, and pushed
|
||||
127
docs/plans/2026-03-11-nordvpn-client.md
Normal file
127
docs/plans/2026-03-11-nordvpn-client.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# NordVPN Client Skill Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Build a cross-platform `nordvpn-client` skill for macOS and Linux that can install/bootstrap NordVPN, log in, connect to a target country or city, verify the VPN session, disconnect, and report status.
|
||||
|
||||
**Architecture:** Implement one skill with one script entrypoint and platform-specific backends. Linux uses the official NordVPN CLI. macOS uses a CLI path when present and otherwise falls back to the NordVPN app workflow. The skill manages VPN state only, leaving follow-up operations like `web-automation` to separate agent steps.
|
||||
|
||||
**Tech Stack:** Node.js, shell/OS commands, NordVPN CLI/app integration, OpenClaw skills, git
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Create isolated worktree
|
||||
|
||||
**Files:**
|
||||
- Modify: repo git metadata only
|
||||
|
||||
**Step 1: Create worktree**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
git -C /Users/stefano/.openclaw/workspace/projects/stef-openclaw-skills worktree add /Users/stefano/.openclaw/workspace/projects/stef-openclaw-skills/.worktrees/nordvpn-client -b feature/nordvpn-client
|
||||
```
|
||||
|
||||
**Step 2: Verify baseline**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
git -C /Users/stefano/.openclaw/workspace/projects/stef-openclaw-skills/.worktrees/nordvpn-client status --short --branch
|
||||
```
|
||||
Expected: clean feature branch
|
||||
|
||||
### Task 2: Create the local skill runtime
|
||||
|
||||
**Files:**
|
||||
- Create: `skills/nordvpn-client/SKILL.md`
|
||||
- Create: `skills/nordvpn-client/scripts/nordvpn-client.js`
|
||||
- Optional Create: helper files under `skills/nordvpn-client/scripts/`
|
||||
|
||||
**Step 1: Write the failing checks**
|
||||
- Missing command/action should fail with clear usage output
|
||||
- Unsupported platform should fail clearly
|
||||
|
||||
**Step 2: Implement platform detection and install probe**
|
||||
- detect `darwin` vs `linux`
|
||||
- detect whether NordVPN CLI/app is already present
|
||||
- expose `status` with install/auth/connect fields
|
||||
|
||||
### Task 3: Implement install and auth bootstrap
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/nordvpn-client/scripts/nordvpn-client.js`
|
||||
|
||||
**Step 1: Linux install/login path**
|
||||
- implement official CLI probe/install path
|
||||
- implement token-based login path
|
||||
|
||||
**Step 2: macOS install/login path**
|
||||
- probe CLI first
|
||||
- if absent, probe/install NordVPN app path
|
||||
- implement login/bootstrap state verification for the app workflow
|
||||
|
||||
**Step 3: Keep secrets external**
|
||||
- env vars or local credential path only
|
||||
- no raw secrets in docs or skill text
|
||||
|
||||
### Task 4: Implement connect/disconnect/status/verification
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/nordvpn-client/scripts/nordvpn-client.js`
|
||||
|
||||
**Step 1: Connect**
|
||||
- support `--country` and `--city`
|
||||
- normalize target handling per platform
|
||||
|
||||
**Step 2: Verify**
|
||||
- report local connection state
|
||||
- run public IP / geolocation verification
|
||||
- fail if connection target cannot be reasonably verified
|
||||
|
||||
**Step 3: Disconnect and status**
|
||||
- implement clean disconnect
|
||||
- ensure `status` emits machine-readable output for agent use
|
||||
|
||||
### Task 5: Validate locally in OpenClaw workspace
|
||||
|
||||
**Files:**
|
||||
- Test: local workspace copy of `nordvpn-client`
|
||||
|
||||
**Step 1: Direct command validation**
|
||||
- usage errors are correct
|
||||
- install probe works on this host
|
||||
- status output is coherent before login/connect
|
||||
|
||||
**Step 2: One real connect flow**
|
||||
- connect to a test country/city if credentials are available
|
||||
- verify local state + external IP/location
|
||||
- disconnect cleanly
|
||||
|
||||
### Task 6: Promote to repo docs and publish
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
- Modify: `docs/README.md`
|
||||
- Create: `docs/nordvpn-client.md`
|
||||
- Create/Modify: `skills/nordvpn-client/...`
|
||||
|
||||
**Step 1: Document the skill**
|
||||
- install/bootstrap behavior
|
||||
- auth expectations
|
||||
- connect/disconnect/status commands
|
||||
- macOS vs Linux notes
|
||||
|
||||
**Step 2: Commit and push**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
git add skills/nordvpn-client docs README.md
|
||||
git commit -m "feat: add nordvpn client skill"
|
||||
git push -u origin feature/nordvpn-client
|
||||
```
|
||||
|
||||
**Step 3: Merge and cleanup**
|
||||
- fast-forward or merge to `main`
|
||||
- push `main`
|
||||
- remove the worktree
|
||||
- delete the feature branch
|
||||
34
docs/plans/2026-03-11-nordvpn-wireguard-macos-design.md
Normal file
34
docs/plans/2026-03-11-nordvpn-wireguard-macos-design.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# NordVPN macOS WireGuard Backend Design
|
||||
|
||||
## Goal
|
||||
Replace the current macOS app-manual fallback in `nordvpn-client` with a scripted WireGuard/NordLynx backend inspired by `wg-nord` and `wgnord`, while preserving the official Linux `nordvpn` CLI backend.
|
||||
|
||||
## Key decisions
|
||||
- Keep Linux on the official `nordvpn` CLI.
|
||||
- Prefer a native macOS WireGuard backend over the GUI app.
|
||||
- Do not vendor third-party scripts directly; reimplement the needed logic in our own JSON-based Node skill.
|
||||
- Do not require uninstalling the Homebrew `nordvpn` app. The new backend can coexist with it.
|
||||
|
||||
## macOS backend model
|
||||
- Bootstrap via Homebrew:
|
||||
- `wireguard-tools`
|
||||
- `wireguard-go`
|
||||
- Read NordVPN token from existing env/file inputs.
|
||||
- Discover a WireGuard-capable NordVPN server via the public Nord API.
|
||||
- Generate a private key locally.
|
||||
- Exchange the private key for Nord-provided interface credentials using the token.
|
||||
- Materialize a temporary WireGuard config under a skill-owned state directory.
|
||||
- Connect and disconnect via `wg-quick`.
|
||||
- Verify with public IP/geolocation after connect.
|
||||
|
||||
## Data/state
|
||||
- Keep state under a skill-owned directory in the user's home, not `/etc`.
|
||||
- Persist only what is needed for reconnect/disconnect/status.
|
||||
- Never store secrets in docs.
|
||||
|
||||
## Rollout
|
||||
1. Implement the macOS WireGuard backend in the skill.
|
||||
2. Update status output so backend selection is explicit.
|
||||
3. Update skill docs and repo docs.
|
||||
4. Verify non-destructive flows on this host.
|
||||
5. Commit, push, and then decide whether to run a live connect test.
|
||||
11
docs/plans/2026-03-11-nordvpn-wireguard-macos.md
Normal file
11
docs/plans/2026-03-11-nordvpn-wireguard-macos.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# NordVPN macOS WireGuard Backend Plan
|
||||
|
||||
1. Add a backend selector to `nordvpn-client`.
|
||||
2. Keep Linux CLI behavior unchanged.
|
||||
3. Add macOS WireGuard dependency probing and install guidance.
|
||||
4. Implement token-based NordLynx config generation inspired by `wg-nord`/`wgnord`.
|
||||
5. Replace the current preferred macOS control mode from `app-manual` to WireGuard when dependencies and token are available.
|
||||
6. Keep app-manual as the last fallback only.
|
||||
7. Update `status`, `login`, `connect`, `disconnect`, and `verify` JSON to expose the backend in use.
|
||||
8. Update repo docs and skill docs to reflect the new model and required token/dependencies.
|
||||
9. Verify command behavior locally without forcing a live VPN connection unless requested.
|
||||
76
docs/plans/2026-03-12-nordvpn-client-docs-refresh.md
Normal file
76
docs/plans/2026-03-12-nordvpn-client-docs-refresh.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# NordVPN Client Docs Refresh Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Refresh the `nordvpn-client` documentation so operators and the OpenClaw agent have complete, accurate setup and troubleshooting guidance for the current macOS and Linux backends.
|
||||
|
||||
**Architecture:** Expand the canonical repo doc into a full operator guide, tighten the agent-facing `SKILL.md` to match the current behavior, and lightly update summary docs only if their current one-line descriptions are materially incomplete. Sync the updated `SKILL.md` into the installed OpenClaw workspace copy so runtime guidance matches the repo.
|
||||
|
||||
**Tech Stack:** Markdown docs, local repo skill docs, OpenClaw workspace skill sync
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Refresh canonical operator documentation
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/nordvpn-client.md`
|
||||
|
||||
**Step 1: Rewrite the doc structure**
|
||||
- Add sections for overview, platform backends, prerequisites, credential paths, install/bootstrap, macOS sudoers setup, command flows, output model, and troubleshooting.
|
||||
|
||||
**Step 2: Add exact operator setup details**
|
||||
- Include the exact `visudo` entry for the helper script.
|
||||
- Document default token/password file locations.
|
||||
- Document Homebrew install commands for macOS tooling.
|
||||
|
||||
**Step 3: Add safe troubleshooting guidance**
|
||||
- Include only safe operator procedures from the debugging work:
|
||||
- invalid token handling
|
||||
- `sudoReady: false`
|
||||
- Tailscale suspend/resume expectations
|
||||
- what normal redacted output includes
|
||||
- how to use `--debug` when deeper inspection is needed
|
||||
|
||||
### Task 2: Refresh agent-facing skill documentation
|
||||
|
||||
**Files:**
|
||||
- Modify: `skills/nordvpn-client/SKILL.md`
|
||||
- Sync: `/Users/stefano/.openclaw/workspace/skills/nordvpn-client/SKILL.md`
|
||||
|
||||
**Step 1: Tighten the skill instructions**
|
||||
- Keep the doc shorter than the canonical operator guide.
|
||||
- Ensure it explicitly covers the default credential paths, macOS sudoers requirement, Tailscale suspend/resume behavior, and `--debug` usage.
|
||||
|
||||
**Step 2: Sync installed OpenClaw copy**
|
||||
- Copy the updated repo `SKILL.md` into the installed workspace skill path.
|
||||
|
||||
### Task 3: Update summary docs if needed
|
||||
|
||||
**Files:**
|
||||
- Check: `README.md`
|
||||
- Check: `docs/README.md`
|
||||
- Modify only if current summary text is materially missing the current backend model.
|
||||
|
||||
**Step 1: Review summary descriptions**
|
||||
- Confirm whether the one-line descriptions already adequately describe Linux CLI + macOS NordLynx/WireGuard.
|
||||
|
||||
**Step 2: Update only if necessary**
|
||||
- Avoid churn if the existing summaries are already sufficient.
|
||||
|
||||
### Task 4: Verify and publish
|
||||
|
||||
**Files:**
|
||||
- Verify: `docs/nordvpn-client.md`
|
||||
- Verify: `skills/nordvpn-client/SKILL.md`
|
||||
- Verify: `/Users/stefano/.openclaw/workspace/skills/nordvpn-client/SKILL.md`
|
||||
|
||||
**Step 1: Run doc verification checks**
|
||||
- Run: `rg -n "sudoers|visudo|--debug|Tailscale|token.txt|wireguard-helper" docs/nordvpn-client.md skills/nordvpn-client/SKILL.md`
|
||||
- Expected: all required topics present
|
||||
|
||||
**Step 2: Confirm installed workspace skill matches repo skill**
|
||||
- Run: `cmp skills/nordvpn-client/SKILL.md /Users/stefano/.openclaw/workspace/skills/nordvpn-client/SKILL.md`
|
||||
- Expected: no output
|
||||
|
||||
**Step 3: Commit and push**
|
||||
- Commit message: `docs: expand nordvpn client setup and troubleshooting`
|
||||
40
docs/plans/2026-03-12-nordvpn-macos-dns-design.md
Normal file
40
docs/plans/2026-03-12-nordvpn-macos-dns-design.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# NordVPN macOS DNS Design
|
||||
|
||||
## Goal
|
||||
Keep NordVPN DNS while connected on macOS, but only apply it to active physical services so the WireGuard backend does not break Tailscale or other virtual interfaces.
|
||||
|
||||
## Behavior
|
||||
- Keep the generated WireGuard config free of `DNS = ...`
|
||||
- During `connect` on macOS:
|
||||
- detect active physical network services
|
||||
- snapshot current DNS/search-domain settings
|
||||
- set NordVPN DNS only on those physical services
|
||||
- During `disconnect`:
|
||||
- restore the saved DNS/search-domain settings
|
||||
- During failed `connect` after DNS changes:
|
||||
- restore DNS before returning the error
|
||||
|
||||
## DNS Values
|
||||
- IPv4 primary: `103.86.96.100`
|
||||
- IPv4 secondary: `103.86.99.100`
|
||||
- No IPv6 DNS for now
|
||||
|
||||
## Service Selection
|
||||
Include only enabled physical services from `networksetup`.
|
||||
Exclude names matching:
|
||||
- Tailscale
|
||||
- Bridge
|
||||
- Thunderbolt Bridge
|
||||
- Loopback
|
||||
- VPN
|
||||
- utun
|
||||
|
||||
## Persistence
|
||||
- Save DNS snapshot under `~/.nordvpn-client`
|
||||
- Overwrite on each successful connect
|
||||
- Clear after successful disconnect restore
|
||||
|
||||
## Verification
|
||||
- Unit tests for service selection and DNS snapshot/restore helpers
|
||||
- Direct logic/config tests
|
||||
- Avoid live connect tests from this session unless explicitly requested because they can drop connectivity
|
||||
11
docs/plans/2026-03-12-nordvpn-macos-dns.md
Normal file
11
docs/plans/2026-03-12-nordvpn-macos-dns.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# NordVPN macOS DNS Plan
|
||||
|
||||
1. Add macOS DNS state file support under `~/.nordvpn-client`.
|
||||
2. Implement helpers to enumerate eligible physical services and snapshot existing DNS/search-domain settings.
|
||||
3. Implement helpers to apply NordVPN DNS only to eligible physical services.
|
||||
4. Implement helpers to restore previous DNS/search-domain settings on disconnect or failed connect.
|
||||
5. Add unit tests for service filtering and DNS state transitions.
|
||||
6. Update skill/docs to explain macOS physical-service DNS management.
|
||||
7. Sync the installed workspace copy.
|
||||
8. Run tests and non-destructive verification.
|
||||
9. Commit and push.
|
||||
@@ -0,0 +1,26 @@
|
||||
# NordVPN Tailscale Coordination Design
|
||||
|
||||
## Goal
|
||||
Stabilize macOS NordVPN connects by explicitly stopping Tailscale before bringing up the NordVPN WireGuard tunnel, then restarting Tailscale after NordVPN disconnects.
|
||||
|
||||
## Behavior
|
||||
- macOS only
|
||||
- on `connect`:
|
||||
- detect whether Tailscale is active
|
||||
- if active, stop it and record that state
|
||||
- bring up NordVPN
|
||||
- on `disconnect`:
|
||||
- tear down NordVPN
|
||||
- if the skill stopped Tailscale earlier, start it again
|
||||
- clear the saved state
|
||||
- on connect failure after stopping Tailscale:
|
||||
- attempt to start Tailscale again before returning the error
|
||||
|
||||
## State
|
||||
- persist `tailscaleWasActive` under `~/.nordvpn-client`
|
||||
- only restart Tailscale if the skill actually stopped it
|
||||
|
||||
## Rollback target if successful
|
||||
- remove the temporary macOS physical-service DNS management patch
|
||||
- restore the simpler NordVPN config path that uses NordVPN DNS directly in the WireGuard config
|
||||
- keep Tailscale suspend/resume as the macOS coexistence solution
|
||||
10
docs/plans/2026-03-12-nordvpn-tailscale-coordination.md
Normal file
10
docs/plans/2026-03-12-nordvpn-tailscale-coordination.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# NordVPN Tailscale Coordination Plan
|
||||
|
||||
1. Add macOS Tailscale state file support under `~/.nordvpn-client`.
|
||||
2. Implement helpers to detect, stop, and start Tailscale on macOS.
|
||||
3. Add unit tests for Tailscale state transitions.
|
||||
4. Wire Tailscale stop into macOS `connect` before WireGuard up.
|
||||
5. Wire Tailscale restart into macOS `disconnect` and connect-failure rollback.
|
||||
6. Sync the installed workspace copy.
|
||||
7. Run tests and non-destructive verification.
|
||||
8. Commit and push.
|
||||
@@ -32,6 +32,16 @@ pnpm approve-builds
|
||||
pnpm rebuild better-sqlite3 esbuild
|
||||
```
|
||||
|
||||
## Updating CloakBrowser
|
||||
|
||||
```bash
|
||||
cd ~/.openclaw/workspace/skills/web-automation/scripts
|
||||
pnpm up cloakbrowser playwright-core
|
||||
npx cloakbrowser install
|
||||
pnpm approve-builds
|
||||
pnpm rebuild better-sqlite3 esbuild
|
||||
```
|
||||
|
||||
## System libraries (for OpenClaw Docker builds)
|
||||
|
||||
```bash
|
||||
|
||||
108
skills/nordvpn-client/SKILL.md
Normal file
108
skills/nordvpn-client/SKILL.md
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
name: nordvpn-client
|
||||
description: Use when managing NordVPN on macOS or Linux, including install/bootstrap, login, connect, disconnect, status checks, or verifying a VPN location before running another skill.
|
||||
---
|
||||
|
||||
# NordVPN Client
|
||||
|
||||
Cross-platform NordVPN lifecycle management for macOS and Linux hosts.
|
||||
|
||||
## Use This Skill For
|
||||
|
||||
- probing whether NordVPN automation is ready
|
||||
- bootstrapping missing backend dependencies
|
||||
- validating auth
|
||||
- connecting to a country or city
|
||||
- verifying the public exit location
|
||||
- disconnecting and restoring the normal network state
|
||||
|
||||
## Command Surface
|
||||
|
||||
```bash
|
||||
node scripts/nordvpn-client.js status
|
||||
node scripts/nordvpn-client.js install
|
||||
node scripts/nordvpn-client.js login
|
||||
node scripts/nordvpn-client.js verify
|
||||
node scripts/nordvpn-client.js verify --country "Germany"
|
||||
node scripts/nordvpn-client.js verify --country "Japan" --city "Tokyo"
|
||||
node scripts/nordvpn-client.js connect --country "Germany"
|
||||
node scripts/nordvpn-client.js connect --country "Japan" --city "Tokyo"
|
||||
node scripts/nordvpn-client.js disconnect
|
||||
node scripts/nordvpn-client.js status --debug
|
||||
```
|
||||
|
||||
## Backend Model
|
||||
|
||||
- Linux:
|
||||
- use the official `nordvpn` CLI
|
||||
- `install` uses the official NordVPN installer
|
||||
- token login is supported
|
||||
- macOS:
|
||||
- use NordLynx/WireGuard through `wireguard-go` and `wireguard-tools`
|
||||
- `install` bootstraps them with Homebrew
|
||||
- `login` validates the token for the WireGuard backend
|
||||
- Tailscale is suspended before connect and resumed after disconnect or failed connect
|
||||
- `NordVPN.app` may remain installed but is only the manual fallback
|
||||
|
||||
## Credentials
|
||||
|
||||
Default OpenClaw credential paths:
|
||||
|
||||
- token: `~/.openclaw/workspace/.clawdbot/credentials/nordvpn/token.txt`
|
||||
- password: `~/.openclaw/workspace/.clawdbot/credentials/nordvpn/password.txt`
|
||||
|
||||
Supported env vars:
|
||||
|
||||
- `NORDVPN_TOKEN`
|
||||
- `NORDVPN_TOKEN_FILE`
|
||||
- `NORDVPN_USERNAME`
|
||||
- `NORDVPN_PASSWORD`
|
||||
- `NORDVPN_PASSWORD_FILE`
|
||||
|
||||
## macOS Requirements
|
||||
|
||||
Automated macOS connects require all of:
|
||||
|
||||
- `wireguard-go`
|
||||
- `wireguard-tools`
|
||||
- `NORDVPN_TOKEN` or the default token file
|
||||
- non-interactive `sudo` for the installed helper script:
|
||||
- `~/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh`
|
||||
|
||||
Exact `visudo` rule for the installed OpenClaw skill:
|
||||
|
||||
```sudoers
|
||||
stefano ALL=(root) NOPASSWD: /Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh probe, /Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh up, /Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh down
|
||||
```
|
||||
|
||||
## Agent Guidance
|
||||
|
||||
- run `status` first when the machine state is unclear
|
||||
- on macOS, if tooling is missing, run `install`
|
||||
- if auth is unclear, run `login`
|
||||
- use `connect` before location-sensitive skills such as `web-automation`
|
||||
- use `verify` after connect when you need an explicit location check
|
||||
- use `disconnect` after the follow-up task
|
||||
|
||||
## Output Rules
|
||||
|
||||
- normal JSON output redacts local path metadata
|
||||
- use `--debug` only when deeper troubleshooting requires internal local paths and helper/config metadata
|
||||
|
||||
## Troubleshooting Cues
|
||||
|
||||
- `Invalid authorization header`:
|
||||
- token file exists but the token is invalid; replace the token and rerun `login`
|
||||
- `sudoReady: false`:
|
||||
- the helper is not allowed in sudoers; add the `visudo` rule above
|
||||
- connect succeeds but final state looks inconsistent:
|
||||
- rely on the verified public IP/location first
|
||||
- then inspect `status --debug`
|
||||
- disconnect should leave:
|
||||
- normal public IP restored
|
||||
- no active WireGuard state
|
||||
- Tailscale resumed if the skill suspended it
|
||||
|
||||
For full operator setup and troubleshooting, see:
|
||||
|
||||
- `docs/nordvpn-client.md`
|
||||
1426
skills/nordvpn-client/scripts/nordvpn-client.js
Normal file
1426
skills/nordvpn-client/scripts/nordvpn-client.js
Normal file
File diff suppressed because it is too large
Load Diff
309
skills/nordvpn-client/scripts/nordvpn-client.test.js
Normal file
309
skills/nordvpn-client/scripts/nordvpn-client.test.js
Normal file
@@ -0,0 +1,309 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const vm = require("node:vm");
|
||||
|
||||
function loadInternals() {
|
||||
const scriptPath = path.join(__dirname, "nordvpn-client.js");
|
||||
const source = fs.readFileSync(scriptPath, "utf8").replace(/\nmain\(\);\s*$/, "\n");
|
||||
const wrapped = `${source}
|
||||
module.exports = {
|
||||
buildMacTailscaleState:
|
||||
typeof buildMacTailscaleState === "function" ? buildMacTailscaleState : undefined,
|
||||
buildWireguardConfig:
|
||||
typeof buildWireguardConfig === "function" ? buildWireguardConfig : undefined,
|
||||
buildLookupResult:
|
||||
typeof buildLookupResult === "function" ? buildLookupResult : undefined,
|
||||
cleanupMacWireguardState:
|
||||
typeof cleanupMacWireguardState === "function" ? cleanupMacWireguardState : undefined,
|
||||
getMacTailscalePath:
|
||||
typeof getMacTailscalePath === "function" ? getMacTailscalePath : undefined,
|
||||
isMacTailscaleActive:
|
||||
typeof isMacTailscaleActive === "function" ? isMacTailscaleActive : undefined,
|
||||
normalizeSuccessfulConnectState:
|
||||
typeof normalizeSuccessfulConnectState === "function" ? normalizeSuccessfulConnectState : undefined,
|
||||
normalizeStatusState:
|
||||
typeof normalizeStatusState === "function" ? normalizeStatusState : undefined,
|
||||
sanitizeOutputPayload:
|
||||
typeof sanitizeOutputPayload === "function" ? sanitizeOutputPayload : undefined,
|
||||
shouldAttemptMacWireguardDisconnect:
|
||||
typeof shouldAttemptMacWireguardDisconnect === "function" ? shouldAttemptMacWireguardDisconnect : undefined,
|
||||
detectMacWireguardActiveFromIfconfig:
|
||||
typeof detectMacWireguardActiveFromIfconfig === "function" ? detectMacWireguardActiveFromIfconfig : undefined,
|
||||
resolveHostnameWithFallback:
|
||||
typeof resolveHostnameWithFallback === "function" ? resolveHostnameWithFallback : undefined,
|
||||
verifyConnectionWithRetry:
|
||||
typeof verifyConnectionWithRetry === "function" ? verifyConnectionWithRetry : undefined,
|
||||
};`;
|
||||
|
||||
const sandbox = {
|
||||
require,
|
||||
module: { exports: {} },
|
||||
exports: {},
|
||||
__dirname,
|
||||
__filename: scriptPath,
|
||||
process: { ...process, exit() {} },
|
||||
console,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
Buffer,
|
||||
};
|
||||
|
||||
vm.runInNewContext(wrapped, sandbox, { filename: scriptPath });
|
||||
return sandbox.module.exports;
|
||||
}
|
||||
|
||||
test("detectMacWireguardActiveFromIfconfig detects nordvpn utun client address", () => {
|
||||
const { detectMacWireguardActiveFromIfconfig } = loadInternals();
|
||||
assert.equal(typeof detectMacWireguardActiveFromIfconfig, "function");
|
||||
|
||||
const ifconfig = `
|
||||
utun8: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1380
|
||||
utun9: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1420
|
||||
\tinet 10.5.0.2 --> 10.5.0.2 netmask 0xff000000
|
||||
`;
|
||||
|
||||
assert.equal(detectMacWireguardActiveFromIfconfig(ifconfig), true);
|
||||
assert.equal(detectMacWireguardActiveFromIfconfig("utun7: flags=8051\n\tinet 100.64.0.4"), false);
|
||||
});
|
||||
|
||||
test("buildLookupResult supports lookup all=true mode", () => {
|
||||
const { buildLookupResult } = loadInternals();
|
||||
assert.equal(typeof buildLookupResult, "function");
|
||||
assert.equal(
|
||||
JSON.stringify(buildLookupResult("104.26.9.44", { all: true })),
|
||||
JSON.stringify([{ address: "104.26.9.44", family: 4 }])
|
||||
);
|
||||
assert.equal(JSON.stringify(buildLookupResult("104.26.9.44", { all: false })), JSON.stringify(["104.26.9.44", 4]));
|
||||
});
|
||||
|
||||
test("buildWireguardConfig includes NordVPN DNS for the vanilla macOS config path", () => {
|
||||
const { buildWireguardConfig } = loadInternals();
|
||||
assert.equal(typeof buildWireguardConfig, "function");
|
||||
|
||||
const config = buildWireguardConfig(
|
||||
{
|
||||
hostname: "tr73.nordvpn.com",
|
||||
ips: [{ ip: { version: 4, ip: "45.89.52.1" } }],
|
||||
technologies: [{ identifier: "wireguard_udp", metadata: [{ name: "public_key", value: "PUBKEY" }] }],
|
||||
},
|
||||
"PRIVATEKEY"
|
||||
);
|
||||
|
||||
assert.equal(config.includes("DNS = 103.86.96.100, 103.86.99.100"), true);
|
||||
assert.equal(config.includes("AllowedIPs = 0.0.0.0/0"), true);
|
||||
});
|
||||
|
||||
test("getMacTailscalePath falls back to /opt/homebrew/bin/tailscale when PATH lookup is missing", () => {
|
||||
const { getMacTailscalePath } = loadInternals();
|
||||
assert.equal(typeof getMacTailscalePath, "function");
|
||||
assert.equal(
|
||||
getMacTailscalePath({
|
||||
commandExists: () => "",
|
||||
fileExists: (target) => target === "/opt/homebrew/bin/tailscale",
|
||||
}),
|
||||
"/opt/homebrew/bin/tailscale"
|
||||
);
|
||||
});
|
||||
|
||||
test("buildMacTailscaleState records whether tailscale was active", () => {
|
||||
const { buildMacTailscaleState } = loadInternals();
|
||||
assert.equal(typeof buildMacTailscaleState, "function");
|
||||
assert.equal(
|
||||
JSON.stringify(buildMacTailscaleState(true)),
|
||||
JSON.stringify({
|
||||
tailscaleWasActive: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("cleanupMacWireguardState removes stale config and last-connection files", () => {
|
||||
const { cleanupMacWireguardState } = loadInternals();
|
||||
assert.equal(typeof cleanupMacWireguardState, "function");
|
||||
|
||||
const tmpDir = fs.mkdtempSync(path.join(fs.mkdtempSync("/tmp/nordvpn-client-test-"), "state-"));
|
||||
const configPath = path.join(tmpDir, "nordvpnctl.conf");
|
||||
const lastConnectionPath = path.join(tmpDir, "last-connection.json");
|
||||
fs.writeFileSync(configPath, "wireguard-config");
|
||||
fs.writeFileSync(lastConnectionPath, "{\"country\":\"Germany\"}");
|
||||
|
||||
const result = cleanupMacWireguardState({
|
||||
configPath,
|
||||
lastConnectionPath,
|
||||
});
|
||||
|
||||
assert.equal(result.cleaned, true);
|
||||
assert.equal(fs.existsSync(configPath), false);
|
||||
assert.equal(fs.existsSync(lastConnectionPath), false);
|
||||
});
|
||||
|
||||
test("shouldAttemptMacWireguardDisconnect does not trust active=false when residual state exists", () => {
|
||||
const { shouldAttemptMacWireguardDisconnect } = loadInternals();
|
||||
assert.equal(typeof shouldAttemptMacWireguardDisconnect, "function");
|
||||
|
||||
assert.equal(
|
||||
shouldAttemptMacWireguardDisconnect({
|
||||
active: false,
|
||||
configPath: "/Users/stefano/.nordvpn-client/wireguard/nordvpnctl.conf",
|
||||
endpoint: null,
|
||||
lastConnection: null,
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
shouldAttemptMacWireguardDisconnect({
|
||||
active: false,
|
||||
configPath: null,
|
||||
endpoint: null,
|
||||
lastConnection: { country: "Italy" },
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
shouldAttemptMacWireguardDisconnect({
|
||||
active: false,
|
||||
configPath: null,
|
||||
endpoint: null,
|
||||
lastConnection: null,
|
||||
}),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizeSuccessfulConnectState marks the connect snapshot active after verified macOS wireguard connect", () => {
|
||||
const { normalizeSuccessfulConnectState } = loadInternals();
|
||||
assert.equal(typeof normalizeSuccessfulConnectState, "function");
|
||||
|
||||
const state = normalizeSuccessfulConnectState(
|
||||
{
|
||||
platform: "darwin",
|
||||
controlMode: "wireguard",
|
||||
connected: false,
|
||||
wireguard: {
|
||||
active: false,
|
||||
endpoint: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
backend: "wireguard",
|
||||
server: {
|
||||
hostname: "de1227.nordvpn.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
ok: true,
|
||||
ipInfo: {
|
||||
country: "Germany",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(state.connected, true);
|
||||
assert.equal(state.wireguard.active, true);
|
||||
assert.equal(state.wireguard.endpoint, "de1227.nordvpn.com:51820");
|
||||
});
|
||||
|
||||
test("normalizeStatusState marks macOS wireguard connected when public IP matches the last successful target", () => {
|
||||
const { normalizeStatusState } = loadInternals();
|
||||
assert.equal(typeof normalizeStatusState, "function");
|
||||
|
||||
const state = normalizeStatusState({
|
||||
platform: "darwin",
|
||||
controlMode: "wireguard",
|
||||
connected: false,
|
||||
wireguard: {
|
||||
active: false,
|
||||
endpoint: "tr73.nordvpn.com:51820",
|
||||
lastConnection: {
|
||||
requestedTarget: { country: "Turkey", city: "" },
|
||||
resolvedTarget: { country: "Turkey", city: "Istanbul" },
|
||||
},
|
||||
},
|
||||
publicIp: {
|
||||
ok: true,
|
||||
country: "Turkey",
|
||||
city: "Istanbul",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(state.connected, true);
|
||||
assert.equal(state.wireguard.active, true);
|
||||
});
|
||||
|
||||
test("sanitizeOutputPayload redacts local path metadata from normal JSON output", () => {
|
||||
const { sanitizeOutputPayload } = loadInternals();
|
||||
assert.equal(typeof sanitizeOutputPayload, "function");
|
||||
|
||||
const sanitized = sanitizeOutputPayload({
|
||||
cliPath: "/opt/homebrew/bin/nordvpn",
|
||||
appPath: "/Applications/NordVPN.app",
|
||||
wireguard: {
|
||||
configPath: "/Users/stefano/.nordvpn-client/wireguard/nordvpnctl.conf",
|
||||
helperPath: "/Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh",
|
||||
authCache: {
|
||||
tokenSource: "default:/Users/stefano/.openclaw/workspace/.clawdbot/credentials/nordvpn/token.txt",
|
||||
},
|
||||
endpoint: "jp454.nordvpn.com:51820",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(sanitized.cliPath, null);
|
||||
assert.equal(sanitized.appPath, null);
|
||||
assert.equal(sanitized.wireguard.configPath, null);
|
||||
assert.equal(sanitized.wireguard.helperPath, null);
|
||||
assert.equal(sanitized.wireguard.authCache.tokenSource, null);
|
||||
assert.equal(sanitized.wireguard.endpoint, "jp454.nordvpn.com:51820");
|
||||
});
|
||||
|
||||
test("isMacTailscaleActive treats Running backend as active", () => {
|
||||
const { isMacTailscaleActive } = loadInternals();
|
||||
assert.equal(typeof isMacTailscaleActive, "function");
|
||||
assert.equal(isMacTailscaleActive({ BackendState: "Running" }), true);
|
||||
assert.equal(isMacTailscaleActive({ BackendState: "Stopped" }), false);
|
||||
});
|
||||
|
||||
test("verifyConnectionWithRetry retries transient reachability failures", async () => {
|
||||
const { verifyConnectionWithRetry } = loadInternals();
|
||||
assert.equal(typeof verifyConnectionWithRetry, "function");
|
||||
|
||||
let attempts = 0;
|
||||
const result = await verifyConnectionWithRetry(
|
||||
{ country: "Italy", city: "Milan" },
|
||||
{
|
||||
attempts: 3,
|
||||
delayMs: 1,
|
||||
getPublicIpInfo: async () => {
|
||||
attempts += 1;
|
||||
if (attempts === 1) {
|
||||
return { ok: false, error: "read EHOSTUNREACH" };
|
||||
}
|
||||
return { ok: true, country: "Italy", city: "Milan" };
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.ipInfo.country, "Italy");
|
||||
assert.equal(attempts, 2);
|
||||
});
|
||||
|
||||
test("resolveHostnameWithFallback uses fallback resolvers when system lookup fails", async () => {
|
||||
const { resolveHostnameWithFallback } = loadInternals();
|
||||
assert.equal(typeof resolveHostnameWithFallback, "function");
|
||||
|
||||
const calls = [];
|
||||
const address = await resolveHostnameWithFallback("ipapi.co", {
|
||||
resolvers: ["1.1.1.1", "8.8.8.8"],
|
||||
resolveWithResolver: async (hostname, resolver) => {
|
||||
calls.push(`${resolver}:${hostname}`);
|
||||
if (resolver === "1.1.1.1") return [];
|
||||
return ["104.26.9.44"];
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(address, "104.26.9.44");
|
||||
assert.deepEqual(calls, ["1.1.1.1:ipapi.co", "8.8.8.8:ipapi.co"]);
|
||||
});
|
||||
24
skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh
Executable file
24
skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ACTION="${1:-}"
|
||||
case "$ACTION" in
|
||||
probe|up|down)
|
||||
;;
|
||||
*)
|
||||
echo "Usage: nordvpn-wireguard-helper.sh [probe|up|down]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
WG_QUICK="/opt/homebrew/bin/wg-quick"
|
||||
WG_CONFIG="/Users/stefano/.nordvpn-client/wireguard/nordvpnctl.conf"
|
||||
PATH="/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH
|
||||
|
||||
if [ "$ACTION" = "probe" ]; then
|
||||
test -x "$WG_QUICK"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exec "$WG_QUICK" "$ACTION" "$WG_CONFIG"
|
||||
@@ -32,14 +32,24 @@ pnpm approve-builds
|
||||
pnpm rebuild better-sqlite3 esbuild
|
||||
```
|
||||
|
||||
## Updating CloakBrowser
|
||||
|
||||
```bash
|
||||
cd ~/.openclaw/workspace/skills/web-automation/scripts
|
||||
pnpm up cloakbrowser playwright-core
|
||||
npx cloakbrowser install
|
||||
pnpm approve-builds
|
||||
pnpm rebuild better-sqlite3 esbuild
|
||||
```
|
||||
|
||||
## Prerequisite Check (MANDATORY)
|
||||
|
||||
Before running any automation, verify CloakBrowser and Playwright Core dependencies are installed and scripts are configured to use CloakBrowser.
|
||||
|
||||
```bash
|
||||
cd ~/.openclaw/workspace/skills/web-automation/scripts
|
||||
node -e "require.resolve('cloakbrowser');require.resolve('playwright-core/package.json');console.log('OK: cloakbrowser + playwright-core installed')"
|
||||
node -e "const fs=require('fs');const t=fs.readFileSync('browse.ts','utf8');if(!/launchPersistentContext\s*from\s*\'cloakbrowser\'/.test(t)){throw new Error('browse.ts is not configured for CloakBrowser')}console.log('OK: CloakBrowser integration detected in browse.ts')"
|
||||
node --input-type=module -e "await import('cloakbrowser');import 'playwright-core';console.log('OK: cloakbrowser + playwright-core installed')"
|
||||
node -e "const fs=require('fs');const t=fs.readFileSync('browse.ts','utf8');if(!/import\s*\{[^}]*launchPersistentContext[^}]*\}\s*from\s*['\"]cloakbrowser['\"]/.test(t)){throw new Error('browse.ts is not configured for CloakBrowser')}console.log('OK: CloakBrowser integration detected in browse.ts')"
|
||||
```
|
||||
|
||||
If any check fails, stop and return:
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"cloakbrowser": "^0.3.14",
|
||||
"jsdom": "^24.0.0",
|
||||
"minimist": "^1.2.8",
|
||||
"playwright-core": "^1.40.0",
|
||||
"playwright-core": "^1.58.2",
|
||||
"turndown": "^7.1.2",
|
||||
"turndown-plugin-gfm": "^1.0.2"
|
||||
},
|
||||
|
||||
16
skills/web-automation/scripts/pnpm-lock.yaml
generated
16
skills/web-automation/scripts/pnpm-lock.yaml
generated
@@ -16,7 +16,7 @@ importers:
|
||||
version: 12.6.2
|
||||
cloakbrowser:
|
||||
specifier: ^0.3.14
|
||||
version: 0.3.14(mmdb-lib@3.0.1)(playwright-core@1.57.0)
|
||||
version: 0.3.14(mmdb-lib@3.0.1)(playwright-core@1.58.2)
|
||||
jsdom:
|
||||
specifier: ^24.0.0
|
||||
version: 24.1.3
|
||||
@@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^1.2.8
|
||||
version: 1.2.8
|
||||
playwright-core:
|
||||
specifier: ^1.40.0
|
||||
version: 1.57.0
|
||||
specifier: ^1.58.2
|
||||
version: 1.58.2
|
||||
turndown:
|
||||
specifier: ^7.1.2
|
||||
version: 7.2.2
|
||||
@@ -534,8 +534,8 @@ packages:
|
||||
parse5@7.3.0:
|
||||
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||
|
||||
playwright-core@1.57.0:
|
||||
resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==}
|
||||
playwright-core@1.58.2:
|
||||
resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
@@ -873,12 +873,12 @@ snapshots:
|
||||
|
||||
chownr@3.0.0: {}
|
||||
|
||||
cloakbrowser@0.3.14(mmdb-lib@3.0.1)(playwright-core@1.57.0):
|
||||
cloakbrowser@0.3.14(mmdb-lib@3.0.1)(playwright-core@1.58.2):
|
||||
dependencies:
|
||||
tar: 7.5.11
|
||||
optionalDependencies:
|
||||
mmdb-lib: 3.0.1
|
||||
playwright-core: 1.57.0
|
||||
playwright-core: 1.58.2
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
@@ -1122,7 +1122,7 @@ snapshots:
|
||||
dependencies:
|
||||
entities: 6.0.1
|
||||
|
||||
playwright-core@1.57.0: {}
|
||||
playwright-core@1.58.2: {}
|
||||
|
||||
prebuild-install@7.1.3:
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user