Skip to content

Commit 6439151

Browse files
committed
refactor: improve custom hook URI handling and error reporting
1 parent 81bc176 commit 6439151

3 files changed

Lines changed: 49 additions & 35 deletions

File tree

sdks/python/agenta/sdk/engines/running/handlers.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2275,9 +2275,13 @@ async def remote_forward_v0(
22752275
got=webhook_url,
22762276
) from exc
22772277

2278-
# The stored url is the service base (pre-/invoke); the invoke surface lives
2279-
# at /invoke and is always appended.
2280-
target_url = f"{webhook_url.rstrip('/')}/invoke"
2278+
# The stored url is the service base; /invoke is always the invoke surface.
2279+
# Strip a trailing /invoke first so existing hooks that stored the full
2280+
# endpoint URL don't get /invoke/invoke after this PR's behaviour change.
2281+
base_url = webhook_url.rstrip("/")
2282+
if base_url.endswith("/invoke"):
2283+
base_url = base_url[: -len("/invoke")]
2284+
target_url = f"{base_url}/invoke"
22812285

22822286
log.info("remote_forward_v0 POST", url=target_url)
22832287

sdks/python/agenta/sdk/engines/running/runners/restricted.py

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@
99
full_write_guard,
1010
)
1111

12-
from agenta.sdk.engines.running.runners.base import CodeRunner
12+
from agenta.sdk.engines.running.runners.base import CodeRunner, normalize_result
1313

1414

