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.
- cred_openai_agents-0.1.0/.gitignore +50 -0
- cred_openai_agents-0.1.0/PKG-INFO +64 -0
- cred_openai_agents-0.1.0/README.md +48 -0
- cred_openai_agents-0.1.0/cred_openai_agents/__init__.py +7 -0
- cred_openai_agents-0.1.0/cred_openai_agents/tool.py +125 -0
- cred_openai_agents-0.1.0/pyproject.toml +26 -0
- cred_openai_agents-0.1.0/tests/__init__.py +0 -0
- cred_openai_agents-0.1.0/tests/test_tool.py +221 -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,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,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
|