Skip to main content

VOYD

Ranking is not permission.

A vector index scores relevance. Nothing in an ordinary retrieval path is ever asked the other question — may this fact reach a prompt? — so a search answers with a confident score and no opinion about whether the hit was allowed to be there: an expired row the sweeper has not reached, a fact somebody revoked this morning, a vector from a model that was swapped.

Every other data path in a mature stack solved this years ago. HTTP has middleware. SQL has views and row-level security. The filesystem has permission bits the kernel checks whether or not you remembered to ask. Retrieval has a ranking function and a hope, so the missing answer gets reimplemented inside every query — a tenant filter here, an expire_at clause there, once per call site, per service, per language, forever.

The failure is silent in the worst possible direction. A missing authorization filter returns more documents, not fewer. It does not raise, it does not log, and on a retrieval workload it reads as better recall. There is no error to page on and no test that naturally fails.

Deleting the row does not close it either. MongoDB's TTL monitor runs about once a minute; an object-lifecycle rule runs about once a day. In that window the document is genuinely still on disk and is returned, correctly, as a well-scored result. A faster index does not help and a faster sweeper only narrows it.

deletion   a storage event      eventually consistent, by nature
refusal    a retrieval promise  immediate, by construction

VOYD is the second one, placed where it cannot be bypassed: the wire.


The part that surprised me

Once refusal is on the wire, it has to be pure — no database under it, no I/O, documents in and the admissible ones out. That reads like a tax. It turned out to be the most valuable property in the project, and it paid twice.

A policy change can be diffed before it ships. A pure function can be asked about a policy that is not deployed. voyd-plan asks it twice and fails a pull request that widens the boundary — naming the documents, and the role.

Your reranker cannot leak. Because the check is pure and cheap, it can run last, always, after arbitrary page-shaping code:

pure rules  →  your transform  →  every rule, terminally  →  the wire

Reranking, de-duplication and caching have always lived outside the boundary, and outside is what makes them dangerous — anything downstream of a filter can undo it. Usually by accident: merging a cached list, falling back to the unfiltered candidate pool because an empty page looked like a bug. So they move inside, and the guarantee survives:

A transform cannot widen what a read returns. Not because it was reviewed. Because the boundary is downstream of it.

There is a test that starts a real proxy with a policy whose transform exists solely to inject forgotten facts, and reads through it with a pymongo client that has never heard of this package. The transform is not disabled, sandboxed or reviewed. It runs. It returns the documents. They do not arrive.


No code

pip install git+https://github.com/ranfysvalle02/VOYD

Not on a package index. That is a decision for now rather than a gap waiting to close: nobody outside this repository has run this, and a name on an index invites an install from people who have not read Known gaps.

You get three commands — voyd-wire, voyd-plan, voyd-wire-health — and the vocabulary to write the file they read. There is nothing here for an application to import.

New here? quickstart.md is twenty minutes, and the first step costs nothing at all.

Declare the rules once, in a file that is not your application:

# voydfile.py
from voyd import guard, deadline, revocable, tenant

@guard("notes", on_delete="revoke")
class Notes:
    expire_at = deadline()
    forgotten = revocable()
    tenant_id = tenant()

Point the boundary at your database:

voyd-wire --config voydfile.py --target localhost:27017

Then change one connection string. That is the whole integration. No import is added to your application, no handle replaces a collection, no read path is rewritten, and nobody has to remember anything. A driver in any language — Python, Node, Compass, a notebook — cannot read a forgotten fact, because the boundary binds the connection and there is nothing to reach past.

Your credentials stay yours. The boundary forwards the handshake and then asks the deployment who authenticated — it does not authenticate on your behalf and does not believe a claim a client asserts, because a rule whose only input is the caller's own assertion is not an authorisation system. So a client connecting to the boundary carries exactly the credentials it would have carried to the cluster.

Reach it with directConnection=true, or run it with --advertise-self; without one of the two your driver reads the cluster's own host list and connects straight around it. Without --tls-cert it binds loopback only, which is a decision rather than a default: a plaintext boundary reachable from a network would carry in the clear every document it had just refused to serve.


What a read gets

An expired document and a revoked one stay exactly where they are. The boundary does not delete them and does not wait for anything to:

