Skip to content

Commit 75b3674

Browse files
authored
Sign desktop with the Apple Events entitlement so macOS can prompt (#1876)
1 parent 98d6c6a commit 75b3674

4 files changed

Lines changed: 66 additions & 6 deletions

File tree

‎.changeset/desktop-apple-events.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@executor-js/desktop": patch
3+
"@executor-js/plugin-mcp": patch
4+
---
5+
6+
Let macOS ask before denying Codex plugins Automation access. The desktop app
7+
and its bundled daemon are hardened-runtime signed without the Apple Events
8+
entitlement, so tccd refused to even show the consent prompt: every Messages
9+
call was denied silently, no Automation row was ever created in System
10+
Settings, and the access check sat on "Checking…" for a full minute before
11+
misreporting the hang as a failed start. The app and daemon are now signed
12+
with `com.apple.security.automation.apple-events` and carry a usage
13+
description, so the first call raises the real consent prompt and the grant
14+
becomes visible in Privacy & Security → Automation.
15+
16+
The access check also stops waiting after 25 seconds and says what a hang
17+
means — answer the permission prompt on screen, then check again — instead of
18+
blaming the Codex install.

‎apps/desktop/build/entitlements.mac.plist‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,14 @@
1717
of the Electron bundle's signing chain. -->
1818
<key>com.apple.security.cs.disable-library-validation</key>
1919
<true/>
20+
<!-- Codex plugins (Messages) drive other apps via Apple Events, and TCC
21+
attributes those to the RESPONSIBLE process — this app and the
22+
bundled executor daemon, both signed with this file. Under hardened
23+
runtime, tccd refuses to even PROMPT without this entitlement
24+
("Policy disallows prompt"): the call is denied silently, no
25+
Automation row is created, and there is nothing for the user to
26+
enable. -->
27+
<key>com.apple.security.automation.apple-events</key>
28+
<true/>
2029
</dict>
2130
</plist>

‎apps/desktop/electron-builder.config.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ const config: Configuration = {
3737
entitlements: "build/entitlements.mac.plist",
3838
entitlementsInherit: "build/entitlements.mac.plist",
3939
notarize: true,
40+
extendInfo: {
41+
// Shown in the macOS Automation consent prompt. Required alongside the
42+
// apple-events entitlement in entitlements.mac.plist: without the
43+
// usage string, tccd declines to prompt and denies silently.
44+
NSAppleEventsUsageDescription:
45+
"Executor runs local plugins that control apps like Messages on your behalf.",
46+
},
4047
},
4148
// Same arch rule as mac (see comment above): never pin `arch:` in the
4249
// target objects. The win/linux pins used to force both archs out of a

‎packages/plugins/mcp/src/sdk/plugin.ts‎

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1391,6 +1391,20 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
13911391
// scanner touches node:fs, so it stays behind a dynamic import (the
13921392
// stdio-connector pattern) and behind the stdio gate: with stdio off the
13931393
// presets could not be added anyway.
1394+
/** How long the probe waits for the plugin to answer. Deliberately
1395+
* under the MCP SDK's 60s default: a pending macOS consent prompt
1396+
* blocks the call indefinitely, and the person should be told to look
1397+
* for the prompt rather than watch "Checking…" for a minute. */
1398+
const PROBE_ANSWER_TIMEOUT_MS = 25_000;
1399+
/** The client SDK signals its request timeout as an `SdkError` with
1400+
* code `REQUEST_TIMEOUT`. Matched structurally: the SDK is loaded
1401+
* dynamically, so its error class is not importable here. */
1402+
const isMcpRequestTimeout = (cause: unknown): boolean =>
1403+
typeof cause === "object" &&
1404+
cause !== null &&
1405+
"code" in cause &&
1406+
(cause as { readonly code: unknown }).code === "REQUEST_TIMEOUT";
1407+
13941408
/** Ask a Codex plugin whether macOS will actually let it work.
13951409
*
13961410
* Runs the plugin's own read-only probe tool down the REAL path — the
@@ -1424,15 +1438,26 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
14241438
...(plugin.appServer === undefined ? {} : { appServer: plugin.appServer }),
14251439
});
14261440

1441+
// A probe that hangs is a real outcome, not an edge case: an Apple
1442+
// Event blocks for as long as macOS sits on the consent decision.
1443+
// Under the SDK's 60s default the card shows "Checking…" for a full
1444+
// minute and then misreports the hang as a failed start.
1445+
let timedOut = false;
14271446
return yield* Effect.gen(function* () {
14281447
const connection = yield* connector;
14291448
const result = yield* Effect.tryPromise({
1430-
try: () => connection.client.callTool({ name: probe.name, arguments: probe.args }),
1431-
catch: () =>
1432-
new McpConnectionError({
1449+
try: () =>
1450+
connection.client.callTool(
1451+
{ name: probe.name, arguments: probe.args },
1452+
{ timeout: PROBE_ANSWER_TIMEOUT_MS },
1453+
),
1454+
catch: (cause) => {
1455+
timedOut = isMcpRequestTimeout(cause);
1456+
return new McpConnectionError({
14331457
transport: "appserver",
14341458
message: "The plugin did not answer.",
1435-
}),
1459+
});
1460+
},
14361461
}).pipe(Effect.ensuring(Effect.promise(() => connection.close())));
14371462

14381463
const text = (Array.isArray(result.content) ? result.content : [])
@@ -1454,8 +1479,9 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
14541479
McpConnectionError: () =>
14551480
Effect.succeed({
14561481
status: "blocked" as const,
1457-
message:
1458-
"Could not start the plugin. Check that Codex is installed and signed in.",
1482+
message: timedOut
1483+
? "macOS has not answered yet. If a permission prompt is on screen, answer it, then check again."
1484+
: "Could not start the plugin. Check that Codex is installed and signed in.",
14591485
}),
14601486
McpOAuthReauthorizationRequired: () =>
14611487
Effect.succeed({

0 commit comments

Comments
 (0)