Repository navigation
Releases: docket-dev-org/docket
Release list
Docket 0.2.3
2026-10-20
Fixed
- The
uvinstall path asks the index rather than its own cache.uvkeeps an index
cache with a different lifetime from PyPI's, and the packaging guard reads PyPI's. Measured
on 0.2.2: the simple index served the file immediately whileuv tool install docket-ai==0.2.2resolved no version of docket-ai==0.2.2 until--refreshwas passed.
The test now passes it. This fix is not provable by mutation here: removing both flags
leaves the test green, because the cache has since populated and the failure window has
closed. The evidence is the direct measurement taken in the window.
No change to the shipped code: git diff 0.2.2..0.2.3 -- src/ is empty.
Docket 0.2.1
2026-10-20
Fixed
-
The release gate installs the package under test.
.github/workflows/release.ymlran
the whole suite against a tree whose package was never installed, sotests/test_action.py
andtests/test_packaging.pyfailed withdocket: command not found— exit 127, which is
not a verdict about the tag and reads like one. 0.2.0 was published with a release check
that had never passed. This is the same omission the CItestsjob had, fixed there first
and missed here. -
The packaging tests measure the version the tree declares.
tests/test_packaging.py
pinned the version as a literal, and its pipx test resolved the binary with
shutil.which("docket")after a successful install — which finds whicheverdocketis
already onPATH, not the one pipx just made. It passed on every clean machine and failed
on the release candidate. The install is now directed into a throwawayPIPX_HOMEand
PIPX_BIN_DIR, and the binary is taken from that directory.
Added
tests/test_release_workflow.py, which holds the release gate to the same standard as
the push gate: the package is installed and installed before the suite runs, the release
job's test dependencies are the CItestsjob's, the gate runs thetestsdirectory
rather than a subset, it prints its counts, a tag with no changelog section fails the job,
the workflow uploads nothing to PyPI and carries no PyPI credential, and the GitHub
release uses the run-scoped token. Eight mutations, all caught.
No change to the shipped code: git diff 0.2.0..0.2.1 -- src/ is empty, which is why 0.2.1
exists — so that the newest tag, the newest release and a green gate name the same commit.
Docket 0.2.0
2026-10-20
-
Retention of the provider responses the boundary saw. Every
model.responseevent now
carriesresponse_sha256over the exact bytes returned to the agent, and
[evidence] record_responsesadditionally keeps a bounded copy of the body in aresponses
sub-directory of the run directory, named by its own digest.docket verifyre-hashes every
retained body under the newretained-responsescheck, so one edited or deleted after the run
fails. The reason this exists is a provider Docket cannot read: its usage block is in a fourth
shape, the token counts are estimated from the response text, and that text used to be
discarded, so the estimate could never be checked. A truncated body carries both digests and
says which is which; the verifier checks the bytes that were written and never claims to have
checked bytes it was not given.docket report --redactdrops the retention fields rather
than leaving a pointer to a body the copy does not carry, and records how many under
retained_responses_not_carried. Retention is off by default: a response body can contain
the prompt, a tool result, or a credential, so the digest is the default and the body is the
opt-in. Seedocs/evidence-retention.md. -
Published to PyPI as
docket-ai0.1.0.pip install docket-aiinstalls the same
package, and the import, the console script and the command are all stilldocket. The
distribution name differs because PyPI administratively prohibits the bare namedocket:
the JSON API answers 404 for it, andtwine uploadstill refuses it with "The name
'docket' isn't allowed". PyPI keeps that list to prevent dependency-confusion attacks, and
the only way off it is a PEP 541 request reviewed by the PyPI administrators.PUBLISHING.md
records the reason, the one place the name is written, and the request process. -
PUBLISHING.md, the maintainer runbook for shipping a release. It records what the
release gate does, why the PyPI upload stays a manualtwine uploadrather than a step in
the workflow (a PyPI token can publish under the project's name forever and cannot be
scoped to a branch, so a repository secret hands it to everyone who ever gains admin here,
whilegithub.tokenis scoped to one run and expires with it), the prohibited-name
evidence, and how to cut and re-cut a tag.
Changed
-
The README leads with the install path that works. It told a reader that
docketwas
not on PyPI, that the name had been unregistered on 2026-10-17, and that
pip install docketwould start working after a future 0.1.0 publication. The release
exists, so the apology was the only thing left. The install section now opens with
python3 -m pip install docket-ai, andpipx install docket-aireplaces the wheel URL.
The clone path stays, because it is what a contributor wants rather than what a user wants. -
The version note states what is guaranteed instead of hedging. It said the CLI "may
change before 1.0", which read as an unfinished project. It now says the format is frozen by
the specification and its 44 vectors across three implementations, and that exit codes,
flags and the JSON shape of every command are part of the interface and stable across 0.1.x. -
The exit-code table gains
1, and documents exit-code propagation.docket runhands
back the exit code of the command it wrapped, sodocket run --policy docket.toml -- ./build.shis safe in a pipeline. That was true and undocumented. -
The quickstart verifies
.docket/latest.receipt.jsoninstead of aDKT-*.receipt.json
glob. The glob worked on the first run and exited2on the second, becauseverifytakes
one receipt and argparse rejects two. The path every run rewrites has no such edge. -
pyproject.tomluses the SPDX license expression andlicense-files, so PyPI renders
the licence rather than a free-text string.
Fixed
-
The doc-link guard now asserts the opposite of what it used to. It used to fail any
document that instructed a reader to install an unpublished package, and its own docstring
said to relax it by removing the instruction once published. It now fails any document that
still carries the apology, and reads the distribution name frompyproject.tomlrather than
repeating it, so a rename cannot leave the guard and the guide disagreeing. -
docket verify --impl node: a second opinion from an installation. The claim that the
format is not this tool needed the other tool to be reachable without a clone, so the wheel now
carriesverify-js/verify.mjsand the conformance corpus underdocket/_vendor/, and
docket verify --impl noderuns that program against the same receipt and the same ledger and
prints its own verdict and its own exit codes (0/1/2). A pipeline that already gates on
docket verifyneeds no change when it switches.docket doctornow reports whether this
installation has both the verifier and anodeonPATH, and two tests read the packaging
configuration rather than a built artifact, so a droppedforce-includeline fails here
instead of in a reader's terminal. -
docket verifyfinds the ledger beside the receipt instead of checking less. It used to
need--ledgeror it skipped the chain, the roots, the file digest and the re-binding of the
usage rows. It now looks in the.docket/runs/directory beside the receipt it was handed and
selects by theledger.file_sha256the receipt carries, so the file used is provably the one
the receipt was built from — including for a receipt that arrived in an archive with its
ledger alongside. The line naming the file is printed,--ledgerstill overrides, and a
receipt with no ledger anywhere still degrades toSKIPrather than to a red result. -
A third implementation, written from the specification alone.
verify-go/is a
dependency-free Go verifier built fromspec/and the vectors without reading the Python or
Node one, with aCLEANROOM.mdthat declares the constraint before any code existed. It agrees
with the other two on the whole corpus —132 correct, 0 wrong, 0 not judgedover 44 vectors —
and it produced the artifact the claim needed: twelve questions the specification does not
answer, five of which are worth amending (rounding of the sixth decimal, price-table matching,
the shape oflimits_disclosure, blank lines in a ledger, and the marker for a reached cache
ceiling).make verify-goruns vet, gofmt and 16 table-driven test functions; CI has its own
job;make conformanceincludes it whenever the binary exists. -
Three more members of a receipt were stated and never checked, and now are.
totals.by_model[], the usage-source counts andtotals.cost_basisare derived from
appendix.usage, so they can be recomputed — but §8 only ever named the total, the token
totals and the per-row costs. A sweep that rewrote each of them leftok: trueevery time: a
breakdown could name a model nobody called (with the total still reconciling), or present four
estimated calls as reported.by-model-totalsandusage-source-countsare new checks;
cost-basisis a contradiction check that compares only the class word and reportsquota modeasSKIP, because the meter's mode is stated nowhere else in the receipt and believing
it is not checking it. Four new corpus vectors (39–42), eleven new tests in the Python
suite, eleven in the Node one, and ADR 0007 records why the other designs were rejected. -
ledger_reject: the corpus can now say "this ledger must be refused". Every vector was a
receipt that must fail; a malformed ledger — the file an interrupted append or a hand edit
actually damages — had no way to be expressed, so the two implementations' refusal to repair one
was asserted by their tests and by nothing a stranger could run. Two vectors (37,38: a
duplicated key inside an event, a half-written last line) and a fourth expectation kind close it.
The rule is the same as for receipts: a reason must be stated, and the vector may only require a
token that appears in the ledger. -
The corpus grew to 36 vectors and found four more defects — including one in the same file twice.
spec/vectors/invalid/now covers the parsing family (a UTF-8 BOM, trailing bytes, invalid UTF-8 —
all refused before any check runs) and, for the first time, documents that must be accepted:
4e0where an integer is expected,-0, and a decomposed Unicode form. Those three exist because a
corpus that only says what to refuse lets two implementations disagree about what to read, and the
first one settled a real disagreement: spec §3.2 says an integral float MUST be normalised, Python
refused every float token and Node accepted every one. The spec's sentence decided it; both now
normalise an integral float and refuse a fractional one. Probing the totals member by member also
found thattotals.calls,totals.input_tokensandtotals.output_tokenswere never compared with
the appendix in either implementation — a receipt could say"calls": 0beside four usage rows and
verify — and that Node'sreadFileSync(path, "utf8")silently replaced invalid bytes with U+FFFD, so
it verified a repaired copy of a corrupted document. All four are fixed, with vectors and tests. -
reason_any_of, because a corpus may not demand one implementation's prose. The first version
of the BOM vector required the wordBOM; Node saidUnexpected token '\ufeff', refused correctly,
and was scoredwrong-reason. An expectation now names tokens that must appear in the document
(NaN,Infinity, a member name), or nothing at all when the damage has no nameable token. Writing
a corpus before the third implementation exists is an exercise in not encoding the first two's
vocabulary. -
**`docs/adr/0006-th...
Docket 0.1.8
[0.1.8] - 2026-10-19
Fixed
-
PUBLISHING.mdtold the reader to run a command PyPI now refuses. The guide shipped
twine upload dist/*.whl dist/*.tar.gzwith no username, and PyPI has withdrawn username and
password authentication from the upload endpoint entirely. SettingTWINE_USERNAMEto the
account name, which is the obvious thing to do, is refused with
403 Username/Password authentication is no longer supported— an error that names
authentication rather than the username, so a correct token and a correct account both look
like the problem. The guide now exportsTWINE_USERNAME=__token__beside the token and says
why the literal string is required. This cost one failed upload while shipping 0.1.7. -
The release build left the previous release's artifacts in
dist/, so the documented upload
command was one release away from breaking.tools/build_dist.pycopied the new wheel and
sdist beside the old ones and stopped there, and the commandPUBLISHING.mdgives the reader
names both extensions rather thandist/*. Shipping a second release therefore handed PyPI a
filename it already holds, which fails after the current release's files have been accepted.
A rebuild now clears the distributions it supersedes and prints each removal, and--check
refuses adist/holding a distribution the provenance record does not name. Only the two
extensions the upload command reaches are touched. -
The suite size quoted in four documents was two tests behind. Adding the guards above moved
the collected count from 1236 to 1238, and the README,CONTRIBUTING.md,docs/testing.mdand
docs/sre-coverage.mdeach promise the reader a total. The per-file row for
test_build_dist.pymoved from 16 to 19.
Docket 0.1.7
[0.1.7] - 2026-10-19
Fixed
-
The race the 0.1.6 release gate found, shipped under a number that carries the fix. The
0.1.6 wheel and sdist were uploaded before the race was closed, so the published 0.1.6 is the
one whose receipt could claim 62 events over a ledger holding 64. PyPI reserves a filename once
it is published, so the only way to ship the fix is a release built from the commit that holds
it — which is what this is. The remotev0.1.6tag pointed at that same commit, its release
workflow had failed, and no release existed for it; the tag was deleted rather than left
naming a version whose artifacts did not match it.Nothing else changed. The tree is the one
d8992cadescribes: the drain that stops the receipt
racing the boundary's last recording call, the build-from-ref tooling that keeps an artifact
from predating its tag, and the publishing guide corrected to name the two distributions
rather than the whole ofdist/.
Docket 0.1.5
[0.1.5] - 2026-10-19
Fixed
- The published long description carries the mark. The 0.1.4 wheel and sdist were built from
the commit before the README logo was committed, so the package page rendered the version and
the badges but no artwork.README.mdis the long description, and the two distributions of
0.1.4 froze it at that commit; re-uploading them under the same filenames is not possible,
because PyPI reserves a filename once it is published. 0.1.5 is built from the commit that
holds the logo, and the package page and the repository now agree.
Added
-
The README carries the horizontal lockup, in both schemes. A
<picture>with a
dark-mode<source>and a light<img>, 250x80, sitting above## Why Docket?. Both
targets are absolute and point at the documentation site, which is where the rest of the
README already points:README.mdis the PyPI long description, and PyPI resolves a relative
target againsthttps://pypi.org/project/docket-ai/, so a repo-relative logo would 404 on
the package page while resolving correctly on GitHub. That is the same defect the 29
markdown links already carried, and it took three releases to notice. -
The PyPI link guard now sees raw HTML.
INLINE_LINKmatches](target)only, so a raw
srcorsrcsetwas invisible to both halves oftests/test_doc_links.py; a logo pointed
at a relative path would have 404'd on PyPI unreported.HTML_ASSETreads thesrc,
srcsetandhrefattributes of anyimg,sourceorlinktag, and both guards use it.
Both halves were proven to fire on the defect they now name by injecting a relative logo
path and a dead absolute one into the README and watching the right guard fail.Measured on the published description rather than on the source: rendered through PyPI's own
readme_rendererand nh3 sanitiser, the 0.1.5 description carries 52hrefandsrc
attributes, 36 of them absolute, 0 relative, and all 30 distinct URLs answer 200.
Docket 0.1.3
[0.1.3] - 2026-10-19
Fixed
-
Every receipt since 0.1.0 named a tool version that was not running. The version was written
three times —src/docket/__init__.py's__version__,src/docket/receipt.py's
TOOL_VERSION, andsrc/docket/supervisor.py'sDOCKET_TOOL_VERSION— and each had to be
edited by hand on every release. Two of the three were still0.1.0when the distribution
installed 0.1.2, sodocket --versionsaid one thing,pip showsaid another, and the
tool.versionfield in every receipt named a release that was not the one that wrote it. A
receipt that misreports its author makes a false claim about provenance, which is the one thing
a receipt exists to make checkable. All three now import from a newsrc/docket/_version.py,
which derives the version from the installed distribution, falls back to the nearest
pyproject.toml, and then tounknown.FORMAT_VERSIONmoved there for the same reason. The
guard that had checked only__init__.pynow scans every module undersrc/for a version
literal, in both the assignment and the dict-entry form, because scanning one file is what let
the other two literals survive the first fix. -
examples/custom-agent/agent_sdk.pycrashed on the command its own README advertised. Under
docket run, the session attaches to the supervised run andsession.ledgerisNoneby
design — the supervisor is the only writer to that file, because two writers would produce two
competing hash chains. The example printedsession.ledger.pathunconditionally, so the
supervised command raisedAttributeErrorwhile the standalone one worked. Only the standalone
path was tested. The example now branches onsession.attachedand says what is true in each
case, andtests/test_examples.pydrives the supervised path and asserts the agent's own events
reached the run's ledger labelledreported_by: "agent"and that the receipt verifies. -
The
Typing :: Typedclassifier was a claim with nothing behind it.pyproject.toml
declared the classifier and the wheel shipped nopy.typed, so no consumer's type checker
would read the package's annotations at all. The package was annotated all along — and 65 of
those annotations were wrong, which nobody could see because nothing asked.py.typednow
ships, mypy runs in the lint job, and the 65 errors are fixed: narrowing that did not survive
a second method call,__exit__annotatedboolwhere it always returnsFalse, a signing
key accepted and then used as any key type, a metrics dict annotateddict[str, int]that
holds a decimal string, and the rest. A guard test fails if the marker disappears while the
classifier stays. -
docket keygendid not exist, sodocket run --signhad no way to produce a key.
src/docket/signing.pycould generate an Ed25519 keypair from the first release and nothing
called it: no command, no documentation. The only way to sign a receipt was to already know
the library function.docket keygen [PATH] [--force]writes the keypair at mode 0600, prints
the public key, refuses to replace an existing key without--force(replacing a signing key
invalidates every receipt signed with the old one), and exits 70 naming the install when the
optionalcryptographypackage is absent. Documented in the CLI reference, and tested end to
end: keygen, sign, verify, and a tampered receipt that fails both the totals and the signature. -
Two
asserts were added to narrow a type, andbanditrefused them. Anassertis
removed underpython -O, and a tool whose whole claim is that it does not skip checks cannot
rest one on a statement the interpreter is entitled to delete.verify.pynow tests
chain_problemdirectly — which is the narrowing that was wanted anyway, and drops the
chain_okalias that made the tuple unreachable — andpolicy.pyraises for the case that
cannot happen rather than asserting it. The line citations indocs/testing.mdmoved with the
code and were re-checked against the harness's site list again. -
Eight definitions in the package were dead code.
generate_keypairwas the visible one —
docket run --signwas a flag with no way to produce a key, and the function that could have
produced one was invisible. Seven more had the same shape:short_hash,mb,
money_or_zero,stable_seed,is_known,default_price_table,price_document_digest
and_gzip_bytes, each a one- or two-line helper with no caller anywhere in the tree. Dead
code is not harmless in a project whose pitch is that a receipt says what the tool did: a
package that carries functions nothing runs is making the same kind of claim about itself. All
eight are deleted, andtests/test_supply_chain.pynow fails when a top-level definition in
src/appears nowhere else insrc/,tests/,tools/,examples/orverify-js/. -
A signing key of the wrong type produced an
AttributeErrorfrom insidecryptography.
load_pem_private_keyreturns a union of every key type the library supports, and most of
them have no.sign(data). The call now refuses a non-Ed25519 key with a message naming the
file and the key it holds. -
Every link in the README was broken on the PyPI page.
README.mdis the package's long
description, and PyPI renders it with no repository around it, resolving a relative target
againsthttps://pypi.org/project/docket-ai/. All 29 relative links therefore 404ed there while
resolving perfectly on GitHub — invisible from the repository, which is why it survived three
releases. They are now absolute: a target the documentation site publishes points at the site,
so a reader arriving from PyPI lands in the documentation rather than in raw markdown, and
LICENSEpoints at the repository. Two guards were added: one fails on any relative target, one
fetches every absolute URL and fails on a non-200.
Changed
-
Documentationin[project.urls]pointed at a GitHub directory listing. It now points at the
live documentation site, which is what a reader arriving from PyPI is looking for. -
Two public properties replace a private attribute an example reached into:
Session.docket_numberandSession.ledger_path, the latter returningNonewhen a
supervisor owns the ledger rather than a path this session does not write to. -
The line citations in
docs/testing.mdwere re-based onto the code as it now stands. Each one
was re-checked againsttools/mutate.py's site list, so every number the page quotes is a
line the mutation harness would actually mutate. -
The conformance vectors were regenerated, because they carry
tool.versionand the spec states
that vectors move with the implementation in the same commit. Only the version string and the
tworeceipt_core_sha256digests that cover it changed; all three implementations still agree
(Python and Node: 88 correct, 0 wrong; Node's own suite: 100/100).
Docket 0.1.2
[0.1.2] - 2026-10-18
Fixed
-
docket --versionreported a version the package was not.src/docket/__init__.pycarried a
literal__version__ = "0.1.0"whilepyproject.tomlmoved to 0.1.1, and nothing compared the
two. Every receipt'stool.versionfield and the output ofdocket --version,docket selftestanddocket doctorsaid 0.1.0 whilepip show, the PyPI page and
importlib.metadatasaid 0.1.1. A receipt that names the wrong tool makes a false claim about
provenance, which is the one thing a receipt exists to make checkable.The version is now read from the installed distribution, falling back to the
pyproject.toml
of a source tree and then tounknown. The distribution name is not written into the module,
so renaming it cannot desynchronise the two again.tests/test_supply_chain.pyasserts the
derivation and fails if the literal returns.
Docket 0.1.1
[0.1.1] - 2026-10-18
Fixed
-
The quickstart linked a wheel filename that does not exist. It told a reader to install
docket-0.1.0-py3-none-any.whl, which the rename to thedocket-aidistribution had turned
intodocket_ai-0.1.0-py3-none-any.whl. The quickstart now installs from PyPI like the
README does, keeps the clone path for a contributor, and links toPUBLISHING.mdfor why the
distribution name differs from the command. -
The PyPI long description quoted a stale test count. The 0.1.0 wheel was built before the
supply-chain guard was added, so its rendered page said 1196 tests where the repository said
1197. The wheels are otherwise byte-identical.
Docket 0.1.0
0.1.0 — 2026-10-13
First release: a bounded, legible, provable agent run.
Added
- Boundary (
docket run -- <command>): a loopback recording proxy that injects provider
base URLs (orHTTP_PROXYwith--http-proxy), enforces policy before forwarding a call,
accrues cost from provider-reported usage, and terminates the supervised process group
(SIGTERM, thenSIGKILLafter a grace period) when a cap is exceeded. Exit codes:
0clean,42cap exceeded,43policy error,70internal,130interrupted. - Ledger: append-only JSONL, each event carrying
seq,ts,type,data,prevand
hash; the first event binds tosha256("docket/genesis/v1"). Canonical JSON with integers
only (fractional values are decimal strings) so that the same bytes verify in any runtime. - Receipts:
docket/receipt/v1with an RFC 6962 Merkle root over event hashes, per-call
usage rows carrying inclusion proofs and re-bindable event hashes, the exact price table
digest, deterministic findings with evidence sequence numbers, and alimits_disclosure
block that verification fails if absent. - Verification: offline
docket verify(Python, zero dependencies) and an independent
verifier inverify-js/verify.mjs(Node standard library only), both bound to the committed
conformance vectors inspec/vectors/. Checks reportSKIPwith a reason rather than
passing when they cannot be performed, and the two implementations agree on the shape of a
failure as well as the verdict: both recompute every event hash, both decline to check the
head when the chain is broken, and both re-bind each usage row to the recomputed event at its
sequence number. The Node verifier additionally checks the receipt-core digest against a
committed vector. - Findings: runaway loops (byte-identical repeated requests), repeated context billed
uncached, oversized context calls, policy-blocked calls, deny-write violations, premium-model
leakage and retry storms — each with severity, confidence (deterministic/heuristic),
attributed impact and a cited evidence trail. - Recording surfaces: SDK (
docket.Session,record_model_call,record_tool,
record_note) for Python agents, and/docket/tool/call+/docket/noteHTTP endpoints so
agents in any language can write to the ledger. - Policy: TOML policy with caps, model allow/deny globs with first-match ordering, watched
and ignore globs,deny_writescopes, egress modes (tunnel,allow,deny,allowlist),
price-table override, andon_breach = "kill" | "deny". - Demo:
docket demoruns a scripted runaway agent against an in-process fake provider —
offline, no API key, deterministic clock — prints findings, verifies the receipt, and with
--tamperedits one byte of the ledger to show verification failing at the exact event. - Specification:
spec/docket-receipt-1.md, including the canonical-form rules, the
verification procedure, and §9 what a valid receipt does and does not establish. - Conformance vectors:
spec/vectors/{canonical,merkle,ledger,receipt,receipt-digest}.json
plus a generator, consumed as the cross-implementation contract by both test suites.
Notes
- Attached sessions. An SDK
Sessionstarted insidedocket runwrites through the
boundary's/docket/eventendpoint instead of opening the ledger file itself: one writer per
chain, and every agent-asserted event carriesreported_by: "agent"so a reader can tell
claims from boundary observations. Outside a supervised run it keeps its own ledger, as before. - Totals are derived from the usage appendix, so a receipt can never disagree with the rows
a verifier recomputes (the failure mode being$0.00claimed against$0.0042recomputed).
Rows asserted by the agent are counted and labelled separately (asserted_calls). docket verify --seal-keyaccepts the key file written bydocket run --sealor the hex key.- Zero runtime dependencies.
docket[sign]is the only extra that installs anything
(cryptography, for Ed25519 signatures). - Prices in
src/docket/prices.jsonare as of 2026-10-13 with sources recorded in the file;
they are estimates for budgeting, and every receipt states which table it used and flags
any model priced by fallback asassumed.