around the boundary   5 documents on disk
through the boundary  2 — the expired and the revoked are refused,
                          and the other tenant was never in scope

And the verb already in everybody's code gets the better meaning, if you ask for it. on_delete="revoke" turns a client's deleteOne into a mark:

db.notes.delete_one(...)  -> deleted_count=1   (the driver is satisfied)
reachable now             the row is gone from every read
rows on disk              unchanged — nothing was destroyed
the deadline              pulled in, so the reaper collects the bytes
                          on the schedule they already had

A credential you need out of prompts now and on disk for the investigation are contradictory requirements for DELETE and the same requirement for this. It is opt-in, because silently redefining delete for an operator who did not ask is the kind of surprise this project exists to remove — and declaring it without a revocable() field is refused at load, since there would be nowhere to record that the fact was forgotten.


The vocabulary

in a voydfile means
deadline() this field holds the instant after which the fact is gone
revocable() a mark an operator sets to forget it now, irreversibly
holdable() the reversible kind: a hypothesis, not an instruction
tenant() the tenant id — required in a reduction and checked per document
restricted_to(claim) admit only callers whose claim overlaps this audience
clearance(order, roles) an ordered ladder, the caller's rung read from their roles
subjects(key=...) an array whose elements are subjects in their own right
embedded_with(model) refuse a vector produced by a different model
budget(n) refuse once the prompt has no room left
distinct() refuse a repeat of content already on the page
sealed() ciphertext at rest, under a key scoped to the tenant
auto_embed(model) the server embeds this text; refuse a client's own vector

Beside them, and deliberately not one of them:

@transform(collection) shape the page — rerank, de-duplicate, annotate — inside the boundary, where it cannot widen a read
rerank(collection, ...) the built-in one: maximal marginal relevance, so ten chunks of one contract stop crowding out nine other contracts

Neither is an enforcement point. A rule is what the terminal pass re-asks and what voyd-plan can reason about; a transform gets no credit for filtering and no attestation. See ethos.md.

budget(n) and distinct() are set-relative: they refuse a document because of the other documents on the page, so the same document is admitted alone and refused in company. No index filter and no policy engine can express that — $vectorSearch decides each candidate before the page exists, and enforce(subject, object, action) has nowhere to put the rest of the set.

A rule is a protocol, not a list. Three members — reason, refuses(doc), clause() — and a stranger's rule is a first-class one, written directly in the policy file beside the builtins:

@guard("notes")
class Notes:
    expire_at = deadline()
    region    = Jurisdiction(allowed="eu")     # your dataclass, not ours

Anything in a class body that is half a rule raises at load, by name. The two ways to get the protocol wrong are to omit a member and to misspell one, and from the loader they look identical — a skipped rule would mean a boundary that comes up announcing what it refuses while serving everything the rule was written to stop.

A policy file that is wrong fails when it is loaded, not when somebody's query returns the wrong rows.


Two enforcement points, and only one of them is the guarantee

Each rule is pushed into the query where a query can express it, and re-checked per document on the way out.

The second one is the promise. A $vectorSearch hit does not pass through the collection query, so every pushed-down clause in this package is an optimisation — the database doing cheap work — and none of them is the boundary. The per-document check is what every read path ends at, including the search path, and it is pure: no database, no connection, no I/O. That purity is not an aesthetic; it is what lets the same check run inside a wire proxy at all.

The asymmetry only runs one way. A rule with no query half is slower. A rule with only a query half would be a hole.


Shaping the page, inside the boundary

A rule decides whether one document may reach a prompt. A transform decides what the page looks like — what order, which of them, annotated how. Reranking, de-duplication, a cache.

Those have always been a separate system, running after retrieval, and that is what makes them dangerous rather than useful. A reranker downstream of a filter can put back what the filter removed, and almost never on purpose: it merges a cached list, or falls back to the unfiltered candidate pool because an empty page looked like a bug, or reorders a list it was handed by reference.

# voydfile.py
rerank("notes", diversity=0.3)          # the one that ships

@transform("notes")                      # or your own, any code
class Diversify:
    name = "whatever you call it"

    def on_egress(self, docs, *, request):
        return my_reranker(docs)

