• 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