-
v0.58.0 Stable
released this
2026-09-22 00:31:06 -05:00 | 41 commits to main since this releaseThe weather tenant is complete at nine tools. W2, W3 and W4 all shipped together:
list_stations,get_model_run,get_taf,search_wind_shifts,get_observations,
get_alerts,get_forecast_discussionandget_hourly_forecast, alongside W1's
get_forecast_grid.One release rather than three, deliberately, and the reason is the release rule's own
argument. That rule exists so nothing running in production lacks a version to name it
by. These three phases were built and verified in one session and deploy together behind
one connector reconnect, so three tags would have named three moments that never existed
apart — fictional history rather than a finer-grained record. The entry below is organised
by phase instead.The build overturned three of its own plan's premises, each by measurement. The
Open-Meteo forecast response carries no issuance time at all, so the run time for a
tool namedget_model_runhad to be found in a second document. The configured global
model is three-hourly, not hourly, so its hourly series is interpolated — the grid's
native-versus-filled problem arriving through a different door. And the per-request model
choice could not be built on nulls-outside-the-domain because there are none: a
coordinate outside a model's domain is an explicit HTTP 400.Both of W0's open questions are answered, and one of them without waiting a day. The
plan said the gridpoint reissue cadence needed samples spread across a day; fifteen
offices sampled at one instant gave the same distribution, because offices issue on
staggered schedules. Median issuance age 5.48 hours, maximum 10.36, minimum 0.43 —
so the 3-hour warn line would have fired on 10 of 15 normal gridpoints and a 12-hour
line on none. HRRR's horizon is anchored to its run: the extended 00/06/12/18Z cycles run
48 hours and the hourly cycles between them run 18, so a request gets 42 to 48 hours and
hours=48stands as a default.1805 tests green, 155 of them this tenant's. 40 mutants were run against the new
guards and all 40 were caught — two survived the first pass, and both were the failure
CLAUDE.mdwarns about rather than a gap in the code: one mutant was ineffective, and one
test asserted a true thing the broken code also satisfied. The second is written up under
Fixed.Added
list_stations— every reporting station near a coordinate, nearest first, with
what can be read from each. The phase that proves "anywhere" is real, sincebbox
is the mechanism that works outside the United States and/pointsis the one that does
not. The widening ladder is0.5 → 1 → 2 → 4 → 8degrees, per product, stopping each as
soon as it answers.get_model_run— an hourly model forecast from whichever configured slug covers the
coordinate, with the run's initialisation time, its native step, and how much of the
window actually carried data.get_taf— the terminal forecast for the nearest airfield, with its change groups
read for what they mean rather than as a list of wind samples. One station, never
several;stationis the one argument that overrides the coordinate.search_wind_shifts— a scan over the model run and the TAF, with the raw grid as
corroboration reported alongside and never as the scan surface.get_observations— what the weather is actually doing, from NWS inside the United
States and from the aviation network everywhere else.get_alerts,get_forecast_discussion,get_hourly_forecast— the context
tools. The discussion comes back verbatim and nothing is parsed out of it.- Seven network-free reader modules, each fixture-driven like
calendar/expand.pyand
for the same reason:stations.py,taf.py,model.py,observations.py,alerts.py,
hourly.py,shifts.py. weather/units.py— the conversion layer, moved out ofgrid.pynow that it serves
four endpoint shapes rather than one.- A preflight check per configured model slug, advisory and never fatal, modelled on
_check_espnff_season. The plan specified validating each slug against Open-Meteo's
model list; there is no list —/v1/models404s and the invalid-slug rejection names
the bad value without enumerating the good ones. What replaces it is one real forecast
request per slug at a point inside that slug's own coverage, which validates the string
and that the model answers. Its detail line reports the run state, which is where the
three-hour native step surfaces without anybody going looking for it. envelope.from_epochandenvelope.from_iso— one conversion point for the three
timestamp shapes these upstreams send: an ISO string with an offset, an ISO string with
none, and epoch integers.envelope(deterministic=True)— for a station list or a grid resolution, which
carry no issuance time because they are mappings rather than products. The fields stay
identical so a caller reading the booleans never has to know which kind it holds; only
the sentence differs, because "do not describe it as current" warns about the wrong thing
on a mapping and teaches a reader to discount the note that matters.
Changed
client._classifyno longer calls every 4xx "an argument problem rather than an
outage", and it carries the body out on the exception. Open-Meteo answers both a
coverage boundary and a misspelled slug with a 400, and thereasonstring is the only
thing that distinguishes them — so a fallback that merely caught a 400 would have
routed every CONUS request to the global model forever on a typo, and looked like working
software.WeatherBadRequestcarries up to 2 KiB of the body, read while it arrives and
redacted by coordinate shape, because NWS's own 400 on/alerts/activeechoes the
rejected value and that string reaches a tool response. An unrecognised 400 is never
fallen back from.- The gridpoint staleness line is 12 hours, revised from 3 by use — the first
threshold on this tenant to be revised, which is what the table's own comment invited.
See the measurement above. The 24-hour absurdity ceiling did not move. - The model's staleness thresholds are derived per model from its own document rather
than read fromTHRESHOLDS. Measured availability lags: 1.53 h for the hourly
high-resolution model, 5.51 h forncep_gfs013, 7.12 h forecmwf_ifs025. So a
normal freshest run is ~2.5 hours old on one and ~13 on another, and a single row would
have marked every global-model answer stale. CLAUDE.mdcorrected: the raw gridpoint does not carrygeneratedAt. That section
claimed both/gridpoints/{wfo}/{x},{y}and/forecast/hourlycarry it. Checked across
sixteen offices and then directly on one cell: the raw gridpoint's scalar properties are
@id,@type,updateTime,validTimes,forecastOffice,gridId,gridXand
gridY, andgeneratedAtis not among them. The original measurement was real and came
from the hourly endpoint; the claim about the raw one was inferred and never checked.
The rule is stronger for the correction: on the endpoint that does carry both, the gap
measured ten hours, andgeneratedAtchanged between two requests fourteen minutes
apart on the same unchanged grid.docs/weather-plan.mdis down to four items from a full build plan: the recorded
frontal passage the wind-shift thresholds must be tuned against, the live pass, W5, and
one operator decision. A plan that shrinks to its outstanding items rather than emptying
is that rule working — Canvas's deletion is why, since three live checks were sitting
only in that plan when it went..env.examplesays the global model's native step is three hours, since that is an
operator decision the boot log now prints.
Fixed
- A wind shift that went out and came back reported the return as a remnant.
shifts._collapsetreated a candidate window that began exactly where the previous one
ended as overlapping it, so a wind veering 120 degrees and then backing 120 reported the
veer and a 60-degree fragment of the back. Two rotations that meet at a point are two
events. Found by writing the test, which is the discipline inCLAUDE.mdworking as
advertised. get_observationsreported a mistyped station as an upstream failure. The fallback
to the aviation network was unguarded, so a 204 — which is how that host says "no such
station" — escaped as a transport error and lost the sentence that names the typo. It now
reaches the empty-versus-invalid branch, which asks/stations/{id}and says which of the
two facts it is.- A cap test that could not reach the branch it named. It asserted the error detail was
short, which the final slice guarantees on its own — so it passed against a client with
the read cap removed. It now counts the chunks the upstream was actually asked for: a
body noticed after it has arrived is a body already in memory. This is the a test that
cannot reach a branch is not coverage of it failure, caught by the mutation sweep rather
than by review.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
released this
2026-09-21 21:59:51 -05:00 | 45 commits to main since this releaseThe weather tenant is deployed.
weather.mcp.jrendar.orghas its own Forgejo OAuth
application, its own stack service with its own NFS export, its own certificate and its own
NPM proxy host, and it answered a realget_forecast_gridcall the evening it went up:
/points/<coord>followed by/gridpoints/OKX/42,40, 167 KB of grid, with the coordinate
redacted in the log exactly as designed. Tenant #7 in the code, the fifth deployed. It was
parked besidesubscriptionsfor one release rather than for thirty-six.The one code-adjacent change is a line that should have been there from 0.57.0.
weather.confsaid NPM's access log was "the one place a coordinate could still land;
leave it off for this host" — and nothing implemented it. NPM has no per-host access-log
switch in its UI and no conf indeploy/nginx/carried anaccess_logdirective, so the
instruction was an intention for as long as it existed. Found while writing the standup
steps, which is an argument for writing them down.Added
access_log off;indeploy/nginx/weather.conf, and deliberately nowhere else. On
the other four hostnames the request names a folder or a course and the response is the
private half, so their logs are worth having. Here every tool takes a latitude and a
longitude: the request is the sensitive half and the response is public data. The tenant
redacts its own URLs before they reach its log; nginx would have written the unredacted
one, which made NPM the only remaining place a coordinate could land.
Changed
docs/operations.md's standup list is now six and carries two steps it was missing.
Issue the certificate before pasting the Advanced block — HTTP-01 fetches
/.well-known/acme-challenge/<token>and every conf here has alocation /.well-known/
ending indeny all; longest-prefix matching should save it, but that depends on the NPM
version and a failed issuance is rate-limited. The same applies at renewal, now with the
allowlist in place. Andaccess_log offbelongs in the conf when the request is the
sensitive half, per above.deploy/README.md: six files, five deployed, and whyweather.confis the only one
carryingaccess_log off.CLAUDE.md's status block: five tenants, withweather
moved out of the paragraph that files it alongsidesubscriptions.
docs/weather-plan.md's deployment item is closed, which is what was blocking W2.
Notes
- The live pass has started and has already earned its budget twice, in the first
minutes rather than the first week. 0.57.2 was the boot check probing an HTML landing
page; this release is the access-log line. Neither was reachable from a green suite, which
is the entire argument for the A8 step thatCLAUDE.mdrequires of every tenant. - What the deployment did not need. No DNS change, no secret, no credential rotation and
nothing on a provider's side — every upstream here is a public read endpoint. The whole
job was the identity layer in front, which is what makessubscriptions' remaining
absence a decision rather than an obstacle.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
released this
2026-09-21 21:39:57 -05:00 | 46 commits to main since this releaseThe weather tenant booted for the first time and its own preflight called its main
upstream unavailable. Every tool againstapi.weather.govworked; the check was probing
the wrong URL.https://api.weather.gov/is not an API endpoint — it serves a
9,495-byte HTML landing page, measured identically from inside the container and from
a workstation — so the probe fell into this tenant's own non-JSON guard, which is the
captive-portal rule the tenant exists to enforce. The guard was right. The URL was wrong,
and so was the docstring that justified it, which asserted the root "answers a small JSON
status document".This is the A8-class defect the plan budgeted for, and it arrived in the first minute.
Not a request that fails and reports success this time, but its mirror: one that succeeds
and reports failure. A green suite could not have caught it, because the only thing that
knows what a URL returns is the URL.Fixed
WeatherClient.check_upstreamnow dials/alerts/types. Parameterless, so the
no-location-in-configuration rule still holds and no coordinate is invented;
application/ld+jsonby contract; about 3.4 KB of the API's own event vocabulary. The
detail string reports the decoded count rather than a field the root never carried, so a
passing check now proves a body was parsed and not merely that a host answered. Verified
live againstapi.weather.govwith this deployment's User-Agent:
api.weather.gov reachable, 111 alert types.
Changed
- The fake serves HTML at
/, because the real API does. It answered{"status": "OK"}there, which is precisely how a boot check probing the root passed 1,728 tests and
failed on first contact with production. A fake that returns JSON where the upstream
returns HTML is not a fake; it is a second defect agreeing with the first. Three cache
and header tests that were using the root as a convenient JSON endpoint now use
/points/{lat},{lon}, which is what they were always describing. - The preflight test asserts the path and no longer the root. It previously asserted
["/"]and passed for the same reason the bug shipped.check_upstreamhad no direct
test at all; it now has one for the change-group case that matters — an HTML body must
still fail the boot check, or fixing the URL would have cost the guard that caught it.
Notes
- Deployment is underway and not finished. The stack service is running on the cluster
as of this release and reportstenant=weather version=0.57.1; the Forgejo OAuth app and
the NFS export exist. The NPM proxy host does not yet, so the hostname answers
nothing and no connector has been attached.deploy/README.md's "four deployed" and
CLAUDE.md's status block are therefore still accurate and deliberately unchanged — they
move when the hostname does. - 0.57.1's image reached the registry only on a re-run: the first
imagejob built
correctly and then timed out for 15 seconds againstforgejo.jrendar.org/v2/tokenat
docker login. Transient, same commit and credential succeeded on the re-run, and worth
recording only because a push that never happened looks exactly like a stale digest.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
released this
2026-09-21 21:19:50 -05:00 | 47 commits to main since this release0.57.0's gate was red and its image was never built.
pytestfailed on two PII rules,
theimagejob was skipped in consequence, and:latestin the registry still pointed at
0.56.1 — a release that has no weather tenant at all. The failure was not visible from
anything the release itself claimed: the tag existed, the Forgejo release object existed,
and the code was correct. What was missing was an image, and nothing said so. It
surfaced only when the deploy reached for one, which is the same shape as the defect the
weather tenant exists to guard against: a request that fails and reports success.The previous entry's "1728 tests green" was counted, not run. 1728 was the collected
total while two of them were failing. It is now literally true.Fixed
test_no_pii.py's keyed-decimal rule could not admit a negative coordinate, which
is the defect rather than the four literals it fired on. The separator was\W{0,4},
and\Wmatches-greedily, so onLON = -67.891it consumed,=,and the
sign, capturing67.891. Every allowlisted longitude in that file is negative, so the
allowlist could never match one, and the offender was reported as a number that appears
nowhere in the repo. The class is now[^\w-]{0,4}, which stops at the sign and hands it
to the number. A negative that is genuinely not allowlisted still fires, with its sign
intact — proved by assertion rather than asserted in a comment.- Three coordinate-shaped literals that should never have been written. The redaction
comment inweather/client.pyillustrated itself with a real airport's position, and
test_weather.pycarried a live-lookingbboxaround New York; both now use the repo's
invented vector or one decimal place, which is an 11 km cell and not a place. The bbox
test's docstring says why it is coarse, so the next reader does not sharpen it back. - A fixture named the single-label host
elsewhere. Nowelsewhere.invalid, per the
reserved-domain rule that rejected it. A bare label is a name a search domain resolves.
Notes
- No behaviour changed. Three of the four edits are comments, docstrings and test
vectors; the fourth is a test-only regex. The weather tenant is byte-identical in
behaviour to 0.57.0 and remains code-only and undeployed. - The guard was right to fire and wrong about one of its four hits, which is worth
separating: it caught two real literals and one unreserved host, and it invented the
fourth. A guard that cries wolf once is switched off eventually, whichtest_no_pii.py's
own header says is the failure mode it fears most.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v0.57.0 Stable
released this
2026-09-21 19:01:08 -05:00 | 48 commits to main since this releaseTenant #7, weather, W1. A tenant whose correctness claim is about time rather than
about content: every other one here answers "what does the provider hold", and this one
answers "what does the provider hold, and when was that true". It exists because a NOAA
raw METAR text file came back dated eight months earlier under a clean HTTP 200, with
nothing in the response to say so.It ships code-only and undeployed, deliberately, and that is written down rather than
discovered later. The Forgejo OAuth app, the stack service and the proxy host are the
actual gate and none of them is in this repo — so the A8-equivalent live pass is
impossible until they exist.subscriptionshas been in that state for thirty-six
releases; the difference between "parked" and "forgotten" is whether somebody said which.The W0 probe that ran during the review invalidated the plan's centrepiece, and the
build is shaped around the answer. Counted by hours covered rather than by entry count,
one live gridpoint'swindDirectionseries was 17% native andwindSpeed9% —
so nine tenths of any transition found in the grid is an artifact of how it is encoded.
The grid cannot be the surface a wind-shift scan runs over, andget_forecast_gridis
built to say so in the response rather than to hide it.Added
mcp_gateway/weather/— the seventh tenant, 1,614 lines across four modules.
envelope.py(the staleness wrapper every tool returns through),client.py(four
upstreams, per-host headers, typed errors, the size cap and coordinate redaction),
grid.py(ISO 8601 durations, native-versus-filled, the unit layer) andtools.py.get_forecast_grid— the NWS raw forecast grid for any US coordinate. It returns
the grid's blocks, never an hourly expansion, and reportsnative_shareper
parameter so a caller can see how much of a window is a forecast for that hour and how
much is one value stretched across an afternoon.- The staleness envelope. One wrapper, not a field each tool remembers to populate:
source,issued,fetched,cached,age_minutes,age_unknown,stale. Age
comes from the payload's own issuance time and is measured from now rather than from
when the bytes arrived, so a cached body does not report the age it had when it landed.
An absent issuance time givesstale: null— neverfalse. - Two thresholds per endpoint, and only the second raises.
stale_afteris a
freshness judgement;absurd_afteris a sanity ceiling set against the eight-month
METAR. Putting the ceiling where the threshold belongs would turn "this station reports
hourly" into a tool error. tests/test_no_pii.pyrefuses a coordinate-shaped literal, allowlisted rather than
blocklisted — a rule that looked for coordinates near a named region would have to
write that region down, in the public file whose whole job is not writing it down, and
would still pass on a coordinate from wherever the operator was travelling. Ships with
a negative control.deploy/nginx/weather.conf, written and parked. The sixth copy;proxy_read_timeout 120s, matchingcanvas.conf.- Five settings:
WEATHER_CONTACT_EMAIL(required — NWS publishes no rate limit and
enforces its contact-address courtesy by blocking),WEATHER_MODEL_CONUS,
WEATHER_MODEL_GLOBAL,WEATHER_TIMEOUT_SECONDS,WEATHER_MAX_RESPONSE_BYTES. There
is no location, no station list and no alias table, and that is the design: every
tool takes a coordinate as an argument. - 82 tests — 78 in
tests/test_weather.py, two for the coordinate guard and two from
the healthcheck loop. Thirteen deliberate mutations were run against them and every
one was caught, including the first-pass native rule, thewmoUnit:km_hspelling, an
index-based expansion,generatedAtread as the issuance time, an unredacted log URL
and a fall-through in the healthcheck builder.
Fixed
tests/test_healthcheck.pywas testing the calendar tenant three times. Its
per-tenant builder was a chain ofifs ending in a fall-through tobuild_app, and
canvasandespnffwere never added — so from the day each shipped, a loop written
precisely to catch a forgotten tenant built the calendar app for them, found/healthz
on it, and passed. A loop overTENANTSthat silently tests one tenant twice is worse
than no loop, because the file's own docstring promises the opposite. It now names
every tenant and raises on the fall-through. Both were fine; nothing was broken by this,
which is the only reason it survived two releases.__main__'s tenant dispatch ended in a bareelsethat built the iCloud calendar
app. A tenant added toTENANTSand forgotten there did not fail — it became the
calendar tenant, booting cleanly, passing its healthcheck and serving somebody else's
tools on the new hostname. Now an explicit branch per tenant with anelsethat logs
and returns 1.Config.from_envdemanded an Apple ID by default.required=not (mail or subs or gmail or canvas or espnff)meant "the calendar tenant" and said so only by listing
everything else, so every tenant added after it inherited the demand and the only thing
in the way was somebody remembering to extend anot (...). Both credential conditions
are named positively now; behaviour is unchanged.docs/operations.mdstep 2 said "the 300s timeouts" and it is two numbers.
icloudcalendar.conf,icloudmail.confandgmail.confuse 300s;
subscriptions.conf,canvas.confandweather.confuse 120s. The split tracks what
is behind the location — a credential and a protocol that can walk a mailbox, versus one
or two HTTPS GETs — so the checklist now says to copy the conf of the tenant yours
resembles and read its comment.
Changed
force_refreshis floored rather than refused inside the TTL. The plan specified
that a forced refresh inside the endpoint's TTL should return the cached value, which
reads cleanly and would have made the argument do nothing at all — the cache only ever
holds entries inside their TTL. It is refused outright on the two deterministic
mappings, and elsewhere honoured at most once per 60 seconds per key, saying so when it
declines.CLAUDE.mdgained A tenant whose correctness claim is about time — five of the
plan's seven→ CLAUDE.mdpieces, which leftdocs/weather-plan.mdin the same commit
that finished them. The plan went from 819 lines to 430 and holds only W2 onward..env.example,README.mdanddeploy/README.mdall carry the seventh tenant.
0.55.4 shipped to fix a stale conf count in the last of those, so it is a known miss
rather than a hypothetical one.
Not built, and named so it is not mistaken for an oversight
- The Open-Meteo model-slug preflight check. Both slugs are configuration as of this
release and no code path reads them until the model tool exists in W2, so a boot check
on them would dial a third-party API to validate a setting nothing uses. - An hourly expansion in a tool response.
grid.expandexists for the consumers that
need hours — the wind-shift scan, the fill statistics — and every sample it yields
carriesnative. Putting its output in a response would hand a caller twelve identical
rows that read as twelve forecasts.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v0.56.1 Stable
released this
2026-09-21 13:58:17 -05:00 | 51 commits to main since this release0.56.0's entry counted its own work wrong, three times. Documentation only; no code
changed, and the suite is unmoved at 1646.Changed
- Fifteen, not fourteen, in all three places. The 0.56.0 entry said fourteen
definitions moved intomail/imap.pyand fourteen settings were added to
.env.example; both are fifteen._selectwas omitted from the prose list of what
moved, andCANVAS_COOKIE_SEED— added to the example file in the same release —
was omitted from the settings count. - "236 lines of duplication are gone" was an estimate presented as a measurement,
and it was not even the right shape: there is no single figure, because the two
copies were not the same length.gmail/client.pylost 231 lines and
icloudmail/client.py205, and the one implementation that replaced them is 127
lines of code. The entry now says that instead.
A count written while the work is still moving is a guess with a number on it. All
three of these were written into the changelog before the last edits landed and never
re-measured against the finished diff — which is the same failure as the stale paragraph
0.55.4 corrected, arriving in the entry that described the sweep rather than in the file
the sweep was about. Count fromgit diff, after the commit is built.Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Fifteen, not fourteen, in all three places. The 0.56.0 entry said fourteen
-
v0.56.0 Stable
released this
2026-09-21 13:52:32 -05:00 | 52 commits to main since this releaseThe sweep one release ago found almost nothing; this one asked the same question
of the code rather than of the file list, and found four things. The Canvas tool
layer had never logged a line, both mail tenants carried a byte-identical copy of
their whole allowlist layer, aPhasemember had been dead since the first commit,
and fourteen settingsconfig.pyreads were in no example file. A test now catches
the first and third classes of defect, which nothing could see before.Added
mcp_gateway/mail/imap.py—ImapMailbox, the provider-independent half of an
IMAP mail client, plusCachedFoldersandsuppress_close. Fourteen definitions
that both mail tenants held byte-identical copies of: the read and write allowlists,
the folder cache, handle resolution and its UIDVALIDITY check,count_messages,
_localise, the two best-effort post-send steps, and the two preflight checks.
Both clients inherit it and 236 lines of duplication are gone.- The Canvas tool layer logs. Twelve tools, twelve
infolines on success and one
warn/erroron every refusal, routed through_refusalso the failure path is one
line of code rather than twelve copies of one. tests/test_logging.pyguards the logging vocabulary two ways — everyPhase
member must be passed tofor_phasesomewhere, and everylog_x = for_phase(...)
binding must be called in its own file. Both were broken deliberately and observed
failing before being fixed.- Fourteen settings documented in
.env.example, including the first Canvas
section it has ever had:CANVAS_BASE_URL,CANVAS_COOKIE_PATH,
CANVAS_COOKIE_SEED,CANVAS_KEEPALIVE_SECONDS,CANVAS_TIMEOUTand the five
truncation bounds, plusCALDAV_URL,CALDAV_TIMEOUT,HEALTH_PATH,
FASTMCP_HOMEandMAIL_ALIAS_INDEX_TTL.
Removed
Phase.HTTP_HEALTH— declared in the first commit and never once logged under.
/healthzis the container healthcheck, so a line per call is one every few seconds
saying the thing was up, which is the one fact the absence of the container already
reports. The phase is gone rather than the silence being explained beside it.
Fixed
- The Canvas tenant was invisible in Portainer.
canvas/tools.pybound
log_coursesat the top of the file and never called it, so twelve tools — including
every refusal, and including the session expiry that only a person can fix — produced
no log line at all. Nothing could catch this: ruff sees the name as used because the
assignment uses it, mypy has no opinion, and the binding at the top of the file reads
as evidence the module logs. test_a_classmates_profile_never_leaves_canvasfailed 0.84% of runs, and had
since 0.50.0. It searched the whole serialised response for a three-digit user id as
a substring, and the envelope'snowis an ISO timestamp carrying twenty digits that
change every run — so427or909landed inside the microseconds about one CI run
in a hundred, as a PII leak that had not happened. Measured over 200,000 timestamps.
The clock is now dropped before the needles are looked for; nothing else is relaxed.
Notes
- What was NOT de-duplicated, and it is the finding worth keeping.
_take_copyuid,_parse_copyuid,_take_appenduid,_parse_appenduidand
_appendstay in both tenants. Their bodies are identical and their behaviour is
not: each parser closes over its own regex, and the two differ in both directions —
Gmail's admits*in a UID set and matches case-sensitively, iCloud's forbids*
and passesre.IGNORECASE. Unifying them means adopting the union, which widens what
a handle-minting parser accepts on two deployed tenants, and the note on
_take_appenduidasks for both write probes to be re-run first. Identical bodies
are not identical functions when they close over different constants — the fake
agreeing with the code failure in one more disguise, and a body-level duplicate
detector reports it as safe. - The tool-layer duplicates in
gmail/tools.pyandicloudmail/tools.pywere left
alone. Nine pairs share a body, but each is wrapped in adescription=written for
its own tenant — Gmail says "label" where iCloud says "folder" — and those strings are
the model-facing surface. Collapsing the bodies would mean a factory parameterised by
a page of prose, to save about fifty lines._require_writablestays per-tenant for
the same reason: every string it raises is read by a model. - No live probe was re-run. Everything here is covered by the suite, and nothing
touches a request this project makes to Apple, Google, Canvas or ESPN — but the write
paths on both mail tenants now run through inherited methods rather than their own
copies, so the next mail live pass is the first observation of that.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
v0.55.4 Stable
released this
2026-09-20 23:22:05 -05:00 | 53 commits to main since this releaseA sweep for anything the repo no longer needs, and it found almost nothing. One dead
constant removed, one stale paragraph corrected. The result worth recording is the absence:
every module is imported, every declared dependency is imported, every variable in
.env.exampleresolves inconfig.py, and no test file is orphaned.Removed
_REPORTEDinmail/authresults.py— a four-name tuple left behind when the verdict
block stopped looping over a list of methods and started naming them one at a time in
to_dict. Nothing had read it since.
A comment attached to a dead constant is documentation nobody will find. The rule that
tuple's comment stated is live and tested —bimiis parsed because iCloud sends it and
kept out of the verdict block because a brand-logo indicator beside DMARC reads as an
authentication result — but its anchor had rotted, so the note now sits onto_dict,
where the choice is actually made and where
test_bimi_is_parsed_but_kept_out_of_the_verdict_blockalready holds it. Move the note to
the code that implements it rather than deleting it with the thing it was attached to.Changed
deploy/README.mdnamed two nginx confs and there are five. It still described
icloudcalendar.confandicloudmail.confas "the two files ... near-copies", which was
true untilsubscriptions,gmailandcanvaseach added one. It now says five files,
four deployed,subscriptions.confparked without a service, andespnffdeployed with
no conf at all on purpose. The file already carries a note about a paragraph that
survived the change it described; this was a second one.CLAUDE.mdStatus —0.55.2, two releases out of date. Now0.55.4. Still 1643
tests.
Notes
- Pruning
probes/was proposed on a filename search and withdrawn on the evidence.
Grepping the docs for each script's filename said 34 of the 62 were uncited and therefore
superseded. They are not: the records cite probes by number far more often than by
filename —Probe 7appears inCLAUDE.mdtwice and inauthresults.py's own module
docstring, and its script,probe7_junk_and_auth.py, is one of the 34. Every number from
1 to 14 is cited somewhere. A citation search is only as good as the naming convention
it assumes, and this corpus has two. Nothing inprobes/was deleted but its
__pycache__. - What was removable was all outside the tree the image builds from:
.mypy_cache
(75 MB),.ruff_cache,.pytest_cache,mcp_gateway.egg-infoand the__pycache__
directories. All regenerate; none has ever been committed. - The
subscriptionstenant was left alone. Code-complete and undeployed since 0.21.0
reads like something to delete and is not — it is waiting on a Forgejo OAuth app, a
stack service and a proxy host, none of which are in this repo. Parked is not dead, and
this note exists so the next sweep does not re-propose it.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
released this
2026-09-20 23:18:25 -05:00 | 54 commits to main since this releaseThe weather plan's two open questions are closed, and committing the probe scripts is
formally declined. Documentation only; still no weather code.Changed
- No location aliases.
WEATHER_LOCATIONS— resolving"home"to a coordinate — was
the design note's one phase-2 convenience and the only feature that would have kept a home
address in this project's configuration. What it saved was pasting two numbers. Declined,
and moved to Out of scope so it is not re-proposed. The coordinate-literal guard test
stays: declining the alias removes one leak path, not all of them, and a fixture recorded
against home leaks one just as well. get_forecast_discussionreturns the product verbatim and parses nothing. The AFD is
long free prose, and anything that lifted "the timing sentence" out of it would be
inventing a finding of its own. Issuance time comes from the product metadata, not from
the text.docs/operations.mdrecords whyprobes/stays untracked. Scrubbing and committing
the sixty-two scripts would let the live-pass records shrink to pointers, and it is the
same bet this project already lost once — that scrub missed two spellings of one name and
left seven commits carrying it. The prize is a shorter file. Declined, in writing, so the
next reader who finds that file too long learns why before starting.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- No location aliases.
-
released this
2026-09-20 23:09:54 -05:00 | 55 commits to main since this releaseTenant #7 has a build plan. Documentation only; no weather code exists yet.
Added
docs/weather-plan.md— the queue for aweather.mcp.jrendar.orgtenant, read-only
by construction like Canvas andespnffbecause none of its upstreams has a write API.
It exists because every open-source NWS MCP server wraps the 12-hour worded forecast,
which cannot answer when a front arrives, and because a NOAA METAR came back eight
months stale under a clean response. So: read the raw grid rather than the prose, and
make every response carry its own issuance time, fetch time and computed age.- Five phases, W0 through W5, each ending in something usable. W1 is the staleness
envelope plus one tool, because that one tool at hourly resolution would have answered
the original question and is the cheapest way to learn whether resolution or staleness
was the real problem. The correlation tool that motivated the whole thing is deferred to
W5 and opened by use rather than by plan, the wayget_discussionwas. - The home coordinate is treated as account data, like
CANVAS_BASE_URL. A lat/lon at
two decimal places is a box around a house and this repo is public, so the alias table
is a Portainer env field, examples and fixtures use published airport coordinates, and
the boot log may name an alias but never its value. A guard test is planned alongside,
because the rule is otherwise a habit.
Changed
CLAUDE.mdStatus —0.52.0and 1611 tests, three releases out of date. Now 0.55.2
and 1643.- The
docs/<tenant>-plan.mdrule said no plan was open, which was true for four days
after 0.55.0 deleted Canvas's.docs/weather-plan.mdis open and the line names it.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads