Skip to content

Commit 74ec87b

Browse files
committed
Merge remote-tracking branch 'origin/main' into protocol-v2-schema-paths
# Conflicts: # docs/how-to/iroh.md # mkdocs.yml # pyproject.toml # tests/test_filter_pushdown.py # tests/test_serve.py # uv.lock # vgi/client/client.py # vgi/serve.py
2 parents e69bec0 + fae4f3f commit 74ec87b

23 files changed

Lines changed: 1372 additions & 36 deletions

‎.dockerignore‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.git
2+
.venv
3+
.mypy_cache
4+
.pytest_cache
5+
.ruff_cache
6+
site
7+
__pycache__
8+
*.pyc
9+
.tailnet/vgi-rpc/.git
10+
.env
11+
.env.*
12+
*.key
13+
*.pem
14+
*.p12
15+
*.pfx
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Real Tailnet authentication
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
tailscale_channel:
7+
description: Official Tailscale container channel
8+
required: true
9+
default: stable
10+
type: choice
11+
options: [stable, unstable]
12+
13+
permissions:
14+
contents: read
15+
16+
concurrency:
17+
group: vgi-python-real-tailnet
18+
cancel-in-progress: false
19+
20+
jobs:
21+
tailnet:
22+
name: high-level worker / ${{ inputs.tailscale_channel }}
23+
runs-on: ubuntu-24.04
24+
timeout-minutes: 30
25+
environment: tailnet-integration
26+
env:
27+
TAILSCALE_IMAGE: tailscale/tailscale:${{ inputs.tailscale_channel }}
28+
TS_OAUTH_CLIENT_ID: ${{ secrets.TS_OAUTH_CLIENT_ID }}
29+
TS_OAUTH_SECRET: ${{ secrets.TS_OAUTH_SECRET }}
30+
TAILNET_ISSUER: ${{ vars.TAILNET_ISSUER }}
31+
TAILNET_EXPECTED_CAPABILITY: ${{ vars.TAILNET_EXPECTED_CAPABILITY }}
32+
TAILNET_EXPECTED_CLIENT_TAG: ${{ vars.TAILNET_EXPECTED_CLIENT_TAG }}
33+
steps:
34+
- uses: actions/checkout@v7
35+
36+
- name: Checkout unreleased vgi-rpc identity layer
37+
uses: actions/checkout@v7
38+
with:
39+
repository: Query-farm/vgi-rpc-python
40+
path: .tailnet/vgi-rpc
41+
ref: 04f19147625b4f643b38a687151d8233daa8997c
42+
43+
- name: Check kernel networking support
44+
run: |
45+
test -c /dev/net/tun
46+
docker compose version
47+
48+
- name: Run high-level Tailnet authentication matrix
49+
run: tests/tailnet/run.sh

‎ci/run-integration.sh‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ if [ "$TRANSPORT" = "http" ]; then
8686
# * buffer_input/sizes.test — input buffering semantics differ over HTTP
8787
# (same known limitation). (scale.test_slow is a .test_slow file and is
8888
# never staged below, which only finds *.test.)
89+
# * database_worker/package.test — the packaged fixture is deliberately an
90+
# executable wrapper around VGI_TEST_WORKER. An HTTP URL is not an
91+
# executable artifact; the direct-exec lifecycle is covered by all three
92+
# process/socket lanes.
8993
# cache/revalidate.test now runs on http too: HTTP conditional revalidation is
9094
# implemented (C++ /init-request validators + vgi-rpc >=0.24.0 surfacing them to
9195
# the producer's first process()). It needs a community vgi extension carrying
@@ -96,6 +100,7 @@ if [ "$TRANSPORT" = "http" ]; then
96100
-not -name 'dynamic_filter.test'
97101
-not -name 'partitioned_sequence.test'
98102
-not -path './table_in_out/buffer_input/sizes.test'
103+
-not -path './database_worker/package.test'
99104
)
100105
fi
101106

@@ -120,6 +125,16 @@ mkdir -p "$STAGE/test/sql/integration"
120125
awk -v http="$AWK_HTTP" -f "$HERE/preprocess-require.awk" "$f" > "$STAGE/test/sql/integration/$f"
121126
done )
122127

