poke-gate 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Gate.app ADDED
File without changes
package/README.md CHANGED
@@ -241,7 +241,7 @@ Poke Gate supports three access modes that control what your agent can do:
241
241
 
242
242
  | Mode | Description |
243
243
  |------|-------------|
244
- | **Full** (default) | All tools available. Risky actions (commands, file writes, screenshots) require chat approval. |
244
+ | **Full** (default) | All tools available with no approval required. The agent can run commands, write files, and take screenshots directly. |
245
245
  | **Limited** | Read-only tools plus a curated set of safe commands (`ls`, `cat`, `grep`, `curl`, etc.). `write_file` and `take_screenshot` are disabled. |
246
246
  | **Sandbox** | Broader command support than Limited, but writes are restricted to `~/Downloads` and `/tmp` via macOS `sandbox-exec`. |
247
247
 
@@ -32,9 +32,9 @@ class GateService: ObservableObject {
32
32
 
33
33
  var subtitle: String {
34
34
  switch self {
35
- case .full: return "All tools are available, subject to chat approval."
36
- case .limited: return "Safe tools and curated command families only."
37
- case .sandbox: return "Broader command support, but strictly limited by macOS sandbox-exec policies."
35
+ case .full: return "All tools enabled. Risky actions require chat approval."
36
+ case .limited: return "Read-only tools and safe commands (ls, cat, grep, curl…). File writes and screenshots disabled."
37
+ case .sandbox: return "Commands like brew, node, python, ffmpeg allowed. Writes restricted to ~/Downloads and /tmp."
38
38
  }
39
39
  }
40
40
  }
