Compare commits

...

5 Commits

Author SHA1 Message Date
Greyson LaLonde
e33bf09d1c Merge branch 'main' into lg-isolate-platform-integration-token 2026-02-19 17:51:50 -05:00
github-actions[bot]
50dba2e155 chore: update tool specifications 2026-02-19 14:36:07 +00:00
Lucas Gomide
02d53ab009 style: resolver linter issues 2026-02-19 11:32:41 -03:00
Lucas Gomide
1e3f0c9c8b feat: enhance platform_integration_context with nullcontext support
Improve the platform integration context manager to handle None/falsy tokens gracefully by returning nullcontext
2026-02-18 10:43:38 -03:00
Lucas Gomide
727dccdfb8 feat: isolate retrieval of platform integration token (context.var or env var) 2026-02-18 10:43:38 -03:00
5 changed files with 211 additions and 237 deletions

View File

@@ -1,5 +1,7 @@
import os import os
from crewai.context import get_platform_integration_token as _get_context_token
def get_platform_api_base_url() -> str: def get_platform_api_base_url() -> str:
"""Get the platform API base URL from environment or use default.""" """Get the platform API base URL from environment or use default."""
@@ -8,10 +10,16 @@ def get_platform_api_base_url() -> str:
def get_platform_integration_token() -> str: def get_platform_integration_token() -> str:
"""Get the platform API base URL from environment or use default.""" """Get the platform integration token from the context.
token = os.getenv("CREWAI_PLATFORM_INTEGRATION_TOKEN") or "" Fallback to the environment variable if no token has been set in the context.
Raises:
ValueError: If no token has been set in the context.
"""
token = _get_context_token() or os.getenv("CREWAI_PLATFORM_INTEGRATION_TOKEN")
if not token: if not token:
raise ValueError( raise ValueError(
"No platform integration token found, please set the CREWAI_PLATFORM_INTEGRATION_TOKEN environment variable" "No platform integration token found. "
"Set it via platform_integration_context() or set_platform_integration_token()."
) )
return token # TODO: Use context manager to get token return token

View File

@@ -0,0 +1,56 @@
"""Tests for platform tools misc functionality."""
import os
from unittest.mock import patch
import pytest
from crewai.context import platform_integration_context, set_platform_integration_token, reset_platform_integration_token
from crewai_tools.tools.crewai_platform_tools.misc import (
get_platform_integration_token,
)
class TestTokenRetrievalWithFallback:
"""Test token retrieval logic with environment fallback."""
@pytest.fixture
def clean_context(self):
token = set_platform_integration_token(None)
env_backup = os.environ.pop("CREWAI_PLATFORM_INTEGRATION_TOKEN", None)
yield
reset_platform_integration_token(token)
if env_backup is not None:
os.environ["CREWAI_PLATFORM_INTEGRATION_TOKEN"] = env_backup
else:
os.environ.pop("CREWAI_PLATFORM_INTEGRATION_TOKEN", None)
def test_context_token_takes_precedence(self, clean_context):
"""Test that context token takes precedence over environment variable."""
context_token = "context-token"
env_token = "env-token"
with patch.dict(os.environ, {"CREWAI_PLATFORM_INTEGRATION_TOKEN": env_token}):
with platform_integration_context(context_token):
token = get_platform_integration_token()
assert token == context_token
def test_environment_fallback_when_no_context(self, clean_context):
"""Test fallback to environment variable when no context token."""
env_token = "env-fallback-token"
with patch.dict(os.environ, {"CREWAI_PLATFORM_INTEGRATION_TOKEN": env_token}):
token = get_platform_integration_token()
assert token == env_token
@pytest.mark.parametrize("empty_value", ["", None])
def test_missing_token_raises_error(self, clean_context, empty_value):
"""Test that missing tokens raise appropriate errors."""
env_dict = {"CREWAI_PLATFORM_INTEGRATION_TOKEN": empty_value} if empty_value is not None else {}
with patch.dict(os.environ, env_dict, clear=True):
with pytest.raises(ValueError) as exc_info:
get_platform_integration_token()
assert "No platform integration token found" in str(exc_info.value)
assert "platform_integration_context()" in str(exc_info.value)

View File

