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