agentrail-python 0.1.0__py3-none-any.whl

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,625 @@
1
+ Metadata-Version: 2.5
2
+ Name: agentrail-python
3
+ Version: 0.1.0
4
+ Summary: Agentrail for Python backends: HTTP, identity, milestones, logs and MCP for agent journeys, over OpenTelemetry.
5
+ Project-URL: Homepage, https://www.agentrail.co
6
+ Project-URL: Documentation, https://www.agentrail.co/docs/guides/journeys-python
7
+ Project-URL: Repository, https://github.com/agentrail-co/agentrail-python
8
+ Project-URL: Issues, https://github.com/agentrail-co/agentrail-python/issues
9
+ Author: Agentrail
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: agentrail,agents,asgi,mcp,observability,opentelemetry,wsgi
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: System :: Monitoring
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: opentelemetry-api<1.46,>=1.45.1
26
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http<1.46,>=1.45.1
27
+ Requires-Dist: opentelemetry-sdk<1.46,>=1.45.1
28
+ Provides-Extra: httpx
29
+ Requires-Dist: httpx<1,>=0.28; extra == 'httpx'
30
+ Provides-Extra: mcp
31
+ Requires-Dist: mcp<3,>=2.3; extra == 'mcp'
32
+ Provides-Extra: requests
33
+ Requires-Dist: requests<3,>=2.32; extra == 'requests'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # Agentrail for Python
37
+
38
+ [![CI](https://github.com/agentrail-co/agentrail-python/actions/workflows/ci.yml/badge.svg)](https://github.com/agentrail-co/agentrail-python/actions/workflows/ci.yml)
39
+ [![PyPI](https://img.shields.io/pypi/v/agentrail-python)](https://pypi.org/project/agentrail-python/)
40
+ [![Python](https://img.shields.io/pypi/pyversions/agentrail-python)](https://pypi.org/project/agentrail-python/)
41
+
42
+ `agentrail-python` records what happens inside your Python backend when an AI
43
+ agent (or a person) uses your product, so [Agentrail](https://www.agentrail.co)
44
+ can link it into that visitor's journey. It records:
45
+
46
+ - each incoming HTTP request and each outgoing call;
47
+ - who the request was for, once your application has authenticated them;
48
+ - the business steps you name (`checkout.confirm`, `delivery.requested`);
49
+ - structured logs, tied to the request that wrote them;
50
+ - MCP tool, resource and prompt calls.
51
+
52
+ It sends OpenTelemetry (OTLP over HTTP/protobuf) to the Agentrail collector.
53
+ By default it sends no request or response bodies, no arguments, no query
54
+ values and no raw identities.
55
+
56
+ Works with FastAPI, Starlette, Django, Flask and any other ASGI or WSGI
57
+ application. Requires **Python 3.10 or later**.
58
+
59
+ - [Install](#install)
60
+ - [Quickstart](#quickstart): [FastAPI](#fastapi), [Starlette and other ASGI](#starlette-and-other-asgi-apps),
61
+ [Django](#django), [Flask and other WSGI](#flask-and-other-wsgi-apps)
62
+ - [Outgoing calls](#outgoing-calls)
63
+ - [Identity and milestones](#identity-and-milestones)
64
+ - [Workflow and plugin evidence](#workflow-and-plugin-evidence)
65
+ - [Logs](#logs)
66
+ - [MCP servers](#mcp-servers)
67
+ - [An existing OpenTelemetry setup](#an-existing-opentelemetry-setup)
68
+ - [Approximate IP location](#approximate-ip-location)
69
+ - [Your own servers](#your-own-servers)
70
+ - [Delivery, shutdown and diagnostics](#delivery-shutdown-and-diagnostics)
71
+ - [What is sent, and what is not](#what-is-sent-and-what-is-not)
72
+ - [Node, Go and Python](#node-go-and-python)
73
+ - [Configuration reference](#configuration-reference)
74
+
75
+ ## Install
76
+
77
+ ```sh
78
+ pip install agentrail-python
79
+ # with the integrations you use:
80
+ pip install "agentrail-python[httpx,requests,mcp]"
81
+ ```
82
+
83
+ Then, in Agentrail, open your workspace's journey setup and create a backend
84
+ source. In the setup screen this source type is called **Node**; it is the
85
+ backend source for every backend SDK, Python included. You need two values,
86
+ and both stay on your server, never in a page:
87
+
88
+ - the **source secret**, which authenticates what your backend sends;
89
+ - the workspace **hash key**, which hashes identities before they leave your
90
+ process and signs trace context between your services.
91
+
92
+ ## Quickstart
93
+
94
+ Create the client once, when the process starts, before it serves requests:
95
+
96
+ ```python
97
+ # agentrail_setup.py
98
+ import os
99
+
100
+ import agentrail_python as ar
101
+
102
+ agentrail = ar.init(
103
+ ar.Options(
104
+ collector=ar.CollectorOptions(
105
+ endpoint="https://api.agentrail.co/ingest/otlp",
106
+ secret=os.environ["AGENTRAIL_SOURCE_SECRET"],
107
+ ),
108
+ hash_key=os.environ["AGENTRAIL_HASH_KEY"],
109
+ service_name="orders-api",
110
+ )
111
+ )
112
+ ```
113
+
114
+ `init` installs Agentrail's OpenTelemetry tracer provider, logger provider and
115
+ propagator for the process. If your process already has an OpenTelemetry
116
+ provider (Sentry, Datadog, your own), `init` refuses to replace it: use
117
+ [`create_processors`](#an-existing-opentelemetry-setup) instead. Calling `init`
118
+ again with the same `Options` object returns the same client.
119
+
120
+ The SDK reads no environment variables itself. Read and check your own
121
+ configuration before calling `init`; a missing value is a configuration error,
122
+ not a default.
123
+
124
+ ### FastAPI
125
+
126
+ ```python
127
+ from fastapi import Depends, FastAPI
128
+
129
+ import agentrail_python as ar
130
+ from agentrail_python.asgi import AgentrailASGIMiddleware
131
+ from agentrail_setup import agentrail
132
+ from my_app.auth import current_user # your own authentication
133
+
134
+ app = FastAPI()
135
+ app.add_middleware(AgentrailASGIMiddleware, client=agentrail)
136
+
137
+
138
+ @app.post("/orders/{order_id}/confirm")
139
+ async def confirm(order_id: str, user=Depends(current_user)):
140
+ agentrail.identify(ar.VerifiedIdentity(kind="user", id=user.id))
141
+ ...
142
+ ```
143
+
144
+ The route template (`/orders/{order_id}/confirm`) is recorded with each request.
145
+
146
+ ### Starlette and other ASGI apps
147
+
148
+ ```python
149
+ app = agentrail.asgi(app) # Starlette, Quart, Litestar, any ASGI 3 application
150
+ ```
151
+
152
+ ### Django
153
+
154
+ ```python
155
+ # asgi.py
156
+ from django.core.asgi import get_asgi_application
157
+ from agentrail_setup import agentrail
158
+
159
+ application = agentrail.asgi(get_asgi_application())
160
+
161
+ # or wsgi.py
162
+ from django.core.wsgi import get_wsgi_application
163
+ application = agentrail.wsgi(get_wsgi_application())
164
+ ```
165
+
166
+ ### Flask and other WSGI apps
167
+
168
+ ```python
169
+ from flask import Flask
170
+ from agentrail_setup import agentrail
171
+
172
+ app = Flask(__name__)
173
+ app.wsgi_app = agentrail.wsgi(app.wsgi_app)
174
+ ```
175
+
176
+ Install the middleware once, around the part of your application you want
177
+ recorded. If you install it after your authentication middleware, the
178
+ authentication step itself is not recorded. Each request becomes one entry
179
+ step; work it does (database calls, other OpenTelemetry spans) is recorded
180
+ beneath it.
181
+
182
+ **Trace context from callers.** A request carrying a valid signed handoff
183
+ from your own Agentrail edge or another backend SDK continues that trace. The
184
+ signature covers the method, the exact path and query as sent, the workspace
185
+ key ID, the trace parent and an issue time that is valid for 60 seconds, so
186
+ keep your servers' clocks in sync. A request without one (an agent, a browser,
187
+ anyone on the internet) starts a new trace; a `traceparent` it sends is kept
188
+ only as a link, never as the parent.
189
+
190
+ For WSGI, the exact target comes from `RAW_URI` or `REQUEST_URI`, which
191
+ gunicorn, uWSGI and Werkzeug set. Under a server that does not, the target is
192
+ rebuilt from the decoded path, and a signed handoff to a path with
193
+ percent-encoding may not verify.
194
+
195
+ ## Outgoing calls
196
+
197
+ Wrap the HTTP clients your handlers use, so their calls are recorded and,
198
+ where you allow it, carry signed trace context to your own services:
199
+
200
+ ```python
201
+ import httpx
202
+ import requests
203
+ from agentrail_python.httpx import async_transport, transport
204
+ from agentrail_python.requests import adapter
205
+
206
+ client = httpx.Client(transport=transport(agentrail))
207
+ async_client = httpx.AsyncClient(transport=async_transport(agentrail))
208
+
209
+ session = requests.Session()
210
+ session.mount("https://", adapter(agentrail))
211
+ session.mount("http://", adapter(agentrail))
212
+ ```
213
+
214
+ Signed context goes only to the exact origins you list:
215
+
216
+ ```python
217
+ ar.Options(..., outbound_origins=["https://inventory.internal", "http://billing:8080"])
218
+ ```
219
+
220
+ An origin is a lowercase scheme and host with a non-default port, nothing
221
+ else. The destination a call actually connects to decides, not a `Host`
222
+ header. Each redirect is checked again, so a redirect from an allowed origin
223
+ to another one is not signed. Managed calls never forward `traceparent`,
224
+ `tracestate`, `baggage`, `sentry-trace` or a handoff header they did not sign
225
+ themselves. Calls made outside a request or milestone pass through untouched.
226
+
227
+ `transport(agentrail, base)` wraps your own `httpx.HTTPTransport` (for proxies,
228
+ retries or certificates); `adapter(agentrail, **kwargs)` takes the same
229
+ arguments as `requests.adapters.HTTPAdapter`. A `requests` call's step ends
230
+ when `send` returns: with `stream=True` it does not wait for the body.
231
+
232
+ ## Identity and milestones
233
+
234
+ ```python
235
+ # Only after your application has authenticated this caller:
236
+ agentrail.identify(
237
+ ar.VerifiedIdentity(
238
+ kind="agent_registration", # user, agent_registration, agent_instance, oauth_client, api_key
239
+ id=authenticated_agent.id,
240
+ acting_user_id=authenticated_agent.user_id, # optional
241
+ )
242
+ )
243
+ ```
244
+
245
+ IDs are hashed with the workspace hash key before they leave the process.
246
+ Nothing a caller merely claims (a header, a user agent, MCP client details)
247
+ is ever treated as identity. `identify` works inside a request or milestone;
248
+ outside one it reports `identity_outside_request` and records nothing. The
249
+ first identity in a request stands: repeating it does nothing, and a
250
+ different one is recorded as its own `auth` step.
251
+
252
+ Name the business steps that matter:
253
+
254
+ ```python
255
+ step = ar.MilestoneContext(surface="custom", kind="checkout.confirm")
256
+
257
+ agentrail.milestone("checkout.confirm", step) # an instant step
258
+
259
+ with agentrail.start_milestone("checkout.confirm", step): # or `async with`
260
+ confirm_order()
261
+
262
+ @agentrail.with_milestone("checkout.confirm", step) # each call is one step
263
+ async def confirm_order(): ...
264
+ ```
265
+
266
+ A milestone's exception is recorded (its class name only) and raised
267
+ unchanged; its return value is untouched. Names and kinds are stable labels of
268
+ 1 to 256 characters, never IDs or arguments. Outside a request, a milestone
269
+ starts its own trace; an unrelated span that was current becomes a link.
270
+
271
+ The current request or milestone follows your code through `await`, asyncio
272
+ tasks and `asyncio.to_thread`. A thread you start yourself does not inherit
273
+ it; run its work with `contextvars.copy_context().run(...)`.
274
+
275
+ ## Workflow and plugin evidence
276
+
277
+ Once your application has authorized a business operation, pass its operation
278
+ reference to the milestone. Use a new reference for each attempt, and the same
279
+ one for its start, progress, plugin calls and outcome, across services that
280
+ share the workspace hash key and namespace:
281
+
282
+ ```python
283
+ agentrail.milestone(
284
+ "delivery.requested",
285
+ ar.MilestoneContext(
286
+ surface="custom",
287
+ kind="business.event",
288
+ performance=ar.PerformanceContext(
289
+ environment="production", # or "test"
290
+ operation=ar.AuthorizedOperation(
291
+ namespace_id=OPERATION_NAMESPACE_ID,
292
+ instance_ref=delivery_attempt.id,
293
+ role="start", # start, progress or terminal
294
+ ),
295
+ event_name="delivery.requested",
296
+ plugin=ar.PluginInvocation(binding_id=PLUGIN_BINDING_ID, invocation_id=str(uuid.uuid4())),
297
+ ),
298
+ ),
299
+ )
300
+ ```
301
+
302
+ Record a `terminal` milestone with your configured success event only after
303
+ your backend has confirmed the outcome. A tool's result or an HTTP 200 is not
304
+ business completion, and the MCP integration never declares one.
305
+
306
+ The operation reference never leaves your process: the SDK sends
307
+ HMAC-SHA256 under the workspace hash key of the JSON array
308
+ `["agentrail.operation", namespace_id, instance_ref]`, the same as Node and Go,
309
+ so steps from all three join. Supply `operation` with `event_name`, `operation`
310
+ with `plugin`, or `plugin` alone (plugin activity with no workflow credit).
311
+ Namespace and binding IDs come from Agentrail. Invalid evidence is left off the
312
+ step and reported as `invalid_performance`; the milestone and your code still
313
+ run. Evidence applies only to that milestone, never to its child spans.
314
+
315
+ ## Logs
316
+
317
+ ```python
318
+ import logging
319
+ from agentrail_python.logging import AgentrailHandler
320
+
321
+ logging.getLogger().addHandler(AgentrailHandler(agentrail))
322
+
323
+ log = logging.getLogger("orders")
324
+ log.error("payment declined", extra={"event.name": "checkout.failed"})
325
+ ```
326
+
327
+ By default a log sends its severity, event name, the exception's class name
328
+ (`error.type`) and the exact request and span it was written in. Message text
329
+ and other fields are sent only when you name them:
330
+
331
+ ```python
332
+ ar.Options(..., logs=ar.LogsOptions(include_fields=["message", "order_status"]))
333
+ ```
334
+
335
+ Only the named top-level fields with scalar values (text, numbers, booleans,
336
+ null) are sent, in the order named, as a JSON object of at most 4,096
337
+ characters; a long value is shortened to fit, and a field that cannot fit is
338
+ left out. `message` is the formatted message. Never name fields holding
339
+ credentials, identities or customer content. Set
340
+ `LogsOptions(exclude_uncorrelated=True)` to send only logs written inside a
341
+ request or milestone.
342
+
343
+ You can also record a log directly, or add `processors.log_processor` to your
344
+ own OpenTelemetry `LoggerProvider`:
345
+
346
+ ```python
347
+ agentrail.log(ar.StructuredLog(severity_number=17, error_type="PaymentDeclined", event_name="checkout.failed"))
348
+ ```
349
+
350
+ Logs never prove anything about a journey on their own: they describe the step
351
+ they belong to.
352
+
353
+ ## MCP servers
354
+
355
+ For servers built with the official [`mcp`](https://pypi.org/project/mcp/)
356
+ SDK (2.x, `MCPServer`):
357
+
358
+ ```python
359
+ from mcp.server.mcpserver import MCPServer
360
+ from agentrail_python.mcp import mcp_capture
361
+
362
+ capture = mcp_capture(agentrail)
363
+ server = MCPServer("orders", middleware=[capture.middleware])
364
+
365
+
366
+ def checkout(item: str) -> str: ...
367
+
368
+
369
+ server.tool(name="checkout")(capture.tool("checkout", checkout))
370
+ server.resource("catalog://items", name="catalog")(capture.resource("catalog", read_catalog))
371
+ server.prompt(name="greet")(capture.prompt("greet", greet))
372
+ ```
373
+
374
+ Each call becomes an MCP step named `mcp.tools/call:checkout`. Behind the
375
+ ASGI middleware, `agentrail.asgi(server.streamable_http_app(stateless_http=True))`,
376
+ each call nests under its HTTP request (this is the tested setup). Where the
377
+ call runs outside a recorded request (stdio, or a session task the request did
378
+ not start), it starts its own trace. The middleware adds the calling client's name and version and the
379
+ protocol version; that is what the client says about itself, never an
380
+ identity. A result with `isError` marks the step failed and is returned
381
+ unchanged. Pass the name you registered, never a requested resource URI.
382
+ Arguments, results, prompt text and resource URIs are never recorded.
383
+
384
+ The MCP SDK opens its own OpenTelemetry span for every message. Inside a
385
+ recorded request it is kept as supporting work, not as a second entry.
386
+
387
+ ## An existing OpenTelemetry setup
388
+
389
+ If your process already owns an OpenTelemetry tracer provider, build
390
+ Agentrail's pieces into it when you construct it. A running provider cannot
391
+ recover spans its sampler already dropped.
392
+
393
+ ```python
394
+ from opentelemetry import propagate, trace
395
+ from opentelemetry.sdk.trace import TracerProvider
396
+ from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased
397
+
398
+ processors = ar.create_processors(
399
+ ar.Options(..., propagator=propagate.get_global_textmap())
400
+ )
401
+ provider = TracerProvider(
402
+ sampler=processors.sampler(ParentBased(TraceIdRatioBased(0.1))),
403
+ resource=your_resource,
404
+ )
405
+ provider.add_span_processor(your_processor)
406
+ provider.add_span_processor(processors.span_processor)
407
+ trace.set_tracer_provider(provider)
408
+ propagate.set_global_textmap(processors.propagator)
409
+ your_logger_provider.add_log_record_processor(processors.log_processor)
410
+
411
+ agentrail = processors.bind(provider) # once
412
+ ```
413
+
414
+ `create_processors` registers nothing globally, and Agentrail never changes
415
+ what your other processors and exporters receive. The sampler wrapper keeps
416
+ spans inside a recorded request that your sampler would drop as
417
+ **record-only**, with your sampled flag unchanged: exporters that send only
418
+ sampled spans behave as before. Pass your existing propagator as
419
+ `propagator` and register the returned `processors.propagator`: it hands
420
+ unmanaged work to yours, and keeps unsigned headers out of recorded requests.
421
+ Use Agentrail's ASGI or WSGI middleware for incoming requests instead of
422
+ another incoming HTTP instrumentation, so each request has one entry.
423
+ `agentrail.shutdown()` never shuts down your provider.
424
+
425
+ ## Approximate IP location
426
+
427
+ ```python
428
+ def location(request: ar.IncomingRequest) -> ar.IPLocation | None:
429
+ country = request.headers.get("cf-ipcountry") # trust it only behind Cloudflare
430
+ if not country or not country.isalpha() or country == "XX":
431
+ return None
432
+ return ar.IPLocation(country=country, source="cloudflare")
433
+
434
+ ar.Options(..., incoming_location=location)
435
+ ```
436
+
437
+ The hook runs once per recorded request, before your handler, and must return
438
+ quickly: read trusted platform headers or a local database loaded at startup,
439
+ never call a network service. Resolve the client address with your own proxy
440
+ configuration and never trust an arbitrary forwarded header. `country` is a
441
+ two-letter uppercase code; `region` and `city` are shortened to 128
442
+ characters; `source` is `cloudflare`, `vercel`, `dbip` or `source` (another
443
+ trusted provider). Return `None` when there is no location. A hook that raises
444
+ or returns an invalid location is reported as `location_failed`, and the
445
+ request carries on without one. The location is sent on the request's entry
446
+ and its milestones, never the address itself. An approximate location
447
+ describes a network exit, which may be a VPN, a proxy or an agent's host, not
448
+ the person.
449
+
450
+ ## Your own servers
451
+
452
+ Calls between your own services are not visitors or agents. Leave them out
453
+ with `ignore_incoming_request`, and check something only your servers can
454
+ send, such as a shared secret: a user agent or an address is what any caller
455
+ can claim.
456
+
457
+ ```python
458
+ import hmac
459
+ import os
460
+
461
+ internal_secret = os.environ["INTERNAL_SECRET"]
462
+ if not internal_secret:
463
+ raise RuntimeError("INTERNAL_SECRET is empty")
464
+
465
+ ar.Options(
466
+ ...,
467
+ ignore_incoming_request=lambda request: hmac.compare_digest(
468
+ request.headers.get("x-my-internal", "").encode(), internal_secret.encode()
469
+ ),
470
+ )
471
+ ```
472
+
473
+ Check the secret is set before starting: if it were empty, every request
474
+ without the header would match and nothing would be recorded. Compare bytes:
475
+ `hmac.compare_digest` raises for text that is not ASCII. A hook that raises is
476
+ reported as `ignore_failed`, and that request is recorded like any other. Use the same
477
+ hook for health checks. If the collector itself runs in this process, exclude
478
+ its ingestion routes here too.
479
+
480
+ ## Delivery, shutdown and diagnostics
481
+
482
+ Records wait in a bounded in-memory queue per signal (traces, logs) and are
483
+ sent by one background thread each:
484
+
485
+ | Setting (`QueueOptions`) | Default |
486
+ | --- | --- |
487
+ | `max_records` (including the batch being sent) | 2048 |
488
+ | `batch_size` | 128 |
489
+ | `scheduled_delay` | 1 s |
490
+ | `export_timeout` | 5 s |
491
+ | `flush_timeout`, `shutdown_timeout` | 10 s |
492
+
493
+ One batch is sent at a time. The OTLP exporter retries retryable answers
494
+ within `export_timeout`; a refusal, or a batch still failing at the deadline,
495
+ is dropped and reported. Nothing is written to disk.
496
+
497
+ ```python
498
+ agentrail.flush() # waits for attempts on what was recorded so far
499
+ agentrail.shutdown() # drains once and stops; terminal and idempotent
500
+ agentrail.diagnostics() # accepted, exported, dropped, pending per signal
501
+ ```
502
+
503
+ Stop accepting requests and let them finish, then call `shutdown()`
504
+ (`await asyncio.to_thread(agentrail.shutdown)` from async code). With `init`,
505
+ the providers also drain when the interpreter exits. A finished `flush` means
506
+ the attempts ended; check `diagnostics()` and `on_error` for anything lost.
507
+
508
+ `on_error` receives a `CaptureError` with a `code`, `signal` and `count`, and
509
+ never any customer data:
510
+
511
+ | Code | Meaning |
512
+ | --- | --- |
513
+ | `queue_full`, `closed` | A record was refused: the queue was full, or after `shutdown` |
514
+ | `export_failed`, `deadline` | A batch was refused or failed, or a flush or shutdown deadline passed |
515
+ | `handoff_refused` | A signed handoff did not verify (or one could not be signed) |
516
+ | `entry_not_recording` | A sampler dropped an entry that should have been recorded |
517
+ | `location_failed` | The location hook raised or returned an invalid location |
518
+ | `ignore_failed` | `ignore_incoming_request` raised; the request was recorded |
519
+ | `identity_outside_request` | `identify` was called outside a request or milestone |
520
+ | `invalid_performance` | Workflow or plugin evidence was invalid and left off |
521
+
522
+ Without `on_error`, these go to standard error. The callback runs without
523
+ capture, so logging from it is never recorded. Capture problems never change
524
+ your application's response. Invalid options, identities and milestone labels
525
+ are programming errors and raise `ConfigurationError`.
526
+
527
+ ## What is sent, and what is not
528
+
529
+ Sent, from an allowlist:
530
+
531
+ - for each request and outgoing call: the method, the URL without its query
532
+ (except valid Agentrail `h` markers), the route template, the status code,
533
+ the user agent and the error class name;
534
+ - for supporting spans: only their place in the trace, their timing, status
535
+ and error class name; their names become `backend.operation`;
536
+ - hashed identities, milestone names and kinds, workflow and plugin evidence,
537
+ MCP client details;
538
+ - the resource: service name, instance ID, SDK name and version, and the hash
539
+ key's ID (never the key).
540
+
541
+ Never sent: request or response bodies, query values, headers, cookies,
542
+ arguments, results, exception messages, span events, other span attributes,
543
+ trace state, baggage, raw identities, raw operation references, prompt text or
544
+ resource URIs. Log text only as described in [Logs](#logs). Your service
545
+ names, routes, milestone names and the fields you select are yours to keep
546
+ free of sensitive data.
547
+
548
+ ## Node, Go and Python
549
+
550
+ The three backend SDKs send the same events and share their signing, hashing
551
+ and marker rules, checked against the same test vectors, so services in any
552
+ mix of the three link into one journey.
553
+
554
+ | | Node (`agentrail-node`) | Go (`agentrail-go`) | Python (`agentrail-python`) |
555
+ | --- | --- | --- | --- |
556
+ | Incoming HTTP | Node HTTP instrumentation | `Middleware` | ASGI and WSGI middleware |
557
+ | Outgoing HTTP | `http`, `fetch` | `Transport` | `httpx`, `requests` |
558
+ | Signed handoffs, allowed origins | Yes | Yes | Yes |
559
+ | Verified identity | `identify` | `Identify` | `identify` |
560
+ | Milestones | `milestone` | `Milestone`, `StartMilestone`, `WithMilestone` | `milestone`, `start_milestone`, `with_milestone` |
561
+ | Workflow and plugin evidence | Yes | Yes | Yes |
562
+ | Logs | Pino, OpenTelemetry | `slog`, OpenTelemetry | `logging`, OpenTelemetry |
563
+ | MCP | `@modelcontextprotocol/server` | official Go SDK | official `mcp` SDK |
564
+ | Existing OpenTelemetry | `createProcessors` | `NewProcessors` + `Bind` | `create_processors` + `bind` |
565
+ | Standalone startup | `init` (installs globals) | `New` (no globals) | `init` (installs globals) |
566
+ | IP location, ignored requests | Yes | Yes | Yes |
567
+ | Queue defaults | 2048 / 128 / 1 s / 5 s / 10 s | same | same |
568
+
569
+ For the edge and the page itself, use the JavaScript packages
570
+ [`agentrail-edge` and `agentrail-browser`](https://github.com/agentrail-co/agentrail-js).
571
+
572
+ ## Configuration reference
573
+
574
+ `Options` (all keyword arguments):
575
+
576
+ | Option | |
577
+ | --- | --- |
578
+ | `collector` | `CollectorOptions(endpoint=..., secret=...)`: the OTLP base URL (`/v1/traces` and `/v1/logs` are added) and the backend source secret |
579
+ | `hash_key` | The workspace hash key |
580
+ | `service_name` | A stable, non-sensitive service name |
581
+ | `service_instance_id` | Optional; a random ID per process by default |
582
+ | `outbound_origins` | Exact origins that receive signed trace context; none by default |
583
+ | `ignore_incoming_request` | `(IncomingRequest) -> bool`; True leaves the request unrecorded |
584
+ | `incoming_location` | `(IncomingRequest) -> IPLocation \| None` |
585
+ | `logs` | `LogsOptions(include_fields=..., exclude_uncorrelated=...)` |
586
+ | `queue` | `QueueOptions(...)`, see above |
587
+ | `propagator` | Your existing propagator, for unmanaged work |
588
+ | `on_error` | `(CaptureError) -> None` |
589
+
590
+ `IncomingRequest` has `method`, `target` (path and query as sent), `scheme`,
591
+ `host`, `headers` (lowercase names), `client` (the peer address the server
592
+ saw) and `raw` (the ASGI scope or WSGI environ).
593
+
594
+ Before 1.0, a minor version (0.1 to 0.2) may change the API.
595
+
596
+ ## Development
597
+
598
+ ```sh
599
+ uv sync
600
+ uv run ruff format --check src tests integration scripts
601
+ uv run ruff check src tests integration scripts
602
+ uv run pyright
603
+ uv run pytest
604
+ scripts/check-consumer.sh # the built wheel, FastAPI and Flask, in a fresh venv
605
+ ARGUS_DIR=/path/to/argus integration/run.sh # the real collector, with Docker
606
+ ```
607
+
608
+ The receiver gate needs a checkout of Agentrail's collector with its
609
+ dependencies installed and Docker running; see
610
+ [integration/README.md](integration/README.md). Shared test vectors and their
611
+ provenance are in [testdata/](testdata/README.md).
612
+
613
+ ## Releasing
614
+
615
+ A `v<version>` tag publishes the package to PyPI
616
+ (`.github/workflows/release.yml`). The tag must match `VERSION` in
617
+ `src/agentrail_python/_version.py`. Only the repository's admins can push a
618
+ `v*` tag, and the publish job waits for approval in the `pypi` environment.
619
+ PyPI trusts the workflow (trusted publishing), so no token is stored, and each
620
+ file gets a signed attestation. A run that uploaded some files and then failed
621
+ can be run again: it skips what PyPI already has.
622
+
623
+ ## Licence
624
+
625
+ [Apache-2.0](LICENSE).
@@ -0,0 +1,25 @@
1
+ agentrail_python/__init__.py,sha256=veqY5PqTyXPxqEq-nvEJVO_qTFiJSw2D4kj4K7PZVUk,1498
2
+ agentrail_python/_client.py,sha256=0gJFeKB7wJJHqvLqPehparPG64JkIplPskv229GrFbY,14443
3
+ agentrail_python/_handoff.py,sha256=d33yP8alwexhopgwMkyK1VFq_IJ_dU-12qxmkKUmNQc,6983
4
+ agentrail_python/_init.py,sha256=7F7Z_U-eC2not3C7r4CfSeUc7bMj-L09mJ52YFucYDg,2945
5
+ agentrail_python/_options.py,sha256=2Db-ZlAVeoHF6_ElsYNwVLoVNahruYr3kXeRWBnEbas,10102
6
+ agentrail_python/_outbound.py,sha256=IGGnqs1yTAoMLXuVr7xdxNNy2UmC-_mopx1UzLEgz4o,2824
7
+ agentrail_python/_performance.py,sha256=d42OslVWBLCXrE4VmUqrQ-V2K_7ntSVOQDOVj5hhx9Q,3228
8
+ agentrail_python/_privacy.py,sha256=Bt3gx2gyOhyDj7MirC_-hsgs3zjXD6zyUHWWsl1jyZ0,9933
9
+ agentrail_python/_processors.py,sha256=Gm0MTYOFzmzTGUV8yExYeDTtAP2oAARjyQdSZu7Epa8,10636
10
+ agentrail_python/_queue.py,sha256=Uy7HFBUSiEJLrZ8r-C73lNAKUcJVuYPDyaJKO311_x8,9059
11
+ agentrail_python/_runtime.py,sha256=uDZSG__fqFkT_sE24qIaAuCZ7_68-XY5VU9w72fIK_A,4141
12
+ agentrail_python/_suppression.py,sha256=v0yZngosDp-4Cg99H4DybJeu5UEcX4-aN1cezPVjd24,1045
13
+ agentrail_python/_text.py,sha256=VEPWuHFU9O0tovScuG9wLNFc1oRwcnk2Egx7diTfdVc,1681
14
+ agentrail_python/_version.py,sha256=tKaj8Z1fcaq9R1kXwA6xQEPixv4xDlcK0cMPFOeUw40,94
15
+ agentrail_python/asgi.py,sha256=nepsVFPFpx8ZRqs3EKhQbzFBZ5xm3D9KyyXgItdflwg,4114
16
+ agentrail_python/httpx.py,sha256=RezUnEf0xmRyq8WQ_IbYkgNhVaBE93Np-Vdz4Bp97IQ,5347
17
+ agentrail_python/logging.py,sha256=BWReSqXCSHwN01rU5uUWaFPMmNyReN3xSLLOMjCui9k,2623
18
+ agentrail_python/mcp.py,sha256=nv7wMTErEbkmMMqk5U4HJ3DoKe6DiCsBDFPcQ8YnL28,4582
19
+ agentrail_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ agentrail_python/requests.py,sha256=EODkzZB76PeaVM_-TllFaTCFNoKzJmBbJXelT_Dorfc,2274
21
+ agentrail_python/wsgi.py,sha256=hTLQPI_kZcEmoQqfF1bJu28OVJnQzo8valgPsNPcJek,5715
22
+ agentrail_python-0.1.0.dist-info/METADATA,sha256=n1O--Nl71r16-XNTDcP8lC9Slwo3A0OMZhRqehDaOF4,25995
23
+ agentrail_python-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
24
+ agentrail_python-0.1.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
25
+ agentrail_python-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any