@@ -91,6 +91,7 @@ class GateService: ObservableObject {
91
91
 
92
92
  @Published var status: Status = .stopped
93
93
  @Published var logs: [String] = []
94
+ @Published var processLogs: [String] = []
94
95
  @Published var terminalPreviews: [TerminalPreview] = []
95
96
  @Published var userName: String? = nil
96
97
  @Published var permissionMode: PermissionMode
@@ -108,10 +109,26 @@ class GateService: ObservableObject {
108
109
  private var activeTerminalPreviewId: UUID?
109
110
  private var permissionPollingTimer: Timer?
110
111
 
112
+ private var appActiveObserver: NSObjectProtocol?
113
+
111
114
  init() {
112
115
  self.permissionMode = Self.loadPermissionModeStatic()
113
116
  self.hasCompletedSetup = Self.loadHasCompletedSetupStatic()
114
117
  refreshSystemPermissions()
118
+
119
+ appActiveObserver = NotificationCenter.default.addObserver(
120
+ forName: NSApplication.didBecomeActiveNotification,
121
+ object: nil,
122
+ queue: .main
123
+ ) { [weak self] _ in
124
+ self?.refreshSystemPermissions()
125
+ }
126
+ }
127
+
128
+ deinit {
129
+ if let observer = appActiveObserver {
130
+ NotificationCenter.default.removeObserver(observer)
131
+ }
115
132
  }
116
133
 
117
134
  var hasSystemPermissionsGranted: Bool {
@@ -177,20 +194,24 @@ class GateService: ObservableObject {
177
194
  }
178
195
 
179
196
  let requested = missingSystemPermissions.map { $0.title }.joined(separator: ", ")
180
- appendLog("Opening macOS settings for: \(requested).")
197
+ appendLog("Requesting macOS permissions: \(requested).")
181
198
 
182
199
  for permission in missingSystemPermissions {
183
- guard let url = URL(string: permission.settingsURL) else { continue }
184
- NSWorkspace.shared.open(url)
200
+ openSystemPermission(permission)
185
201
  }
186
-
187
- appendLog("Opened Privacy settings for missing permissions.")
188
202
  }
189
203
 
190
204
  func refreshSystemPermissions() {
205
+ let previous = systemPermissionStatuses
191
206
  systemPermissionStatuses = SystemPermission.allCases.map { permission in
192
207
  SystemPermissionStatus(permission: permission, isGranted: isPermissionGranted(permission))
193
208
  }
209
+ for status in systemPermissionStatuses {
210
+ let was = previous.first(where: { $0.permission == status.permission })
211
+ if was == nil || was?.isGranted != status.isGranted {
212
+ appendLog("\(status.permission.title): \(status.isGranted ? "granted" : "not granted")")
213
+ }
214
+ }
194
215
  }
195
216
 
196
217
  func startPermissionPolling() {
@@ -208,8 +229,11 @@ class GateService: ObservableObject {
208
229
  }
209
230
 
210
231
  func openSystemPermission(_ permission: SystemPermission) {
211
- guard let url = URL(string: permission.settingsURL) else { return }
212
- NSWorkspace.shared.open(url)
232
+ switch permission {
233
+ case .accessibility:
234
+ let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true] as CFDictionary
235
+ _ = AXIsProcessTrustedWithOptions(options)
236
+ }
213
237
  }
214
238
 
215
239
  func captureAndSend() {
@@ -466,11 +490,21 @@ class GateService: ObservableObject {
466
490
  outputPipe?.fileHandleForReading.readabilityHandler = nil
467
491
  process = nil
468
492
  outputPipe = nil
493
+ killOrphanedProcesses()
494
+ }
495
+
496
+ private func killOrphanedProcesses() {
497
+ let task = Process()
498
+ task.executableURL = URL(fileURLWithPath: "/usr/bin/pkill")
499
+ task.arguments = ["-f", "poke-gate"]
500
+ try? task.run()
501
+ task.waitUntilExit()
469
502
  }
470
503
 
471
504
  private func handleOutput(_ raw: String) {
472
505
  for line in raw.components(separatedBy: .newlines) where !line.isEmpty {
473
506
  appendLog(line)
507
+ appendProcessLog(line)
474
508
  parseTerminalPreviewLine(line)
475
509
 
476
510
  if line.contains("Tunnel connected") || line.contains("Ready") {
@@ -487,6 +521,15 @@ class GateService: ObservableObject {
487
521
  }
488
522
  }
489
523
 
524
+ private func appendProcessLog(_ line: String) {
525
+ let stripped = stripToolTimestamp(from: line)
526
+ guard !stripped.isEmpty else { return }
527
+ processLogs.append(stripped)
528
+ if processLogs.count > maxLogs {
529
+ processLogs.removeFirst(processLogs.count - maxLogs)
530
+ }
531
+ }
532
+
490
533
  private func handleTermination(exitCode: Int32) {
491
534
  appendLog("Process exited with code \(exitCode)")
492
535
  stopHealthCheck()
@@ -637,7 +680,19 @@ class GateService: ObservableObject {
637
680
  private func isPermissionGranted(_ permission: SystemPermission) -> Bool {
638
681
  switch permission {
639
682
  case .accessibility:
640
- return AXIsProcessTrusted()
683
+ if AXIsProcessTrusted() { return true }
684
+ // AXIsProcessTrusted() can return false despite the toggle being ON
685
+ // in System Settings when the TCC entry's code signature doesn't match
686
+ // the running binary (ad-hoc signing, rebuild from Xcode, etc.).
687
+ // Probe the API directly as a fallback.
688
+ let systemWide = AXUIElementCreateSystemWide()
689
+ var value: AnyObject?
690
+ let result = AXUIElementCopyAttributeValue(
691
+ systemWide,
692
+ kAXFocusedApplicationAttribute as CFString,
693
+ &value
694
+ )
695
+ return result == .success || result == .noValue || result == .attributeUnsupported
641
696
  }
642
697
  }
643
698
 
@@ -176,11 +176,7 @@ struct PopoverContent: View {
176
176
  VStack(alignment: .leading, spacing: 6) {
177
177
  sectionTitle("Recent activity")
178
178
 
179
- if service.terminalPreviews.isEmpty {
180
- Text("No activity yet")
181
- .font(.caption)
182
- .foregroundStyle(.secondary)
183
- } else {
179
+ if !service.terminalPreviews.isEmpty {
184
180
  ForEach(Array(service.terminalPreviews.suffix(4).enumerated()), id: \.element.id) { _, entry in
185
181
  HStack(spacing: 6) {
186
182
  Circle()
@@ -193,6 +189,18 @@ struct PopoverContent: View {
193
189
  .truncationMode(.tail)
194
190
  }
195
191
  }
192
+ } else if !service.processLogs.isEmpty {
193
+ ForEach(Array(service.processLogs.suffix(4).enumerated()), id: \.offset) { _, entry in
194
+ Text(entry)
195
+ .font(.system(size: 9, design: .monospaced))
196
+ .foregroundStyle(.tertiary)
197
+ .lineLimit(1)
198
+ .truncationMode(.tail)
199
+ }
200
+ } else {
201
+ Text("No activity yet")
202
+ .font(.caption)
203
+ .foregroundStyle(.secondary)
196
204
  }
197
205
  }
198
206
  .frame(maxWidth: .infinity, alignment: .leading)
@@ -228,6 +236,11 @@ struct PopoverContent: View {
228
236
  }
229
237
  }
230
238
 
239
+ Text(activeModeDescription)
240
+ .font(.caption2)
241
+ .foregroundStyle(.secondary)
242
+ .fixedSize(horizontal: false, vertical: true)
243
+
231
244
  if service.permissionMode == .full || pendingFullMode {
232
245
  AccessibilityPermissionView(service: service)
233
246
  }
@@ -237,6 +250,18 @@ struct PopoverContent: View {
237
250
  .macPanelStyle(.neutral, cornerRadius: 12)
238
251
  }
239
252
 
253
+ private var activeModeDescription: String {
254
+ let mode = pendingFullMode ? GateService.PermissionMode.full : service.permissionMode
255
+ switch mode {
256
+ case .full:
257
+ return "All tools enabled. Risky actions require chat approval."
258
+ case .limited:
259
+ return "Read-only tools and safe commands only (ls, cat, grep, curl…). File writes and screenshots are disabled."
260
+ case .sandbox:
261
+ return "Broader commands (brew, node, python, ffmpeg…) but file writes restricted to ~/Downloads and /tmp via macOS sandbox."
262
+ }
263
+ }
264
+
240
265
  private var actionsSection: some View {
241
266
  HStack(spacing: 12) {
242
267
  ActionButton(icon: "text.alignleft", label: "Logs") {
@@ -44,6 +44,8 @@ struct SettingsView: View {
44
44
  }
45
45
  .padding(20)
46
46
  .frame(width: 430)
47
+ .onAppear { service.startPermissionPolling() }
48
+ .onDisappear { service.stopPermissionPolling() }
47
49
  }
48
50
 
49
51
  private var authenticationSection: some View {
@@ -174,7 +174,7 @@
174
174
  COPY_PHASE_STRIP = NO;
175
175
  DEAD_CODE_STRIPPING = YES;
176
176
  DEBUG_INFORMATION_FORMAT = dwarf;
177
- DEVELOPMENT_TEAM = RJA7656U34;
177
+ DEVELOPMENT_TEAM = "";
178
178
  ENABLE_STRICT_OBJC_MSGSEND = YES;
179
179
  ENABLE_TESTABILITY = YES;
180
180
  ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -240,7 +240,7 @@
240
240
  COPY_PHASE_STRIP = NO;
241
241
  DEAD_CODE_STRIPPING = YES;
242
242
  DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
243
- DEVELOPMENT_TEAM = RJA7656U34;
243
+ DEVELOPMENT_TEAM = "";
244
244
  ENABLE_NS_ASSERTIONS = NO;
245
245
  ENABLE_STRICT_OBJC_MSGSEND = YES;
246
246
  ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -272,7 +272,7 @@
272
272
  COMBINE_HIDPI_IMAGES = YES;
273
273
  CURRENT_PROJECT_VERSION = 1;
274
274
  DEAD_CODE_STRIPPING = YES;
275
- DEVELOPMENT_TEAM = RJA7656U34;
275
+ DEVELOPMENT_TEAM = "";
276
276
  ENABLE_APP_SANDBOX = NO;
277
277
  ENABLE_HARDENED_RUNTIME = NO;
278
278
  ENABLE_PREVIEWS = YES;
@@ -286,7 +286,7 @@
286
286
  "@executable_path/../Frameworks",
287
287
  );
288
288
  MACOSX_DEPLOYMENT_TARGET = 15.0;
289
- MARKETING_VERSION = 0.1.8;
289
+ MARKETING_VERSION = 0.2.1;
290
290
  PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
291
291
  PRODUCT_NAME = "$(TARGET_NAME)";
292
292
  REGISTER_APP_GROUPS = YES;
@@ -322,7 +322,7 @@
322
322
  "@executable_path/../Frameworks",
323
323
  );
324
324
  MACOSX_DEPLOYMENT_TARGET = 15.0;
325
- MARKETING_VERSION = 0.1.9;
325
+ MARKETING_VERSION = 0.2.1;
326
326
  PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
327
327
  PRODUCT_NAME = "$(TARGET_NAME)";
328
328
  REGISTER_APP_GROUPS = YES;
package/docs/security.md CHANGED
@@ -10,7 +10,7 @@ Poke Gate supports three access modes that control what tools your agent can use
10
10
 
11
11
  ### Full (default)
12
12
 
13
- All tools are available. Risky actions (`run_command`, `write_file`, `take_screenshot`) require **chat approval** the agent must ask you in chat before executing, and you approve with a signed token.
13
+ All tools are available with no approval required. The agent can run commands, write files, and take screenshots directly.
14
14
 
15
15
  ### Limited
16
16
 
@@ -48,13 +48,15 @@ POKE_GATE_PERMISSION_MODE=sandbox npx poke-gate
48
48
 
49
49
  ## Tool approval flow
50
50
 
51
- In **full** mode, risky tools (`run_command`, `write_file`, `take_screenshot`) use an HMAC-signed approval flow:
51
+ In **limited** and **sandbox** modes, risky tools (`run_command`, `write_file`, `take_screenshot`) use an HMAC-signed approval flow:
52
52
 
53
53
  1. The agent calls the tool — Poke Gate returns `AWAITING_APPROVAL` with a signed token
54
54
  2. The agent asks you in chat to approve
55
55
  3. You approve — the agent re-calls the tool with the approval token
56
56
  4. Optionally, you can `remember_in_session` (same command) or `remember_all_risky` (all risky tools for the session)
57
57
 
58
+ In **full** mode, all tools execute directly without approval.
59
+
58
60
  ## What protects you
59
61
 
60
62
  - **Authentication** — only your Poke agent (authenticated via Poke OAuth) can reach the tunnel
package/macOS ADDED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poke-gate",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Expose your machine to your Poke AI assistant via MCP tunnel",
5
5
  "type": "module",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -2,10 +2,23 @@ import { startMcpServer, enableLogging, getPermissionMode } from "./mcp-server.j
2
2
  import { startTunnel } from "./tunnel.js";
3
3
  import { startAgentScheduler, stopAgentScheduler } from "./agents.js";
4
4
  import { Poke, isLoggedIn, login, getToken } from "poke";
5
+ import { execSync } from "node:child_process";
5
6
 
6
7
  const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
7
8
  enableLogging(verbose);
8
9
 
10
+ function killExistingInstances() {
11
+ const myPid = process.pid;
12
+ try {
13
+ const out = execSync("pgrep -f 'poke-gate'", { encoding: "utf-8" }).trim();
14
+ const pids = out.split("\n").map(Number).filter((p) => p && p !== myPid);
15
+ for (const pid of pids) {
16
+ try { process.kill(pid, "SIGTERM"); } catch {}
17
+ }
18
+ if (pids.length > 0) log(`Killed ${pids.length} existing poke-gate process(es).`);
19
+ } catch {}
20
+ }
21
+
9
22
  function log(msg) {
10
23
  const ts = new Date().toISOString().slice(11, 19);
11
24
  console.log(`[${ts}] ${msg}`);
@@ -102,6 +115,7 @@ function scheduleReconnect(mcpUrl, token) {
102
115
  }
103
116
 
104
117
  async function main() {
118
+ killExistingInstances();
105
119
  log("poke-gate starting...");
106
120
  log(`Access mode: ${getPermissionMode()}`);
107
121
 
@@ -115,14 +129,38 @@ async function main() {
115
129
  await connectWithRetry(mcpUrl, token);
116
130
  }
117
131
 
132
+ function buildAccessModeMessage(mode) {
133
+ switch (mode) {
134
+ case "limited":
135
+ return (
136
+ "Access mode: Limited. " +
137
+ "You can read files, list directories, and run safe read-only commands (ls, cat, grep, curl, jq…). " +
138
+ "You cannot write files, take screenshots, or run other commands."
139
+ );
140
+ case "sandbox":
141
+ return (
142
+ "Access mode: Sandbox. " +
143
+ "You can read files, list directories, and run commands like brew, node, python, ffmpeg, curl, and more. " +
144
+ "File writes are restricted to ~/Downloads and /tmp by macOS sandbox. Screenshots are disabled."
145
+ );
146
+ default:
147
+ return (
148
+ "Access mode: Full. " +
149
+ "You can run any shell command, read and write files, list directories, take screenshots, and check system info. " +
150
+ "Use the tools directly whenever needed — no approval required."
151
+ );
152
+ }
153
+ }
154
+
118
155
  async function notifyPoke(connectionId, token) {
119
156
  try {
157
+ const mode = getPermissionMode();
120
158
  const poke = new Poke({ token });
121
159
  await poke.sendMessage(
122
160
  `Hey! I've connected my computer to you via Poke Gate (tunnel: ${connectionId}). ` +
123
- `You can now run commands, read and write files, list directories, take screenshots, and check system info on my machine. ` +
124
- `Just use the tools whenever I ask you to do something on my computer.` +
125
- `Now reply me with "now I am connected to your computer" but everytime write those replies in most creativev fun way.`
161
+ `${buildAccessModeMessage(mode)} ` +
162
+ `Just use the tools whenever I ask you to do something on my computer. ` +
163
+ `Now reply me with "now I am connected to your computer" but everytime write those replies in most creative fun way.`
126
164
  );
127
165
  log("Notified Poke agent about connection.");
128
166
  } catch (err) {
package/src/mcp-server.js CHANGED
@@ -565,7 +565,7 @@ function handleToolCall(name, args, context = {}) {
565
565
  return blocked;
566
566
  }
567
567
 
568
- if (permissionService.isRisky(name)) {
568
+ if (PERMISSION_MODE !== "full" && permissionService.isRisky(name)) {
569
569
  const commandText = typeof cleanArgs.command === "string" ? cleanArgs.command : "";
570
570
  const alreadyAllowed = sessionAutoApproveAllRisky.has(sessionId) ||
571
571
  (commandText && permissionService.isAllowedBySessionPattern(sessionId, commandText));