Skip to content

Commit a20f8ca

Browse files
rustyconoverclaude
andcommitted
release: 0.22.1 — advertise VGI-Proxy-Proof-Required from create_app
A `require`-mode worker now tells proxies it actually enforces the proof they mint. Without the header, `allow` → `require` has no confirmation step: a proxy minting proofs at a worker that silently ignores them looks identical to one that checks every hop. `proxy_proof_required` defaults to `None`, deriving from VGI_PROXY_PROOF_MODE — the same env var `_resolve_proxy_proof_gate` reads, so the advertisement cannot drift from the posture it describes. Pass a bool explicitly only when supplying a hand-built gate via `authenticate`. Requires vgi-rpc 0.28.3, which adds the `make_wsgi_app` kwarg and two shared conformance cases pinning both postures across all five ports. Verified against a real waitress-served worker: off and allow silent, require emits "true". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 58ddac9 commit a20f8ca

3 files changed

Lines changed: 64 additions & 2 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "vgi-python"
3-
version = "0.22.0"
3+
version = "0.22.1"
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.28.2",
43+
"vgi-rpc>=0.28.3",
4444
"httpx>=0.24",
4545
]
4646

tests/test_serve.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,57 @@ def test_describe_disabled(self) -> None:
206206

207207
assert isinstance(app, falcon.App)
208208

209+
@staticmethod
210+
def _health_proof_header(**env: str) -> str | None:
211+
"""Serve an app under ``env`` and read the proof capability header.
212+
213+
Args:
214+
**env: ``VGI_PROXY_PROOF_*`` values to set for the duration.
215+
216+
Returns:
217+
The ``VGI-Proxy-Proof-Required`` value, or ``None`` when absent.
218+
219+
"""
220+
import falcon.testing
221+
222+
with pytest.MonkeyPatch.context() as mp:
223+
for key in ("VGI_PROXY_PROOF_MODE", "VGI_PROXY_PROOF_ORIGIN_ID", "VGI_PROXY_PROOF_SECRETS"):
224+
mp.delenv(key, raising=False)
225+
for key, value in env.items():
226+
mp.setenv(key, value)
227+
app = create_app(_SingleWorker, prefix="/vgi", describe=False)
228+
resp = falcon.testing.TestClient(app).simulate_get("/vgi/health")
229+
assert resp.status_code == 200
230+
return {k.lower(): v for k, v in resp.headers.items()}.get("vgi-proxy-proof-required")
231+
232+
def test_advertises_proof_required_in_require_mode(self) -> None:
233+
"""A ``require``-mode worker tells proxies it actually enforces.
234+
235+
Derived from ``VGI_PROXY_PROOF_MODE`` rather than passed separately,
236+
because that env var is also where the gate itself comes from — so the
237+
advertisement cannot drift from the posture it describes.
238+
"""
239+
value = self._health_proof_header(
240+
VGI_PROXY_PROOF_MODE="require",
241+
VGI_PROXY_PROOF_ORIGIN_ID="worker-a",
242+
VGI_PROXY_PROOF_SECRETS="k:" + "11" * 32,
243+
)
244+
assert value == "true"
245+
246+
@pytest.mark.parametrize("mode", ["off", "allow"])
247+
def test_does_not_advertise_below_require(self, mode: str) -> None:
248+
"""Only ``require`` advertises — ``allow`` never denies, so it must not.
249+
250+
A proxy reads this to confirm a rollout landed; an ``allow``-mode worker
251+
that claimed to require would report the rollout complete while every
252+
unproofed direct caller still sailed through.
253+
"""
254+
env = {"VGI_PROXY_PROOF_MODE": mode}
255+
if mode != "off":
256+
env["VGI_PROXY_PROOF_ORIGIN_ID"] = "worker-a"
257+
env["VGI_PROXY_PROOF_SECRETS"] = "k:" + "11" * 32
258+
assert self._health_proof_header(**env) is None
259+
209260
def test_signing_key_passed(self) -> None:
210261
"""Explicit signing_key is accepted without warning."""
211262
import warnings

vgi/serve.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ def create_app(
202202
signing_key: bytes | None = None,
203203
log_level: int = logging.INFO,
204204
authenticate: Callable[[falcon.Request], AuthContext] | None = None,
205+
proxy_proof_required: bool | None = None,
205206
oauth_resource_metadata: Any = None,
206207
otel_config: OtelConfig | None = None,
207208
max_stream_response_bytes: int | None = None,
@@ -224,6 +225,12 @@ def create_app(
224225
authenticate: Optional callback that validates each HTTP request
225226
and returns an `AuthContext`. When ``None``, all requests are
226227
anonymous.
228+
proxy_proof_required: Whether to advertise ``VGI-Proxy-Proof-Required``
229+
so a proxy can confirm this worker actually enforces the proof it
230+
mints. ``None`` (the default) derives it from
231+
``VGI_PROXY_PROOF_MODE``, which is also where the gate itself comes
232+
from — so the advertisement cannot drift from the posture. Pass a
233+
bool only when supplying a hand-built gate via ``authenticate``.
227234
oauth_resource_metadata: Optional `OAuthResourceMetadata` for
228235
RFC 9728 discovery endpoint.
229236
otel_config: Optional OpenTelemetry configuration. When provided,
@@ -259,6 +266,9 @@ def create_app(
259266
if signing_key is None:
260267
signing_key = os.urandom(32)
261268

269+
if proxy_proof_required is None:
270+
proxy_proof_required = (os.environ.get("VGI_PROXY_PROOF_MODE") or "").strip().lower() == "require"
271+
262272
worker = worker_cls(quiet=True, log_level=log_level)
263273
worker._vgi_tracer = VgiTracer.create(otel_config)
264274
worker._signing_key = signing_key
@@ -271,6 +281,7 @@ def create_app(
271281
cors_origins=cors_origins,
272282
token_key=signing_key,
273283
authenticate=authenticate,
284+
proxy_proof_required=proxy_proof_required,
274285
oauth_resource_metadata=oauth_resource_metadata,
275286
otel_config=otel_config,
276287
max_stream_response_bytes=max_stream_response_bytes,

0 commit comments

Comments
 (0)