Skip to content

Commit 0695076

Browse files
neo-gpttobiu
andauthored
fix(ai): guard osascript frontmost delivery (#10422) (#12424)
Co-authored-by: tobiu <tobiasuhlig78@gmail.com>
1 parent 6ddfc4f commit 0695076

2 files changed

Lines changed: 67 additions & 2 deletions

File tree

ai/daemons/bridge/daemon.mjs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,15 @@ function spawnAsync(command, args) {
598598
});
599599
}
600600

601+
/**
602+
* @summary Escapes values interpolated into AppleScript string literals.
603+
* @param {String} value
604+
* @returns {String}
605+
*/
606+
function escapeAppleScriptString(value) {
607+
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
608+
}
609+
601610
/**
602611
* Global mutex for serializing adapter deliveries.
603612
* Prevents concurrent osascript calls from colliding when multiple agents wake simultaneously.
@@ -725,20 +734,49 @@ async function deliverDigest(subscription, digest) {
725734
// Instance-addressed wake raises the resolved pid's process to frontmost (verified
726735
// addressable via System Events `whose unix id`); single-instance wakes keep the
727736
// app-activate path unchanged.
737+
const appleScriptAppName = escapeAppleScriptString(appName),
738+
targetProcessId = instancePid ? String(instancePid) : '';
739+
728740
const activateLine = instancePid
729741
? ` tell application "System Events" to set frontmost of (first process whose unix id is ${instancePid}) to true`
730-
: ` tell application "${appName}" to activate`;
742+
: ` tell application "${appleScriptAppName}" to activate`;
731743

732744
const osascriptArgs = [
745+
'-e', 'on assertTargetFrontmost(appName, targetBundleId, targetProcessId, phase)',
746+
'-e', ' tell application "System Events"',
747+
'-e', ' set frontmostProcess to first application process whose frontmost is true',
748+
'-e', ' if targetProcessId is not "" then',
749+
'-e', ' set currentPid to (unix id of frontmostProcess) as string',
750+
'-e', ' if currentPid is not targetProcessId then error "Target app lost frontmost status " & phase & " (pid " & currentPid & " != " & targetProcessId & ")"',
751+
'-e', ' else if targetBundleId is not "" then',
752+
'-e', ' set currentBundleId to ""',
753+
'-e', ' try',
754+
'-e', ' set currentBundleId to (bundle identifier of frontmostProcess) as string',
755+
'-e', ' end try',
756+
'-e', ' if currentBundleId is not targetBundleId then error "Target app lost frontmost status " & phase & " (bundle " & currentBundleId & " != " & targetBundleId & ")"',
757+
'-e', ' else',
758+
'-e', ' set currentApp to name of frontmostProcess',
759+
'-e', ' if currentApp is not appName then error "Target app lost frontmost status " & phase & " (" & currentApp & " != " & appName & ")"',
760+
'-e', ' end if',
761+
'-e', ' end tell',
762+
'-e', 'end assertTargetFrontmost',
733763
'-e', 'on run argv',
734764
'-e', ' set wakePayload to (item 1 of argv)',
765+
'-e', ` set targetAppName to "${appleScriptAppName}"`,
766+
'-e', ' set targetBundleId to ""',
767+
'-e', ' try',
768+
'-e', ` set targetBundleId to id of application "${appleScriptAppName}"`,
769+
'-e', ' end try',
770+
'-e', ` set targetProcessId to "${targetProcessId}"`,
735771
'-e', ' try',
736772
'-e', ' set savedClipboard to the clipboard as string',
737773
'-e', ' on error',
738774
'-e', ' set savedClipboard to ""',
739775
'-e', ' end try',
776+
'-e', ' try',
740777
'-e', activateLine,
741778
'-e', ' delay 0.5',
779+
'-e', ' my assertTargetFrontmost(targetAppName, targetBundleId, targetProcessId, "after activation")',
742780
'-e', ' tell application "System Events"',
743781
'-e', ' set frontmostProcess to first application process whose frontmost is true',
744782
'-e', ' tell frontmostProcess'
@@ -777,6 +815,7 @@ async function deliverDigest(subscription, digest) {
777815
}
778816

779817
osascriptArgs.push(
818+
'-e', ' my assertTargetFrontmost(targetAppName, targetBundleId, targetProcessId, "before prompt clear")',
780819
'-e', ' set the clipboard to ""',
781820
'-e', ' keystroke "a" using command down',
782821
'-e', ' delay 0.2',
@@ -789,8 +828,10 @@ async function deliverDigest(subscription, digest) {
789828
'-e', ' on error',
790829
'-e', ' set userInput to ""',
791830
'-e', ' end try',
831+
'-e', ' my assertTargetFrontmost(targetAppName, targetBundleId, targetProcessId, "before wake clipboard set")',
792832
'-e', ' set the clipboard to wakePayload',
793833
'-e', ' delay 0.2',
834+
'-e', ' my assertTargetFrontmost(targetAppName, targetBundleId, targetProcessId, "before wake paste")',
794835
'-e', ' tell application "System Events"',
795836
'-e', ' set frontmostProcess to first application process whose frontmost is true',
796837
'-e', ' tell frontmostProcess',
@@ -801,8 +842,10 @@ async function deliverDigest(subscription, digest) {
801842
'-e', ' end tell',
802843
'-e', ' end tell',
803844
'-e', ' if userInput is not "" then',
845+
'-e', ' my assertTargetFrontmost(targetAppName, targetBundleId, targetProcessId, "before user input restore clipboard set")',
804846
'-e', ' set the clipboard to userInput',
805847
'-e', ' delay 0.2',
848+
'-e', ' my assertTargetFrontmost(targetAppName, targetBundleId, targetProcessId, "before user input restore paste")',
806849
'-e', ' tell application "System Events"',
807850
'-e', ' set frontmostProcess to first application process whose frontmost is true',
808851
'-e', ' tell frontmostProcess',
@@ -812,6 +855,10 @@ async function deliverDigest(subscription, digest) {
812855
'-e', ' end if',
813856
'-e', ' delay 0.5',
814857
'-e', ' set the clipboard to savedClipboard',
858+
'-e', ' on error errMsg',
859+
'-e', ' set the clipboard to savedClipboard',
860+
'-e', ' error errMsg',
861+
'-e', ' end try',
815862
'-e', 'end run',
816863
digest
817864
);

test/playwright/unit/ai/daemons/bridge/daemon.spec.mjs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -701,7 +701,7 @@ test.describe('Bridge Daemon', () => {
701701
expect(args.join(' ')).not.toContain('key code 49');
702702
});
703703

704-
test('Claude default focus seed emits r -> Cmd+Z before prompt clear (#10987)', async () => {
704+
test('Claude default focus seed emits r -> Cmd+Z before prompt clear and guards frontmost (#10987, #10422)', async () => {
705705
const subId = 'sub_' + crypto.randomUUID();
706706
const agentId = '@test-agent-claude';
707707

@@ -787,12 +787,30 @@ test.describe('Bridge Daemon', () => {
787787
const rIndex = scriptContent.indexOf('keystroke "r"');
788788
const zIndex = scriptContent.indexOf('keystroke "z" using command down');
789789
const clearIndex = scriptContent.indexOf('keystroke "a" using command down');
790+
const guardAfterActivationIndex = scriptContent.indexOf('my assertTargetFrontmost(targetAppName, targetBundleId, targetProcessId, "after activation")');
791+
const guardBeforeClearIndex = scriptContent.indexOf('my assertTargetFrontmost(targetAppName, targetBundleId, targetProcessId, "before prompt clear")');
792+
const guardBeforeWakeSetIndex = scriptContent.indexOf('my assertTargetFrontmost(targetAppName, targetBundleId, targetProcessId, "before wake clipboard set")');
793+
const wakePayloadIndex = scriptContent.indexOf('set the clipboard to wakePayload');
794+
const guardBeforeWakePasteIndex = scriptContent.indexOf('my assertTargetFrontmost(targetAppName, targetBundleId, targetProcessId, "before wake paste")');
795+
const pasteIndex = scriptContent.indexOf('keystroke "v" using command down');
790796

791797
expect(activateIndex).toBeGreaterThan(-1);
798+
expect(scriptContent).toContain('on assertTargetFrontmost(appName, targetBundleId, targetProcessId, phase)');
799+
expect(scriptContent).toContain('set targetBundleId to id of application "Claude"');
800+
expect(scriptContent).toContain('bundle identifier of frontmostProcess');
801+
expect(scriptContent).toContain('set the clipboard to savedClipboard\n error errMsg');
792802
expect(tabIndex).toBeGreaterThan(activateIndex);
803+
expect(guardAfterActivationIndex).toBeGreaterThan(activateIndex);
804+
expect(guardAfterActivationIndex).toBeLessThan(tabIndex);
793805
expect(rIndex).toBeGreaterThan(tabIndex);
794806
expect(zIndex).toBeGreaterThan(rIndex);
807+
expect(guardBeforeClearIndex).toBeGreaterThan(zIndex);
808+
expect(guardBeforeClearIndex).toBeLessThan(clearIndex);
795809
expect(clearIndex).toBeGreaterThan(zIndex);
810+
expect(guardBeforeWakeSetIndex).toBeGreaterThan(clearIndex);
811+
expect(guardBeforeWakeSetIndex).toBeLessThan(wakePayloadIndex);
812+
expect(guardBeforeWakePasteIndex).toBeGreaterThan(wakePayloadIndex);
813+
expect(guardBeforeWakePasteIndex).toBeLessThan(pasteIndex);
796814
expect(scriptContent).not.toContain('key code 49');
797815
});
798816

0 commit comments

Comments
 (0)