From 7cf95464d634479c94b14ee59314cba55c79e336 Mon Sep 17 00:00:00 2001 From: turinglambdaai Date: Fri, 23 Oct 2026 17:14:01 +0800 Subject: [PATCH] feat(macos): ship an InternetAccessPolicy.plist (network transparency) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Taskly's trust story is "local-first, no telemetry, one network use" — now the bundle says so in the format firewall apps already speak. The macOS package carries an InternetAccessPolicy.plist (adopted from the Raycast teardown) declaring the github.com update feed, its purpose, and the deny consequences; Little Snitch-class firewalls surface it to the user verbatim, and denying it leaves Taskly fully functional offline. UPDATE.md states the single-endpoint contract. To carry the file, rivet gained `macos-resource-files` (#319, merged): a manifest key copying files into Contents/Resources root before signing. This bumps RIVET_PIN 1ebb3ac → 288c81f across all four workflows (check-dependency-pins green) and regenerates the clients with that rivet; release preflight stays aligned at 0.2.4. Verified locally: raco rivet package passes launch smoke with the plist at Contents/Resources/InternetAccessPolicy.plist (plutil OK) — the copy precedes signing, so the seal holds. --- .github/workflows/acceptance.yml | 4 +-- .github/workflows/ci.yml | 4 +-- .../workflows/release-version-client-sync.yml | 4 +-- .github/workflows/release.yml | 4 +-- CHANGELOG.md | 5 +++ assets/InternetAccessPolicy.plist | 27 +++++++++++++++ .../Sources/RivetHost/GeneratedBackend.swift | 34 +++++++++++++++++-- rivet.rktd | 2 +- shared/spec/UPDATE.md | 10 ++++++ 9 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 assets/InternetAccessPolicy.plist diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index 100b00e..2a5ba8e 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -15,7 +15,7 @@ concurrency: cancel-in-progress: true env: - RIVET_PIN: 1ebb3ac09ed2e23eeb0a91cbec6e689aac01ab1a + RIVET_PIN: 288c81f547eb64ec043546acc4df07efc19aceec KEEL_PIN: 0740fdfbdab7bf5dab32232c82f07dd98aa6a196 jobs: @@ -102,7 +102,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up embeddable Racket CS - uses: turinglambdaai/rivet/.github/actions/setup-embed-racket@1ebb3ac09ed2e23eeb0a91cbec6e689aac01ab1a + uses: turinglambdaai/rivet/.github/actions/setup-embed-racket@288c81f547eb64ec043546acc4df07efc19aceec - name: Install native and pinned Racket dependencies shell: bash run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6795c93..7cf8ba1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ concurrency: # Single source of truth for the rivet pin; every install step below # references this so a bump is one edit, not a grep-replace across jobs. env: - RIVET_PIN: 1ebb3ac09ed2e23eeb0a91cbec6e689aac01ab1a + RIVET_PIN: 288c81f547eb64ec043546acc4df07efc19aceec jobs: contract: name: Contract preflight (i18n + release metadata) @@ -87,7 +87,7 @@ jobs: # Builds and caches the embeddable Racket CS runtime and exports # RIVET_RACKET_* (rivet's own composite action). - name: Set up embeddable Racket CS - uses: turinglambdaai/rivet/.github/actions/setup-embed-racket@1ebb3ac09ed2e23eeb0a91cbec6e689aac01ab1a + uses: turinglambdaai/rivet/.github/actions/setup-embed-racket@288c81f547eb64ec043546acc4df07efc19aceec # Installed after the embeddable runtime so the catalog dependency # closure (cext-lib, crypto-lib, keel) resolves against the minimal # build — it ships no packages of its own. db backs taskly's storage; diff --git a/.github/workflows/release-version-client-sync.yml b/.github/workflows/release-version-client-sync.yml index c025809..bf5ba40 100644 --- a/.github/workflows/release-version-client-sync.yml +++ b/.github/workflows/release-version-client-sync.yml @@ -14,7 +14,7 @@ concurrency: cancel-in-progress: false env: - RIVET_PIN: 1ebb3ac09ed2e23eeb0a91cbec6e689aac01ab1a + RIVET_PIN: 288c81f547eb64ec043546acc4df07efc19aceec KEEL_PIN: 0740fdfbdab7bf5dab32232c82f07dd98aa6a196 jobs: @@ -48,7 +48,7 @@ jobs: libsecret-1-dev libglib2.0-dev - name: Set up embeddable Racket CS - uses: turinglambdaai/rivet/.github/actions/setup-embed-racket@1ebb3ac09ed2e23eeb0a91cbec6e689aac01ab1a + uses: turinglambdaai/rivet/.github/actions/setup-embed-racket@288c81f547eb64ec043546acc4df07efc19aceec - name: Install pinned Racket dependencies run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d92d754..7697b2a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ concurrency: # Single source of truth for the rivet pin; every install step below # references this so a bump is one edit, not a grep-replace across jobs. env: - RIVET_PIN: 1ebb3ac09ed2e23eeb0a91cbec6e689aac01ab1a + RIVET_PIN: 288c81f547eb64ec043546acc4df07efc19aceec jobs: macos: name: macOS (${{ matrix.arch }}) @@ -151,7 +151,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up embeddable Racket CS - uses: turinglambdaai/rivet/.github/actions/setup-embed-racket@1ebb3ac09ed2e23eeb0a91cbec6e689aac01ab1a + uses: turinglambdaai/rivet/.github/actions/setup-embed-racket@288c81f547eb64ec043546acc4df07efc19aceec # Installed after the embeddable runtime so the catalog dependency # closure (cext-lib, crypto-lib, keel) resolves against the minimal # build — it ships no packages of its own. db backs taskly's storage; diff --git a/CHANGELOG.md b/CHANGELOG.md index 555e554..e0afbc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Network transparency: the macOS bundle ships an + `InternetAccessPolicy.plist` declaring the update feed as the app's + only network use — firewall apps (Little Snitch and friends) show the + policy's per-host purpose and deny consequences to the user, and + denying it leaves Taskly fully functional offline (#58). - About dialog becomes the update/link/feedback portal on every platform: User Manual and Changelog links join Website and Report an Issue, and a diagnostics block (release identity, platform, database and config diff --git a/assets/InternetAccessPolicy.plist b/assets/InternetAccessPolicy.plist new file mode 100644 index 0000000..b176cdd --- /dev/null +++ b/assets/InternetAccessPolicy.plist @@ -0,0 +1,27 @@ + + + + + ApplicationDescription + Taskly is a local-first task manager: every task lives in one SQLite file on this Mac. It ships no telemetry and no account connections — its only network use is checking for and downloading application updates. + Connections + + + Host + github.com, *.github.com, objects.githubusercontent.com, *.objects.githubusercontent.com + IsIncoming + + NetworkProtocol + TCP + Port + 443 + Purpose + Taskly checks for new versions and downloads application updates: the signed update manifest and the update package itself. + DenyConsequences + If you deny these connections, Taskly keeps working fully offline — it just cannot check for or install updates. + Relevance + Essential + + + + diff --git a/macos-host/Sources/RivetHost/GeneratedBackend.swift b/macos-host/Sources/RivetHost/GeneratedBackend.swift index be5adb1..60a1cee 100644 --- a/macos-host/Sources/RivetHost/GeneratedBackend.swift +++ b/macos-host/Sources/RivetHost/GeneratedBackend.swift @@ -187,9 +187,39 @@ public enum RivetEvent: Sendable { } } +public protocol RivetGeneratedRPCClient: Sendable { + func call(_ name: String, arguments: [RivetValue]) async throws -> RivetValue +} + +// Keep generated clients compatible with the last released SwiftPM runtime. +// Authenticated network clients can use RivetAPI(call: deviceClient.call). +public extension RivetGeneratedRPCClient { + func getState(_ name: String) async throws -> RivetValue { + try await call("$state/get", arguments: [.string(name)]) + } + + @discardableResult + func setState(_ name: String, value: RivetValue) async throws -> RivetValue { + try await call("$state/set", arguments: [.string(name), value]) + } +} + +extension RivetClient: RivetGeneratedRPCClient {} + +private struct RivetGeneratedClosureClient: RivetGeneratedRPCClient { + let invoke: @Sendable (String, [RivetValue]) async throws -> RivetValue + + func call(_ name: String, arguments: [RivetValue]) async throws -> RivetValue { + try await invoke(name, arguments) + } +} + public struct RivetAPI: Sendable { - public let client: RivetClient - public init(client: RivetClient) { self.client = client } + public let client: any RivetGeneratedRPCClient + public init(client: any RivetGeneratedRPCClient) { self.client = client } + public init(call: @escaping @Sendable (String, [RivetValue]) async throws -> RivetValue) { + self.client = RivetGeneratedClosureClient(invoke: call) + } public func add_task(text: String, list_id: Int64?, due_date: String?, due_time: String?, notes: String?) async throws -> RivetTypes.Task { let rivetResult = try await client.call("add_task", arguments: [encode_String(text), encode__Optional_Int64_(list_id), encode__Optional_String_(due_date), encode__Optional_String_(due_time), encode__Optional_String_(notes)]) diff --git a/rivet.rktd b/rivet.rktd index c28bae4..e6b71d2 100644 --- a/rivet.rktd +++ b/rivet.rktd @@ -1 +1 @@ -#hasheq((name . "taskly") (display-name . "Taskly") (version . "0.2.4") (build . 1) (identifier . "app.taskly.Taskly") (macos-min-version . "14.0") (windows-min-version . "10.0.19041.0") (backend . "racket/taskly/backend.rkt") (module . "backend") (entry . "start") (protocol . 1) (windows-icon . "assets/taskly.ico") (macos-icon . "assets/taskly.icns") (dmg-background . "assets/dmg-background.png") (windows-installer-bitmaps . #hasheq((banner . "assets/wizard-banner.bmp") (dialog . "assets/wizard-dialog.bmp"))) (linux-icon . "assets/icon_512.png") (linux-formats . ("deb" "rpm" "appimage")) (resources . ("shared/i18n" "shared/emoji.json"))) +#hasheq((name . "taskly") (display-name . "Taskly") (version . "0.2.4") (build . 1) (identifier . "app.taskly.Taskly") (macos-min-version . "14.0") (windows-min-version . "10.0.19041.0") (backend . "racket/taskly/backend.rkt") (module . "backend") (entry . "start") (protocol . 1) (windows-icon . "assets/taskly.ico") (macos-icon . "assets/taskly.icns") (dmg-background . "assets/dmg-background.png") (windows-installer-bitmaps . #hasheq((banner . "assets/wizard-banner.bmp") (dialog . "assets/wizard-dialog.bmp"))) (macos-resource-files . ("assets/InternetAccessPolicy.plist")) (linux-icon . "assets/icon_512.png") (linux-formats . ("deb" "rpm" "appimage")) (resources . ("shared/i18n" "shared/emoji.json"))) diff --git a/shared/spec/UPDATE.md b/shared/spec/UPDATE.md index e3a2a26..41961fd 100644 --- a/shared/spec/UPDATE.md +++ b/shared/spec/UPDATE.md @@ -147,6 +147,16 @@ backend boundary every host shares: and packaged-acceptance jobs; rollback and manual-recovery notes per install shape live in the lifecycle section of this document. +## Network transparency + +The update feed is the application's only network use: Taskly ships no +telemetry, accounts, or any other connection. The macOS bundle carries +an `InternetAccessPolicy.plist` (packaged via rivet's +`macos-resource-files`) declaring the `github.com` feed hosts, their +purpose, and the deny consequences — firewall apps such as Little +Snitch surface that policy to the user verbatim. Denying the feed +leaves every other feature fully functional offline. + ## Ed25519 signature - Key type: Ed25519. Private key lives ONLY in the GitHub secret