rerank() is maximal marginal relevance. A vector index returns the most similar documents, which on a real corpus means the most similar documents to each other — ten chunks of one contract outranking one chunk each from ten contracts:

index order        a0 a1 a2 a3 a4 a5    one cluster, six deep
diversity=0.0      a0 a1 a2 a3 a4 a5    exactly the index's order
diversity=0.7      a0 b0 c0 a1 a2 a3    one from each, then the rest

Relevance comes from arrival order, not from a query vector: a wire boundary does not reliably have one, and every ranked page already carries the index's own judgement. That makes this rank-based MMR rather than the textbook form, which is a real technique and not the same one, so it is named as what it is.

It runs inside:

pure rules  →  transform  →  every rule, terminally  →  the wire

The terminal pass is not a stage you compose. It is the last thing that touches a document, always, and a transform cannot be placed after it because there is nowhere after it. Everything a transform returns — reordered, merged, restored from a cache, invented — is checked before it leaves. So a transform may reorder, drop, annotate and even inject, and a forgotten fact still cannot come out.

The first pass is a different argument: a transform is never shown a forgotten fact. That is defence in depth, not the guarantee — code that never receives a fact cannot mishandle it.

A transform is not an enforcement point and must never be written as one. Dropping a document for a security reason here duplicates a rule badly: the rule is what is re-asked terminally, and the rule is the half voyd-plan can tell you about before you ship it. A transform gets no credit and no attestation.

Two members, name and on_egress, checked at load the way a half-written rule is. A transform that raises is skipped and its input carried forward — the opposite of the decision for a rule, and for the opposite reason: a rule that fails open is a leak, a transform that fails open is impossible, and refusing a whole read over a broken reranker would be an outage caused by an optimisation.

Cumulative rules are held back to the terminal pass, so a budget() charges the page that is served rather than the one that was proposed and then reranked down.

A collection with no transforms runs the loop it always ran.

What it costs. One laptop, 1024-dimension vectors, measured rather than estimated:

                    page of 10   page of 50   page of 200
with numpy                ~2ms         ~2ms          ~9ms
pure Python               ~3ms        ~72ms      declines

NumPy is an accelerant, not a dependency — uv sync --extra rerank, or just have it in the environment running voyd-wire. Without it the same arithmetic runs in Python, and past a measured ceiling the transform declines and says so rather than adding a second to every read: an optimisation is not allowed to be the slow part.


What a policy change would let through

Because the per-document check is pure — a function of a document, a spec and a clock, with no database under it — it can be asked about a policy that is not deployed. voyd-plan asks it twice and reports the difference:

voyd-plan --current voydfile.py --proposed voydfile.new.py \
          --target $URI --database app --all
the boundary moves, in the admitting direction
  notes  tenant_removed
    reads were scoped by 'tenant_id' and no longer are: a read can
    return documents belonging to any tenant

documents that become reachable
  notes  +9 of 200 read
           9  were refused as revoked

200 documents, read
newly reachable: 9

The exit code is the product. 1 when something becomes reachable, 0 when nothing does — so a policy change that opens the boundary fails a pull request and says which documents and why. A change that closes it exits zero on purpose: a tool that blocked those would teach people to bypass it, and a read path that got shorter is visible to whoever it refused.

Without --target it reports the structural half only, which needs no cluster and no credentials: a @guard deleted, a tenant() dropped, a subjects() array that stops being subjects. Those are facts about the policy, so they are not weakened by a sample and do not disappear against an empty collection.

It refuses to answer three questions, by name. budget() and distinct() are set-relative — they refuse a document because of the other documents on the page, and a sample is not a page — so they are set aside and printed rather than evaluated one document at a time. clearance() and restricted_to() decide by who is asking, so they are planned only against a caller you name with --as. And the tenant is enforced by the handle against the scope a read is bound to rather than by a rule, so a plan reports that the boundary moved, not which rows crossed it. A plan that quietly folded any of the three into a total would be this project's own complaint, one level up.

--at runs the clock at another instant, which makes "what would this policy have refused last Tuesday?" a real question — a deadline and a hold are both functions of time. It moves the clock and not the data: nothing here keeps history, and the documents are the ones on disk now.

