@minhspark/codex-mcp-bridge 1.27.1 → 2.0.0

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.
@@ -35,7 +35,7 @@ import { createRuntimeState } from "./runtime-state.mjs";
35
35
  import { hardenedBridgeEnabled } from "./hardened-root-policy.mjs";
36
36
  import { protectCurrentUserPipe } from "./windows-pipe-acl.mjs";
37
37
  import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
38
- import { removeRelayFile, relayFileIdentity, verifyRelaySocketMode } from "./native-relay-files.mjs";
38
+ import { releasePrivateSocketLinks, removeRelayFile, relayFileIdentity, staleSocketIdentity, verifyRelaySocketMode } from "./native-relay-files.mjs";
39
39
 
40
40
  import { NativeCreationReceipts, creationIdValid } from "./native-creation-receipts.mjs";
41
41
  import { registerRemoteCodexTools } from "./remote-codex-tools.mjs";
@@ -43,7 +43,7 @@ import { remoteCodexSocketPath, remoteNativeAdapter, remoteRequestResponse } fro
43
43
 
44
44
  exitForVersionRequest(import.meta.url);
45
45
 
46
- const VERSION = "1.27.1";
46
+ const VERSION = "2.0.0";
47
47
  const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
48
48
 
49
49
  function errorResponse(code, message, sent, details) {
@@ -565,9 +565,13 @@ export class RelaySocketServer {
565
565
  try { fs.linkSync(privatePath, this.socketPath); break; }
566
566
  catch (error) {
567
567
  if (error.code !== "EEXIST") throw error;
568
- if (!replaceStale || attempt || await this.#socketIsLive()) throw new Error(`another native relay companion already owns ${this.socketPath}`);
568
+ if (!replaceStale || attempt) throw new Error(`another native relay companion already owns ${this.socketPath}`);
569
+ const stale = staleSocketIdentity(this.socketPath);
570
+ if (stale === null) continue;
571
+ if (await this.#socketIsLive()) throw new Error(`another native relay companion already owns ${this.socketPath}`);
569
572
  this.log(`removing the stale relay socket left at ${this.socketPath}`);
570
- removeRelayFile(this.socketPath);
573
+ releasePrivateSocketLinks(this.socketPath, stale);
574
+ removeRelayFile(this.socketPath, { expected: stale });
571
575
  }
572
576
  }
573
577
  const identity = fs.lstatSync(this.socketPath, { bigint: true });
@@ -17,6 +17,49 @@ export function relayFileIdentity(target, kind, expected) {
17
17
  return current;
18
18
  }
19
19
 
20
+ const PRIVATE_SOCKET_LINK = /^\.[0-9a-f]{12}\.sock$/;
21
+
22
+ const ownedSocket = (stat) => stat.isSocket() && (typeof process.getuid !== "function" || stat.uid === BigInt(process.getuid()));
23
+
24
+ export function staleSocketIdentity(target) {
25
+ let root;
26
+ try { root = fs.realpathSync.native(path.dirname(target)); }
27
+ catch (error) { if (error.code === "ENOENT") return null; throw error; }
28
+ const file = path.resolve(root, path.basename(target));
29
+ if (!file.startsWith(root.endsWith(path.sep) ? root : `${root}${path.sep}`)) throw new Error("Relay file escaped its directory");
30
+ let current;
31
+ try { current = fs.lstatSync(file, { bigint: true }); }
32
+ catch (error) { if (error.code === "ENOENT") return null; throw error; }
33
+ if (!ownedSocket(current)) throw new Error(`Unsafe relay socket file: ${target}`);
34
+ return { dev: current.dev, ino: current.ino };
35
+ }
36
+
37
+ export function releasePrivateSocketLinks(target, expected) {
38
+ let root;
39
+ try { root = fs.realpathSync.native(path.dirname(target)); }
40
+ catch (error) { if (error.code === "ENOENT") return 0; throw error; }
41
+ const prefix = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
42
+ const file = path.resolve(root, path.basename(target));
43
+ if (!file.startsWith(prefix)) throw new Error("Relay file escaped its directory");
44
+ let current;
45
+ try { current = fs.lstatSync(file, { bigint: true }); }
46
+ catch (error) { if (error.code === "ENOENT") return 0; throw error; }
47
+ if (current.nlink === 1n || !ownedSocket(current) || current.dev !== expected.dev || current.ino !== expected.ino) return 0;
48
+ let released = 0;
49
+ for (const name of fs.readdirSync(root)) {
50
+ if (!PRIVATE_SOCKET_LINK.test(name)) continue;
51
+ const link = path.resolve(root, name);
52
+ if (!link.startsWith(prefix)) continue;
53
+ let stat;
54
+ try { stat = fs.lstatSync(link, { bigint: true }); }
55
+ catch (error) { if (error.code === "ENOENT") continue; throw error; }
56
+ if (!ownedSocket(stat) || stat.dev !== current.dev || stat.ino !== current.ino) continue;
57
+ fs.unlinkSync(link);
58
+ released += 1;
59
+ }
60
+ return released;
61
+ }
62
+
20
63
  export function removeRelayFile(target, { kind = "socket", directory = path.dirname(target), expected } = {}) {
21
64
  let root;
22
65
  try { root = fs.realpathSync.native(directory); }
@@ -324,29 +324,6 @@ export function readTranscript(sessionId, cwd, limit = 10) {
324
324
  return { file, messages: messages.slice(-limit) };
325
325
  }
326
326
 
327
- export function readInitialTranscriptMessage(sessionId, cwd) {
328
- const file = findTranscriptFile(sessionId, cwd);
329
- if (!file || !fs.existsSync(file)) return { file, messages: [] };
330
- const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
331
- try {
332
- const stat = fs.fstatSync(descriptor);
333
- if (!stat.isFile()) return { file, messages: [] };
334
- const buffer = Buffer.alloc(Math.min(stat.size, 1024 * 1024));
335
- const bytes = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
336
- const text = buffer.subarray(0, bytes).toString("utf8");
337
- for (const line of text.slice(0, text.lastIndexOf("\n") + 1).split("\n")) {
338
- if (!line.trim()) continue;
339
- let entry;
340
- try { entry = JSON.parse(line); } catch { return { file, messages: [] }; }
341
- if (entry.isMeta || entry.isSidechain || entry.message?.role !== "user") continue;
342
- const content = entry.message.content;
343
- const initial = (Array.isArray(content) ? content.filter((part) => part?.type === "text").map((part) => part.text ?? "").join("\n") : String(content ?? "")).trim();
344
- return { file, messages: initial ? [{ role: "user", text: initial }] : [] };
345
- }
346
- return { file, messages: [] };
347
- } finally { fs.closeSync(descriptor); }
348
- }
349
-
350
327
  /**
351
328
  * Claude Code records an injected peer message in one of two shapes. An idle
352
329
  * recipient starts a new turn with a user entry whose uuid is the message id
package/src/platform.mjs CHANGED
@@ -561,23 +561,3 @@ export async function openThreadInCodexApp(threadId, { activate = true, platform
561
561
  await run("open", args, { timeout: 10000 });
562
562
  return url;
563
563
  }
564
-
565
- export async function openClaudeCodeComposer(url, { platform = process.platform, env = process.env, run = execFileAsync } = {}) {
566
- const parsed = new URL(url);
567
- const keys = parsed.pathname === "/continue" ? ["session"] : ["q", "folder"];
568
- if (parsed.protocol !== "claude:" || parsed.hostname !== "code" || !["/new", "/continue"].includes(parsed.pathname) || parsed.username || parsed.password || parsed.port || parsed.hash
569
- || [...parsed.searchParams.keys()].some((key) => !keys.includes(key) || parsed.searchParams.getAll(key).length !== 1)
570
- || (parsed.pathname === "/continue" && !/^local_[0-9a-f-]{36}$/i.test(parsed.searchParams.get("session") ?? ""))) {
571
- throw new Error("Invalid Claude Desktop Code creation link");
572
- }
573
- if (platform === "win32") {
574
- const shell = windowsPowerShell(env);
575
- const encodedUrl = Buffer.from(url, "utf8").toString("base64");
576
- const script = String.raw`$ErrorActionPreference='Stop'; $creationUrl=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedUrl}')); try { Start-Process -FilePath $creationUrl -ErrorAction Stop } catch { $desktopPaths=@(Get-Process claude -ErrorAction SilentlyContinue | ForEach-Object { $_.Path } | Where-Object { $_ -match '\\(?:AnthropicClaude\\app-[^\\]+|WindowsApps\\Claude_[^\\]+\\app)\\claude\.exe$' -and (Test-Path -LiteralPath $_) } | Select-Object -Unique); if ($desktopPaths.Count -eq 0 -and $env:LOCALAPPDATA) { $desktopPaths=@(Get-ChildItem -LiteralPath (Join-Path $env:LOCALAPPDATA 'AnthropicClaude') -Directory -Filter 'app-*' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | ForEach-Object { Join-Path $_.FullName 'claude.exe' } | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1) }; if ($desktopPaths.Count -ne 1) { throw }; Start-Process -FilePath $desktopPaths[0] -ArgumentList ('"'+$creationUrl+'"') -WindowStyle Hidden -ErrorAction Stop }`;
577
- await run(shell, ["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { windowsHide: true, timeout: 10000 });
578
- return;
579
- }
580
- const command = platform === "darwin" ? "/usr/bin/open" : platform === "linux" ? "/usr/bin/xdg-open" : null;
581
- if (!command) throw new Error("Claude Desktop Code links are unsupported on this platform");
582
- await run(command, [url], { windowsHide: true, timeout: 10000 });
583
- }
@@ -109,7 +109,7 @@ const LIVE_CHECKS = {
109
109
  },
110
110
  codex: {
111
111
  senderDestination: "Each Claude delivery separately verifies the Codex sender, the recipient session, its permission class and the accounts. A project grant does not replace these checks. Check claude_bridge_status in the calling task.",
112
- creationTarget: "Creating a new Claude session is confirmed separately through read_claude_creation; this inspection does not open or check one.",
112
+ creationTarget: "The bridge does not create Claude sessions. The user opens a new session in Claude Desktop; this inspection does not open or check one.",
113
113
  },
114
114
  };
115
115
 
@@ -1,102 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { promisify } from "node:util";
5
- import { setTimeout as delay } from "node:timers/promises";
6
-
7
- const execute = promisify(execFile);
8
- const helperPath = fileURLToPath(new URL("./claude-composer-submit.ps1", import.meta.url));
9
-
10
- export async function trustClaudeCodeComposer({ cwd, prompt }, options = {}) {
11
- const platform = options.platform ?? process.platform;
12
- if (platform !== "win32") return { status: "blocked", reason: "native_workspace_trust_unsupported", platform };
13
- if (typeof cwd !== "string" || !path.win32.isAbsolute(cwd)) return { status: "blocked", reason: "absolute_project_path_required" };
14
- const payload = Buffer.from(JSON.stringify({ cwd, prompt }), "utf8").toString("base64");
15
- try {
16
- const result = await (options.run ?? execute)("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", helperPath, "-PayloadBase64", payload, "-Action", "Trust", "-WaitMilliseconds", "8000"], { windowsHide: true, timeout: 12000, encoding: "utf8", maxBuffer: 1024 * 1024 });
17
- const trusted = JSON.parse(result.stdout.trim().replace(/^\uFEFF/, ""));
18
- if (!["trusted", "absent", "blocked", "uncertain"].includes(trusted?.status)) return { status: "uncertain", reason: "native_workspace_trust_outcome_unknown" };
19
- return trusted;
20
- } catch { return { status: "uncertain", reason: "native_workspace_trust_outcome_unknown" }; }
21
- }
22
-
23
- export function assessClaudeCodeComposer(snapshot, { prompt, cwd }) {
24
- const block = (reason) => ({ status: "blocked", reason });
25
- if (!snapshot || !Array.isArray(snapshot.windows) || snapshot.windows.length !== 1) {
26
- return block("desktop_window_ambiguous_or_missing");
27
- }
28
- const window = snapshot.windows[0];
29
- if (window.installedDesktop !== true) return block("desktop_process_not_verified");
30
- if (snapshot.dialogs?.length) return block("desktop_dialog_requires_attention");
31
- if (snapshot.codeSelected !== true) return block("code_mode_not_selected");
32
- if (!Array.isArray(snapshot.composers) || snapshot.composers.length !== 1) {
33
- return block("composer_ambiguous_or_missing");
34
- }
35
- const composerText = (text) => typeof text === "string" ? text.replace(/\r\n/g, "\n").split("\n").filter((line) => line !== "").join("\n") : null;
36
- if (composerText(snapshot.composers[0].text) !== composerText(prompt)) return block("composer_prompt_mismatch");
37
- if (snapshot.worktreeEnabled !== false) return block("original_project_folder_not_verified");
38
- if (snapshot.folderCount !== 1 || snapshot.localMode !== true) return block("project_folder_ambiguous_or_remote");
39
- if (!Array.isArray(snapshot.selectedFolderPaths) || snapshot.selectedFolderPaths.length !== 1) {
40
- return block("selected_project_path_not_verified");
41
- }
42
- const normalize = (value) => path.win32.normalize(value).replace(/[\\/]+$/, "").toLowerCase();
43
- if (typeof snapshot.selectedFolderPaths[0] !== "string" || !path.win32.isAbsolute(snapshot.selectedFolderPaths[0]) || normalize(snapshot.selectedFolderPaths[0]) !== normalize(cwd)) {
44
- return block("selected_project_path_mismatch");
45
- }
46
- if (snapshot.sendButtons?.length !== 1 || snapshot.sendButtons[0].enabled !== true || snapshot.sendButtons[0].invokable !== true) {
47
- return block("send_button_ambiguous_disabled_or_unavailable");
48
- }
49
- return { status: "ready", reason: "exact_composer_and_project_verified", windowId: window.id };
50
- }
51
-
52
- export async function submitClaudeCodeComposer({ prompt, cwd, beforeSubmit }, options = {}) {
53
- const platform = options.platform ?? process.platform;
54
- if (typeof prompt !== "string" || !prompt.length || typeof cwd !== "string" || !cwd.length) {
55
- return { status: "blocked", reason: "prompt_and_project_required" };
56
- }
57
- if (platform !== "win32") {
58
- return { status: "blocked", reason: "native_composer_submission_unsupported", platform };
59
- }
60
- if (!path.win32.isAbsolute(cwd)) return { status: "blocked", reason: "absolute_project_path_required" };
61
- const run = options.run ?? execute;
62
- const requestedBudget = options.timeoutMs ?? 20000;
63
- const budget = Number.isFinite(requestedBudget) ? Math.max(1000, Math.min(20000, requestedBudget)) : 20000;
64
- const started = Date.now();
65
- const payload = Buffer.from(JSON.stringify({ prompt, cwd }), "utf8").toString("base64");
66
- let attempted = false;
67
- try {
68
- const call = async (action, windowId) => {
69
- const remaining = budget - (Date.now() - started);
70
- if (remaining <= 0) throw new Error("Native composer deadline exceeded");
71
- const args = ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", helperPath,
72
- "-PayloadBase64", payload, "-Action", action, "-WaitMilliseconds", String(Math.min(8000, remaining - 250))];
73
- if (windowId != null) args.push("-ExpectedWindowId", String(windowId));
74
- const result = await run("powershell.exe", args, {
75
- windowsHide: true, timeout: remaining, maxBuffer: 1024 * 1024, encoding: "utf8",
76
- });
77
- return JSON.parse(result.stdout.trim().replace(/^\uFEFF/, ""));
78
- };
79
- const inspection = await call("Inspect");
80
- if (inspection.status === "blocked") return { ...inspection, submissionAttempted: false };
81
- const assessment = assessClaudeCodeComposer(inspection.snapshot, { prompt, cwd });
82
- if (assessment.status !== "ready") return { ...assessment, submissionAttempted: false };
83
- if (beforeSubmit) await beforeSubmit();
84
- const selection = await call("SelectFolder", assessment.windowId);
85
- if (selection.status !== "selected") return { status: "blocked", reason: selection.reason ?? "native_folder_selection_not_confirmed", submissionAttempted: false };
86
- await (options.sleep ?? delay)(2000);
87
- if (beforeSubmit) await beforeSubmit();
88
- attempted = true;
89
- const result = await call("Submit", assessment.windowId);
90
- if (!["submitted", "blocked", "uncertain"].includes(result.status)) {
91
- return { status: "uncertain", reason: "native_submit_result_invalid", submissionAttempted: true };
92
- }
93
- return { ...result, submissionAttempted: result.status !== "blocked" };
94
- } catch (error) {
95
- return {
96
- status: attempted ? "uncertain" : "blocked",
97
- reason: attempted ? "native_submit_outcome_unknown" : "native_accessibility_unavailable",
98
- submissionAttempted: attempted,
99
- error: error.code ?? error.name ?? "Error",
100
- };
101
- }
102
- }
@@ -1,211 +0,0 @@
1
- param(
2
- [Parameter(Mandatory = $true)][string]$PayloadBase64,
3
- [ValidateSet('Inspect', 'SelectFolder', 'Submit', 'Trust')][string]$Action = 'Inspect',
4
- [int]$WaitMilliseconds = 8000,
5
- [long]$ExpectedWindowId = 0
6
- )
7
-
8
- $ErrorActionPreference = 'Stop'
9
- $ProgressPreference = 'SilentlyContinue'
10
- [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
11
- Add-Type -AssemblyName UIAutomationClient
12
- Add-Type -AssemblyName UIAutomationTypes
13
- $request = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($PayloadBase64)) | ConvertFrom-Json
14
- $deadline = [DateTime]::UtcNow.AddMilliseconds([Math]::Max(0, [Math]::Min(8000, $WaitMilliseconds)))
15
- $invocationStarted = $false
16
-
17
- function Finish([string]$Status, [string]$Reason, $Snapshot = $null) {
18
- $result = @{ status = $Status; reason = $Reason }
19
- if ($null -ne $Snapshot) { $result.snapshot = $Snapshot }
20
- $result | ConvertTo-Json -Depth 8 -Compress
21
- exit 0
22
- }
23
-
24
- function Descendants($Element) {
25
- return $Element.FindAll([System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition)
26
- }
27
-
28
- function Dialogs($Element) {
29
- for ($attempt = 0; $attempt -lt 3; $attempt++) {
30
- try {
31
- @(Descendants $Element | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::Window -or $_.GetCurrentPropertyValue([System.Windows.Automation.AutomationElement]::IsDialogProperty) -eq $true })
32
- return
33
- } catch {
34
- if ($attempt -eq 2) { throw }
35
- Start-Sleep -Milliseconds 150
36
- }
37
- }
38
- }
39
-
40
- function TextValue($Element) {
41
- $pattern = $null
42
- if ($Element.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$pattern)) {
43
- return ([System.Windows.Automation.ValuePattern]$pattern).Current.Value
44
- }
45
- if ($Element.TryGetCurrentPattern([System.Windows.Automation.TextPattern]::Pattern, [ref]$pattern)) {
46
- return ([System.Windows.Automation.TextPattern]$pattern).DocumentRange.GetText(-1)
47
- }
48
- return $null
49
- }
50
-
51
- function IsInstalledDesktop($Process) {
52
- try {
53
- $exe = Get-Item -LiteralPath $Process.Path
54
- if ($exe.VersionInfo.ProductName -ne 'Claude' -or $exe.VersionInfo.CompanyName -ne 'Anthropic') { return $false }
55
- $localRoot = Join-Path $env:LOCALAPPDATA 'AnthropicClaude'
56
- $root = [IO.Path]::GetFullPath($localRoot).TrimEnd('\') + '\'
57
- if ($exe.FullName.StartsWith($root, [StringComparison]::OrdinalIgnoreCase) -and $exe.Directory.Name -match '^app-\d+(\.\d+)+$' -and $exe.Name -ieq 'claude.exe') { return $true }
58
- foreach ($package in @(Get-AppxPackage -Name '*Claude*' -ErrorAction SilentlyContinue)) {
59
- if ($package.InstallLocation -and $exe.FullName.StartsWith($package.InstallLocation.TrimEnd('\') + '\', [StringComparison]::OrdinalIgnoreCase)) { return $true }
60
- }
61
- } catch {}
62
- return $false
63
- }
64
-
65
- function PathEqual([string]$First, [string]$Second) {
66
- try {
67
- if ($First -notmatch '^(?:[A-Za-z]:[\\/]|\\\\[^\\]+\\[^\\]+)') { return $false }
68
- return [IO.Path]::GetFullPath($First).TrimEnd('\').Equals([IO.Path]::GetFullPath($Second).TrimEnd('\'), [StringComparison]::OrdinalIgnoreCase)
69
- } catch { return $false }
70
- }
71
-
72
- function PromptEqual([string]$First, [string]$Second) {
73
- $firstLines = @($First.Replace("`r`n", "`n").Split("`n") | Where-Object { $_ -cne '' })
74
- $secondLines = @($Second.Replace("`r`n", "`n").Split("`n") | Where-Object { $_ -cne '' })
75
- return ($firstLines -join "`n") -ceq ($secondLines -join "`n")
76
- }
77
-
78
- function SelectedPaths($Folder, $Root) {
79
- $expander = [System.Windows.Automation.ExpandCollapsePattern]$Folder.GetCurrentPattern([System.Windows.Automation.ExpandCollapsePattern]::Pattern)
80
- try {
81
- if ($expander.Current.ExpandCollapseState -eq [System.Windows.Automation.ExpandCollapseState]::Collapsed) { $expander.Expand() }
82
- Start-Sleep -Milliseconds 150
83
- $menus = @(Descendants $Root | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::Menu -and $_.Current.Name -eq $Folder.Current.Name -and -not $_.Current.IsOffscreen })
84
- if ($menus.Count -ne 1) { throw 'Folder menu not verified' }
85
- foreach ($radio in @(Descendants $menus[0] | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::RadioButton })) {
86
- $selection = [System.Windows.Automation.SelectionItemPattern]$radio.GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern)
87
- if ($selection.Current.IsSelected) { $radio.Current.HelpText }
88
- }
89
- } finally {
90
- if ($expander.Current.ExpandCollapseState -eq [System.Windows.Automation.ExpandCollapseState]::Expanded) { $expander.Collapse() }
91
- Start-Sleep -Milliseconds 250
92
- }
93
- }
94
-
95
- try {
96
- do {
97
- $processes = @(Get-Process -Name Claude -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 -and (IsInstalledDesktop $_) })
98
- if ($processes.Count -ne 1) { Finish 'blocked' 'desktop_window_ambiguous_or_missing' }
99
- $process = $processes[0]
100
- $windowId = $process.MainWindowHandle.ToInt64()
101
- if ($ExpectedWindowId -ne 0 -and $ExpectedWindowId -ne $windowId) { Finish 'blocked' 'desktop_window_changed' }
102
- $root = [System.Windows.Automation.AutomationElement]::FromHandle($process.MainWindowHandle)
103
- $elements = Descendants $root
104
- $dialogs = @(Dialogs $root)
105
- if ($Action -eq 'Trust') {
106
- if ($dialogs.Count -gt 0) {
107
- if ($dialogs.Count -ne 1 -or $dialogs[0].Current.Name -cne 'Trust this workspace?') { Finish 'blocked' 'workspace_trust_dialog_not_verified' }
108
- $trustElements = Descendants $dialogs[0]
109
- $trustPaths = @($trustElements | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::Text -and (PathEqual $_.Current.Name $request.cwd) })
110
- $trustButtons = @($trustElements | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::Button -and $_.Current.Name -ceq 'Trust workspace' -and $_.Current.IsEnabled -and -not $_.Current.IsOffscreen })
111
- if ($trustPaths.Count -ne 1 -or $trustButtons.Count -ne 1) { Finish 'blocked' 'workspace_trust_path_mismatch_or_ambiguous' }
112
- $trustInvoke = $null
113
- if (-not $trustButtons[0].TryGetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern, [ref]$trustInvoke)) { Finish 'blocked' 'workspace_trust_button_unavailable' }
114
- $invocationStarted = $true
115
- ([System.Windows.Automation.InvokePattern]$trustInvoke).Invoke()
116
- do {
117
- Start-Sleep -Milliseconds 150
118
- $remainingTrust = @(Descendants $root | Where-Object { $_.Current.Name -ceq 'Trust this workspace?' -and $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::Window })
119
- if ($remainingTrust.Count -eq 0) { Finish 'trusted' 'exact_native_workspace_trust_confirmed' }
120
- } while ([DateTime]::UtcNow -lt $deadline)
121
- Finish 'uncertain' 'native_workspace_trust_outcome_unknown'
122
- }
123
- }
124
- if ($dialogs.Count -gt 0) { Finish 'blocked' 'desktop_dialog_requires_attention' }
125
- $composers = @($elements | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::Edit -and $_.Current.Name -eq 'Prompt' -and -not $_.Current.IsOffscreen })
126
- if ($composers.Count -gt 1) { Finish 'blocked' 'composer_ambiguous_or_missing' }
127
- if ($composers.Count -eq 1 -and (PromptEqual (TextValue $composers[0]) $request.prompt)) { break }
128
- if ([DateTime]::UtcNow -ge $deadline) { Finish 'blocked' 'composer_prompt_mismatch' }
129
- Start-Sleep -Milliseconds 150
130
- } while ($true)
131
-
132
- $codeControls = @($elements | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::RadioButton -and $_.Current.Name -eq 'Code' -and -not $_.Current.IsOffscreen })
133
- if ($codeControls.Count -ne 1) { Finish 'blocked' 'code_mode_not_selected' }
134
- $codeSelected = ([System.Windows.Automation.SelectionItemPattern]$codeControls[0].GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern)).Current.IsSelected
135
- if (-not $codeSelected) { Finish 'blocked' 'code_mode_not_selected' }
136
- $pane = $composers[0]
137
- $walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
138
- for ($level = 0; $level -lt 12; $level++) {
139
- $pane = $walker.GetParent($pane)
140
- if ($null -eq $pane) { Finish 'blocked' 'composer_scope_unavailable' }
141
- if ($pane.Current.ClassName -match '(?:^|\s)dframe-pane-primary(?:\s|$)') { break }
142
- }
143
- if ($pane.Current.ClassName -notmatch '(?:^|\s)dframe-pane-primary(?:\s|$)') { Finish 'blocked' 'composer_scope_unavailable' }
144
- $scope = Descendants $pane
145
- $pillButtons = @($scope | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::Button -and $_.Current.ClassName -like '*pill-h*' -and -not $_.Current.IsOffscreen })
146
- $localMode = @($pillButtons | Where-Object { $_.Current.Name -eq 'Local' }).Count -eq 1
147
- $folders = @($pillButtons | Where-Object { $_.Current.Name -notin @('Local', 'Add another folder') })
148
- if (-not $localMode -or $folders.Count -ne 1) { Finish 'blocked' 'project_folder_ambiguous_or_remote' }
149
- $worktrees = @($scope | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::CheckBox -and $_.Current.Name -eq 'worktree' })
150
- $worktreeEnabled = $false
151
- if ($worktrees.Count -gt 1) { Finish 'blocked' 'original_project_folder_not_verified' }
152
- if ($worktrees.Count -eq 1) { $worktreeEnabled = ([System.Windows.Automation.TogglePattern]$worktrees[0].GetCurrentPattern([System.Windows.Automation.TogglePattern]::Pattern)).Current.ToggleState -ne [System.Windows.Automation.ToggleState]::Off }
153
- if ($worktreeEnabled) { Finish 'blocked' 'original_project_folder_not_verified' }
154
-
155
- $folder = $folders[0]
156
- $paths = @(SelectedPaths $folder $root)
157
- if ($paths.Count -ne 1 -or -not (PathEqual $paths[0] $request.cwd)) { Finish 'blocked' 'selected_project_path_mismatch' }
158
- $scope = Descendants $pane
159
- $sends = @($scope | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::Button -and $_.Current.Name -eq 'Send' -and -not $_.Current.IsOffscreen })
160
- if ($sends.Count -ne 1 -or -not $sends[0].Current.IsEnabled) { Finish 'blocked' 'send_button_ambiguous_disabled_or_unavailable' }
161
- $invoke = $null
162
- if (-not $sends[0].TryGetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern, [ref]$invoke)) { Finish 'blocked' 'send_button_ambiguous_disabled_or_unavailable' }
163
- $currentPaths = @(SelectedPaths $folder $root)
164
- if ($currentPaths.Count -ne 1 -or -not (PathEqual $currentPaths[0] $request.cwd) -or -not (PromptEqual (TextValue $composers[0]) $request.prompt)) { Finish 'blocked' 'composer_changed_before_submission' }
165
- $currentDialogs = @(Dialogs $root)
166
- if ($currentDialogs.Count -gt 0) { Finish 'blocked' 'desktop_dialog_requires_attention' }
167
- if (-not ([System.Windows.Automation.SelectionItemPattern]$codeControls[0].GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern)).Current.IsSelected) { Finish 'blocked' 'code_mode_not_selected' }
168
- if ($worktrees.Count -eq 1 -and ([System.Windows.Automation.TogglePattern]$worktrees[0].GetCurrentPattern([System.Windows.Automation.TogglePattern]::Pattern)).Current.ToggleState -ne [System.Windows.Automation.ToggleState]::Off) { Finish 'blocked' 'original_project_folder_not_verified' }
169
- $snapshot = @{
170
- windows = @(@{ id = $windowId; installedDesktop = $true })
171
- dialogs = @()
172
- codeSelected = $true
173
- composers = @(@{ text = (TextValue $composers[0]) })
174
- folderCount = $folders.Count
175
- localMode = $localMode
176
- worktreeEnabled = $worktreeEnabled
177
- selectedFolderPaths = @($paths)
178
- sendButtons = @(@{ enabled = $true; invokable = $true })
179
- }
180
- if ($Action -eq 'Inspect') { Finish 'ready' 'exact_composer_and_project_verified' $snapshot }
181
- if ($Action -eq 'Trust') { Finish 'absent' 'exact_composer_and_project_verified' $snapshot }
182
- if ($Action -eq 'SelectFolder') {
183
- $expander = [System.Windows.Automation.ExpandCollapsePattern]$folder.GetCurrentPattern([System.Windows.Automation.ExpandCollapsePattern]::Pattern)
184
- $selectionInvoked = $false
185
- try {
186
- $expander.Expand()
187
- Start-Sleep -Milliseconds 150
188
- $menus = @(Descendants $root | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::Menu -and $_.Current.Name -eq $folder.Current.Name -and -not $_.Current.IsOffscreen })
189
- if ($menus.Count -ne 1) { throw 'Folder menu not verified' }
190
- $choices = @(Descendants $menus[0] | Where-Object { $_.Current.ControlType -eq [System.Windows.Automation.ControlType]::RadioButton -and (PathEqual $_.Current.HelpText $request.cwd) -and $_.Current.IsEnabled -and -not $_.Current.IsOffscreen })
191
- if ($choices.Count -ne 1) { throw 'Folder choice not verified' }
192
- $choiceInvoke = $null
193
- if (-not $choices[0].TryGetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern, [ref]$choiceInvoke)) { throw 'Folder choice unavailable' }
194
- if (-not (PromptEqual (TextValue $composers[0]) $request.prompt)) { throw 'Composer changed' }
195
- ([System.Windows.Automation.InvokePattern]$choiceInvoke).Invoke()
196
- $selectionInvoked = $true
197
- } finally {
198
- try {
199
- if ($expander.Current.ExpandCollapseState -eq [System.Windows.Automation.ExpandCollapseState]::Expanded) { $expander.Collapse() }
200
- Start-Sleep -Milliseconds 250
201
- } catch { if (-not $selectionInvoked) { throw } }
202
- }
203
- Finish 'selected' 'exact_native_folder_explicitly_selected'
204
- }
205
- $invocationStarted = $true
206
- ([System.Windows.Automation.InvokePattern]$invoke).Invoke()
207
- Finish 'submitted' 'native_send_invoked_once'
208
- } catch {
209
- if ($invocationStarted) { Finish 'uncertain' 'native_submit_outcome_unknown' }
210
- Finish 'blocked' 'native_accessibility_unavailable'
211
- }