tracengent 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.
Files changed (54) hide show
  1. tracengent-0.1.0/.gitignore +11 -0
  2. tracengent-0.1.0/LICENSE +21 -0
  3. tracengent-0.1.0/PKG-INFO +180 -0
  4. tracengent-0.1.0/README.md +144 -0
  5. tracengent-0.1.0/pyproject.toml +68 -0
  6. tracengent-0.1.0/samples/demo_agent.py +66 -0
  7. tracengent-0.1.0/src/tracengent/__init__.py +81 -0
  8. tracengent-0.1.0/src/tracengent/_auth.py +41 -0
  9. tracengent-0.1.0/src/tracengent/_jwt.py +62 -0
  10. tracengent-0.1.0/src/tracengent/_permissions.py +15 -0
  11. tracengent-0.1.0/src/tracengent/_serialization.py +41 -0
  12. tracengent-0.1.0/src/tracengent/authorization.py +75 -0
  13. tracengent-0.1.0/src/tracengent/client.py +244 -0
  14. tracengent-0.1.0/src/tracengent/delegation.py +116 -0
  15. tracengent-0.1.0/src/tracengent/dispatcher.py +163 -0
  16. tracengent-0.1.0/src/tracengent/events.py +140 -0
  17. tracengent-0.1.0/src/tracengent/gateway.py +135 -0
  18. tracengent-0.1.0/src/tracengent/integrations/__init__.py +8 -0
  19. tracengent-0.1.0/src/tracengent/integrations/fastapi.py +56 -0
  20. tracengent-0.1.0/src/tracengent/integrations/flask.py +57 -0
  21. tracengent-0.1.0/src/tracengent/options.py +96 -0
  22. tracengent-0.1.0/src/tracengent/py.typed +0 -0
  23. tracengent-0.1.0/src/tracengent/redaction.py +117 -0
  24. tracengent-0.1.0/src/tracengent/token_provider.py +89 -0
  25. tracengent-0.1.0/src/tracengent/tool_gate.py +113 -0
  26. tracengent-0.1.0/src/tracengent/tool_governance.py +82 -0
  27. tracengent-0.1.0/src/tracengent/trace.py +226 -0
  28. tracengent-0.1.0/src/tracengent/trace_context.py +31 -0
  29. tracengent-0.1.0/src/tracengent/tracking.py +87 -0
  30. tracengent-0.1.0/tests/conformance/test_conformance.py +171 -0
  31. tracengent-0.1.0/tests/conformance/wire_contract.json +93 -0
  32. tracengent-0.1.0/tests/conftest.py +6 -0
  33. tracengent-0.1.0/tests/integration/test_live_platform.py +64 -0
  34. tracengent-0.1.0/tests/unit/_helpers.py +40 -0
  35. tracengent-0.1.0/tests/unit/_jwt_helper.py +14 -0
  36. tracengent-0.1.0/tests/unit/test_authorization.py +175 -0
  37. tracengent-0.1.0/tests/unit/test_client.py +88 -0
  38. tracengent-0.1.0/tests/unit/test_delegation.py +161 -0
  39. tracengent-0.1.0/tests/unit/test_dispatcher.py +122 -0
  40. tracengent-0.1.0/tests/unit/test_events.py +60 -0
  41. tracengent-0.1.0/tests/unit/test_gateway.py +136 -0
  42. tracengent-0.1.0/tests/unit/test_has_permission.py +50 -0
  43. tracengent-0.1.0/tests/unit/test_integration_fastapi.py +76 -0
  44. tracengent-0.1.0/tests/unit/test_integration_flask.py +66 -0
  45. tracengent-0.1.0/tests/unit/test_jwt.py +73 -0
  46. tracengent-0.1.0/tests/unit/test_options.py +76 -0
  47. tracengent-0.1.0/tests/unit/test_permissions.py +23 -0
  48. tracengent-0.1.0/tests/unit/test_redaction.py +99 -0
  49. tracengent-0.1.0/tests/unit/test_serialization.py +49 -0
  50. tracengent-0.1.0/tests/unit/test_token_provider.py +92 -0
  51. tracengent-0.1.0/tests/unit/test_tool_gate.py +112 -0
  52. tracengent-0.1.0/tests/unit/test_trace.py +146 -0
  53. tracengent-0.1.0/tests/unit/test_track_llm.py +81 -0
  54. tracengent-0.1.0/tests/unit/test_tracking.py +60 -0
