cred-openai-agents 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,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: cred-openai-agents
3
+ Version: 0.1.0
4
+ Summary: Cred OpenAI Agents SDK integration — credential delegation FunctionTool
5
+ Author-email: Cred <hello@cred.ninja>
6
+ License: Apache-2.0
7
+ Keywords: agents,ai-agents,cred,delegation,oauth,openai
8
+ Requires-Python: >=3.9
9
+ Requires-Dist: cred-auth>=0.1.0
10
+ Requires-Dist: openai-agents>=0.1
11
+ Provides-Extra: dev
12
+ Requires-Dist: hatchling; extra == 'dev'
13
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
14
+ Requires-Dist: pytest>=7.0; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # cred-openai-agents
18
+
19
+ OpenAI Agents SDK integration for Cred. OAuth2 credential delegation for AI agents.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install cred-openai-agents
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ```python
30
+ import os
31
+ from cred_openai_agents import cred_delegate_tool
32
+ from agents import Agent
33
+
34
+ tool = cred_delegate_tool(
35
+ agent_token=os.environ["CRED_AGENT_TOKEN"],
36
+ user_id="user_123",
37
+ app_client_id="my_app_client_id",
38
+ )
39
+
40
+ agent = Agent(
41
+ name="assistant",
42
+ tools=[tool],
43
+ )
44
+ ```
45
+
46
+ ## Tool Schema
47
+
48
+ The `cred_delegate` tool matches the Cred MCP tool spec:
49
+
50
+ | Parameter | Type | Description |
51
+ |-----------|------|-------------|
52
+ | `service` | `string` | Service slug (e.g. `google`, `github`) |
53
+ | `scopes` | `string[]` | OAuth scopes to request |
54
+
55
+ `user_id` and `app_client_id` are pre-configured at factory time, not agent-controlled.
56
+
57
+ ## Handling Consent
58
+
59
+ When the user hasn't connected the service, the tool raises `ConsentRequiredError`.
60
+ The error's `consent_url` attribute contains the URL to redirect the user.
61
+
62
+ ## Cred Cloud (Coming Soon)
63
+
64
+ Managed cloud delegation is coming. [Join the waitlist](https://cred.ninja/waitlist).
@@ -0,0 +1,48 @@
1
+ # cred-openai-agents
2
+
3
+ OpenAI Agents SDK integration for Cred. OAuth2 credential delegation for AI agents.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install cred-openai-agents
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ import os
15
+ from cred_openai_agents import cred_delegate_tool
16
+ from agents import Agent
17
+
18
+ tool = cred_delegate_tool(
19
+ agent_token=os.environ["CRED_AGENT_TOKEN"],
20
+ user_id="user_123",
21
+ app_client_id="my_app_client_id",
22
+ )
23
+
24
+ agent = Agent(
25
+ name="assistant",
26
+ tools=[tool],
27
+ )
28
+ ```
29
+
30
+ ## Tool Schema
31
+
32
+ The `cred_delegate` tool matches the Cred MCP tool spec:
33
+
34
+ | Parameter | Type | Description |
35
+ |-----------|------|-------------|
36
+ | `service` | `string` | Service slug (e.g. `google`, `github`) |
37
+ | `scopes` | `string[]` | OAuth scopes to request |
38
+
39
+ `user_id` and `app_client_id` are pre-configured at factory time, not agent-controlled.
40
+
41
+ ## Handling Consent
42
+
43
+ When the user hasn't connected the service, the tool raises `ConsentRequiredError`.
44
+ The error's `consent_url` attribute contains the URL to redirect the user.
45
+
46
+ ## Cred Cloud (Coming Soon)
47
+
48
+ Managed cloud delegation is coming. [Join the waitlist](https://cred.ninja/waitlist).
@@ -0,0 +1,7 @@
1
+ """Cred OpenAI Agents SDK integration."""
2
+
3
+ from .tool import cred_delegate_tool
4
+
5
+ CredTool = cred_delegate_tool
6
+
7
+ __all__ = ["cred_delegate_tool", "CredTool"]
@@ -0,0 +1,125 @@
1
+ """Cred OpenAI Agents SDK integration.
2
+
3
+ Returns a FunctionTool that agents can call to get delegated OAuth tokens.
4
+ The tool schema matches the Cred MCP tool spec (Move 5):
5
+
6
+ name: cred_delegate
7
+ params: { service: str, scopes: list[str] | None }
8
+
9
+ user_id and app_client_id are pre-configured at factory time — they come from
10
+ the authenticated user context and are not agent-controlled inputs.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import json
17
+ from typing import Any, Optional
18
+
19
+ from agents import FunctionTool, RunContextWrapper
20
+
21
+ from cred import Cred
22
+ from cred.exceptions import ConsentRequiredError
23
+
24
+ # JSON schema for the tool's input parameters
25
+ _PARAMS_SCHEMA: dict[str, Any] = {
26
+ "type": "object",
27
+ "properties": {
28
+ "service": {
29
+ "type": "string",
30
+ "description": (
31
+ "Service slug to get a token for "
32
+ "(e.g. 'google', 'github', 'google-calendar')."
33
+ ),
34
+ },
35
+ "scopes": {
36
+ "type": "array",
37
+ "items": {"type": "string"},
38
+ "description": (
39
+ "OAuth scopes to request (e.g. ['calendar.readonly']). "
40
+ "Pass an empty list to use all consented scopes."
41
+ ),
42
+ },
43
+ },
44
+ "required": ["service", "scopes"],
45
+ "additionalProperties": False,
46
+ }
47
+
48
+
49
+ def cred_delegate_tool(
50
+ agent_token: str,
51
+ user_id: str,
52
+ app_client_id: str,
53
+ base_url: str = "https://api.cred.ninja",
54
+ ) -> FunctionTool:
55
+ """Create an OpenAI Agents SDK FunctionTool for credential delegation.
56
+
57
+ The returned tool is pre-configured with the user's identity and app context.
58
+ At runtime, the agent supplies ``service`` and ``scopes``.
59
+
60
+ Args:
61
+ agent_token: Agent token issued by Cred (starts with ``cred_at_``).
62
+ user_id: The user whose credentials to delegate.
63
+ app_client_id: The Cred app client ID to delegate for.
64
+ base_url: Override the API base URL.
65
+
66
+ Returns:
67
+ A ``FunctionTool`` ready to pass to ``Agent(tools=[...])``.
68
+
69
+ Raises:
70
+ ConsentRequiredError: (from the tool at runtime) when the user hasn't
71
+ connected the requested service. The error message includes
72
+ ``consent_url`` so the caller can redirect the user.
73
+
74
+ Example::
75
+
76
+ from cred_openai_agents import cred_delegate_tool
77
+ from agents import Agent
78
+
79
+ tool = cred_delegate_tool(
80
+ agent_token=os.environ["CRED_AGENT_TOKEN"],
81
+ user_id="user_123",
82
+ app_client_id="my_app",
83
+ )
84
+ agent = Agent(name="assistant", tools=[tool])
85
+ """
86
+ cred = Cred(agent_token=agent_token, base_url=base_url)
87
+
88
+ async def on_invoke_tool(
89
+ ctx: RunContextWrapper[Any],
90
+ args_json: str,
91
+ ) -> str:
92
+ args: dict[str, Any] = json.loads(args_json)
93
+ service: str = args["service"]
94
+ scopes: Optional[list[str]] = args.get("scopes") or None
95
+
96
+ # Cred SDK is sync — run in thread pool to avoid blocking event loop
97
+ result = await asyncio.to_thread(
98
+ cred.delegate,
99
+ service=service,
100
+ user_id=user_id,
101
+ app_client_id=app_client_id,
102
+ scopes=scopes,
103
+ )
104
+
105
+ return json.dumps({
106
+ "access_token": result.access_token,
107
+ "token_type": result.token_type,
108
+ "expires_in": result.expires_in,
109
+ "service": result.service,
110
+ "scopes": result.scopes,
111
+ "delegation_id": result.delegation_id,
112
+ })
113
+
114
+ return FunctionTool(
115
+ name="cred_delegate",
116
+ description=(
117
+ "Get a delegated OAuth access token for a third-party service "
118
+ "on behalf of the current user. "
119
+ "Returns access_token, token_type, expires_in, service, and scopes. "
120
+ "Raises an error with consent_url if the user hasn't connected the service."
121
+ ),
122
+ params_json_schema=_PARAMS_SCHEMA,
123
+ on_invoke_tool=on_invoke_tool,
124
+ strict_json_schema=False, # allow scopes to be omitted at agent discretion
125
+ )
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cred-openai-agents"
7
+ version = "0.1.0"
8
+ description = "Cred OpenAI Agents SDK integration — credential delegation FunctionTool"
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", "openai", "agents", "oauth", "delegation", "ai-agents"]
14
+ dependencies = [
15
+ "cred-auth>=0.1.0",
16
+ "openai-agents>=0.1",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ dev = ["pytest>=7.0", "pytest-asyncio>=0.21", "hatchling"]
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["cred_openai_agents"]
24
+
25
+ [tool.pytest.ini_options]
26
+ testpaths = ["tests"]
File without changes
@@ -0,0 +1,221 @@
1
+ """Cred OpenAI Agents SDK integration tests.
2
+
3
+ Tests focus on: FunctionTool shape, async invocation, output JSON,
4
+ and error propagation. The Cred client is mocked — no real HTTP.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ from datetime import datetime, timezone
10
+ from typing import Optional
11
+ import pytest
12
+ import httpx
13
+ from unittest.mock import MagicMock, patch
14
+
15
+ from agents import FunctionTool
16
+
17
+ from cred import DelegationResult, ConsentRequiredError, CredError
18
+ from cred_openai_agents import cred_delegate_tool, 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
+ with patch("cred_openai_agents.tool.Cred", return_value=mock_cred):
53
+ return cred_delegate_tool(
54
+ agent_token=TOKEN,
55
+ user_id=USER_ID,
56
+ app_client_id=APP_CLIENT_ID,
57
+ )
58
+
59
+
60
+ def run(coro):
61
+ """Run a coroutine synchronously (test helper)."""
62
+ return asyncio.run(coro)
63
+
64
+
65
+ # ── factory function ──────────────────────────────────────────────────────────
66
+
67
+ class TestFactory:
68
+ def test_returns_function_tool(self):
69
+ with patch("cred_openai_agents.tool.Cred"):
70
+ t = cred_delegate_tool(
71
+ agent_token=TOKEN,
72
+ user_id=USER_ID,
73
+ app_client_id=APP_CLIENT_ID,
74
+ )
75
+ assert isinstance(t, FunctionTool)
76
+
77
+ def test_tool_name_is_cred_delegate(self, tool):
78
+ assert tool.name == "cred_delegate"
79
+
80
+ def test_tool_description_mentions_oauth(self, tool):
81
+ assert "access token" in tool.description.lower()
82
+ assert "service" in tool.description.lower()
83
+
84
+ def test_params_schema_has_service_and_scopes(self, tool):
85
+ props = tool.params_json_schema["properties"]
86
+ assert "service" in props
87
+ assert "scopes" in props
88
+
89
+ def test_service_is_required(self, tool):
90
+ assert "service" in tool.params_json_schema.get("required", [])
91
+
92
+ def test_creates_cred_client_with_token_and_base_url(self):
93
+ with patch("cred_openai_agents.tool.Cred") as mock_cred_class:
94
+ cred_delegate_tool(
95
+ agent_token=TOKEN,
96
+ user_id=USER_ID,
97
+ app_client_id=APP_CLIENT_ID,
98
+ base_url="http://localhost:3001",
99
+ )
100
+ mock_cred_class.assert_called_once_with(
101
+ agent_token=TOKEN,
102
+ base_url="http://localhost:3001",
103
+ )
104
+
105
+ def test_exports_cred_tool_alias(self):
106
+ assert CredTool is cred_delegate_tool
107
+
108
+
109
+ # ── on_invoke_tool ────────────────────────────────────────────────────────────
110
+
111
+ class TestInvoke:
112
+ def test_returns_json_with_access_token(self, tool, mock_cred):
113
+ mock_cred.delegate.return_value = make_delegation_result(
114
+ access_token="ya29.mock",
115
+ expires_in=3600,
116
+ service="google",
117
+ scopes=["calendar.readonly"],
118
+ delegation_id="del_abc",
119
+ )
120
+
121
+ args_json = json.dumps({"service": "google", "scopes": ["calendar.readonly"]})
122
+ result_str = run(tool.on_invoke_tool(MagicMock(), args_json))
123
+ result = json.loads(result_str)
124
+
125
+ assert result["access_token"] == "ya29.mock"
126
+ assert result["token_type"] == "Bearer"
127
+ assert result["expires_in"] == 3600
128
+ assert result["service"] == "google"
129
+ assert result["scopes"] == ["calendar.readonly"]
130
+ assert result["delegation_id"] == "del_abc"
131
+
132
+ def test_passes_service_user_app_to_cred(self, tool, mock_cred):
133
+ mock_cred.delegate.return_value = make_delegation_result(service="github")
134
+
135
+ args_json = json.dumps({"service": "github", "scopes": ["repo"]})
136
+ run(tool.on_invoke_tool(MagicMock(), args_json))
137
+
138
+ mock_cred.delegate.assert_called_once_with(
139
+ service="github",
140
+ user_id=USER_ID,
141
+ app_client_id=APP_CLIENT_ID,
142
+ scopes=["repo"],
143
+ )
144
+
145
+ def test_passes_none_scopes_when_empty_list(self, tool, mock_cred):
146
+ mock_cred.delegate.return_value = make_delegation_result()
147
+
148
+ args_json = json.dumps({"service": "google", "scopes": []})
149
+ run(tool.on_invoke_tool(MagicMock(), args_json))
150
+
151
+ call_kwargs = mock_cred.delegate.call_args.kwargs
152
+ assert call_kwargs["scopes"] is None
153
+
154
+ def test_propagates_consent_required_error(self, tool, mock_cred):
155
+ mock_cred.delegate.side_effect = ConsentRequiredError(
156
+ "User has not consented",
157
+ "https://api.cred.ninja/api/connect/google/authorize?app_client_id=app_1",
158
+ )
159
+
160
+ args_json = json.dumps({"service": "google", "scopes": ["calendar.readonly"]})
161
+
162
+ with pytest.raises(ConsentRequiredError) as exc_info:
163
+ run(tool.on_invoke_tool(MagicMock(), args_json))
164
+
165
+ assert "/api/connect/google/authorize" in exc_info.value.consent_url
166
+
167
+ def test_propagates_cred_error_on_401(self, tool, mock_cred):
168
+ mock_cred.delegate.side_effect = CredError(
169
+ "Invalid agent token", "unauthorized", 401
170
+ )
171
+
172
+ args_json = json.dumps({"service": "google", "scopes": []})
173
+
174
+ with pytest.raises(CredError) as exc_info:
175
+ run(tool.on_invoke_tool(MagicMock(), args_json))
176
+
177
+ assert exc_info.value.status_code == 401
178
+
179
+ def test_runs_sync_cred_in_thread(self, tool, mock_cred):
180
+ """Verifies the handler doesn't block the event loop (delegate is sync)."""
181
+ call_count = 0
182
+
183
+ def sync_delegate(**kwargs):
184
+ nonlocal call_count
185
+ call_count += 1
186
+ return make_delegation_result()
187
+
188
+ mock_cred.delegate.side_effect = sync_delegate
189
+
190
+ args_json = json.dumps({"service": "google", "scopes": []})
191
+ run(tool.on_invoke_tool(MagicMock(), args_json))
192
+
193
+ assert call_count == 1
194
+
195
+ def test_real_cred_client_delegates_against_mock_server(self):
196
+ tool = cred_delegate_tool(
197
+ agent_token=TOKEN,
198
+ user_id=USER_ID,
199
+ app_client_id=APP_CLIENT_ID,
200
+ base_url="https://cred.example.com",
201
+ )
202
+
203
+ with patch("httpx.Client.request", return_value=httpx.Response(
204
+ 200,
205
+ json={
206
+ "access_token": "ya29.real",
207
+ "token_type": "Bearer",
208
+ "expires_in": 3600,
209
+ "service": "google",
210
+ "scopes": ["calendar.readonly"],
211
+ "delegation_id": "del_real",
212
+ },
213
+ )) as request_mock:
214
+ result_str = run(tool.on_invoke_tool(MagicMock(), json.dumps({
215
+ "service": "google",
216
+ "scopes": ["calendar.readonly"],
217
+ })))
218
+
219
+ payload = json.loads(result_str)
220
+ assert payload["access_token"] == "ya29.real"
221
+ assert request_mock.called