1515
# Pure data/iteration builtins that RestrictedPython's safe_builtins omits but
1616
# evaluators routinely need. All operate on data only — none reach the host or
17-
# the class graph, so adding them does not widen the sandbox (escapes go through
18-
# attribute access, which safer_getattr blocks).
17+
# the class graph.
1918
_SAFE_EXTRA_BUILTINS = (
2019
"dict",
2120
"list",
@@ -38,16 +37,21 @@
3837
# pathlib, socket, importlib, io, shutil, ...) or the network (httpx, urllib,
3938
# requests, ...) is excluded. Operators who need unrestricted execution must opt
4039
# into the `local` runner; hostile multi-tenant should use `daytona`.
40+
#
41+
# EXCLUDED intentionally even though they look safe:
42+
# - `typing` — exposes `typing.sys`, giving access to `sys.modules` and
43+
# therefore every already-loaded module (including `os`).
44+
# - `datetime` — same escape: `datetime.sys.modules`.
45+
# - `statistics`— same escape: `statistics.sys.modules`.
46+
# safer_getattr only blocks underscore-prefixed names; plain public attributes
47+
# like `sys` and `modules` on imported module objects are not blocked.
4148
_ALLOWED_IMPORTS = frozenset(
4249
{
4350
"math",
44-
"statistics",
45-
"datetime",
4651
"json",
4752
"re",
4853
"random",
4954
"string",
50-
"typing",
5155
"collections",
5256
"itertools",
5357
"functools",
@@ -129,16 +133,21 @@ def run(
129133
trace: Full trace data (v2 only)
130134
131135
Returns:
132-
Float score between 0 and 1, or None if execution fails
136+
Versions "1"/"2": float score between 0 and 1.
137+
Version "3": any JSON-serializable value (dict, list, str, float, bool).
133138
"""
134139
# Normalize runtime: None means python
135140
runtime = runtime or "python"
136141

137142
# The restricted sandbox runs in-process and only supports Python.
143+
# JavaScript and TypeScript require the Daytona runner
144+
# (AGENTA_SERVICES_CODE_SANDBOX_RUNNER=daytona).
138145
if runtime != "python":
139146
raise ValueError(
140-
f"RestrictedRunner only supports 'python' runtime, got: {runtime}. "
141-
"Use the Daytona runner for javascript/typescript."
147+
f"Runtime '{runtime}' is not supported by the default sandbox. "
148+
"JavaScript and TypeScript evaluators require the Daytona runner. "
149+
"Set AGENTA_SERVICES_CODE_SANDBOX_RUNNER=daytona, or change the "
150+
"runtime to 'python'."
142151
)
143152

144153
try:
@@ -153,24 +162,12 @@ def run(
153162

154163
fn = environment["evaluate"]
155164

156-
if version == "2":
165+
if version in ("2", "3"):
157166
result = fn(inputs, output, trace)
158167
else:
159168
result = fn(app_params, inputs, output, correct_answer)
160169

161-
# Attempt to convert result to float
162-
if isinstance(result, (float, int, str)):
163-
try:
164-
result = float(result)
165-
except ValueError as e:
166-
raise ValueError(f"Result cannot be converted to float: {e}")
167-
168-
if not isinstance(result, float):
169-
raise TypeError(
170-
f"Result is not a float after conversion: {type(result)}"
171-
)
172-
173-
return result
170+
return normalize_result(result, version)
174171

175172
except KeyError as e:
176173
raise KeyError(f"Missing expected key in environment: {e}")

sdks/python/agenta/sdk/middlewares/running/resolver.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
parse_uri,
1717
)
1818
from agenta.sdk.engines.running.handlers import remote_forward_v0
19-
from agenta.sdk.engines.running.errors import InvalidInterfaceURIV0Error
19+
from agenta.sdk.engines.running.errors import (
20+
InvalidInterfaceURIV0Error,
21+
MissingConfigurationParameterV0Error,
22+
)
2023

2124
# Internal embeds resolution defaults (not user-configurable)
2225
_EMBEDS_MAX_CHECKS = 20
@@ -28,17 +31,22 @@
2831
_AG_EMBED_MARKER = "@ag.embed"
2932

3033

31-
def _is_url_backed_custom_hook(revision: Optional[WorkflowRevisionData]) -> bool:
32-
"""True for a custom-hook revision that carries a url (forwards remotely)."""
33-
if not revision or not revision.uri or not revision.url:
34+
def _is_custom_hook_uri(uri: Optional[str]) -> bool:
35+
"""True for any custom:hook URI regardless of whether a url is set."""
36+
if not uri:
3437
return False
3538
try:
36-
_provider, kind, key, _version = parse_uri(revision.uri)
39+
_provider, kind, key, _version = parse_uri(uri)
3740
except Exception:
3841
return False
3942
return kind == "custom" and key == "hook"
4043

4144

45+
def _is_url_backed_custom_hook(revision: Optional[WorkflowRevisionData]) -> bool:
46+
"""True for a custom-hook revision that carries a url (forwards remotely)."""
47+
return bool(revision and revision.url and _is_custom_hook_uri(revision.uri))
48+
49+
4250
def _raise_bad_request(message: str) -> None:
4351
error = ValueError(message)
4452
error.status_code = 400 # type: ignore[attr-defined]
@@ -604,12 +612,17 @@ async def __call__(
604612
# forwarder); only resolve from the URI registry for pure URI dispatch.
605613
# A URL-backed custom hook has no local handler, so its URI would resolve
606614
# to the raising registry stub — forward it remotely instead.
607-
if ctx.handler is None and _is_url_backed_custom_hook(revision):
615+
# A custom hook with no URL is a user misconfiguration: raise a clear
616+
# error rather than letting it fall through to the internal stub.
617+
uri = revision.uri if revision else None
618+
if ctx.handler is None and _is_custom_hook_uri(uri):
619+
if not (revision and revision.url):
620+
raise MissingConfigurationParameterV0Error(
621+
path="url",
622+
)
608623
handler = remote_forward_v0
609624
else:
610-
handler = ctx.handler or await resolve_handler(
611-
uri=(revision.uri if revision else None)
612-
)
625+
handler = ctx.handler or await resolve_handler(uri=uri)
613626

614627
ctx.revision = (
615628
{"data": revision.model_dump(mode="json", exclude_none=True)}

0 commit comments

Comments
 (0)