cred-crewai 0.1.0__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.
- cred_crewai-0.1.0/.gitignore +50 -0
- cred_crewai-0.1.0/PKG-INFO +71 -0
- cred_crewai-0.1.0/README.md +55 -0
- cred_crewai-0.1.0/cred_crewai/__init__.py +5 -0
- cred_crewai-0.1.0/cred_crewai/tool.py +105 -0
- cred_crewai-0.1.0/pyproject.toml +27 -0
- cred_crewai-0.1.0/tests/__init__.py +0 -0
- cred_crewai-0.1.0/tests/test_tool.py +247 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
dist/
|
|
3
|
+
build/
|
|
4
|
+
|
|
5
|
+
# Sprint artifacts
|
|
6
|
+
.factory/
|
|
7
|
+
.sprint-*
|
|
8
|
+
.sprint-pid
|
|
9
|
+
.rewire-done
|
|
10
|
+
.content-rewrite-done
|
|
11
|
+
.hubspot-done
|
|
12
|
+
.integrations-done
|
|
13
|
+
.done
|
|
14
|
+
.validation-report.md
|
|
15
|
+
.sprint-logs/
|
|
16
|
+
.sprint-prompts/
|
|
17
|
+
SESSION_PACKAGE.md
|
|
18
|
+
run-sprint.sh
|
|
19
|
+
validate.sh
|
|
20
|
+
|
|
21
|
+
# Internal planning docs (not for public repo)
|
|
22
|
+
CLAUDE.md
|
|
23
|
+
PLAN.md
|
|
24
|
+
HANDOFF.md
|
|
25
|
+
VALIDATION.md
|
|
26
|
+
MARKETING-VALIDATION.md
|
|
27
|
+
FINAL-SUBMISSION-VALIDATION.md
|
|
28
|
+
.next/
|
|
29
|
+
out/
|
|
30
|
+
coverage/
|
|
31
|
+
*.tsbuildinfo
|
|
32
|
+
.turbo/
|
|
33
|
+
.DS_Store
|
|
34
|
+
*.pem
|
|
35
|
+
.env
|
|
36
|
+
.env.local
|
|
37
|
+
.env*.local
|
|
38
|
+
!.env.example
|
|
39
|
+
|
|
40
|
+
# Python
|
|
41
|
+
__pycache__/
|
|
42
|
+
*.py[cod]
|
|
43
|
+
*.pyo
|
|
44
|
+
*.egg-info/
|
|
45
|
+
.pytest_cache/
|
|
46
|
+
.venv/
|
|
47
|
+
venv/
|
|
48
|
+
dist/
|
|
49
|
+
*.dist-info/
|
|
50
|
+
packages/sdk-python/.venv/
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cred-crewai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cred CrewAI integration — pre-configured credential delegation tools for CrewAI agents
|
|
5
|
+
Author-email: Cred <hello@cred.ninja>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Keywords: ai-agents,cred,crewai,delegation,oauth
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: cred-auth>=0.1.0
|
|
10
|
+
Requires-Dist: crewai>=0.11
|
|
11
|
+
Requires-Dist: langchain-core>=0.2
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: hatchling; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# cred-crewai
|
|
18
|
+
|
|
19
|
+
CrewAI integration for Cred. OAuth2 credential delegation for AI agents.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install cred-crewai
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import os
|
|
31
|
+
from cred_crewai import CredTool
|
|
32
|
+
from crewai import Agent
|
|
33
|
+
|
|
34
|
+
# Create a pre-configured tool for Google Calendar
|
|
35
|
+
google_tool = CredTool(
|
|
36
|
+
agent_token=os.environ["CRED_AGENT_TOKEN"],
|
|
37
|
+
user_id="user_123",
|
|
38
|
+
service="google",
|
|
39
|
+
app_client_id="my_app_client_id",
|
|
40
|
+
scopes=["calendar.readonly"],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Use with CrewAI agent
|
|
44
|
+
agent = Agent(
|
|
45
|
+
role="Calendar Manager",
|
|
46
|
+
goal="Manage user's calendar",
|
|
47
|
+
tools=[google_tool],
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Key Differences from LangChain
|
|
52
|
+
|
|
53
|
+
**LangChain integration** (`cred-langchain`):
|
|
54
|
+
- `CredToolkit` returns 3 generic tools: delegate, status, revoke
|
|
55
|
+
- Best for agents that need full credential management
|
|
56
|
+
|
|
57
|
+
**CrewAI integration** (`cred-crewai`):
|
|
58
|
+
- `CredTool` is pre-configured for a **single service**
|
|
59
|
+
- Tool name auto-generated: `cred_google_delegate`, `cred_github_delegate`, etc.
|
|
60
|
+
- Best for specialized agents (e.g., a "Calendar Manager" agent only needs Google tokens)
|
|
61
|
+
|
|
62
|
+
One `CredTool` per service per agent = clearer intent, smaller decision space for the LLM.
|
|
63
|
+
|
|
64
|
+
## Handling Consent
|
|
65
|
+
|
|
66
|
+
When a user hasn't connected the service yet, `_run()` raises `ConsentRequiredError`.
|
|
67
|
+
Catch it and redirect the user to `e.consent_url` to complete the OAuth flow.
|
|
68
|
+
|
|
69
|
+
## Cred Cloud (Coming Soon)
|
|
70
|
+
|
|
71
|
+
Managed cloud delegation is coming. [Join the waitlist](https://cred.ninja/waitlist).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# cred-crewai
|
|
2
|
+
|
|
3
|
+
CrewAI integration for Cred. OAuth2 credential delegation for AI agents.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install cred-crewai
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import os
|
|
15
|
+
from cred_crewai import CredTool
|
|
16
|
+
from crewai import Agent
|
|
17
|
+
|
|
18
|
+
# Create a pre-configured tool for Google Calendar
|
|
19
|
+
google_tool = CredTool(
|
|
20
|
+
agent_token=os.environ["CRED_AGENT_TOKEN"],
|
|
21
|
+
user_id="user_123",
|
|
22
|
+
service="google",
|
|
23
|
+
app_client_id="my_app_client_id",
|
|
24
|
+
scopes=["calendar.readonly"],
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Use with CrewAI agent
|
|
28
|
+
agent = Agent(
|
|
29
|
+
role="Calendar Manager",
|
|
30
|
+
goal="Manage user's calendar",
|
|
31
|
+
tools=[google_tool],
|
|
32
|
+
)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Key Differences from LangChain
|
|
36
|
+
|
|
37
|
+
**LangChain integration** (`cred-langchain`):
|
|
38
|
+
- `CredToolkit` returns 3 generic tools: delegate, status, revoke
|
|
39
|
+
- Best for agents that need full credential management
|
|
40
|
+
|
|
41
|
+
**CrewAI integration** (`cred-crewai`):
|
|
42
|
+
- `CredTool` is pre-configured for a **single service**
|
|
43
|
+
- Tool name auto-generated: `cred_google_delegate`, `cred_github_delegate`, etc.
|
|
44
|
+
- Best for specialized agents (e.g., a "Calendar Manager" agent only needs Google tokens)
|
|
45
|
+
|
|
46
|
+
One `CredTool` per service per agent = clearer intent, smaller decision space for the LLM.
|
|
47
|
+
|
|
48
|
+
## Handling Consent
|
|
49
|
+
|
|
50
|
+
When a user hasn't connected the service yet, `_run()` raises `ConsentRequiredError`.
|
|
51
|
+
Catch it and redirect the user to `e.consent_url` to complete the OAuth flow.
|
|
52
|
+
|
|
53
|
+
## Cred Cloud (Coming Soon)
|
|
54
|
+
|
|
55
|
+
Managed cloud delegation is coming. [Join the waitlist](https://cred.ninja/waitlist).
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Cred CrewAI Tool — pre-configured delegation tool for a single service.
|
|
2
|
+
|
|
3
|
+
CrewAI's BaseTool is langchain_core.tools.BaseTool under the hood
|
|
4
|
+
(verified in crewai 0.11.x source). We import from langchain_core so this
|
|
5
|
+
package works in environments where crewai is installed alongside langchain,
|
|
6
|
+
without requiring crewai as a hard build dependency at import time.
|
|
7
|
+
|
|
8
|
+
When crewai is installed, pass CredTool instances directly to Agent(tools=[...]).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
from typing import Any, Optional, Type
|
|
15
|
+
|
|
16
|
+
from langchain_core.tools import BaseTool
|
|
17
|
+
from pydantic import BaseModel, Field
|
|
18
|
+
|
|
19
|
+
from cred import Cred
|
|
20
|
+
from cred.exceptions import ConsentRequiredError # re-export for convenience
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _CredToolInput(BaseModel):
|
|
24
|
+
"""Placeholder input — CredTool has no runtime inputs; all params are pre-configured."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CredTool(BaseTool):
|
|
29
|
+
"""A CrewAI-compatible tool that delegates credentials for a single service.
|
|
30
|
+
|
|
31
|
+
Pre-configured at construction time with a specific service, user, and
|
|
32
|
+
optional scopes. The agent calls it with no additional parameters — the
|
|
33
|
+
service/user context is baked in, keeping the agent's decision surface small.
|
|
34
|
+
|
|
35
|
+
Example::
|
|
36
|
+
|
|
37
|
+
from cred_crewai import CredTool
|
|
38
|
+
|
|
39
|
+
google_tool = CredTool(
|
|
40
|
+
agent_token=os.environ["CRED_AGENT_TOKEN"],
|
|
41
|
+
user_id="user_123",
|
|
42
|
+
service="google",
|
|
43
|
+
app_client_id="my_app",
|
|
44
|
+
scopes=["calendar.readonly"],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Use with CrewAI
|
|
48
|
+
from crewai import Agent
|
|
49
|
+
agent = Agent(role="Calendar Manager", tools=[google_tool])
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# langchain_core requires these as class-level annotations
|
|
53
|
+
name: str = "cred_delegate"
|
|
54
|
+
description: str = "Get a delegated OAuth access token for a service"
|
|
55
|
+
args_schema: Type[BaseModel] = _CredToolInput
|
|
56
|
+
|
|
57
|
+
# Tool state — set in __init__
|
|
58
|
+
_cred: Any = None
|
|
59
|
+
_user_id: str = ""
|
|
60
|
+
_service: str = ""
|
|
61
|
+
_app_client_id: str = ""
|
|
62
|
+
_scopes: list[str] = []
|
|
63
|
+
_cached_token: str | None = None
|
|
64
|
+
_cache_expires_at: float = 0.0
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
agent_token: str,
|
|
69
|
+
user_id: str,
|
|
70
|
+
service: str,
|
|
71
|
+
app_client_id: str,
|
|
72
|
+
scopes: Optional[list[str]] = None,
|
|
73
|
+
base_url: str = "https://api.cred.ninja",
|
|
74
|
+
**kwargs: Any,
|
|
75
|
+
) -> None:
|
|
76
|
+
super().__init__(**kwargs)
|
|
77
|
+
self._cred = Cred(agent_token=agent_token, base_url=base_url)
|
|
78
|
+
self._user_id = user_id
|
|
79
|
+
self._service = service
|
|
80
|
+
self._app_client_id = app_client_id
|
|
81
|
+
self._scopes = scopes or []
|
|
82
|
+
# Set dynamic name and description after init
|
|
83
|
+
self.name = f"cred_{service.replace('-', '_')}_delegate"
|
|
84
|
+
self.description = (
|
|
85
|
+
f"Get a delegated OAuth access token for {service} on behalf of the current user. "
|
|
86
|
+
f"Returns the access token string. "
|
|
87
|
+
f"Raises ConsentRequiredError (with consent_url) if the user hasn't connected {service}."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def _run(self, **kwargs: Any) -> str:
|
|
91
|
+
"""Delegate credentials and return the access token string."""
|
|
92
|
+
now = time.monotonic()
|
|
93
|
+
if self._cached_token is not None and self._cache_expires_at > now:
|
|
94
|
+
return self._cached_token
|
|
95
|
+
|
|
96
|
+
result = self._cred.delegate(
|
|
97
|
+
service=self._service,
|
|
98
|
+
user_id=self._user_id,
|
|
99
|
+
app_client_id=self._app_client_id,
|
|
100
|
+
scopes=self._scopes if self._scopes else None,
|
|
101
|
+
)
|
|
102
|
+
self._cached_token = result.access_token
|
|
103
|
+
expires_in = getattr(result, "expires_in", None)
|
|
104
|
+
self._cache_expires_at = now + expires_in - 60 if isinstance(expires_in, int) and expires_in > 60 else 0.0
|
|
105
|
+
return result.access_token
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cred-crewai"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Cred CrewAI integration — pre-configured credential delegation tools for CrewAI agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "Cred", email = "hello@cred.ninja" }]
|
|
13
|
+
keywords = ["cred", "crewai", "oauth", "delegation", "ai-agents"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"cred-auth>=0.1.0",
|
|
16
|
+
"crewai>=0.11",
|
|
17
|
+
"langchain-core>=0.2",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = ["pytest>=7.0", "hatchling"]
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["cred_crewai"]
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
testpaths = ["tests"]
|
|
File without changes
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Cred CrewAI Tool tests.
|
|
2
|
+
|
|
3
|
+
Mocks the Cred client — no real HTTP, no real API calls.
|
|
4
|
+
Tests focus on: tool configuration, naming, input schema, output format, error propagation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import warnings
|
|
8
|
+
warnings.filterwarnings("ignore", category=UserWarning)
|
|
9
|
+
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
import httpx
|
|
15
|
+
from unittest.mock import MagicMock, patch
|
|
16
|
+
|
|
17
|
+
from cred import DelegationResult, ConsentRequiredError
|
|
18
|
+
from cred_crewai import CredTool
|
|
19
|
+
|
|
20
|
+
TOKEN = "cred_at_test"
|
|
21
|
+
USER_ID = "user_123"
|
|
22
|
+
APP_CLIENT_ID = "app_1"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def make_delegation_result(
|
|
26
|
+
*,
|
|
27
|
+
access_token: str = "at",
|
|
28
|
+
token_type: str = "Bearer",
|
|
29
|
+
expires_in: int = 3600,
|
|
30
|
+
service: str = "google",
|
|
31
|
+
scopes: Optional[list[str]] = None,
|
|
32
|
+
delegation_id: str = "del_1",
|
|
33
|
+
) -> DelegationResult:
|
|
34
|
+
return DelegationResult(
|
|
35
|
+
access_token=access_token,
|
|
36
|
+
token_type=token_type,
|
|
37
|
+
expires_in=expires_in,
|
|
38
|
+
expires_at=datetime(2026, 3, 1, tzinfo=timezone.utc),
|
|
39
|
+
service=service,
|
|
40
|
+
scopes=scopes or [],
|
|
41
|
+
delegation_id=delegation_id,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.fixture
|
|
46
|
+
def mock_cred():
|
|
47
|
+
return MagicMock()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.fixture
|
|
51
|
+
def tool(mock_cred):
|
|
52
|
+
t = CredTool(
|
|
53
|
+
agent_token=TOKEN,
|
|
54
|
+
user_id=USER_ID,
|
|
55
|
+
service="google",
|
|
56
|
+
app_client_id=APP_CLIENT_ID,
|
|
57
|
+
scopes=["calendar.readonly"],
|
|
58
|
+
)
|
|
59
|
+
t._cred = mock_cred
|
|
60
|
+
return t
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ── constructor & configuration ───────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
class TestConstructor:
|
|
66
|
+
def test_sets_service_from_param(self):
|
|
67
|
+
t = CredTool(
|
|
68
|
+
agent_token=TOKEN,
|
|
69
|
+
user_id=USER_ID,
|
|
70
|
+
service="github",
|
|
71
|
+
app_client_id=APP_CLIENT_ID,
|
|
72
|
+
)
|
|
73
|
+
assert t._service == "github"
|
|
74
|
+
assert "github" in t.name
|
|
75
|
+
|
|
76
|
+
def test_dynamic_name_includes_service(self):
|
|
77
|
+
google_tool = CredTool(
|
|
78
|
+
agent_token=TOKEN,
|
|
79
|
+
user_id=USER_ID,
|
|
80
|
+
service="google-calendar",
|
|
81
|
+
app_client_id=APP_CLIENT_ID,
|
|
82
|
+
)
|
|
83
|
+
assert google_tool.name == "cred_google_calendar_delegate"
|
|
84
|
+
|
|
85
|
+
def test_dynamic_description_includes_service(self):
|
|
86
|
+
t = CredTool(
|
|
87
|
+
agent_token=TOKEN,
|
|
88
|
+
user_id=USER_ID,
|
|
89
|
+
service="github",
|
|
90
|
+
app_client_id=APP_CLIENT_ID,
|
|
91
|
+
)
|
|
92
|
+
assert "github" in t.description
|
|
93
|
+
assert "access token" in t.description
|
|
94
|
+
|
|
95
|
+
def test_stores_user_id(self):
|
|
96
|
+
t = CredTool(
|
|
97
|
+
agent_token=TOKEN,
|
|
98
|
+
user_id="user_456",
|
|
99
|
+
service="google",
|
|
100
|
+
app_client_id=APP_CLIENT_ID,
|
|
101
|
+
)
|
|
102
|
+
assert t._user_id == "user_456"
|
|
103
|
+
|
|
104
|
+
def test_stores_scopes(self):
|
|
105
|
+
scopes = ["calendar.readonly", "calendar.events"]
|
|
106
|
+
t = CredTool(
|
|
107
|
+
agent_token=TOKEN,
|
|
108
|
+
user_id=USER_ID,
|
|
109
|
+
service="google",
|
|
110
|
+
app_client_id=APP_CLIENT_ID,
|
|
111
|
+
scopes=scopes,
|
|
112
|
+
)
|
|
113
|
+
assert t._scopes == scopes
|
|
114
|
+
|
|
115
|
+
def test_defaults_scopes_to_empty_list(self):
|
|
116
|
+
t = CredTool(
|
|
117
|
+
agent_token=TOKEN,
|
|
118
|
+
user_id=USER_ID,
|
|
119
|
+
service="google",
|
|
120
|
+
app_client_id=APP_CLIENT_ID,
|
|
121
|
+
)
|
|
122
|
+
assert t._scopes == []
|
|
123
|
+
|
|
124
|
+
def test_custom_base_url(self):
|
|
125
|
+
with patch("cred_crewai.tool.Cred") as mock_cred_class:
|
|
126
|
+
CredTool(
|
|
127
|
+
agent_token=TOKEN,
|
|
128
|
+
user_id=USER_ID,
|
|
129
|
+
service="google",
|
|
130
|
+
app_client_id=APP_CLIENT_ID,
|
|
131
|
+
base_url="http://localhost:3001",
|
|
132
|
+
)
|
|
133
|
+
mock_cred_class.assert_called_once_with(
|
|
134
|
+
agent_token=TOKEN,
|
|
135
|
+
base_url="http://localhost:3001",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ── _run() ────────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
class TestRun:
|
|
142
|
+
def test_returns_access_token_string(self, tool, mock_cred):
|
|
143
|
+
mock_cred.delegate.return_value = make_delegation_result(
|
|
144
|
+
access_token="ya29.mock",
|
|
145
|
+
expires_in=3600,
|
|
146
|
+
service="google",
|
|
147
|
+
scopes=["calendar.readonly"],
|
|
148
|
+
delegation_id="del_abc",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
result = tool._run()
|
|
152
|
+
|
|
153
|
+
assert result == "ya29.mock"
|
|
154
|
+
assert isinstance(result, str)
|
|
155
|
+
|
|
156
|
+
def test_passes_service_user_app_client_id(self, tool, mock_cred):
|
|
157
|
+
mock_cred.delegate.return_value = make_delegation_result()
|
|
158
|
+
|
|
159
|
+
tool._run()
|
|
160
|
+
|
|
161
|
+
mock_cred.delegate.assert_called_once_with(
|
|
162
|
+
service="google",
|
|
163
|
+
user_id=USER_ID,
|
|
164
|
+
app_client_id=APP_CLIENT_ID,
|
|
165
|
+
scopes=["calendar.readonly"],
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def test_passes_scopes_when_configured(self, tool, mock_cred):
|
|
169
|
+
tool._scopes = ["calendar.readonly", "calendar.events"]
|
|
170
|
+
mock_cred.delegate.return_value = make_delegation_result(
|
|
171
|
+
scopes=["calendar.readonly", "calendar.events"],
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
tool._run()
|
|
175
|
+
|
|
176
|
+
call_args = mock_cred.delegate.call_args
|
|
177
|
+
assert call_args.kwargs["scopes"] == ["calendar.readonly", "calendar.events"]
|
|
178
|
+
|
|
179
|
+
def test_passes_none_scopes_when_empty(self, mock_cred):
|
|
180
|
+
t = CredTool(
|
|
181
|
+
agent_token=TOKEN,
|
|
182
|
+
user_id=USER_ID,
|
|
183
|
+
service="google",
|
|
184
|
+
app_client_id=APP_CLIENT_ID,
|
|
185
|
+
)
|
|
186
|
+
t._cred = mock_cred
|
|
187
|
+
t._scopes = []
|
|
188
|
+
|
|
189
|
+
mock_cred.delegate.return_value = make_delegation_result()
|
|
190
|
+
|
|
191
|
+
t._run()
|
|
192
|
+
|
|
193
|
+
call_args = mock_cred.delegate.call_args
|
|
194
|
+
assert call_args.kwargs["scopes"] is None
|
|
195
|
+
|
|
196
|
+
def test_propagates_consent_required_error(self, tool, mock_cred):
|
|
197
|
+
mock_cred.delegate.side_effect = ConsentRequiredError(
|
|
198
|
+
"User has not consented",
|
|
199
|
+
"https://api.cred.ninja/api/connect/google/authorize?app_client_id=app_1",
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
with pytest.raises(ConsentRequiredError) as exc_info:
|
|
203
|
+
tool._run()
|
|
204
|
+
|
|
205
|
+
assert "/api/connect/google/authorize" in exc_info.value.consent_url
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ── langchain compatibility ───────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
class TestLangChainCompatibility:
|
|
211
|
+
def test_has_args_schema(self, tool):
|
|
212
|
+
assert hasattr(tool, "args_schema")
|
|
213
|
+
schema = tool.args_schema.model_json_schema()
|
|
214
|
+
# Placeholder schema should be minimal
|
|
215
|
+
assert isinstance(schema, dict)
|
|
216
|
+
|
|
217
|
+
def test_is_base_tool(self, tool):
|
|
218
|
+
from langchain_core.tools import BaseTool
|
|
219
|
+
assert isinstance(tool, BaseTool)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class TestIntegrationFlow:
|
|
223
|
+
def test_real_cred_client_delegates_against_mock_server(self):
|
|
224
|
+
tool = CredTool(
|
|
225
|
+
agent_token=TOKEN,
|
|
226
|
+
user_id=USER_ID,
|
|
227
|
+
service="google",
|
|
228
|
+
app_client_id=APP_CLIENT_ID,
|
|
229
|
+
scopes=["calendar.readonly"],
|
|
230
|
+
base_url="https://cred.example.com",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
with patch("httpx.Client.request", return_value=httpx.Response(
|
|
234
|
+
200,
|
|
235
|
+
json={
|
|
236
|
+
"access_token": "ya29.real",
|
|
237
|
+
"token_type": "Bearer",
|
|
238
|
+
"expires_in": 3600,
|
|
239
|
+
"service": "google",
|
|
240
|
+
"scopes": ["calendar.readonly"],
|
|
241
|
+
"delegation_id": "del_real",
|
|
242
|
+
},
|
|
243
|
+
)) as request_mock:
|
|
244
|
+
token = tool._run()
|
|
245
|
+
|
|
246
|
+
assert token == "ya29.real"
|
|
247
|
+
assert request_mock.called
|