@@ -0,0 +1,11 @@
1
+ # Python build / cache artifacts
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .venv/
11
+ venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Besar Kutleshi
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,180 @@
1
+ Metadata-Version: 2.4
2
+ Name: tracengent
3
+ Version: 0.1.0
4
+ Summary: First-party Python SDK for Tracengent: instrument and govern agents (events, decision traces, authorization, tool governance, delegation).
5
+ Project-URL: Homepage, https://github.com/besarkutleshi/agentgov
6
+ Project-URL: Source, https://github.com/besarkutleshi/agentgov
7
+ Project-URL: Repository, https://github.com/besarkutleshi/agentgov.git
8
+ Author: Besar Kutleshi
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: agent,authorization,governance,llm,observability,tracengent
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: httpx>=0.27
23
+ Requires-Dist: typing-extensions>=4.7; python_version < '3.11'
24
+ Provides-Extra: dev
25
+ Requires-Dist: fastapi>=0.110; extra == 'dev'
26
+ Requires-Dist: flask>=3.0; extra == 'dev'
27
+ Requires-Dist: mypy>=1.10; extra == 'dev'
28
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
29
+ Requires-Dist: pytest>=8.0; extra == 'dev'
30
+ Requires-Dist: ruff>=0.5; extra == 'dev'
31
+ Provides-Extra: fastapi
32
+ Requires-Dist: fastapi>=0.110; extra == 'fastapi'
33
+ Provides-Extra: flask
34
+ Requires-Dist: flask>=3.0; extra == 'flask'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # Tracengent — Python SDK
38
+
39
+ First-party Python SDK for [Tracengent](../../phases/phase-10-multi-language-sdks.md): instrument and
40
+ govern agents written in Python with parity to the .NET SDK ([`src/Tracengent.Sdk`](../../src/Tracengent.Sdk/)).
41
+
42
+ > **Status: feature-complete (Phase 10).** Full parity with the .NET SDK across telemetry, traces,
43
+ > authorization, tool governance, and delegation. The reference implementation is the .NET SDK; the
44
+ > contract is [§3 of the phase doc](../../phases/phase-10-multi-language-sdks.md#3-the-shared-wire-contract-the-spec-all-five-sdks-implement).
45
+
46
+ ## Milestone status
47
+
48
+ | Milestone | Capability | State |
49
+ |---|---|---|
50
+ | **M1** | Options + validation, JWT/permission helpers, JSON redactor (pure logic) | ✅ done |
51
+ | **M2** | Event model, token provider, background dispatcher, `record` | ✅ done |
52
+ | **M3** | Decision traces + auto-tracking + ambient context + `authorize`/`ensure_authorized` | ✅ done |
53
+ | **M4** | `has_permission` (token-local) + framework bindings (FastAPI/Flask) | ✅ done |
54
+ | **M5** | Tool governance: egress gate, MCP/function gate, redaction | ✅ done |
55
+ | **M6** | Delegation: delegated session + attenuation (A2A) | ✅ done |
56
+ | **M7** | Packaging + sample + conformance + (gated) integration tests | ✅ done |
57
+
58
+ **Tests:** 152 unit + 7 conformance, 3 gated integration tests (`pytest -m integration`). The
59
+ conformance suite replays the shared [`wire_contract.json`](tests/conformance/wire_contract.json)
60
+ fixture and asserts the SDK's request shapes match the contract byte-for-byte (modulo volatile
61
+ ids/timestamps) — this same fixture is vendored into every Tracengent SDK. A runnable end-to-end
62
+ example lives in [`samples/demo_agent.py`](samples/demo_agent.py).
63
+
64
+ ## What works today (M1–M3)
65
+
66
+ ```python
67
+ from tracengent import TracengentClient, TracengentOptions, LlmCallResult, Money
68
+
69
+ opts = TracengentOptions.from_env(agent_id="travel-agent")
70
+ with TracengentClient(opts) as client:
71
+ # Open a decision trace — groups steps, tool calls and actions into one timeline.
72
+ with client.begin_trace("book a trip to Lisbon", actor_user_id="u_123") as trace:
73
+ trace.step("decided to search flights")
74
+
75
+ # Wrap tool calls / actions — timed and recorded automatically.
76
+ flights = trace.track_tool_call("search-flights", lambda: search(...))
77
+
78
+ # Govern an LLM call: reserve token budget before, reconcile actual usage after.
79
+ answer = trace.track_llm_call(
80
+ "summarize",
81
+ lambda: LlmCallResult(value=call_model(...), tokens=900, model="gpt"),
82
+ estimated_tokens=1000,
83
+ )
84
+
85
+ # Imperative authorization for a sensitive action (raises if denied).
86
+ # Send the spend's currency too — the platform converts it into the policy/budget
87
+ # currency before enforcing limits, so a USD charge is checked against a EUR cap.
88
+ client.ensure_authorized("payments:refund", {"amount": 280, "currency": "USD"})
89
+ trace.track_action("refund", lambda: do_refund(...), cost=Money.eur(280))
90
+ # leaving the trace flushes its completion; leaving the client flushes remaining events.
91
+ ```
92
+
93
+ Outbound HTTP can be auto-recorded as `tool_call` events by routing it through a tracked client:
94
+
95
+ ```python
96
+ from tracengent import tracked_client
97
+
98
+ http = tracked_client(client, base_url="https://api.example.com")
99
+ http.get("/things") # recorded as a tool_call, attached to the active trace
100
+ ```
101
+
102
+ Declarative gating for web-hosted agents (the equivalent of .NET's `[RequireAgentPermission]`):
103
+
104
+ ```python
105
+ from fastapi import FastAPI, Depends
106
+ from tracengent.integrations.fastapi import install_tracengent, require_agent_permission
107
+
108
+ app = FastAPI()
109
+ install_tracengent(app, client)
110
+
111
+ @app.post("/refunds", dependencies=[Depends(require_agent_permission("payments:refund"))])
112
+ def refund(): ... # 403s automatically when the agent lacks the permission
113
+ ```
114
+
115
+ A Flask decorator (`tracengent.integrations.flask`) and a token-local coarse check
116
+ (`client.has_permission("flights:book")`, no round-trip) are also available.
117
+
118
+ **Tool governance (Phase 8).** Govern outbound tool calls the org doesn't own — block, hold for
119
+ approval, or redact before the request leaves:
120
+
121
+ ```python
122
+ from tracengent import governed_client
123
+
124
+ # Every request through this client is authorized via /v1/tool-authorize first.
125
+ tools = governed_client(client, base_url="https://api.stripe.com")
126
+ tools.post("/v1/refunds", json={"amount": 280, "card": "4242"})
127
+ # -> denied/held → raises; allowed → forwarded with any redactions applied.
128
+
129
+ # Non-HTTP tools (MCP tools/call, LLM function calls):
130
+ client.tools.guard_function("transfer_funds", {"to": "...", "amount": 50}, invoke=do_transfer)
131
+ ```
132
+
133
+ **Delegation / agent-to-agent (Phase 5).** Act under a delegated token and attenuate authority for a
134
+ downstream agent:
135
+
136
+ ```python
137
+ session = client.begin_delegated_task(delegated_token) # token granted out of band
138
+ session.authorize("flights:book", {"amount": 280}) # intersects base policy with the grant
139
+
140
+ # Mint a narrower child token to hand to another agent for an A2A hop:
141
+ child = session.attenuate_for_call("agt_hotel", ["hotels:book"], max_amount=500)
142
+ ```
143
+
144
+ Pure helpers are also exported directly:
145
+
146
+ ```python
147
+ from tracengent import permission_matches, Redaction, apply_redactions
148
+
149
+ permission_matches("flights:*", "flights:book") # True
150
+ apply_redactions('{"amount":100,"card":"4242"}', [Redaction("$.card", "mask")])
151
+ # -> '{"amount":100,"card":"***REDACTED***"}'
152
+ ```
153
+
154
+ ## Requirements
155
+
156
+ - Python 3.10+
157
+ - `httpx` (HTTP transport)
158
+
159
+ ## Development
160
+
161
+ ```bash
162
+ cd sdk/python
163
+ pip install -e ".[dev]"
164
+ pytest -q # unit tests
165
+ ruff check . # lint
166
+ mypy # type-check
167
+ ```
168
+
169
+ CI runs lint + type-check + tests on every push (see [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml), job `sdk-python`).
170
+
171
+ ## Environment variables
172
+
173
+ | Var | Meaning |
174
+ |---|---|
175
+ | `TRACENGENT_URL` | Base URL of the Tracengent API |
176
+ | `TRACENGENT_CLIENT_ID` | Agent OAuth client id (`key_...`) — preferred |
177
+ | `TRACENGENT_CLIENT_SECRET` | Agent OAuth client secret (`ags_...`) |
178
+ | `TRACENGENT_INGEST_KEY` | Legacy org ingest key (`agk_...`) — fallback |
179
+ | `TRACENGENT_AGENT_ID` | Logical agent key reported with events |
180
+ | `TRACENGENT_ENVIRONMENT` | Environment tag (default `production`) |
@@ -0,0 +1,144 @@
1
+ # Tracengent — Python SDK
2
+
3
+ First-party Python SDK for [Tracengent](../../phases/phase-10-multi-language-sdks.md): instrument and
4
+ govern agents written in Python with parity to the .NET SDK ([`src/Tracengent.Sdk`](../../src/Tracengent.Sdk/)).
5
+
6
+ > **Status: feature-complete (Phase 10).** Full parity with the .NET SDK across telemetry, traces,
7
+ > authorization, tool governance, and delegation. The reference implementation is the .NET SDK; the
8
+ > contract is [§3 of the phase doc](../../phases/phase-10-multi-language-sdks.md#3-the-shared-wire-contract-the-spec-all-five-sdks-implement).
9
+
10
+ ## Milestone status
11
+
12
+ | Milestone | Capability | State |
13
+ |---|---|---|
14
+ | **M1** | Options + validation, JWT/permission helpers, JSON redactor (pure logic) | ✅ done |
15
+ | **M2** | Event model, token provider, background dispatcher, `record` | ✅ done |
16
+ | **M3** | Decision traces + auto-tracking + ambient context + `authorize`/`ensure_authorized` | ✅ done |
17
+ | **M4** | `has_permission` (token-local) + framework bindings (FastAPI/Flask) | ✅ done |
18
+ | **M5** | Tool governance: egress gate, MCP/function gate, redaction | ✅ done |
19
+ | **M6** | Delegation: delegated session + attenuation (A2A) | ✅ done |
20
+ | **M7** | Packaging + sample + conformance + (gated) integration tests | ✅ done |
21
+
22
+ **Tests:** 152 unit + 7 conformance, 3 gated integration tests (`pytest -m integration`). The
23
+ conformance suite replays the shared [`wire_contract.json`](tests/conformance/wire_contract.json)
24
+ fixture and asserts the SDK's request shapes match the contract byte-for-byte (modulo volatile
25
+ ids/timestamps) — this same fixture is vendored into every Tracengent SDK. A runnable end-to-end
26
+ example lives in [`samples/demo_agent.py`](samples/demo_agent.py).
27
+
28
+ ## What works today (M1–M3)
29
+
30
+ ```python
31
+ from tracengent import TracengentClient, TracengentOptions, LlmCallResult, Money
32
+
33
+ opts = TracengentOptions.from_env(agent_id="travel-agent")
34
+ with TracengentClient(opts) as client:
35
+ # Open a decision trace — groups steps, tool calls and actions into one timeline.
36
+ with client.begin_trace("book a trip to Lisbon", actor_user_id="u_123") as trace:
37
+ trace.step("decided to search flights")
38
+
39
+ # Wrap tool calls / actions — timed and recorded automatically.
40
+ flights = trace.track_tool_call("search-flights", lambda: search(...))
41
+
42
+ # Govern an LLM call: reserve token budget before, reconcile actual usage after.
43
+ answer = trace.track_llm_call(
44
+ "summarize",
45
+ lambda: LlmCallResult(value=call_model(...), tokens=900, model="gpt"),
46
+ estimated_tokens=1000,
47
+ )
48
+
49
+ # Imperative authorization for a sensitive action (raises if denied).
50
+ # Send the spend's currency too — the platform converts it into the policy/budget
51
+ # currency before enforcing limits, so a USD charge is checked against a EUR cap.
52
+ client.ensure_authorized("payments:refund", {"amount": 280, "currency": "USD"})
53
+ trace.track_action("refund", lambda: do_refund(...), cost=Money.eur(280))
54
+ # leaving the trace flushes its completion; leaving the client flushes remaining events.
55
+ ```
56
+
57
+ Outbound HTTP can be auto-recorded as `tool_call` events by routing it through a tracked client:
58
+
59
+ ```python
60
+ from tracengent import tracked_client
61
+
62
+ http = tracked_client(client, base_url="https://api.example.com")
63
+ http.get("/things") # recorded as a tool_call, attached to the active trace
64
+ ```
65
+
66
+ Declarative gating for web-hosted agents (the equivalent of .NET's `[RequireAgentPermission]`):
67
+
68
+ ```python
69
+ from fastapi import FastAPI, Depends
70
+ from tracengent.integrations.fastapi import install_tracengent, require_agent_permission
71
+
72
+ app = FastAPI()
73
+ install_tracengent(app, client)
74
+
75
+ @app.post("/refunds", dependencies=[Depends(require_agent_permission("payments:refund"))])
76
+ def refund(): ... # 403s automatically when the agent lacks the permission
77
+ ```
78
+
79
+ A Flask decorator (`tracengent.integrations.flask`) and a token-local coarse check
80
+ (`client.has_permission("flights:book")`, no round-trip) are also available.
81
+
82
+ **Tool governance (Phase 8).** Govern outbound tool calls the org doesn't own — block, hold for
83
+ approval, or redact before the request leaves:
84
+
85
+ ```python
86
+ from tracengent import governed_client
87
+
88
+ # Every request through this client is authorized via /v1/tool-authorize first.
89
+ tools = governed_client(client, base_url="https://api.stripe.com")
90
+ tools.post("/v1/refunds", json={"amount": 280, "card": "4242"})
91
+ # -> denied/held → raises; allowed → forwarded with any redactions applied.
92
+
93
+ # Non-HTTP tools (MCP tools/call, LLM function calls):
94
+ client.tools.guard_function("transfer_funds", {"to": "...", "amount": 50}, invoke=do_transfer)
95
+ ```
96
+
97
+ **Delegation / agent-to-agent (Phase 5).** Act under a delegated token and attenuate authority for a
98
+ downstream agent:
99
+
100
+ ```python
101
+ session = client.begin_delegated_task(delegated_token) # token granted out of band
102
+ session.authorize("flights:book", {"amount": 280}) # intersects base policy with the grant
103
+
104
+ # Mint a narrower child token to hand to another agent for an A2A hop:
105
+ child = session.attenuate_for_call("agt_hotel", ["hotels:book"], max_amount=500)
106
+ ```
107
+
108
+ Pure helpers are also exported directly:
109
+
110
+ ```python
111
+ from tracengent import permission_matches, Redaction, apply_redactions
112
+
113
+ permission_matches("flights:*", "flights:book") # True
114
+ apply_redactions('{"amount":100,"card":"4242"}', [Redaction("$.card", "mask")])
115
+ # -> '{"amount":100,"card":"***REDACTED***"}'
116
+ ```
117
+
118
+ ## Requirements
119
+
120
+ - Python 3.10+
121
+ - `httpx` (HTTP transport)
122
+
123
+ ## Development
124
+
125
+ ```bash
126
+ cd sdk/python
127
+ pip install -e ".[dev]"
128
+ pytest -q # unit tests
129
+ ruff check . # lint
130
+ mypy # type-check
131
+ ```
132
+
133
+ CI runs lint + type-check + tests on every push (see [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml), job `sdk-python`).
134
+
135
+ ## Environment variables
136
+
137
+ | Var | Meaning |
138
+ |---|---|
139
+ | `TRACENGENT_URL` | Base URL of the Tracengent API |
140
+ | `TRACENGENT_CLIENT_ID` | Agent OAuth client id (`key_...`) — preferred |
141
+ | `TRACENGENT_CLIENT_SECRET` | Agent OAuth client secret (`ags_...`) |
142
+ | `TRACENGENT_INGEST_KEY` | Legacy org ingest key (`agk_...`) — fallback |
143
+ | `TRACENGENT_AGENT_ID` | Logical agent key reported with events |
144
+ | `TRACENGENT_ENVIRONMENT` | Environment tag (default `production`) |
@@ -0,0 +1,68 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "tracengent"
7
+ version = "0.1.0"
8
+ description = "First-party Python SDK for Tracengent: instrument and govern agents (events, decision traces, authorization, tool governance, delegation)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Besar Kutleshi" }]
13
+ keywords = ["agent", "observability", "governance", "authorization", "llm", "tracengent"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Operating System :: OS Independent",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = [
26
+ "httpx>=0.27",
27
+ "typing-extensions>=4.7; python_version < '3.11'",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ fastapi = ["fastapi>=0.110"]
32
+ flask = ["flask>=3.0"]
33
+ dev = [
34
+ "pytest>=8.0",
35
+ "pytest-asyncio>=0.23",
36
+ "ruff>=0.5",
37
+ "mypy>=1.10",
38
+ "fastapi>=0.110",
39
+ "flask>=3.0",
40
+ ]
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/besarkutleshi/agentgov"
44
+ Source = "https://github.com/besarkutleshi/agentgov"
45
+ Repository = "https://github.com/besarkutleshi/agentgov.git"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/tracengent"]
49
+
50
+ [tool.pytest.ini_options]
51
+ testpaths = ["tests"]
52
+ pythonpath = ["src"]
53
+ asyncio_mode = "auto"
54
+ markers = [
55
+ "integration: end-to-end tests against a live platform (require TRACENGENT_TEST_* env vars)",
56
+ ]
57
+
58
+ [tool.ruff]
59
+ line-length = 100
60
+ src = ["src", "tests"]
61
+
62
+ [tool.ruff.lint]
63
+ select = ["E", "F", "I", "UP", "B"]
64
+
65
+ [tool.mypy]
66
+ python_version = "3.10"
67
+ strict = true
68
+ files = ["src/tracengent"]
@@ -0,0 +1,66 @@
1
+ """A tiny end-to-end sample agent.
2
+
3
+ Run against a live Tracengent platform with credentials in the environment::
4
+
5
+ export TRACENGENT_URL=http://localhost:8080
6
+ export TRACENGENT_CLIENT_ID=key_...
7
+ export TRACENGENT_CLIENT_SECRET=ags_...
8
+ python samples/demo_agent.py
9
+
10
+ It opens a decision trace, records tool/LLM/action steps, authorizes a sensitive action, and
11
+ governs an external tool call — exercising the same surface the .NET demo agent does.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+
18
+ from tracengent import (
19
+ LlmCallResult,
20
+ Money,
21
+ NotAuthorizedException,
22
+ TracengentClient,
23
+ TracengentOptions,
24
+ governed_client,
25
+ )
26
+
27
+
28
+ def main() -> None:
29
+ opts = TracengentOptions.from_env(agent_id=os.getenv("TRACENGENT_AGENT_ID", "demo-agent"))
30
+
31
+ with TracengentClient(opts) as client:
32
+ with client.begin_trace("book a trip to Lisbon", actor_user_id="u_demo") as trace:
33
+ trace.step("decided to search for flights")
34
+
35
+ flights = trace.track_tool_call(
36
+ "search-flights", lambda: [{"id": "LIS-JFK", "price": 280}]
37
+ )
38
+ print(f"found {len(flights)} flight(s)")
39
+
40
+ summary = trace.track_llm_call(
41
+ "summarize-options",
42
+ lambda: LlmCallResult(value="One direct flight for €280.", tokens=120, model="gpt"),
43
+ estimated_tokens=200,
44
+ )
45
+ print(f"summary: {summary}")
46
+
47
+ try:
48
+ client.ensure_authorized("payments:refund", {"amount": 280})
49
+ trace.track_action("charge-card", lambda: None, cost=Money.eur(280))
50
+ print("charged €280")
51
+ except NotAuthorizedException as ex:
52
+ print(f"charge not authorized: {ex.reason}")
53
+
54
+ # Govern an external tool the org doesn't own (raises if denied/held).
55
+ try:
56
+ tools = governed_client(client, base_url="https://api.example.com")
57
+ tools.get("/availability")
58
+ tools.close()
59
+ except Exception as ex: # noqa: BLE001 — demo: surface governance outcomes, don't crash
60
+ print(f"external tool call governed: {ex}")
61
+
62
+ print("done; events flushed")
63
+
64
+
65
+ if __name__ == "__main__":
66
+ main()
@@ -0,0 +1,81 @@
1
+ """Tracengent — first-party Python SDK for agent observability & governance.
2
+
3
+ Through M3: configuration, the event model, OAuth token management, the background batching
4
+ dispatcher (``record``), decision traces, auto-tracking, and authorization (``authorize`` /
5
+ ``ensure_authorized``). Tool governance and delegation arrive in later milestones.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from ._permissions import permission_matches
11
+ from .authorization import AuthDecision, AuthObligations, NotAuthorizedException
12
+ from .client import TracengentClient, new_id
13
+ from .delegation import DelegatedSession, DelegationException
14
+ from .events import (
15
+ Actor,
16
+ AgentEvent,
17
+ EventStatuses,
18
+ EventTypes,
19
+ LlmCallResult,
20
+ Money,
21
+ Usage,
22
+ )
23
+ from .options import TracengentConfigError, TracengentOptions
24
+ from .redaction import MASK_TOKEN, Redaction
25
+ from .redaction import apply as apply_redactions
26
+ from .gateway import GatewayTransport, governed_client
27
+ from .token_provider import AgentTokenProvider
28
+ from .tool_gate import ToolGate, ToolGuardResult
29
+ from .tool_governance import (
30
+ ToolDecision,
31
+ ToolDeniedException,
32
+ ToolPendingApprovalException,
33
+ )
34
+ from .trace import AgentTrace
35
+ from .trace_context import current_trace
36
+ from .tracking import TrackingTransport, tracked_client
37
+
38
+ __version__ = "0.1.0"
39
+
40
+ __all__ = [
41
+ # config
42
+ "TracengentOptions",
43
+ "TracengentConfigError",
44
+ # client + telemetry
45
+ "TracengentClient",
46
+ "AgentEvent",
47
+ "Money",
48
+ "Usage",
49
+ "Actor",
50
+ "LlmCallResult",
51
+ "EventTypes",
52
+ "EventStatuses",
53
+ "AgentTokenProvider",
54
+ "new_id",
55
+ # traces
56
+ "AgentTrace",
57
+ "current_trace",
58
+ "TrackingTransport",
59
+ "tracked_client",
60
+ # authorization
61
+ "AuthDecision",
62
+ "AuthObligations",
63
+ "NotAuthorizedException",
64
+ # tool governance (Phase 8)
65
+ "ToolDecision",
66
+ "ToolDeniedException",
67
+ "ToolPendingApprovalException",
68
+ "ToolGate",
69
+ "ToolGuardResult",
70
+ "GatewayTransport",
71
+ "governed_client",
72
+ # delegation (Phase 5)
73
+ "DelegatedSession",
74
+ "DelegationException",
75
+ # helpers
76
+ "Redaction",
77
+ "apply_redactions",
78
+ "MASK_TOKEN",
79
+ "permission_matches",
80
+ "__version__",
81
+ ]
@@ -0,0 +1,41 @@
1
+ """Attaches auth to platform requests — mirrors the .NET ``AgentAuthHandler``.
2
+
3
+ A per-agent bearer token (client-credentials) when configured, otherwise the legacy org ingest key
4
+ header. On a 401 it refreshes the token once and retries. Returns ``None`` when the agent is disabled
5
+ so the caller can count a drop instead of spamming the server.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import httpx
11
+
12
+ from .options import TracengentOptions
13
+ from .token_provider import AgentTokenProvider
14
+
15
+
16
+ class AgentAuth:
17
+ def __init__(self, options: TracengentOptions, tokens: AgentTokenProvider) -> None:
18
+ self._o = options
19
+ self._tokens = tokens
20
+
21
+ def send(self, http: httpx.Client, request: httpx.Request) -> httpx.Response | None:
22
+ """Send ``request`` with auth applied. ``None`` means the agent is disabled (treat as a drop)."""
23
+ if self._o.uses_client_credentials:
24
+ token = self._tokens.get_token()
25
+ if token is None:
26
+ return None # disabled — short-circuit locally
27
+ request.headers["Authorization"] = f"Bearer {token}"
28
+ response = http.send(request)
29
+ if response.status_code == httpx.codes.UNAUTHORIZED:
30
+ self._tokens.invalidate()
31
+ retry = self._tokens.get_token()
32
+ if retry is not None:
33
+ response.close()
34
+ request.headers["Authorization"] = f"Bearer {retry}"
35
+ return http.send(request)
36
+ return response
37
+
38
+ # Legacy: org ingest key.
39
+ if self._o.ingest_key:
40
+ request.headers["X-Ingest-Key"] = self._o.ingest_key
41
+ return http.send(request)
@@ -0,0 +1,62 @@
1
+ """Decode claims from a JWT *payload* (base64url) without a JWT library or signature validation.
2
+
3
+ Mirrors the .NET ``JwtPayload`` helper and ``AgentTokenProvider.DecodePermissions``. This is used
4
+ only for coarse, local hints (e.g. ``has_permission``) and to read delegation claims — it never
5
+ validates the signature, so it must not be trusted for security decisions on its own.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import binascii
12
+ import json
13
+ from typing import Any
14
+
15
+
16
+ def _decode_payload(jwt: str | None) -> dict[str, Any] | None:
17
+ """Base64url-decode the payload (second) segment of a JWT and parse it as a JSON object."""
18
+ if not jwt:
19
+ return None
20
+ parts = jwt.split(".")
21
+ if len(parts) < 2:
22
+ return None
23
+ try:
24
+ segment = parts[1]
25
+ # Restore base64url -> base64 and pad to a multiple of 4 (Python is strict about padding).
26
+ padded = segment + "=" * (-len(segment) % 4)
27
+ raw = base64.urlsafe_b64decode(padded)
28
+ obj = json.loads(raw)
29
+ except (binascii.Error, ValueError, UnicodeDecodeError):
30
+ return None
31
+ return obj if isinstance(obj, dict) else None
32
+
33
+
34
+ def _as_str_list(value: Any) -> list[str]:
35
+ """Coerce a claim that may be an array of strings or a single string into a list[str]."""
36
+ if isinstance(value, list):
37
+ return [s for s in value if isinstance(s, str) and s]
38
+ if isinstance(value, str) and value:
39
+ return [value]
40
+ return []
41
+
42
+
43
+ def read_string(jwt: str | None, claim: str) -> str | None:
44
+ """Read a string claim from the payload; ``None`` if absent or not a string."""
45
+ payload = _decode_payload(jwt)
46
+ if payload is None:
47
+ return None
48
+ value = payload.get(claim)
49
+ return value if isinstance(value, str) else None
50
+
51
+
52
+ def read_string_array(jwt: str | None, claim: str) -> list[str]:
53
+ """Read a claim as a list of strings (accepts a single string or an array); empty if absent."""
54
+ payload = _decode_payload(jwt)
55
+ if payload is None:
56
+ return []
57
+ return _as_str_list(payload.get(claim))
58
+
59
+
60
+ def decode_permissions(jwt: str | None) -> list[str]:
61
+ """Decode the ``permissions`` claim (array or single string) for coarse local checks."""
62
+ return read_string_array(jwt, "permissions")