newmark-agent 0.6.4 → 0.6.8

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.
Files changed (76) hide show
  1. package/config.example.json +13 -3
  2. package/dist/cli-commands.d.ts +1 -1
  3. package/dist/cli-commands.js +54 -3
  4. package/dist/cli-discovery.js +3 -0
  5. package/dist/conversation-utility-host.bundle.cjs +4472 -1767
  6. package/dist/conversation-utility-host.js +94 -7
  7. package/dist/core/agent.d.ts +126 -12
  8. package/dist/core/agent.js +785 -172
  9. package/dist/core/agentKernelRunner.d.ts +7 -1
  10. package/dist/core/agentKernelRunner.js +160 -20
  11. package/dist/core/autoRouter.d.ts +49 -44
  12. package/dist/core/autoRouter.js +117 -302
  13. package/dist/core/computerUseSession.js +1 -1
  14. package/dist/core/config.js +27 -11
  15. package/dist/core/continuation/contracts.d.ts +1 -1
  16. package/dist/core/continuation/store.d.ts +91 -1
  17. package/dist/core/continuation/store.js +291 -61
  18. package/dist/core/conversationKernel.d.ts +49 -7
  19. package/dist/core/conversationKernel.js +303 -51
  20. package/dist/core/conversationStateDocument.d.ts +18 -0
  21. package/dist/core/conversationStateDocument.js +88 -0
  22. package/dist/core/conversationVisualBudget.d.ts +74 -0
  23. package/dist/core/conversationVisualBudget.js +153 -0
  24. package/dist/core/electronUtilityAgentClient.d.ts +3 -2
  25. package/dist/core/electronUtilityAgentClient.js +32 -8
  26. package/dist/core/electronUtilityRuntimePool.d.ts +28 -3
  27. package/dist/core/electronUtilityRuntimePool.js +93 -24
  28. package/dist/core/fileWriteObservation.d.ts +41 -0
  29. package/dist/core/fileWriteObservation.js +454 -0
  30. package/dist/core/hostRuntimeHooks.d.ts +23 -0
  31. package/dist/core/hostRuntimeHooks.js +17 -0
  32. package/dist/core/jevDecision.d.ts +144 -0
  33. package/dist/core/jevDecision.js +226 -0
  34. package/dist/core/memoryProbe.d.ts +24 -0
  35. package/dist/core/memoryProbe.js +104 -0
  36. package/dist/core/mobilePairing.d.ts +7 -1
  37. package/dist/core/mobilePairing.js +9 -1
  38. package/dist/core/performanceDiagnostics.d.ts +1 -1
  39. package/dist/core/routeDecisionValidator.d.ts +86 -0
  40. package/dist/core/routeDecisionValidator.js +249 -0
  41. package/dist/core/routeEligibility.d.ts +52 -0
  42. package/dist/core/routeEligibility.js +108 -0
  43. package/dist/core/runtimeMemoryBudget.d.ts +6 -0
  44. package/dist/core/runtimeMemoryBudget.js +12 -0
  45. package/dist/core/toolPolicy.d.ts +1 -1
  46. package/dist/core/toolPolicy.js +1 -1
  47. package/dist/core/utilityAgentProtocol.d.ts +8 -10
  48. package/dist/core/utilityHostToolRouter.js +2 -0
  49. package/dist/core/visualDownscale.d.ts +6 -0
  50. package/dist/core/visualDownscale.js +137 -0
  51. package/dist/core/wslAgentClient.d.ts +1 -2
  52. package/dist/core/wslAgentClient.js +0 -8
  53. package/dist/core/wslAgentProtocol.d.ts +1 -10
  54. package/dist/core/wslAgentRuntimePool.d.ts +1 -3
  55. package/dist/core/wslAgentRuntimePool.js +17 -22
  56. package/dist/llm/provider.d.ts +3 -1
  57. package/dist/llm/provider.js +53 -9
  58. package/dist/main.js +107 -30
  59. package/dist/preload.js +3 -1
  60. package/dist/server.js +30 -18
  61. package/dist/tools/computerUse.d.ts +6 -0
  62. package/dist/tools/computerUse.js +406 -21
  63. package/dist/tools/computerUsePowerShellHost.d.ts +6 -1
  64. package/dist/tools/computerUsePowerShellHost.js +105 -31
  65. package/dist/tools/index.d.ts +3 -4
  66. package/dist/tools/index.js +146 -66
  67. package/dist/tui/src/adapters/core-runtime-adapter.js +13 -1
  68. package/dist/tui/src/i18n.js +12 -1
  69. package/dist/tui/src/render.js +9 -2
  70. package/dist/tui/src/settings-schema.js +19 -0
  71. package/dist/tui/src/state.js +69 -1
  72. package/dist/ui/index.html +651 -233
  73. package/dist/ui/lucide-sprite.svg +0 -8
  74. package/dist/wsl-agent-host.bundle.cjs +4302 -1767
  75. package/dist/wsl-agent-host.js +0 -3
  76. package/package.json +39 -10
@@ -35,48 +35,90 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.runPersistentPowerShell = runPersistentPowerShell;
37
37
  exports.computerUsePowerShellLaneReady = computerUsePowerShellLaneReady;
38
+ exports.prepareComputerUsePowerShellLane = prepareComputerUsePowerShellLane;
39
+ exports.stopComputerUsePowerShellLane = stopComputerUsePowerShellLane;
38
40
  exports.stopComputerUsePowerShellHost = stopComputerUsePowerShellHost;
39
41
  const child_process_1 = require("child_process");
40
42
  const crypto = __importStar(require("crypto"));
41
43
  const readline = __importStar(require("readline"));
