gurdy — Python SDK
The on-ramp. It marks the task boundary, obtains a transaction credential from the local TIS, and stamps it on calls that go to the proxy.
It is not an enforcement point, it never holds signing keys, and it contains no governance logic (§5.9, ADR-9). The proxy is the authority.
import gurdy
@gurdy.task(agent="orchestrator", human_actor="alice@example.com")
def handle(ticket):
client.post("/", content=tool_call) # enriched automatically
with gurdy.spawn(agent="fetcher", scope=narrower):
client.post("/", content=tool_call) # lineage: orchestrator > fetcher
Configuration is environment-first: GURDY_PROXY_URL and GURDY_TIS_SOCKET,
or gurdy.configure(proxy_url=..., tis_socket=...).
What it records, and what it cannot
Everything this SDK supplies is asserted identity — the agent's own claim about itself. The proxy separately records what it observed, and that is what policy evaluates on, so an agent cannot pick the identity it is authorized as.
The consequence worth internalising: the SDK never fabricates a claim. Outside a task context a call goes out unenriched and the ledger records an attested-coarse principal. That is a visible, readable gap. The alternative — inventing a credential so the record looks complete — would put an assertion nobody made into evidence a third party is meant to be able to trust.
Propagation: what is automatic, and what is not
This table is the honest part of this README. A silently lost lineage is worse than a documented gap, because the record would then attribute a sub-agent's action to nobody while the developer believes instrumentation is on.
| Boundary | Carries the context? | How |
|---|---|---|
| Ordinary calls inside a task | Yes | ContextVar |
asyncio.create_task, gather, TaskGroup |
Yes | asyncio copies the context at task creation |
threading.Thread |
No, by default | gurdy.bound(fn), or gurdy.instrument_threading() |
ThreadPoolExecutor |
No, by default | gurdy.ThreadPoolExecutor, or gurdy.instrument_threading() |
loop.run_in_executor |
No, by default | gurdy.bound(fn), or gurdy.instrument_threading() |
ProcessPoolExecutor, multiprocessing, subprocesses |
No — explicit only | gurdy.carrier() → send → gurdy.adopt() |
| A new process (gunicorn/uvicorn worker) | No | mark the task boundary again at the request entry point |
A generator that yields inside a with gurdy.task(...) |
Leaks — see below | put the with around the work, not around the yields |
Generators are the one sharp edge. Python does not give a generator its own context, so a binding set inside one stays visible to the caller between yields — where an unrelated task's calls would pick it up. Decorating a generator function is therefore refused outright, at decoration time:
@gurdy.task(agent="streamer") # raises TypeError on import
def stream(): yield ...
The decorator would otherwise acquire the credential, return the generator object and exit before a single line of the body ran, leaving the task unenriched while looking instrumented. Wrap the work instead of the yields.
Two of those deserve their reasoning stated.
Worker threads start empty on purpose. A pool thread that inherited
whatever its creator was doing would stamp one task's identity onto another
task's calls. gurdy.bound captures in the submitting thread, never in the
worker, and clears the binding when there is nothing to carry — so a reused
worker cannot leak the previous task's identity into this one's evidence.
Process pools are explicit and will stay that way. Auto-pickling a context across a fork is the kind of magic that fails silently. A worker that receives no carrier runs unenriched, which the export shows as attested-coarse rather than as somebody else's identity.
carried = gurdy.carrier() # in the parent
...
with gurdy.adopt(carried): # in the worker
do_work()
A carrier holds a live bearer credential for the whole transaction. Treat it as the secret it is; do not log it.
instrument_threading()
Agent frameworks fan tools out across thread pools you never constructed, so without patching, lineage stops inside somebody else's executor. With patching, this package is modifying classes it does not own — which breaks gevent and eventlet and interacts badly with anything that has already subclassed them.
Neither default is safe enough to choose on your behalf, so it is opt-in:
gurdy.instrument_threading() # patches threading.Thread and ThreadPoolExecutor
Sub-agents and scope
gurdy.spawn() derives a child credential eagerly, at the spawn site,
before the block runs. A scope that is not provably a narrowing of the parent's
raises gurdy.ScopeNarrowingRefused there and then.
It is never clamped and never falls back to the parent's token. Clamping would hand back a credential nobody asked for; falling back would record the child's actions under the parent's identity, which is a false lineage rather than a missing one. The algebra itself lives in the Go TIS — this SDK asks and reports the answer rather than keeping a second copy of a security rule that would drift.
Which calls get the credential
Exactly those whose scheme, host and port match the configured
GURDY_PROXY_URL, and whose path sits under its path. One origin, no patterns,
no host list.
Gurdy-Txn is a live bearer credential for the whole transaction and the proxy
is the only hop meant to consume it, so a permissive rule here is how a
mis-aimed call hands a credential to an upstream tool server that can then mint
assertions in the agent's name. The matching is done by parsing rather than by
string prefix, because a prefix is wrong in the dangerous direction:
http://gurdy.internal is a prefix of http://gurdy.internal.attacker.example/,
and configuring the proxy without a port is the ordinary case.
The hook is also authoritative, not additive: it removes the header on a request that should not carry it. That matters on redirects, where httpx copies headers onto the redirected request — an additive hook would follow a redirect off the proxy with the credential still attached.
When the TIS is down
The SDK enriches; it does not gate.
gurdy.task()andgurdy.spawn()log once and run the block unenriched. Your traffic is unaffected; the proxy still observes it and still records an attested-coarse principal. An on-ramp that makes an agent less reliable than not installing it gets uninstalled, and takes the evidence with it.gurdy.start_task()andgurdy.derive()raiseTISUnavailable. Their job is to return a credential and there is no degraded value to return.- A missing
GURDY_TIS_SOCKETraisesNotConfiguredeither way. That is a deployment error, it fails identically every run, and it fails the first time anyone tries it — unlike quietly enriching nothing, which lets a whole service look instrumented when it is not.
A spawn() that cannot derive degrades to no binding, never to the
parent's. Running a child under its parent's credential would record the
child's actions as the parent's, and a false lineage is worse than a missing
one.
Conformance
conformance_driver.py runs the shared corpus against this SDK. New behavior
lands in the corpus first, then here — that ordering is what keeps the Python
and TypeScript SDKs from drifting.
cd proxy && go build -o /tmp/gurdy-proxy ./cmd/gurdy-proxy && go build -o /tmp/gurdy-conform ./cmd/gurdy-conform
/tmp/gurdy-conform -cases ../conformance/cases -proxy /tmp/gurdy-proxy \
-driver ../sdk/python/run-driver.sh
Not built yet
gurdy.annotate(...)(§5.9). Deliberately absent rather than shipped as a no-op:/minttakesagent,human_actor,scopeandttland there is nowhere on the wire for a task description or a ticket ref to go. A function that accepted annotations and dropped them would be worse than its absence — the developer would believe the ledger had them. Needs a wire change first.- Framework hooks (LangChain, Claude agent SDK).
- Dev mode: the wheel does not yet bundle the Go core.
Metadata
Release files for gurdy 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| gurdy-0.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | Python 3 | none | Linux glibc 2.17+ x86-64 | Details |
| gurdy-0.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | Python 3 | none | Linux glibc 2.17+ ARM64 | Details |
| gurdy-0.1.1-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details |
| gurdy-0.1.1-py3-none-macosx_10_13_x86_64.whl | Python 3 | none | macOS 10.13+ x86-64 | Details |
| gurdy-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 28.9 MB
Release files / gurdy-0.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | gurdy-0.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 7.4 MB |
| Tags | Linux glibc 2.17+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
1aab280086491d378fed698f91a6ba9bdf26130d40387935bcb8e1b25660cd1d
|
|
BLAKE2b-256 checksum How to use checksums |
28556d50d702d4981e2b791791befef70dde9cebbdb6dd2863f26b405af66490
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / gurdy-0.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | gurdy-0.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 6.7 MB |
| Tags | Linux glibc 2.17+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
8803ac7fa7e36f955c4beb86a2f9adeba4169fb9701886b117145852ced9b34f
|
|
BLAKE2b-256 checksum How to use checksums |
fe25d8d5169c66773100ea0e333a860ddf0ac7477a4b45028c04b60a11c6170c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / gurdy-0.1.1-py3-none-macosx_11_0_arm64.whl
| Download URL | gurdy-0.1.1-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 7.1 MB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
88656e46e59cb04d00cabf515c3eeac9dc6314c927a67465dc234da99a8b5e55
|
|
BLAKE2b-256 checksum How to use checksums |
bee5b825e922117daa848c7e338850c9f013f2ef68bc1c7a9d067d82462775f6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / gurdy-0.1.1-py3-none-macosx_10_13_x86_64.whl
| Download URL | gurdy-0.1.1-py3-none-macosx_10_13_x86_64.whl |
|---|---|
| Size | 7.6 MB |
| Tags | Python 3 macOS 10.13+ x86-64 |
|
SHA-256 checksum How to use checksums |
881bda7530f786341b4378d8b61161126309e2402c316c6914b41d81cae57120
|
|
BLAKE2b-256 checksum How to use checksums |
36db4b34f3f4a6d4af62e6480501f647863f8000003e3c00890bd6374237eaea
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / gurdy-0.1.1-py3-none-any.whl
| Download URL | gurdy-0.1.1-py3-none-any.whl |
|---|---|
| Size | 26.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5c375181cf739e5ad6c5c31effbf60f7f0849ddffaa1fd9212818cadc5368280
|
|
BLAKE2b-256 checksum How to use checksums |
c2fdcaf4d7f8b676a9ef3c4c1f535a1ddfd128bc39a64e875d9624fe0c6cf50f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency log