Skip to content

Commit cda573b

Browse files
committed
feat(credentials): guest-side helper + integration tests (Phase 1)
Adds the missing guest-facing surface for the scoped-credentials feature defined in hyperlight:sandbox/credentials (Phase 1 host plumbing landed in the earlier WIT/RootImports/CredentialRegistry commits). Guest helper additions (src/wasm_sandbox/guests/python): - sandbox_executor.py: credential= kwarg on http_get / http_post / the shared _http_request helper, plus standalone attach_credential(req, id) for callers building wasi-http requests by hand. - hyperlight.py: re-export attach_credential so guest scripts can rom hyperlight import attach_credential. Tests (src/wasm_sandbox/tests/credential_integration.rs, 8 cases): - credential_header_injected_on_get / _on_post -> happy path - no_credential_means_no_auth_header -> default unchanged - duplicate_credential_registration_rejected -> registry rejects re-use - unknown_credential_raises_error -> unknown id surfaces error - guest_cannot_override_credential_header -> host injection wins - scope_mismatch_denied -> scope enforcement - double_attach_rejected -> single-attach invariant All 8 pass via just wasm test and direct cargo test. Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
1 parent c36cfae commit cda573b

3 files changed

Lines changed: 505 additions & 9 deletions

File tree

src/wasm_sandbox/guests/python/hyperlight.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
from sandbox_executor import _call_tool as call_tool
44
from sandbox_executor import http_get, http_post
5+
from sandbox_executor import attach_credential
56

6-
__all__ = ["call_tool", "http_get", "http_post"]
7+
__all__ = ["call_tool", "http_get", "http_post", "attach_credential"]

src/wasm_sandbox/guests/python/sandbox_executor.py

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import wit_world.imports.tools as tools
1616
import wit_world.imports.outgoing_handler as outgoing_handler
1717
import wit_world.imports.wasi_http_types as http_types
18+
import wit_world.imports.credentials as credentials
1819

1920

2021
def _call_tool(tool_name: str, **kwargs):
@@ -26,17 +27,30 @@ def _call_tool(tool_name: str, **kwargs):
2627
return json.loads(result_json)
2728

2829

29-
def http_get(url: str) -> dict:
30-
"""Make an HTTP GET request via WASI-HTTP. Returns {"status": int, "body": str}."""
31-
return _http_request("GET", url)
30+
def http_get(url: str, credential: str = None) -> dict:
31+
"""Make an HTTP GET request via WASI-HTTP. Returns {"status": int, "body": str}.
3232
33+
If credential is set, attaches the named credential to the request.
34+
The host will inject the credential's header at dispatch time — the
35+
guest never sees the secret value.
36+
"""
37+
return _http_request("GET", url, credential=credential)
3338

34-
def http_post(url: str, body: str = "", content_type: str = "application/json") -> dict:
35-
"""Make an HTTP POST request via WASI-HTTP. Returns {"status": int, "body": str}."""
36-
return _http_request("POST", url, body=body, content_type=content_type)
3739

40+
def http_post(url: str, body: str = "", content_type: str = "application/json",
41+
credential: str = None) -> dict:
42+
"""Make an HTTP POST request via WASI-HTTP. Returns {"status": int, "body": str}.
3843
39-
def _http_request(method: str, url: str, body: str = "", content_type: str = "") -> dict:
44+
If credential is set, attaches the named credential to the request.
45+
The host will inject the credential's header at dispatch time — the
46+
guest never sees the secret value.
47+
"""
48+
return _http_request("POST", url, body=body, content_type=content_type,
49+
credential=credential)
50+
51+
52+
def _http_request(method: str, url: str, body: str = "", content_type: str = "",
53+
credential: str = None) -> dict:
4054
"""Internal: make an HTTP request via WASI-HTTP outgoing-handler."""
4155
# Parse URL into scheme, authority, path
4256
scheme_str, rest = url.split("://", 1) if "://" in url else ("https", url)
@@ -81,6 +95,16 @@ def _http_request(method: str, url: str, body: str = "", content_type: str = "")
8195
req.set_authority(authority)
8296
req.set_path_with_query(path)
8397

98+
# Attach credential if specified — the host resolves the secret
99+
# and injects the header at dispatch time.
100+
if credential is not None:
101+
try:
102+
credentials.attach(req, credential)
103+
except Exception as e:
104+
raise RuntimeError(
105+
f"Failed to attach credential '{credential}': {e}"
106+
) from e
107+
84108
# Write body if present
85109
if body:
86110
outgoing_body = req.body()
@@ -138,6 +162,23 @@ def _http_request(method: str, url: str, body: str = "", content_type: str = "")
138162
return {"status": status, "body": body_text}
139163

140164

165+
def attach_credential(request, credential_id: str):
166+
"""Attach a registered credential to an outgoing-request by name.
167+
168+
Low-level helper for guests building WASI-HTTP requests manually.
169+
Most callers should use the `credential=` kwarg on `http_get`/`http_post`
170+
instead.
171+
172+
Raises RuntimeError if the credential is unknown or already attached.
173+
"""
174+
try:
175+
credentials.attach(request, credential_id)
176+
except Exception as e:
177+
raise RuntimeError(
178+
f"Failed to attach credential '{credential_id}': {e}"
179+
) from e
180+
181+
141182
class Executor:
142183
"""Implements the WIT executor interface for componentize-py."""
143184

@@ -152,7 +193,13 @@ def run(self, code: str) -> ExecutionResult:
152193

153194
exit_code = 0
154195
try:
155-
exec(code, {"__builtins__": __builtins__, "call_tool": _call_tool, "http_get": http_get, "http_post": http_post})
196+
exec(code, {
197+
"__builtins__": __builtins__,
198+
"call_tool": _call_tool,
199+
"http_get": http_get,
200+
"http_post": http_post,
201+
"attach_credential": attach_credential,
202+
})
156203
except SystemExit as e:
157204
exit_code = e.code if isinstance(e.code, int) else 1
158205
except Exception as e:

0 commit comments

Comments
 (0)