• v0.9.3 12342d8356

    v0.9.3
    All checks were successful
    CI / gate (push) Successful in 1m28s
    CI / image (push) Successful in 57s
    Stable

    joeyr released this 2026-08-10 22:15:35 -05:00 | 85 commits to main since this release

    Secret and supply-chain hygiene — phase 3 of docs/audit.md. No behaviour change to any
    tool. Also records a live finding that removes a feature from the roadmap.

    Fixed

    • Feed URLs are redacted wherever they are written down. Config.summary() logged
      every ICS_FEEDS entry in full, under a comment asserting they "are public subscription
      URLs and carry no credential". True of Apple's holiday file; false of most other ways
      people subscribe to a calendar. Google's "secret address in iCal format", an iCloud
      private share link, an Outlook published-calendar URL and virtually every self-hosted
      equivalent are capability URLs — possession of the string is the authorisation, and
      there is no revoking one short of regenerating the address.

      Nothing validated which kind was configured, and a boot log is durable and gets pasted
      into issues, which is the same argument this project already makes for never logging
      even a prefix of the app password. Now reduced to scheme, host and the last path
      segment, with the query string dropped — enough to tell a typo'd host from a 404. The
      fetch-failure log line and every FeedUnavailable message go through the same
      reduction, since those reach the boot log through the preflight check's detail.

    • Feed decompression is bounded. MAX_FEED_BYTES was enforced against the compressed
      body only, and _decode then handed up to 5 MB to gzip.decompress with no ceiling on
      the output. Gzip reaches roughly 1000:1 on repetitive input, which is exactly what an
      ICS file is, so a hostile or compromised feed host could turn one allowed response into
      gigabytes of allocation in a process running one replica. Now decompressed incrementally
      with the cap checked before each chunk is requested, so the bomb is never held.

    • REGISTRY_TOKEN is passed via env: rather than interpolated into the step's
      script text, matching the rule the workflow already stated for GITHUB_TOKEN and
      applied nowhere else.

    Added

    • pip-audit runs in gate. Clean at the time of writing. Deliberately advisory
      rather than blocking: image has needs: gate and the stack tracks :latest, so a red
      gate stops production receiving any update — an advisory published overnight against a
      transitive dependency would block the very push that fixes something urgent. The
      reasoning is recorded next to the step rather than left to be rediscovered.

    • The base image is pinned by digest, so two builds of the same commit produce the
      same image and a rollback to a :<sha> tag is a rollback to the same base. Pinned to
      the multi-arch index digest, so it still resolves to linux/arm64/v8 on the Pi runners.

    • Upper bounds on the four version-sensitive dependenciesfastmcp, caldav,
      icalendar, recurring-ical-events. Each has already renamed or moved an API this code
      calls, and each such change was silent here rather than loud: caldav's expand=
      became server_expand= and the old spelling is swallowed into **searchargs, which
      presents as a read that quietly stops expanding.

    • secrets/ and *.secret excluded from the Docker build context. No secret reaches
      the image today, because the Dockerfile copies only pyproject.toml and mcp_gateway/.
      The exposure was entirely future-tense: one COPY . . would put a live OAuth client
      secret and the token signing key into a layer of a public image.

    Changed

    • Free-busy is off the roadmap permanently, and INVITES_ENABLED now gates attendees
      alone. Probed against the live account, read-only: iCloud answers a free-busy-query
      REPORT with 400 Bad Request and refuses the RFC 6638 scheduling-outbox POST with
      403 Forbidden, even when asking about the account's own address — despite
      advertising both the outbox and the inbox, returning 207 on them, and granting
      read-free-busy. That is Apple declining the operation for an app-specific password,
      the same shape as the 0.8.1 412. Recorded in docs/plan.md so nobody rediscovers it.

      find_free_time is unaffected: it computes availability from events this server reads
      itself, which is now the only way availability will be computed here.

    Downloads