• 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