42
- const HOST_SCRIPT = [
43
- '[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)',
44
- '[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)',
45
- '$OutputEncoding = [System.Text.UTF8Encoding]::new($false)',
46
- 'Add-Type -AssemblyName System.Windows.Forms',
47
- 'Add-Type -AssemblyName System.Drawing',
48
- 'Add-Type -AssemblyName UIAutomationClient',
49
- 'Add-Type -AssemblyName UIAutomationTypes',
50
- `if (-not ("NewmarkComputerUseNative" -as [type])) { Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public static class NewmarkComputerUseNative { [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y); [DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, int dwData, UIntPtr dwExtraInfo); [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); }' }`,
51
- 'while (($line = [Console]::In.ReadLine()) -ne $null) {',
52
- ' $id = ""',
53
- ' $timer = [System.Diagnostics.Stopwatch]::StartNew()',
54
- ' try {',
55
- ' $request = $line | ConvertFrom-Json',
56
- ' $id = [string]$request.id',
57
- ' $source = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String([string]$request.script))',
58
- ' $previousPreference = $ErrorActionPreference',
59
- ' $ErrorActionPreference = "Stop"',
60
- ' try { $output = (& ([scriptblock]::Create($source)) 2>&1 | Out-String -Width 1048576).Trim() } finally { $ErrorActionPreference = $previousPreference }',
61
- ' $response = @{ id=$id; ok=$true; output=$output; elapsed_ms=[int]$timer.ElapsedMilliseconds }',
62
- ' } catch {',
63
- ' $response = @{ id=$id; ok=$false; output=[string]$_; elapsed_ms=[int]$timer.ElapsedMilliseconds }',
64
- ' }',
65
- ' [Console]::Out.WriteLine(($response | ConvertTo-Json -Compress -Depth 6))',
66
- ' [Console]::Out.Flush()',
67
- '}',
68
- ].join('; ');
44
+ function createHostScript(includeUiAutomation) {
45
+ return [
46
+ '[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)',
47
+ '[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)',
48
+ '$OutputEncoding = [System.Text.UTF8Encoding]::new($false)',
49
+ 'Add-Type -AssemblyName System.Windows.Forms',
50
+ 'Add-Type -AssemblyName System.Drawing',
51
+ ...(includeUiAutomation ? ['Add-Type -AssemblyName UIAutomationClient', 'Add-Type -AssemblyName UIAutomationTypes'] : []),
52
+ ...(includeUiAutomation ? [`if (-not ("NewmarkComputerUseNative" -as [type])) { Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public static class NewmarkComputerUseNative { [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y); [DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, int dwData, UIntPtr dwExtraInfo); [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); }' }`] : []),
53
+ ...(!includeUiAutomation ? [`if (-not ("NewmarkSparseCapture" -as [type])) { Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public static class NewmarkSparseCapture { [StructLayout(LayoutKind.Sequential)] struct BitmapInfoHeader { public uint Size; public int Width; public int Height; public ushort Planes; public ushort BitCount; public uint Compression; public uint SizeImage; public int XPelsPerMeter; public int YPelsPerMeter; public uint ColorsUsed; public uint ColorsImportant; } [StructLayout(LayoutKind.Sequential)] struct BitmapInfo { public BitmapInfoHeader Header; public uint Colors; } [DllImport("user32.dll")] static extern IntPtr GetDC(IntPtr hWnd); [DllImport("user32.dll")] static extern int ReleaseDC(IntPtr hWnd, IntPtr hdc); [DllImport("gdi32.dll")] static extern IntPtr CreateCompatibleDC(IntPtr hdc); [DllImport("gdi32.dll")] static extern bool DeleteDC(IntPtr hdc); [DllImport("gdi32.dll")] static extern IntPtr CreateDIBSection(IntPtr hdc, ref BitmapInfo info, uint usage, out IntPtr bits, IntPtr section, uint offset); [DllImport("gdi32.dll")] static extern IntPtr SelectObject(IntPtr hdc, IntPtr obj); [DllImport("gdi32.dll")] static extern bool DeleteObject(IntPtr obj); [DllImport("gdi32.dll")] static extern int SetStretchBltMode(IntPtr hdc, int mode); [DllImport("gdi32.dll")] static extern bool SetBrushOrgEx(IntPtr hdc, int x, int y, IntPtr oldOrigin); [DllImport("gdi32.dll", SetLastError=true)] static extern bool StretchBlt(IntPtr dst, int dx, int dy, int dw, int dh, IntPtr src, int sx, int sy, int sw, int sh, int operation); [DllImport("gdi32.dll")] static extern bool GdiFlush(); public sealed class Capture : IDisposable { private int width; private byte[] pixels; internal Capture(int w, byte[] p) { width=w; pixels=p; } public byte GetLuma(int x, int y) { int offset=(y*width+x)*4; return (byte)Math.Round((pixels[offset+2]*0.299)+(pixels[offset+1]*0.587)+(pixels[offset]*0.114)); } public void Dispose() { pixels=null; } } public static Capture CaptureScaled(int x, int y, int width, int height, int targetWidth, int targetHeight) { IntPtr screen=GetDC(IntPtr.Zero); if (screen == IntPtr.Zero) throw new InvalidOperationException("Could not read desktop DC."); IntPtr memory=IntPtr.Zero; IntPtr bitmap=IntPtr.Zero; IntPtr previous=IntPtr.Zero; IntPtr bits=IntPtr.Zero; try { memory=CreateCompatibleDC(screen); BitmapInfo info=new BitmapInfo(); info.Header.Size=(uint)System.Runtime.InteropServices.Marshal.SizeOf(typeof(BitmapInfoHeader)); info.Header.Width=targetWidth; info.Header.Height=-targetHeight; info.Header.Planes=1; info.Header.BitCount=32; info.Header.Compression=0; info.Header.SizeImage=(uint)(targetWidth*targetHeight*4); bitmap=CreateDIBSection(screen,ref info,0,out bits,IntPtr.Zero,0); if (memory == IntPtr.Zero || bitmap == IntPtr.Zero || bits == IntPtr.Zero) throw new InvalidOperationException("Could not allocate sparse DIB section."); previous=SelectObject(memory,bitmap); if (previous == IntPtr.Zero) throw new InvalidOperationException("Could not select sparse DIB section."); if (SetStretchBltMode(memory,4)==0 || !SetBrushOrgEx(memory,0,0,IntPtr.Zero)) throw new InvalidOperationException("Could not configure sparse capture scaling."); if (!StretchBlt(memory,0,0,targetWidth,targetHeight,screen,x,y,width,height,0x00CC0020)) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); if (!GdiFlush()) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); byte[] pixels=new byte[targetWidth*targetHeight*4]; Marshal.Copy(bits,pixels,0,pixels.Length); return new Capture(targetWidth,pixels); } finally { if (memory != IntPtr.Zero && previous != IntPtr.Zero) SelectObject(memory,previous); if (bitmap != IntPtr.Zero) DeleteObject(bitmap); if (memory != IntPtr.Zero) DeleteDC(memory); ReleaseDC(IntPtr.Zero,screen); } } }' }`] : []),
54
+ '$hostReady = @{ id="__newmark_ready__"; ready=$true }; [Console]::Out.WriteLine(($hostReady | ConvertTo-Json -Compress)); [Console]::Out.Flush()',
55
+ 'while (($line = [Console]::In.ReadLine()) -ne $null) {',
56
+ ' $id = ""',
57
+ ' $timer = [System.Diagnostics.Stopwatch]::StartNew()',
58
+ ' try {',
59
+ ' $request = $line | ConvertFrom-Json',
60
+ ' $id = [string]$request.id',
61
+ ' $source = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String([string]$request.script))',
62
+ ' $previousPreference = $ErrorActionPreference',
63
+ ' $ErrorActionPreference = "Stop"',
64
+ ' try { $output = (& ([scriptblock]::Create($source)) 2>&1 | Out-String -Width 1048576).Trim() } finally { $ErrorActionPreference = $previousPreference }',
65
+ ' $response = @{ id=$id; ok=$true; output=$output; elapsed_ms=[int]$timer.ElapsedMilliseconds }',
66
+ ' } catch {',
67
+ ' $response = @{ id=$id; ok=$false; output=[string]$_; elapsed_ms=[int]$timer.ElapsedMilliseconds }',
68
+ ' }',
69
+ ' [Console]::Out.WriteLine(($response | ConvertTo-Json -Compress -Depth 6))',
70
+ ' [Console]::Out.Flush()',
71
+ '}',
72
+ ].join('; ');
73
+ }
74
+ const HOST_SCRIPT = createHostScript(true);
75
+ const SPARSE_HOST_SCRIPT = createHostScript(false);
69
76
  class PowerShellWorker {
70
77
  lane;
71
78
  child = null;
79
+ readyChild = null;
72
80
  pending = new Map();
81
+ readinessWaiters = new Set();
73
82
  restarting = false;
74
83
  unavailableUntil = 0;
75
84
  constructor(lane) {
76
85
  this.lane = lane;
77
86
  }
78
87
  ready() {
79
- return Date.now() >= this.unavailableUntil && !!this.child && !this.child.killed && this.child.exitCode === null;
88
+ return Date.now() >= this.unavailableUntil
89
+ && !!this.child
90
+ && !this.child.killed
91
+ && this.child.exitCode === null
92
+ && this.readyChild === this.child;
93
+ }
94
+ async prepare(timeoutMs) {
95
+ const startedAt = Date.now();
96
+ if (Date.now() < this.unavailableUntil)
97
+ return { ready: false, elapsedMs: 0 };
98
+ const child = this.ensureChild();
99
+ if (this.readyChild === child)
100
+ return { ready: true, elapsedMs: Date.now() - startedAt };
101
+ const ready = await new Promise(resolve => {
102
+ let settled = false;
103
+ let waiter;
104
+ const finish = (value) => {
105
+ if (settled)
106
+ return;
107
+ settled = true;
108
+ clearTimeout(waiter.timer);
109
+ this.readinessWaiters.delete(waiter);
110
+ resolve(value);
111
+ };
112
+ waiter = {
113
+ child,
114
+ resolve: finish,
115
+ timer: setTimeout(() => finish(false), Math.max(0, Math.floor(timeoutMs))),
116
+ };
117
+ this.readinessWaiters.add(waiter);
118
+ if (this.readyChild === child)
119
+ finish(true);
120
+ });
121
+ return { ready, elapsedMs: Date.now() - startedAt };
80
122
  }
81
123
  async run(script, timeoutMs) {
82
124
  const startedAt = Date.now();
@@ -114,6 +156,9 @@ class PowerShellWorker {
114
156
  stop() {
115
157
  const child = this.child;
116
158
  this.child = null;
159
+ this.readyChild = null;
160
+ if (child)
161
+ this.resolveReadiness(child, false);
117
162
  if (child && !child.killed)
118
163
  child.kill();
119
164
  this.rejectPending(`Computer Use ${this.lane} helper stopped.`);
@@ -154,14 +199,14 @@ class PowerShellWorker {
154
199
  '-ExecutionPolicy',
155
200
  'Bypass',
156
201
  '-Command',
157
- HOST_SCRIPT,
202
+ this.lane === 'sparse' ? SPARSE_HOST_SCRIPT : HOST_SCRIPT,
158
203
  ], {
159
204
  windowsHide: true,
160
205
  stdio: ['pipe', 'pipe', 'pipe'],
161
206
  });
162
207
  child.stdin.setDefaultEncoding('utf8');
163
208
  const lines = readline.createInterface({ input: child.stdout });
164
- lines.on('line', line => this.handleLine(line));
209
+ lines.on('line', line => this.handleLine(line, child));
165
210
  let stderr = '';
166
211
  child.stderr.on('data', chunk => {
167
212
  stderr = `${stderr}${String(chunk)}`.slice(-8192);
@@ -170,6 +215,8 @@ class PowerShellWorker {
170
215
  if (this.child !== child)
171
216
  return;
172
217
  this.child = null;
218
+ this.readyChild = null;
219
+ this.resolveReadiness(child, false);
173
220
  this.rejectPending(error.message);
174
221
  });
175
222
  child.once('exit', (code, signal) => {
@@ -177,12 +224,14 @@ class PowerShellWorker {
177
224
  if (this.child !== child)
178
225
  return;
179
226
  this.child = null;
227
+ this.readyChild = null;
228
+ this.resolveReadiness(child, false);
180
229
  this.rejectPending(`Computer Use ${this.lane} helper exited (${code ?? signal ?? 'unknown'}).${stderr ? ` ${stderr.trim()}` : ''}`);
181
230
  });
182
231
  this.child = child;
183
232
  return child;
184
233
  }
185
- handleLine(line) {
234
+ handleLine(line, child) {
186
235
  let parsed;
187
236
  try {
188
237
  parsed = JSON.parse(line);
@@ -190,6 +239,13 @@ class PowerShellWorker {
190
239
  catch {
191
240
  return;
192
241
  }
242
+ if (String(parsed.id || '') === '__newmark_ready__' && parsed.ready === true) {
243
+ if (this.child === child) {
244
+ this.readyChild = child;
245
+ this.resolveReadiness(child, true);
246
+ }
247
+ return;
248
+ }
193
249
  const id = String(parsed.id || '');
194
250
  const pending = this.pending.get(id);
195
251
  if (!pending)
@@ -209,6 +265,12 @@ class PowerShellWorker {
209
265
  }
210
266
  this.pending.clear();
211
267
  }
268
+ resolveReadiness(child, ready) {
269
+ for (const waiter of [...this.readinessWaiters]) {
270
+ if (waiter.child === child)
271
+ waiter.resolve(ready);
272
+ }
273
+ }
212
274
  }
213
275
  const workers = {
214
276
  action: new PowerShellWorker('action'),
@@ -216,6 +278,7 @@ const workers = {
216
278
  windows: new PowerShellWorker('windows'),
217
279
  uia_advisory: new PowerShellWorker('uia_advisory'),
218
280
  windows_advisory: new PowerShellWorker('windows_advisory'),
281
+ sparse: new PowerShellWorker('sparse'),
219
282
  };
220
283
  let cleanupRegistered = false;
221
284
  function registerCleanup() {
@@ -228,6 +291,7 @@ function registerCleanup() {
228
291
  workers.windows.stop();
229
292
  workers.uia_advisory.stop();
230
293
  workers.windows_advisory.stop();
294
+ workers.sparse.stop();
231
295
  };
232
296
  process.once('exit', stop);
233
297
  }
@@ -241,11 +305,21 @@ async function runPersistentPowerShell(script, timeoutMs = 30000, lane = 'action
241
305
  function computerUsePowerShellLaneReady(lane) {
242
306
  return process.platform === 'win32' && workers[lane].ready();
243
307
  }
308
+ async function prepareComputerUsePowerShellLane(lane, timeoutMs) {
309
+ if (process.platform !== 'win32')
310
+ return { ready: false, elapsedMs: 0 };
311
+ registerCleanup();
312
+ return await workers[lane].prepare(timeoutMs);
313
+ }
314
+ function stopComputerUsePowerShellLane(lane) {
315
+ workers[lane].stop();
316
+ }
244
317
  function stopComputerUsePowerShellHost() {
245
318
  workers.action.stop();
246
319
  workers.uia.stop();
247
320
  workers.windows.stop();
248
321
  workers.uia_advisory.stop();
249
322
  workers.windows_advisory.stop();
323
+ workers.sparse.stop();
250
324
  }
251
325
  //# sourceMappingURL=computerUsePowerShellHost.js.map
@@ -16,6 +16,7 @@ export interface ToolExecutionContext {
16
16
  actorId?: string;
17
17
  workspaceId?: string;
18
18
  runtimeKey?: string;
19
+ runId?: string;
19
20
  backend?: string;
20
21
  invocation?: 'agent' | 'cli';
21
22
  signal?: AbortSignal;
@@ -42,6 +43,7 @@ export declare class ToolExecutor {
42
43
  private readonly localOcr;
43
44
  private readonly argumentValidators;
44
45
  private readonly searchMcpPool;
46
+ private readonly fileWriteObservationGate;
45
47
  private hostProfile;
46
48
  constructor(root: string, config: ConfigManager, ssh?: SshManager | undefined, workspace?: WorkspaceManager | undefined);
47
49
  webSearch(query: string, signal?: AbortSignal): Promise<string>;
@@ -72,16 +74,13 @@ export declare class ToolExecutor {
72
74
  private decorateBrowserUseReceipt;
73
75
  private sshWorkspace;
74
76
  private checkWorkspaceAccess;
77
+ private fileWriteObservationScope;
75
78
  private checkBashWorkspaceAccess;
76
79
  private extractCommandPathRefs;
77
80
  private looksLikePath;
78
81
  private isReadonlyShellCommand;
79
82
  private resolveBashTimeout;
80
83
  private bash;
81
- private fread;
82
- private fwrite;
83
- private fedit;
84
- private fdelete;
85
84
  private glob;
86
85
  private grep;
87
86
  private createProxyAgent;
@@ -64,6 +64,7 @@ const localOcr_1 = require("../core/localOcr");
64
64
  const visualTextFallback_1 = require("../core/visualTextFallback");
65
65
  const computerUseSession_1 = require("../core/computerUseSession");
66
66
  const searchMcpPool_1 = require("../core/searchMcpPool");
67
+ const fileWriteObservation_1 = require("../core/fileWriteObservation");
67
68
  function normalizeComputerUseAction(action) {
68
69
  return String(action || '').trim().toLowerCase();
69
70
  }
@@ -279,6 +280,7 @@ class ToolExecutor {
279
280
  localOcr;
280
281
  argumentValidators = new toolArgumentValidator_1.ToolArgumentValidatorRegistry();
281
282
  searchMcpPool;
283
+ fileWriteObservationGate = new fileWriteObservation_1.FileWriteObservationGate();
282
284
  hostProfile = {
283
285
  kind: 'desktop',
284
286
  platform: process.platform,
@@ -351,15 +353,15 @@ class ToolExecutor {
351
353
  const tools = [
352
354
  t('bash', `${shellDescription} Optional timeout_ms lets the Agent choose this command timeout in milliseconds; 0 requests no limit, but a nonzero terminal.interrupt_timeout_ms setting is the upper cap.`, { command: { type: 'string' }, timeout_ms: { type: 'number', description: 'Per-command timeout in milliseconds. 0 means no requested limit unless capped by settings.' } }, ['command']),
353
355
  t('pwd', 'Print working directory (current folder path)', {}, []),
354
- t('read', 'Read file contents. Use ABSOLUTE paths. The working directory is given in system prompt.', { path: { type: 'string' } }, ['path']),
355
- t('write', 'Write/create a file. Use ABSOLUTE paths.', { path: { type: 'string' }, content: { type: 'string' } }, ['path', 'content']),
356
- t('edit', 'Edit file with find-and-replace. Use ABSOLUTE paths.', { path: { type: 'string' }, old_str: { type: 'string' }, new_str: { type: 'string' } }, ['path', 'old_str', 'new_str']),
357
- t('delete_file', 'Delete ONE file under Agent supervision. Use ABSOLUTE paths. This tool refuses directory deletion and wildcard paths; delete files one by one. Never use bash rm/del/Remove-Item for batch (recursive/wildcard/loop/pipe/multi-target) deletion — the runtime hard-blocks such commands.', { path: { type: 'string' } }, ['path']),
356
+ t('read', 'Read file contents. Use ABSOLUTE paths. A complete read or explicit line range records a short-lived, conversation/workspace/run-scoped observation for write/edit/delete_file. Read an exact target path before changing it. Large files can be read with start_line/end_line; overwrite/delete requires full line coverage, while edit requires the exact old_str lines.', { path: { type: 'string' }, start_line: { type: 'integer', minimum: 1, description: 'Optional inclusive first line for a bounded read. Provide together with end_line.' }, end_line: { type: 'integer', minimum: 1, description: 'Optional inclusive last line for a bounded read. Provide together with start_line.' } }, ['path']),
357
+ t('write', 'Write/create a file only after the exact target path has been read in this conversation, workspace, actor, and Build run. Existing files require a complete read; new files require an exact-path absence observation. Changed targets are refused until read again.', { path: { type: 'string' }, content: { type: 'string' } }, ['path', 'content']),
358
+ t('edit', 'Edit a file with find-and-replace only after reading the exact target lines in this conversation, workspace, actor, and Build run. The old_str must have appeared in a successful read range; stale or ambiguous matches are refused.', { path: { type: 'string' }, old_str: { type: 'string' }, new_str: { type: 'string' } }, ['path', 'old_str', 'new_str']),
359
+ t('delete_file', 'Delete ONE file only after reading its complete contents at the exact target path in this conversation, workspace, actor, and Build run. This tool refuses directory deletion and wildcard paths; delete files one by one. Never use bash rm/del/Remove-Item for batch (recursive/wildcard/loop/pipe/multi-target) deletion — the runtime hard-blocks such commands.', { path: { type: 'string' } }, ['path']),
358
360
  t('glob', 'Find files by glob pattern (e.g. **/*.ts, src/**/*.html)', { pattern: { type: 'string' } }, ['pattern']),
359
361
  t('grep', 'Search file content with regex', { pattern: { type: 'string' }, path: { type: 'string' } }, ['pattern', 'path']),
360
362
  t('web_search', 'Search the web', { query: { type: 'string' } }, ['query']),
361
363
  t('web_fetch', 'Fetch and extract URL content', { url: { type: 'string' } }, ['url']),
362
- t('web_catch', 'Download a public web resource, save a webpage, or capture one page component into an exact workspace destination. Advanced Build/Goal tool; unavailable in Plan and Chat. This writes only the requested local output and never changes the remote page.', {
364
+ t('web_catch', 'Download a public web resource, save a webpage, or capture one page component into an exact workspace destination. Read the exact destination first; existing files require a complete read and new files require an exact-path absence observation. Advanced Build/Goal tool; unavailable in Plan and Chat. This writes only the requested local output and never changes the remote page.', {
363
365
  action: { type: 'string', enum: ['download', 'save_page', 'capture_component'], description: 'download saves response bytes; save_page stores the HTML response; capture_component selects one CSS element and saves its src/href asset, or its outer HTML when no asset attribute exists.' },
364
366
  url: { type: 'string', description: 'Public http:// or https:// URL.' },
365
367
  destination: { type: 'string', description: 'Exact output file path. Relative paths resolve inside the active workspace.' },
@@ -405,22 +407,25 @@ class ToolExecutor {
405
407
  capture_max_height: { type: 'number', minimum: 240, maximum: 2048, description: 'Maximum ephemeral screenshot height; defaults to 960. Aspect ratio is preserved and the source is never enlarged.' },
406
408
  max_chars: { type: 'number', minimum: 1000, maximum: 100000, description: 'Maximum returned semantic UI text characters.' },
407
409
  }, []),
408
- t('computer_use', 'Native Windows desktop control with persistent observation/action helpers. Use observe/app_observe before acting. sequence may perform up to three stable low-risk move/click/scroll/wait/app_activate steps and stops when focus, window, menu, dialog, scene, or risk changes. Vision screenshots remain one-time inputs and are deleted immediately.', {
409
- action: { type: 'string', enum: ['observe', 'app_list', 'app_observe', 'sequence', 'takeover_start', 'takeover_stop', 'move', 'click', 'scroll', 'type', 'key', 'wait', 'app_activate', 'app_click', 'app_scroll', 'app_type', 'app_key'] },
410
+ t('computer_use', 'Native Windows desktop control with persistent observation/action helpers. Use observe/app_observe before acting. Set include_ui_tree=false for a fast screenshot-only observation; the default includes a bounded UI Automation tree for semantic targets. wait_for polls one visible app window for text, a target_id from the latest observation, or window availability. A match refreshes the semantic target cache; inspect the returned element before acting. For passive observe, sparse_wait_ms can wait up to 5 seconds on a coarse local pixel-change signal without a local perception model; after a signal or timeout it captures a fresh screenshot and the requested UI Automation data. This low-resolution signal can miss small or brief changes and does not prove that no change occurred. sequence may perform up to three stable low-risk move/click/scroll/wait/app_activate steps and stops when focus, window, menu, dialog, scene, or risk changes. Vision screenshots remain one-time inputs and are deleted immediately.', {
411
+ action: { type: 'string', enum: ['observe', 'app_list', 'app_observe', 'wait_for', 'sequence', 'takeover_start', 'takeover_stop', 'move', 'click', 'scroll', 'type', 'key', 'wait', 'app_activate', 'app_click', 'app_scroll', 'app_type', 'app_key'] },
410
412
  capture_max_width: { type: 'number', minimum: 320, maximum: 2048, description: 'For observe/app_observe only. Maximum ephemeral screenshot width; defaults to 1280. Aspect ratio is preserved and the source is never enlarged.' },
411
413
  capture_max_height: { type: 'number', minimum: 240, maximum: 2048, description: 'For observe/app_observe only. Maximum ephemeral screenshot height; defaults to 960. Aspect ratio is preserved and the source is never enlarged.' },
414
+ include_ui_tree: { type: 'boolean', description: 'For observe/app_observe only. Defaults to true. Set false to skip UI Automation traversal and return a screenshot-only observation; prior semantic target IDs are invalidated.' },
415
+ sparse_wait_ms: { type: 'integer', minimum: 0, maximum: 5000, description: 'Optional for action=observe only; defaults to 0 (immediate capture). Wait this many milliseconds for a coarse 32×18 grayscale change signal with no local neural model, then capture a fresh screenshot and the UI Automation tree when include_ui_tree=true, whether signaled or timed out. Small/transient changes can be missed.' },
412
416
  x: { type: 'number' },
413
417
  y: { type: 'number' },
414
- target_id: { type: 'string', description: 'Stable id from the latest observe perception.objects entry. Preferred over raw coordinates when available.' },
418
+ target_id: { type: 'string', description: 'Stable id from the latest observe perception.objects entry. Preferred over raw coordinates when available; for wait_for it must come from the latest semantic observation.' },
415
419
  app_target: { type: 'string', description: 'Application title, process name, or process id for app-scoped observation and actions.' },
416
420
  window_handle: { type: 'string', description: 'Native window handle from app_list/app_observe for exact app-scoped actions.' },
417
421
  max_chars: { type: 'number', minimum: 1000, maximum: 100000, description: 'Maximum returned text characters for observation and application listings.' },
418
422
  scroll_x: { type: 'number', description: 'Horizontal scroll delta for action=scroll.' },
419
423
  scroll_y: { type: 'number', description: 'Vertical scroll delta for action=scroll. Positive scrolls down.' },
420
424
  button: { type: 'string', enum: ['left', 'right'] },
421
- text: { type: 'string' },
425
+ text: { type: 'string', description: 'Text to type, or for wait_for a case-insensitive substring of a visible UI Automation Name or AutomationId.' },
422
426
  key: { type: 'string' },
423
427
  duration_ms: { type: 'number' },
428
+ timeout_ms: { type: 'integer', minimum: 250, maximum: 30000, description: 'For wait_for only. Polling budget in milliseconds; defaults to 5000 and is capped at 30000. A UI Automation call already in flight can add bounded completion latency.' },
424
429
  dry_run: { type: 'boolean' },
425
430
  include_raw_ui: { type: 'boolean', description: 'Debug only. Include the raw UI Automation element list; normal observation returns compact semantic objects.' },
426
431
  steps: {
@@ -650,14 +655,18 @@ class ToolExecutor {
650
655
  if (tool.function?.name !== 'computer_use')
651
656
  return tool;
652
657
  const copy = JSON.parse(JSON.stringify(tool));
653
- copy.function.description = 'Plan read-only desktop: observe, app_list, app_observe only.';
658
+ copy.function.description = 'Plan read-only desktop: observe, app_list, app_observe, wait_for only.';
654
659
  copy.function.parameters.properties = {
655
660
  action: { type: 'string', enum: [...toolPolicy_1.PLAN_COMPUTER_USE_ACTIONS] },
656
661
  app_target: copy.function.parameters.properties.app_target,
657
662
  window_handle: copy.function.parameters.properties.window_handle,
663
+ target_id: copy.function.parameters.properties.target_id,
664
+ text: copy.function.parameters.properties.text,
658
665
  max_chars: copy.function.parameters.properties.max_chars,
659
666
  capture_max_width: copy.function.parameters.properties.capture_max_width,
660
667
  capture_max_height: copy.function.parameters.properties.capture_max_height,
668
+ include_ui_tree: copy.function.parameters.properties.include_ui_tree,
669
+ timeout_ms: copy.function.parameters.properties.timeout_ms,
661
670
  include_raw_ui: copy.function.parameters.properties.include_raw_ui,
662
671
  };
663
672
  return copy;
@@ -787,15 +796,99 @@ class ToolExecutor {
787
796
  switch (tool) {
788
797
  case 'bash': return await this.bash(g('command'), wsPath, args.timeout_ms, context.signal);
789
798
  case 'pwd': return `Current directory: ${wsPath}`;
790
- case 'read': return this.fread(resolve(g('path')));
791
- case 'write': return this.fwrite(resolve(g('path')), g('content'));
792
- case 'edit': return this.fedit(resolve(g('path')), g('old_str'), g('new_str'));
793
- case 'delete_file': return this.fdelete(resolve(g('path')));
799
+ case 'read':
800
+ return await this.fileWriteObservationGate.read(resolve(g('path')), this.fileWriteObservationScope(context, wsPath), {
801
+ startLine: args.start_line === undefined ? undefined : Number(args.start_line),
802
+ endLine: args.end_line === undefined ? undefined : Number(args.end_line),
803
+ });
804
+ case 'write': {
805
+ const target = resolve(g('path'));
806
+ const mutation = await this.fileWriteObservationGate.mutate(target, this.fileWriteObservationScope(context, wsPath), { kind: 'full' }, gate => {
807
+ const denied = this.checkWorkspaceAccess('write', gate.canonicalPath, context.workspacePath || wsPath);
808
+ if (denied)
809
+ return { value: denied, committed: false };
810
+ if (gate.exists) {
811
+ gate.assertBeforeCommit();
812
+ fs.writeFileSync(target, g('content'), 'utf-8');
813
+ }
814
+ else {
815
+ fs.mkdirSync(path.dirname(target), { recursive: true });
816
+ gate.assertBeforeCommit();
817
+ fs.writeFileSync(target, g('content'), { encoding: 'utf-8', flag: 'wx' });
818
+ }
819
+ return { value: '[write] OK: ' + target, committed: true };
820
+ });
821
+ return mutation.ok ? mutation.value : mutation.error;
822
+ }
823
+ case 'edit': {
824
+ const target = resolve(g('path'));
825
+ const oldText = g('old_str');
826
+ const replacement = g('new_str');
827
+ const mutation = await this.fileWriteObservationGate.mutate(target, this.fileWriteObservationScope(context, wsPath), { kind: 'edit', oldText }, gate => {
828
+ const denied = this.checkWorkspaceAccess('edit', gate.canonicalPath, context.workspacePath || wsPath);
829
+ if (denied)
830
+ return { value: denied, committed: false };
831
+ if (!gate.exists || gate.text === undefined)
832
+ return { value: '[edit] Target file does not exist: ' + target, committed: false };
833
+ const first = gate.text.indexOf(oldText);
834
+ if (first < 0)
835
+ return { value: '[edit] String not found in ' + target + '.', committed: false };
836
+ if (gate.text.indexOf(oldText, first + oldText.length) >= 0) {
837
+ return { value: '[edit] Refused: old_str matches more than one position in ' + target + '; read a more specific context and retry.', committed: false };
838
+ }
839
+ gate.assertBeforeCommit();
840
+ const updated = gate.text.slice(0, first) + replacement + gate.text.slice(first + oldText.length);
841
+ fs.writeFileSync(target, updated, 'utf-8');
842
+ return { value: '[edit] OK: ' + target, committed: true };
843
+ });
844
+ return mutation.ok ? mutation.value : mutation.error;
845
+ }
846
+ case 'delete_file': {
847
+ const target = resolve(g('path'));
848
+ if (/[*?]/.test(target))
849
+ return '[delete_file] Refused: wildcard paths are not allowed. Delete one file per call.';
850
+ try {
851
+ if (fs.lstatSync(target).isDirectory()) {
852
+ return '[delete_file] Refused: deleting a directory is not allowed. Delete files one by one under Agent supervision.';
853
+ }
854
+ }
855
+ catch (error) {
856
+ if (error?.code !== 'ENOENT') {
857
+ return '[delete_file] ' + (error instanceof Error ? error.message : String(error));
858
+ }
859
+ }
860
+ const mutation = await this.fileWriteObservationGate.mutate(target, this.fileWriteObservationScope(context, wsPath), { kind: 'full' }, gate => {
861
+ const denied = this.checkWorkspaceAccess('delete_file', gate.canonicalPath, context.workspacePath || wsPath);
862
+ if (denied)
863
+ return { value: denied, committed: false };
864
+ if (!gate.exists)
865
+ return { value: '[delete_file] Target does not exist: ' + target, committed: false };
866
+ gate.assertBeforeCommit();
867
+ fs.unlinkSync(target);
868
+ return { value: '[delete_file] OK: ' + target, committed: true };
869
+ });
870
+ return mutation.ok ? mutation.value : mutation.error;
871
+ }
794
872
  case 'glob': return this.glob(g('pattern'), wsPath);
795
873
  case 'grep': return this.grep(g('pattern'), resolve(g('path')));
796
874
  case 'web_search': return await this.wsearch(g('query'), context.signal);
797
875
  case 'web_fetch': return await this.wfetch(g('url'), context.signal);
798
- case 'web_catch': return await this.wcatch(args, resolve(g('destination')), context.signal);
876
+ case 'web_catch': {
877
+ const destination = resolve(g('destination'));
878
+ const mutation = await this.fileWriteObservationGate.mutate(destination, this.fileWriteObservationScope(context, wsPath), { kind: 'full' }, async (gate) => {
879
+ const denied = this.checkWorkspaceAccess('web_catch', gate.canonicalPath, context.workspacePath || wsPath);
880
+ if (denied)
881
+ return { value: denied, committed: false };
882
+ const value = await this.wcatch(args, destination, context.signal, () => gate.assertBeforeCommit());
883
+ let committed = false;
884
+ try {
885
+ committed = JSON.parse(value).ok === true;
886
+ }
887
+ catch { }
888
+ return { value, committed };
889
+ });
890
+ return mutation.ok ? mutation.value : mutation.error;
891
+ }
799
892
  case 'browser_open': return await this.browserRun({ action: 'open', url: g('url') }, context.signal, context, wsPath);
800
893
  case 'browser_snapshot': return await this.browserRun({ action: 'snapshot', maxChars: Number(args.max_chars || 12000) }, context.signal, context, wsPath);
801
894
  case 'browser_click': return await this.browserRun({ action: 'click', selector: g('selector') }, context.signal, context, wsPath);
@@ -999,6 +1092,14 @@ class ToolExecutor {
999
1092
  }
1000
1093
  case 'computer_use': {
1001
1094
  const action = normalizeComputerUseAction(g('action'));
1095
+ if (process.env.NEWMARK_TEST_CU_OBSERVE_ONLY === '1'
1096
+ && !['observe', 'app_observe', 'app_list', 'wait_for'].includes(action)) {
1097
+ return JSON.stringify({
1098
+ ok: false,
1099
+ action,
1100
+ error: '[computer_use test guard] The live-model smoke only permits read-only desktop observation.',
1101
+ });
1102
+ }
1002
1103
  const owner = `${computerUseOwner(context, wsPath)}:${String(context.actorId || 'root')}`;
1003
1104
  const lockGuard = action === 'takeover_stop'
1004
1105
  ? assertComputerUseLockOwner(action, owner, context, wsPath)
@@ -1063,6 +1164,10 @@ class ToolExecutor {
1063
1164
  allowEphemeralVisionImage: context.allowEphemeralVisionImage === true,
1064
1165
  captureMaxWidth: Number(args.capture_max_width),
1065
1166
  captureMaxHeight: Number(args.capture_max_height),
1167
+ sparseWaitMs: Number(args.sparse_wait_ms),
1168
+ includeUiTree: args.include_ui_tree !== false,
1169
+ timeoutMs: Number(args.timeout_ms),
1170
+ signal: context.signal,
1066
1171
  gradientColors: Array.isArray(args.gradient_colors) ? args.gradient_colors : undefined,
1067
1172
  gradientSpeed: args.gradient_speed !== undefined ? Number(args.gradient_speed) : undefined,
1068
1173
  gradientWidth: args.gradient_width !== undefined ? Number(args.gradient_width) : undefined,
@@ -1299,6 +1404,18 @@ class ToolExecutor {
1299
1404
  const action = policy === 'ask_user' ? 'User approval required' : 'Denied';
1300
1405
  return `[permission] ${action}: ${tool} cannot access outside workspace (${target}).`;
1301
1406
  }
1407
+ fileWriteObservationScope(context, wsPath) {
1408
+ const workspacePath = path.resolve(context.workspacePath || wsPath || this.root);
1409
+ return JSON.stringify({
1410
+ runtimeKey: String(context.runtimeKey || process.env.NEWMARK_RUNTIME_KEY || ''),
1411
+ runId: String(context.runId || ''),
1412
+ workspaceId: String(context.workspaceId || workspacePath),
1413
+ workspacePath,
1414
+ conversationId: String(context.conversationId || 'default'),
1415
+ actorId: String(context.actorId || terminalTakeover_1.ROOT_TERMINAL_ACTOR_ID),
1416
+ backend: String(context.backend || (process.env.NEWMARK_WSL_DISTRO ? 'wsl' : process.platform)),
1417
+ });
1418
+ }
1302
1419
  checkBashWorkspaceAccess(command, wsPath) {
1303
1420
  const perm = this.config.getStr('workspace', 'access_permission');
1304
1421
  if (perm === 'full_access')
@@ -1386,54 +1503,6 @@ class ToolExecutor {
1386
1503
  return `[bash] ${e instanceof Error ? e.message : String(e)}`;
1387
1504
  }
1388
1505
  }
1389
- fread(p) {
1390
- try {
1391
- const c = fs.readFileSync(p, 'utf-8');
1392
- return c.length > 30000 ? c.slice(0, 30000) + '...\n[truncated]' : c;
1393
- }
1394
- catch (e) {
1395
- return `[read] ${e}`;
1396
- }
1397
- }
1398
- fwrite(p, content) {
1399
- try {
1400
- fs.mkdirSync(path.dirname(p), { recursive: true });
1401
- fs.writeFileSync(p, content, 'utf-8');
1402
- return `[write] OK: ${p}`;
1403
- }
1404
- catch (e) {
1405
- return `[write] ${e}`;
1406
- }
1407
- }
1408
- fedit(p, oldStr, newStr) {
1409
- try {
1410
- const c = fs.readFileSync(p, 'utf-8');
1411
- if (!c.includes(oldStr))
1412
- return `[edit] String not found in ${p}.`;
1413
- const updated = c.replace(oldStr, newStr);
1414
- fs.writeFileSync(p, updated, 'utf-8');
1415
- return `[edit] OK: ${p}`;
1416
- }
1417
- catch (e) {
1418
- return `[edit] ${e}`;
1419
- }
1420
- }
1421
- fdelete(p) {
1422
- try {
1423
- if (/[*?]/.test(p))
1424
- return '[delete_file] Refused: wildcard paths are not allowed. Delete one file per call.';
1425
- const resolved = path.resolve(p);
1426
- const stat = fs.lstatSync(resolved);
1427
- if (stat.isDirectory()) {
1428
- return '[delete_file] Refused: deleting a directory is not allowed. Delete files one by one under Agent supervision.';
1429
- }
1430
- fs.unlinkSync(resolved);
1431
- return `[delete_file] OK: ${resolved}`;
1432
- }
1433
- catch (e) {
1434
- return `[delete_file] ${e instanceof Error ? e.message : String(e)}`;
1435
- }
1436
- }
1437
1506
  glob(pattern, ws) {
1438
1507
  try {
1439
1508
  const results = globSync(pattern, {
@@ -1885,7 +1954,7 @@ class ToolExecutor {
1885
1954
  guard.dispose();
1886
1955
  }
1887
1956
  }
1888
- async wcatch(args, destination, signal) {
1957
+ async wcatch(args, destination, signal, beforeCommit) {
1889
1958
  const action = String(args.action || 'download');
1890
1959
  const maxBytes = Math.max(1, Math.min(100 * 1024 * 1024, Number(args.max_bytes || 50 * 1024 * 1024)));
1891
1960
  const sourceUrl = this.checkedPublicWebUrl(String(args.url || ''));
@@ -1929,9 +1998,20 @@ class ToolExecutor {
1929
1998
  const backup = destinationExists ? `${destination}.newmark-${crypto.randomUUID()}.bak` : '';
1930
1999
  try {
1931
2000
  fs.writeFileSync(temp, bytes, { flag: 'wx' });
1932
- if (destinationExists)
2001
+ beforeCommit?.();
2002
+ const currentDestinationExists = fs.existsSync(destination);
2003
+ if (currentDestinationExists !== destinationExists)
2004
+ throw new Error('Destination changed during download; read it again and retry.');
2005
+ if (currentDestinationExists && !fs.statSync(destination).isFile())
2006
+ throw new Error('Destination changed to a non-file during download.');
2007
+ if (destinationExists) {
1933
2008
  fs.renameSync(destination, backup);
1934
- fs.renameSync(temp, destination);
2009
+ fs.renameSync(temp, destination);
2010
+ }
2011
+ else {
2012
+ fs.linkSync(temp, destination);
2013
+ fs.unlinkSync(temp);
2014
+ }
1935
2015
  if (backup)
1936
2016
  fs.unlinkSync(backup);
1937
2017
  }