Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions packages/toolbox-core/src/toolbox_core/sync_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,22 @@ def _description(self) -> str:
def _params(self) -> Sequence[ParameterSchema]:
return self.__async_tool._params

@property
def _secure_params(self) -> Sequence[ParameterSchema]:
return self.__async_tool._secure_params

@property
def _bound_params(
self,
) -> Mapping[str, Union[Callable[[], Any], Callable[[], Awaitable[Any]], Any]]:
return self.__async_tool._bound_params

@property
def _bound_secure_params(
self,
) -> Mapping[str, Union[Callable[[], Any], Callable[[], Awaitable[Any]], Any]]:
return self.__async_tool._bound_secure_params

@property
def _required_authn_params(self) -> Mapping[str, list[str]]:
return self.__async_tool._required_authn_params
Expand Down Expand Up @@ -241,6 +251,51 @@ def bind_param(
"""
return self.bind_params({param_name: param_value})

def bind_secure_params(
Comment thread
anubhav756 marked this conversation as resolved.
self,
bound_secure_params: Mapping[
str, Union[Callable[[], Any], Callable[[], Awaitable[Any]], Any]
],
) -> "ToolboxSyncTool":
"""
Binds secure parameters to values or callables that produce values.

Args:
bound_secure_params: A mapping of secure parameter names to values or
callables that produce values.

Returns:
A new ToolboxSyncTool instance with the specified secure parameters bound.

Raises:
ValueError: If a parameter is already bound or is not defined as a
secure parameter by the tool's definition.
"""
new_async_tool = self.__async_tool.bind_secure_params(bound_secure_params)
return ToolboxSyncTool(new_async_tool, self.__loop, self.__thread)

def bind_secure_param(
self,
param_name: str,
param_value: Union[Callable[[], Any], Callable[[], Awaitable[Any]], Any],
) -> "ToolboxSyncTool":
"""
Binds a secure parameter to the value or callable that produces the value.

Args:
param_name: The name of the secure parameter to bind.
param_value: The value of the bound secure parameter, or a callable that
returns the value.

Returns:
A new ToolboxSyncTool instance with the specified secure parameter bound.

Raises:
ValueError: If the parameter is already bound or is not defined as a
secure parameter by the tool's definition.
"""
return self.bind_secure_params({param_name: param_value})

def add_telemetry_attributes(
self, telemetry_attributes: TelemetryAttributes
) -> "ToolboxSyncTool":
Expand Down
150 changes: 140 additions & 10 deletions packages/toolbox-core/src/toolbox_core/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ def __init__(
client_headers: Mapping[
str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]
],
secure_params: Sequence[ParameterSchema] = (),
bound_secure_params: Mapping[
str, Union[Callable[[], Any], Callable[[], Awaitable[Any]], Any]
] = {},
):
"""
Initializes a callable that will trigger the tool invocation through the
Expand All @@ -78,11 +82,16 @@ def __init__(
bound_params: A mapping of parameter names to bind to specific
values or callables that are called to produce values as needed.
client_headers: Client specific headers bound to the tool.
secure_params: The secure parameters of the tool.
bound_secure_params: A mapping of secure parameter names to bind to
specific values or callables.
"""
# used to invoke the toolbox API
self.__transport = transport
self.__description = description
self.__params = params
self.__secure_params = secure_params
self.__bound_secure_params = bound_secure_params
self.__pydantic_model = params_to_pydantic_model(name, self.__params)

# Separate parameters into those without a default and those with a
Expand Down Expand Up @@ -133,12 +142,22 @@ def _description(self) -> str:
def _params(self) -> Sequence[ParameterSchema]:
return copy.deepcopy(self.__params)

@property
def _secure_params(self) -> Sequence[ParameterSchema]:
return copy.deepcopy(self.__secure_params)

@property
def _bound_params(
self,
) -> Mapping[str, Union[Callable[[], Any], Callable[[], Awaitable[Any]], Any]]:
return MappingProxyType(self.__bound_parameters)

@property
def _bound_secure_params(
self,
) -> Mapping[str, Union[Callable[[], Any], Callable[[], Awaitable[Any]], Any]]:
return MappingProxyType(self.__bound_secure_params)

@property
def _required_authn_params(self) -> Mapping[str, list[str]]:
return MappingProxyType(self.__required_authn_params)
Expand Down Expand Up @@ -177,6 +196,10 @@ def __copy(
Mapping[str, Union[Callable[[], str], Callable[[], Awaitable[str]], str]]
] = None,
telemetry_attributes: Optional[TelemetryAttributes] = None,
secure_params: Optional[Sequence[ParameterSchema]] = None,
bound_secure_params: Optional[
Mapping[str, Union[Callable[[], Any], Callable[[], Awaitable[Any]], Any]]
] = None,
) -> "ToolboxTool":
"""
Creates a copy of the ToolboxTool, overriding specific fields.
Expand All @@ -197,6 +220,9 @@ def __copy(
client_headers: Client specific headers bound to the tool.
telemetry_attributes: Telemetry attributes for the derived tool.
Set directly on the new instance (not exposed via __init__).
secure_params: The secure parameters of the tool.
bound_secure_params: A mapping of secure parameter names to bind to
specific values or callables.
"""
check = lambda val, default: val if val is not None else default
new_tool = ToolboxTool(
Expand All @@ -215,6 +241,8 @@ def __copy(
),
bound_params=check(bound_params, self.__bound_parameters),
client_headers=check(client_headers, self.__client_headers),
secure_params=check(secure_params, self.__secure_params),
bound_secure_params=check(bound_secure_params, self.__bound_secure_params),
)
new_tool.__telemetry_attributes = check(
telemetry_attributes, self.__telemetry_attributes
Expand Down Expand Up @@ -255,6 +283,19 @@ async def __call__(self, *args: Any, **kwargs: Any) -> str:
f": {','.join(req_auth_services)}"
)

# validate missing required secure parameters
missing_secure = [
p.name
for p in self.__secure_params
if p.required
and p.default is None
and p.name not in self.__bound_secure_params
]
if missing_secure:
raise ValueError(
f"Missing required secure parameter(s) {missing_secure} for tool '{self.__name__}'"
)

# validate inputs to this call using the signature
all_args = self.__signature__.bind(*args, **kwargs)

Expand All @@ -274,6 +315,13 @@ async def __call__(self, *args: Any, **kwargs: Any) -> str:
# error if it receives a None value, which it cannot convert.
payload = OrderedDict({k: v for k, v in payload.items() if v is not None})

# resolve secure bound parameters
secure_payload = {}
for param, value in self.__bound_secure_params.items():
resolved = await resolve_value(value)
if resolved is not None:
secure_payload[param] = resolved

# create headers for auth services
headers = {}
for client_header_name, client_header_val in self.__client_headers.items():
Expand All @@ -287,17 +335,17 @@ async def __call__(self, *args: Any, **kwargs: Any) -> str:

warn_if_http_and_headers(self.__transport.base_url, headers)

kwargs_to_pass: dict[str, Any] = {}
if self.__telemetry_attributes is not None:
return await self.__transport.tool_invoke(
self.__name__,
payload,
headers,
telemetry_attributes=self.__telemetry_attributes,
)
kwargs_to_pass["telemetry_attributes"] = self.__telemetry_attributes
if secure_payload:
kwargs_to_pass["secure_arguments"] = secure_payload

return await self.__transport.tool_invoke(
self.__name__,
payload,
headers,
**kwargs_to_pass,
)

def add_telemetry_attributes(
Expand Down Expand Up @@ -431,17 +479,25 @@ def bind_params(
A new ToolboxTool instance with the specified parameters bound.

Raises:
ValueError: If a parameter is already bound or is not defined by the
tool's definition.
ValueError: If a parameter is already bound, is not defined by the
tool's definition, or is a secure parameter.

"""
param_names = set(p.name for p in self.__params)
secure_param_names = set(p.name for p in self.__secure_params) | set(
self.__bound_secure_params.keys()
)
for name in bound_params.keys():
if name in self.__bound_parameters:
raise ValueError(
f"cannot re-bind parameter: parameter '{name}' is already bound"
)

if name in secure_param_names:
raise ValueError(
f"parameter '{name}' is a secure parameter; use bind_secure_param/bind_secure_params instead"
)

if name not in param_names:
raise ValueError(
f"unable to bind parameters: no parameter named {name}"
Expand Down Expand Up @@ -476,8 +532,82 @@ def bind_param(
A new ToolboxTool instance with the specified parameter bound.

Raises:
ValueError: If the parameter is already bound or is not defined by
the tool's definition.
ValueError: If the parameter is already bound, is not defined by
the tool's definition, or is a secure parameter.

"""
return self.bind_params({param_name: param_value})

def bind_secure_params(
self,
bound_secure_params: Mapping[
str, Union[Callable[[], Any], Callable[[], Awaitable[Any]], Any]
],
) -> "ToolboxTool":
"""
Binds secure parameters to values or callables that produce values.

Args:
bound_secure_params: A mapping of secure parameter names to values or
callables that produce values.

Returns:
A new ToolboxTool instance with the specified secure parameters bound.

Raises:
ValueError: If a secure parameter is already bound, is a regular parameter,
or is not defined by the tool's secure parameter definition.
"""
secure_param_names = set(p.name for p in self.__secure_params)
regular_param_names = set(p.name for p in self.__params) | set(
self.__bound_parameters.keys()
)
for name in bound_secure_params.keys():
if name in self.__bound_secure_params:
raise ValueError(
f"cannot re-bind secure parameter: secure parameter '{name}' is already bound"
)

if name in regular_param_names:
raise ValueError(
f"parameter '{name}' is a regular parameter; use bind_param/bind_params instead"
)

if name not in secure_param_names:
raise ValueError(
f"unable to bind secure parameters: no secure parameter named {name}"
)

new_secure_params = []
for p in self.__secure_params:
if p.name not in bound_secure_params:
new_secure_params.append(p)
all_bound_secure_params = dict(self.__bound_secure_params)
all_bound_secure_params.update(bound_secure_params)

return self.__copy(
secure_params=new_secure_params,
bound_secure_params=MappingProxyType(all_bound_secure_params),
)

def bind_secure_param(
self,
param_name: str,
param_value: Union[Callable[[], Any], Callable[[], Awaitable[Any]], Any],
) -> "ToolboxTool":
"""
Binds a secure parameter to the value or callable that produces the value.

Args:
param_name: The name of the bound secure parameter.
param_value: The value of the bound secure parameter, or a callable that
returns the value.

Returns:
A new ToolboxTool instance with the specified secure parameter bound.

Raises:
ValueError: If the secure parameter is already bound, is a regular parameter,
or is not defined by the tool's definition.
"""
return self.bind_secure_params({param_name: param_value})
27 changes: 21 additions & 6 deletions packages/toolbox-core/src/toolbox_core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
Callable,
Iterable,
Mapping,
Optional,
Sequence,
Type,
Union,
Expand Down Expand Up @@ -177,32 +178,46 @@ def validate_unused_requirements(
name: str,
is_toolset: bool = False,
target_type: str | None = None,
provided_secure_keys: Optional[set[str]] = None,
used_secure_keys: Optional[set[str]] = None,
) -> None:
"""
Validates that no provided authentication tokens or bound parameters went unused.
Validates that no provided authentication tokens, bound parameters, or secure parameters went unused.
Raises a ValueError if any unused requirements are found, formatted appropriately
for either a single tool or a full toolset.
"""
unused_auth = provided_auth_keys - used_auth_keys
unused_bound = provided_bound_keys - used_bound_keys
unused_secure = (provided_secure_keys or set()) - (used_secure_keys or set())

if unused_auth or unused_bound:
if unused_auth or unused_bound or unused_secure:
error_messages = []
if unused_auth:
if is_toolset:
error_messages.append(
f"unused auth tokens could not be applied to any tool: {', '.join(unused_auth)}"
f"unused auth tokens could not be applied to any tool: {', '.join(sorted(unused_auth))}"
)
else:
error_messages.append(f"unused auth tokens: {', '.join(unused_auth)}")
error_messages.append(
f"unused auth tokens: {', '.join(sorted(unused_auth))}"
)
if unused_bound:
if is_toolset:
error_messages.append(
f"unused bound parameters could not be applied to any tool: {', '.join(unused_bound)}"
f"unused bound parameters could not be applied to any tool: {', '.join(sorted(unused_bound))}"
)
else:
error_messages.append(
f"unused bound parameters: {', '.join(sorted(unused_bound))}"
)
if unused_secure:
if is_toolset:
error_messages.append(
f"unused secure parameters could not be applied to any tool: {', '.join(sorted(unused_secure))}"
)
else:
error_messages.append(
f"unused bound parameters: {', '.join(unused_bound)}"
f"unused secure parameters: {', '.join(sorted(unused_secure))}"
)

final_target_type = (
Expand Down
Loading
Loading