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.
@@ -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 bool IsWindow(IntPtr hWnd);
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) throw InputInterrupted("INPUT_TARGET_CHANGED", problem + ". Remaining input was refused; inspect any partial effect before retrying.");
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) throw new Exception(HostGuardMsg("click", unchecked((int)PidOf(under))));
4843
- if (blocked != null && !GetB(a, "allowCovered", false))
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 && InputWindowPid(scrollTarget) != InputWindowPid(_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
- if (how == null || !Native.IsWindow(h) || !IsForeground(h) || (confirmedPid = PidOf(h)) == 0) how = "failed";
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
- SetSticky(h);
6111
- _kickoffFocus.Confirm(h.ToInt64(), confirmedPid);
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", h.ToInt64(), "pid", confirmedPid == 0 ? null : (object)confirmedPid,
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
- return problem == null ? target : 0;
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":"HOST-GUARDED: ..."}
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
- throw new Exception(HostGuardMsg(op, unchecked((int)PidOf(window))));
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
- throw new Exception(HostGuardMsg(op, pid));
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;