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 +0 -0
- package/README.md +1 -1
- package/clients/Poke macOS Gate/Poke macOS Gate/GateService.swift +66 -11
- package/clients/Poke macOS Gate/Poke macOS Gate/Poke_macOS_GateApp.swift +30 -5
- package/clients/Poke macOS Gate/Poke macOS Gate/SettingsView.swift +2 -0
- package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.pbxproj +5 -5
- package/docs/security.md +4 -2
- package/macOS +0 -0
- package/package.json +1 -1
- package/src/app.js +41 -3
- package/src/mcp-server.js +1 -1
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.
|
|
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
|
|
36
|
-
case .limited: return "
|
|
37
|
-
case .sandbox: return "
|
|
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("
|
|
197
|
+
appendLog("Requesting macOS permissions: \(requested).")
|
|
181
198
|
|
|
182
199
|
for permission in missingSystemPermissions {
|
|
183
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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") {
|
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 **
|
|
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
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
|
-
|
|
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
|
|
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));
|