cred-semantic-kernel 1.0.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,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: cred-semantic-kernel
3
+ Version: 1.0.0
4
+ Summary: Cred Semantic Kernel integration — credential delegation plugin for Microsoft Semantic Kernel agents
5
+ Author-email: Cred <hello@cred.ninja>
6
+ License: Apache-2.0
7
+ Keywords: ai-agents,cred,delegation,microsoft,oauth,semantic-kernel
8
+ Requires-Python: >=3.9
9
+ Requires-Dist: cred-auth>=0.1.0
10
+ Requires-Dist: semantic-kernel>=1.0
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-semantic-kernel
18
+
19
+ Microsoft Semantic Kernel integration for Cred. OAuth2 credential delegation for AI agents.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install cred-semantic-kernel
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ```python
30
+ import os
31
+ import semantic_kernel as sk
32
+ from cred_semantic_kernel import CredPlugin
33
+
34
+ kernel = sk.Kernel()
35
+
36
+ plugin = CredPlugin(
37
+ agent_token=os.environ["CRED_AGENT_TOKEN"],
38
+ user_id="user_123",
39
+ app_client_id="my_app_client_id",
40
+ )
41
+
42
+ kernel.add_plugin(plugin, plugin_name="cred")
43
+ ```
44
+
45
+ ## Plugin Functions
46
+
47
+ The `cred` plugin exposes one kernel function:
48
+
49
+ ### `delegate`
50
+
51
+ | Parameter | Type | Description |
52
+ |-----------|------|-------------|
53
+ | `service` | `string` | Service slug (e.g. `google`, `github`) |
54
+ | `scopes` | `string` | Comma-separated OAuth scopes to request |
55
+
56
+ `user_id` and `app_client_id` are pre-configured at construction time, not agent-controlled.
57
+
58
+ Returns a JSON string with `access_token`, `token_type`, `expires_in`, `service`, `scopes`, and `delegation_id`.
59
+
60
+ ## Handling Consent
61
+
62
+ When the user hasn't connected the service, the `delegate` function raises `ConsentRequiredError`.
63
+ The error's `consent_url` attribute contains the URL to redirect the user.
64
+
65
+ ```python
66
+ from cred import ConsentRequiredError
67
+
68
+ try:
69
+ result = plugin.delegate(service="google", scopes="calendar.readonly")
70
+ except ConsentRequiredError as e:
71
+ print(f"Redirect user to: {e.consent_url}")
72
+ ```
73
+
74
+ ## Cred Cloud (Coming Soon)
75
+
76
+ Managed cloud delegation is coming. [Join the waitlist](https://cred.ninja/waitlist).
@@ -0,0 +1,60 @@
1
+ # cred-semantic-kernel
2
+
3
+ Microsoft Semantic Kernel integration for Cred. OAuth2 credential delegation for AI agents.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install cred-semantic-kernel
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ import os
15
+ import semantic_kernel as sk
16
+ from cred_semantic_kernel import CredPlugin
17
+
18
+ kernel = sk.Kernel()
19
+
20
+ plugin = CredPlugin(
21
+ agent_token=os.environ["CRED_AGENT_TOKEN"],
22
+ user_id="user_123",
23
+ app_client_id="my_app_client_id",
24
+ )
25
+
26
+ kernel.add_plugin(plugin, plugin_name="cred")
27
+ ```
28
+
29
+ ## Plugin Functions
30
+
31
+ The `cred` plugin exposes one kernel function:
32
+
33
+ ### `delegate`
34
+
35
+ | Parameter | Type | Description |
36
+ |-----------|------|-------------|
37
+ | `service` | `string` | Service slug (e.g. `google`, `github`) |
38
+ | `scopes` | `string` | Comma-separated OAuth scopes to request |
39
+
40
+ `user_id` and `app_client_id` are pre-configured at construction time, not agent-controlled.
41
+
42
+ Returns a JSON string with `access_token`, `token_type`, `expires_in`, `service`, `scopes`, and `delegation_id`.
43
+
44
+ ## Handling Consent
45
+
46
+ When the user hasn't connected the service, the `delegate` function raises `ConsentRequiredError`.
47
+ The error's `consent_url` attribute contains the URL to redirect the user.
48
+
49
+ ```python
50
+ from cred import ConsentRequiredError
51
+
52
+ try:
53
+ result = plugin.delegate(service="google", scopes="calendar.readonly")
54
+ except ConsentRequiredError as e:
55
+ print(f"Redirect user to: {e.consent_url}")
56
+ ```
57
+
58
+ ## Cred Cloud (Coming Soon)
59
+
60
+ Managed cloud delegation is coming. [Join the waitlist](https://cred.ninja/waitlist).
@@ -0,0 +1,5 @@
1
+ """Cred Semantic Kernel integration."""
2
+
3
+ from .plugin import CredPlugin
4
+
5
+ __all__ = ["CredPlugin"]
@@ -0,0 +1,88 @@
1
+ """Cred Semantic Kernel Plugin.
2
+
3
+ Provides a kernel function that delegates OAuth credentials for AI agents.
4
+ The plugin is pre-configured with agent identity and app context at construction.
5
+ At runtime, the agent provides ``service`` and ``scopes`` as function parameters.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Annotated, Optional
12
+
13
+ from semantic_kernel.functions import kernel_function
14
+
15
+ from cred import Cred
16
+
17
+
18
+ class CredPlugin:
19
+ """Semantic Kernel plugin for Cred credential delegation.
20
+
21
+ Pre-configured at construction with the agent token, user ID, and
22
+ app client ID. Exposes a single ``delegate`` kernel function.
23
+
24
+ Example::
25
+
26
+ from cred_semantic_kernel import CredPlugin
27
+ import semantic_kernel as sk
28
+
29
+ kernel = sk.Kernel()
30
+ plugin = CredPlugin(
31
+ agent_token=os.environ["CRED_AGENT_TOKEN"],
32
+ user_id="user_123",
33
+ app_client_id="my_app",
34
+ )
35
+ kernel.add_plugin(plugin, plugin_name="cred")
36
+
37
+ Raises:
38
+ ConsentRequiredError: (from delegate at runtime) when the user hasn't
39
+ connected the requested service. The error message includes
40
+ ``consent_url`` so the caller can redirect the user.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ agent_token: str,
46
+ user_id: str,
47
+ app_client_id: str,
48
+ base_url: str = "https://api.cred.ninja",
49
+ ) -> None:
50
+ self._cred = Cred(agent_token=agent_token, base_url=base_url)
51
+ self._user_id = user_id
52
+ self._app_client_id = app_client_id
53
+
54
+ @kernel_function(
55
+ name="delegate",
56
+ description=(
57
+ "Get a delegated OAuth access token for a third-party service "
58
+ "on behalf of the current user. "
59
+ "Returns JSON with access_token, token_type, expires_in, service, scopes, "
60
+ "and delegation_id. "
61
+ "Raises an error with consent_url if the user hasn't connected the service."
62
+ ),
63
+ )
64
+ def delegate(
65
+ self,
66
+ service: Annotated[str, "Service slug to get a token for (e.g. 'google', 'github', 'google-calendar')."],
67
+ scopes: Annotated[str, "Comma-separated OAuth scopes to request (e.g. 'calendar.readonly,calendar.events'). Pass empty string to use all consented scopes."],
68
+ ) -> str:
69
+ """Delegate credentials and return JSON result."""
70
+ scope_list: Optional[list[str]] = None
71
+ if scopes:
72
+ scope_list = [s.strip() for s in scopes.split(",") if s.strip()]
73
+
74
+ result = self._cred.delegate(
75
+ service=service,
76
+ user_id=self._user_id,
77
+ app_client_id=self._app_client_id,
78
+ scopes=scope_list if scope_list else None,
79
+ )
80
+
81
+ return json.dumps({
82
+ "access_token": result.access_token,
83
+ "token_type": result.token_type,
84
+ "expires_in": result.expires_in,
85
+ "service": result.service,
86
+ "scopes": result.scopes,
87
+ "delegation_id": result.delegation_id,
88
+ })
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cred-semantic-kernel"
7
+ version = "1.0.0"
8
+ description = "Cred Semantic Kernel integration — credential delegation plugin for Microsoft Semantic Kernel 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", "semantic-kernel", "microsoft", "oauth", "delegation", "ai-agents"]
14
+ dependencies = [
15
+ "cred-auth>=0.1.0",
16
+ "semantic-kernel>=1.0",
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_semantic_kernel"]
24
+
25
+ [tool.pytest.ini_options]
26
+ testpaths = ["tests"]
File without changes
@@ -0,0 +1,187 @@
1
+ """Cred Semantic Kernel plugin tests.
2
+
3
+ Tests focus on: plugin construction, kernel_function metadata, invocation,
4
+ output JSON, and error propagation. The Cred client is mocked -- no real HTTP.
5
+ """
6
+
7
+ import json
8
+ from datetime import datetime, timezone
9
+ from typing import Optional
10
+ import pytest
11
+ import httpx
12
+ from unittest.mock import MagicMock, patch
13
+
14
+ from cred import DelegationResult, ConsentRequiredError, CredError
15
+ from cred_semantic_kernel import CredPlugin
16
+
17
+ TOKEN = "cred_at_test"
18
+ USER_ID = "user_123"
19
+ APP_CLIENT_ID = "app_1"
20
+
21
+
22
+ def make_delegation_result(
23
+ *,
24
+ access_token: str = "at",
25
+ token_type: str = "Bearer",
26
+ expires_in: int = 3600,
27
+ service: str = "google",
28
+ scopes: Optional[list[str]] = None,
29
+ delegation_id: str = "del_1",
30
+ ) -> DelegationResult:
31
+ return DelegationResult(
32
+ access_token=access_token,
33
+ token_type=token_type,
34
+ expires_in=expires_in,
35
+ expires_at=datetime(2026, 3, 1, tzinfo=timezone.utc),
36
+ service=service,
37
+ scopes=scopes or [],
38
+ delegation_id=delegation_id,
39
+ )
40
+
41
+
42
+ @pytest.fixture
43
+ def mock_cred():
44
+ return MagicMock()
45
+
46
+
47
+ @pytest.fixture
48
+ def plugin(mock_cred):
49
+ with patch("cred_semantic_kernel.plugin.Cred", return_value=mock_cred):
50
+ return CredPlugin(
51
+ agent_token=TOKEN,
52
+ user_id=USER_ID,
53
+ app_client_id=APP_CLIENT_ID,
54
+ )
55
+
56
+
57
+ # -- constructor ---------------------------------------------------------------
58
+
59
+ class TestConstructor:
60
+ def test_creates_cred_client(self):
61
+ with patch("cred_semantic_kernel.plugin.Cred") as mock_cls:
62
+ CredPlugin(
63
+ agent_token=TOKEN,
64
+ user_id=USER_ID,
65
+ app_client_id=APP_CLIENT_ID,
66
+ base_url="http://localhost:3001",
67
+ )
68
+ mock_cls.assert_called_once_with(
69
+ agent_token=TOKEN,
70
+ base_url="http://localhost:3001",
71
+ )
72
+
73
+ def test_stores_user_id(self, plugin):
74
+ assert plugin._user_id == USER_ID
75
+
76
+ def test_stores_app_client_id(self, plugin):
77
+ assert plugin._app_client_id == APP_CLIENT_ID
78
+
79
+
80
+ # -- kernel_function metadata --------------------------------------------------
81
+
82
+ class TestKernelFunction:
83
+ def test_delegate_has_kernel_function_metadata(self, plugin):
84
+ # kernel_function decorator adds __kernel_function_metadata__
85
+ assert hasattr(plugin.delegate, "__kernel_function__")
86
+
87
+ def test_delegate_is_callable(self, plugin):
88
+ assert callable(plugin.delegate)
89
+
90
+
91
+ # -- delegate invocation -------------------------------------------------------
92
+
93
+ class TestDelegate:
94
+ def test_returns_json_with_access_token(self, plugin, mock_cred):
95
+ mock_cred.delegate.return_value = make_delegation_result(
96
+ access_token="ya29.mock",
97
+ expires_in=3600,
98
+ service="google",
99
+ scopes=["calendar.readonly"],
100
+ delegation_id="del_abc",
101
+ )
102
+
103
+ result_str = plugin.delegate(service="google", scopes="calendar.readonly")
104
+ result = json.loads(result_str)
105
+
106
+ assert result["access_token"] == "ya29.mock"
107
+ assert result["token_type"] == "Bearer"
108
+ assert result["expires_in"] == 3600
109
+ assert result["service"] == "google"
110
+ assert result["scopes"] == ["calendar.readonly"]
111
+ assert result["delegation_id"] == "del_abc"
112
+
113
+ def test_passes_service_user_app_to_cred(self, plugin, mock_cred):
114
+ mock_cred.delegate.return_value = make_delegation_result(service="github")
115
+
116
+ plugin.delegate(service="github", scopes="repo")
117
+
118
+ mock_cred.delegate.assert_called_once_with(
119
+ service="github",
120
+ user_id=USER_ID,
121
+ app_client_id=APP_CLIENT_ID,
122
+ scopes=["repo"],
123
+ )
124
+
125
+ def test_parses_comma_separated_scopes(self, plugin, mock_cred):
126
+ mock_cred.delegate.return_value = make_delegation_result(
127
+ scopes=["calendar.readonly", "calendar.events"],
128
+ )
129
+
130
+ plugin.delegate(service="google", scopes="calendar.readonly, calendar.events")
131
+
132
+ call_kwargs = mock_cred.delegate.call_args.kwargs
133
+ assert call_kwargs["scopes"] == ["calendar.readonly", "calendar.events"]
134
+
135
+ def test_passes_none_scopes_when_empty_string(self, plugin, mock_cred):
136
+ mock_cred.delegate.return_value = make_delegation_result()
137
+
138
+ plugin.delegate(service="google", scopes="")
139
+
140
+ call_kwargs = mock_cred.delegate.call_args.kwargs
141
+ assert call_kwargs["scopes"] is None
142
+
143
+ def test_propagates_consent_required_error(self, plugin, mock_cred):
144
+ mock_cred.delegate.side_effect = ConsentRequiredError(
145
+ "User has not consented",
146
+ "https://api.cred.ninja/api/connect/google/authorize?app_client_id=app_1",
147
+ )
148
+
149
+ with pytest.raises(ConsentRequiredError) as exc_info:
150
+ plugin.delegate(service="google", scopes="calendar.readonly")
151
+
152
+ assert "/api/connect/google/authorize" in exc_info.value.consent_url
153
+
154
+ def test_propagates_cred_error_on_401(self, plugin, mock_cred):
155
+ mock_cred.delegate.side_effect = CredError(
156
+ "Invalid agent token", "unauthorized", 401
157
+ )
158
+
159
+ with pytest.raises(CredError) as exc_info:
160
+ plugin.delegate(service="google", scopes="")
161
+
162
+ assert exc_info.value.status_code == 401
163
+
164
+ def test_real_cred_client_delegates_against_mock_server(self):
165
+ plugin = CredPlugin(
166
+ agent_token=TOKEN,
167
+ user_id=USER_ID,
168
+ app_client_id=APP_CLIENT_ID,
169
+ base_url="https://cred.example.com",
170
+ )
171
+
172
+ with patch("httpx.Client.request", return_value=httpx.Response(
173
+ 200,
174
+ json={
175
+ "access_token": "ya29.real",
176
+ "token_type": "Bearer",
177
+ "expires_in": 3600,
178
+ "service": "google",
179
+ "scopes": ["calendar.readonly"],
180
+ "delegation_id": "del_real",
181
+ },
182
+ )) as request_mock:
183
+ result_str = plugin.delegate(service="google", scopes="calendar.readonly")
184
+
185
+ payload = json.loads(result_str)
186
+ assert payload["access_token"] == "ya29.real"
187
+ assert request_mock.called