Before anything is installed

Two questions, two costs, and neither one is a deployment.

"Where in my code is the hole?" — costs nothing at all. The scanner is a single stdlib file in its own distribution with no dependencies and no import of this package, because the first thing a stranger runs must cost them nothing:

python3 scanner/voyd_scan app/ services/
notes: `expire_at` (declared by a write or index); `forgotten` (declared by a write or index)

25 of 25 read(s) against them do not:
  refuse.py:79   notes  (filter does not name `expire_at`, `forgotten`)
  embed.py:163   notes  (filter does not name `expire_at`, `forgotten`)

It works out what the mark is rather than being told: a TTL index names its own field, and beyond that, the fields most reads filter on are the convention — so a field most reads name and some do not is a deviation from your own spec. It gets stronger on larger codebases, because the majority that establishes the convention is bigger.

"How much is exposed right now?" — costs a read-only URI. --audit compares the cluster against no policy at all, so every document the policy would refuse is a document reachable today:

The same arithmetic, asked about the present. --audit compares the cluster against no policy at all, so every document the policy would refuse is a document reachable right now:

voyd-plan --audit --proposed voydfile.py --target $READONLY_URI --all
reachable today, and refused by this policy

  records  1000 of 4100 read are reachable now and would be refused
         812  are past an expire_at the TTL monitor has not reached
         188  carry an erasure mark and are still being served

No proxy, no sidecar, no connection string changed, nothing in a query path — a read-only credential and a batch job. That is only possible because the check is a pure function: a boundary whose enforcement lives inside a running process has nowhere to stand to ask this.

what it costs you what it tells you
nothing — one stdlib file which reads in your code can serve a forgotten fact
a read-only URI how many documents are reachable right now that should not be
one line in CI what a policy change would let through, before it merges
one connection string nothing gets out, in any language, ever again

In the place a policy is actually reviewed

A check nobody runs is a man page, so the plan ships as an action:

- uses: actions/checkout@v4
  with: { fetch-depth: 0 }   # the policy in force is read out of git
- uses: ranfysvalle02/VOYD@main   # or a tag, which pins both halves

That is the whole configuration. The action installs voyd from its own checkout rather than from PyPI, so pinning its ref pins the planner and the policy vocabulary together — a voydfile that loads in the check has to be one that loads at the boundary. It reads the voydfile at the pull request's base, compares it to the one on the branch, posts the plan as a comment it updates in place, and fails the job if the boundary opened. No cluster and no secret: the structural findings are facts about the policy, and they are the ones a reviewer is least equipped to see in a diff — a @guard deleted is one removed line.

Add target: ${{ secrets.MONGODB_URI }} and the findings get document counts beside them. fail-on-open: false makes it a reporter instead of a gate, which is a reasonable way to adopt it and a bad way to keep it.

A plan that could not be computed — a voydfile that will not load — exits 2 and fails the job before anything is posted, whatever fail-on-open says. A check that answered "nothing found" when it had not run is the one failure this could have that would be worse than not existing.

Whose access did it widen?

"The boundary widened" is the wrong granularity for a review. --as-each takes a table of callers and plans the same change once for each:

voyd-plan --current voydfile.py --proposed voydfile.new.py \
          --target $URI --database app --all --as-each roles.json
per caller
  caller          newly reachable   examined
  tier1-support               412        824
  analyst                       0        824
  clinician                     0        824

what tier1-support gains
  records       412  were refused as not_cleared

newly reachable: 412 (worst caller: tier1-support)

The headline is the worst caller, not the sum: one document reachable by four roles is one document that got out, not four, and a number that grew when somebody added a read-only role to the JSON would be measuring the role table. The data is read once however many callers there are — not an optimisation, a correctness requirement, since a second role handed an exhausted cursor reports zero and looks clean.

A plan somebody can still believe next year

--attest PATH writes the result, the SHA-256 of each policy file's contents, when it ran and what ran it. --sign env:NAME adds an HMAC over the whole envelope.

voyd-plan --verify plan.att.json --sign env:VOYD_ATTEST_KEY \
          --current voydfile.py --proposed voydfile.new.py
