pinta-ai-sdk 0.2.2__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.
Files changed (60) hide show
  1. pinta_ai_sdk-0.2.2/.gitignore +21 -0
  2. pinta_ai_sdk-0.2.2/LICENSE +21 -0
  3. pinta_ai_sdk-0.2.2/PACKAGE.md +110 -0
  4. pinta_ai_sdk-0.2.2/PKG-INFO +142 -0
  5. pinta_ai_sdk-0.2.2/pyproject.toml +87 -0
  6. pinta_ai_sdk-0.2.2/src/pinta/__init__.py +50 -0
  7. pinta_ai_sdk-0.2.2/src/pinta/_version.py +1 -0
  8. pinta_ai_sdk-0.2.2/src/pinta/asgi.py +114 -0
  9. pinta_ai_sdk-0.2.2/src/pinta/auto.py +117 -0
  10. pinta_ai_sdk-0.2.2/src/pinta/core/__init__.py +1 -0
  11. pinta_ai_sdk-0.2.2/src/pinta/core/config.py +131 -0
  12. pinta_ai_sdk-0.2.2/src/pinta/core/context.py +175 -0
  13. pinta_ai_sdk-0.2.2/src/pinta/core/export.py +229 -0
  14. pinta_ai_sdk-0.2.2/src/pinta/core/guard.py +130 -0
  15. pinta_ai_sdk-0.2.2/src/pinta/core/masking.py +83 -0
  16. pinta_ai_sdk-0.2.2/src/pinta/core/policy_client.py +173 -0
  17. pinta_ai_sdk-0.2.2/src/pinta/core/propagation.py +124 -0
  18. pinta_ai_sdk-0.2.2/src/pinta/core/redact.py +34 -0
  19. pinta_ai_sdk-0.2.2/src/pinta/core/runtime.py +153 -0
  20. pinta_ai_sdk-0.2.2/src/pinta/core/spans.py +301 -0
  21. pinta_ai_sdk-0.2.2/src/pinta/guard/__init__.py +36 -0
  22. pinta_ai_sdk-0.2.2/src/pinta/guard/analyze.py +191 -0
  23. pinta_ai_sdk-0.2.2/src/pinta/guard/axes.py +83 -0
  24. pinta_ai_sdk-0.2.2/src/pinta/guard/ceiling.py +222 -0
  25. pinta_ai_sdk-0.2.2/src/pinta/guard/cel.py +415 -0
  26. pinta_ai_sdk-0.2.2/src/pinta/guard/config.py +92 -0
  27. pinta_ai_sdk-0.2.2/src/pinta/guard/credential_context.py +366 -0
  28. pinta_ai_sdk-0.2.2/src/pinta/guard/data/default_guard_config.json +846 -0
  29. pinta_ai_sdk-0.2.2/src/pinta/guard/data_export.py +565 -0
  30. pinta_ai_sdk-0.2.2/src/pinta/guard/explain.py +245 -0
  31. pinta_ai_sdk-0.2.2/src/pinta/guard/export_sql.py +214 -0
  32. pinta_ai_sdk-0.2.2/src/pinta/guard/mask.py +126 -0
  33. pinta_ai_sdk-0.2.2/src/pinta/guard/pii.py +328 -0
  34. pinta_ai_sdk-0.2.2/src/pinta/guard/policy.py +69 -0
  35. pinta_ai_sdk-0.2.2/src/pinta/guard/regex_runner.py +180 -0
  36. pinta_ai_sdk-0.2.2/src/pinta/guard/shell_invocation.py +261 -0
  37. pinta_ai_sdk-0.2.2/src/pinta/guard/sql_runner.py +285 -0
  38. pinta_ai_sdk-0.2.2/src/pinta/guard/types.py +109 -0
  39. pinta_ai_sdk-0.2.2/src/pinta/langchain/__init__.py +7 -0
  40. pinta_ai_sdk-0.2.2/src/pinta/langchain/middleware.py +393 -0
  41. pinta_ai_sdk-0.2.2/src/pinta/langchain/retriever.py +25 -0
  42. pinta_ai_sdk-0.2.2/src/pinta/langchain/tools.py +44 -0
  43. pinta_ai_sdk-0.2.2/src/pinta/poc/__init__.py +7 -0
  44. pinta_ai_sdk-0.2.2/src/pinta/poc/__main__.py +57 -0
  45. pinta_ai_sdk-0.2.2/src/pinta/poc/agent.py +115 -0
  46. pinta_ai_sdk-0.2.2/src/pinta/poc/business.py +95 -0
  47. pinta_ai_sdk-0.2.2/src/pinta/poc/corpus.py +261 -0
  48. pinta_ai_sdk-0.2.2/src/pinta/poc/data/corpus-v1.json +186 -0
  49. pinta_ai_sdk-0.2.2/src/pinta/poc/data/skax-reference-policy-v1.json +10 -0
  50. pinta_ai_sdk-0.2.2/src/pinta/poc/evaluate.py +332 -0
  51. pinta_ai_sdk-0.2.2/src/pinta/poc/evidence.py +263 -0
  52. pinta_ai_sdk-0.2.2/src/pinta/poc/execution.py +312 -0
  53. pinta_ai_sdk-0.2.2/src/pinta/poc/metrics.py +245 -0
  54. pinta_ai_sdk-0.2.2/src/pinta/poc/model.py +64 -0
  55. pinta_ai_sdk-0.2.2/src/pinta/poc/observability.py +438 -0
  56. pinta_ai_sdk-0.2.2/src/pinta/poc/policy.py +68 -0
  57. pinta_ai_sdk-0.2.2/src/pinta/poc/profiles.py +212 -0
  58. pinta_ai_sdk-0.2.2/src/pinta/poc/report.py +163 -0
  59. pinta_ai_sdk-0.2.2/src/pinta/py.typed +0 -0
  60. pinta_ai_sdk-0.2.2/src/pinta/selfcheck.py +176 -0