128+
# Some integration cases package an executable from the upstream support tree.
129+
# The staged suite runs with ``$STAGE`` as its working directory, so preserve
130+
# the relative path used by those SQL fixtures instead of making it depend on
131+
# the checkout layout outside the stage.
132+
if [ -f "$VGI_SRC/test/support/database_worker_fixture.sh" ]; then
133+
mkdir -p "$STAGE/test/support"
134+
cp "$VGI_SRC/test/support/database_worker_fixture.sh" "$STAGE/test/support/"
135+
chmod +x "$STAGE/test/support/database_worker_fixture.sh"
136+
fi
137+
123138
# Empty VGI_RPC_SHM_SIZE_BYTES must not reach the C++ client (it would try to
124139
# attach a zero-size segment); only a real value enables the shm side channel.
125140
[ -n "${VGI_RPC_SHM_SIZE_BYTES:-}" ] || unset VGI_RPC_SHM_SIZE_BYTES

‎docs/how-to/iroh.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ route, and configure the worker to trust only the intermediary's exact address.
6666

6767
The same high-level client accepts both endpoint schemes:
6868

69-
```python
69+
```python test="skip"
7070
from vgi.client import Client
7171

7272
with Client.from_iroh("iroh://<endpoint-id>") as client:
@@ -78,7 +78,7 @@ with Client.from_iroh("httpi://<endpoint-id>/vgi") as client:
7878

7979
For a private relay:
8080

