Skip to content

Add CI + complete packaging, and fix two latent ledger bugs it surfaced - #1

Merged
thorwhalen merged 5 commits into
mainfrom
claude/rollout-modernize
Aug 7, 2026
Merged

Add CI + complete packaging, and fix two latent ledger bugs it surfaced#1
thorwhalen merged 5 commits into
mainfrom
claude/rollout-modernize

Conversation

@thorwhalen

Copy link
Copy Markdown
Member

Rollout pass over this repo.

  • CI: the repo had none at all — added the wads uv-CI stub.
  • Packaging: complete pyproject.toml (SPDX license, classifiers, wads CI config, testpaths); add the Apache-2.0 LICENSE and .editorconfig.
  • fix(ledger): two latent bugs that turning CI on surfaced — key traversal went through the tool name rather than the key, and a naive timestamp raised TypeError when compared against an aware one.
  • Applied ruff format to the Python sources.

https://claude.ai/code/session_01Uz2YYxAfa1DX8UcCDmeKC5

Bring the packaging metadata and the wads CI SSOT block up to the ecosystem
standard. The repo already had a pyproject (no setup.cfg/setup.py to convert),
so this is a completion pass rather than a conversion:

- license: replace the deprecated `[project.license] text = ...` table with the
  PEP 639 SPDX string `license = "Apache-2.0"` + `license-files = ["LICENSE"]`.
  Verified: the built wheel now carries `License-Expression: Apache-2.0` and
  ships `dist-info/licenses/LICENSE`.
- classifiers: added (development status, audience, OS, Python versions, topics).
- testpaths: `["tests"]` -> `["enlace_metering", "tests"]`. wads CI runs
  `pytest --doctest-modules` with NO path argument, so collection is driven
  entirely by testpaths; with only `tests` the package's own doctests would run
  nowhere while CI still reported green.
- `[tool.wads.ci]`: explicit `project_name` (it is the `ruff check` target and
  the `--cov` target), plus testing / build / publish / env / quality / docs
  sections so nothing falls through to a moving default.
- `[tool.wads.ci.publish].enabled = false`: this name has never been published
  to PyPI. First publication of a new name is a deliberate act, not a side
  effect of merging a modernization PR.

`[tool.wads.ci.install].extras = "dev"` was already correct and is kept: the
dev extra carries fastmcp, which `--doctest-modules` needs to import
enlace_metering.middleware in CI's clean environment.

Claude-Session: https://claude.ai/code/session_01VipiLaG4xy7WctqY9w2475
Nothing tested or built this repo. Adds `.github/workflows/ci.yml` as the
5-line stub calling the reusable workflow
`i2mint/wads/.github/workflows/uv-ci.yml@master`, generated from the wads
template with the secrets pass-through block rendered from
`[tool.wads.ci.env]` (PYPI_PASSWORD only — no test secrets are needed here).

All configuration lives in `[tool.wads.ci.*]` in pyproject.toml. Publishing is
disabled there, so a merge to the default branch runs validation and docs only.

Claude-Session: https://claude.ai/code/session_01VipiLaG4xy7WctqY9w2475
The pyproject declared Apache-2.0 but no LICENSE file existed, so the built
distributions carried no licence text. Adds the standard Apache-2.0 text used
across the ecosystem, plus the wads-template .editorconfig.

Claude-Session: https://claude.ai/code/session_01VipiLaG4xy7WctqY9w2475
Pure formatting, no behaviour change — brings the tree in line with what the
wads CI's `ruff format` step produces (whitespace/line-wrapping only; the
README's hand-aligned example block is deliberately left untouched).

Claude-Session: https://claude.ai/code/session_01VipiLaG4xy7WctqY9w2475
…amp TypeError)

Both surfaced while reviewing the package during the packaging/CI modernization
pass. Both are silent in the existing suite.

1. Traversal via the entry id. The ledger key is
   `{principal}/{month}/{id}.json` and the module docstring claimed the key was
   traversal-checked — but only `principal` was. The middleware builds the id as
   `{ns}-{seq}-{tool}`, and `tool` is the *client-supplied* name from a
   `tools/call`, which reaches `on_call_tool` BEFORE the tool is resolved. So an
   authorized caller could name a nonexistent tool `../../../etc/pwn` and the
   write-ahead row would be written to
   `a@b.com/2026-08/<ns>-0-../../../etc/pwn.json` — outside the ledger prefix on
   any path-backed store (the documented production case is a dol file store).
   Fix, in two layers:
   - `ledger.safe_key_component` (extracted from `safe_principal`) now validates
     the `month` and `id` components too, so any producer writing to the ledger
     is checked, not just the middleware;
   - the middleware slugifies the tool component (`[^A-Za-z0-9._-]` -> `_`,
     capped at 64 chars) so a hostile *or* merely unknown name is still recorded
     rather than refused. The raw name stays in the entry's `tool` field, so the
     audit trail is unchanged.

2. Naive timestamps raise instead of being tolerated. `_entry_ts` parsed `ts`
   with `datetime.fromisoformat` and returned it as-is; a value written without
   a UTC offset yields a naive datetime, and `ts >= since` then raises
   `TypeError: can't compare offset-naive and offset-aware datetimes`. That
   propagates out of `spend_since`, through the gate policy, into the
   fail-closed middleware — so ONE badly-stamped row would deny every subsequent
   gated call for that principal. The module already treats an unparseable `ts`
   as "include" (conservative), so raising here was inconsistent as well as
   fragile. `_as_utc` now reads a naive datetime as UTC, applied to both the
   entry timestamp and the caller-supplied `since`.

Regression tests added for both (traversal in `id`/`month`, the middleware
slug, naive `ts`, naive `since`).

Claude-Session: https://claude.ai/code/session_01VipiLaG4xy7WctqY9w2475
@thorwhalen
thorwhalen merged commit e438bb4 into main Aug 7, 2026
12 checks passed
@thorwhalen
thorwhalen deleted the claude/rollout-modernize branch August 7, 2026 08:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant