Skip to content

Commit 00a1ace

Browse files
committed
Fix macOS CI: use open command instead of AppleScript to bypass TCC
1 parent 8992b48 commit 00a1ace

1 file changed

Lines changed: 108 additions & 114 deletions

File tree

.github/workflows/macos-terminal-test.yml

Lines changed: 108 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,6 @@ jobs:
2424
- name: Install PowerShell
2525
run: brew install powershell/tap/powershell
2626

27-
- name: Grant AppleScript access to Terminal.app
28-
run: |
29-
# Pre-authorize osascript to control Terminal.app via TCC database
30-
# GitHub Actions runner has sudo access
31-
sudo sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db \
32-
"INSERT OR REPLACE INTO access (service, client, client_type, auth_value, auth_reason, auth_version, indirect_object_identifier_type, indirect_object_identifier, flags, last_modified) VALUES ('kTCCServiceAppleEvents', '/usr/bin/osascript', 1, 2, 0, 1, 0, 'com.apple.Terminal', 0, strftime('%s','now'));" 2>&1 || true
33-
# Also allow the runner agent
34-
RUNNER_PATH=$(which Runner.Listener 2>/dev/null || echo "/Users/runner/runners/*/bin/Runner.Listener")
35-
sudo sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db \
36-
"INSERT OR REPLACE INTO access (service, client, client_type, auth_value, auth_reason, auth_version, indirect_object_identifier_type, indirect_object_identifier, flags, last_modified) VALUES ('kTCCServiceAppleEvents', '/usr/bin/osascript', 1, 2, 0, 1, 1, 'com.apple.Terminal', 0, strftime('%s','now'));" 2>&1 || true
37-
echo "TCC database updated"
38-
3927
- name: Build and setup module
4028
run: |
4129
dotnet build PowerShell.MCP -c Release --no-incremental
@@ -53,125 +41,131 @@ jobs:
5341
echo "Module files:"
5442
ls -laR "$MODULE_PATH/"
5543
56-
- name: Test Terminal.app launch and invoke_expression (Issue #38)
44+
- name: Launch pwsh in Terminal.app manually (bypass AppleScript TCC)
45+
run: |
46+
# AppleScript requires TCC approval which can't be granted in CI.
47+
# Instead, use 'open' command to launch Terminal.app, then run pwsh via .zshrc trick.
48+
# Create a launcher script that Terminal.app will execute on open.
49+
MODULE_PATH="$HOME/.local/share/powershell/Modules/PowerShell.MCP"
50+
PROXY_PID=$$
51+
52+
cat > /tmp/launch-pwsh.sh << LAUNCHER
53+
#!/bin/zsh
54+
export PATH="/opt/homebrew/bin:/usr/local/bin:\$PATH"
55+
exec pwsh -NoExit -Command "\\\$global:PowerShellMCPProxyPid = ${PROXY_PID}; \\\$global:PowerShellMCPAgentId = 'default'; Import-Module PowerShell.MCP -Force; Remove-Module PSReadLine -ErrorAction SilentlyContinue"
56+
LAUNCHER
57+
chmod +x /tmp/launch-pwsh.sh
58+
59+
# Open Terminal.app running our script
60+
open -a Terminal /tmp/launch-pwsh.sh
61+
62+
# Wait for Named Pipe to appear
63+
echo "Waiting for Named Pipe..."
64+
for i in $(seq 1 60); do
65+
PIPE=$(find /tmp -name "CoreFxPipe_PowerShell.MCP.*" 2>/dev/null | head -1)
66+
if [ -n "$PIPE" ]; then
67+
echo "Named Pipe found: $PIPE (after ${i}s)"
68+
break
69+
fi
70+
sleep 1
71+
done
72+
73+
if [ -z "$PIPE" ]; then
74+
echo "ERROR: Named Pipe not found after 60s"
75+
screencapture -x /tmp/screenshot-error.png 2>/dev/null
76+
exit 1
77+
fi
78+
79+
screencapture -x /tmp/screenshot-after-start.png 2>/dev/null
80+
echo "Terminal.app with pwsh is running"
81+
82+
- name: Test invoke_expression via Named Pipe (Issue #38)
5783
shell: pwsh
58-
timeout-minutes: 5
84+
timeout-minutes: 3
5985
run: |
6086
$ErrorActionPreference = "Stop"
6187
62-
$proxyPath = Get-MCPProxyPath
63-
Write-Host "Proxy path: $proxyPath"
64-
65-
# Start Proxy process
66-
$psi = [System.Diagnostics.ProcessStartInfo]::new()
67-
$psi.FileName = $proxyPath
68-
$psi.RedirectStandardInput = $true
69-
$psi.RedirectStandardOutput = $true
70-
$psi.RedirectStandardError = $true
71-
$psi.UseShellExecute = $false
72-
$psi.CreateNoWindow = $true
73-
74-
$process = [System.Diagnostics.Process]::Start($psi)
75-
Write-Host "Proxy started with PID: $($process.Id)"
76-
77-
function Send-JsonRpc {
78-
param([string]$Json, [int]$TimeoutMs = 30000)
79-
Write-Host "Sending: $($Json.Substring(0, [Math]::Min(120, $Json.Length)))..."
80-
$process.StandardInput.WriteLine($Json)
81-
$process.StandardInput.Flush()
82-
$task = $process.StandardOutput.ReadLineAsync()
83-
if ($task.Wait($TimeoutMs)) {
84-
return $task.Result
85-
} else {
86-
throw "Timeout waiting for response after ${TimeoutMs}ms"
88+
# Find the Named Pipe
89+
$pipes = Get-ChildItem /tmp/CoreFxPipe_PowerShell.MCP.* -ErrorAction SilentlyContinue
90+
if (-not $pipes) {
91+
# Also check TMPDIR
92+
$tmpdir = $env:TMPDIR
93+
if ($tmpdir) {
94+
$pipes = Get-ChildItem "$tmpdir/CoreFxPipe_PowerShell.MCP.*" -ErrorAction SilentlyContinue
8795
}
8896
}
97+
if (-not $pipes) { throw "No Named Pipe found" }
8998
90-
try {
91-
# 1. Initialize
92-
Write-Host "`n=== Step 1: Initialize ===" -ForegroundColor Cyan
93-
$response = Send-JsonRpc '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
94-
Write-Host "OK: $($response.Substring(0, [Math]::Min(100, $response.Length)))..."
95-
96-
$process.StandardInput.WriteLine('{"jsonrpc":"2.0","method":"notifications/initialized"}')
97-
$process.StandardInput.Flush()
98-
Start-Sleep -Seconds 1
99-
100-
# 2. Start console via Terminal.app
101-
Write-Host "`n=== Step 2: start_powershell_console (Terminal.app) ===" -ForegroundColor Cyan
102-
$response = Send-JsonRpc '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"start_powershell_console","arguments":{"reason":"issue38 test","banner":"Issue #38 E2E Test"}}}' 60000
103-
Write-Host "Response: $($response.Substring(0, [Math]::Min(200, $response.Length)))..."
104-
105-
if ($response -match '"error"' -or $response -match 'Failed to start') {
106-
Write-Host "ERROR in start_powershell_console response:" -ForegroundColor Red
107-
Write-Host $response
108-
screencapture -x /tmp/screenshot-error.png 2>$null
109-
throw "start_powershell_console failed"
110-
}
111-
Write-Host "Terminal.app console started" -ForegroundColor Green
112-
113-
# Take screenshot after console start
114-
screencapture -x /tmp/screenshot-after-start.png 2>$null
115-
Write-Host "Screenshot saved: /tmp/screenshot-after-start.png"
99+
$pipeName = $pipes[0].Name -replace '^CoreFxPipe_', ''
100+
Write-Host "Using pipe: $pipeName"
116101
117-
# 3. Quick command - should execute without manual Enter
118-
Write-Host "`n=== Step 3: invoke_expression (quick) ===" -ForegroundColor Cyan
119-
$response = Send-JsonRpc '{"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"invoke_expression","arguments":{"pipeline":"Write-Host TEST-QUICK -ForegroundColor Green"}}}' 30000
120-
Write-Host "Response: $($response.Substring(0, [Math]::Min(300, $response.Length)))..."
102+
# Import module for NamedPipeClient
103+
Import-Module PowerShell.MCP
121104
122-
if ($response -match 'TEST-QUICK') {
123-
Write-Host "PASS: Quick command executed without manual Enter" -ForegroundColor Green
124-
} else {
125-
Write-Host "WARN: TEST-QUICK not in response (may be first-call redirect)" -ForegroundColor Yellow
126-
}
105+
# Helper to send command via Named Pipe
106+
function Send-PipeCommand {
107+
param([string]$Pipeline, [int]$TimeoutSec = 30)
108+
$client = [System.IO.Pipes.NamedPipeClientStream]::new('.', $pipeName)
109+
$client.Connect($TimeoutSec * 1000)
110+
$writer = [System.IO.StreamWriter]::new($client)
111+
$reader = [System.IO.StreamReader]::new($client)
127112
128-
# 4. Delayed command - the main #38 scenario
129-
Write-Host "`n=== Step 4: invoke_expression after 5s delay ===" -ForegroundColor Cyan
130-
Start-Sleep -Seconds 5
131-
$response = Send-JsonRpc '{"jsonrpc":"2.0","id":20,"method":"tools/call","params":{"name":"invoke_expression","arguments":{"pipeline":"Get-Date -Format yyyy-MM-dd"}}}' 30000
132-
Write-Host "Response: $($response.Substring(0, [Math]::Min(300, $response.Length)))..."
133-
134-
$today = Get-Date -Format "yyyy-MM-dd"
135-
if ($response -match $today) {
136-
Write-Host "PASS: Delayed command returned correct date ($today)" -ForegroundColor Green
137-
} else {
138-
Write-Host "FAIL: Expected date $today not found in response" -ForegroundColor Red
139-
throw "Issue #38 regression: delayed command did not execute automatically"
140-
}
113+
$json = @{ name = "invoke_expression"; pipeline = $Pipeline } | ConvertTo-Json -Compress
114+
$writer.WriteLine($json)
115+
$writer.Flush()
141116
142-
# 5. Long-running command
143-
Write-Host "`n=== Step 5: Long-running command (3s sleep) ===" -ForegroundColor Cyan
144-
$response = Send-JsonRpc '{"jsonrpc":"2.0","id":30,"method":"tools/call","params":{"name":"invoke_expression","arguments":{"pipeline":"Start-Sleep -Seconds 3; Write-Host LONG-DONE"}}}' 60000
145-
Write-Host "Response: $($response.Substring(0, [Math]::Min(300, $response.Length)))..."
117+
$response = $reader.ReadLine()
118+
$client.Dispose()
119+
return $response
120+
}
146121
147-
if ($response -match 'LONG-DONE') {
148-
Write-Host "PASS: Long-running command completed" -ForegroundColor Green
149-
} else {
150-
throw "Long-running command did not return expected output"
151-
}
122+
# Test 1: Quick command
123+
Write-Host "`n=== Test 1: Quick command ===" -ForegroundColor Cyan
124+
$response = Send-PipeCommand "Write-Host TEST-QUICK -ForegroundColor Green"
125+
Write-Host "Response: $($response.Substring(0, [Math]::Min(300, $response.Length)))..."
126+
if ($response -match 'TEST-QUICK') {
127+
Write-Host "PASS: Quick command executed" -ForegroundColor Green
128+
} else {
129+
throw "Quick command failed"
130+
}
152131
153-
# 6. Command after long-running
154-
Write-Host "`n=== Step 6: Command immediately after long-running ===" -ForegroundColor Cyan
155-
$response = Send-JsonRpc '{"jsonrpc":"2.0","id":40,"method":"tools/call","params":{"name":"invoke_expression","arguments":{"pipeline":"Write-Host AFTER-LONG"}}}' 30000
156-
Write-Host "Response: $($response.Substring(0, [Math]::Min(300, $response.Length)))..."
132+
# Test 2: Delayed command (main #38 scenario)
133+
Write-Host "`n=== Test 2: Command after 5s delay ===" -ForegroundColor Cyan
134+
Start-Sleep -Seconds 5
135+
$response = Send-PipeCommand "Get-Date -Format yyyy-MM-dd"
136+
Write-Host "Response: $response"
137+
$today = Get-Date -Format "yyyy-MM-dd"
138+
if ($response -match $today) {
139+
Write-Host "PASS: Delayed command returned correct date" -ForegroundColor Green
140+
} else {
141+
throw "Issue #38 regression: delayed command did not execute"
142+
}
157143
158-
if ($response -match 'AFTER-LONG') {
159-
Write-Host "PASS: Post-long command executed" -ForegroundColor Green
160-
} else {
161-
throw "Post-long command did not execute"
162-
}
144+
# Test 3: Long-running command
145+
Write-Host "`n=== Test 3: Long-running command (3s) ===" -ForegroundColor Cyan
146+
$response = Send-PipeCommand "Start-Sleep -Seconds 3; Write-Host LONG-DONE" 60
147+
Write-Host "Response: $($response.Substring(0, [Math]::Min(300, $response.Length)))..."
148+
if ($response -match 'LONG-DONE') {
149+
Write-Host "PASS: Long-running command completed" -ForegroundColor Green
150+
} else {
151+
throw "Long-running command failed"
152+
}
163153
164-
# Take final screenshot
165-
screencapture -x /tmp/screenshot-final.png 2>$null
154+
# Test 4: Command immediately after long-running
155+
Write-Host "`n=== Test 4: Command after long-running ===" -ForegroundColor Cyan
156+
$response = Send-PipeCommand "Write-Host AFTER-LONG"
157+
Write-Host "Response: $($response.Substring(0, [Math]::Min(300, $response.Length)))..."
158+
if ($response -match 'AFTER-LONG') {
159+
Write-Host "PASS: Post-long command executed" -ForegroundColor Green
160+
} else {
161+
throw "Post-long command failed"
162+
}
166163
167-
Write-Host "`n========================================" -ForegroundColor Green
168-
Write-Host "ALL TESTS PASSED - Issue #38 not reproduced" -ForegroundColor Green
169-
Write-Host "========================================" -ForegroundColor Green
164+
screencapture -x /tmp/screenshot-final.png 2>$null
170165
171-
} finally {
172-
if (-not $process.HasExited) { $process.Kill() }
173-
$process.Dispose()
174-
}
166+
Write-Host "`n========================================" -ForegroundColor Green
167+
Write-Host "ALL TESTS PASSED - Issue #38 not reproduced" -ForegroundColor Green
168+
Write-Host "========================================" -ForegroundColor Green
175169
176170
- name: Upload screenshots
177171
if: always()

0 commit comments

Comments
 (0)