81-
```python
81+
```python test="skip"
8282
client = Client.from_iroh(
8383
"httpi://<endpoint-id>/vgi",
8484
relay_urls=["https://relay.example.com"],

‎docs/how-to/tailnet.md‎

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
---
2+
description: "How to connect the Python VGI client to workers through Tailscale over HTTPS, direct TCP, or userspace SOCKS5h."
3+
---
4+
5+
# Run VGI through a Tailnet
6+
7+
The VGI client uses ordinary HTTPS and TCP endpoints. It does not join a
8+
Tailnet, manage Tailscale credentials, or introduce a `tailscale://` URL
9+
scheme. Run Tailscale on the client host or in a sidecar and pass the worker's
10+
MagicDNS name to the normal client factory.
11+
12+
Connectivity and authentication are separate. A client can reach a worker
13+
through a Tailnet without the worker using Tailnet identity. To authorize from
14+
the caller's verified Tailscale user, node, tags, or application capabilities,
15+
configure a peer-identity provider and an authentication policy on the worker.
16+
17+
## Authenticate an HTTP worker
18+
19+
When Tailscale Serve terminates HTTPS, trust its identity headers only from the
20+
exact local proxy addresses that can reach the application. `require_peer_identity`
21+
requires verified evidence but deliberately does not invent a stable principal
22+
for capability-only or login-scoped Serve requests:
23+
24+
```python test="lint"
25+
from vgi_rpc.http import tailscale_serve_header_provider
26+
from vgi_rpc.rpc import require_peer_identity
27+
28+
from my_worker import MyWorker
29+
from vgi.serve import create_app
30+
31+
tailscale = tailscale_serve_header_provider(
32+
issuer="tailnet:example",
33+
trusted_proxy_addresses=("127.0.0.1", "::1"),
34+
)
35+
36+
app = create_app(
37+
MyWorker,
38+
peer_identity_providers=(tailscale,),
39+
peer_authentication_policy=require_peer_identity("tailscale"),
40+
)
41+
```
42+
43+
Serve login names have `subject_stability="login"`; tagged clients may be
44+
capability-only and have no subject at all. Consequently, the built-in
45+
`peer_identity_primary("tailscale")` policy is intentionally unsuitable for
46+
Serve. If a deployment elects to authenticate from a Serve login, it must use
47+
an explicit application policy that accepts that weaker identity and scopes it
48+
to the configured issuer and trusted proxy boundary.
49+
50+
For direct HTTP over a Tailnet, LocalAPI WhoIs can produce a stable user or
51+
node subject and therefore can be the primary authenticator:
52+
53+
```python test="lint"
54+
from vgi_rpc.http import tailscale_localapi_provider
55+
from vgi_rpc.rpc import peer_identity_primary
56+
57+
from my_worker import MyWorker
58+
from vgi.serve import create_app
59+
60+
tailscale = tailscale_localapi_provider(issuer="tailnet:example")
61+
app = create_app(
62+
MyWorker,
63+
peer_identity_providers=(tailscale,),
64+
peer_authentication_policy=peer_identity_primary("tailscale"),
65+
)
66+
```
67+
68+
`create_app()` remains a standard WSGI application, so the same configuration
69+
works behind Waitress, Granian, gunicorn, Envoy, nginx, or a cloud HTTP load
70+
balancer. The provider determines which proxy evidence is trusted; merely
71+
placing the worker behind a proxy never makes forwarded identity trustworthy.
72+
73+
Trusted-proxy validation must see the physical socket peer. Do not install
74+
`ProxyFix`, forwarded-address middleware, or an equivalent rewrite ahead of
75+
VGI's identity middleware: attacker-controlled `Forwarded` or
76+
`X-Forwarded-For` values must never become `REMOTE_ADDR` before the exact proxy
77+
allowlist is checked. Keep the backend unreachable except through those
78+
allowlisted proxies.
79+
80+
## HTTPS through Tailscale Serve
81+
82+
On a host using Tailscale's normal network interface, connect to a worker
83+
published with Tailscale Serve exactly like any other HTTPS worker:
84+
85+
```python test="lint"
86+
from vgi.client import Client
87+
88+
with Client.from_http("https://worker.example-tailnet.ts.net") as client:
89+
catalogs = client.catalogs()
90+
```
91+
92+
Tailscale terminates HTTPS and adds its verified Serve identity and application
93+
capability headers. The VGI server validates those headers against its trusted
94+
proxy configuration; the client neither constructs nor trusts identity
95+
headers. Do not send `Tailscale-*` headers from application code.
96+
97+
For userspace networking, inject an HTTP client configured for Tailscale's
98+
SOCKS5 server. Install SOCKS support with `pip install 'httpx2[socks]'`:
99+
100+
```python test="lint"
101+
import httpx2
102+
103+
from vgi.client import Client
104+
105+
url = "https://worker.example-tailnet.ts.net"
106+
with httpx2.Client(
107+
base_url=url,
108+
proxy="socks5h://127.0.0.1:1055",
109+
trust_env=False,
110+
timeout=httpx2.Timeout(60.0, connect=15.0),
111+
) as http_client, Client.from_http(url, httpx_client=http_client) as client:
112+
catalogs = client.catalogs()
113+
```
114+
115+
Using `socks5h` keeps MagicDNS resolution inside the Tailscale sidecar. A proxy
116+
failure is an error; this configuration does not fall back to a direct route.
117+
118+
## Raw TCP through a Tailnet
119+
120+
Start a high-level Worker with LocalAPI identity using `Worker.serve_tcp()`:
121+
122+
```python test="lint"
123+
from vgi_rpc.http import tailscale_localapi_provider
124+
from vgi_rpc.rpc import peer_identity_primary
125+
126+
from my_worker import MyWorker
127+
128+
tailscale = tailscale_localapi_provider(issuer="tailnet:example")
129+
MyWorker().serve_tcp(
130+
"0.0.0.0",
131+
9400,
132+
threaded=True,
133+
max_connections=64,
134+
peer_identity_providers=(tailscale,),
135+
peer_authentication_policy=peer_identity_primary("tailscale"),
136+
)
137+
```
138+
139+
The connection's verified stable Tailnet subject becomes `CallContext.auth` and
140+
is available to function parameters annotated with `Auth()`. Identity is
141+
snapshotted once for the stateful TCP connection.
142+
143+
When `threaded=True`, all connections share the same Worker instance. Protect
144+
mutable subclass state with appropriate synchronization and never store
145+
per-caller authentication in shared instance attributes. Leave the default
146+
serial mode in place if the implementation is not safe for concurrent calls.
147+
148+
Behind a Tailscale Service configured to emit PROXY protocol v2, require the
149+
preamble and name every immediate proxy address explicitly:
150+
151+
```python test="lint"
152+
MyWorker().serve_tcp(
153+
"127.0.0.1",
154+
9400,
155+
proxy_protocol="required",
156+
trusted_proxy_addresses=("127.0.0.1", "::1"),
157+
service_name="svc:vgi-worker",
158+
peer_identity_providers=(tailscale,),
159+
peer_authentication_policy=peer_identity_primary("tailscale"),
160+
)
161+
```
162+
163+
Keep that backend unreachable except through the trusted proxy. The asserted
164+
source is accepted only after immediate-peer validation, and `service_name`
165+
selects destination-scoped application capabilities in LocalAPI WhoIs.
166+
167+
Direct TCP works with a MagicDNS name when the client host has Tailscale's
168+
normal network interface:
169+
170+
```python test="lint"
171+
from vgi.client import Client
172+
173+
with Client.from_tcp("worker.example-tailnet.ts.net", 9400) as client:
174+
catalogs = client.catalogs()
175+
```
176+
177+
For a userspace Tailscale sidecar, use the explicit SOCKS5h option:
178+
179+
```python test="lint"
180+
with Client.from_tcp(
181+
"worker.example-tailnet.ts.net",
182+
9400,
183+
proxy="socks5h://127.0.0.1:1055",
184+
) as client:
185+
catalogs = client.catalogs()
186+
```
187+
188+
The proxy applies to ordinary function calls, additional worker connections,
189+
and catalog discovery. Target hostnames are passed untouched to SOCKS5h and
190+
are never resolved locally by VGI.
191+
192+
Raw VGI TCP has no application-layer encryption. Tailscale encrypts the
193+
Tailnet path, but the worker port should remain reachable only through the
194+
Tailnet and should not be exposed directly to an untrusted network. Prefer
195+
HTTPS through Tailscale Serve at public or mixed-trust boundaries.

‎mkdocs.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ nav:
125125
- Expose a catalog: how-to/catalogs.md
126126
- Persist state across workers: how-to/state-storage.md
127127
- Serve over HTTP with auth: how-to/http-auth.md
128+
- Connect through a Tailnet: how-to/tailnet.md
128129
- Serve and connect over Iroh: how-to/iroh.md
129130
- Integrate with the optimizer: how-to/pushdown-and-statistics.md
130131
- Function Metadata: metadata.md

‎pyproject.toml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "vgi-python"
3-
version = "0.31.2"
3+
version = "0.31.3"
44
description = "Vector Gateway Interface - Connect DuckDB to external programs via Apache Arrow"
55
readme = "README.md"
66
keywords = [
@@ -40,7 +40,7 @@ dependencies = [
4040
"pyarrow",
4141
"typer>=0.9",
4242
"platformdirs",
43-
"vgi-rpc>=0.45.0",
43+
"vgi-rpc>=0.45.1",
4444
"httpx2>=2.9.1",
4545
]
4646

@@ -52,7 +52,7 @@ dependencies = [
5252
haybarn = ["haybarn>=1.5.5rc1"]
5353
duckdb = ["duckdb>=1.5.3"]
5454
http = ["vgi-rpc[http]"]
55-
iroh = ["vgi-rpc[iroh]"]
55+
iroh = ["vgi-rpc[http,iroh]"]
5656
# transport="launch": vgi_rpc.launcher imports filelock unconditionally at
5757
# module level, which lives behind vgi-rpc's own `cli` extra (alongside
5858
# typer, already a core dependency here, and httpx2, ditto) rather than in

‎tests/conformance/test_directory_parity.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@
4242
"path, ProcessParams.if_none_match threading, and the revalidatable "
4343
"fixture's 304 behavior)."
4444
),
45+
"database_worker": (
46+
"Resolving database:// package rows, validating artifact digests and "
47+
"entrypoints, installing archives, and managing runtime leases are "
48+
"C++-extension responsibilities. The Python SDK supplies workers that "
49+
"the extension packages and launches, but its Client neither parses "
50+
"database:// locations nor owns the extension's artifact cache. The "
51+
"cross-language executable/archive cases remain covered by "
52+
"vgi/test/sql/integration/database_worker/."
53+
),
4554
"global_functions": (
4655
"Publishing worker-declared functions into DuckDB's system.main is "
4756
"entirely a C++-extension concern: the registry, prefix application, "

‎tests/tailnet/Dockerfile‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# syntax=docker/dockerfile:1
2+
FROM python:3.13-slim
3+
4+
ENV PYTHONDONTWRITEBYTECODE=1 \
5+
PYTHONUNBUFFERED=1 \
6+
PIP_DISABLE_PIP_VERSION_CHECK=1
7+
8+
COPY .tailnet/vgi-rpc /workspace/vgi-rpc
9+
COPY . /workspace/vgi-python
10+
WORKDIR /workspace/vgi-python
11+
RUN python -m pip install --no-cache-dir "/workspace/vgi-rpc[http]" "/workspace/vgi-python[http]"
12+
13+
CMD ["python", "-m", "tests.tailnet.probe", "--help"]

‎tests/tailnet/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Real-Tailnet integration fixtures for the high-level VGI SDK."""

0 commit comments

Comments
 (0)