@@ -0,0 +1,21 @@
1
+ # 빌드 산출물
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+
6
+ # 파이썬
7
+ __pycache__/
8
+ *.py[cod]
9
+ .venv/
10
+ venv/
11
+
12
+ # 도구 캐시
13
+ .pytest_cache/
14
+ .mypy_cache/
15
+ .ruff_cache/
16
+ .coverage
17
+ htmlcov/
18
+ .poc-results/
19
+
20
+ # 로컬 설정
21
+ .env
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pinta AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,110 @@
1
+ # Pinta AI SDK for Python agents
2
+
3
+ Pinta adds observability, telemetry masking and local tool-call policy evaluation
4
+ to Python agents. LangChain integration uses middleware; no sidecar is required.
5
+
6
+ **Requirements:** Python 3.12 or newer. LangChain integration requires
7
+ `langchain >= 1.0` with `create_agent`.
8
+
9
+ The distribution name is **`pinta-ai-sdk`**; the Python import remains **`pinta`**.
10
+ It is not the unrelated `pinta` distribution on PyPI. If you installed an earlier
11
+ private `pinta-ai` wheel, uninstall that distribution before installing this one:
12
+ both use the same Python namespace.
13
+
14
+ ## Install
15
+
16
+ If your application already has a compatible LangChain installation:
17
+
18
+ ```bash
19
+ python -m pip install pinta-ai-sdk
20
+ ```
21
+
22
+ To install LangChain dependencies as well:
23
+
24
+ ```bash
25
+ python -m pip install "pinta-ai-sdk[langchain]"
26
+ ```
27
+
28
+ Check the installed version and run the bundled offline self-check:
29
+
30
+ ```bash
31
+ python -c "import pinta; print(pinta.__version__)"
32
+ python -m pinta.selfcheck
33
+ ```
34
+
35
+ The self-check requires LangChain. It uses a deterministic local model and
36
+ in-memory tools, does not contact a model provider or the Pinta backend, and does
37
+ not prove that your application's telemetry has reached the management console.
38
+
39
+ ## Offline installation into an existing LangChain application
40
+
41
+ On an internet-connected computer using the same Python minor version as the
42
+ target server, download the SDK and its dependencies. Replace `VERSION` with
43
+ the selected release number:
44
+
45
+ ```bash
46
+ python -m pip download "pinta-ai-sdk==VERSION" -d sdk --only-binary=:all: --platform any
47
+ ```
48
+
49
+ Transfer the entire `sdk` directory through your approved file-transfer channel.
50
+ In the target application's Python environment:
51
+
52
+ ```bash
53
+ python -m pip install --no-index --find-links=sdk "pinta-ai-sdk==VERSION"
54
+ python -m pinta.selfcheck
55
+ ```
56
+
57
+ This path deliberately omits the `langchain` extra: the application already
58
+ provides LangChain. `--platform any` accepts platform-independent wheels only,
59
+ instead of accidentally downloading a workstation-specific native dependency.
60
+ Installing or upgrading LangChain itself requires a separate bundle compatible
61
+ with the server's Python version, operating system and architecture.
62
+
63
+ ## Connect an application
64
+
65
+ Set `PINTA_BACKEND_URL` and `PINTA_API_KEY` in the application process using your
66
+ organization's approved endpoint and secret-management mechanism. Start with
67
+ `PINTA_GUARD_MODE=monitor`; switch to `enforce` only after reviewing policies and
68
+ normal application behavior.
69
+
70
+ At process startup, configure Pinta. Add middleware at the agent factory and
71
+ provide the authenticated application's user identity around each invocation:
72
+
73
+ ```python
74
+ from langchain.agents import create_agent
75
+ from pinta import PintaSettings, configure, get_runtime, pinta_context
76
+ from pinta.langchain import PintaMiddleware
77
+
78
+ configure(PintaSettings.from_env())
79
+ agent = create_agent(model, tools=tools, middleware=[PintaMiddleware()])
80
+
81
+ async def answer(user, session_id, messages):
82
+ async with pinta_context(
83
+ user_id=user.id, user_email=user.email, session_id=session_id,
84
+ ):
85
+ return await agent.ainvoke({"messages": messages})
86
+
87
+ # In the application's async shutdown hook:
88
+ # await get_runtime().ashutdown()
89
+ ```
90
+
91
+ Here `model`, `tools` and `user` are supplied by your application. Installation
92
+ alone does not instrument an agent or enable telemetry transmission. Outbound
93
+ HTTPS to the configured Pinta endpoint is required for export and policy refresh;
94
+ local evaluation and the self-check do not require that connection.
95
+
96
+ ## Scope and limitations
97
+
98
+ - Masking protects Pinta telemetry, not original business inputs, tool results
99
+ or prompts sent to a model provider.
100
+ - `monitor` evaluates without blocking. `enforce` blocks `DENY` and `REVIEW`
101
+ before the selected tool handler executes.
102
+ - Policy evaluation errors fail open with diagnostics; masking errors redact
103
+ the affected telemetry value and are reported as failures.
104
+ - Detection depends on configured policies and supported input shapes.
105
+ This SDK is not universally equivalent to every TypeScript runtime extractor
106
+ and does not guarantee detection of every credential or unsafe action.
107
+ - Treat this alpha SDK as a version-pinned integration and validate your own
108
+ normal and violation cases before enforcement.
109
+
110
+ MIT licensed. Learn more at [pinta.sh](https://pinta.sh).
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.5
2
+ Name: pinta-ai-sdk
3
+ Version: 0.2.2
4
+ Summary: Pinta AI runtime guard adaptor for Python agents (OpenTelemetry spans + tool-call guard)
5
+ Project-URL: Homepage, https://pinta.sh
6
+ Project-URL: Documentation, https://pypi.org/project/pinta-ai-sdk/
7
+ Author: Pinta AI
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: ai-spm,guardrails,langchain,observability,opentelemetry,security
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Security
16
+ Classifier: Topic :: System :: Monitoring
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: httpx>=0.27
20
+ Requires-Dist: opentelemetry-api>=1.44
21
+ Requires-Dist: opentelemetry-sdk>=1.44
22
+ Provides-Extra: langchain
23
+ Requires-Dist: langchain-core>=1.0; extra == 'langchain'
24
+ Requires-Dist: langchain>=1.0; extra == 'langchain'
25
+ Provides-Extra: poc
26
+ Requires-Dist: langchain-core>=1.0; extra == 'poc'
27
+ Requires-Dist: langchain-mcp-adapters<0.3,>=0.2.2; extra == 'poc'
28
+ Requires-Dist: langchain>=1.0; extra == 'poc'
29
+ Requires-Dist: langsmith<1,>=0.12; extra == 'poc'
30
+ Requires-Dist: mcp<2,>=1.30; extra == 'poc'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # Pinta AI SDK for Python agents
34
+
35
+ Pinta adds observability, telemetry masking and local tool-call policy evaluation
36
+ to Python agents. LangChain integration uses middleware; no sidecar is required.
37
+
38
+ **Requirements:** Python 3.12 or newer. LangChain integration requires
39
+ `langchain >= 1.0` with `create_agent`.
40
+
41
+ The distribution name is **`pinta-ai-sdk`**; the Python import remains **`pinta`**.
42
+ It is not the unrelated `pinta` distribution on PyPI. If you installed an earlier
43
+ private `pinta-ai` wheel, uninstall that distribution before installing this one:
44
+ both use the same Python namespace.
45
+
46
+ ## Install
47
+
48
+ If your application already has a compatible LangChain installation:
49
+
50
+ ```bash
51
+ python -m pip install pinta-ai-sdk
52
+ ```
53
+
54
+ To install LangChain dependencies as well:
55
+
56
+ ```bash
57
+ python -m pip install "pinta-ai-sdk[langchain]"
58
+ ```
59
+
60
+ Check the installed version and run the bundled offline self-check:
61
+
62
+ ```bash
63
+ python -c "import pinta; print(pinta.__version__)"
64
+ python -m pinta.selfcheck
65
+ ```
66
+
67
+ The self-check requires LangChain. It uses a deterministic local model and
68
+ in-memory tools, does not contact a model provider or the Pinta backend, and does
69
+ not prove that your application's telemetry has reached the management console.
70
+
71
+ ## Offline installation into an existing LangChain application
72
+
73
+ On an internet-connected computer using the same Python minor version as the
74
+ target server, download the SDK and its dependencies. Replace `VERSION` with
75
+ the selected release number:
76
+
77
+ ```bash
78
+ python -m pip download "pinta-ai-sdk==VERSION" -d sdk --only-binary=:all: --platform any
79
+ ```
80
+
81
+ Transfer the entire `sdk` directory through your approved file-transfer channel.
82
+ In the target application's Python environment:
83
+
84
+ ```bash
85
+ python -m pip install --no-index --find-links=sdk "pinta-ai-sdk==VERSION"
86
+ python -m pinta.selfcheck
87
+ ```
88
+
89
+ This path deliberately omits the `langchain` extra: the application already
90
+ provides LangChain. `--platform any` accepts platform-independent wheels only,
91
+ instead of accidentally downloading a workstation-specific native dependency.
92
+ Installing or upgrading LangChain itself requires a separate bundle compatible
93
+ with the server's Python version, operating system and architecture.
94
+
95
+ ## Connect an application
96
+
97
+ Set `PINTA_BACKEND_URL` and `PINTA_API_KEY` in the application process using your
98
+ organization's approved endpoint and secret-management mechanism. Start with
99
+ `PINTA_GUARD_MODE=monitor`; switch to `enforce` only after reviewing policies and
100
+ normal application behavior.
101
+
102
+ At process startup, configure Pinta. Add middleware at the agent factory and
103
+ provide the authenticated application's user identity around each invocation:
104
+
105
+ ```python
106
+ from langchain.agents import create_agent
107
+ from pinta import PintaSettings, configure, get_runtime, pinta_context
108
+ from pinta.langchain import PintaMiddleware
109
+
110
+ configure(PintaSettings.from_env())
111
+ agent = create_agent(model, tools=tools, middleware=[PintaMiddleware()])
112
+
113
+ async def answer(user, session_id, messages):
114
+ async with pinta_context(
115
+ user_id=user.id, user_email=user.email, session_id=session_id,
116
+ ):
117
+ return await agent.ainvoke({"messages": messages})
118
+
119
+ # In the application's async shutdown hook:
120
+ # await get_runtime().ashutdown()
121
+ ```
122
+
123
+ Here `model`, `tools` and `user` are supplied by your application. Installation
124
+ alone does not instrument an agent or enable telemetry transmission. Outbound
125
+ HTTPS to the configured Pinta endpoint is required for export and policy refresh;
126
+ local evaluation and the self-check do not require that connection.
127
+
128
+ ## Scope and limitations
129
+
130
+ - Masking protects Pinta telemetry, not original business inputs, tool results
131
+ or prompts sent to a model provider.
132
+ - `monitor` evaluates without blocking. `enforce` blocks `DENY` and `REVIEW`
133
+ before the selected tool handler executes.
134
+ - Policy evaluation errors fail open with diagnostics; masking errors redact
135
+ the affected telemetry value and are reported as failures.
136
+ - Detection depends on configured policies and supported input shapes.
137
+ This SDK is not universally equivalent to every TypeScript runtime extractor
138
+ and does not guarantee detection of every credential or unsafe action.
139
+ - Treat this alpha SDK as a version-pinned integration and validate your own
140
+ normal and violation cases before enforcement.
141
+
142
+ MIT licensed. Learn more at [pinta.sh](https://pinta.sh).
@@ -0,0 +1,87 @@
1
+ [project]
2
+ name = "pinta-ai-sdk"
3
+ version = "0.2.2"
4
+ description = "Pinta AI runtime guard adaptor for Python agents (OpenTelemetry spans + tool-call guard)"
5
+ readme = "PACKAGE.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "Pinta AI" }]
10
+ keywords = ["langchain", "opentelemetry", "observability", "ai-spm", "guardrails", "security"]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Intended Audience :: Developers",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python :: 3.13",
16
+ "Topic :: Security",
17
+ "Topic :: System :: Monitoring",
18
+ "Typing :: Typed",
19
+ ]
20
+ dependencies = [
21
+ "opentelemetry-api>=1.44",
22
+ "opentelemetry-sdk>=1.44",
23
+ "httpx>=0.27",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ langchain = ["langchain>=1.0", "langchain-core>=1.0"]
28
+ poc = [
29
+ "langchain>=1.0",
30
+ "langchain-core>=1.0",
31
+ "langsmith>=0.12,<1",
32
+ "mcp>=1.30,<2",
33
+ "langchain-mcp-adapters>=0.2.2,<0.3",
34
+ ]
35
+
36
+ [project.urls]
37
+ Homepage = "https://pinta.sh"
38
+ Documentation = "https://pypi.org/project/pinta-ai-sdk/"
39
+
40
+ [dependency-groups]
41
+ dev = [
42
+ "pytest>=8.3",
43
+ "pytest-asyncio>=0.24",
44
+ "ruff>=0.6",
45
+ "mypy>=1.11",
46
+ "starlette>=0.40",
47
+ "deepagents>=0.1",
48
+ ]
49
+
50
+ [build-system]
51
+ requires = ["hatchling"]
52
+ build-backend = "hatchling.build"
53
+
54
+ [tool.hatch.build.targets.wheel]
55
+ packages = ["src/pinta"]
56
+
57
+ [tool.hatch.build.targets.sdist]
58
+ include = [
59
+ "src/pinta",
60
+ "PACKAGE.md",
61
+ "LICENSE",
62
+ "pyproject.toml",
63
+ ]
64
+
65
+ [tool.pytest.ini_options]
66
+ testpaths = ["tests"]
67
+ pythonpath = ["src"]
68
+ asyncio_mode = "auto"
69
+ asyncio_default_fixture_loop_scope = "function"
70
+
71
+ [tool.ruff]
72
+ line-length = 100
73
+ target-version = "py312"
74
+ src = ["src", "tests"]
75
+
76
+ [tool.ruff.lint]
77
+ select = ["E", "F", "I", "UP", "B"]
78
+
79
+ [tool.mypy]
80
+ python_version = "3.12"
81
+ strict = true
82
+ mypy_path = "src"
83
+ packages = ["pinta"]
84
+
85
+ [[tool.mypy.overrides]]
86
+ module = ["langchain.*", "langchain_core.*", "langgraph.*"]
87
+ ignore_missing_imports = true
@@ -0,0 +1,50 @@
1
+ """Pinta AI adaptor for Python agents.
2
+
3
+ Quickstart::
4
+
5
+ from pinta import configure, PintaSettings, pinta_context
6
+
7
+ runtime = configure(PintaSettings.from_env())
8
+ async with pinta_context(user_id="u1", user_email="a@corp.com", session_id="s1"):
9
+ ... # every span in here shares one trace
10
+ """
11
+
12
+ from pinta._version import __version__
13
+ from pinta.core.config import PintaSettings
14
+ from pinta.core.context import Identity, current_identity, pinta_context
15
+ from pinta.core.guard import GuardClient, GuardDecision, GuardInput
16
+ from pinta.core.propagation import Propagated, extract_propagation, outbound_headers
17
+ from pinta.core.runtime import PintaRuntime, configure, get_runtime
18
+
19
+
20
+ def instrument() -> list[str]:
21
+ """Patch agent factories so every agent gets PintaMiddleware (see ``pinta.auto``)."""
22
+ from pinta.auto import instrument as _instrument
23
+
24
+ return _instrument()
25
+
26
+
27
+ def uninstrument() -> None:
28
+ from pinta.auto import uninstrument as _uninstrument
29
+
30
+ _uninstrument()
31
+
32
+
33
+ __all__ = [
34
+ "GuardClient",
35
+ "GuardDecision",
36
+ "GuardInput",
37
+ "Identity",
38
+ "PintaRuntime",
39
+ "PintaSettings",
40
+ "Propagated",
41
+ "__version__",
42
+ "configure",
43
+ "current_identity",
44
+ "extract_propagation",
45
+ "get_runtime",
46
+ "instrument",
47
+ "outbound_headers",
48
+ "pinta_context",
49
+ "uninstrument",
50
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.2.2"
@@ -0,0 +1,114 @@
1
+ """ASGI middleware that opens ``pinta_context`` for every HTTP request.
2
+
3
+ Pure ASGI (no Starlette dependency): the ``resolver`` receives a ``starlette.requests.Request``
4
+ when Starlette is installed, else the raw ASGI ``scope``. It returns an ``Identity`` (or a
5
+ mapping of ``pinta_context`` keyword arguments), ``None`` to leave the request anonymous, and
6
+ may be sync or async. Errors in the resolver are logged and the request continues anonymous.
7
+
8
+ app.add_middleware(PintaIdentityMiddleware, resolver=lambda req: Identity(
9
+ user_id=req.state.user.id, user_email=req.state.user.email,
10
+ session_id=req.headers.get("x-thread-id"),
11
+ ))
12
+
13
+ Register it *before* the authentication middleware (Starlette wraps later ``add_middleware``
14
+ calls outside earlier ones), so authentication has run when the resolver reads the user.
15
+ Background jobs and queue workers have no request: wrap those in ``pinta_context`` yourself.
16
+
17
+ ``propagate=True``: for requests the resolver trusts with an identity, read the W3C
18
+ ``traceparent``/``baggage`` headers so the root span joins the caller's trace. Identity itself
19
+ is never taken from headers — only trace linkage and the calling agent are, and only once the
20
+ resolver has already declared the request trusted. ``propagate`` may also be a callable that
21
+ decides per request (e.g. only the A2A endpoint) — it receives the same request object the
22
+ resolver does and is consulted only when the resolver returned an identity.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import inspect
28
+ import logging
29
+ from collections.abc import Awaitable, Callable, Mapping
30
+ from typing import Any
31
+
32
+ from pinta.core.context import Identity, pinta_context
33
+ from pinta.core.propagation import Propagated, extract_propagation
34
+
35
+ log = logging.getLogger("pinta.asgi")
36
+
37
+ Resolved = Identity | Mapping[str, Any] | None
38
+ Resolver = Callable[[Any], Resolved | Awaitable[Resolved]]
39
+
40
+
41
+ def _request_for(scope: dict[str, Any], receive: Any) -> Any:
42
+ try:
43
+ from starlette.requests import Request
44
+ except ImportError: # pragma: no cover - starlette is optional
45
+ return scope
46
+ return Request(scope, receive)
47
+
48
+
49
+ def _headers(scope: dict[str, Any]) -> dict[str, str]:
50
+ out: dict[str, str] = {}
51
+ for raw_key, raw_value in scope.get("headers") or ():
52
+ out[bytes(raw_key).decode("latin-1").lower()] = bytes(raw_value).decode("latin-1")
53
+ return out
54
+
55
+
56
+ class PintaIdentityMiddleware:
57
+ def __init__(
58
+ self,
59
+ app: Any,
60
+ resolver: Resolver,
61
+ *,
62
+ propagate: bool | Callable[[Any], bool] = False,
63
+ ) -> None:
64
+ self.app = app
65
+ self.resolver = resolver
66
+ self.propagate = propagate
67
+
68
+ async def _resolve(self, request: Any) -> Identity | None:
69
+ try:
70
+ result = self.resolver(request)
71
+ if inspect.isawaitable(result):
72
+ result = await result
73
+ except Exception: # noqa: BLE001 - never break the request because of us
74
+ log.exception("pinta: identity resolver failed; request stays anonymous")
75
+ return None
76
+ if result is None:
77
+ return None
78
+ if isinstance(result, Identity):
79
+ return result
80
+ return Identity(**dict(result))
81
+
82
+ def _should_propagate(self, request: Any) -> bool:
83
+ if not callable(self.propagate):
84
+ return bool(self.propagate)
85
+ try:
86
+ return bool(self.propagate(request))
87
+ except Exception: # noqa: BLE001 - never break the request because of us
88
+ log.exception("pinta: propagate predicate failed; request joins no trace")
89
+ return False
90
+
91
+ async def __call__(self, scope: dict[str, Any], receive: Any, send: Any) -> None:
92
+ if scope.get("type") != "http":
93
+ await self.app(scope, receive, send)
94
+ return
95
+ request = _request_for(scope, receive)
96
+ identity = await self._resolve(request)
97
+ if identity is None:
98
+ await self.app(scope, receive, send)
99
+ return
100
+ parent: Propagated | None = None
101
+ if self._should_propagate(request):
102
+ parent = extract_propagation(_headers(scope))
103
+ async with pinta_context(
104
+ user_id=identity.user_id,
105
+ user_email=identity.user_email,
106
+ session_id=identity.session_id,
107
+ conversation_id=identity.conversation_id,
108
+ attributes=identity.attributes,
109
+ parent=parent,
110
+ ):
111
+ await self.app(scope, receive, send)
112
+
113
+
114
+ __all__ = ["PintaIdentityMiddleware", "Resolver"]
@@ -0,0 +1,117 @@
1
+ """Zero-touch instrumentation: patch the agent factories so every agent gets ``PintaMiddleware``.
2
+
3
+ import pinta
4
+ pinta.instrument() # before the modules that build agents are imported
5
+
6
+ Patches ``langchain.agents.create_agent`` (and its defining module) and, when deepagents is
7
+ installed, ``deepagents.create_deep_agent`` — giving every ``SubAgent`` its own middleware with
8
+ ``agent={"name": ..., "kind": "subagent"}`` and the parent a ``subagents=[...]`` list so the
9
+ manifest and the delegation edges are complete. Agents that already carry a ``PintaMiddleware``
10
+ are left alone. ``uninstrument()`` restores the originals.
11
+
12
+ Caveat of any monkeypatch: a module that did ``from langchain.agents import create_agent``
13
+ *before* ``instrument()`` keeps the original function. Call ``instrument()`` first thing in the
14
+ process (or use the explicit ``middleware=[PintaMiddleware()]`` form, which is the default
15
+ recommendation).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import functools
21
+ import importlib
22
+ import logging
23
+ from collections.abc import Callable, Sequence
24
+ from typing import Any
25
+
26
+ from pinta.langchain.middleware import PintaMiddleware
27
+
28
+ log = logging.getLogger("pinta.instrument")
29
+
30
+ _PATCHED: dict[tuple[str, str], Callable[..., Any]] = {}
31
+
32
+
33
+ def _has_pinta(middleware: Sequence[Any] | None) -> bool:
34
+ return any(isinstance(m, PintaMiddleware) for m in middleware or ())
35
+
36
+
37
+ def _with_pinta(middleware: Sequence[Any] | None, **kwargs: Any) -> list[Any]:
38
+ items = list(middleware or ())
39
+ if not _has_pinta(items):
40
+ items.append(PintaMiddleware(**kwargs))
41
+ return items
42
+
43
+
44
+ def _wrap_create_agent(original: Callable[..., Any]) -> Callable[..., Any]:
45
+ @functools.wraps(original)
46
+ def create_agent(*args: Any, **kwargs: Any) -> Any:
47
+ kwargs["middleware"] = _with_pinta(kwargs.get("middleware"))
48
+ return original(*args, **kwargs)
49
+
50
+ return create_agent
51
+
52
+
53
+ def _wrap_create_deep_agent(original: Callable[..., Any]) -> Callable[..., Any]:
54
+ @functools.wraps(original)
55
+ def create_deep_agent(*args: Any, **kwargs: Any) -> Any:
56
+ subagents = kwargs.get("subagents") or []
57
+ names: list[str] = []
58
+ patched: list[Any] = []
59
+ for sub in subagents:
60
+ if isinstance(sub, dict):
61
+ name = str(sub.get("name") or "")
62
+ names.append(name)
63
+ sub = {
64
+ **sub,
65
+ "middleware": _with_pinta(
66
+ sub.get("middleware"), agent={"name": name, "kind": "subagent"}
67
+ ),
68
+ }
69
+ patched.append(sub)
70
+ if subagents:
71
+ kwargs["subagents"] = patched
72
+ kwargs["middleware"] = _with_pinta(kwargs.get("middleware"), subagents=names)
73
+ return original(*args, **kwargs)
74
+
75
+ return create_deep_agent
76
+
77
+
78
+ def _patch(
79
+ module_name: str, attr: str, wrap: Callable[[Callable[..., Any]], Callable[..., Any]]
80
+ ) -> None:
81
+ key = (module_name, attr)
82
+ if key in _PATCHED:
83
+ return
84
+ try:
85
+ module = importlib.import_module(module_name)
86
+ except ImportError:
87
+ return
88
+ original = getattr(module, attr, None)
89
+ if original is None:
90
+ return
91
+ _PATCHED[key] = original
92
+ setattr(module, attr, wrap(original))
93
+
94
+
95
+ def instrument() -> list[str]:
96
+ """Patch the factories. Returns the fully-qualified names that were patched (idempotent)."""
97
+ for module_name in ("langchain.agents", "langchain.agents.factory"):
98
+ _patch(module_name, "create_agent", _wrap_create_agent)
99
+ for module_name in ("deepagents", "deepagents.graph"):
100
+ _patch(module_name, "create_deep_agent", _wrap_create_deep_agent)
101
+ names = [f"{m}.{a}" for m, a in _PATCHED]
102
+ log.info("pinta: instrumented %s", ", ".join(names) or "nothing")
103
+ return names
104
+
105
+
106
+ def uninstrument() -> None:
107
+ """Restore every patched factory."""
108
+ for (module_name, attr), original in list(_PATCHED.items()):
109
+ setattr(importlib.import_module(module_name), attr, original)
110
+ del _PATCHED[(module_name, attr)]
111
+
112
+
113
+ def is_instrumented() -> bool:
114
+ return bool(_PATCHED)
115
+
116
+
117
+ __all__ = ["instrument", "is_instrumented", "uninstrument"]
@@ -0,0 +1 @@
1
+ """Framework-free core: settings, identity context, guard client, span recorder, exporter."""