• v0.58.0 40cf04f3a8

    v0.58.0 Stable

    joeyr released this 2026-09-22 00:31:06 -05:00 | 41 commits to main since this release

    The 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_discussion and get_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 named get_model_run had 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=48 stands 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.md warns 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, since bbox
      is the mechanism that works outside the United States and /points is the one that does
      not. The widening ladder is 0.5 → 1 → 2 → 4 → 8 degrees, 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; station is 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.py and
      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 of grid.py now 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/models 404s 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_epoch and envelope.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._classify no 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 the reason string 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. WeatherBadRequest carries up to 2 KiB of the body, read while it arrives and
      redacted by coordinate shape, because NWS's own 400 on /alerts/active echoes 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 from THRESHOLDS. Measured availability lags: 1.53 h for the hourly
      high-resolution model, 5.51 h for ncep_gfs013, 7.12 h for ecmwf_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.md corrected: the raw gridpoint does not carry generatedAt. That section
      claimed both /gridpoints/{wfo}/{x},{y} and /forecast/hourly carry it. Checked across
      sixteen offices and then directly on one cell: the raw gridpoint's scalar properties are
      @id, @type, updateTime, validTimes, forecastOffice, gridId, gridX and
      gridY, and generatedAt is 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, and generatedAt changed between two requests fourteen minutes
      apart on the same unchanged grid.
    • docs/weather-plan.md is 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.example says 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._collapse treated 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 in CLAUDE.md working as
      advertised.
    • get_observations reported 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
  • v0.57.3 d0d6b492cf

    joeyr released this 2026-09-21 21:59:51 -05:00 | 45 commits to main since this release

    The weather tenant is deployed. weather.mcp.jrendar.org has 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 real get_forecast_grid call 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 beside subscriptions for 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.conf said 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 in deploy/nginx/ carried an access_log directive, 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; in deploy/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 a location /.well-known/
      ending in deny 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. And access_log off belongs in the conf when the request is the
      sensitive half
      , per above.
    • deploy/README.md: six files, five deployed, and why weather.conf is the only one
      carrying access_log off. CLAUDE.md's status block: five tenants, with weather
      moved out of the paragraph that files it alongside subscriptions.
      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 that CLAUDE.md requires 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 makes subscriptions' remaining
      absence a decision rather than an obstacle.
    Downloads
  • v0.57.2 b4c5a3501f

    joeyr released this 2026-09-21 21:39:57 -05:00 | 46 commits to main since this release

    The weather tenant booted for the first time and its own preflight called its main
    upstream unavailable.
    Every tool against api.weather.gov worked; 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_upstream now dials /alerts/types. Parameterless, so the
      no-location-in-configuration rule still holds and no coordinate is invented;
      application/ld+json by 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 against api.weather.gov with 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_upstream had 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 reports tenant=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 image job built
      correctly and then timed out for 15 seconds against forgejo.jrendar.org/v2/token at
      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
  • v0.57.1 1310d67ae7

    joeyr released this 2026-09-21 21:19:50 -05:00 | 47 commits to main since this release

    0.57.0's gate was red and its image was never built. pytest failed on two PII rules,
    the image job was skipped in consequence, and :latest in 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 \W matches - greedily, so on LON = -67.891 it consumed , =, and the
      sign
      , capturing 67.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 in weather/client.py illustrated itself with a real airport's position, and
      test_weather.py carried a live-looking bbox around 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. Now elsewhere.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, which test_no_pii.py's
      own header says is the failure mode it fears most.
    Downloads
  • v0.57.0 298d814dd7

    v0.57.0 Stable

    joeyr released this 2026-09-21 19:01:08 -05:00 | 48 commits to main since this release

    Tenant #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. subscriptions has 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's windDirection series was 17% native and windSpeed 9% —
    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, and get_forecast_grid is
    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) and tools.py.
    • get_forecast_grid — the NWS raw forecast grid for any US coordinate. It returns
      the grid's blocks, never an hourly expansion, and reports native_share per
      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 gives stale: null — never false.
    • Two thresholds per endpoint, and only the second raises. stale_after is a
      freshness judgement; absurd_after is 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.py refuses 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, matching canvas.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, the wmoUnit:km_h spelling, an
      index-based expansion, generatedAt read as the issuance time, an unredacted log URL
      and a fall-through in the healthcheck builder.

    Fixed

    • tests/test_healthcheck.py was testing the calendar tenant three times. Its
      per-tenant builder was a chain of ifs ending in a fall-through to build_app, and
      canvas and espnff were 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 over TENANTS that 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 bare else that built the iCloud calendar
      app.
      A tenant added to TENANTS and 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 an else that logs
      and returns 1.
    • Config.from_env demanded 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 a not (...). Both credential conditions
      are named positively now; behaviour is unchanged.
    • docs/operations.md step 2 said "the 300s timeouts" and it is two numbers.
      icloudcalendar.conf, icloudmail.conf and gmail.conf use 300s;
      subscriptions.conf, canvas.conf and weather.conf use 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_refresh is 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.md gained A tenant whose correctness claim is about time — five of the
      plan's seven → CLAUDE.md pieces, which left docs/weather-plan.md in the same commit
      that finished them. The plan went from 819 lines to 430 and holds only W2 onward.
    • .env.example, README.md and deploy/README.md all 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.expand exists for the consumers that
      need hours — the wind-shift scan, the fill statistics — and every sample it yields
      carries native. Putting its output in a response would hand a caller twelve identical
      rows that read as twelve forecasts.
    Downloads
  • v0.56.1 1971c59975

    v0.56.1 Stable

    joeyr released this 2026-09-21 13:58:17 -05:00 | 51 commits to main since this release

    0.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 into mail/imap.py and fourteen settings were added to
      .env.example; both are fifteen. _select was omitted from the prose list of what
      moved, and CANVAS_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.py lost 231 lines and
      icloudmail/client.py 205, 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 from git diff, after the commit is built.

    Downloads
  • v0.56.0 4c4fb6ccf1

    v0.56.0 Stable

    joeyr released this 2026-09-21 13:52:32 -05:00 | 52 commits to main since this release

    The 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, a Phase member had been dead since the first commit,
    and fourteen settings config.py reads 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, plus CachedFolders and suppress_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 info lines on success and one
      warn/error on every refusal, routed through _refusal so the failure path is one
      line of code rather than twelve copies of one.
    • tests/test_logging.py guards the logging vocabulary two ways — every Phase
      member must be passed to for_phase somewhere, and every log_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_TIMEOUT and the five
      truncation bounds, plus CALDAV_URL, CALDAV_TIMEOUT, HEALTH_PATH,
      FASTMCP_HOME and MAIL_ALIAS_INDEX_TTL.

    Removed

    • Phase.HTTP_HEALTH — declared in the first commit and never once logged under.
      /healthz is 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.py bound
      log_courses at 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_canvas failed 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's now is an ISO timestamp carrying twenty digits that
      change every run — so 427 or 909 landed 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_appenduid and
      _append stay 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 passes re.IGNORECASE. Unifying them means adopting the union, which widens what
      a handle-minting parser accepts on two deployed tenants, and the note on
      _take_appenduid asks 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.py and icloudmail/tools.py were left
      alone.
      Nine pairs share a body, but each is wrapped in a description= 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_writable stays 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
  • v0.55.4 885f5eeea3

    v0.55.4 Stable

    joeyr released this 2026-09-20 23:22:05 -05:00 | 53 commits to main since this release

    A 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.example resolves in config.py, and no test file is orphaned.

    Removed

    • _REPORTED in mail/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 — bimi is 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 on to_dict,
    where the choice is actually made and where
    test_bimi_is_parsed_but_kept_out_of_the_verdict_block already 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.md named two nginx confs and there are five. It still described
      icloudcalendar.conf and icloudmail.conf as "the two files ... near-copies", which was
      true until subscriptions, gmail and canvas each added one. It now says five files,
      four deployed, subscriptions.conf parked without a service, and espnff deployed 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.md Status — 0.55.2, two releases out of date. Now 0.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 7 appears in CLAUDE.md twice and in authresults.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 in probes/ 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-info and the __pycache__
      directories. All regenerate; none has ever been committed.
    • The subscriptions tenant 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
  • v0.55.3 f4ccdd6316

    joeyr released this 2026-09-20 23:18:25 -05:00 | 54 commits to main since this release

    The 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_discussion returns 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.md records why probes/ 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
  • v0.55.2 b7780a9ed8

    joeyr released this 2026-09-20 23:09:54 -05:00 | 55 commits to main since this release

    Tenant #7 has a build plan. Documentation only; no weather code exists yet.

    Added

    • docs/weather-plan.md — the queue for a weather.mcp.jrendar.org tenant, read-only
      by construction like Canvas and espnff because 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 way get_discussion was.
    • 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.md Status — 0.52.0 and 1611 tests, three releases out of date. Now 0.55.2
      and 1643.
    • The docs/<tenant>-plan.md rule said no plan was open, which was true for four days
      after 0.55.0 deleted Canvas's. docs/weather-plan.md is open and the line names it.
    Downloads