intact:  digest and signature both check out
current: the attested policies are the ones on disk
attested 2026-09-22T13:38:39+00:00: fails_open=True, newly reachable=412

Four outcomes, and they are deliberately four rather than a boolean:

intact nothing has changed since it was produced
edited the payload no longer matches its digest — caught with no key
wrong key or altered the digest was recomputed; the signature was not
stale intact, but a policy file has changed since. A different finding, and usually the more interesting one

What it proves. That the envelope has not changed since something holding the key produced it, and — via the digests — that it is a verdict about the exact bytes of those two files.

What it does not. That the plan ran against a real cluster, that the sample was representative, or who produced it. It is a symmetric MAC: anyone who can verify can forge. Evidence of integrity, never of origin. The key is taken as env:NAME or file:/path and never on the command line, because argv reaches the process table and the build log.


The server owns the encoding

An embedding is not a vector, it is a (vector, model) pair, and a vector without its model is an orphan. Comparing orphans does not fail — it returns a number between -1 and 1. Measured against a real embedding API, the same text, both 1024-wide, two generations of one vendor's model:

identical text, old model vs new       cosine -0.053
unrelated text, both on the new one    cosine +0.301

A model swap does not degrade ranking, it inverts it: unrelated text scores five times higher than the document you were looking for, with no error and no log. embedded_with(model) refuses the document. auto_embed(model) removes the way it comes to be wrong — the index holds text, mongot embeds it on write and embeds the query with the same model at read time, so nothing in your process ever computes a vector and nothing can drift from the index.

A client that sends its own queryVector at that collection has put the embedder back, through a driver that never read the policy file. The boundary refuses it by name and says which form works instead.


The erasure refusal cannot perform

Refusal binds this read path, so it has nothing to say about a replica, a snapshot, or the backup somebody restores next year — none of those run it. sealed() is how a field opts into being destroyable instead: ciphertext at rest under a key scoped to the tenant, so destroying the key makes every copy unreadable at once.

refusal          immediate            this application's read path
crypto erasure   ~60s (key cache)     every copy that exists anywhere
the TTL reaper   ~60s (measured)      this deployment's disk

Each one's window is the other's guarantee, which is the argument for having both rather than choosing. The boundary orders them accordingly: unreachable first, unreadable second.

sealed() without tenant() is refused at load. A scope with no name is one key for the whole collection, and destroying it to forget one subject would take every other tenant's rows with it.


Running the tests

uv sync --all-extras
uv run pytest -q -m 'not needs_mongo'   # the pure half: no database, ~0.2s
uv run --extra crypto pytest -q         # including the erasure path
docker compose up -d mongo              # a real mongod and a real mongot
uv run pytest -q                        # everything except `slow`
uv run pytest -q -m ""                  # everything

The pure files — the per-document check, every decision in the policy package, the planner that asks it about a policy nobody deployed, the wire codec, the custody rungs and the sweep below — need no database at all, and CI runs them in a step with no services:. If that step ever needs one, the boundary has stopped being pure.

The live tests drive a real voyd-wire in front of a real deployment with a plain pymongo client that has never heard of this package, because the claims are about queries and bytes and a mock would only prove the mock was filtered. Between them they cover refusal (expired, revoked, off-scope, and delete become a revocation, holding across every batch of a cursor); what a driver keeps when its connection string points here (sessions and causal consistency, transactions committed and aborted, several cursors interleaved on one socket, eight clients paging at once, and a real election caused with replSetStepDown and followed without a restart); the erasure a refusal cannot perform (a key destroyed, and the ciphertext read back by a reader that never held it); and a refusal travelling to what was made out of the fact, children marked before the source.

They need a cluster, which they find in VOYD_TEST_MONGO_URI, VOYD_MONGO_URI or a .env beside the compose file; with none of those they skip by name rather than passing quietly. The failover test needs the three-node set — docker compose up -d rs — because Atlas Local is a single node, so every routing assertion against it would pass by having nowhere else to go.

Every live test works in a database named voyd_test_<epoch>_<uuid> and drops it on the way out whatever happened — including the failures, since a cleanup that only runs on the happy path stops running exactly when a test starts leaving rows behind. The epoch in the name is what lets each run also sweep databases abandoned by an earlier one, bounded to a two-hour window so it can never reach a suite running concurrently on the same cluster.

The erasure tests destroy a real key and then try to read the ciphertext back, so they need pymongocrypt — the crypto extra, and nothing else. They deliberately do not need crypt_shared or mongocryptd: those are for automatic encryption, where the driver must analyse a command to learn which fields to encrypt, and this boundary has already parsed the command and already knows. Gating them on the stricter question is how they came to skip in CI, silently, on the claim this file leads with.

auto_embed against a cluster that really embeds is marked slow and reads VOYD_ATLAS_URI. Atlas Local registers no model, so it declines the declaration and falls back to an ordinary vector index — a different outcome, not a weaker one, and a test that accepted the fallback would assert the opposite of what it claims.


When you do not need this

  • One service, one language, one read path, one person maintaining it. The filter in your query is the same guarantee and one fewer process.
  • No deadlines, no revocation, no tenants — nothing to forget.
  • A retrieval path that does not reach a model, where a wrong row is a bug rather than a disclosure.

The case for a boundary starts at the second read path, and it compounds at the second language.


Known gaps

  • Nobody has run this but its author. Every number here comes from this repository's own benchmarks on one machine and one cluster.
  • A boundary is a process, so it is a hop, a thing to deploy, and a thing that can be down. It follows a failover and re-resolves on the server's own NotWritablePrimary, but it is not a replacement for your driver's topology.
  • A sealed read decrypts before it refuses, which costs the boundary its purity: holding keys makes it a custody holder.
  • No policy file can see an index created somewhere else. sealed() on a field that an Atlas index auto-embeds is a contradiction nothing here can detect.
  • voyd-plan --at replays the clock against today's documents. Answering it against the documents as they were needs history this package does not keep.
  • A change stream over a guarded collection is refused, not filtered: a change event is not a document, and the rules read top-level fields. See docs/why-not-native.md.
  • A transform cannot widen a read, and that is the only promise made about one. It can still be slow, wrong, or expensive, and nothing here bounds how long somebody's reranker runs inside the egress path.
  • There is no observe-only mode. voyd-wire enforces or it is not in the path; it cannot yet run alongside a read logging what it would have refused. voyd-plan --audit answers most of that question without a proxy at all, which is why this has not been urgent.

Reading this repository

This file is the reference. The others are not the same argument at different lengths — each answers a question this one does not:

quickstart.md four steps, smallest first; the first costs nothing at all
ethos.md what a policy file is for, and the four tests that keep logic out of it. Read this before writing rules
blog.md the story: every failure in this domain is disguised as its own opposite, including one in this project's own CI
docs/why-not-native.md change streams, $where, views, $$USER_ROLES, TTL, RBAC, Queryable Encryption — what each one gives you and where the line is
docs/cosine.md the embedding-model failure, reproducible without an API key, with its provenance and its limits
docs/ranking-is-not-permission.md the long-form design argument for putting a boundary on the wire
genius.md the commercial case: one property, three motions, and the corrections that keep it honest

MIT.

Metadata

Release files for voyd 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for voyd 0.1.0
File Size Uploaded
voyd-0.1.0.tar.gz 501.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for voyd 0.1.0
File Interpreter ABI Platform
voyd-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 794.5 kB

Release files / voyd-0.1.0.tar.gz

Download URL voyd-0.1.0.tar.gz
Size 501.5 kB
Tags Source
SHA-256 checksum
How to use checksums
701d3611e763c3128cb87c61c533b166828cdab876a093885168a50f236c1275
BLAKE2b-256 checksum
How to use checksums
8968244dc6fe7283e5bd848abbc2fa1abdcd4027d2350067610633d28909e900
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / voyd-0.1.0-py3-none-any.whl

Download URL voyd-0.1.0-py3-none-any.whl
Size 293.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
341fcdaded3ac2f74f092e701b693d4eac5dc0112710c10547d75cf3baf2edbc
BLAKE2b-256 checksum
How to use checksums
0c331639c20aca479e134e4726db5e8cfbf6e91231793e1852af36d5a3f91775
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.24 {"installer":{"name":"uv","version":"0.11.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

This release

0.1.0 This release

2 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