dsh-codex-style-computer-use 2.0.0 → 2.2.3
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/README.ar.md +234 -0
- package/README.de.md +234 -0
- package/README.es.md +264 -0
- package/README.fa.md +234 -0
- package/README.fr.md +243 -0
- package/README.md +45 -8
- package/README.pt.md +234 -0
- package/README.ru.md +259 -0
- package/README.zh.md +45 -8
- package/client.js +130 -0
- package/icon.svg +20 -4
- package/lib/bin/attention-worker.exe +0 -0
- package/lib/bin/attention-worker.exe.json +1 -1
- package/lib/core/bin/dsh-computer-use-worker.exe +0 -0
- package/lib/core/bin/dsh-computer-use-worker.exe.json +2 -2
- package/lib/core/index.js +51 -3
- package/lib/core/worker.cs +467 -18
- package/lib/host-debug.js +88 -0
- package/lib/image-output.js +28 -6
- package/lib/index.js +104 -0
- package/lib/launch.js +67 -0
- package/lib/policy.js +22 -2
- package/lib/tools.js +74 -8
- package/package.json +101 -91
- package/scripts/check-behavior.mjs +1 -1
- package/scripts/verify-package.mjs +9 -0
- package/skills/computer-use/SKILL.md +59 -50
package/lib/core/worker.cs
CHANGED
|
@@ -489,8 +489,22 @@ namespace DshComputerUse.Worker
|
|
|
489
489
|
// ---- focus / integrity probing (2026-09-13 "typing never lands" investigation) ----
|
|
490
490
|
[DllImport("user32.dll")]
|
|
491
491
|
public static extern IntPtr GetParent(IntPtr hWnd);
|
|
492
|
+
// GW_OWNER=4: a modal dialog's owner is the window that opened it (SameInputFamily).
|
|
492
493
|
[DllImport("user32.dll")]
|
|
493
|
-
public static extern
|
|
494
|
+
public static extern IntPtr GetWindow(IntPtr hWnd, uint cmd);
|
|
495
|
+
// ---- caret evidence: where WOULD the next character go? ----
|
|
496
|
+
// GUITHREADINFO reports the caret for the FOREGROUND THREAD without AttachThreadInput, and
|
|
497
|
+
// works in apps where UIA has no TextPattern (Word's document, many Win32 editors).
|
|
498
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
499
|
+
public struct GUITHREADINFO
|
|
500
|
+
{
|
|
501
|
+
public int cbSize;
|
|
502
|
+
public int flags;
|
|
503
|
+
public IntPtr hwndActive, hwndFocus, hwndCapture, hwndMenuOwner, hwndMoveSize, hwndCaret;
|
|
504
|
+
public RECT rcCaret;
|
|
505
|
+
}
|
|
506
|
+
[DllImport("user32.dll")] public static extern bool GetGUIThreadInfo(uint idThread, ref GUITHREADINFO info);
|
|
507
|
+
[DllImport("user32.dll")] public static extern bool IsWindow(IntPtr hWnd);
|
|
494
508
|
[DllImport("user32.dll")]
|
|
495
509
|
public static extern IntPtr WindowFromPoint(POINT p);
|
|
496
510
|
[DllImport("user32.dll")]
|
|
@@ -512,6 +526,24 @@ namespace DshComputerUse.Worker
|
|
|
512
526
|
public static extern IntPtr OpenProcess(uint access, bool inherit, uint pid);
|
|
513
527
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
514
528
|
public static extern bool CloseHandle(IntPtr h);
|
|
529
|
+
// ---- process ancestry: is a covered window the target app's OWN embedded renderer? ----
|
|
530
|
+
// A WeChat article panel (WeChatAppEx) or an Electron utility process is a legitimate
|
|
531
|
+
// descendant of the app the caller aimed at; an unrelated window on top is not.
|
|
532
|
+
public const uint TH32CS_SNAPPROCESS = 0x00000002;
|
|
533
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
534
|
+
public struct PROCESSENTRY32W
|
|
535
|
+
{
|
|
536
|
+
public uint dwSize, cntUsage, th32ProcessID;
|
|
537
|
+
public IntPtr th32DefaultHeapID;
|
|
538
|
+
public uint th32ModuleID, cntThreads, th32ParentProcessID, pcPriClassBase, dwFlags;
|
|
539
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szExeFile;
|
|
540
|
+
}
|
|
541
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
542
|
+
public static extern IntPtr CreateToolhelp32Snapshot(uint flags, uint pid);
|
|
543
|
+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
544
|
+
public static extern bool Process32FirstW(IntPtr snapshot, ref PROCESSENTRY32W entry);
|
|
545
|
+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
546
|
+
public static extern bool Process32NextW(IntPtr snapshot, ref PROCESSENTRY32W entry);
|
|
515
547
|
// ---- host guard: which executable is the process that owns this window? ----
|
|
516
548
|
// OpenProcess + QueryFullProcessImageName is the ONLY safe way to ask: Process.MainModule
|
|
517
549
|
// throws for any process this worker may not read (see IsHostPid).
|
|
@@ -739,6 +771,9 @@ namespace DshComputerUse.Worker
|
|
|
739
771
|
{
|
|
740
772
|
_sceneVisible = false; _preparing = false; _thinking = false; _flashStart = 0;
|
|
741
773
|
_killNow = true;
|
|
774
|
+
// The hands-off state is over (release/calm/brake): the countdown continuity window
|
|
775
|
+
// must not survive into the next sequence.
|
|
776
|
+
Program.ResetReannounceGrace();
|
|
742
777
|
}
|
|
743
778
|
|
|
744
779
|
// Cyan thinking/monitoring is a lifecycle latch, with no elapsed-time expiry. Off/Kill
|
|
@@ -2870,7 +2905,12 @@ namespace DshComputerUse.Worker
|
|
|
2870
2905
|
// "calibrate" drives too — it walks the pointer across the screen to prove pixel==click.
|
|
2871
2906
|
// (selectRange / shiftClick / calibrate were found by build/test-op-classification.mjs on
|
|
2872
2907
|
// its first run: three real dispatcher ops the hand-written list had simply missed.)
|
|
2873
|
-
"calibrate"
|
|
2908
|
+
"calibrate",
|
|
2909
|
+
// "launch" starts a process: the machine changes, so it may not re-open a session the
|
|
2910
|
+
// human ended (Ctrl+Alt+Q) and it must not answer while a brake is up.
|
|
2911
|
+
"launch",
|
|
2912
|
+
// "focusField" clicks a field: it drives the machine exactly like a click.
|
|
2913
|
+
"focusField"
|
|
2874
2914
|
};
|
|
2875
2915
|
|
|
2876
2916
|
static readonly HashSet<string> ReadOnlyOps = new HashSet<string> {
|
|
@@ -3436,6 +3476,23 @@ namespace DshComputerUse.Worker
|
|
|
3436
3476
|
static volatile bool _operationLive, _operationDispatched;
|
|
3437
3477
|
static bool _operationActing;
|
|
3438
3478
|
static string _lastAnnouncedSeries;
|
|
3479
|
+
// CONTINUOUS-OPERATION GRACE (human rule 2026-11-07): after one purple countdown the human
|
|
3480
|
+
// has been told to keep hands off. While operations keep arriving inside this sliding
|
|
3481
|
+
// window, the "do not intervene" state is CONTINUOUS, so no further countdown of any
|
|
3482
|
+
// duration is shown. Every dispatched mutation pushes the deadline forward, which is what
|
|
3483
|
+
// makes a real sequence exempt indefinitely; a gap longer than the window ends continuity
|
|
3484
|
+
// and the next action warns again. Wrap-safe comparison: both sides are TickCount deltas
|
|
3485
|
+
// bounded by the window itself.
|
|
3486
|
+
const int ReannounceGraceMs = 5000;
|
|
3487
|
+
static int _noReannounceUntil;
|
|
3488
|
+
static bool InReannounceGrace()
|
|
3489
|
+
{ return unchecked(_noReannounceUntil - Environment.TickCount) > 0; }
|
|
3490
|
+
static void ExtendReannounceGrace()
|
|
3491
|
+
{ _noReannounceUntil = unchecked(Environment.TickCount + ReannounceGraceMs); }
|
|
3492
|
+
/** Ends the continuity window. Called when the hands-off state itself ends (cycle release,
|
|
3493
|
+
* calm, brake), because a later action then genuinely starts a new sequence. */
|
|
3494
|
+
public static void ResetReannounceGrace()
|
|
3495
|
+
{ _noReannounceUntil = 0; }
|
|
3439
3496
|
static long _announcedHumanRevision, _announcedInputEpoch, _preparedInputEpoch;
|
|
3440
3497
|
static volatile int _activeRequestId, _lastCompletedRequestId;
|
|
3441
3498
|
// The seam is internal to the compiled program; no tool can replace it.
|
|
@@ -3462,12 +3519,47 @@ namespace DshComputerUse.Worker
|
|
|
3462
3519
|
if (actual != expected) return "input receiver changed from hwnd=" + expected + " to hwnd=" + actual;
|
|
3463
3520
|
return null;
|
|
3464
3521
|
}
|
|
3522
|
+
|
|
3523
|
+
/// True when two windows belong to the SAME input family: the same window, another window
|
|
3524
|
+
/// of the same process, a descendant process's window (an embedded renderer), or a window
|
|
3525
|
+
/// owned/parented by the other. A modal dialog (Find/Replace, Save As, a browser file
|
|
3526
|
+
/// picker) is a DIFFERENT hwnd in the SAME process that takes the foreground while the
|
|
3527
|
+
/// operation it belongs to is still running — treating it as foreign made dialogs a
|
|
3528
|
+
/// keyboard black hole and blocked the screenshot that would have shown the question.
|
|
3529
|
+
public static bool SameInputFamily(IntPtr a, IntPtr b)
|
|
3530
|
+
{
|
|
3531
|
+
if (a == b) return true;
|
|
3532
|
+
if (a == IntPtr.Zero || b == IntPtr.Zero) return false;
|
|
3533
|
+
uint pa = PidOf(a), pb = PidOf(b);
|
|
3534
|
+
if (pa == 0 || pb == 0) return false;
|
|
3535
|
+
if (pa == pb) return true;
|
|
3536
|
+
if (ProcessDescendsFrom(unchecked((int)pb), unchecked((int)pa)) ||
|
|
3537
|
+
ProcessDescendsFrom(unchecked((int)pa), unchecked((int)pb))) return true;
|
|
3538
|
+
IntPtr x = b;
|
|
3539
|
+
for (int hop = 0; hop < 8 && x != IntPtr.Zero; hop++)
|
|
3540
|
+
{
|
|
3541
|
+
IntPtr owner = Native.GetWindow(x, 4); // GW_OWNER
|
|
3542
|
+
if (owner == a) return true;
|
|
3543
|
+
IntPtr parent = Native.GetParent(x);
|
|
3544
|
+
if (parent == a) return true;
|
|
3545
|
+
x = owner != IntPtr.Zero ? owner : parent;
|
|
3546
|
+
}
|
|
3547
|
+
return false;
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3465
3550
|
static IDisposable GuardReceiver(IntPtr expected, uint pid, bool keyboard)
|
|
3466
3551
|
{
|
|
3467
3552
|
return new ReceiverScope(delegate {
|
|
3468
3553
|
IntPtr actual = keyboard ? InputForeground() : InputPointReceiver();
|
|
3469
3554
|
string problem = InputReceiverProblem(expected.ToInt64(), pid, InputWindowExists(expected), InputWindowPid(expected), actual.ToInt64());
|
|
3470
|
-
if (problem != null)
|
|
3555
|
+
if (problem != null)
|
|
3556
|
+
{
|
|
3557
|
+
// Same-family foreground for KEYBOARD only: the dialog is where the keystrokes
|
|
3558
|
+
// belong, so accept it and follow it. Mouse/point receivers keep the strict
|
|
3559
|
+
// check — a click must land on the pixel's real owner, never be retargeted.
|
|
3560
|
+
if (keyboard && SameInputFamily(expected, TopLevel(actual))) { SetSticky(TopLevel(actual)); return; }
|
|
3561
|
+
throw InputInterrupted("INPUT_TARGET_CHANGED", problem + ". Remaining input was refused; inspect any partial effect before retrying.");
|
|
3562
|
+
}
|
|
3471
3563
|
});
|
|
3472
3564
|
}
|
|
3473
3565
|
|
|
@@ -3492,6 +3584,9 @@ namespace DshComputerUse.Worker
|
|
|
3492
3584
|
static void MarkMutation()
|
|
3493
3585
|
{
|
|
3494
3586
|
Panic.Check("actuation"); BeginActuation(); CheckHumanInput(); _operationDispatched = true;
|
|
3587
|
+
// Every dispatched mutation slides the grace window: a continuous sequence therefore
|
|
3588
|
+
// stays exempt indefinitely, while a real pause lets the window lapse.
|
|
3589
|
+
ExtendReannounceGrace();
|
|
3495
3590
|
}
|
|
3496
3591
|
static void BeginActuation()
|
|
3497
3592
|
{
|
|
@@ -3527,6 +3622,14 @@ namespace DshComputerUse.Worker
|
|
|
3527
3622
|
throw InputInterrupted("HUMAN_REOBSERVE", "The user changed the desktop between batch steps. Obtain fresh target evidence and continue the remaining task without replaying completed input.");
|
|
3528
3623
|
_preparedInputEpoch = Panic.InputEpoch; return;
|
|
3529
3624
|
}
|
|
3625
|
+
// Inside the sliding grace the sequence is still in progress: no countdown of any
|
|
3626
|
+
// duration, but a desktop change still refuses (the human may have taken over).
|
|
3627
|
+
if (InReannounceGrace())
|
|
3628
|
+
{
|
|
3629
|
+
if (_announcedInputEpoch != Panic.InputEpoch)
|
|
3630
|
+
throw InputInterrupted("HUMAN_REOBSERVE", "The user changed the desktop between actions. Obtain fresh target evidence and continue the remaining task without replaying completed input.");
|
|
3631
|
+
_preparedInputEpoch = Panic.InputEpoch; return;
|
|
3632
|
+
}
|
|
3530
3633
|
Panic.Check(op); CheckHumanInput();
|
|
3531
3634
|
long inputEpoch = Panic.InputEpoch;
|
|
3532
3635
|
IntPtr foreground = PreparationForeground();
|
|
@@ -3553,6 +3656,7 @@ namespace DshComputerUse.Worker
|
|
|
3553
3656
|
throw InputInterrupted("HUMAN_REOBSERVE", "The user changed the desktop during the preparation cue. No action was dispatched. Preserve the task, obtain fresh target evidence, and continue the authorized task immediately; this is not cancellation or abandonment.");
|
|
3554
3657
|
_lastAnnouncedSeries = series; _announcedHumanRevision = Panic.HumanRevision;
|
|
3555
3658
|
_announcedInputEpoch = _preparedInputEpoch = inputEpoch;
|
|
3659
|
+
ExtendReannounceGrace();
|
|
3556
3660
|
}
|
|
3557
3661
|
finally { Glow.Prepare(false); }
|
|
3558
3662
|
}
|
|
@@ -4008,6 +4112,10 @@ namespace DshComputerUse.Worker
|
|
|
4008
4112
|
object o; object av;
|
|
4009
4113
|
string op = req.TryGetValue("op", out o) ? o as string : null;
|
|
4010
4114
|
Dictionary<string, object> a = (req.TryGetValue("args", out av) && av is Dictionary<string, object>) ? (Dictionary<string, object>)av : new Dictionary<string, object>();
|
|
4115
|
+
// Host-driving debug gates travel WITH the request (lib/host-debug.js): gate 1 = the
|
|
4116
|
+
// calling conversation is danger-full-access, gate 2 = debug mode was enabled by hand.
|
|
4117
|
+
_requestHostDebug = GetB(a, "__hostDebug", false);
|
|
4118
|
+
_requestHostFull = GetB(a, "__hostFull", false);
|
|
4011
4119
|
bool cycleOp = GetB(a, "__cycle", false) && op != null &&
|
|
4012
4120
|
op != "abort" && op != "ping" && op != "echoargs" &&
|
|
4013
4121
|
op != "probe" && op != "keyClass" && op != "stuckPlan" &&
|
|
@@ -4136,6 +4244,8 @@ namespace DshComputerUse.Worker
|
|
|
4136
4244
|
case "type": FailsafeCheck(a); return Ok(Type(a));
|
|
4137
4245
|
case "clipRead": return Ok(ClipRead(a));
|
|
4138
4246
|
case "clipWrite": FailsafeCheck(a); return Ok(ClipWrite(a));
|
|
4247
|
+
case "launch": FailsafeCheck(a); return Ok(Launch(a));
|
|
4248
|
+
case "focusField": FailsafeCheck(a); return Ok(FocusField(a));
|
|
4139
4249
|
case "windows": return Ok(Windows(a));
|
|
4140
4250
|
case "activate": FailsafeCheck(a); return Ok(Activate(a));
|
|
4141
4251
|
case "windowOp": FailsafeCheck(a); return Ok(WindowOp(a));
|
|
@@ -4839,8 +4949,21 @@ namespace DshComputerUse.Worker
|
|
|
4839
4949
|
"verdict", detail == null ? "ok — the pixel belongs to the intended window" : (blocked + ": " + detail));
|
|
4840
4950
|
return dry;
|
|
4841
4951
|
}
|
|
4842
|
-
if (hostVictim)
|
|
4843
|
-
|
|
4952
|
+
if (hostVictim) GuardHostDriving(unchecked((int)PidOf(under)), "click");
|
|
4953
|
+
bool allowCovered = GetB(a, "allowCovered", false);
|
|
4954
|
+
// NARROWED (2026-11-07): allowCovered is an escape for the target app's OWN embedded
|
|
4955
|
+
// renderer only. It used to authorise the click whatever owned the pixel, so an
|
|
4956
|
+
// unrelated front window could receive input meant for the target app — measured
|
|
4957
|
+
// live when a covered click landed on the agent's own GUI instead of WeChat. An
|
|
4958
|
+
// ancestor test cannot be spoofed by z-order: the receiver must be the target's
|
|
4959
|
+
// descendant process (WeChatAppEx under Weixin.exe, an Electron utility process).
|
|
4960
|
+
if (allowCovered && blocked == "COVERED" &&
|
|
4961
|
+
!ProcessDescendsFrom(unchecked((int)PidOf(under)), unchecked((int)PidOf(want))))
|
|
4962
|
+
throw new Exception("precondition failed (COVERED-UNRELATED): " + detail +
|
|
4963
|
+
". allowCovered only authorises the target application's own embedded renderer " +
|
|
4964
|
+
"(a descendant of the target process); an unrelated window on top is never clicked. " +
|
|
4965
|
+
"NOTHING was clicked. Bring the target into view, or aim at a pixel inside it.");
|
|
4966
|
+
if (blocked != null && !allowCovered)
|
|
4844
4967
|
throw new Exception("precondition failed (" + blocked + "): " + detail + ". NOTHING was clicked. " +
|
|
4845
4968
|
"Bring the target into view first (activate / move / restore), or pass allowCovered:true to click anyway. " +
|
|
4846
4969
|
"Use computer_uia at:\"" + tx + "," + ty + "\" for the full ground truth.");
|
|
@@ -5024,7 +5147,7 @@ namespace DshComputerUse.Worker
|
|
|
5024
5147
|
AgentActing(a, "scroll");
|
|
5025
5148
|
if (a.ContainsKey("x") && a.ContainsKey("y")) Move(a);
|
|
5026
5149
|
IntPtr scrollTarget = InputPointReceiver();
|
|
5027
|
-
if (_stickyHwnd != IntPtr.Zero && scrollTarget != _stickyHwnd &&
|
|
5150
|
+
if (_stickyHwnd != IntPtr.Zero && scrollTarget != _stickyHwnd && !SameInputFamily(_stickyHwnd, scrollTarget))
|
|
5028
5151
|
throw InputInterrupted("INPUT_TARGET_CHANGED", "Scroll receiver differs from the observed target. No wheel input was sent.");
|
|
5029
5152
|
using (GuardReceiver(scrollTarget, InputWindowPid(scrollTarget), false)) {
|
|
5030
5153
|
for (int i = 0; i < lines; i++) {
|
|
@@ -5981,6 +6104,103 @@ namespace DshComputerUse.Worker
|
|
|
5981
6104
|
return Dict("written", true, "chars", text.Length);
|
|
5982
6105
|
}
|
|
5983
6106
|
|
|
6107
|
+
// ---------- launch: open an app / URL / folder / document in ONE op ----------
|
|
6108
|
+
// WHY THIS IS AN OP AND NOT A SIDE-EFFECT IN THE PLUGIN (2026-11-07, measured): the
|
|
6109
|
+
// plugin's first version spawned explorer from Node. It worked, but it never touched the
|
|
6110
|
+
// worker — so the status halo never lit, the human got no signal that the machine had just
|
|
6111
|
+
// been acted on, and audit/rate/cooperation had to be re-implemented by hand. This op puts
|
|
6112
|
+
// launch on the SAME funnel as every other actuation.
|
|
6113
|
+
//
|
|
6114
|
+
// RESOLUTION IS ShellExecute's, not ours: ProcessStartInfo.UseShellExecute resolves App
|
|
6115
|
+
// Paths aliases ("calc"), shell: verbs, URLs, folders and document associations exactly
|
|
6116
|
+
// like the Run dialog. Measured on this machine: `explorer.exe calc` opens a FILE EXPLORER
|
|
6117
|
+
// window (a bare word is a location to explorer), while ShellExecute("calc") starts the
|
|
6118
|
+
// Calculator — so the launcher, not a shell wrapper, is the correct resolver.
|
|
6119
|
+
//
|
|
6120
|
+
// INJECTION: the target crosses as ONE Executable/FileName value. No command line is
|
|
6121
|
+
// composed, no shell interpreter is involved, and nothing is appended after the target —
|
|
6122
|
+
// the same argv-boundary argument the plugin's validate() documents.
|
|
6123
|
+
static Dictionary<string, object> Launch(Dictionary<string, object> a)
|
|
6124
|
+
{
|
|
6125
|
+
string target = GetS(a, "target", "");
|
|
6126
|
+
if (target.Length == 0) throw new Exception("LAUNCH_BAD_TARGET: target is empty");
|
|
6127
|
+
// Optional arguments (2026-11-07). A dedicated browser profile is a LAUNCHER argument,
|
|
6128
|
+
// not part of the target: without this the only route to `msedge.exe --user-data-dir=…`
|
|
6129
|
+
// was Win+R, which the input guard reports as INPUT_TARGET_CHANGED after the chord has
|
|
6130
|
+
// already landed and then refuses screenshots of the dialog it just opened.
|
|
6131
|
+
string arguments = LaunchArguments(a);
|
|
6132
|
+
// Same gate as every other mutation: brake/pause/human-input checks run BEFORE the
|
|
6133
|
+
// process is started, and the deep steady blue says "the machine is being acted on".
|
|
6134
|
+
AgentActing(a, "launch");
|
|
6135
|
+
MarkMutation();
|
|
6136
|
+
System.Diagnostics.Process started = null;
|
|
6137
|
+
try
|
|
6138
|
+
{
|
|
6139
|
+
var info = new System.Diagnostics.ProcessStartInfo(target) { UseShellExecute = true };
|
|
6140
|
+
if (arguments.Length != 0) info.Arguments = arguments;
|
|
6141
|
+
started = System.Diagnostics.Process.Start(info);
|
|
6142
|
+
}
|
|
6143
|
+
catch (Exception e)
|
|
6144
|
+
{
|
|
6145
|
+
throw new Exception("LAUNCH_FAILED: " + e.Message);
|
|
6146
|
+
}
|
|
6147
|
+
// A launch returns in milliseconds; a 5 ms flash is invisible to a human. Hold the lit
|
|
6148
|
+
// border for a short, SELF-EXPIRING tail (Glow.Touch) instead of a latch, so this can
|
|
6149
|
+
// never claim the machine is still being driven after the action has finished.
|
|
6150
|
+
int tailMs = GetI(a, "tailMs", 2500);
|
|
6151
|
+
if (tailMs > 0)
|
|
6152
|
+
{
|
|
6153
|
+
tailMs = Math.Min(10000, tailMs);
|
|
6154
|
+
Glow.Enabled = true;
|
|
6155
|
+
Glow.Touch(tailMs);
|
|
6156
|
+
}
|
|
6157
|
+
int pid = 0;
|
|
6158
|
+
try { if (started != null) pid = started.Id; } catch { pid = 0; }
|
|
6159
|
+
return Dict("launched", true, "target", target, "pid", pid, "glowMs", Math.Max(0, tailMs));
|
|
6160
|
+
}
|
|
6161
|
+
|
|
6162
|
+
/// Quote ONE argument the way CommandLineToArgvW parses it back. Quoting happens here,
|
|
6163
|
+
/// exactly once: the caller passes an array of values, never a pre-built command line.
|
|
6164
|
+
static string QuoteArgument(string s)
|
|
6165
|
+
{
|
|
6166
|
+
if (s.Length > 0 && s.IndexOfAny(new[] { ' ', '\t', '"' }) < 0) return s;
|
|
6167
|
+
var sb = new StringBuilder();
|
|
6168
|
+
sb.Append('"');
|
|
6169
|
+
int backslashes = 0;
|
|
6170
|
+
foreach (char c in s)
|
|
6171
|
+
{
|
|
6172
|
+
if (c == '\\') { backslashes++; continue; }
|
|
6173
|
+
if (c == '"') { sb.Append('\\', backslashes * 2 + 1); sb.Append('"'); backslashes = 0; continue; }
|
|
6174
|
+
if (backslashes > 0) { sb.Append('\\', backslashes); backslashes = 0; }
|
|
6175
|
+
sb.Append(c);
|
|
6176
|
+
}
|
|
6177
|
+
if (backslashes > 0) sb.Append('\\', backslashes * 2);
|
|
6178
|
+
sb.Append('"');
|
|
6179
|
+
return sb.ToString();
|
|
6180
|
+
}
|
|
6181
|
+
|
|
6182
|
+
/// Validate the optional `args` array and turn it into one Windows command line.
|
|
6183
|
+
static string LaunchArguments(Dictionary<string, object> a)
|
|
6184
|
+
{
|
|
6185
|
+
object raw;
|
|
6186
|
+
if (!a.TryGetValue("args", out raw) || raw == null) return "";
|
|
6187
|
+
if (raw is string) throw new Exception("LAUNCH_BAD_ARGS: args must be an array of strings, not a string");
|
|
6188
|
+
var list = raw as System.Collections.IEnumerable;
|
|
6189
|
+
if (list == null) throw new Exception("LAUNCH_BAD_ARGS: args must be an array of strings");
|
|
6190
|
+
var parts = new List<string>();
|
|
6191
|
+
foreach (object item in list)
|
|
6192
|
+
{
|
|
6193
|
+
if (item == null) throw new Exception("LAUNCH_BAD_ARGS: null argument");
|
|
6194
|
+
string s = Convert.ToString(item);
|
|
6195
|
+
if (s.Length == 0) throw new Exception("LAUNCH_BAD_ARGS: empty argument");
|
|
6196
|
+
if (s.Length > 2000) throw new Exception("LAUNCH_BAD_ARGS: argument longer than 2000 characters");
|
|
6197
|
+
if (s.IndexOf('\0') >= 0) throw new Exception("LAUNCH_BAD_ARGS: NUL in argument");
|
|
6198
|
+
parts.Add(QuoteArgument(s));
|
|
6199
|
+
}
|
|
6200
|
+
if (parts.Count > 64) throw new Exception("LAUNCH_BAD_ARGS: more than 64 arguments");
|
|
6201
|
+
return string.Join(" ", parts);
|
|
6202
|
+
}
|
|
6203
|
+
|
|
5984
6204
|
// ---------- windows ----------
|
|
5985
6205
|
static Dictionary<string, object> RectDict(RECT r)
|
|
5986
6206
|
{
|
|
@@ -6104,16 +6324,25 @@ namespace DshComputerUse.Worker
|
|
|
6104
6324
|
// guard, which must re-assert the same window before typing).
|
|
6105
6325
|
string how = RefocusWindow(h, GetB(a, "clickFallback", true));
|
|
6106
6326
|
uint confirmedPid = 0;
|
|
6107
|
-
|
|
6327
|
+
long modalOf = 0;
|
|
6328
|
+
IntPtr foreground = Native.GetForegroundWindow();
|
|
6329
|
+
// A MODAL DIALOG of the same app becomes the real input receiver (Find/Replace, Save As,
|
|
6330
|
+
// a print dialog). Confirming the parent instead left every later key and screenshot
|
|
6331
|
+
// refused while the app was literally waiting for an answer.
|
|
6332
|
+
IntPtr dialog = (foreground != IntPtr.Zero && foreground != h && SameInputFamily(h, foreground)) ? foreground : IntPtr.Zero;
|
|
6333
|
+
IntPtr confirmed = dialog != IntPtr.Zero ? dialog : h;
|
|
6334
|
+
if (how == null || !Native.IsWindow(h) || (confirmedPid = PidOf(confirmed)) == 0 || !IsForeground(confirmed)) how = "failed";
|
|
6108
6335
|
else
|
|
6109
6336
|
{
|
|
6110
|
-
|
|
6111
|
-
|
|
6337
|
+
if (dialog != IntPtr.Zero) modalOf = h.ToInt64();
|
|
6338
|
+
SetSticky(confirmed);
|
|
6339
|
+
_kickoffFocus.Confirm(confirmed.ToInt64(), confirmedPid);
|
|
6112
6340
|
}
|
|
6113
6341
|
CheckHumanInput();
|
|
6114
6342
|
if (how != "failed") _keyboardReobserveRequired = false;
|
|
6115
6343
|
return Dict("activated", how != "failed", "activatedVia", how,
|
|
6116
|
-
"hwnd",
|
|
6344
|
+
"hwnd", confirmed.ToInt64(), "pid", confirmedPid == 0 ? null : (object)confirmedPid,
|
|
6345
|
+
"modalOf", modalOf == 0 ? null : (object)modalOf,
|
|
6117
6346
|
"hint", how == "failed"
|
|
6118
6347
|
? "foreground lock refused every method; click the window (or its taskbar button) and retry"
|
|
6119
6348
|
: null);
|
|
@@ -6158,7 +6387,9 @@ namespace DshComputerUse.Worker
|
|
|
6158
6387
|
long target = _kickoffFocus.Target;
|
|
6159
6388
|
string problem = _kickoffFocus.CaptureProblem(true, false, false,
|
|
6160
6389
|
Native.IsWindow(new IntPtr(target)), foreground.ToInt64(), pid, false);
|
|
6161
|
-
|
|
6390
|
+
if (problem == null) return target;
|
|
6391
|
+
// Same-family foreground (a dialog of this app) is the target now.
|
|
6392
|
+
return target != 0 && SameInputFamily(new IntPtr(target), foreground) ? foreground.ToInt64() : 0;
|
|
6162
6393
|
}
|
|
6163
6394
|
static void ValidateKickoffCapture(Dictionary<string, object> a)
|
|
6164
6395
|
{
|
|
@@ -6167,7 +6398,10 @@ namespace DshComputerUse.Worker
|
|
|
6167
6398
|
IntPtr target = new IntPtr(_kickoffFocus.Target);
|
|
6168
6399
|
IntPtr fg = TopLevel(Native.GetForegroundWindow());
|
|
6169
6400
|
string problem = _kickoffFocus.CaptureProblem(Panic.CycleLit, Panic.Stopped, Panic.Exited,
|
|
6170
|
-
target != IntPtr.Zero && Native.IsWindow(target), fg.ToInt64(), PidOf(fg), IsHostWindow(fg));
|
|
6401
|
+
target != IntPtr.Zero && Native.IsWindow(target), fg.ToInt64(), PidOf(fg), IsHostWindow(fg) && !HostDrivingAllowed());
|
|
6402
|
+
// A dialog of the same app is the target now (its hwnd replaced the one we confirmed):
|
|
6403
|
+
// refusing there blocked the very screenshot that shows what the dialog is asking.
|
|
6404
|
+
if (problem != null && target != IntPtr.Zero && SameInputFamily(target, fg)) return;
|
|
6171
6405
|
if (problem != null)
|
|
6172
6406
|
{
|
|
6173
6407
|
// NAME THE ACTUAL FOREGROUND (test report 2026-11-04): "identify the intended
|
|
@@ -6215,7 +6449,7 @@ namespace DshComputerUse.Worker
|
|
|
6215
6449
|
// The host is never "the window the agent is working in": 2026-09-12 the sticky
|
|
6216
6450
|
// target followed the host window and the next click went into it. Click/activate
|
|
6217
6451
|
// already refuse a host window, so this only removes the retry path.
|
|
6218
|
-
if (t != IntPtr.Zero && Native.IsWindow(t) && !IsHostWindow(t)) _stickyHwnd = t;
|
|
6452
|
+
if (t != IntPtr.Zero && Native.IsWindow(t) && (!IsHostWindow(t) || HostDrivingAllowed())) _stickyHwnd = t;
|
|
6219
6453
|
}
|
|
6220
6454
|
|
|
6221
6455
|
static uint PidOf(IntPtr h) { uint pid = 0; try { Native.GetWindowThreadProcessId(h, out pid); } catch { } return pid; }
|
|
@@ -6489,6 +6723,39 @@ namespace DshComputerUse.Worker
|
|
|
6489
6723
|
return IsHostPid(unchecked((int)PidOf(h)));
|
|
6490
6724
|
}
|
|
6491
6725
|
|
|
6726
|
+
/// True when `child` is `ancestor` itself or a descendant process of it (bounded walk).
|
|
6727
|
+
/// This is the ONLY relationship that makes a covered click legitimate: the target app's
|
|
6728
|
+
/// own embedded renderer (e.g. WeChatAppEx under Weixin.exe). An unrelated window that
|
|
6729
|
+
/// happens to be on top must never receive the input.
|
|
6730
|
+
public static bool ProcessDescendsFrom(int child, int ancestor)
|
|
6731
|
+
{
|
|
6732
|
+
if (child <= 0 || ancestor <= 0) return false;
|
|
6733
|
+
if (child == ancestor) return true;
|
|
6734
|
+
IntPtr snapshot = Native.CreateToolhelp32Snapshot(Native.TH32CS_SNAPPROCESS, 0);
|
|
6735
|
+
if (snapshot == IntPtr.Zero || snapshot == new IntPtr(-1)) return false;
|
|
6736
|
+
try
|
|
6737
|
+
{
|
|
6738
|
+
uint size = (uint)Marshal.SizeOf(typeof(Native.PROCESSENTRY32W));
|
|
6739
|
+
int pid = child;
|
|
6740
|
+
for (int hop = 0; hop < 16 && pid > 0; hop++)
|
|
6741
|
+
{
|
|
6742
|
+
var entry = new Native.PROCESSENTRY32W();
|
|
6743
|
+
entry.dwSize = size;
|
|
6744
|
+
if (!Native.Process32FirstW(snapshot, ref entry)) return false;
|
|
6745
|
+
int parent = 0;
|
|
6746
|
+
do
|
|
6747
|
+
{
|
|
6748
|
+
if (unchecked((int)entry.th32ProcessID) == pid) { parent = unchecked((int)entry.th32ParentProcessID); break; }
|
|
6749
|
+
} while (Native.Process32NextW(snapshot, ref entry));
|
|
6750
|
+
if (parent <= 0) return false;
|
|
6751
|
+
if (parent == ancestor) return true;
|
|
6752
|
+
pid = parent;
|
|
6753
|
+
}
|
|
6754
|
+
return false;
|
|
6755
|
+
}
|
|
6756
|
+
finally { try { Native.CloseHandle(snapshot); } catch { } }
|
|
6757
|
+
}
|
|
6758
|
+
|
|
6492
6759
|
/// Did the host identity reach us from the launcher? The keyboard half of the human-presence
|
|
6493
6760
|
/// monitor is gated on it (see Panic.MonitorHuman / HookCallback): without it we cannot
|
|
6494
6761
|
/// exempt "the human is talking to the agent in the chat", and braking on every chat message
|
|
@@ -6506,20 +6773,88 @@ namespace DshComputerUse.Worker
|
|
|
6506
6773
|
"The agent must never drive its own host — activate the intended application first.";
|
|
6507
6774
|
}
|
|
6508
6775
|
|
|
6776
|
+
// ---- host-driving debug gates (lib/host-debug.js) ----
|
|
6777
|
+
// Driving the agent's OWN Harness window is refused by default: the host holds the human's
|
|
6778
|
+
// approval cards, the question card and the Stop control, so an agent that could click it
|
|
6779
|
+
// could approve itself. Self-iteration (debugging another plugin in this same instance)
|
|
6780
|
+
// needs it, so there are exactly two gates, both fail-closed, both set OUTSIDE a tool call:
|
|
6781
|
+
// 1. the CALLING conversation is danger-full-access (the human's composer setting);
|
|
6782
|
+
// 2. debug mode was switched on by hand in Settings -> 电脑操控(调试).
|
|
6783
|
+
// A tool cannot write either: gate 1 lives in the session policy, gate 2 in a host-side file
|
|
6784
|
+
// that only the settings page writes.
|
|
6785
|
+
static bool _requestHostDebug, _requestHostFull;
|
|
6786
|
+
static string _hostDebugFile; static bool _hostDebugFileInit, _hostDebugValue; static int _hostDebugAt;
|
|
6787
|
+
|
|
6788
|
+
/// Hot read of the settings switch (500 ms cache), so toggling it needs no restart.
|
|
6789
|
+
static bool HostDebugFileEnabled()
|
|
6790
|
+
{
|
|
6791
|
+
if (!_hostDebugFileInit)
|
|
6792
|
+
{
|
|
6793
|
+
_hostDebugFileInit = true;
|
|
6794
|
+
try { string p = Environment.GetEnvironmentVariable("DSH_CU_HOST_DEBUG_FILE"); _hostDebugFile = string.IsNullOrEmpty(p) ? null : p; }
|
|
6795
|
+
catch { _hostDebugFile = null; }
|
|
6796
|
+
}
|
|
6797
|
+
if (_hostDebugFile == null) return false;
|
|
6798
|
+
int now = unchecked(Environment.TickCount);
|
|
6799
|
+
if (_hostDebugAt != 0 && unchecked(now - _hostDebugAt) < 500) return _hostDebugValue;
|
|
6800
|
+
_hostDebugAt = now;
|
|
6801
|
+
try
|
|
6802
|
+
{
|
|
6803
|
+
string text = System.IO.File.ReadAllText(_hostDebugFile).Replace(" ", "");
|
|
6804
|
+
_hostDebugValue = text.IndexOf("\"enabled\":true", StringComparison.Ordinal) >= 0;
|
|
6805
|
+
}
|
|
6806
|
+
catch { _hostDebugValue = false; }
|
|
6807
|
+
return _hostDebugValue;
|
|
6808
|
+
}
|
|
6809
|
+
|
|
6810
|
+
/// True when the core configured host identity at all (production always does).
|
|
6811
|
+
static bool HostGuardConfigured() { HostGuardInit(); return _hostPid > 0 || _hostExe != null; }
|
|
6812
|
+
|
|
6813
|
+
/// Last-resort protection for workers launched WITHOUT host identity (CLI/MCP/test
|
|
6814
|
+
/// harnesses): the 2026-11-08 accident sent WM_CLOSE to the real Harness from exactly such
|
|
6815
|
+
/// a harness. Such a worker must never close/minimize/hide the DSH window unless the caller
|
|
6816
|
+
/// explicitly passes confirmHost:true.
|
|
6817
|
+
static bool LooksLikeHarness(IntPtr h)
|
|
6818
|
+
{
|
|
6819
|
+
try
|
|
6820
|
+
{
|
|
6821
|
+
if (h == IntPtr.Zero) return false;
|
|
6822
|
+
string p = ProcessImagePath(PidOf(h));
|
|
6823
|
+
if (p == null) return false;
|
|
6824
|
+
return string.Equals(System.IO.Path.GetFileName(p.Trim()), "DeepSeek Harness.exe", StringComparison.OrdinalIgnoreCase);
|
|
6825
|
+
}
|
|
6826
|
+
catch { return false; }
|
|
6827
|
+
}
|
|
6828
|
+
|
|
6829
|
+
/// BOTH gates required. Unknown state fails closed.
|
|
6830
|
+
static bool HostDrivingAllowed() { return _requestHostDebug && _requestHostFull && HostDebugFileEnabled(); }
|
|
6831
|
+
|
|
6832
|
+
static void GuardHostDriving(int pid, string op)
|
|
6833
|
+
{
|
|
6834
|
+
if (op == "close" || op == "minimize" || op == "hide")
|
|
6835
|
+
throw new Exception("HOST_DEBUG_GATE: " + op + " on the agent's own Harness window is never allowed, not even in debug mode. Nothing was dispatched.");
|
|
6836
|
+
if (HostDrivingAllowed())
|
|
6837
|
+
{
|
|
6838
|
+
try { AuditRecord("HOST-CONTROL", op + "; pid=" + pid + "; debug=1; full=1"); } catch { }
|
|
6839
|
+
return;
|
|
6840
|
+
}
|
|
6841
|
+
throw new Exception("HOST_DEBUG_GATE: " + op + " targets the agent's own Harness window (pid " + pid + "). " +
|
|
6842
|
+
"To iterate on plugins in this same instance, open Settings -> 电脑操控(调试), switch debug mode ON, and make sure THIS conversation is 完全权限 (danger-full-access). " +
|
|
6843
|
+
"Nothing was dispatched. Ordinary work should target the application window instead.");
|
|
6844
|
+
}
|
|
6845
|
+
|
|
6509
6846
|
/// The refusal itself. It throws, exactly like the click precondition below, so Main
|
|
6510
|
-
/// answers through the existing error helper Err(...) -> {"ok":false,"error":"
|
|
6511
|
-
/// and the Node layer rejects the call with that message. Returning an Err(...) dict from a
|
|
6512
|
-
/// handler would NOT work: the dispatcher wraps every handler result in Ok(...).
|
|
6847
|
+
/// answers through the existing error helper Err(...) -> {"ok":false,"error":"HOST_DEBUG_GATE: ..."}
|
|
6513
6848
|
static void HostGuard(IntPtr window, string op)
|
|
6514
6849
|
{
|
|
6515
6850
|
if (!IsHostWindow(window)) return;
|
|
6516
|
-
|
|
6851
|
+
GuardHostDriving(unchecked((int)PidOf(window)), op);
|
|
6517
6852
|
}
|
|
6518
6853
|
|
|
6519
6854
|
static void HostGuardPid(int pid, string op)
|
|
6520
6855
|
{
|
|
6521
6856
|
if (!IsHostPid(pid)) return;
|
|
6522
|
-
|
|
6857
|
+
GuardHostDriving(pid, op);
|
|
6523
6858
|
}
|
|
6524
6859
|
|
|
6525
6860
|
/// For ops whose victim is the window under a POINT: the pixel decides, exactly as the
|
|
@@ -6963,6 +7298,113 @@ namespace DshComputerUse.Worker
|
|
|
6963
7298
|
|
|
6964
7299
|
|
|
6965
7300
|
|
|
7301
|
+
// ---------- focus: put the keyboard somewhere, and PROVE it before typing ----------
|
|
7302
|
+
// Typing follows the focused CONTROL, not the foreground window: activating Word's Find
|
|
7303
|
+
// dialog and typing still landed in the document (measured 2026-11-07 — "abc" was inserted
|
|
7304
|
+
// into the .doc while the field stayed empty). This op clicks the field, reports where focus
|
|
7305
|
+
// actually went (UIA focused element), and whether a caret is provable there, so a caller
|
|
7306
|
+
// can refuse to type into an unproven field instead of discovering it in the file.
|
|
7307
|
+
static Dictionary<string, object> FocusedElementReport()
|
|
7308
|
+
{
|
|
7309
|
+
try
|
|
7310
|
+
{
|
|
7311
|
+
AutomationElement fe = AutomationElement.FocusedElement;
|
|
7312
|
+
if (fe == null) return Dict("available", false);
|
|
7313
|
+
string type = null, name = null, id = null;
|
|
7314
|
+
try { type = fe.Current.ControlType.ProgrammaticName; } catch { }
|
|
7315
|
+
try { name = fe.Current.Name; } catch { }
|
|
7316
|
+
try { id = fe.Current.AutomationId; } catch { }
|
|
7317
|
+
return Dict("available", true, "controlType", type, "name", Short(name, 48), "automationId", id);
|
|
7318
|
+
}
|
|
7319
|
+
catch (Exception e) { return Dict("available", false, "error", e.GetType().Name); }
|
|
7320
|
+
}
|
|
7321
|
+
|
|
7322
|
+
static Dictionary<string, object> CaretReport()
|
|
7323
|
+
{
|
|
7324
|
+
try
|
|
7325
|
+
{
|
|
7326
|
+
Native.GUITHREADINFO info = new Native.GUITHREADINFO();
|
|
7327
|
+
info.cbSize = Marshal.SizeOf(typeof(Native.GUITHREADINFO));
|
|
7328
|
+
IntPtr fg = Native.GetForegroundWindow();
|
|
7329
|
+
uint pid;
|
|
7330
|
+
uint thread = Native.GetWindowThreadProcessId(fg, out pid);
|
|
7331
|
+
if (thread == 0 || !Native.GetGUIThreadInfo(thread, ref info) || info.hwndCaret == IntPtr.Zero)
|
|
7332
|
+
return Dict("available", false);
|
|
7333
|
+
POINT p = new POINT();
|
|
7334
|
+
p.X = info.rcCaret.Left; p.Y = info.rcCaret.Top;
|
|
7335
|
+
Native.ClientToScreen(info.hwndCaret, ref p);
|
|
7336
|
+
return Dict("available", true, "x", p.X, "y", p.Y,
|
|
7337
|
+
"height", Math.Max(1, info.rcCaret.Bottom - info.rcCaret.Top));
|
|
7338
|
+
}
|
|
7339
|
+
catch (Exception e) { return Dict("available", false, "error", e.GetType().Name); }
|
|
7340
|
+
}
|
|
7341
|
+
|
|
7342
|
+
static Dictionary<string, object> FocusField(Dictionary<string, object> a)
|
|
7343
|
+
{
|
|
7344
|
+
int x = GetI(a, "x", int.MinValue), y = GetI(a, "y", int.MinValue);
|
|
7345
|
+
if (x == int.MinValue || y == int.MinValue)
|
|
7346
|
+
throw new Exception("FOCUS_POINT_REQUIRED: focus needs the field's x,y in screen pixels; nothing was clicked");
|
|
7347
|
+
IntPtr want = a.ContainsKey("hwnd") && GetI(a, "hwnd", 0) != 0 ? new IntPtr(GetI(a, "hwnd", 0)) : IntPtr.Zero;
|
|
7348
|
+
AgentActing(a, "focusField");
|
|
7349
|
+
MarkMutation();
|
|
7350
|
+
if (want != IntPtr.Zero) { RefocusWindow(want, true); Thread.Sleep(60); }
|
|
7351
|
+
AnimatedMove(x, y, GetI(a, "dur", 140));
|
|
7352
|
+
Thread.Sleep(GetI(a, "settleMs", 90));
|
|
7353
|
+
IntPtr under = InputPointReceiver();
|
|
7354
|
+
IntPtr target = want != IntPtr.Zero ? want : TopLevel(under);
|
|
7355
|
+
if (!SameInputFamily(target, under))
|
|
7356
|
+
throw new Exception("precondition failed (COVERED): the pixel belongs to \"" + Short(WinTitleOf(under), 40) + "\" (pid " +
|
|
7357
|
+
PidOf(under) + "), not to the window you aimed at. NOTHING was clicked.");
|
|
7358
|
+
using (GuardReceiver(under, InputWindowPid(under), false))
|
|
7359
|
+
{
|
|
7360
|
+
SendMouse(Native.MOUSEEVENTF_LEFTDOWN, 0, 0, 0);
|
|
7361
|
+
Thread.Sleep(30);
|
|
7362
|
+
SendMouse(Native.MOUSEEVENTF_LEFTUP, 0, 0, 0);
|
|
7363
|
+
}
|
|
7364
|
+
// The click moved focus inside this app: confirm the foreground as the observation
|
|
7365
|
+
// target, or the capture that must prove the field would be refused as unconfirmed.
|
|
7366
|
+
IntPtr fgTop = TopLevel(Native.GetForegroundWindow());
|
|
7367
|
+
if (fgTop != IntPtr.Zero)
|
|
7368
|
+
{
|
|
7369
|
+
SetSticky(fgTop);
|
|
7370
|
+
_kickoffFocus.Confirm(fgTop.ToInt64(), PidOf(fgTop));
|
|
7371
|
+
}
|
|
7372
|
+
EndActuationForReadback();
|
|
7373
|
+
Thread.Sleep(GetI(a, "readDelayMs", 180));
|
|
7374
|
+
Dictionary<string, object> focused = FocusedElementReport();
|
|
7375
|
+
Dictionary<string, object> caret = CaretReport();
|
|
7376
|
+
object rawType;
|
|
7377
|
+
string controlType = focused.TryGetValue("controlType", out rawType) ? Convert.ToString(rawType) : null;
|
|
7378
|
+
bool textTarget = controlType != null &&
|
|
7379
|
+
(controlType.IndexOf("Edit", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
|
7380
|
+
controlType.IndexOf("Document", StringComparison.OrdinalIgnoreCase) >= 0);
|
|
7381
|
+
bool caretVisible = caret.ContainsKey("available") && Convert.ToBoolean(caret["available"]);
|
|
7382
|
+
// UNCERTAIN CASE: the OS reports no caret (browser content, some editors). Fall back to
|
|
7383
|
+
// the one property a caret always has — it blinks: two tight crops 260 ms apart differ.
|
|
7384
|
+
bool blink = false;
|
|
7385
|
+
if (!caretVisible && GetB(a, "verifyCaret", true))
|
|
7386
|
+
{
|
|
7387
|
+
int bx = Math.Max(0, x - 30), by = Math.Max(0, y - 16);
|
|
7388
|
+
byte[] first = FrameSignatureRect(bx, by, 60, 32, 2);
|
|
7389
|
+
Thread.Sleep(260);
|
|
7390
|
+
byte[] second = FrameSignatureRect(bx, by, 60, 32, 2);
|
|
7391
|
+
blink = first != null && second != null && first.Length == second.Length && SigDiffPct(first, second, 8) >= 0.05;
|
|
7392
|
+
}
|
|
7393
|
+
bool verified = textTarget && (caretVisible || blink);
|
|
7394
|
+
return Dict(
|
|
7395
|
+
"clicked", Dict("x", x, "y", y),
|
|
7396
|
+
"receiver", under == IntPtr.Zero ? null : (object)(Short(WinTitleOf(under), 40) + " (pid " + PidOf(under) + ")"),
|
|
7397
|
+
"focused", focused,
|
|
7398
|
+
"textTarget", textTarget,
|
|
7399
|
+
"caret", caret,
|
|
7400
|
+
"caretVerified", caretVisible,
|
|
7401
|
+
"caretBlink", blink,
|
|
7402
|
+
"verified", verified,
|
|
7403
|
+
"reason", verified ? null : (textTarget
|
|
7404
|
+
? "focus reached a text control but no caret is provable; capture the field, click it again, and re-check before typing"
|
|
7405
|
+
: "focus landed on a non-text control (" + (controlType ?? "unknown") + "); click a field before typing"));
|
|
7406
|
+
}
|
|
7407
|
+
|
|
6966
7408
|
static Dictionary<string, object> WindowOp(Dictionary<string, object> a)
|
|
6967
7409
|
{
|
|
6968
7410
|
IntPtr h = FindHwnd(a);
|
|
@@ -6971,6 +7413,13 @@ namespace DshComputerUse.Worker
|
|
|
6971
7413
|
string op = GetS(a, "op", "minimize");
|
|
6972
7414
|
AgentActing(a, "windowOp");
|
|
6973
7415
|
MarkMutation();
|
|
7416
|
+
// Closing/minimizing/hiding the host is never negotiable: debug mode exists so the agent
|
|
7417
|
+
// can click a sidebar to debug a plugin, not so it can make its own window disappear.
|
|
7418
|
+
// The LooksLikeHarness fallback covers a worker launched WITHOUT host identity (CLI,
|
|
7419
|
+
// MCP, a test harness): it must not be able to close the Harness by accident either.
|
|
7420
|
+
bool harnessTarget = IsHostWindow(h) || (!HostGuardConfigured() && LooksLikeHarness(h));
|
|
7421
|
+
if ((op == "close" || op == "minimize" || op == "hide") && harnessTarget && !GetB(a, "confirmHost", false))
|
|
7422
|
+
throw new Exception("HOST_DEBUG_GATE: " + op + " on the agent's own Harness window is never allowed, not even in debug mode. Nothing was dispatched. (confirmHost:true is for an intentional CLI shutdown only.)");
|
|
6974
7423
|
switch (op)
|
|
6975
7424
|
{
|
|
6976
7425
|
case "minimize": Native.ShowWindow(h, Native.SW_MINIMIZE); break;
|