@@ -20117,18 +20117,6 @@
"humanized_name": "Web Automation Tool", "humanized_name": "Web Automation Tool",
"init_params_schema": { "init_params_schema": {
"$defs": { "$defs": {
"AvailableModel": {
"enum": [
"gpt-4o",
"gpt-4o-mini",
"claude-3-5-sonnet-latest",
"claude-3-7-sonnet-latest",
"computer-use-preview",
"gemini-2.0-flash"
],
"title": "AvailableModel",
"type": "string"
},
"EnvVar": { "EnvVar": {
"properties": { "properties": {
"default": { "default": {
@@ -20206,17 +20194,6 @@
"default": null, "default": null,
"title": "Model Api Key" "title": "Model Api Key"
}, },
"model_name": {
"anyOf": [
{
"$ref": "#/$defs/AvailableModel"
},
{
"type": "null"
}
],
"default": "claude-3-7-sonnet-latest"
},
"project_id": { "project_id": {
"anyOf": [ "anyOf": [
{ {

View File

@@ -1,7 +1,6 @@
from collections.abc import Generator from collections.abc import Generator
from contextlib import contextmanager from contextlib import AbstractContextManager, contextmanager, nullcontext
import contextvars import contextvars
import os
from typing import Any from typing import Any
@@ -10,40 +9,50 @@ _platform_integration_token: contextvars.ContextVar[str | None] = (
) )
def set_platform_integration_token(integration_token: str) -> None: def set_platform_integration_token(integration_token: str) -> contextvars.Token[str | None]:
"""Set the platform integration token in the current context. """Set the platform integration token in the current context.
Args: Args:
integration_token: The integration token to set. integration_token: The integration token to set.
""" """
_platform_integration_token.set(integration_token) return _platform_integration_token.set(integration_token)
def reset_platform_integration_token(token: contextvars.Token[str | None]) -> None:
"""Reset the platform integration token to its previous value."""
_platform_integration_token.reset(token)
def get_platform_integration_token() -> str | None: def get_platform_integration_token() -> str | None:
"""Get the platform integration token from the current context or environment. """Get the platform integration token from the current context.
Returns: Returns:
The integration token if set, otherwise None. The integration token if set, otherwise None.
""" """
token = _platform_integration_token.get() return _platform_integration_token.get()
if token is None:
token = os.getenv("CREWAI_PLATFORM_INTEGRATION_TOKEN")
return token
@contextmanager def platform_integration_context(integration_token: str | None) -> AbstractContextManager[None]:
def platform_context(integration_token: str) -> Generator[None, Any, None]:
"""Context manager to temporarily set the platform integration token. """Context manager to temporarily set the platform integration token.
Args: Args:
integration_token: The integration token to set within the context. integration_token: The integration token to set within the context.
""" If None or falsy, returns nullcontext (no-op).
token = _platform_integration_token.set(integration_token)
try:
yield
finally:
_platform_integration_token.reset(token)
Returns:
A context manager that either sets the token or does nothing.
"""
if not integration_token:
return nullcontext()
@contextmanager
def _token_context() -> Generator[None, Any, None]:
token = set_platform_integration_token(integration_token)
try:
yield
finally:
reset_platform_integration_token(token)
return _token_context()
_current_task_id: contextvars.ContextVar[str | None] = contextvars.ContextVar( _current_task_id: contextvars.ContextVar[str | None] = contextvars.ContextVar(
"current_task_id", default=None "current_task_id", default=None

View File

@@ -7,215 +7,139 @@ import pytest
from crewai.context import ( from crewai.context import (
_platform_integration_token, _platform_integration_token,
get_platform_integration_token, get_platform_integration_token,
platform_context, platform_integration_context,
reset_platform_integration_token,
set_platform_integration_token, set_platform_integration_token,
) )
class TestPlatformIntegrationToken: @pytest.fixture
def setup_method(self): def clean_context():
_platform_integration_token.set(None) """Fixture to ensure clean context state for each test."""
_platform_integration_token.set(None)
yield
_platform_integration_token.set(None)
def teardown_method(self):
_platform_integration_token.set(None)
@patch.dict(os.environ, {}, clear=True) class TestContextVariableCore:
def test_set_platform_integration_token(self): """Test core context variable functionality (set/get/reset)."""
def test_set_and_get_token(self, clean_context):
"""Test basic token setting and retrieval."""
test_token = "test-token-123" test_token = "test-token-123"
assert get_platform_integration_token() is None assert get_platform_integration_token() is None
set_platform_integration_token(test_token) context_token = set_platform_integration_token(test_token)
assert get_platform_integration_token() == test_token assert get_platform_integration_token() == test_token
assert context_token is not None
def test_get_platform_integration_token_from_context_var(self): def test_reset_token_restores_previous_state(self, clean_context):
test_token = "context-var-token" """Test that reset properly restores previous context state."""
_platform_integration_token.set(test_token)
assert get_platform_integration_token() == test_token
@patch.dict(os.environ, {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "env-token-456"})
def test_get_platform_integration_token_from_env_var(self):
assert _platform_integration_token.get() is None
assert get_platform_integration_token() == "env-token-456"
@patch.dict(os.environ, {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "env-token"})
def test_context_var_takes_precedence_over_env_var(self):
context_token = "context-token"
set_platform_integration_token(context_token)
assert get_platform_integration_token() == context_token
@patch.dict(os.environ, {}, clear=True)
def test_get_platform_integration_token_returns_none_when_not_set(self):
assert _platform_integration_token.get() is None
assert get_platform_integration_token() is None
@patch.dict(os.environ, {}, clear=True)
def test_platform_context_manager_basic_usage(self):
test_token = "context-manager-token"
assert get_platform_integration_token() is None
with platform_context(test_token):
assert get_platform_integration_token() == test_token
assert get_platform_integration_token() is None
@patch.dict(os.environ, {}, clear=True)
def test_platform_context_manager_nested_contexts(self):
"""Test nested platform_context context managers."""
outer_token = "outer-token"
inner_token = "inner-token"
assert get_platform_integration_token() is None
with platform_context(outer_token):
assert get_platform_integration_token() == outer_token
with platform_context(inner_token):
assert get_platform_integration_token() == inner_token
assert get_platform_integration_token() == outer_token
assert get_platform_integration_token() is None
def test_platform_context_manager_preserves_existing_token(self):
"""Test that platform_context preserves existing token when exiting."""
initial_token = "initial-token"
context_token = "context-token"
set_platform_integration_token(initial_token)
assert get_platform_integration_token() == initial_token
with platform_context(context_token):
assert get_platform_integration_token() == context_token
assert get_platform_integration_token() == initial_token
def test_platform_context_manager_exception_handling(self):
"""Test that platform_context properly resets token even when exception occurs."""
initial_token = "initial-token"
context_token = "context-token"
set_platform_integration_token(initial_token)
with pytest.raises(ValueError):
with platform_context(context_token):
assert get_platform_integration_token() == context_token
raise ValueError("Test exception")
assert get_platform_integration_token() == initial_token
@patch.dict(os.environ, {}, clear=True)
def test_platform_context_manager_with_none_initial_state(self):
"""Test platform_context when initial state is None."""
context_token = "context-token"
assert get_platform_integration_token() is None
with pytest.raises(RuntimeError):
with platform_context(context_token):
assert get_platform_integration_token() == context_token
raise RuntimeError("Test exception")
assert get_platform_integration_token() is None
@patch.dict(os.environ, {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "env-backup"})
def test_platform_context_with_env_fallback(self):
"""Test platform_context interaction with environment variable fallback."""
context_token = "context-token"
assert get_platform_integration_token() == "env-backup"
with platform_context(context_token):
assert get_platform_integration_token() == context_token
assert get_platform_integration_token() == "env-backup"
@patch.dict(os.environ, {}, clear=True)
def test_multiple_sequential_context_managers(self):
"""Test multiple sequential uses of platform_context."""
token1 = "token-1" token1 = "token-1"
token2 = "token-2" token2 = "token-2"
token3 = "token-3"
with platform_context(token1): context_token1 = set_platform_integration_token(token1)
assert get_platform_integration_token() == token1 assert get_platform_integration_token() == token1
context_token2 = set_platform_integration_token(token2)
assert get_platform_integration_token() == token2
reset_platform_integration_token(context_token2)
assert get_platform_integration_token() == token1
reset_platform_integration_token(context_token1)
assert get_platform_integration_token() is None
def test_nested_token_management(self, clean_context):
"""Test proper token management with deeply nested contexts."""
tokens = ["token-1", "token-2", "token-3"]
context_tokens = []
for token in tokens:
context_tokens.append(set_platform_integration_token(token))
assert get_platform_integration_token() == token
for i in range(len(tokens) - 1, 0, -1):
reset_platform_integration_token(context_tokens[i])
assert get_platform_integration_token() == tokens[i - 1]
reset_platform_integration_token(context_tokens[0])
assert get_platform_integration_token() is None
@patch.dict(os.environ, {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "env-token"})
def test_context_module_ignores_environment_variables(self, clean_context):
"""Test that context module only returns context values, not env vars."""
# Context module should not read environment variables
assert get_platform_integration_token() is None
# Only context variable should be returned
set_platform_integration_token("context-token")
assert get_platform_integration_token() == "context-token"
class TestPlatformIntegrationContext:
"""Test platform integration context manager behavior."""
def test_basic_context_manager_usage(self, clean_context):
"""Test basic context manager functionality."""
test_token = "context-token"
assert get_platform_integration_token() is None assert get_platform_integration_token() is None
with platform_context(token2): with platform_integration_context(test_token):
assert get_platform_integration_token() == token2
assert get_platform_integration_token() is None
with platform_context(token3):
assert get_platform_integration_token() == token3
assert get_platform_integration_token() is None
def test_empty_string_token(self):
empty_token = ""
set_platform_integration_token(empty_token)
assert get_platform_integration_token() == ""
with platform_context(empty_token):
assert get_platform_integration_token() == ""
def test_special_characters_in_token(self):
special_token = "token-with-!@#$%^&*()_+-={}[]|\\:;\"'<>?,./"
set_platform_integration_token(special_token)
assert get_platform_integration_token() == special_token
with platform_context(special_token):
assert get_platform_integration_token() == special_token
def test_very_long_token(self):
long_token = "a" * 10000
set_platform_integration_token(long_token)
assert get_platform_integration_token() == long_token
with platform_context(long_token):
assert get_platform_integration_token() == long_token
@patch.dict(os.environ, {"CREWAI_PLATFORM_INTEGRATION_TOKEN": ""})
def test_empty_env_var(self):
assert _platform_integration_token.get() is None
assert get_platform_integration_token() == ""
@patch("crewai.context.os.getenv")
def test_env_var_access_error_handling(self, mock_getenv):
mock_getenv.side_effect = OSError("Environment access error")
with pytest.raises(OSError):
get_platform_integration_token()
@patch.dict(os.environ, {}, clear=True)
def test_context_var_isolation_between_tests(self):
"""Test that context variable changes don't leak between test methods."""
test_token = "isolation-test-token"
assert get_platform_integration_token() is None
set_platform_integration_token(test_token)
assert get_platform_integration_token() == test_token
def test_context_manager_return_value(self):
"""Test that platform_context can be used in with statement with return value."""
test_token = "return-value-token"
with platform_context(test_token):
assert get_platform_integration_token() == test_token assert get_platform_integration_token() == test_token
with platform_context(test_token) as ctx: assert get_platform_integration_token() is None
assert ctx is None
assert get_platform_integration_token() == test_token @pytest.mark.parametrize("falsy_value", [None, "", False, 0])
def test_falsy_values_return_nullcontext(self, clean_context, falsy_value):
"""Test that falsy values return nullcontext (no-op)."""
# Set initial token to verify nullcontext doesn't affect it
initial_token = "initial-token"
initial_context_token = set_platform_integration_token(initial_token)
try:
with platform_integration_context(falsy_value):
# Should preserve existing context (nullcontext behavior)
assert get_platform_integration_token() == initial_token
# Should still have initial token after nullcontext
assert get_platform_integration_token() == initial_token
finally:
reset_platform_integration_token(initial_context_token)
@pytest.mark.parametrize("truthy_value", ["token", "123", " ", "0"])
def test_truthy_values_create_context(self, clean_context, truthy_value):
"""Test that truthy values create proper context."""
with platform_integration_context(truthy_value):
assert get_platform_integration_token() == truthy_value
# Should be cleaned up
assert get_platform_integration_token() is None
def test_context_preserves_existing_token(self, clean_context):
"""Test that context manager preserves existing token when exiting."""
existing_token = "existing-token"
context_token = "context-token"
existing_context_token = set_platform_integration_token(existing_token)
try:
with platform_integration_context(context_token):
assert get_platform_integration_token() == context_token
assert get_platform_integration_token() == existing_token
finally:
reset_platform_integration_token(existing_context_token)
def test_context_manager_return_type(self, clean_context):
"""Test that context manager returns proper types for both cases."""
# Both should be usable as context managers
valid_ctx = platform_integration_context("token")
none_ctx = platform_integration_context(None)
assert hasattr(valid_ctx, '__enter__')
assert hasattr(valid_ctx, '__exit__')
assert hasattr(none_ctx, '__enter__')
assert hasattr(none_ctx, '__exit__')