@gakim-digital/dexter-bridge 0.11.16 → 0.11.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gakim-digital/dexter-bridge",
3
- "version": "0.11.16",
3
+ "version": "0.11.17",
4
4
  "description": "Local bridge for InstaWebAI and Dexter — runs Codex, Claude Code, or OpenCode on your machine.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/agent.js CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import crossSpawn from 'cross-spawn';
7
7
  import {
8
8
  getFramerProjectAuthorization,
9
+ readFramerWebsite,
9
10
  isCompanionPairingInvalidError,
10
11
  postRunEvent,
11
12
  reportFramerProjectAuthorizationVerification,
@@ -383,6 +384,18 @@ function argsWithStructuredOutput(args, definition, env = process.env) {
383
384
  return args;
384
385
  }
385
386
 
387
+ function framerWebHosts(framerHarness) {
388
+ const hosts = framerHarness?.context?.webAccess?.hosts;
389
+ if (!Array.isArray(hosts)) return [];
390
+ return [...new Set(
391
+ hosts
392
+ .map((value) => String(value || '').trim().toLowerCase())
393
+ .filter((value) =>
394
+ value.length <= 253
395
+ && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/.test(value)),
396
+ )].slice(0, 20);
397
+ }
398
+
386
399
  function argsWithClaudeIsolation(args, definition, options = {}) {
387
400
  if (definition.id !== 'claude-code') return args;
388
401
  const isolated = argsWithoutVariadicFlag(args, '--tools');
@@ -405,8 +418,17 @@ function argsWithClaudeIsolation(args, definition, options = {}) {
405
418
  const harnessTools = harnessToolNames.map(
406
419
  (name) => `mcp__instawebai__${name}`,
407
420
  );
421
+ const webHosts = options.framerHarness
422
+ ? framerWebHosts(options.framerHarness)
423
+ : [];
408
424
  const nativeTools = options.framerHarness
409
- ? [...(options.nativeSkillsEnabled ? ['Skill'] : []), ...harnessTools]
425
+ ? [
426
+ ...(options.nativeSkillsEnabled ? ['Skill'] : []),
427
+ // Web access is limited to the sites the user named. WebSearch cannot
428
+ // be limited by domain, so only WebFetch is offered.
429
+ ...(webHosts.length ? ['WebFetch'] : []),
430
+ ...harnessTools,
431
+ ]
410
432
  : [
411
433
  'Read',
412
434
  'Write',
@@ -435,7 +457,16 @@ function argsWithClaudeIsolation(args, definition, options = {}) {
435
457
  '--permission-mode',
436
458
  options.harnessMode && !options.framerHarness ? 'acceptEdits' : 'dontAsk',
437
459
  );
438
- if (options.harnessMode) permissionMode.push('--allowedTools', nativeTools.join(','));
460
+ if (options.harnessMode) {
461
+ const allowedTools = options.framerHarness
462
+ ? [
463
+ ...(options.nativeSkillsEnabled ? ['Skill'] : []),
464
+ ...webHosts.map((host) => `WebFetch(domain:${host})`),
465
+ ...harnessTools,
466
+ ]
467
+ : nativeTools;
468
+ permissionMode.push('--allowedTools', ...allowedTools);
469
+ }
439
470
  if (!argsIncludeFlag(permissionMode, '--strict-mcp-config')) permissionMode.push('--strict-mcp-config');
440
471
  if (!argsIncludeFlag(permissionMode, '--no-chrome')) permissionMode.push('--no-chrome');
441
472
  let configured = permissionMode;
@@ -2293,6 +2324,51 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2293
2324
  let rejectCancellation;
2294
2325
  let latestTokenUsage = normalizeCompanionTokenUsage();
2295
2326
  let activityChain = Promise.resolve();
2327
+ const webCalls = new Map();
2328
+ const recordWebCall = (webCall) => {
2329
+ if (!webCall || typeof webCall !== 'object') return;
2330
+ const id = String(webCall.id || '').trim();
2331
+ const tool =
2332
+ webCall.tool === 'WebFetch'
2333
+ ? 'WebFetch'
2334
+ : webCall.tool === 'WebSearch'
2335
+ ? 'WebSearch'
2336
+ : '';
2337
+ if (!id || !tool) return;
2338
+ const current = webCalls.get(id) || {
2339
+ tool,
2340
+ site: null,
2341
+ status: 'started',
2342
+ };
2343
+ webCalls.set(id, {
2344
+ ...current,
2345
+ tool,
2346
+ site:
2347
+ typeof webCall.site === 'string' && webCall.site.trim()
2348
+ ? webCall.site.trim().toLowerCase().slice(0, 253)
2349
+ : current.site,
2350
+ status:
2351
+ webCall.status === 'succeeded' || webCall.status === 'failed'
2352
+ ? webCall.status
2353
+ : current.status,
2354
+ errorMessage:
2355
+ webCall.status === 'failed' && typeof webCall.errorMessage === 'string'
2356
+ ? clip(webCall.errorMessage, 300)
2357
+ : current.errorMessage,
2358
+ });
2359
+ if (webCalls.size > 100) {
2360
+ webCalls.delete(webCalls.keys().next().value);
2361
+ }
2362
+ };
2363
+ const webCallSummary = () =>
2364
+ [...webCalls.values()].map((call) => ({
2365
+ tool: call.tool,
2366
+ site: call.site,
2367
+ status: call.status === 'succeeded' ? 'succeeded' : 'failed',
2368
+ ...(call.status === 'failed'
2369
+ ? { errorMessage: call.errorMessage || 'The web call did not complete.' }
2370
+ : {}),
2371
+ }));
2296
2372
  const cancellation = new Promise((_, reject) => {
2297
2373
  rejectCancellation = reject;
2298
2374
  });
@@ -2345,6 +2421,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2345
2421
  return activityChain;
2346
2422
  };
2347
2423
  const progress = (event = {}) => {
2424
+ recordWebCall(event.webCall);
2348
2425
  if (event.tokenUsage) {
2349
2426
  latestTokenUsage = normalizeCompanionTokenUsage(event.tokenUsage);
2350
2427
  }
@@ -2536,6 +2613,18 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2536
2613
  allowInsecureHttp: options.allowInsecureHttp,
2537
2614
  })
2538
2615
  : undefined,
2616
+ readWebsite:
2617
+ options.apiBaseUrl && options.deviceToken
2618
+ ? ({ url, screenshotTop }) =>
2619
+ readFramerWebsite(options.apiBaseUrl, {
2620
+ deviceToken: options.deviceToken,
2621
+ runId: run.runId,
2622
+ url,
2623
+ screenshotTop,
2624
+ fetchImpl: options.fetchImpl,
2625
+ allowInsecureHttp: options.allowInsecureHttp,
2626
+ })
2627
+ : undefined,
2539
2628
  verifyProjectAuthorization:
2540
2629
  options.apiBaseUrl && options.deviceToken
2541
2630
  ? ({
@@ -2667,7 +2756,8 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2667
2756
  maxDurationMs: outcomeMaxDurationMs,
2668
2757
  cwd: materialized.directory,
2669
2758
  nativeSkills,
2670
- framerHarness: framerAgentOutcome,
2759
+ framerHarness: framerAgentOutcome ? executionOutcome : false,
2760
+ webHosts: framerAgentOutcome ? framerWebHosts(executionOutcome) : [],
2671
2761
  imageFiles,
2672
2762
  harnessTools: {
2673
2763
  definitions: codexDynamicToolSpecs(harnessTools.definitions),
@@ -2708,7 +2798,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2708
2798
  maxDurationMs: outcomeMaxDurationMs,
2709
2799
  cwd: materialized.directory,
2710
2800
  harnessMode: true,
2711
- framerHarness: framerAgentOutcome,
2801
+ framerHarness: framerAgentOutcome ? executionOutcome : false,
2712
2802
  harnessToolNames: harnessTools.toolNames,
2713
2803
  nativeSkillsEnabled: Boolean(materializedNativeSkills),
2714
2804
  imageFiles,
@@ -2731,7 +2821,7 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2731
2821
  maxDurationMs: outcomeMaxDurationMs,
2732
2822
  cwd: materialized.directory,
2733
2823
  harnessMode: true,
2734
- framerHarness: framerAgentOutcome,
2824
+ framerHarness: framerAgentOutcome ? executionOutcome : false,
2735
2825
  harnessToolNames: harnessTools.toolNames,
2736
2826
  nativeSkillsEnabled: Boolean(materializedNativeSkills),
2737
2827
  imageFiles,
@@ -2871,7 +2961,12 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2871
2961
  sourceHash:
2872
2962
  synchronization?.sourceHash || harnessTools.sourceHash() || null,
2873
2963
  ...(typeof harnessTools.summary === 'function'
2874
- ? { framer: harnessTools.summary() }
2964
+ ? {
2965
+ framer: {
2966
+ ...harnessTools.summary(),
2967
+ webCalls: webCallSummary(),
2968
+ },
2969
+ }
2875
2970
  : {}),
2876
2971
  }
2877
2972
  : completion;
@@ -2914,7 +3009,10 @@ async function executeOutcomeRun(run, send, agent, options = {}) {
2914
3009
  usageAccumulator.add(failure.companionUsage || {});
2915
3010
  const framer =
2916
3011
  typeof harnessTools.summary === 'function'
2917
- ? harnessTools.summary()
3012
+ ? {
3013
+ ...harnessTools.summary(),
3014
+ webCalls: webCallSummary(),
3015
+ }
2918
3016
  : undefined;
2919
3017
  failure.framer = framer;
2920
3018
  failure.companionUsage = {
@@ -320,6 +320,30 @@ function claudeToolProgress(name, input = {}, cwd) {
320
320
  };
321
321
  }
322
322
 
323
+ function webSite(value) {
324
+ if (typeof value !== 'string' || !value.trim()) return null;
325
+ for (const candidate of value.match(/https?:\/\/[^\s"'<>]+/gi) || []) {
326
+ try {
327
+ return new URL(candidate).hostname.toLowerCase();
328
+ } catch {
329
+ // Keep looking for a valid URL.
330
+ }
331
+ }
332
+ return null;
333
+ }
334
+
335
+ function claudeToolResultMessage(part) {
336
+ if (typeof part?.content === 'string') return progressText(part.content, 300);
337
+ if (!Array.isArray(part?.content)) return '';
338
+ return progressText(
339
+ part.content
340
+ .map((item) => typeof item?.text === 'string' ? item.text : '')
341
+ .filter(Boolean)
342
+ .join(' '),
343
+ 300,
344
+ );
345
+ }
346
+
323
347
  function summedClaudeProgressUsage(turnUsage) {
324
348
  const total = normalizeCompanionTokenUsage();
325
349
  for (const usage of turnUsage.values()) {
@@ -403,7 +427,21 @@ export function createClaudeProgressParser(onProgress, options = {}) {
403
427
  continue;
404
428
  }
405
429
  const progress = claudeToolProgress(part.name, input, options.cwd);
406
- tools.set(part.id, { name: part.name, ...progress });
430
+ const nativeWebTool =
431
+ part.name === 'WebFetch'
432
+ ? 'WebFetch'
433
+ : part.name === 'WebSearch'
434
+ ? 'WebSearch'
435
+ : null;
436
+ const webCall = nativeWebTool
437
+ ? {
438
+ id: part.id,
439
+ tool: nativeWebTool,
440
+ site: webSite(input.url || input.query),
441
+ status: 'started',
442
+ }
443
+ : null;
444
+ tools.set(part.id, { name: part.name, ...progress, webCall });
407
445
  emit({
408
446
  kind: 'item_started',
409
447
  name: part.name,
@@ -411,6 +449,7 @@ export function createClaudeProgressParser(onProgress, options = {}) {
411
449
  activity: `tool:${part.id}`,
412
450
  paths: progress.paths,
413
451
  message: progress.startedMessage,
452
+ ...(webCall ? { webCall } : {}),
414
453
  });
415
454
  }
416
455
  };
@@ -430,6 +469,17 @@ export function createClaudeProgressParser(onProgress, options = {}) {
430
469
  activity: `tool:${part.tool_use_id}`,
431
470
  paths: tool.paths,
432
471
  message: failed ? tool.failedMessage : tool.completedMessage,
472
+ ...(tool.webCall
473
+ ? {
474
+ webCall: {
475
+ ...tool.webCall,
476
+ status: failed ? 'failed' : 'succeeded',
477
+ ...(failed
478
+ ? { errorMessage: claudeToolResultMessage(part) || tool.failedMessage }
479
+ : {}),
480
+ },
481
+ }
482
+ : {}),
433
483
  });
434
484
  }
435
485
  };
package/src/api.js CHANGED
@@ -235,6 +235,37 @@ export function getFramerProjectAuthorization(apiBaseUrl, {
235
235
  );
236
236
  }
237
237
 
238
+ // Reads one page of a website the user named, through Dexter. A page render
239
+ // can take most of a minute, so this waits longer than other calls.
240
+ export function readFramerWebsite(apiBaseUrl, {
241
+ deviceToken,
242
+ runId,
243
+ url,
244
+ screenshotTop,
245
+ fetchImpl,
246
+ signal,
247
+ allowInsecureHttp,
248
+ }) {
249
+ return requestJson(
250
+ apiBaseUrl,
251
+ `/ai/framer/companion/runs/${encodeURIComponent(runId)}/read-website`,
252
+ {
253
+ method: 'POST',
254
+ deviceToken,
255
+ fetchImpl,
256
+ signal,
257
+ allowInsecureHttp,
258
+ timeoutMs: 150_000,
259
+ body: {
260
+ url,
261
+ ...(Number.isInteger(screenshotTop) ? { screenshotTop } : {}),
262
+ platform: process.platform,
263
+ companionVersion: BRIDGE_VERSION,
264
+ },
265
+ },
266
+ );
267
+ }
268
+
238
269
  export function reportFramerProjectAuthorizationVerification(apiBaseUrl, {
239
270
  deviceToken,
240
271
  runId,
package/src/config.js CHANGED
@@ -44,6 +44,7 @@ export const BRIDGE_CAPABILITIES = [
44
44
  'framer-project-bootstrap-v1',
45
45
  'framer-project-verification-v1',
46
46
  'framer-interaction-verification-v4',
47
+ 'framer-read-website-v1',
47
48
  'project-shell-v1',
48
49
  'build-fingerprint-v1',
49
50
  'tool-schema-parity-v1',
@@ -49,7 +49,7 @@ const PAGE_PATH_PATTERN = /^\/[^\0\r\n]{0,499}$/;
49
49
  const UNSAFE_EXEC_PATTERN =
50
50
  /\b(?:process|globalThis|global|require|import\s*\(|child_process|worker_threads|fs|os|net|tls|dgram|http|https|fetch|WebSocket|eval|Function)\b/;
51
51
  const MUTATING_EXEC_PATTERN =
52
- /\.(?:set|add|create|remove|delete|update|insert|move|clone|duplicate|upload|publish|deploy|switch|merge|rename|join|leave|apply)[A-Za-z0-9_]*\s*\(/;
52
+ /\.(?:(?:set|add|delete|join|apply)(?:[A-Z_][A-Za-z0-9_]*)|(?:create|remove|update|insert|move|clone|duplicate|upload|publish|deploy|switch|merge|rename|leave)[A-Za-z0-9_]*)\s*\(/;
53
53
  const CODE_FILE_SOURCE_WRITE_PATTERN =
54
54
  /(?:\.setFileContent|\[\s*['"]setFileContent['"]\s*\])\s*\(/;
55
55
  const TASK_DOMAINS = [
@@ -148,6 +148,24 @@ export const FRAMER_AGENT_TOOL_DEFINITIONS = [
148
148
  additionalProperties: false,
149
149
  },
150
150
  },
151
+ {
152
+ name: 'framer_read_website',
153
+ description:
154
+ 'Read one page of a website the user named: exact copy per section, fonts and text styles, colours, image URLs (including CSS backgrounds), and desktop and phone screenshots. Use it before recreating or migrating that page. Only sites the user named can be read.',
155
+ inputSchema: {
156
+ type: 'object',
157
+ properties: {
158
+ url: { type: 'string', minLength: 1, maxLength: 2048 },
159
+ screenshotTop: {
160
+ type: 'integer',
161
+ minimum: 0,
162
+ description: 'Desktop page offset in pixels for the screenshots. Use the value the previous result suggests to see further down.',
163
+ },
164
+ },
165
+ required: ['url'],
166
+ additionalProperties: false,
167
+ },
168
+ },
151
169
  {
152
170
  name: 'framer_plan_task',
153
171
  description:
@@ -1129,6 +1147,7 @@ export function createFramerAgentToolRuntime({
1129
1147
  fetchImpl = fetch,
1130
1148
  authorizeProject,
1131
1149
  verifyProjectAuthorization,
1150
+ readWebsite,
1132
1151
  } = {}) {
1133
1152
  const projectId = normalizeProjectId(assignment?.context?.framerProjectId);
1134
1153
  const projectUrl = normalizeProjectUrl(
@@ -2324,6 +2343,23 @@ export function createFramerAgentToolRuntime({
2324
2343
  if (queries.length > 0) instructionFallbacks += 1;
2325
2344
  result = builtInInstructions(queries.length > 0);
2326
2345
  }
2346
+ } else if (name === 'framer_read_website') {
2347
+ if (typeof readWebsite !== 'function') {
2348
+ throw toolError(
2349
+ 'FRAMER_WEBSITE_READ_UNAVAILABLE',
2350
+ 'Reading websites is not available in this run.',
2351
+ );
2352
+ }
2353
+ const website = await readWebsite({
2354
+ url: String(rawArguments.url || '').trim(),
2355
+ screenshotTop: Number.isInteger(rawArguments.screenshotTop)
2356
+ ? rawArguments.screenshotTop
2357
+ : undefined,
2358
+ });
2359
+ modelImages = (Array.isArray(website?.images) ? website.images : [])
2360
+ .filter((image) => typeof image?.data === 'string' && image.data)
2361
+ .map((image) => ({ url: null, mimeType: image.mimeType || 'image/jpeg', data: image.data }));
2362
+ result = { stdout: JSON.stringify(website?.result ?? {}), stderr: '' };
2327
2363
  } else if (name === 'framer_context') {
2328
2364
  result = await execCode(
2329
2365
  [
@@ -2898,6 +2934,9 @@ export function createFramerAgentToolRuntime({
2898
2934
  // Result size is what this call adds to every later model turn.
2899
2935
  toolCall.resultBytes = Buffer.byteLength(JSON.stringify(normalized), 'utf8');
2900
2936
  if (modelImages.length > 0) toolCall.imageCount = modelImages.length;
2937
+ if (name === 'framer_read_website') {
2938
+ return modelImages.length ? { ...normalized, modelImages } : normalized;
2939
+ }
2901
2940
  if (name !== 'framer_read_project') return normalized;
2902
2941
  const screenshotCount = screenshotQueries(rawArguments).length;
2903
2942
  return {
@@ -2955,6 +2994,10 @@ export function createFramerAgentToolRuntime({
2955
2994
  }
2956
2995
  toolCall.durationMs = Date.now() - startedAt;
2957
2996
  toolCall.errorCode = error?.code || 'FRAMER_AGENT_TOOL_FAILED';
2997
+ toolCall.errorMessage = boundedText(
2998
+ String(error?.message || 'Framer Agent tool failed.'),
2999
+ 300,
3000
+ );
2958
3001
  trace?.warn?.('framer_agent_tool_failed', {
2959
3002
  name,
2960
3003
  code: error?.code,
@@ -177,9 +177,21 @@ function codexOutcomeConfig() {
177
177
  };
178
178
  }
179
179
 
180
- function codexFramerOutcomeConfig() {
180
+ // Live web search is limited to the sites the user named; with none named
181
+ // the tool stays off.
182
+ function codexFramerOutcomeConfig(webHosts = []) {
183
+ const config = codexModelOnlyConfig();
181
184
  return {
182
- ...codexModelOnlyConfig(),
185
+ ...config,
186
+ ...(webHosts.length
187
+ ? {
188
+ web_search: 'live',
189
+ tools: {
190
+ ...config.tools,
191
+ web_search: { allowed_domains: webHosts },
192
+ },
193
+ }
194
+ : {}),
183
195
  history: {
184
196
  persistence: 'save-all',
185
197
  },
@@ -293,10 +305,50 @@ function codexItemProgress(item, phase, workspaceRoot) {
293
305
  return {};
294
306
  }
295
307
  if (type === 'webSearch' || type === 'web_search') {
308
+ const candidateUrl =
309
+ item.url
310
+ || item.uri
311
+ || item.action?.url
312
+ || item.query
313
+ || '';
314
+ let site = null;
315
+ const match = String(candidateUrl).match(/https?:\/\/[^\s"'<>]+/i);
316
+ if (match) {
317
+ try {
318
+ site = new URL(match[0]).hostname.toLowerCase();
319
+ } catch {
320
+ site = null;
321
+ }
322
+ }
323
+ const failed =
324
+ phase === 'completed'
325
+ && (
326
+ String(item.status || '').toLowerCase() === 'failed'
327
+ || Boolean(item.error)
328
+ );
296
329
  return {
297
330
  activity: 'search',
298
331
  phase,
299
332
  message: phase === 'started' ? 'Researching the requested change' : 'Finished research',
333
+ webCall: {
334
+ id: String(item.id || item.item_id || `web-search:${candidateUrl}`),
335
+ tool: 'WebSearch',
336
+ site,
337
+ status:
338
+ phase === 'started'
339
+ ? 'started'
340
+ : failed
341
+ ? 'failed'
342
+ : 'succeeded',
343
+ ...(failed
344
+ ? {
345
+ errorMessage:
346
+ item.error?.message
347
+ || item.error
348
+ || 'Codex web search failed.',
349
+ }
350
+ : {}),
351
+ },
300
352
  };
301
353
  }
302
354
  return {};
@@ -1078,7 +1130,7 @@ export function createCodexAppServerAdapter({
1078
1130
 
1079
1131
  async function ensureThread(
1080
1132
  runId,
1081
- { model, cwd: requestedCwd, harnessMode = false, dynamicTools = [], dynamicToolHandler, resumeThreadId } = {},
1133
+ { model, cwd: requestedCwd, harnessMode = false, dynamicTools = [], dynamicToolHandler, resumeThreadId, webHosts = [] } = {},
1082
1134
  ) {
1083
1135
  const resolution = await resolveProvider();
1084
1136
  const existing = threadsByRun.get(runId);
@@ -1094,6 +1146,7 @@ export function createCodexAppServerAdapter({
1094
1146
  harnessMode,
1095
1147
  framerHarness,
1096
1148
  tools: normalizedDynamicTools,
1149
+ webHosts,
1097
1150
  });
1098
1151
  if (existing && threadContexts.get(existing) === contextKey) {
1099
1152
  threadsByRun.delete(runId);
@@ -1145,7 +1198,7 @@ export function createCodexAppServerAdapter({
1145
1198
  developerInstructions: instructions,
1146
1199
  config: harnessMode
1147
1200
  ? framerHarness
1148
- ? codexFramerOutcomeConfig()
1201
+ ? codexFramerOutcomeConfig(webHosts)
1149
1202
  : codexOutcomeConfig()
1150
1203
  : codexModelOnlyConfig(),
1151
1204
  ...(model ? { model } : {}),
@@ -1232,6 +1285,7 @@ export function createCodexAppServerAdapter({
1232
1285
  harnessTools,
1233
1286
  resumeSessionId,
1234
1287
  imageFiles = [],
1288
+ webHosts = [],
1235
1289
  } = {}) {
1236
1290
  const transportSchema = outputSchema ? codexStructuredOutputSchema(outputSchema, { responseContract }) : null;
1237
1291
  const transportPrompt = transportSchema ? codexStructuredOutputPrompt(prompt, responseContract) : prompt;
@@ -1252,6 +1306,7 @@ export function createCodexAppServerAdapter({
1252
1306
  dynamicTools: harnessMode ? harnessTools?.definitions || [] : [],
1253
1307
  dynamicToolHandler: harnessMode ? harnessTools?.invoke : undefined,
1254
1308
  resumeThreadId: harnessMode ? resumeSessionId : undefined,
1309
+ webHosts: harnessMode ? webHosts : [],
1255
1310
  });
1256
1311
  const priorSkillDigest = invokedSkillDigestsByThread.get(threadId);
1257
1312
  const nativeSkillsReused =