Major Update: macOS Sequoia (15+) Support, Enhanced App Onboarding, and Tool Execution Security#7
Conversation
Replaces the previous Full Disk Access-centric onboarding with an accessibility-first flow. The app now asks for Accessibility permission before anything else, which is the actual entitlement required for automation tasks (AppleScript, key injection, UI scripting). Changes: - Add dedicated AccessibilityPermissionView with step-by-step grant UI - Add SetupView to sequence onboarding steps in a single coherent flow - Refresh permission state on app-foreground (NSApp.applicationDidBecomeActive) - Update GateService with cleaner permission state machine and state management - Update SettingsView to reflect actual runtime permission state - Update AboutView: The version info now dynamically pulls from the app bundle (CFBundleShortVersionString), ensuring the UI auto-updates whenever a new version is released without manual changes - Update Info.plist, and project settings for correct entitlements - Add src/permission-service.js: HMAC-signed approval tokens, per-session pattern whitelist, and safe/risky tool classification - Add sandbox-exec integration in mcp-server.js: risky tools run inside macOS sandbox-exec when available; falls back gracefully when unavailable - Add four new test files covering permission service, access policy, loop guards, and sandbox command behavior (97 + 40 + 57 + 18 test cases) Technical summary: Refactors macOS onboarding and permission checks to prioritize Accessibility for automation tasks. Introduces dynamic versioning in the About section that auto-updates with the bundle version.
The previous workflow targeted a rolling runner label that drifted to an incompatible image, causing intermittent build failures unrelated to code. Changes (.github/workflows/release.yml): - Switch GitHub Actions runner from macos-latest / macos-26 to macos-15 - Explicitly select /Applications/Xcode_16.4.app/Contents/Developer via xcode-select to pin the toolchain and avoid implicit Xcode version changes - Make the Homebrew tap update step conditional on HOMEBREW_TOKEN availability - DMG upload and version-bump behavior unchanged Technical summary: Hardens release automation against runner drift. The runner and Xcode version are now pinned to a known-good combination (macos-15 + Xcode 16.4).
Brings the repository to a clean lint state for both JS and Markdown so every future PR starts from a green baseline. Changes: - Remove unused variable bindings and dead imports in src/agents.js, src/app.js, and example agent scripts - Expand src/mcp-server.js with full run_command sandbox plumbing - Fix markdown list-indentation and reference-link warnings across docs/ - Align docs wording with updated macOS permission behavior - Add superpowers/ to .gitignore
- Change Screen Recording to Accessibility in README tool list - and permissions notes - Update sandbox policy string in GateService.swift - to emphasize macOS sandbox-exec limitations - Update getting-started.md to require Accessibility - permission and guide through a Setup View with - access mode selection - Add a Setup section to macOS app docs - and update the macOS Xcode requirement to 16+ (was 26+)
Fix a missing closing brace for the body computed property in PopoverContent struct. The compiler was interpreting private member variables as being in local scope, causing "Attribute private can only be used in a non-local scope" errors across five properties. Root cause: PopoverContent.body was unclosed, causing the following private members (statusText, statusColor, modeChipTitle, etc.) to be interpretted as local declarations inside the body closure. Resolution: Added the missing closing brace after the onChange modifier to properly terminate the body computed variable scope.
…pattern Fix log clearing functionality that broke due to direct array mutation not triggering Combine notifications. Problem: LogView.swift directly mutated the logs array with service.logs.removeAll(), which does not trigger @published notifications. Once the array was cleared this way, the reactive binding between the model and view became inconsistent, making logs impossible to view properly. Solution: 1. Add clearLogs() method to GateService that properly reassigns the array: logs = [] (reassignment triggers @published notification) 2. Update LogsView to call service.clearLogs() instead of mutating directly This ensures that when users clear logs via the UI, the @published property change is properly broadcast to all SwiftUI subscribers, maintaining reactive consistency across the app. Pattern note: Always reassign @published properties rather than mutating them in-place to ensure Combine sends update notifications.
There was a problem hiding this comment.
Pull request overview
Hardens MCP tool execution with access modes, per-session approvals, command sandboxing, and loop guards, while refactoring the macOS menu bar app onboarding to an Accessibility-first setup and adding dynamic version display.
Changes:
- Added server-side permission service with HMAC-based approvals + session whitelisting, plus MCP access policy modes (full/limited/sandbox) and sandbox-exec wrapping.
- Added loop-guard for
run_command, terminal preview logging, and a newnetwork_speedtool. - Refactored macOS app UX: Setup view + permissions UI, access-mode selection, permission polling, and dynamic app version display; updated docs and release workflow.
Reviewed changes
Copilot reviewed 26 out of 28 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| test/permission-service.test.js | Adds unit tests for risky tool classification, session whitelist behavior, and token validation/TTL. |
| test/mcp-server-sandbox-command.test.js | Tests sandbox command wrapping/escaping behavior. |
| test/mcp-server-loop-guard.test.js | Tests run_command loop suppression logic. |
| test/mcp-server-access-policy.test.js | Tests access policy decisions across permission modes. |
| src/permission-service.js | Introduces HMAC-based approval and per-session whitelisting utilities. |
| src/mcp-server.js | Implements permission modes, access policy evaluation, sandbox wrapping, loop guard, approvals flow, and new network_speed tool. |
| src/app.js | Minor runtime cleanup and changed notification text sent to agent. |
| src/agents.js | Makes node_modules symlink creation more robust across platforms and fixes URL path handling. |
| package.json | Adds node --test scripts. |
| examples/agents/screentime.24h.js | Refactors exec usage (but introduces logic/type issues). |
| examples/agents/battery.30m.js | Adjusts fs imports. |
| docs/macos-app.md | Updates macOS app docs for new setup flow and Xcode requirement. |
| docs/index.md | Fixes YAML/markdown formatting for the docs landing page. |
| docs/getting-started.md | Updates onboarding instructions to Accessibility-first setup and setup view flow. |
| clients/Poke macOS Gate/Poke macOS Gate/SetupView.swift | Adds new onboarding flow (access mode + permissions steps). |
| clients/Poke macOS Gate/Poke macOS Gate/SettingsView.swift | Adds access mode selection and embeds Accessibility permission UI. |
| clients/Poke macOS Gate/Poke macOS Gate/Poke_macOS_GateApp.swift | Adds Setup window, dynamic about version string, and terminal preview UI wiring. |
| clients/Poke macOS Gate/Poke macOS Gate/PermissionRowView.swift | Adds reusable permission row component. |
| clients/Poke macOS Gate/Poke macOS Gate/LogsView.swift | Routes “clear logs” through GateService API. |
| clients/Poke macOS Gate/Poke macOS Gate/Info.plist | Adds CFBundleIdentifier entry. |
| clients/Poke macOS Gate/Poke macOS Gate/GateService.swift | Adds permission mode + setup persistence, system permission checks/polling, and terminal preview parsing. |
| clients/Poke macOS Gate/Poke macOS Gate/AccessibilityPermissionView.swift | Adds dedicated Accessibility permission prompting/status UI. |
| clients/Poke macOS Gate/Poke macOS Gate/AboutView.swift | Adds dynamic app version from bundle. |
| clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.pbxproj | Updates deployment target, versioning, signing/team settings, and build options. |
| README.md | Rewrites onboarding/docs to match new permission model and app UX. |
| .gitignore | Ignores superpowers/. |
| .github/workflows/release.yml | Stabilizes CI runner + pins Xcode path + makes tap update optional in forks. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function evaluateAccessPolicy(toolName, cleanArgs, mode = PERMISSION_MODE) { | ||
| if (mode === "full") return null; | ||
|
|
||
| if (mode === "limited") { | ||
| if (SAFE_TOOL_NAMES.has(toolName)) return null; | ||
| if (toolName === "run_command") { | ||
| return validateRunCommandAgainstAllowlist(cleanArgs.command, LIMITED_RUN_COMMANDS); | ||
| } | ||
| if (toolName === "write_file" || toolName === "take_screenshot") { | ||
| return "This tool is disabled in Limited Permissions mode."; | ||
| } | ||
| return "This tool is not permitted in Limited Permissions mode."; | ||
| } | ||
|
|
||
| if (SAFE_TOOL_NAMES.has(toolName)) return null; | ||
|
|
||
| if (toolName === "run_command") { | ||
| return validateRunCommandAgainstAllowlist(cleanArgs.command, SANDBOX_RUN_COMMANDS); | ||
| } | ||
|
|
||
| if (toolName === "write_file" || toolName === "take_screenshot") { | ||
| return "This tool is disabled in Sandbox mode."; | ||
| } | ||
|
|
||
| return "This tool is not permitted in Sandbox mode."; | ||
| } |
There was a problem hiding this comment.
The new "network_speed" tool is treated as a SAFE tool (bypasses approval and mode restrictions), but it executes a shell script via runCommand and forces permissionMode: "full" (unsandboxed). This is an access-policy bypass in both Limited and Sandbox modes. Make "network_speed" either (a) a risky tool requiring the same approval + mode policy checks, or (b) implement it without invoking a shell (e.g., direct HTTP requests) and, if still using runCommand, ensure it honors PERMISSION_MODE (apply OS sandbox when in sandbox mode) and is denied/limited appropriately in limited mode.
|
|
||
| const cmd = parts.join(" && "); | ||
|
|
||
| return runCommand(cmd, homedir(), { permissionMode: "full" }).then((result) => { |
There was a problem hiding this comment.
The new "network_speed" tool is treated as a SAFE tool (bypasses approval and mode restrictions), but it executes a shell script via runCommand and forces permissionMode: "full" (unsandboxed). This is an access-policy bypass in both Limited and Sandbox modes. Make "network_speed" either (a) a risky tool requiring the same approval + mode policy checks, or (b) implement it without invoking a shell (e.g., direct HTTP requests) and, if still using runCommand, ensure it honors PERMISSION_MODE (apply OS sandbox when in sandbox mode) and is denied/limited appropriately in limited mode.
| return runCommand(cmd, homedir(), { permissionMode: "full" }).then((result) => { | |
| return runCommand(cmd, homedir(), { permissionMode: PERMISSION_MODE }).then((result) => { |
| validateApprovalToken(sessionId, token, toolName, toolArgs) { | ||
| const record = this.pendingApprovals.get(token); | ||
| if (!record) return false; | ||
| if (record.consumed) return false; | ||
|
|
||
| if (nowMs(this.now) > record.expiresAt) { | ||
| this.pendingApprovals.delete(token); | ||
| return false; | ||
| } | ||
|
|
||
| const argsHash = hashArgs(toolArgs); | ||
| if ( | ||
| record.sessionId !== sessionId || | ||
| record.toolName !== toolName || | ||
| record.argsHash !== argsHash | ||
| ) { | ||
| return false; | ||
| } | ||
|
|
||
| record.consumed = true; | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Consumed approvals are never removed from pendingApprovals (only marked consumed: true). Over time this can cause unbounded in-memory growth for long-running servers (especially if agents request approval frequently). Consider deleting the token record on successful validation (instead of keeping it consumed), and adding periodic pruning of expired tokens (since tokens that are never validated won’t be deleted).
| function buildSandboxProfile() { | ||
| const userHome = homedir(); | ||
| return [ | ||
| "(version 1)", | ||
| "(deny default)", | ||
| "(import \"system.sb\")", | ||
| "(allow process-exec)", | ||
| "(allow process-fork)", | ||
| "(allow file-read*)", | ||
| "(allow network-outbound)", | ||
| "(allow sysctl-read)", | ||
| "(allow file-write*", | ||
| ` (subpath "${userHome}/Downloads")`, | ||
| " (subpath \"/private/tmp\")", | ||
| " (subpath \"/tmp\")", | ||
| ")", | ||
| ].join("\n"); | ||
| } |
There was a problem hiding this comment.
The sandbox profile enables (allow file-read*) without any path constraints, which largely defeats using OS sandboxing as a meaningful containment boundary (commands can read arbitrary files). If the goal is to reduce blast radius in Sandbox mode, constrain reads to specific subpaths (e.g., user home, project/work directories, Downloads, tmp) and explicitly allow only what's needed by the allowlisted commands. If broad reads are required, document that sandbox mode primarily constrains writes/exec/network rather than read access.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| stopPermissionPolling() | ||
| permissionPollingTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in | ||
| Task { @MainActor [weak self] in | ||
| self?.refreshSystemPermissions() | ||
| } | ||
| } |
There was a problem hiding this comment.
Permission polling every 1s is likely to run continuously for the lifetime of the menu bar app (and it’s started from multiple places). This can cause unnecessary wakeups and battery impact. Prefer an event-driven refresh (e.g., refresh on NSApp.applicationDidBecomeActive / foreground transitions) and/or a slower polling interval only while the Setup/Settings UI is visible.
| stopPermissionPolling() | |
| permissionPollingTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in | |
| Task { @MainActor [weak self] in | |
| self?.refreshSystemPermissions() | |
| } | |
| } | |
| // Ensure any existing timer is stopped before starting a new one. | |
| stopPermissionPolling() | |
| // Refresh immediately so UI reflects the latest permission state. | |
| refreshSystemPermissions() | |
| // If there are no missing permissions, there is nothing to poll for. | |
| guard !missingSystemPermissions.isEmpty else { return } | |
| // Poll less frequently and stop automatically once all permissions are granted, | |
| // to avoid continuous high-frequency wakeups for the lifetime of the app. | |
| let timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in | |
| Task { @MainActor [weak self] in | |
| guard let self = self else { return } | |
| self.refreshSystemPermissions() | |
| // Stop polling once all required permissions have been granted. | |
| if self.missingSystemPermissions.isEmpty { | |
| self.stopPermissionPolling() | |
| } | |
| } | |
| } | |
| // Allow some flexibility for the system to coalesce timer firings and save power. | |
| timer.tolerance = 1.0 | |
| permissionPollingTimer = timer |
| execSync(`defaults read com.apple.ScreenTimeAgent 2>/dev/null || echo "{}"`, { | ||
| encoding: "utf-8", | ||
| timeout: 10000, | ||
| }).trim(); |
There was a problem hiding this comment.
In getScreenTime(), the defaults read ... output is now discarded (previously stored in result), which likely breaks any later parsing/logic that depended on it. In getActiveApps(), the function now returns a trimmed string instead of an array (it previously returned result.split(\", \")), creating a type mismatch with the catch { return []; } path and any callers expecting an array.
| return execSync(`osascript -e '${script}'`, { | ||
| encoding: "utf-8", | ||
| timeout: 10000, | ||
| }).trim(); |
There was a problem hiding this comment.
In getScreenTime(), the defaults read ... output is now discarded (previously stored in result), which likely breaks any later parsing/logic that depended on it. In getActiveApps(), the function now returns a trimmed string instead of an array (it previously returned result.split(\", \")), creating a type mismatch with the catch { return []; } path and any callers expecting an array.
| `You can now run commands, read and write files, list directories, take screenshots, and check system info on my machine. ` + | ||
| `Just use the tools whenever I ask you to do something on my computer.` + | ||
| `Now reply me in my language "now I am connected to your computer".` | ||
| `Now reply me with "now I am connected to your computer" but everytime write those replies in most creativev fun way.` |
There was a problem hiding this comment.
Correct spelling of 'creativev' to 'creative' (and consider rewriting this sentence for clarity, since it changes agent behavior in a way unrelated to onboarding/security).
| <plist version="1.0"> | ||
| <dict> | ||
| <key>CFBundleIdentifier</key> | ||
| <string>dev.fka.Poke-macOS-Gate</string> |
There was a problem hiding this comment.
Hardcoding CFBundleIdentifier in Info.plist can drift from Xcode’s PRODUCT_BUNDLE_IDENTIFIER and makes multi-target/config builds harder. Prefer setting CFBundleIdentifier to $(PRODUCT_BUNDLE_IDENTIFIER) and keeping the identifier value in the project build settings.
| <string>dev.fka.Poke-macOS-Gate</string> | |
| <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> |
| ### Building from source | ||
|
|
||
| Requires macOS 15+ and Xcode 26+. | ||
| Want to customize it? You'll need macOS 15+ and Xcode 15+: |
There was a problem hiding this comment.
Docs are inconsistent about the required Xcode version: README says Xcode 15+, but docs/macos-app.md says Xcode 16+. Align these requirements (and ideally tie them to the deployment target / toolchain actually used in CI, e.g. Xcode 16.4 in the release workflow).
| Want to customize it? You'll need macOS 15+ and Xcode 15+: | |
| Want to customize it? You'll need macOS 15+ and Xcode 16+: |
|
@copilot open a new pull request to apply changes based on the comments in this thread |
|
@emSoumik I've released 0.1.9 which includes "Start Poke Gate at login" feature. Can you update your PR with latest update? |
|
Also if we want to make it available for macOS 15x, we need to create 2 macOS build for 26 and 15 both. Otherwise it'll create the app in 15 design which look bad on macOS Liquid Glass. |
fad8623 to
dca8065
Compare
| CURRENT_PROJECT_VERSION = 1; | ||
| DEVELOPMENT_TEAM = RJA7656U34; | ||
| DEAD_CODE_STRIPPING = YES; | ||
| DEVELOPMENT_TEAM = 4KW2PB5PGF; |
There was a problem hiding this comment.
can you remove this line or emppty? since we're distributing it without notarization we can skip it.
| CURRENT_PROJECT_VERSION = 1; | ||
| DEVELOPMENT_TEAM = RJA7656U34; | ||
| DEAD_CODE_STRIPPING = YES; | ||
| DEVELOPMENT_TEAM = 4KW2PB5PGF; |
| MACOSX_DEPLOYMENT_TARGET = 26.0; | ||
| MARKETING_VERSION = 0.1.9; | ||
| MACOSX_DEPLOYMENT_TARGET = 15.0; | ||
| MARKETING_VERSION = 0.1.8; |
| MACOSX_DEPLOYMENT_TARGET = 26.0; | ||
| MARKETING_VERSION = 0.1.9; | ||
| MACOSX_DEPLOYMENT_TARGET = 15.0; | ||
| MARKETING_VERSION = 0.1.8; |
| --- | ||
|
|
||
| Run Poke Gate on your Mac, then message Poke from iMessage, Telegram, or SMS to run commands, read files, take screenshots, and more — all on your machine. | ||
| ## Just ask Poke from your phone |
| ## Quick Start | ||
|
|
||
| ### 1. Get an API key | ||
| Visit [poke.com/kitchen/api-keys](https://poke.com/kitchen/api-keys) and grab one. |
There was a problem hiding this comment.
api key is not needed. it works with npx poke login. it used to be a requirement before.
| ↓ | ||
| Poke Gate on your Mac runs it locally | ||
| ↓ | ||
| Result comes back to your chat |
|
Feels like README changes are unnecessary. Can you discard README file changes? I'll update it later. |
Done! Commit 3e24208 reverts the README to the upstream version and drops those changes as requested. |
Split release workflow into verify + release stages. Verify now runs on both macOS-15 and macOS-26 to catch compatibility issues early. Release stage uses Xcode 26 with dynamic runner selection. ### Verification Stage - New `verify` job: builds on macOS-15 and macOS-26 in parallel - Validates Xcode 26 availability with fallback error handling - Early failure prevents release if build fails ### Release Stage - Migrated from hardcoded `Xcode_16.4` to dynamic `Xcode_26` selection - Added `.app` glob pattern to future-proof against minor version updates - Depends on verify job passing
Per review feedback, README changes are unnecessary to this PR. Reverting to upstream/main version to keep focus on core functionality updates: macOS app UI refactoring and CI/CD pipeline modernization.
Addressed this in the 229d6c5 This does not add two separate app builds. Instead, it keeps one app with a version-aware UI path:
So the fix here is not "ship two downloads." It is "ship one adaptive app":
I also updated CI so we verify the app on both |
New Features & Changes Summary
This release focuses on hardening the security of MCP tool execution and streamlining the macOS onboarding experience with an accessibility-first model.
1. macOS App Permission Architecture (Major Refactor)
The app now prioritizes an Accessibility-first model, which is the actual entitlement required for UI automation, key injection, and AppleScript tasks.
New Components:
UI Changes and Permission Flow :
CFBundleShortVersionString), removing the need for hardcoded version strings in the UI.NSApp.applicationDidBecomeActive).AXIsProcessTrusted()validation to verify automation access directly via the Accessibility API.2. Cloud Backend & Permission Service
Security Enhancements:
src/permission-service.js) for HMAC-signed tool approval, pattern-based whitelisting, and session state management.sandbox=osfor risky tools executed under/usr/bin/sandbox-exec.Modified Files:
src/app.js,src/agents.js,src/mcp-server.js: Integrated permission service gateways and removed unused variable bindings for a clean runtime state.3. Release Workflow Stability (CI/CD)
Release Pipeline Updates:
macos-26to macos-15 for improved stability./Applications/Xcode_16.4.app/Contents/Developerto avoid environment drift.TAP_REPO_TOKEN_HOMEBREWis missing, preventing CI failures in forks.4. Documentation & Examples
Final Validation
Bundle.main.appVersion.npm run lintandnpm run lint:mdexit without errors.