pi-webveil 0.1.0 → 0.1.1
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 +233 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,33 +13,258 @@ works perfectly well non-anonymously (direct egress).
|
|
|
13
13
|
webveil is a pnpm workspace monorepo. The **core** (`search()` / `fetch()`) is plain,
|
|
14
14
|
framework-agnostic. Two thin frontends wrap that same core:
|
|
15
15
|
|
|
16
|
-
- **[`webveil`](packages/webveil)
|
|
16
|
+
- **[`webveil`](packages/webveil)**, an [incur](https://github.com/wevm/incur)-based
|
|
17
17
|
**CLI + MCP server** (`--mcp`, skills, `--llms`, TOON output). Pi-agnostic; usable by any
|
|
18
18
|
agent (pi via pi-mcp-adapter, Claude Code, Cursor, Codex, bash). Has a `webveil` bin.
|
|
19
|
-
- **[`pi-webveil`](packages/pi-webveil)
|
|
19
|
+
- **[`pi-webveil`](packages/pi-webveil)**, a **pi extension** registering `web_search` and
|
|
20
20
|
`web_fetch` tools that call the core in-process. A drop-in replacement for Ollama's tools
|
|
21
21
|
(same names), which is the original motivation. Depends on `webveil` via `workspace:*`.
|
|
22
22
|
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
webveil needs a **backend** to get results from. The zero-config default is a local
|
|
26
|
+
**SearXNG** at `http://127.0.0.1:8080` on `direct` egress (non-anonymous). There is
|
|
27
|
+
**no** zero-setup + anonymous + real-web-results option in the ecosystem, see
|
|
28
|
+
[`work/notes/ideas/default-backend-policy-account-vs-origin.md`](work/notes/ideas/default-backend-policy-account-vs-origin.md);
|
|
29
|
+
SearXNG (you run it) is the closest, `tavily-compat` (needs an account/key) is the other.
|
|
30
|
+
|
|
31
|
+
### Run SearXNG (matches the default with no config)
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
# Docker: the container binds 8080 internally; map host 8080 -> container 8080
|
|
35
|
+
# so it matches webveil's default baseUrl exactly.
|
|
36
|
+
docker run -d --name searxng -p 8080:8080 searxng/searxng
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then `webveil search "…"` / `web_fetch` work with no config.
|
|
40
|
+
|
|
41
|
+
> **Port gotcha (you WILL hit this):** SearXNG's default port depends on how you install
|
|
42
|
+
> it. A bare-metal / pip / source install defaults to **8888** (`settings.yml`
|
|
43
|
+
> `server.port: 8888`). The Docker image binds **8080** internally regardless (its
|
|
44
|
+
> entrypoint forces `0.0.0.0:8080`). SearXNG's own docs suggest `docker run … -p 8888:8080`
|
|
45
|
+
> (host 8888 → container 8080). webveil's default expects **8080**. If your instance is on
|
|
46
|
+
> any other port, point webveil at it:
|
|
47
|
+
>
|
|
48
|
+
> ```sh
|
|
49
|
+
> export WEBVEIL_BASE_URL=http://127.0.0.1:8888 # or wherever your instance listens
|
|
50
|
+
> ```
|
|
51
|
+
>
|
|
52
|
+
> or set `baseUrl` in `.pi/webveil.json` (see config seam below).
|
|
53
|
+
|
|
54
|
+
### Other SearXNG install options
|
|
55
|
+
|
|
56
|
+
webveil needs something to point `baseUrl` at: an **HTTP `host:port`**, or (script install)
|
|
57
|
+
the **Unix socket** itself. How you get one:
|
|
58
|
+
|
|
59
|
+
- **Docker (above)**, binds a real TCP port directly; simplest if you only need webveil.
|
|
60
|
+
- **Install script as a background service** (`sudo -H ./utils/searxng.sh install all`,
|
|
61
|
+
see <https://docs.searxng.org/admin/installation-scripts.html>), sets SearXNG up as a
|
|
62
|
+
systemd/uWSGI service. **Gotcha:** by default this listens on a **Unix socket**
|
|
63
|
+
(`socket = /usr/local/searxng/run/socket`), NOT a TCP port. And, crucially, that default
|
|
64
|
+
socket speaks the **native uwsgi protocol, NOT HTTP** (`socket = …`, not `http-socket =
|
|
65
|
+
…`), so even a `curl --unix-socket … http://localhost/` returns HTTP 000. webveil's
|
|
66
|
+
`unix:` baseUrl speaks **HTTP over a unix socket** via undici, so it CANNOT reach that
|
|
67
|
+
default uwsgi socket directly. Three ways to reach the install-script instance:
|
|
68
|
+
- **Point webveil straight at an HTTP unix socket** (no proxy, no extra process), once the
|
|
69
|
+
socket actually speaks HTTP. The install-script default does NOT, so first make uWSGI
|
|
70
|
+
serve HTTP on the socket: in the generated `.ini`, replace
|
|
71
|
+
`socket = /usr/local/searxng/run/socket` with
|
|
72
|
+
`http-socket = /usr/local/searxng/run/socket` (HTTP over the socket instead of the
|
|
73
|
+
uwsgi protocol). THEN point webveil at it with a `unix:` URL naming the socket file:
|
|
74
|
+
```sh
|
|
75
|
+
export WEBVEIL_BASE_URL=unix:/usr/local/searxng/run/socket
|
|
76
|
+
```
|
|
77
|
+
webveil dials the socket directly over undici (`Agent({connect:{socketPath}})`, no
|
|
78
|
+
extra dependency) and issues its normal `/search?...&format=json` request. The grammar
|
|
79
|
+
is `unix:<socketPath>[:<httpPath>]`: the socket file path, then an OPTIONAL `:` +
|
|
80
|
+
base path (mount point) the SearXNG app lives under (defaults to `/`, so the example
|
|
81
|
+
above requests `/search`; a non-root mount is `unix:/usr/local/searxng/run/socket:/searxng`).
|
|
82
|
+
(`unix:` works against ANY HTTP-on-a-unix-socket server, e.g. a Caddy/nginx upstream
|
|
83
|
+
bound to a socket; the uwsgi-vs-`http-socket` distinction above is the SearXNG-specific
|
|
84
|
+
catch.)
|
|
85
|
+
**Egress must be `direct`** for this: a Unix socket is inherently local, so combining a
|
|
86
|
+
`unix:` baseUrl with `egress=http`/`socks5` fails loud (proxying a local hop is fake
|
|
87
|
+
anonymity, see "Where does anonymity live?" below; proxy SearXNG's `outgoing.proxies`
|
|
88
|
+
instead and keep webveil `direct`).
|
|
89
|
+
- **Front it with a reverse proxy** (this is what the SearXNG docs' nginx/apache step is
|
|
90
|
+
for, it bridges HTTP-on-a-port to the uWSGI socket, serving BOTH the browser UI and
|
|
91
|
+
webveil). **Any HTTP server works**, the docs say so explicitly; **Caddy is fine** and
|
|
92
|
+
a good pick if you already run it. Plain Caddy `reverse_proxy` speaks **HTTP** to its
|
|
93
|
+
upstream, so point it at an `http-socket` (see below) or a TCP `http-socket`:
|
|
94
|
+
```caddy
|
|
95
|
+
searxng.example.com {
|
|
96
|
+
reverse_proxy unix//usr/local/searxng/run/socket # plain reverse_proxy = HTTP, so the socket must be http-socket = (not the uwsgi socket =)
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
Then point webveil at the Caddy address. (Set SearXNG's `server.base_url` in
|
|
100
|
+
`settings.yml` to match, and keep the limiter in mind, see below.) If you want a Caddy
|
|
101
|
+
frontend AND webveil-direct, the simplest path is ONE `http-socket` that both consume
|
|
102
|
+
(Caddy's HTTP `reverse_proxy` and webveil's `unix:` both speak HTTP to it); you only
|
|
103
|
+
need the uwsgi `socket = ` form if Caddy uses an explicit uwsgi transport.
|
|
104
|
+
- **Or make uWSGI listen on a TCP port** instead of the socket: in the generated
|
|
105
|
+
`.ini`, replace `socket = …/run/socket` with `http-socket = 127.0.0.1:8888`, then point
|
|
106
|
+
webveil at `http://127.0.0.1:8888`. Good when you want ONLY webveil (no public web UI /
|
|
107
|
+
TLS).
|
|
108
|
+
|
|
109
|
+
> **You will also need to enable the JSON API and (for a local instance) disable the
|
|
110
|
+
> limiter.** A fresh script install ships with `server.limiter: true` and often no `json`
|
|
111
|
+
> output format, so webveil gets `429 TOO MANY REQUESTS` or an HTML page. In SearXNG's
|
|
112
|
+
> `settings.yml` set `server.limiter: false` + `server.public_instance: false` (safe for a
|
|
113
|
+
> LOCAL, socket-only instance, NOT internet-exposed) and add `json` under `search.formats:`
|
|
114
|
+
> (`[html, json]`), then restart uWSGI. This applies to EVERY option above, it is a
|
|
115
|
+
> SearXNG-side requirement, not a webveil one.
|
|
116
|
+
|
|
117
|
+
Full SearXNG install options (Docker, Compose, script, bare-metal): the official docs at
|
|
118
|
+
<https://docs.searxng.org/admin/installation.html>. Install topology + the
|
|
119
|
+
uwsgi-vs-`http-socket`, limiter, and reverse-proxy details captured in
|
|
120
|
+
[`work/notes/findings/searxng-install-topology.md`](work/notes/findings/searxng-install-topology.md)
|
|
121
|
+
and
|
|
122
|
+
[`work/notes/findings/searxng-script-socket-is-uwsgi-not-http.md`](work/notes/findings/searxng-script-socket-is-uwsgi-not-http.md).
|
|
123
|
+
|
|
124
|
+
### Where does anonymity live? (read before turning on egress)
|
|
125
|
+
|
|
126
|
+
**webveil's egress only anonymizes webveil's OWN outbound hop** (webveil → backend, and
|
|
127
|
+
`web_fetch` → the target URL). It does NOT anonymize what a backend does next. This has a
|
|
128
|
+
load-bearing consequence for SearXNG:
|
|
129
|
+
|
|
130
|
+
- A **local** SearXNG makes its actual search-engine requests (→ Google/Bing/…) from
|
|
131
|
+
**its own process, on your machine, with your real IP**. That hop is OUTSIDE webveil's
|
|
132
|
+
egress. So setting `WEBVEIL_EGRESS=socks5` while `baseUrl` is `127.0.0.1` does **NOT**
|
|
133
|
+
make your searches anonymous, webveil would just be proxying a pointless localhost call,
|
|
134
|
+
while SearXNG crawls the web from your real IP. That is **false confidence**, the worst
|
|
135
|
+
outcome.
|
|
136
|
+
- **webveil refuses this combo (fail-loud):** a non-`direct` egress (`http`/`socks5`) with
|
|
137
|
+
a **loopback `baseUrl`** is rejected with an error, rather than silently giving you fake
|
|
138
|
+
anonymity. (A *remote* SearXNG over SOCKS is legitimate and allowed, the guard keys on
|
|
139
|
+
loopback specifically.)
|
|
140
|
+
|
|
141
|
+
So the correct setups:
|
|
142
|
+
|
|
143
|
+
| Goal | webveil egress | backend | Who anonymizes the web hop |
|
|
144
|
+
| --- | --- | --- | --- |
|
|
145
|
+
| Local SearXNG, anonymous searches | `direct` | local SearXNG | **SearXNG itself**, set its `outgoing.proxies` (Tor/SOCKS) in `settings.yml` |
|
|
146
|
+
| Remote SearXNG, hide your IP from it | `socks5` | the **remote** SearXNG url | webveil's hop (Mullvad/Tor) |
|
|
147
|
+
| Anonymous `web_fetch` of arbitrary URLs | `socks5` | (any) | webveil's hop |
|
|
148
|
+
| Non-anonymous everyday use | `direct` | local SearXNG | nobody (honest) |
|
|
149
|
+
|
|
150
|
+
Rule of thumb: **proxy the hop that actually reaches the public internet.** For a
|
|
151
|
+
self-hosted SearXNG that hop is SearXNG's, so the proxy goes on SearXNG
|
|
152
|
+
(`outgoing.proxies`), and webveil stays `direct`. webveil's `socks5` mode is for *remote*
|
|
153
|
+
backends and for `web_fetch`. See
|
|
154
|
+
[`work/notes/findings/webveil-anonymity-boundary.md`](work/notes/findings/webveil-anonymity-boundary.md).
|
|
155
|
+
|
|
23
156
|
## How it works (seams)
|
|
24
157
|
|
|
25
|
-
- **core
|
|
158
|
+
- **core**, the framework-agnostic `search(query, opts)` and `fetch(url, opts)` functions.
|
|
26
159
|
Both frontends call the same core.
|
|
27
|
-
- **backend seam
|
|
160
|
+
- **backend seam**, where results/content come from: `searxng` (keyless self-hosted
|
|
28
161
|
metasearch), `tavily-compat` (a generic Tavily-shaped `/search` + `/extract`), and
|
|
29
162
|
`custom` (a local command via a JSON stdin/stdout contract). The backend is handed a
|
|
30
163
|
proxied `http` helper so it cannot bypass egress.
|
|
31
|
-
- **egress seam
|
|
164
|
+
- **egress seam**, how outbound HTTP leaves the machine: `direct`, `http` (undici
|
|
32
165
|
`ProxyAgent`), or `socks5` (Tor `127.0.0.1:9050`, Mullvad `10.64.0.1:1080`). SOCKS5 is
|
|
33
166
|
the mode that matters for anonymity. Fail-loud if a configured proxy cannot be built.
|
|
34
|
-
|
|
167
|
+
**Egress is per-request and scoped to webveil ONLY**, it is NOT a system-wide proxy. It
|
|
168
|
+
governs webveil's own search/fetch traffic (and the `fetch` it injects into distilly),
|
|
169
|
+
and nothing else: your shell, `git push`, the browser, and the OS are untouched. So
|
|
170
|
+
webveil on `socks5` does NOT route your `git push` through the proxy. See
|
|
171
|
+
[Anonymous egress](#anonymous-egress-mullvad--tor) and
|
|
172
|
+
[`work/notes/findings/mullvad-socks5-egress-mechanics.md`](work/notes/findings/mullvad-socks5-egress-mechanics.md).
|
|
173
|
+
- **config seam**, per-folder resolution: env > nearest `.pi/webveil.json` walking up from
|
|
35
174
|
cwd > global `~/.pi/agent/webveil.json` > defaults. Per folder = per account/egress.
|
|
36
|
-
- **extractor seam
|
|
175
|
+
- **extractor seam**, `urlToMarkdown` via `distilly/fetch` by default, injected with
|
|
37
176
|
webveil's egress-bound `fetch`; a backend's own `/extract` (Tavily-compat) may override
|
|
38
177
|
it. Owns the context-friendly markdown + size presets (`s`/`m`/`l`/`f`). See
|
|
39
178
|
[`docs/adr/0001`](docs/adr/0001-extractor-uses-distilly-fetch-with-injected-egress.md).
|
|
40
|
-
- **security
|
|
179
|
+
- **security**, an SSRF guard lives in the egress fetch, so it covers distilly's
|
|
41
180
|
rule-rewritten requests too.
|
|
42
181
|
|
|
182
|
+
## Anonymous egress (Mullvad / Tor)
|
|
183
|
+
|
|
184
|
+
By default webveil uses `direct` egress (your real IP, non-anonymous). Anonymity is
|
|
185
|
+
**opt-in**: it is enabled ONLY when you set it in config/env. webveil never auto-enables a
|
|
186
|
+
proxy (silent anonymity would be a footgun in the other direction).
|
|
187
|
+
|
|
188
|
+
Enable SOCKS5 egress for webveil:
|
|
189
|
+
|
|
190
|
+
```sh
|
|
191
|
+
export WEBVEIL_EGRESS=socks5
|
|
192
|
+
export WEBVEIL_EGRESS_URL=socks5://10.64.0.1:1080 # Mullvad
|
|
193
|
+
# or socks5://127.0.0.1:9050 # Tor
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
or per folder in `.pi/webveil.json`:
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{ "egress": { "mode": "socks5", "url": "socks5://10.64.0.1:1080" } }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Two layers keep your `git push` (and everything else) off the proxy
|
|
203
|
+
|
|
204
|
+
A common worry: "if I route through Mullvad, will my `git push` to GitHub leak under the
|
|
205
|
+
VPN exit IP?" With webveil, **no**, for two independent reasons:
|
|
206
|
+
|
|
207
|
+
1. **webveil's egress is per-request and webveil-only.** It applies the SOCKS5 dispatcher
|
|
208
|
+
inside its own search/fetch code; it does not install a system proxy. `git`, your shell,
|
|
209
|
+
and the OS are never touched. webveil on `socks5` proxies webveil's traffic and nothing
|
|
210
|
+
else.
|
|
211
|
+
2. **You configure split routing** (below) so that even at the OS level, only the proxy IP
|
|
212
|
+
goes through the tunnel.
|
|
213
|
+
|
|
214
|
+
### Mullvad: use the SOCKS5 proxy WITHOUT tunnelling all your traffic
|
|
215
|
+
|
|
216
|
+
Mullvad's SOCKS5 proxy at `10.64.0.1:1080` **only exists while a Mullvad WireGuard tunnel
|
|
217
|
+
is up** (it is reachable only through the tunnel). The trick is to keep the tunnel up but
|
|
218
|
+
tell WireGuard NOT to route your normal traffic through it, only the proxy IP. Add this to
|
|
219
|
+
your Mullvad WireGuard `.conf` (`[Interface]` section):
|
|
220
|
+
|
|
221
|
+
```ini
|
|
222
|
+
Table = off
|
|
223
|
+
PostUp = ip -4 route add 10.64.0.1/32 dev %i; ip -4 route add 10.124.0.0/22 dev %i
|
|
224
|
+
PreDown = ip -4 route delete 10.64.0.1/32 dev %i; ip -4 route delete 10.124.0.0/22 dev %i
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`Table = off` stops WireGuard from grabbing the default route; the manual routes send ONLY
|
|
228
|
+
Mullvad's SOCKS5 proxy IPs through the tunnel (`10.124.0.0/22` is the multihop range).
|
|
229
|
+
Result: webveil's SOCKS5 requests exit via Mullvad; all other traffic (git, browser, OS)
|
|
230
|
+
uses your normal ISP connection. (Simpler alternative: leave WireGuard's routing alone and
|
|
231
|
+
rely on layer 1, but split routing is the belt-and-braces version.)
|
|
232
|
+
|
|
233
|
+
Verify the proxy works: `curl https://ipv4.am.i.mullvad.net --socks5-hostname 10.64.0.1`
|
|
234
|
+
should return a Mullvad exit IP; a plain `curl https://am.i.mullvad.net` should return your
|
|
235
|
+
real IP (proving only the proxy is tunnelled).
|
|
236
|
+
|
|
237
|
+
### "Different exit identity for webveil than for the rest of the machine"
|
|
238
|
+
|
|
239
|
+
If you want webveil to exit somewhere different from your system, you have options, but be
|
|
240
|
+
clear on what is and isn't possible (see
|
|
241
|
+
[`work/notes/findings/mullvad-socks5-egress-mechanics.md`](work/notes/findings/mullvad-socks5-egress-mechanics.md)):
|
|
242
|
+
|
|
243
|
+
- **Different exit LOCATION, same account (easy).** Point webveil at a specific multihop
|
|
244
|
+
SOCKS5 host so it exits elsewhere than your tunnel's entry:
|
|
245
|
+
`WEBVEIL_EGRESS_URL=socks5://us-nyc-wg-socks5-001.relays.mullvad.net:1080`. Your tunnel
|
|
246
|
+
enters where your Mullvad app is connected; webveil's traffic exits in NYC. Same Mullvad
|
|
247
|
+
account, unlinkable-by-location.
|
|
248
|
+
- **Two DIFFERENT Mullvad ACCOUNTS at once (hard, not a webveil feature).** Mullvad's
|
|
249
|
+
SOCKS5 proxy is a property of the ONE active WireGuard tunnel, which is tied to ONE
|
|
250
|
+
account's key. SOCKS5 multihop changes exit location, NOT account. To run account A
|
|
251
|
+
system-wide AND account B for webveil simultaneously, you must isolate them at the OS
|
|
252
|
+
level: run webveil inside its own network namespace / VM / container that has its own
|
|
253
|
+
WireGuard tunnel on account B, while the host runs account A. That is infrastructure work
|
|
254
|
+
outside webveil. For most people, "don't link my searches to my git" is already solved by
|
|
255
|
+
split routing above (searches exit via Mullvad, git stays on your real IP, not correlated
|
|
256
|
+
by exit IP), without needing a second account.
|
|
257
|
+
|
|
258
|
+
### Tor
|
|
259
|
+
|
|
260
|
+
`WEBVEIL_EGRESS_URL=socks5://127.0.0.1:9050` with the Tor daemon running. Same per-request,
|
|
261
|
+
webveil-only scoping applies.
|
|
262
|
+
|
|
263
|
+
> **Caveat:** webveil's `socks5` mode is NOT a whole-machine VPN. Do not assume enabling it
|
|
264
|
+
> anonymizes anything other than webveil. Conversely, a system-wide full-tunnel VPN under
|
|
265
|
+
> your logged-in identity is the thing that CAN deanonymize a `git push`; webveil's scoped
|
|
266
|
+
> egress deliberately avoids that.
|
|
267
|
+
|
|
43
268
|
## License
|
|
44
269
|
|
|
45
270
|
AGPL-3.0-or-later. webveil depends on `distilly` (MIT, the local HTML-to-markdown
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-webveil",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Pi extension: web_search and web_fetch tools backed by webveil. A drop-in, anonymity-capable replacement for Ollama's web_search/web_fetch.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"keywords": [
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
]
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"webveil": "0.1.
|
|
44
|
+
"webveil": "0.1.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^25.2.0",
|