kobil-aitrust 1.2.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,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
@@ -0,0 +1,128 @@
1
+ KOBIL Agent Trust SDK License
2
+ Version 1.0
3
+
4
+ Copyright (c) 2026 KOBIL GmbH. All rights reserved.
5
+ The Software is developed by the KOBIL AI Team.
6
+
7
+ In short: you may install and use the Software free of charge, also in
8
+ commercial products, and ship it inside your own application. You may not
9
+ change it, copy it into other software, or pass it on or sell it on its
10
+ own. The numbered terms below are the license; this summary is not.
11
+
12
+ 1. Definitions
13
+
14
+ "The Software" is the code and documentation in this package.
15
+
16
+ "You" is the person or organization that accepts these terms.
17
+
18
+ "Your company" is the organization you work for, and the organizations
19
+ that control it, that it controls, or that are under common control
20
+ with it.
21
+
22
+ "Your application" is software or a service that you build and that
23
+ does more than give access to the Software. Your application is not a
24
+ work based on the Software just because it uses the Software.
25
+
26
+ 2. Acceptance
27
+
28
+ By installing, copying or using the Software you accept these terms.
29
+ If you do not accept them, do not use the Software.
30
+
31
+ 3. What you may do
32
+
33
+ KOBIL grants you and your company a non-exclusive, worldwide,
34
+ non-transferable license, free of charge, to:
35
+
36
+ a. install and use the Software, unchanged, to build, test and run your
37
+ applications, including commercial ones that you charge for;
38
+ b. make the copies of the Software that this needs, for example on
39
+ developer machines, on build servers and in your application's
40
+ environments;
41
+ c. ship the Software, unchanged, as part of your application, for
42
+ example inside its container image, package or bundle.
43
+
44
+ Your company's employees and contractors may do the same on your
45
+ company's behalf.
46
+
47
+ Building your application with tools such as bundlers, minifiers,
48
+ transpilers or compilers is not a change to the Software. Using AI
49
+ coding tools to write applications that use the Software is allowed.
50
+
51
+ 4. Shipping the Software inside your application
52
+
53
+ When you ship the Software as part of your application:
54
+
55
+ a. the Software must be needed for your application to work, and must
56
+ not be offered as a library, SDK or tool of its own;
57
+ b. the people who receive your application get no right to use the
58
+ Software apart from your application;
59
+ c. keep this license and the copyright notice with the Software, or in
60
+ your application's third-party notices.
61
+
62
+ 5. What you may not do
63
+
64
+ You may not:
65
+
66
+ a. change, adapt or translate the Software, or make works based on it;
67
+ b. copy the Software's code, or substantial parts of it, into other
68
+ software, whether by hand or with automated or AI tools, or use the
69
+ Software to train AI models;
70
+ c. publish, share, sublicense, sell, rent or lease the Software on its
71
+ own, or as part of another SDK, library, framework or developer
72
+ tool;
73
+ d. remove or change this license or any copyright notice in the
74
+ Software;
75
+ e. use the Software to break the law, or to suggest that KOBIL endorses
76
+ your application.
77
+
78
+ These limits apply to the Software itself. You may sell and
79
+ commercially distribute your application.
80
+
81
+ 6. KOBIL services
82
+
83
+ The Software can connect to KOBIL services. Using those services is
84
+ governed by their own terms, not by this license. This license does not
85
+ give you access to any KOBIL service.
86
+
87
+ 7. Ownership
88
+
89
+ KOBIL keeps all rights in the Software that this license does not
90
+ expressly grant. This license gives you no right to KOBIL's names,
91
+ logos or trademarks, except to say truthfully that your application
92
+ uses the Software.
93
+
94
+ 8. Other software
95
+
96
+ The Software uses third-party open-source packages. Each of them is
97
+ covered by its own license, not by this one.
98
+
99
+ 9. New versions
100
+
101
+ KOBIL may release new versions of the Software under a new version of
102
+ this license. Each copy of the Software is covered by the license that
103
+ came with it.
104
+
105
+ 10. Violations
106
+
107
+ If you break these terms, KOBIL may tell you so in writing. The first
108
+ time this happens, your license continues if, within 30 days of that
109
+ notice, you fully comply again and take practical steps to correct past
110
+ violations. Otherwise, and after any later violation, your license
111
+ ends. You must then stop using and shipping the Software.
112
+
113
+ 11. No warranty
114
+
115
+ As far as the law allows, the Software comes "as is", without any
116
+ warranty or condition, express or implied, including fitness for a
117
+ particular purpose and non-infringement.
118
+
119
+ 12. Limitation of liability
120
+
121
+ As far as the law allows, KOBIL is not liable for any damages that come
122
+ from these terms or from using, or not being able to use, the Software,
123
+ under any kind of legal claim.
124
+
125
+ 13. Other permissions
126
+
127
+ For any use these terms do not allow, ask KOBIL for written permission
128
+ at www.kobil.com.
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.5
2
+ Name: kobil-aitrust
3
+ Version: 1.2.0
4
+ Summary: KOBIL AI Trust Platform — unified meta-package wrapping SDK and Audit into a simple developer-facing API
5
+ License-Expression: LicenseRef-KOBIL-Agent-Trust-SDK-1.0
6
+ License-File: LICENSE
7
+ Keywords: agent,ai,audit,ciba,fga,kobil,mfa,oauth,security,trust
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: kobil-agent-sdk<4,>=3.0
10
+ Requires-Dist: mcp>=2.0.0
11
+ Provides-Extra: all
12
+ Requires-Dist: anthropic>=0.20; extra == 'all'
13
+ Requires-Dist: kobil-agent-audit<4,>=3.0; extra == 'all'
14
+ Requires-Dist: openai>=1.0; extra == 'all'
15
+ Provides-Extra: anthropic
16
+ Requires-Dist: anthropic>=0.20; extra == 'anthropic'
17
+ Provides-Extra: audit
18
+ Requires-Dist: kobil-agent-audit<4,>=3.0; extra == 'audit'
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
21
+ Requires-Dist: pytest>=8.0; extra == 'dev'
22
+ Provides-Extra: openai
23
+ Requires-Dist: openai>=1.0; extra == 'openai'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # kobil-aitrust (Python)
27
+
28
+ Python mirror of the `@uveysservetoglu/kobil-aitrust` TypeScript meta-package — wraps the SDK
29
+ and Audit packages into a single developer-facing API via `AITrust()`.
30
+
31
+ Behavioral 1:1 port of the TypeScript package in `../src`. The TypeScript
32
+ source is the reference specification.
33
+
34
+ ## Dependency note
35
+
36
+ `AITrust()` constructs a `KobilAI` from `kobil_agent_sdk` (the agent-sdk port).
37
+ That dependency is resolved lazily through `kobil_aitrust.trust._make_kobil_ai`
38
+ so the package imports without agent-sdk installed and so tests can inject a
39
+ fake — mirroring the TS `vi.mock('@uveysservetoglu/kobil-agent-sdk')`. Audit is resolved
40
+ the same way via `_import_audit_cls`.
41
+
42
+ ## Install (dev)
43
+
44
+ ```bash
45
+ python -m venv .venv && source .venv/bin/activate
46
+ pip install -e ".[dev]"
47
+ ```
48
+
49
+ ## Test
50
+
51
+ ```bash
52
+ pytest
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ ```python
58
+ import asyncio
59
+ from kobil_aitrust import AITrust, AITrustConfig
60
+
61
+ ai = AITrust(AITrustConfig(platform_url="https://trust.kobil.com", client_id="my-agent"))
62
+
63
+ @ai.tool # or: secured = ai.tool(fn, ToolOptions(name="get_weather", scopes=["weather:read"]))
64
+ async def get_weather(city, ctx):
65
+ return "sunny"
66
+ ```
@@ -0,0 +1,41 @@
1
+ # kobil-aitrust (Python)
2
+
3
+ Python mirror of the `@uveysservetoglu/kobil-aitrust` TypeScript meta-package — wraps the SDK
4
+ and Audit packages into a single developer-facing API via `AITrust()`.
5
+
6
+ Behavioral 1:1 port of the TypeScript package in `../src`. The TypeScript
7
+ source is the reference specification.
8
+
9
+ ## Dependency note
10
+
11
+ `AITrust()` constructs a `KobilAI` from `kobil_agent_sdk` (the agent-sdk port).
12
+ That dependency is resolved lazily through `kobil_aitrust.trust._make_kobil_ai`
13
+ so the package imports without agent-sdk installed and so tests can inject a
14
+ fake — mirroring the TS `vi.mock('@uveysservetoglu/kobil-agent-sdk')`. Audit is resolved
15
+ the same way via `_import_audit_cls`.
16
+
17
+ ## Install (dev)
18
+
19
+ ```bash
20
+ python -m venv .venv && source .venv/bin/activate
21
+ pip install -e ".[dev]"
22
+ ```
23
+
24
+ ## Test
25
+
26
+ ```bash
27
+ pytest
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ```python
33
+ import asyncio
34
+ from kobil_aitrust import AITrust, AITrustConfig
35
+
36
+ ai = AITrust(AITrustConfig(platform_url="https://trust.kobil.com", client_id="my-agent"))
37
+
38
+ @ai.tool # or: secured = ai.tool(fn, ToolOptions(name="get_weather", scopes=["weather:read"]))
39
+ async def get_weather(city, ctx):
40
+ return "sunny"
41
+ ```
@@ -0,0 +1,88 @@
1
+ """kobil-aitrust — unified meta-package for the KOBIL AI Trust Platform (Python).
2
+
3
+ Python mirror of ``@uveysservetoglu/kobil-aitrust``. Wraps the SDK and Audit packages
4
+ into a simple developer-facing API via the :func:`AITrust` factory.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .config import AITrustConfig
10
+ from .define_tool import (
11
+ SecuredTool,
12
+ ToolDefinition,
13
+ ToolExecute,
14
+ ToolInput,
15
+ define_tool,
16
+ register_tools,
17
+ to_anthropic_tools,
18
+ to_openai_tools,
19
+ )
20
+ from .errors import (
21
+ AITrustError,
22
+ ApprovalDenied,
23
+ ApprovalTimeout,
24
+ ConfigurationError,
25
+ NotAuthenticated,
26
+ NotAuthorized,
27
+ UntrustedAgent,
28
+ get_error_code,
29
+ is_aitrust_error,
30
+ )
31
+ from .integrations import (
32
+ secure_anthropic_client,
33
+ secure_langchain_model,
34
+ secure_openai_client,
35
+ secure_vercel_ai,
36
+ )
37
+ from .trust import (
38
+ AITrust,
39
+ AITrustInstance,
40
+ ApprovalOptions,
41
+ ToolContext,
42
+ ToolOptions,
43
+ detect_client_framework,
44
+ detect_framework,
45
+ )
46
+
47
+ # The release pipeline stamps the version into pyproject.toml only, so read it back from the installed metadata.
48
+ from importlib.metadata import PackageNotFoundError as _PackageNotFoundError, version as _version
49
+
50
+ try:
51
+ __version__ = _version("kobil-aitrust")
52
+ except _PackageNotFoundError: # running from a source checkout that is not installed
53
+ __version__ = "0.0.0"
54
+
55
+ __all__ = [
56
+ "AITrust",
57
+ "AITrustInstance",
58
+ "AITrustConfig",
59
+ "ToolOptions",
60
+ "ToolContext",
61
+ "ApprovalOptions",
62
+ "detect_framework",
63
+ "detect_client_framework",
64
+ # define-tool
65
+ "ToolDefinition",
66
+ "SecuredTool",
67
+ "ToolExecute",
68
+ "ToolInput",
69
+ "define_tool",
70
+ "register_tools",
71
+ "to_anthropic_tools",
72
+ "to_openai_tools",
73
+ # errors
74
+ "AITrustError",
75
+ "NotAuthenticated",
76
+ "NotAuthorized",
77
+ "ApprovalDenied",
78
+ "ApprovalTimeout",
79
+ "UntrustedAgent",
80
+ "ConfigurationError",
81
+ "is_aitrust_error",
82
+ "get_error_code",
83
+ # integrations
84
+ "secure_langchain_model",
85
+ "secure_openai_client",
86
+ "secure_anthropic_client",
87
+ "secure_vercel_ai",
88
+ ]
@@ -0,0 +1,219 @@
1
+ """Loopback MCP bridge (A-E3) — for runtimes that accept only STATIC MCP headers
2
+ (Claude Agent SDK, ADK-TS) and so cannot refresh a Bearer in-code.
3
+
4
+ A tiny local MCP reverse-proxy: it is an MCP *server* to the runtime (on an ephemeral
5
+ `127.0.0.1` port) and an MCP *client* to the real broker. It holds the
6
+ `BrokerBearerProvider` and injects a fresh Bearer on every forwarded call — invisibly,
7
+ so the runtime's static config never carries (or can leak) the token.
8
+
9
+ Hardening (plan A-E3):
10
+ * bound to `127.0.0.1` only; requests from a non-loopback client are refused;
11
+ * a 256-bit in-memory secret, checked constant-time, gates the MCP path (so another
12
+ local process can't drive the bridge);
13
+ * the secret lives only in memory and in the runtime's in-process config — never in
14
+ argv, logs, or a URL;
15
+ * `dispose()` is idempotent and stops the server with the process.
16
+
17
+ This module is imported lazily (only when `create_claude_broker_mcp` is called), so
18
+ kobil-aitrust keeps no hard dependency on `mcp`/`uvicorn`.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import hmac
23
+ import json
24
+ import secrets
25
+ import socket
26
+ import threading
27
+ import time
28
+ from typing import Any
29
+
30
+ BRIDGE_SECRET_HEADER = "x-aitrust-bridge-secret"
31
+ _READY_PATH = "/_bridge_ready"
32
+ _MCP_PATH = "/mcp"
33
+ _LOOPBACK = {"127.0.0.1", "::1"}
34
+ # Connect / write / pool budget for the broker leg — the mcp library's own default.
35
+ _CONNECT_TIMEOUT_S = 30.0
36
+
37
+
38
+ class _BrokerForwardClient:
39
+ """Forwards list/call to the real broker over a fresh connection carrying a fresh
40
+ Bearer. Mirrors the broker's own per-call upstream client, but INJECTS auth."""
41
+
42
+ def __init__(
43
+ self,
44
+ endpoint: str,
45
+ provider: Any,
46
+ *,
47
+ tool_timeout_s: float | None = None,
48
+ end_user: Any = None,
49
+ approver: Any = None,
50
+ ) -> None:
51
+ from .mcp_broker import DEFAULT_TOOL_TIMEOUT_S
52
+
53
+ self._endpoint = endpoint
54
+ self._provider = provider
55
+ self._tool_timeout_s = tool_timeout_s or DEFAULT_TOOL_TIMEOUT_S
56
+ # Resolved per call, not here: a callable has to be consulted at the moment of the
57
+ # call, or a server handling several people pins whoever came first.
58
+ self._end_user = end_user
59
+ # Who the agent names to approve a held call (Revision 14); resolved per call too.
60
+ self._approver = approver
61
+
62
+ async def _session(self):
63
+ import httpx2
64
+ from mcp import ClientSession
65
+ from mcp.client.streamable_http import streamable_http_client
66
+
67
+ token = await self._provider.token()
68
+ # The read timeout must outlast the broker's approval hold: a CIBA-gated call
69
+ # is answered only after a human decides (up to max_approval_wait_s = 120 s),
70
+ # and nothing is sent meanwhile. httpx2's 5 s default cut every held call —
71
+ # and a slow allow call — before the broker could answer. Passing our own
72
+ # client also drops the mcp library's 300 s SSE default, so set it here.
73
+ timeout = httpx2.Timeout(_CONNECT_TIMEOUT_S, read=self._tool_timeout_s)
74
+ from .mcp_broker import approver_headers, end_user_headers
75
+
76
+ hc = httpx2.AsyncClient(
77
+ headers={"Authorization": f"Bearer {token}", **end_user_headers(self._end_user),
78
+ **approver_headers(self._approver)},
79
+ timeout=timeout,
80
+ )
81
+ return hc, streamable_http_client, ClientSession
82
+
83
+ async def list_tools(self):
84
+ hc, factory, ClientSession = await self._session()
85
+ async with hc:
86
+ async with factory(self._endpoint, http_client=hc) as (read, write):
87
+ async with ClientSession(read, write) as session:
88
+ await session.initialize()
89
+ return await session.list_tools()
90
+
91
+ async def call_tool(self, name: str, arguments: dict | None):
92
+ hc, factory, ClientSession = await self._session()
93
+ async with hc:
94
+ async with factory(self._endpoint, http_client=hc) as (read, write):
95
+ async with ClientSession(read, write) as session:
96
+ await session.initialize()
97
+ return await session.call_tool(name, arguments or {})
98
+
99
+
100
+ class LoopbackBridge:
101
+ def __init__(self, *, endpoint: str | None = None, provider: Any = None, backend: Any = None,
102
+ tool_timeout_s: float | None = None, end_user: Any = None, approver: Any = None) -> None:
103
+ if backend is None and (endpoint is None or provider is None):
104
+ raise ValueError("endpoint/provider or backend is required")
105
+ self._forward = backend or _BrokerForwardClient(
106
+ endpoint, provider, tool_timeout_s=tool_timeout_s, end_user=end_user, approver=approver
107
+ )
108
+ self._secret = secrets.token_urlsafe(32) # 256-bit
109
+ self._port = _free_loopback_port()
110
+ self._server = None
111
+ self._thread: threading.Thread | None = None
112
+ self._disposed = False
113
+
114
+ # -- public surface -----------------------------------------------------------
115
+ @property
116
+ def url(self) -> str:
117
+ return f"http://127.0.0.1:{self._port}{_MCP_PATH}"
118
+
119
+ @property
120
+ def secret(self) -> str:
121
+ return self._secret
122
+
123
+ def start(self, *, ready_timeout_s: float = 10.0) -> "LoopbackBridge":
124
+ import httpx2
125
+ import uvicorn
126
+
127
+ app = self._build_app()
128
+ config = uvicorn.Config(app, host="127.0.0.1", port=self._port, log_level="warning")
129
+ self._server = uvicorn.Server(config)
130
+ self._thread = threading.Thread(target=self._server.run, daemon=True)
131
+ self._thread.start()
132
+
133
+ deadline = time.monotonic() + ready_timeout_s
134
+ while time.monotonic() < deadline:
135
+ try:
136
+ if httpx2.get(f"http://127.0.0.1:{self._port}{_READY_PATH}", timeout=0.3).status_code == 200:
137
+ return self
138
+ except Exception:
139
+ time.sleep(0.05)
140
+ self.dispose()
141
+ raise RuntimeError("loopback bridge did not become ready")
142
+
143
+ def dispose(self) -> None:
144
+ if self._disposed:
145
+ return
146
+ self._disposed = True
147
+ if self._server is not None:
148
+ self._server.should_exit = True
149
+ if self._thread is not None:
150
+ self._thread.join(timeout=5)
151
+
152
+ # -- server assembly ----------------------------------------------------------
153
+ def _build_app(self):
154
+ import mcp.types as t
155
+ from mcp.server.mcpserver import MCPServer
156
+ from starlette.requests import Request
157
+ from starlette.responses import JSONResponse
158
+
159
+ server = MCPServer(name="aitrust-broker-bridge", version="0.0.1")
160
+
161
+ @server.custom_route(_READY_PATH, methods=["GET"])
162
+ async def _ready(_request: Request) -> JSONResponse:
163
+ return JSONResponse({"status": "ready"})
164
+
165
+ async def _list(_ctx, _params):
166
+ return await self._forward.list_tools()
167
+
168
+ async def _call(_ctx, params):
169
+ try:
170
+ return await self._forward.call_tool(params.name, params.arguments)
171
+ except Exception as exc: # forward failure surfaces as an MCP tool error
172
+ return t.CallToolResult(
173
+ content=[t.TextContent(type="text", text=f"broker bridge error: {exc}")],
174
+ is_error=True,
175
+ )
176
+
177
+ ll = server._lowlevel_server
178
+ ll.add_request_handler("tools/list", t.PaginatedRequestParams, _list)
179
+ ll.add_request_handler("tools/call", t.CallToolRequestParams, _call)
180
+
181
+ app = server.streamable_http_app(streamable_http_path=_MCP_PATH, host="127.0.0.1")
182
+ return _LoopbackGuard(app, secret=self._secret)
183
+
184
+
185
+ class _LoopbackGuard:
186
+ """Pure-ASGI guard: MCP path requires a loopback client AND the constant-time secret.
187
+ The readiness route stays open (no secret) for local startup polling."""
188
+
189
+ def __init__(self, app, *, secret: str) -> None:
190
+ self._app = app
191
+ self._secret = secret
192
+
193
+ async def __call__(self, scope, receive, send):
194
+ if scope["type"] == "http":
195
+ path = scope.get("path", "")
196
+ if path == _MCP_PATH or path.startswith(_MCP_PATH + "/"):
197
+ client = scope.get("client") or ("", 0)
198
+ if client[0] not in _LOOPBACK:
199
+ return await _deny(send, 403, "non_loopback")
200
+ headers = {k.decode("latin-1").lower(): v.decode("latin-1") for k, v in scope.get("headers", [])}
201
+ if not hmac.compare_digest(headers.get(BRIDGE_SECRET_HEADER, ""), self._secret):
202
+ return await _deny(send, 401, "bad_bridge_secret")
203
+ return await self._app(scope, receive, send)
204
+
205
+
206
+ async def _deny(send, status: int, reason: str) -> None:
207
+ body = json.dumps({"error": reason}).encode()
208
+ await send({"type": "http.response.start", "status": status,
209
+ "headers": [(b"content-type", b"application/json"), (b"content-length", str(len(body)).encode())]})
210
+ await send({"type": "http.response.body", "body": body})
211
+
212
+
213
+ def _free_loopback_port() -> int:
214
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
215
+ try:
216
+ s.bind(("127.0.0.1", 0))
217
+ return s.getsockname()[1]
218
+ finally:
219
+ s.close()
@@ -0,0 +1,123 @@
1
+ """Config resolution for kobil-aitrust (mirror of src/config.ts).
2
+
3
+ Note: ``create_kobil_ai_config`` returns a snake_case dict — the internal API
4
+ contract for the Python KobilAI (agent-sdk). This differs from the TS camelCase
5
+ only by naming convention; the resolution precedence (explicit → AITRUST_* env
6
+ → KOBIL_* env) is identical.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from dataclasses import dataclass
13
+ from typing import Any, Callable, Optional
14
+
15
+
16
+ @dataclass
17
+ class AITrustConfig:
18
+ platform_url: Optional[str] = None
19
+ client_id: Optional[str] = None
20
+ client_secret: Optional[str] = None
21
+ agent_id: Optional[str] = None
22
+ scopes: Optional[list[str]] = None
23
+ dpop: Optional[bool] = None
24
+ management_url: Optional[str] = None
25
+ audit_url: Optional[str] = None
26
+ idp_url: Optional[str] = None
27
+ proxy_url: Optional[str] = None
28
+ proxy_cert_path: Optional[str] = None
29
+ proxy_key_path: Optional[str] = None
30
+ audience: Optional[str] = None
31
+ api_token: Optional[str] = None
32
+ realm: Optional[str] = None
33
+ token_endpoint: Optional[str] = None
34
+ authorization_endpoint: Optional[str] = None
35
+ backchannel_auth_endpoint: Optional[str] = None
36
+ keycloak_token_url: Optional[str] = None
37
+ timeout_ms: Optional[int] = None
38
+ isolated: Optional[bool] = None
39
+ # () -> Optional[str] | Awaitable[Optional[str]]
40
+ secret_provider: Optional[Callable[[], Any]] = None
41
+ # (kobil_agent_sdk ApprovalPendingEvent) -> None | Awaitable[None]. Called once when a
42
+ # native tool call is held for approval, before the SDK starts waiting; observational.
43
+ # The MCP equivalent is attach_mcp(on_approval_pending=...).
44
+ on_approval_pending: Optional[Callable[[Any], Any]] = None
45
+
46
+
47
+ def resolve_config_value(
48
+ explicit: Optional[str], aitrust_env: str, kobil_env: str, env: Optional[dict] = None
49
+ ) -> Optional[str]:
50
+ env = env if env is not None else os.environ
51
+ if explicit is not None:
52
+ return explicit
53
+ return env.get(aitrust_env) if env.get(aitrust_env) is not None else env.get(kobil_env)
54
+
55
+
56
+ def resolve_local_config(config: Optional[AITrustConfig] = None, env: Optional[dict] = None) -> dict:
57
+ config = config or AITrustConfig()
58
+ env = env if env is not None else os.environ
59
+
60
+ def rv(explicit, a, k):
61
+ return resolve_config_value(explicit, a, k, env)
62
+
63
+ return {
64
+ "platform_url": rv(config.platform_url, "AITRUST_PLATFORM_URL", "KOBIL_PLATFORM_URL") or "",
65
+ "management_url": rv(config.management_url, "AITRUST_MANAGEMENT_URL", "KOBIL_MANAGEMENT_URL"),
66
+ "audit_url": rv(config.audit_url, "AITRUST_AUDIT_URL", "KOBIL_AUDIT_URL"),
67
+ "idp_url": rv(config.idp_url, "AITRUST_IDP_URL", "KOBIL_IDP_URL"),
68
+ "client_id": rv(config.client_id, "AITRUST_CLIENT_ID", "KOBIL_CLIENT_ID") or "",
69
+ "agent_id": rv(config.agent_id, "AITRUST_AGENT_ID", "KOBIL_AGENT_ID"),
70
+ "client_secret": rv(config.client_secret, "AITRUST_CLIENT_SECRET", "KOBIL_CLIENT_SECRET"),
71
+ "api_token": rv(config.api_token, "AITRUST_API_TOKEN", "KOBIL_API_TOKEN"),
72
+ "audience": rv(config.audience, "AITRUST_AUDIENCE", "KOBIL_AUDIENCE"),
73
+ "scopes": config.scopes,
74
+ "dpop": config.dpop,
75
+ "proxy_url": rv(config.proxy_url, "AITRUST_PROXY_URL", "KOBIL_PROXY_URL"),
76
+ "proxy_cert_path": rv(config.proxy_cert_path, "AITRUST_PROXY_CERT_PATH", "KOBIL_PROXY_CERT_PATH"),
77
+ "proxy_key_path": rv(config.proxy_key_path, "AITRUST_PROXY_KEY_PATH", "KOBIL_PROXY_KEY_PATH"),
78
+ "realm": rv(config.realm, "AITRUST_REALM", "KOBIL_REALM"),
79
+ "token_endpoint": rv(config.token_endpoint, "AITRUST_TOKEN_ENDPOINT", "KOBIL_TOKEN_ENDPOINT"),
80
+ "authorization_endpoint": rv(
81
+ config.authorization_endpoint, "AITRUST_AUTHORIZATION_ENDPOINT", "KOBIL_AUTHORIZATION_ENDPOINT"
82
+ ),
83
+ "backchannel_auth_endpoint": rv(
84
+ config.backchannel_auth_endpoint, "AITRUST_BACKCHANNEL_AUTH_ENDPOINT", "KOBIL_BACKCHANNEL_AUTH_ENDPOINT"
85
+ ),
86
+ "keycloak_token_url": rv(config.keycloak_token_url, "AITRUST_KEYCLOAK_TOKEN_URL", "KOBIL_KEYCLOAK_TOKEN_URL"),
87
+ "timeout_ms": config.timeout_ms,
88
+ "isolated": config.isolated,
89
+ }
90
+
91
+
92
+ def create_kobil_ai_config(config: Optional[AITrustConfig] = None) -> dict:
93
+ resolved = resolve_local_config(config)
94
+ return {
95
+ "platform_url": resolved["platform_url"],
96
+ "management_url": resolved["management_url"],
97
+ "audit_url": resolved["audit_url"],
98
+ "idp_url": resolved["idp_url"],
99
+ "client_id": resolved["client_id"],
100
+ "agent_id": resolved["agent_id"],
101
+ "client_secret": resolved["client_secret"],
102
+ "api_token": resolved["api_token"],
103
+ "audience": resolved["audience"],
104
+ "scopes": resolved["scopes"],
105
+ "dpop_enabled": resolved["dpop"],
106
+ "timeout_ms": resolved["timeout_ms"],
107
+ "proxy_url": resolved["proxy_url"],
108
+ "proxy_cert_path": resolved["proxy_cert_path"],
109
+ "proxy_key_path": resolved["proxy_key_path"],
110
+ "realm": resolved["realm"],
111
+ "token_endpoint": resolved["token_endpoint"],
112
+ "authorization_endpoint": resolved["authorization_endpoint"],
113
+ "backchannel_auth_endpoint": resolved["backchannel_auth_endpoint"],
114
+ "keycloak_token_url": resolved["keycloak_token_url"],
115
+ "isolated": resolved["isolated"],
116
+ # Carried explicitly: this is a dict literal, so a key left out is silently dropped
117
+ # and the callback never fires. It is not configuration to resolve from the
118
+ # environment, so it comes straight from the config object.
119
+ "on_approval_pending": config.on_approval_pending if config else None,
120
+ }
121
+
122
+
123
+ __all__ = ["AITrustConfig", "resolve_config_value", "resolve_local_config", "create_kobil_ai_config"]