Skip to main content

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() and gurdy.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() and gurdy.derive() raise TISUnavailable. Their job is to return a credential and there is no degraded value to return.
  • A missing GURDY_TIS_SOCKET raises NotConfigured either 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: /mint takes agent, human_actor, scope and ttl and 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)

Table of built distributions (wheels) for gurdy 0.1.1
File
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 log

Release 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 log

Release 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 log

Release 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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.1 This release

5 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page