optimizely-opal.opal-tools-sdk 0.1.41.dev0__tar.gz → 0.1.43.dev0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/PKG-INFO +1 -1
- optimizely_opal_opal_tools_sdk-0.1.43.dev0/opal_tools_sdk/auth/__init__.py +6 -0
- optimizely_opal_opal_tools_sdk-0.1.43.dev0/opal_tools_sdk/auth/epi_hmac.py +156 -0
- optimizely_opal_opal_tools_sdk-0.1.43.dev0/opal_tools_sdk/auth/requires_auth.py +41 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/opal_tools_sdk/decorators.py +4 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/opal_tools_sdk/models.py +9 -3
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/opal_tools_sdk/service.py +4 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/optimizely_opal.opal_tools_sdk.egg-info/PKG-INFO +1 -1
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/optimizely_opal.opal_tools_sdk.egg-info/SOURCES.txt +3 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/pyproject.toml +3 -3
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/setup.py +1 -1
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/tests/test_integration.py +19 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/README.md +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/opal_tools_sdk/__init__.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/opal_tools_sdk/_registry.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/opal_tools_sdk/config.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/opal_tools_sdk/context.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/opal_tools_sdk/logging.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/opal_tools_sdk/proteus.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/opal_tools_sdk/response.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/opal_tools_sdk/ui.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/optimizely_opal.opal_tools_sdk.egg-info/dependency_links.txt +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/optimizely_opal.opal_tools_sdk.egg-info/requires.txt +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/optimizely_opal.opal_tools_sdk.egg-info/top_level.txt +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/setup.cfg +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/tests/test_async_tool.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/tests/test_auth_middleware.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/tests/test_dynamic_ui.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/tests/test_hmac.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/tests/test_nested_schema.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/tests/test_proteus.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/tests/test_response.py +0 -0
- {optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/tests/test_rollback.py +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""epi-hmac signing and verification utility.
|
|
2
|
+
|
|
3
|
+
Implements the canonical epi-hmac scheme used across Opal services
|
|
4
|
+
(API Gateway, system-tools, file-server, instructions-service).
|
|
5
|
+
|
|
6
|
+
Signing algorithm::
|
|
7
|
+
|
|
8
|
+
timestamp = milliseconds since epoch
|
|
9
|
+
nonce = UUID v4
|
|
10
|
+
body_md5 = base64(md5(body)) # empty body hashes to base64(md5(""))
|
|
11
|
+
message = app_key + method + endpoint + timestamp + nonce + body_md5
|
|
12
|
+
secret_bytes = base64_decode(hmac_secret)
|
|
13
|
+
signature = base64(HMAC-SHA256(secret_bytes, message))
|
|
14
|
+
header = "epi-hmac {app_key}:{timestamp}:{nonce}:{signature}"
|
|
15
|
+
|
|
16
|
+
Replay protection (v1 posture): verification enforces the 5-minute timestamp
|
|
17
|
+
window but does NOT track nonces, so a captured signed request can be replayed
|
|
18
|
+
within that window. Nonce dedup (e.g. a shared store) is the caller's
|
|
19
|
+
responsibility if stronger replay protection is required.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import base64
|
|
25
|
+
import binascii
|
|
26
|
+
import hashlib
|
|
27
|
+
import hmac as hmac_mod
|
|
28
|
+
import time
|
|
29
|
+
import uuid
|
|
30
|
+
|
|
31
|
+
_TIMESTAMP_WINDOW_MS = 300_000 # 5 minutes (past timestamps)
|
|
32
|
+
_MAX_FUTURE_SKEW_MS = 60_000 # 1 minute (clock skew tolerance)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class EpiHmac:
|
|
36
|
+
"""epi-hmac signing and verification utility.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
app_key: Credential identifier.
|
|
40
|
+
secret: Base64-encoded HMAC secret.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, app_key: str, secret: str) -> None:
|
|
44
|
+
self.app_key = app_key
|
|
45
|
+
# Fail closed on a malformed secret rather than silently decoding to the
|
|
46
|
+
# wrong bytes (b64decode without validate ignores non-alphabet chars).
|
|
47
|
+
try:
|
|
48
|
+
self._secret_bytes = base64.b64decode(secret, validate=True)
|
|
49
|
+
except (binascii.Error, ValueError) as exc:
|
|
50
|
+
raise ValueError("EpiHmac secret must be valid base64") from exc
|
|
51
|
+
|
|
52
|
+
def sign(
|
|
53
|
+
self,
|
|
54
|
+
method: str,
|
|
55
|
+
endpoint: str,
|
|
56
|
+
body: bytes = b"",
|
|
57
|
+
) -> str:
|
|
58
|
+
"""Generate an ``Authorization: epi-hmac ...`` header value.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
method: HTTP method (GET, POST, etc.).
|
|
62
|
+
endpoint: Request path including query string.
|
|
63
|
+
body: Request body bytes (empty for GET/DELETE).
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The full Authorization header value.
|
|
67
|
+
"""
|
|
68
|
+
timestamp = str(int(time.time() * 1000))
|
|
69
|
+
nonce = str(uuid.uuid4())
|
|
70
|
+
return self._build_header(method, endpoint, body, timestamp, nonce)
|
|
71
|
+
|
|
72
|
+
def verify(
|
|
73
|
+
self,
|
|
74
|
+
auth_header: str,
|
|
75
|
+
method: str,
|
|
76
|
+
endpoint: str,
|
|
77
|
+
body: bytes = b"",
|
|
78
|
+
) -> bool:
|
|
79
|
+
"""Verify an ``Authorization: epi-hmac ...`` header.
|
|
80
|
+
|
|
81
|
+
Checks app_key match, timestamp freshness (5-minute window),
|
|
82
|
+
and signature correctness using timing-safe comparison.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
auth_header: The full Authorization header value.
|
|
86
|
+
method: HTTP method.
|
|
87
|
+
endpoint: Request path including query string.
|
|
88
|
+
body: Request body bytes.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
True if the header is valid.
|
|
92
|
+
"""
|
|
93
|
+
if not auth_header.startswith("epi-hmac "):
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
parts = auth_header[len("epi-hmac ") :].split(":")
|
|
97
|
+
if len(parts) != 4:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
recv_app_key, timestamp, nonce, recv_signature = parts
|
|
101
|
+
|
|
102
|
+
# Validate app_key
|
|
103
|
+
if not hmac_mod.compare_digest(recv_app_key, self.app_key):
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
# Validate timestamp freshness: reject far-future timestamps (beyond clock
|
|
107
|
+
# skew) and old timestamps (beyond replay window). The original symmetric
|
|
108
|
+
# check `abs(now - ts) <= window` accepted timestamps up to 5 min in the
|
|
109
|
+
# FUTURE, doubling the replay surface. A future timestamp is never
|
|
110
|
+
# legitimate beyond small clock skew.
|
|
111
|
+
try:
|
|
112
|
+
ts_ms = int(timestamp)
|
|
113
|
+
except ValueError:
|
|
114
|
+
return False
|
|
115
|
+
now_ms = int(time.time() * 1000)
|
|
116
|
+
# Reject timestamps too far in the future (beyond clock skew tolerance)
|
|
117
|
+
if ts_ms - now_ms > _MAX_FUTURE_SKEW_MS:
|
|
118
|
+
return False
|
|
119
|
+
# Reject timestamps too far in the past (beyond replay window)
|
|
120
|
+
if now_ms - ts_ms > _TIMESTAMP_WINDOW_MS:
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
# Recompute expected signature
|
|
124
|
+
expected_header = self._build_header(method, endpoint, body, timestamp, nonce)
|
|
125
|
+
expected_signature = expected_header.split(":")[-1]
|
|
126
|
+
|
|
127
|
+
return hmac_mod.compare_digest(recv_signature, expected_signature)
|
|
128
|
+
|
|
129
|
+
def _build_header(
|
|
130
|
+
self,
|
|
131
|
+
method: str,
|
|
132
|
+
endpoint: str,
|
|
133
|
+
body: bytes,
|
|
134
|
+
timestamp: str,
|
|
135
|
+
nonce: str,
|
|
136
|
+
) -> str:
|
|
137
|
+
# Empty-body rule: when body is b"", we compute base64(md5(b"")) and
|
|
138
|
+
# include it in the signed message. The Python, C# and TypeScript SDKs
|
|
139
|
+
# all use this same canonical construction (proven by the shared golden
|
|
140
|
+
# vectors in test_hmac.py).
|
|
141
|
+
#
|
|
142
|
+
# CROSS-IMPL: this is the tool-provider trust boundary (TMS -> tool
|
|
143
|
+
# provider) and is SEPARATE from the api-gateway's inbound HMAC
|
|
144
|
+
# (external -> Opal). The gateway uses a different canonical (no body
|
|
145
|
+
# hash) on its own routes; the two boundaries are independent and are
|
|
146
|
+
# NOT expected to share a wire format. TMS itself has no HMAC
|
|
147
|
+
# implementation. This signer/verifier only needs to be internally
|
|
148
|
+
# consistent; do not assume interop with the gateway canonical.
|
|
149
|
+
body_md5 = base64.b64encode(hashlib.md5(body).digest()).decode()
|
|
150
|
+
|
|
151
|
+
message = f"{self.app_key}{method}{endpoint}{timestamp}{nonce}{body_md5}"
|
|
152
|
+
signature = base64.b64encode(
|
|
153
|
+
hmac_mod.new(self._secret_bytes, message.encode(), hashlib.sha256).digest()
|
|
154
|
+
).decode()
|
|
155
|
+
|
|
156
|
+
return f"epi-hmac {self.app_key}:{timestamp}:{nonce}:{signature}"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from functools import wraps
|
|
3
|
+
|
|
4
|
+
from fastapi import Header, HTTPException
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def requires_auth(provider: str, scope_bundle: str, required: bool = True):
|
|
8
|
+
"""Decorator to indicate that a tool requires authentication.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
provider: Authentication provider (e.g., "google", "microsoft")
|
|
12
|
+
scope_bundle: Scope bundle required (e.g., "calendar", "drive")
|
|
13
|
+
required: Whether authentication is mandatory (default: True)
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Decorator function
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def decorator(func: Callable):
|
|
20
|
+
@wraps(func)
|
|
21
|
+
async def wrapper(*args, authorization: str | None = Header(None), **kwargs):
|
|
22
|
+
if required and not authorization:
|
|
23
|
+
raise HTTPException(status_code=401, detail="Authentication required")
|
|
24
|
+
|
|
25
|
+
# The Tools Management Service will provide the appropriate token
|
|
26
|
+
# in the Authorization header
|
|
27
|
+
return await func(*args, authorization=authorization, **kwargs)
|
|
28
|
+
|
|
29
|
+
# Store auth requirements in function metadata
|
|
30
|
+
auth_req = {"provider": provider, "scope_bundle": scope_bundle, "required": required}
|
|
31
|
+
|
|
32
|
+
# Initialize the list if it doesn't exist
|
|
33
|
+
if not hasattr(wrapper, "__auth_requirements__"):
|
|
34
|
+
wrapper.__auth_requirements__ = [] # type: ignore[attr-defined]
|
|
35
|
+
|
|
36
|
+
# Add this auth requirement to the list
|
|
37
|
+
wrapper.__auth_requirements__.append(auth_req) # type: ignore[attr-defined]
|
|
38
|
+
|
|
39
|
+
return wrapper
|
|
40
|
+
|
|
41
|
+
return decorator
|
|
@@ -99,6 +99,7 @@ def tool(
|
|
|
99
99
|
timeout: int | None = None,
|
|
100
100
|
sensitive: str | bool | None = None,
|
|
101
101
|
wait: bool = False,
|
|
102
|
+
skippable_interaction: bool = False,
|
|
102
103
|
):
|
|
103
104
|
"""Decorator to register a function as an Opal tool.
|
|
104
105
|
|
|
@@ -114,6 +115,8 @@ def tool(
|
|
|
114
115
|
Example: "ui://my-app/create-form"
|
|
115
116
|
wait: When True, invoking this tool pauses the agent execution until the caller
|
|
116
117
|
signals completion.
|
|
118
|
+
skippable_interaction: When True, the tool's confirmation interaction (card) can be
|
|
119
|
+
skipped; a user who opts out has the tool run headless instead.
|
|
117
120
|
|
|
118
121
|
Returns:
|
|
119
122
|
Decorator function
|
|
@@ -309,6 +312,7 @@ def tool(
|
|
|
309
312
|
timeout=timeout,
|
|
310
313
|
sensitive=sensitive,
|
|
311
314
|
wait=wait,
|
|
315
|
+
skippable_interaction=skippable_interaction,
|
|
312
316
|
)
|
|
313
317
|
|
|
314
318
|
return func
|
|
@@ -3,7 +3,7 @@ from enum import Enum
|
|
|
3
3
|
from typing import Any, Literal
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
6
|
-
from typing_extensions import TypedDict # pydantic requires this on Python < 3.12
|
|
6
|
+
from typing_extensions import NotRequired, TypedDict # pydantic requires this on Python < 3.12
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class ParameterType(str, Enum):
|
|
@@ -83,14 +83,17 @@ class InteractionContext:
|
|
|
83
83
|
auth_data: AuthData | None = None
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
class Environment(TypedDict
|
|
86
|
+
class Environment(TypedDict):
|
|
87
87
|
"""Execution environment for an Opal tool.
|
|
88
88
|
|
|
89
89
|
Interactive mode provides interaction islands, while headless does not.
|
|
90
|
+
`execution_mode` is always sent by TMS — required. `is_proteus_enabled`
|
|
91
|
+
was added later, so older TMS versions may omit it; mark it `NotRequired`
|
|
92
|
+
for cross-version forward compatibility.
|
|
90
93
|
"""
|
|
91
94
|
|
|
92
95
|
execution_mode: Literal["headless", "interactive"]
|
|
93
|
-
is_proteus_enabled: bool
|
|
96
|
+
is_proteus_enabled: NotRequired[bool]
|
|
94
97
|
|
|
95
98
|
|
|
96
99
|
@dataclass
|
|
@@ -110,6 +113,7 @@ class Function:
|
|
|
110
113
|
# to a non-empty advisory string when sensitive, else None.
|
|
111
114
|
sensitive: str | bool | None = None
|
|
112
115
|
wait: bool = False # When True, pauses the agent until the caller signals completion
|
|
116
|
+
skippable_interaction: bool = False # When True, the tool's confirmation card can be skipped
|
|
113
117
|
|
|
114
118
|
def __post_init__(self) -> None:
|
|
115
119
|
# Normalize `sensitive` to the wire shape: a single nullable advisory
|
|
@@ -144,6 +148,8 @@ class Function:
|
|
|
144
148
|
result["sensitive"] = self.sensitive
|
|
145
149
|
if self.wait:
|
|
146
150
|
result["wait"] = self.wait
|
|
151
|
+
if self.skippable_interaction:
|
|
152
|
+
result["skippable_interaction"] = self.skippable_interaction
|
|
147
153
|
|
|
148
154
|
return result
|
|
149
155
|
|
|
@@ -314,6 +314,7 @@ class ToolsService:
|
|
|
314
314
|
timeout: int | None = None,
|
|
315
315
|
sensitive: str | bool | None = None,
|
|
316
316
|
wait: bool = False,
|
|
317
|
+
skippable_interaction: bool = False,
|
|
317
318
|
) -> None:
|
|
318
319
|
"""Register a tool function.
|
|
319
320
|
|
|
@@ -329,6 +330,8 @@ class ToolsService:
|
|
|
329
330
|
timeout: Timeout in seconds for async mode
|
|
330
331
|
sensitive: Sensitive data field declaration
|
|
331
332
|
wait: When True, invoking this tool pauses the agent until the caller signals completion
|
|
333
|
+
skippable_interaction: When True, the tool's confirmation interaction (card) can be
|
|
334
|
+
skipped; a user who opts out has the tool run headless instead
|
|
332
335
|
"""
|
|
333
336
|
logger.info(f"Registering tool: {name} with endpoint: {endpoint}")
|
|
334
337
|
|
|
@@ -359,6 +362,7 @@ class ToolsService:
|
|
|
359
362
|
timeout=timeout,
|
|
360
363
|
sensitive=sensitive,
|
|
361
364
|
wait=wait,
|
|
365
|
+
skippable_interaction=skippable_interaction,
|
|
362
366
|
)
|
|
363
367
|
|
|
364
368
|
self.functions.append(function)
|
|
@@ -12,6 +12,9 @@ opal_tools_sdk/proteus.py
|
|
|
12
12
|
opal_tools_sdk/response.py
|
|
13
13
|
opal_tools_sdk/service.py
|
|
14
14
|
opal_tools_sdk/ui.py
|
|
15
|
+
opal_tools_sdk/auth/__init__.py
|
|
16
|
+
opal_tools_sdk/auth/epi_hmac.py
|
|
17
|
+
opal_tools_sdk/auth/requires_auth.py
|
|
15
18
|
optimizely_opal.opal_tools_sdk.egg-info/PKG-INFO
|
|
16
19
|
optimizely_opal.opal_tools_sdk.egg-info/SOURCES.txt
|
|
17
20
|
optimizely_opal.opal_tools_sdk.egg-info/dependency_links.txt
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "optimizely-opal.opal-tools-sdk"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.43-dev"
|
|
8
8
|
description = "SDK for creating Opal-compatible tools services"
|
|
9
9
|
authors = [{ name = "Optimizely", email = "opal-team@optimizely.com" }]
|
|
10
10
|
readme = "README.md"
|
|
@@ -32,8 +32,8 @@ dev = [
|
|
|
32
32
|
"Homepage" = "https://github.com/optimizely/opal-tools-sdk"
|
|
33
33
|
"Bug Tracker" = "https://github.com/optimizely/opal-tools-sdk/issues"
|
|
34
34
|
|
|
35
|
-
[tool.setuptools]
|
|
36
|
-
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
include = ["opal_tools_sdk*"]
|
|
37
37
|
|
|
38
38
|
[tool.ruff]
|
|
39
39
|
line-length = 100
|
|
@@ -860,3 +860,22 @@ def test_v1_tool_bare_dict_unchanged():
|
|
|
860
860
|
assert "application/json" in resp.headers["content-type"]
|
|
861
861
|
# Should NOT be wrapped in envelope
|
|
862
862
|
assert resp.json() == {"v1": True, "name": "Dave"}
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def test_tool_with_skippable_interaction_flag():
|
|
866
|
+
"""A tool marked skippable_interaction advertises it in discovery; others omit the key."""
|
|
867
|
+
app = FastAPI()
|
|
868
|
+
_ = ToolsService(app)
|
|
869
|
+
|
|
870
|
+
@tool(name="skip_me", description="skippable tool", skippable_interaction=True)
|
|
871
|
+
async def skip_me(params: SimpleParams) -> dict:
|
|
872
|
+
return {"ok": True}
|
|
873
|
+
|
|
874
|
+
@tool(name="plain", description="plain tool")
|
|
875
|
+
async def plain(params: SimpleParams) -> dict:
|
|
876
|
+
return {"ok": True}
|
|
877
|
+
|
|
878
|
+
discovery = TestClient(app).get("/discovery").json()
|
|
879
|
+
by_name = {f["name"]: f for f in discovery["functions"]}
|
|
880
|
+
assert by_name["skip_me"]["skippable_interaction"] is True
|
|
881
|
+
assert "skippable_interaction" not in by_name["plain"]
|
{optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{optimizely_opal_opal_tools_sdk-0.1.41.dev0 → optimizely_opal_opal_tools_sdk-0.1.43.dev0}/setup.cfg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|