cred-langchain 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.
@@ -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,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: cred-langchain
3
+ Version: 0.1.0
4
+ Summary: Cred LangChain integration — credential delegation tools for LangChain agents
5
+ Author-email: Cred <hello@cred.ninja>
6
+ License: Apache-2.0
7
+ Keywords: ai-agents,cred,delegation,langchain,oauth
8
+ Requires-Python: >=3.9
9
+ Requires-Dist: cred-auth>=0.1.0
10
+ Requires-Dist: langchain-core>=0.2
11
+ Provides-Extra: dev
12
+ Requires-Dist: hatchling; extra == 'dev'
13
+ Requires-Dist: pytest>=7.0; extra == 'dev'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # cred-langchain
17
+
18
+ LangChain integration for Cred. OAuth2 credential delegation for AI agents.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install cred-langchain
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```python
29
+ import os
30
+ from cred_langchain import CredToolkit
31
+ from cred import ConsentRequiredError
32
+
33
+ toolkit = CredToolkit(
34
+ agent_token=os.environ["CRED_AGENT_TOKEN"],
35
+ user_id="user_123",
36
+ )
37
+ tools = toolkit.get_tools()
38
+ # tools = [CredDelegateTool, CredStatusTool, CredRevokeTool]
39
+
40
+ # Use with any LangChain agent
41
+ from langchain.agents import AgentExecutor
42
+ agent = AgentExecutor(agent=..., tools=tools)
43
+ ```
44
+
45
+ ## Tools
46
+
47
+ | Tool | Name | Description |
48
+ |------|------|-------------|
49
+ | `CredDelegateTool` | `cred_delegate` | Get an OAuth access token for a service |
50
+ | `CredStatusTool` | `cred_status` | List user's connected services |
51
+ | `CredRevokeTool` | `cred_revoke` | Revoke a service connection |
52
+
53
+ ## Handling Consent
54
+
55
+ When a user hasn't connected a service yet, `CredDelegateTool` raises `ConsentRequiredError`.
56
+ Catch it and redirect the user to `e.consent_url` to complete the OAuth flow.
57
+
58
+ ## Cred Cloud (Coming Soon)
59
+
60
+ Managed cloud delegation is coming. [Join the waitlist](https://cred.ninja/waitlist).
@@ -0,0 +1,45 @@
1
+ # cred-langchain
2
+
3
+ LangChain integration for Cred. OAuth2 credential delegation for AI agents.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install cred-langchain
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ import os
15
+ from cred_langchain import CredToolkit
16
+ from cred import ConsentRequiredError
17
+
18
+ toolkit = CredToolkit(
19
+ agent_token=os.environ["CRED_AGENT_TOKEN"],
20
+ user_id="user_123",
21
+ )
22
+ tools = toolkit.get_tools()
23
+ # tools = [CredDelegateTool, CredStatusTool, CredRevokeTool]
24
+
25
+ # Use with any LangChain agent
26
+ from langchain.agents import AgentExecutor
27
+ agent = AgentExecutor(agent=..., tools=tools)
28
+ ```
29
+
30
+ ## Tools
31
+
32
+ | Tool | Name | Description |
33
+ |------|------|-------------|
34
+ | `CredDelegateTool` | `cred_delegate` | Get an OAuth access token for a service |
35
+ | `CredStatusTool` | `cred_status` | List user's connected services |
36
+ | `CredRevokeTool` | `cred_revoke` | Revoke a service connection |
37
+
38
+ ## Handling Consent
39
+
40
+ When a user hasn't connected a service yet, `CredDelegateTool` raises `ConsentRequiredError`.
41
+ Catch it and redirect the user to `e.consent_url` to complete the OAuth flow.
42
+
43
+ ## Cred Cloud (Coming Soon)
44
+
45
+ Managed cloud delegation is coming. [Join the waitlist](https://cred.ninja/waitlist).
@@ -0,0 +1,32 @@
1
+ """Cred LangChain integration."""
2
+
3
+ from pydantic import SecretStr
4
+
5
+ from cred import Cred
6
+ from .toolkit import CredToolkit
7
+ from .tools import CredDelegateTool, CredStatusTool, CredRevokeTool
8
+
9
+
10
+ def secret_from_cred(
11
+ service: str,
12
+ user_id: str,
13
+ cred: Cred,
14
+ scopes: list[str] | None = None,
15
+ app_client_id: str | None = None,
16
+ ) -> SecretStr:
17
+ """Drop-in replacement for secret_from_env() backed by Cred delegation."""
18
+ result = cred.delegate(
19
+ service=service,
20
+ user_id=user_id,
21
+ app_client_id=app_client_id,
22
+ scopes=scopes,
23
+ )
24
+ return SecretStr(result.access_token)
25
+
26
+ __all__ = [
27
+ "CredToolkit",
28
+ "CredDelegateTool",
29
+ "CredStatusTool",
30
+ "CredRevokeTool",
31
+ "secret_from_cred",
32
+ ]
@@ -0,0 +1,54 @@
1
+ """Cred LangChain Toolkit — bundles all Cred tools for use with LangChain agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ from langchain_core.tools import BaseTool
8
+
9
+ from cred import Cred
10
+ from .tools import CredDelegateTool, CredStatusTool, CredRevokeTool
11
+
12
+
13
+ class CredToolkit:
14
+ """LangChain toolkit that provides Cred credential delegation tools.
15
+
16
+ Instantiate once per agent invocation with the target user's ID.
17
+ All tools share the same Cred client and user context.
18
+
19
+ Example::
20
+
21
+ from cred_langchain import CredToolkit
22
+
23
+ toolkit = CredToolkit(
24
+ agent_token=os.environ["CRED_AGENT_TOKEN"],
25
+ user_id="user_123",
26
+ )
27
+ tools = toolkit.get_tools()
28
+ # Pass tools to your LangChain AgentExecutor
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ agent_token: str,
34
+ user_id: str,
35
+ base_url: str = "https://api.cred.ninja",
36
+ ) -> None:
37
+ self._cred = Cred(agent_token=agent_token, base_url=base_url)
38
+ self._user_id = user_id
39
+
40
+ def get_tools(self) -> list[BaseTool]:
41
+ """Return all Cred tools pre-configured with this toolkit's user context."""
42
+ delegate = CredDelegateTool()
43
+ delegate._cred = self._cred
44
+ delegate._user_id = self._user_id
45
+
46
+ status = CredStatusTool()
47
+ status._cred = self._cred
48
+ status._user_id = self._user_id
49
+
50
+ revoke = CredRevokeTool()
51
+ revoke._cred = self._cred
52
+ revoke._user_id = self._user_id
53
+
54
+ return [delegate, status, revoke]
@@ -0,0 +1,150 @@
1
+ """Cred LangChain Tools — individual BaseTool implementations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from typing import TYPE_CHECKING, Any, Optional, Type
8
+
9
+ from langchain_core.tools import BaseTool
10
+ from pydantic import BaseModel, Field
11
+
12
+ if TYPE_CHECKING:
13
+ from cred import Cred
14
+
15
+
16
+ # ── Input schemas ─────────────────────────────────────────────────────────────
17
+
18
+ class DelegateInput(BaseModel):
19
+ service: str = Field(description="Service slug to get a token for (e.g. 'google', 'github')")
20
+ app_client_id: str = Field(description="The Cred app client ID to delegate for")
21
+ scopes: list[str] = Field(
22
+ default_factory=list,
23
+ description="OAuth scopes to request (e.g. ['calendar.readonly']). "
24
+ "Leave empty to use all consented scopes.",
25
+ )
26
+
27
+
28
+ class RevokeInput(BaseModel):
29
+ service: str = Field(description="Service slug to revoke (e.g. 'google', 'github')")
30
+ app_client_id: Optional[str] = Field(
31
+ default=None,
32
+ description="Optional app client ID to scope the revocation.",
33
+ )
34
+
35
+
36
+ # ── Tools ─────────────────────────────────────────────────────────────────────
37
+
38
+ class CredDelegateTool(BaseTool):
39
+ """Get an OAuth access token for a third-party service on behalf of the current user."""
40
+
41
+ name: str = "cred_delegate"
42
+ description: str = (
43
+ "Get an OAuth access token for a third-party service on behalf of the current user. "
44
+ "Returns the access token, expiry, service name, and granted scopes. "
45
+ "Raises ConsentRequiredError (with consent_url) if the user hasn't connected the service."
46
+ )
47
+ args_schema: Type[BaseModel] = DelegateInput
48
+
49
+ # Injected by CredToolkit
50
+ _cred: Any = None
51
+ _user_id: str = ""
52
+ _cache: dict[tuple[str, str, tuple[str, ...]], tuple[Any, float]] = {}
53
+
54
+ def _get_delegation_result(
55
+ self,
56
+ service: str,
57
+ app_client_id: str,
58
+ scopes: Optional[list[str]],
59
+ ) -> Any:
60
+ resolved_scopes = scopes or []
61
+ cache_key = (service, app_client_id, tuple(resolved_scopes))
62
+ cached = self._cache.get(cache_key)
63
+ now = time.monotonic()
64
+ if cached and cached[1] > now:
65
+ return cached[0]
66
+
67
+ result = self._cred.delegate(
68
+ service=service,
69
+ user_id=self._user_id,
70
+ app_client_id=app_client_id,
71
+ scopes=resolved_scopes,
72
+ )
73
+
74
+ expires_in = getattr(result, "expires_in", None)
75
+ refresh_deadline = 0.0
76
+ if isinstance(expires_in, int) and expires_in > 60:
77
+ refresh_deadline = now + expires_in - 60
78
+ self._cache[cache_key] = (result, refresh_deadline)
79
+ return result
80
+
81
+ def _run(
82
+ self,
83
+ service: str,
84
+ app_client_id: str,
85
+ scopes: Optional[list[str]] = None,
86
+ **kwargs: Any,
87
+ ) -> str:
88
+ result = self._get_delegation_result(service, app_client_id, scopes)
89
+ return json.dumps({
90
+ "access_token": result.access_token,
91
+ "token_type": result.token_type,
92
+ "expires_in": result.expires_in,
93
+ "service": result.service,
94
+ "scopes": result.scopes,
95
+ "delegation_id": result.delegation_id,
96
+ })
97
+
98
+
99
+ class CredStatusTool(BaseTool):
100
+ """Check which third-party services the current user has connected."""
101
+
102
+ name: str = "cred_status"
103
+ description: str = (
104
+ "Check which third-party services the current user has connected via OAuth. "
105
+ "Returns a list of connected services with their granted scopes."
106
+ )
107
+
108
+ # Injected by CredToolkit
109
+ _cred: Any = None
110
+ _user_id: str = ""
111
+
112
+ def _run(self, **kwargs: Any) -> str:
113
+ connections = self._cred.get_user_connections(self._user_id)
114
+ return json.dumps([
115
+ {
116
+ "slug": c.slug,
117
+ "scopes_granted": c.scopes_granted,
118
+ "app_client_id": c.app_client_id,
119
+ "consented_at": c.consented_at,
120
+ }
121
+ for c in connections
122
+ ])
123
+
124
+
125
+ class CredRevokeTool(BaseTool):
126
+ """Revoke the current user's connection to a third-party service."""
127
+
128
+ name: str = "cred_revoke"
129
+ description: str = (
130
+ "Revoke the current user's OAuth connection to a third-party service. "
131
+ "Use this when the user wants to disconnect a service."
132
+ )
133
+ args_schema: Type[BaseModel] = RevokeInput
134
+
135
+ # Injected by CredToolkit
136
+ _cred: Any = None
137
+ _user_id: str = ""
138
+
139
+ def _run(
140
+ self,
141
+ service: str,
142
+ app_client_id: Optional[str] = None,
143
+ **kwargs: Any,
144
+ ) -> str:
145
+ self._cred.revoke(
146
+ service=service,
147
+ user_id=self._user_id,
148
+ app_client_id=app_client_id,
149
+ )
150
+ return json.dumps({"revoked": True, "service": service})
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cred-langchain"
7
+ version = "0.1.0"
8
+ description = "Cred LangChain integration — credential delegation tools for LangChain 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", "langchain", "oauth", "delegation", "ai-agents"]
14
+ dependencies = [
15
+ "cred-auth>=0.1.0",
16
+ "langchain-core>=0.2",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ dev = ["pytest>=7.0", "hatchling"]
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["cred_langchain"]
24
+
25
+ [tool.pytest.ini_options]
26
+ testpaths = ["tests"]
File without changes
@@ -0,0 +1,280 @@
1
+ """Cred LangChain integration tests.
2
+
3
+ Mocks the Cred client — no real HTTP, no real API calls.
4
+ Tests focus on: tool wiring, input schema, output shape, error propagation.
5
+ """
6
+
7
+ import json
8
+ import warnings
9
+ warnings.filterwarnings("ignore", category=UserWarning)
10
+
11
+ from datetime import datetime, timezone
12
+ from typing import Optional
13
+
14
+ import pytest
15
+ import httpx
16
+ from unittest.mock import MagicMock, patch
17
+ from langchain_core.tools import BaseTool
18
+ from pydantic import SecretStr
19
+
20
+ from cred import DelegationResult, Connection, ConsentRequiredError
21
+ from cred_langchain import (
22
+ CredToolkit,
23
+ CredDelegateTool,
24
+ CredStatusTool,
25
+ CredRevokeTool,
26
+ secret_from_cred,
27
+ )
28
+
29
+
30
+ TOKEN = "cred_at_test"
31
+ USER_ID = "user_123"
32
+
33
+
34
+ def make_delegation_result(
35
+ *,
36
+ access_token: str = "at",
37
+ token_type: str = "Bearer",
38
+ expires_in: int = 3600,
39
+ service: str = "google",
40
+ scopes: Optional[list[str]] = None,
41
+ delegation_id: str = "del_1",
42
+ ) -> DelegationResult:
43
+ return DelegationResult(
44
+ access_token=access_token,
45
+ token_type=token_type,
46
+ expires_in=expires_in,
47
+ expires_at=datetime(2026, 3, 1, tzinfo=timezone.utc),
48
+ service=service,
49
+ scopes=scopes or [],
50
+ delegation_id=delegation_id,
51
+ )
52
+
53
+
54
+ @pytest.fixture
55
+ def mock_cred():
56
+ return MagicMock()
57
+
58
+
59
+ @pytest.fixture
60
+ def toolkit(mock_cred):
61
+ tk = CredToolkit(agent_token=TOKEN, user_id=USER_ID)
62
+ tk._cred = mock_cred
63
+ return tk
64
+
65
+
66
+ # ── CredToolkit ───────────────────────────────────────────────────────────────
67
+
68
+ class TestCredToolkit:
69
+ def test_get_tools_returns_three_tools(self, toolkit):
70
+ tools = toolkit.get_tools()
71
+ assert len(tools) == 3
72
+
73
+ def test_all_tools_are_base_tool_instances(self, toolkit):
74
+ tools = toolkit.get_tools()
75
+ assert all(isinstance(t, BaseTool) for t in tools)
76
+
77
+ def test_tool_names(self, toolkit):
78
+ names = {t.name for t in toolkit.get_tools()}
79
+ assert names == {"cred_delegate", "cred_status", "cred_revoke"}
80
+
81
+ def test_tools_share_user_id(self, toolkit):
82
+ tools = toolkit.get_tools()
83
+ for tool in tools:
84
+ assert tool._user_id == USER_ID
85
+
86
+ def test_tools_share_cred_client(self, toolkit, mock_cred):
87
+ tools = toolkit.get_tools()
88
+ for tool in tools:
89
+ assert tool._cred is mock_cred
90
+
91
+
92
+ # ── CredDelegateTool ──────────────────────────────────────────────────────────
93
+
94
+ class TestCredDelegateTool:
95
+ def test_returns_json_with_access_token(self, toolkit, mock_cred):
96
+ mock_cred.delegate.return_value = make_delegation_result(
97
+ access_token="ya29.mock",
98
+ expires_in=3600,
99
+ service="google",
100
+ scopes=["calendar.readonly"],
101
+ delegation_id="del_abc",
102
+ )
103
+
104
+ tools = toolkit.get_tools()
105
+ delegate = next(t for t in tools if t.name == "cred_delegate")
106
+ result = delegate._run(
107
+ service="google",
108
+ app_client_id="app_1",
109
+ scopes=["calendar.readonly"],
110
+ )
111
+
112
+ data = json.loads(result)
113
+ assert data["access_token"] == "ya29.mock"
114
+ assert data["service"] == "google"
115
+ assert data["expires_in"] == 3600
116
+ assert data["delegation_id"] == "del_abc"
117
+
118
+ def test_passes_user_id_to_cred(self, toolkit, mock_cred):
119
+ mock_cred.delegate.return_value = make_delegation_result()
120
+
121
+ tools = toolkit.get_tools()
122
+ delegate = next(t for t in tools if t.name == "cred_delegate")
123
+ delegate._run(service="google", app_client_id="app_1", scopes=[])
124
+
125
+ mock_cred.delegate.assert_called_once_with(
126
+ service="google",
127
+ user_id=USER_ID,
128
+ app_client_id="app_1",
129
+ scopes=[],
130
+ )
131
+
132
+ def test_propagates_consent_required_error(self, toolkit, mock_cred):
133
+ mock_cred.delegate.side_effect = ConsentRequiredError(
134
+ "User has not consented",
135
+ "https://api.cred.ninja/api/connect/google/authorize?app_client_id=app_1",
136
+ )
137
+
138
+ tools = toolkit.get_tools()
139
+ delegate = next(t for t in tools if t.name == "cred_delegate")
140
+
141
+ with pytest.raises(ConsentRequiredError) as exc_info:
142
+ delegate._run(service="google", app_client_id="app_1", scopes=["calendar.readonly"])
143
+
144
+ assert "/api/connect/google/authorize" in exc_info.value.consent_url
145
+
146
+ def test_has_args_schema(self, toolkit):
147
+ tools = toolkit.get_tools()
148
+ delegate = next(t for t in tools if t.name == "cred_delegate")
149
+ schema = delegate.args_schema.model_json_schema()
150
+ assert "service" in schema["properties"]
151
+ assert "scopes" in schema["properties"]
152
+
153
+ def test_real_cred_client_delegates_against_mock_server(self):
154
+ toolkit = CredToolkit(
155
+ agent_token=TOKEN,
156
+ user_id=USER_ID,
157
+ base_url="https://cred.example.com",
158
+ )
159
+ delegate = next(t for t in toolkit.get_tools() if t.name == "cred_delegate")
160
+
161
+ with patch("httpx.Client.request", return_value=httpx.Response(
162
+ 200,
163
+ json={
164
+ "access_token": "ya29.real",
165
+ "token_type": "Bearer",
166
+ "expires_in": 3600,
167
+ "service": "google",
168
+ "scopes": ["calendar.readonly"],
169
+ "delegation_id": "del_real",
170
+ },
171
+ )) as request_mock:
172
+ result = delegate._run(
173
+ service="google",
174
+ app_client_id="app_1",
175
+ scopes=["calendar.readonly"],
176
+ )
177
+
178
+ payload = json.loads(result)
179
+ assert payload["access_token"] == "ya29.real"
180
+ assert request_mock.called
181
+
182
+
183
+ # ── CredStatusTool ────────────────────────────────────────────────────────────
184
+
185
+ class TestCredStatusTool:
186
+ def test_returns_json_list_of_connections(self, toolkit, mock_cred):
187
+ mock_cred.get_user_connections.return_value = [
188
+ Connection(slug="google", scopes_granted=["calendar.readonly"],
189
+ app_client_id="app1", consented_at="2026-03-01T00:00:00Z"),
190
+ Connection(slug="github", scopes_granted=["repo"],
191
+ app_client_id="app1", consented_at="2026-03-01T00:00:00Z"),
192
+ ]
193
+
194
+ tools = toolkit.get_tools()
195
+ status = next(t for t in tools if t.name == "cred_status")
196
+ result = status._run()
197
+
198
+ data = json.loads(result)
199
+ assert len(data) == 2
200
+ assert data[0]["slug"] == "google"
201
+ assert data[1]["slug"] == "github"
202
+
203
+ def test_passes_user_id_to_cred(self, toolkit, mock_cred):
204
+ mock_cred.get_user_connections.return_value = []
205
+
206
+ tools = toolkit.get_tools()
207
+ status = next(t for t in tools if t.name == "cred_status")
208
+ status._run()
209
+
210
+ mock_cred.get_user_connections.assert_called_once_with(USER_ID)
211
+
212
+ def test_returns_empty_list_when_no_connections(self, toolkit, mock_cred):
213
+ mock_cred.get_user_connections.return_value = []
214
+ tools = toolkit.get_tools()
215
+ status = next(t for t in tools if t.name == "cred_status")
216
+ result = status._run()
217
+ assert json.loads(result) == []
218
+
219
+
220
+ # ── CredRevokeTool ────────────────────────────────────────────────────────────
221
+
222
+ class TestCredRevokeTool:
223
+ def test_returns_json_revoked_true(self, toolkit, mock_cred):
224
+ mock_cred.revoke.return_value = None
225
+
226
+ tools = toolkit.get_tools()
227
+ revoke = next(t for t in tools if t.name == "cred_revoke")
228
+ result = revoke._run(service="google")
229
+
230
+ data = json.loads(result)
231
+ assert data["revoked"] is True
232
+ assert data["service"] == "google"
233
+
234
+ def test_passes_user_id_and_service_to_cred(self, toolkit, mock_cred):
235
+ mock_cred.revoke.return_value = None
236
+
237
+ tools = toolkit.get_tools()
238
+ revoke = next(t for t in tools if t.name == "cred_revoke")
239
+ revoke._run(service="google", app_client_id="app1")
240
+
241
+ mock_cred.revoke.assert_called_once_with(
242
+ service="google",
243
+ user_id=USER_ID,
244
+ app_client_id="app1",
245
+ )
246
+
247
+ def test_has_args_schema(self, toolkit):
248
+ tools = toolkit.get_tools()
249
+ revoke = next(t for t in tools if t.name == "cred_revoke")
250
+ schema = revoke.args_schema.model_json_schema()
251
+ assert "service" in schema["properties"]
252
+
253
+
254
+ class TestSecretFromCred:
255
+ def test_returns_secret_str_from_real_cred_client(self):
256
+ from cred import Cred
257
+
258
+ cred = Cred(agent_token=TOKEN, base_url="https://cred.example.com")
259
+
260
+ with patch("httpx.Client.request", return_value=httpx.Response(
261
+ 200,
262
+ json={
263
+ "access_token": "ya29.secret",
264
+ "token_type": "Bearer",
265
+ "expires_in": 3600,
266
+ "service": "google",
267
+ "scopes": ["calendar.readonly"],
268
+ "delegation_id": "del_secret",
269
+ },
270
+ )):
271
+ secret = secret_from_cred(
272
+ "google",
273
+ USER_ID,
274
+ cred,
275
+ scopes=["calendar.readonly"],
276
+ app_client_id="app_1",
277
+ )
278
+
279
+ assert isinstance(secret, SecretStr)
280
+ assert secret.get_secret_value() == "ya29.secret"