fix(secrets): complete the binding and value contract at the Rust boundary

`validate` checked only the binding count, so a caller reaching the low-level
entry point past `bind_secrets` could register an environment variable name the
runtime cannot deliver, or bind a Secret to a name `runtime.env` already sets --
which would resolve by delivery order, with a value visible in the Function's
record and a value that is not.

Secret values are bounded here too, at the limit the service enforces, so an
oversized credential is refused before a request body is built rather than
after it has been serialized and uploaded.

Sandbox-reserved names are deliberately still the service's alone: that list
belongs to the runtime that owns it, and a copy here would drift from it
silently.

Both gate reproducers now fail closed with no request reaching the service.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfmeJ533rQDnPBkMtjerV6
This commit is contained in:
Jonathan M Hsieh
2026-09-09 17:33:59 +00:00
co-authored by Claude Opus 5
parent b85f5f141f
commit e541a3be93
4 changed files with 183 additions and 7 deletions
@@ -179,6 +179,35 @@ def test_a_function_binds_at_most_sixteen_secrets():
assert state["requests"] == []
def test_binding_names_are_validated_below_the_python_api():
"""The low-level entry point reaches the same validator the typed API does.
Registration envelopes can be hand-rolled past ``bind_secrets``, so the
grammar and the disjointness rule live in Rust, above the backend.
"""
with _mock_remote_function_catalog() as (host, state):
db = lancedb.connect(
"db://dev",
api_key="fake",
host_override=host,
client_config={"retry_config": {"retries": 0}},
)
envelope = json.loads(analyze_caption.registration_request.to_canonical_json())
envelope["secret_env_bindings"] = {
"BAD=NAME": "openai-prod",
"TOKEN_0": "secret-0",
}
envelope["runtime"]["env"]["TOKEN_0"] = "public"
async def submit_envelope():
return await db._conn._inner.create_function_async(json.dumps(envelope))
with pytest.raises(ValueError, match="portable"):
LOOP.run(submit_envelope())
assert state["requests"] == []
_SECRET_DEBUG_LOG_SOURCE = """
import http.server
import json