smooth-operator-mcp 3.0.0 → 3.0.2

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/.env.example CHANGED
@@ -40,16 +40,16 @@ SMOOTH_OPERATOR_ALLOW_EVAL=true
40
40
  # these private roots by default.
41
41
  # SMOOTH_OPERATOR_ALLOWED_FILE_ROOTS=~/.smooth-operator/files,~/.smooth-operator/downloads
42
42
 
43
- # Optional automation-control baseline. The native default is enabled; set false
44
- # to preserve raw browser automation signals.
43
+ # Compatibility baseline. Browser identity and automation signals remain native;
44
+ # set false to skip the explicit-viewport compatibility script.
45
45
  # SMOOTH_OPERATOR_STEALTH_ENABLED=true
46
46
  # Stealth profile: balanced (default) | max (accepted compatibility names; same supported patch set)
47
47
  # SMOOTH_OPERATOR_STEALTH_PROFILE=balanced
48
48
  # Add opt-in GPU launch flags (does not make an identity/coherence guarantee)
49
49
  # SMOOTH_OPERATOR_STEALTH_GPU=false
50
- # Behavioral realism defaults on with short bounded timing; set false for the
51
- # fastest raw interaction path or override to decouple from stealth.
52
- # SMOOTH_OPERATOR_BEHAVIOR_ENABLED=true
50
+ # Behavioral timing is off for the fastest deterministic path; set true only
51
+ # when a site or workflow explicitly needs it.
52
+ # SMOOTH_OPERATOR_BEHAVIOR_ENABLED=false
53
53
 
54
54
  # HTTP is opt-in; use a strong token before enabling remote access
55
55
  # SMOOTH_OPERATOR_TRANSPORT=http
package/README.md CHANGED
@@ -44,9 +44,12 @@ Ask: *“Scrape pricing into a table”*, *“Fill this form with ~/resume.pdf
44
44
 
45
45
  ## How to use
46
46
 
47
- Talk to your harness normally. It can call `browser_navigate` → `browser_snapshot` → `browser_click` and the rest of the MCP surface as needed. For a challenge, `browser_solve_challenge` returns fresh bounded visual/state evidence for the connected AI; the AI uses ordinary browser actions and calls it again to verify the final classification. Human handoff remains available for logins or challenges that require the user.
47
+ Talk to your harness normally. It can call `browser_navigate` → `browser_snapshot` → `browser_click` and the rest of the MCP surface as needed. For a challenge, `browser_solve_challenge` returns fresh bounded visual/state evidence and an attempt budget; the connected AI keeps using ordinary browser actions and calls it again until the final classification is clear or the budget is exhausted. Human handoff remains available only as an explicit final option.
48
48
 
49
- Stealth and short behavioral timing are enabled in the recommended native profile. Page JavaScript is also available by default; set the explicit environment flags to `false` when a stricter profile is needed. See `docs/STEALTH-GUIDE.md` for details and responsible use.
49
+ Browser identity remains native, page JavaScript is available by default, and
50
+ behavioral timing is off for fast deterministic input. Set the explicit
51
+ environment flags to change those choices. See `docs/STEALTH-GUIDE.md` for
52
+ details and responsible use.
50
53
 
51
54
  For the fastest supported operation, keep behavioral timing off with
52
55
  `SMOOTH_OPERATOR_BEHAVIOR_ENABLED=false`; the native browser remains bounded and
@@ -397,7 +397,7 @@ var SERVER_VERSION;
397
397
  var init_version = __esm({
398
398
  "src/server/version.ts"() {
399
399
  "use strict";
400
- SERVER_VERSION = "3.0.0";
400
+ SERVER_VERSION = "3.0.2";
401
401
  }
402
402
  });
403
403
 
@@ -1554,7 +1554,7 @@ function writeSummary(ui, harness, choices) {
1554
1554
  ["Blocked sites", choices.blockedDomains.length ? choices.blockedDomains.join(", ") : "none"],
1555
1555
  ["Page JavaScript", choices.allowEval ? "enabled" : "off"],
1556
1556
  ["Stealth baseline", "enabled (balanced)"],
1557
- ["Behavioral realism", "enabled"],
1557
+ ["Behavioral timing", "off (fast native input)"],
1558
1558
  ["Data directory", choices.dataDir]
1559
1559
  ]
1560
1560
  ]);
@@ -1629,7 +1629,7 @@ async function persistWizardConfig(rawChoices, homeDir) {
1629
1629
  enabled: true,
1630
1630
  profile: "balanced",
1631
1631
  gpu: false,
1632
- behaviorEnabled: true
1632
+ behaviorEnabled: false
1633
1633
  };
1634
1634
  const config = { ...previous, browser: browserSection, security: securitySection, stealth: stealthSection };
1635
1635
  delete config.captchaSolver;
@@ -1734,7 +1734,7 @@ import { realpathSync as realpathSync2 } from "node:fs";
1734
1734
  import process4 from "node:process";
1735
1735
  import { Readable } from "node:stream";
1736
1736
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1737
- import { createMcpHandler } from "@modelcontextprotocol/server";
1737
+ import { createMcpHandler, isJsonContentType as sdkIsJsonContentType } from "@modelcontextprotocol/server";
1738
1738
  import { toNodeHandler } from "@modelcontextprotocol/node";
1739
1739
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
1740
1740
 
@@ -2570,7 +2570,7 @@ function loadServerConfig(args = [], environment = env, homeDirectory = homedir(
2570
2570
  enabled: stealthEnabled,
2571
2571
  profile: environment.SMOOTH_OPERATOR_STEALTH_PROFILE ?? nestedStealth.profile ?? "balanced",
2572
2572
  gpu: parseBoolean(environment.SMOOTH_OPERATOR_STEALTH_GPU, nestedStealth.gpu ?? false),
2573
- behaviorEnabled: environment.SMOOTH_OPERATOR_BEHAVIOR_ENABLED === void 0 ? nestedStealth.behaviorEnabled ?? true : parseBoolean(environment.SMOOTH_OPERATOR_BEHAVIOR_ENABLED, stealthEnabled)
2573
+ behaviorEnabled: environment.SMOOTH_OPERATOR_BEHAVIOR_ENABLED === void 0 ? nestedStealth.behaviorEnabled ?? false : parseBoolean(environment.SMOOTH_OPERATOR_BEHAVIOR_ENABLED, stealthEnabled)
2574
2574
  };
2575
2575
  const config = {
2576
2576
  transport: argValue("--transport") ?? environment.SMOOTH_OPERATOR_TRANSPORT ?? fileConfig.transport ?? "stdio",
@@ -2820,6 +2820,7 @@ var BrowserActionFieldsSchema = z2.object({
2820
2820
  verify: z2.boolean().optional(),
2821
2821
  durationMs: z2.number().int().min(0).max(3e4).optional(),
2822
2822
  pollMs: z2.number().int().min(250).max(1e4).optional(),
2823
+ maxAttempts: z2.number().int().min(1).max(100).optional(),
2823
2824
  optionValue: BoundedString(2e3).optional(),
2824
2825
  optionValues: z2.array(BoundedString(2e3)).min(1).max(200).optional(),
2825
2826
  cookieName: BoundedString(256).optional(),
@@ -3256,7 +3257,8 @@ var SolveChallengeRequestSchema = z2.object({
3256
3257
  full: z2.boolean().optional(),
3257
3258
  maxDimension: z2.number().int().min(1).max(2e4).optional(),
3258
3259
  max_dim: z2.number().int().min(1).max(2e4).optional(),
3259
- maxChars: z2.number().int().min(1e3).max(MCP_PAGE_TEXT_MAX_CHARS).optional()
3260
+ maxChars: z2.number().int().min(1e3).max(MCP_PAGE_TEXT_MAX_CHARS).optional(),
3261
+ maxAttempts: z2.number().int().min(1).max(100).optional()
3260
3262
  }).strict().superRefine((input, context) => {
3261
3263
  if (input.includeScreenshot !== void 0 && input.include_screenshot !== void 0) {
3262
3264
  context.addIssue({ code: "custom", message: "Provide includeScreenshot or include_screenshot, not both." });
@@ -3419,6 +3421,23 @@ var MCP_OUTPUT_TRUNCATION_MARKER_BYTES = UTF8_ENCODER2.encode(MCP_OUTPUT_TRUNCAT
3419
3421
  var MCP_ERROR_CODE_MAX_BYTES = 200;
3420
3422
  var MCP_ERROR_MESSAGE_MAX_BYTES = 4e3;
3421
3423
  var MCP_JSON_TEXT_CACHE = /* @__PURE__ */ new WeakMap();
3424
+ var MCP_OUTPUT_CONTRACT_ARRAY_KEYS = /* @__PURE__ */ new Set([
3425
+ "links",
3426
+ "results",
3427
+ "entries",
3428
+ "interactive",
3429
+ "nodes",
3430
+ "matches",
3431
+ "frames"
3432
+ ]);
3433
+ var MCP_OUTPUT_ARRAY_BOUNDS = [
3434
+ ["links", "linksTruncated"],
3435
+ ["entries", "entriesTruncated"],
3436
+ ["interactive", "interactiveTruncated"],
3437
+ ["nodes", "nodesTruncated"],
3438
+ ["matches", "matchesTruncated"],
3439
+ ["frames", "framesTruncated"]
3440
+ ];
3422
3441
  var NetworkIdleSchema = z3.object({
3423
3442
  timeoutMs: z3.number().int().min(100).max(12e4).optional(),
3424
3443
  pageId: z3.string().trim().min(1).max(200).optional()
@@ -3552,28 +3571,14 @@ var BrowserExecCodeSchema = z3.string().trim().min(1).max(8e4).superRefine((code
3552
3571
  context.addIssue({ code: "custom", message: "code must be a JSON array of validated browser actions." });
3553
3572
  return;
3554
3573
  }
3555
- if (!BrowserActionPlanSchema.safeParse(parsed).success) {
3556
- context.addIssue({ code: "custom", message: "code must be a non-empty JSON array of validated browser actions without nested scripts or screenshots." });
3574
+ if (!Array.isArray(parsed) || parsed.length === 0 || parsed.length > 100) {
3575
+ context.addIssue({ code: "custom", message: "code must be a non-empty JSON array of at most 100 browser actions." });
3557
3576
  }
3558
3577
  });
3559
3578
  var BrowserExecRequestSchema = z3.object({
3560
3579
  code: BrowserExecCodeSchema,
3561
3580
  confirmDestructive: z3.boolean().optional()
3562
- }).strict().superRefine((input, context) => {
3563
- if (input.confirmDestructive) {
3564
- return;
3565
- }
3566
- let parsed;
3567
- try {
3568
- parsed = JSON.parse(input.code);
3569
- } catch {
3570
- return;
3571
- }
3572
- const actions = BrowserActionPlanSchema.safeParse(parsed);
3573
- if (actions.success && actions.data.some((action) => isDestructiveBatchAction(action.action))) {
3574
- context.addIssue({ code: "custom", path: ["confirmDestructive"], message: "This action plan contains destructive actions. Set confirmDestructive=true to execute them." });
3575
- }
3576
- });
3581
+ }).strict();
3577
3582
  var BrowserUseStateSchema = z3.object({
3578
3583
  include_screenshot: z3.boolean().optional(),
3579
3584
  fullPage: z3.boolean().optional(),
@@ -3613,7 +3618,7 @@ var BROWSER_MUTATING = { ...MUTATING, openWorldHint: true };
3613
3618
  var BROWSER_DESTRUCTIVE = { ...DESTRUCTIVE, openWorldHint: true };
3614
3619
  var MCP_INSTRUCTIONS = [
3615
3620
  "Use browser_snapshot or browser_get_state before interacting so element refs/indexes and viewport coordinates are current.",
3616
- "Serialize dependent browser calls as observe -> one navigation or mutation -> observe. Parallel calls are appropriate only for independent read-only observations; a parallel snapshot and action do not form a transaction.",
3621
+ "Use an observe -> act -> verify loop: serialize dependent browser calls as one navigation or mutation between observations. Parallel calls are appropriate only for independent read-only observations; a parallel snapshot and action do not form a transaction.",
3617
3622
  "Give each request a bounded timeout or cancellation signal. After a timeout or cancellation, inspect current state before retrying a mutation; cancellation is not proof that a mutation did not happen.",
3618
3623
  "After navigation, tab switching, scrolling that changes lazy content, or any DOM-changing action, discard old refs and indexes and capture a fresh snapshot instead of silently falling back to coordinates, text, or a different selector.",
3619
3624
  "Only report titles, URLs, snippets, and metadata that are explicitly present in the returned MCP fields. Absence of a field is evidence of absence: never invent titles, summaries, counts, or other metadata that the tools did not return.",
@@ -3623,8 +3628,7 @@ var MCP_INSTRUCTIONS = [
3623
3628
  "Prefer stable refs, indexes, and selectors over coordinates; use coordinates only when the page cannot expose a reliable target.",
3624
3629
  "For open shadow roots, Puppeteer pierce/ selectors may be used explicitly; closed shadow roots remain unavailable.",
3625
3630
  "Use browser_batch for short validated sequences, but keep destructive actions separate when user confirmation is needed.",
3626
- "Use an efficient observe -> act -> verify loop: observe with browser_snapshot or browser_get_state, perform one bounded browser action, then observe again to verify the resulting state. Keep refs and indexes fresh after navigation, scrolling, or DOM changes; parallelize only independent read-only observations.",
3627
- "browser_solve_challenge is an internal connected-AI loop. It collects a fresh challenge classification plus bounded visual/state evidence, then the connected AI uses normal browser actions and calls it again to verify. Never claim a challenge is solved unless the final classification explicitly reports it absent; unknown or failed classification is not success.",
3631
+ "browser_solve_challenge is an internal connected-AI loop. Each call is one bounded verification cycle; present and exhausted classifications include fresh visual/state evidence and attemptsRemaining. The connected AI should keep using normal browser actions and call it again until the final classification explicitly reports the challenge absent or automation_exhausted. Never claim a challenge is solved from a present, unknown, or failed classification. Human handoff is only an explicit final option after exhaustion.",
3628
3632
  "The server contains no LLM or agent planner; the MCP client is responsible for reasoning, retries, and task completion."
3629
3633
  ].join(" ");
3630
3634
  function createMcpServer(runtime) {
@@ -3719,7 +3723,10 @@ function registerBrowserTools(server, runtime) {
3719
3723
  inputSchema: BrowserUseExtractSchema,
3720
3724
  annotations: BROWSER_READ_ONLY
3721
3725
  },
3722
- async (input, ctx) => callTool(() => runtime.run({ action: "extract", query: input.query, includeLinks: input.extract_links, pageId: input.pageId, frameId: input.frameId, maxChars: MCP_PAGE_TEXT_MAX_CHARS }, ctx.mcpReq.signal), runtime)
3726
+ async (input, ctx) => {
3727
+ const { extract_links, ...fields } = input;
3728
+ return callTool(() => runtime.run({ action: "extract", ...fields, includeLinks: extract_links, maxChars: MCP_PAGE_TEXT_MAX_CHARS }, ctx.mcpReq.signal), runtime);
3729
+ }
3723
3730
  );
3724
3731
  registerAction(server, runtime, "browser_navigate", "Navigate the browser", "Open an HTTP(S) URL after domain and private-network policy validation. DNS is checked before navigation but the browser resolver is not pinned. Set includeSnapshot=true for one trailing snapshot.", NavigateRequestSchema, "navigate", (input) => {
3725
3732
  const { new_tab, ...fields } = input;
@@ -3757,7 +3764,10 @@ function registerBrowserTools(server, runtime) {
3757
3764
  { title: "Read browser console log", description: "Enable, disable, read, clear, or read-and-clear the bounded console log.", inputSchema: NetworkLogRequestSchema, annotations: BROWSER_DESTRUCTIVE },
3758
3765
  async (input, ctx) => callTool(() => runtime.run({ action: consoleAction(input.operation), pageId: input.pageId }, ctx.mcpReq.signal), runtime)
3759
3766
  );
3760
- registerAction(server, runtime, "browser_find_text", "Find text", "Find and center the first matching text on the page.", PageQuerySchema, "find_text", (input) => ({ ...input, text: input.query }));
3767
+ registerAction(server, runtime, "browser_find_text", "Find text", "Find and center the first matching text on the page.", PageQuerySchema, "find_text", (input) => {
3768
+ const { query, ...fields } = input;
3769
+ return { ...fields, text: query };
3770
+ });
3761
3771
  registerAction(server, runtime, "browser_extract", "Extract page text", "Extract at most 8,000 page-text characters from the page or a CSS selector. Check truncated, offset, nextOffset, hasMore, and revision; use browser_page_next for later slices.", ExtractRequestSchema, "extract", (input) => ({ ...input, maxChars: input.maxChars ?? MCP_PAGE_TEXT_MAX_CHARS }));
3762
3772
  registerAction(server, runtime, "browser_upload", "Upload a file", "Upload a file from an allowed server file root into a file input.", UploadRequestSchema, "upload_file");
3763
3773
  registerAction(server, runtime, "browser_screenshot", "Capture a screenshot", "Capture a bounded PNG or JPEG screenshot of the current page.", ScreenshotRequestSchema, "screenshot", (input) => {
@@ -3776,7 +3786,10 @@ function registerBrowserTools(server, runtime) {
3776
3786
  registerAction(server, runtime, "browser_computed_style", "Read computed style", "Read a small safe subset of computed style for an element.", SelectorRequestSchema, "get_computed_style");
3777
3787
  registerAction(server, runtime, "browser_page_info", "Read page information", "Read URL, title, viewport, and document dimensions.", EmptyInputSchema, "get_page_info");
3778
3788
  registerAction(server, runtime, "browser_hover", "Hover an element", "Move the pointer over a CSS selector or snapshot ref.", TargetRequestSchema, "hover");
3779
- registerAction(server, runtime, "browser_move", "Move the pointer", "Move the pointer to bounded top-level viewport coordinates without clicking. Use this to inspect hover-driven UI before choosing a click point.", MoveRequestSchema, "move", (input) => ({ ...input, coordinateX: input.coordinateX ?? input.coordinate_x, coordinateY: input.coordinateY ?? input.coordinate_y }));
3789
+ registerAction(server, runtime, "browser_move", "Move the pointer", "Move the pointer to bounded top-level viewport coordinates without clicking. Use this to inspect hover-driven UI before choosing a click point.", MoveRequestSchema, "move", (input) => {
3790
+ const { coordinate_x, coordinate_y, ...fields } = input;
3791
+ return { ...fields, coordinateX: fields.coordinateX ?? coordinate_x, coordinateY: fields.coordinateY ?? coordinate_y };
3792
+ });
3780
3793
  registerAction(server, runtime, "browser_press_and_hold", "Press and hold or drag", "Press a mouse button on an element for a bounded duration. Optional startCoordinateX/startCoordinateY and endCoordinateX/endCoordinateY drag with interpolated mouse events; path supplies a bounded explicit pointer path for drawing or selection gestures.", HoldRequestSchema, "press_and_hold");
3781
3794
  registerAction(server, runtime, "browser_challenge", "Detect a web challenge", "Detect bounded challenge markers and return a fresh classification for the current page. A detected challenge is not evidence that it has been solved.", EmptyInputSchema, "detect_challenge");
3782
3795
  registerAction(server, runtime, "browser_wait_for_human", "Wait for human takeover", "Optionally wait for a user to complete a visible challenge or sign-in step in the browser. The result includes a fresh final classification.", WaitForHumanRequestSchema, "wait_for_human");
@@ -3784,7 +3797,7 @@ function registerBrowserTools(server, runtime) {
3784
3797
  "browser_solve_challenge",
3785
3798
  {
3786
3799
  title: "Solve a web challenge",
3787
- description: "Run the internal connected-AI challenge loop: collect fresh challenge classification and bounded visual/state evidence, let the connected AI use normal browser actions, then call again to verify. The result is successful only when the final classification explicitly reports the challenge absent.",
3800
+ description: "Run one cycle of the internal connected-AI challenge loop. Collect fresh bounded visual/state evidence, use normal browser actions, and call again until the challenge is explicitly absent or the bounded attempt budget is exhausted. No external solver or token injection is used.",
3788
3801
  inputSchema: SolveChallengeRequestSchema,
3789
3802
  annotations: BROWSER_READ_ONLY
3790
3803
  },
@@ -3811,11 +3824,24 @@ function registerBrowserTools(server, runtime) {
3811
3824
  "browser_exec",
3812
3825
  {
3813
3826
  title: "Execute a browser action program",
3814
- description: "Browser-use CLI compatibility entry point. The code must be a JSON array of validated browser actions; arbitrary Python or JavaScript is not executed.",
3827
+ description: "Browser-use CLI compatibility entry point. The code must be a JSON array of validated browser actions; it is not a shell or arbitrary Python runner. Page JavaScript is limited to the explicit evaluate action and server policy.",
3815
3828
  inputSchema: BrowserExecRequestSchema,
3816
3829
  annotations: BROWSER_DESTRUCTIVE
3817
3830
  },
3818
- async (input, ctx) => callBatchTool(() => runtime.runBatch(parseBrowserExecCode(input.code), { confirmDestructive: input.confirmDestructive }, ctx.mcpReq.signal), runtime)
3831
+ async (input, ctx) => callBatchTool(() => {
3832
+ const actions = parseBrowserExecCode(input.code);
3833
+ if (!input.confirmDestructive) {
3834
+ const destructiveIndex = actions.findIndex((action) => isDestructiveBatchAction(action.action));
3835
+ if (destructiveIndex >= 0) {
3836
+ const action = actions[destructiveIndex];
3837
+ throw new AppError("DESTRUCTIVE_CONFIRMATION_REQUIRED", `Action '${action.action}' must be executed separately or with confirmDestructive=true.`, {
3838
+ retryable: true,
3839
+ details: { failedIndex: destructiveIndex, failedAction: action.action, hint: "Set confirmDestructive=true or run the action separately." }
3840
+ });
3841
+ }
3842
+ }
3843
+ return runtime.runBatch(actions, { confirmDestructive: input.confirmDestructive }, ctx.mcpReq.signal);
3844
+ }, runtime)
3819
3845
  );
3820
3846
  server.registerTool(
3821
3847
  "browser_batch",
@@ -4138,7 +4164,7 @@ function truncateUtf82(value, maxBytes) {
4138
4164
  while (low < high) {
4139
4165
  const midpoint = Math.ceil((low + high) / 2);
4140
4166
  const candidate = decoder.decode(bytes.slice(0, midpoint));
4141
- if (UTF8_ENCODER2.encode(candidate).byteLength <= maxBytes) {
4167
+ if (UTF8_ENCODER2.encode(candidate).byteLength <= boundedMaxBytes) {
4142
4168
  low = midpoint;
4143
4169
  } else {
4144
4170
  high = midpoint - 1;
@@ -4259,9 +4285,8 @@ function boundMcpOutput(value, options = {}) {
4259
4285
  capArray("nodes", MCP_OUTPUT_NODE_LIMIT, "nodesTruncated");
4260
4286
  capArray("matches", MCP_OUTPUT_MATCH_LIMIT, "matchesTruncated");
4261
4287
  capArray("frames", 20, "framesTruncated");
4262
- const contractArrayKeys = /* @__PURE__ */ new Set(["links", "results", "entries", "interactive", "nodes", "matches", "frames"]);
4263
4288
  for (const [key, item] of Object.entries(output)) {
4264
- if (!contractArrayKeys.has(key) && Array.isArray(item)) {
4289
+ if (!MCP_OUTPUT_CONTRACT_ARRAY_KEYS.has(key) && Array.isArray(item)) {
4265
4290
  capArray(key, MCP_OUTPUT_ARRAY_ITEM_LIMIT, `${key}Truncated`);
4266
4291
  }
4267
4292
  }
@@ -4324,10 +4349,7 @@ function boundMcpOutput(value, options = {}) {
4324
4349
  markOutputTruncated();
4325
4350
  }
4326
4351
  }
4327
- const arrayBounds = [["links", "linksTruncated"], ["entries", "entriesTruncated"], ["interactive", "interactiveTruncated"], ["nodes", "nodesTruncated"], ["matches", "matchesTruncated"], ["frames", "framesTruncated"]];
4328
- if (!options.preserveBatchResults) {
4329
- arrayBounds.push(["results", "resultsTruncated"]);
4330
- }
4352
+ const arrayBounds = options.preserveBatchResults ? MCP_OUTPUT_ARRAY_BOUNDS : [...MCP_OUTPUT_ARRAY_BOUNDS, ["results", "resultsTruncated"]];
4331
4353
  for (const [key, flag] of arrayBounds) {
4332
4354
  while (jsonByteLength2(output) > MCP_OUTPUT_MAX_BYTES && Array.isArray(output[key]) && output[key].length > 1) {
4333
4355
  const items = output[key];
@@ -4732,81 +4754,16 @@ function boundedList(values) {
4732
4754
  }
4733
4755
 
4734
4756
  // src/server/browser/stealth.ts
4735
- var STEALTH_BASELINE_ARGS = [
4736
- "--disable-blink-features=AutomationControlled"
4737
- // hide navigator.webdriver at the C++ source
4738
- ];
4757
+ var STEALTH_BASELINE_ARGS = [];
4739
4758
  function buildStealthInitScript(profile, options = {}) {
4740
4759
  const { width, height } = profile.viewport;
4741
4760
  const applyViewport = options.applyViewport === true;
4742
- const head = `
4743
- // ---- shared helpers (ported, minimal) ----
4744
- function makeNativeString(fnName) {
4745
- return 'function ' + fnName + '() { [native code] }';
4746
- }
4747
- function patchToString(target, fnStr) {
4748
- try {
4749
- Object.defineProperty(target, 'toString', {
4750
- configurable: true,
4751
- writable: true,
4752
- value: function toString() { return fnStr; }
4753
- });
4754
- } catch (e) {}
4755
- }
4756
- function stripProxyFromErrors(fn) {
4757
- try {
4758
- return new Proxy(fn, {
4759
- apply: function applyTrap(target, thisArg, args) {
4760
- try { return Reflect.apply(target, thisArg, args); }
4761
- catch (e) { throw e; }
4762
- }
4763
- });
4764
- } catch (e) { return fn; }
4765
- }
4766
-
4767
- // ---- supported runtime values (only explicit configuration is interpolated) ----
4761
+ const source = `
4762
+ // Only an explicitly configured viewport is applied. Browser identity and
4763
+ // native automation signals remain untouched.
4768
4764
  var APPLY_VIEWPORT = ${String(applyViewport)};
4769
4765
  var VIEWPORT = { width: ${width}, height: ${height} };`;
4770
- const balanced = `
4771
- // 1. navigator.webdriver \u2014 belt-and-suspenders (the launch flag is primary).
4772
- try {
4773
- var navProto = Object.getPrototypeOf(navigator);
4774
- if (navProto && Object.prototype.hasOwnProperty.call(navProto, 'webdriver')) {
4775
- delete navProto.webdriver;
4776
- }
4777
- } catch (e) {}
4778
-
4779
- // 2. navigator.permissions.query \u2014 resolve the "impossible combination".
4780
- try {
4781
- if (navigator.permissions && typeof navigator.permissions.query === 'function') {
4782
- var perms = navigator.permissions;
4783
- var originalQuery = perms.query.bind(perms);
4784
- perms.query = function (query) {
4785
- if (query && query.name === 'notifications' &&
4786
- window.location && window.location.protocol !== 'https:') {
4787
- try {
4788
- if (typeof PermissionStatus !== 'undefined') {
4789
- return Promise.resolve(new PermissionStatus({ state: 'denied' }));
4790
- }
4791
- } catch (e) {}
4792
- }
4793
- return originalQuery(query);
4794
- };
4795
- }
4796
- } catch (e) {}
4797
-
4798
- // 3. toString / Proxy trace hiding \u2014 the patched getters hide themselves.
4799
- try {
4800
- if (navigator.permissions && typeof navigator.permissions.query === 'function') {
4801
- var hiddenQuery = stripProxyFromErrors(navigator.permissions.query);
4802
- patchToString(hiddenQuery, makeNativeString('query'));
4803
- navigator.permissions.query = hiddenQuery;
4804
- }
4805
- } catch (e) {}
4806
-
4807
- // Coherence: screen dimensions track an explicitly configured launch
4808
- // viewport (guarded, best-effort). Without that explicit input, leave the
4809
- // browser's real dimensions untouched.
4766
+ const viewport = `
4810
4767
  try {
4811
4768
  if (APPLY_VIEWPORT && window && typeof window.innerWidth === 'number') {
4812
4769
  Object.defineProperty(window, 'innerWidth', {
@@ -4818,8 +4775,8 @@ function buildStealthInitScript(profile, options = {}) {
4818
4775
  }
4819
4776
  } catch (e) {}`;
4820
4777
  return `(function () {
4821
- ${head}
4822
- ${balanced}
4778
+ ${source}
4779
+ ${viewport}
4823
4780
  })();
4824
4781
  `;
4825
4782
  }
@@ -4868,67 +4825,10 @@ function nativeBrowserLaunchArgs(options = {}) {
4868
4825
  init_discovery();
4869
4826
 
4870
4827
  // src/server/browser/fingerprints.ts
4871
- var DEFAULT_PROFILE = "balanced";
4872
- var DEFAULT_VERSION = 124;
4873
- var DEFAULT_PLATFORM = "Windows";
4874
- var DEFAULT_LANGUAGE = "en-US";
4875
- var DEFAULT_TIME_ZONE = "America/New_York";
4876
4828
  var DEFAULT_VIEWPORT_WIDTH = 1920;
4877
4829
  var DEFAULT_VIEWPORT_HEIGHT = 1080;
4878
- var DEFAULT_SEED = 0;
4879
- var PLATFORM_SEGMENTS = {
4880
- Windows: "Windows NT 10.0; Win64; x64",
4881
- macOS: "Mac OS X 10_15_7",
4882
- Linux: "X11; Linux x86_64"
4883
- };
4884
- var HARDWARE_CONCURRENCY_SET = [2, 4, 8, 16, 32];
4885
- var DEVICEMEMORY_SET = [1, 2, 4, 8];
4886
- var MAX_TOUCH_POINTS_SET = [0, 5, 10];
4887
4830
  function buildFingerprintProfile(options = {}) {
4888
- const profile = options.profile ?? DEFAULT_PROFILE;
4889
- const version = normalizeVersion(options.version);
4890
- const platform3 = normalizePlatform(options.platform);
4891
- const viewport = normalizeViewport(options.viewport);
4892
- const language = options.language ?? DEFAULT_LANGUAGE;
4893
- const seed = normalizeSeed(options.seed);
4894
- const versionStr = String(version);
4895
- const userAgent = `Mozilla/5.0 (${PLATFORM_SEGMENTS[platform3]}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${versionStr}.0.0.0 Safari/537.36`;
4896
- const brands = [
4897
- { brand: "Chromium", version: versionStr },
4898
- { brand: "Google Chrome", version: versionStr },
4899
- { brand: "Not=A?Brand", version: "8" }
4900
- ];
4901
- const fullVersionList = {
4902
- Chromium: versionStr,
4903
- "Google Chrome": versionStr
4904
- };
4905
- const profileWithLanguages = {
4906
- version,
4907
- userAgent,
4908
- platform: platform3,
4909
- mobile: false,
4910
- brands,
4911
- fullVersionList,
4912
- languages: buildLanguages(language),
4913
- acceptLanguage: `${language},en;q=0.9`,
4914
- viewport
4915
- };
4916
- if (profile === "max") {
4917
- return {
4918
- ...profileWithLanguages,
4919
- hardwareConcurrency: pickFromValidSet(HARDWARE_CONCURRENCY_SET, seed),
4920
- deviceMemory: pickFromValidSet(DEVICEMEMORY_SET, seed),
4921
- maxTouchPoints: pickFromValidSet(MAX_TOUCH_POINTS_SET, seed),
4922
- timeZone: options.timeZone ?? DEFAULT_TIME_ZONE
4923
- };
4924
- }
4925
- return profileWithLanguages;
4926
- }
4927
- function normalizeVersion(version) {
4928
- return typeof version === "number" && Number.isFinite(version) && version > 0 ? Math.floor(version) : DEFAULT_VERSION;
4929
- }
4930
- function normalizePlatform(platform3) {
4931
- return platform3 && platform3 in PLATFORM_SEGMENTS ? platform3 : DEFAULT_PLATFORM;
4831
+ return { viewport: normalizeViewport(options.viewport) };
4932
4832
  }
4933
4833
  function normalizeViewport(viewport) {
4934
4834
  if (viewport && Number.isFinite(viewport.width) && Number.isFinite(viewport.height) && viewport.width > 0 && viewport.height > 0) {
@@ -4936,16 +4836,6 @@ function normalizeViewport(viewport) {
4936
4836
  }
4937
4837
  return { width: DEFAULT_VIEWPORT_WIDTH, height: DEFAULT_VIEWPORT_HEIGHT };
4938
4838
  }
4939
- function normalizeSeed(seed) {
4940
- return typeof seed === "number" && Number.isFinite(seed) ? seed : DEFAULT_SEED;
4941
- }
4942
- function buildLanguages(language) {
4943
- const parts = language.split(/[-_]/);
4944
- return parts.length > 1 ? [language, parts[0].toLowerCase()] : [language];
4945
- }
4946
- function pickFromValidSet(set, seed) {
4947
- return set[Math.floor(Math.abs(seed)) % set.length];
4948
- }
4949
4839
 
4950
4840
  // src/server/browser/utils.ts
4951
4841
  var SENSITIVE_URL_PART = /(access[_-]?token|api[_-]?key|auth|code|credential|jwt|nonce|otp|password|secret|session|sig(?:nature)?|token)/i;
@@ -5051,6 +4941,8 @@ var MAX_DOM_TRAVERSAL_NODES = 2e4;
5051
4941
  var MAX_TEXT_SCAN_CHARS = 5e5;
5052
4942
  var MAX_MARKUP_EVIDENCE_CHARS = 12e4;
5053
4943
  var CHALLENGE_AI_GUIDANCE = "Use normal browser click, input, scroll, or key tools on the visible challenge controls, then call solve_challenge again to verify that the challenge is cleared.";
4944
+ var CHALLENGE_DEFAULT_MAX_ATTEMPTS = 32;
4945
+ var CHALLENGE_MAX_ATTEMPTS = 100;
5054
4946
  var MAX_DOWNLOAD_ENTRIES = 100;
5055
4947
  var TARGET_GUARD_MAX_REQUEST_IDS = 128;
5056
4948
  var CLICK_SETTLE_TIMEOUT_MS = 10;
@@ -8338,6 +8230,7 @@ var BrowserService = class {
8338
8230
  this.clearTargetGuardNavigationError(state.page);
8339
8231
  state.policyVerifiedUrls?.clear();
8340
8232
  state.challengeStatus = void 0;
8233
+ state.challengeAttempts = 0;
8341
8234
  } catch (error) {
8342
8235
  this.logger.debug("Blocked navigation recovery could not restore a blank page", { pageId: state.id, error: String(error) });
8343
8236
  }
@@ -8570,7 +8463,7 @@ var BrowserService = class {
8570
8463
  state.stealthInjected = true;
8571
8464
  } catch (error) {
8572
8465
  throwIfAborted(signal);
8573
- throw new AppError("STEALTH_INITIALIZATION_FAILED", "The stealth fingerprint init script could not be injected.", { retryable: true, cause: error });
8466
+ throw new AppError("STEALTH_INITIALIZATION_FAILED", "The browser compatibility script could not be injected.", { retryable: true, cause: error });
8574
8467
  }
8575
8468
  }
8576
8469
  await this.releaseTargetGuardForPage(state.page);
@@ -8751,6 +8644,7 @@ var BrowserService = class {
8751
8644
  const mainFrame = state.page.mainFrame();
8752
8645
  if (frame === mainFrame) {
8753
8646
  state.challengeStatus = void 0;
8647
+ state.challengeAttempts = 0;
8754
8648
  }
8755
8649
  } catch (error) {
8756
8650
  this.logger.debug("Browser frame navigation event was unavailable after page disposal", { pageId: state.id, error: String(error) });
@@ -10372,12 +10266,15 @@ var BrowserService = class {
10372
10266
  async solveChallenge(state, action, signal) {
10373
10267
  throwIfAborted(signal);
10374
10268
  const previousChallengeStatus = state.challengeStatus;
10269
+ const requestedMaxAttempts = action.maxAttempts;
10270
+ const maxAttempts = Number.isFinite(requestedMaxAttempts) ? Math.min(CHALLENGE_MAX_ATTEMPTS, Math.max(1, Math.floor(requestedMaxAttempts))) : CHALLENGE_DEFAULT_MAX_ATTEMPTS;
10375
10271
  const detection = await this.detectChallenge(state, signal);
10376
10272
  if (isChallengeUnknown(detection)) {
10377
10273
  state.challengeStatus = "unknown";
10378
10274
  return {
10379
10275
  solved: false,
10380
10276
  verified: false,
10277
+ challengeState: "unknown",
10381
10278
  resolution: "challenge_state_unverified",
10382
10279
  verification: "unknown",
10383
10280
  workflow: "verification_unavailable",
@@ -10389,18 +10286,41 @@ var BrowserService = class {
10389
10286
  }
10390
10287
  if (isChallengeAbsent(detection)) {
10391
10288
  const cleared = previousChallengeStatus === "present";
10289
+ const attempts2 = state.challengeAttempts ?? 0;
10392
10290
  state.challengeStatus = "absent";
10291
+ state.challengeAttempts = 0;
10393
10292
  return {
10394
10293
  solved: true,
10395
10294
  verified: true,
10295
+ challengeState: "clear",
10396
10296
  resolution: cleared ? "challenge_cleared" : "no_challenge",
10397
10297
  verification: "verified",
10398
10298
  workflow: "verified",
10399
10299
  pageId: state.id,
10300
+ ...attempts2 > 0 ? { attempts: attempts2 } : {},
10400
10301
  classification: detection
10401
10302
  };
10402
10303
  }
10403
10304
  state.challengeStatus = "present";
10305
+ const attempts = (state.challengeAttempts ?? 0) + 1;
10306
+ state.challengeAttempts = attempts;
10307
+ if (attempts > maxAttempts) {
10308
+ return {
10309
+ solved: false,
10310
+ verified: true,
10311
+ challengeState: "present",
10312
+ resolution: "automation_exhausted",
10313
+ verification: "challenge_present",
10314
+ workflow: "human_handoff_available",
10315
+ pageId: state.id,
10316
+ attempts: attempts - 1,
10317
+ maxAttempts,
10318
+ attemptsRemaining: 0,
10319
+ classification: detection,
10320
+ nextAction: "Automation attempts are exhausted. Human handoff is available if the site permits it.",
10321
+ guidance: "Automation attempts are exhausted. Human handoff is available if the site permits it."
10322
+ };
10323
+ }
10404
10324
  const includeScreenshot = action.includeScreenshot ?? action.include_screenshot ?? true;
10405
10325
  const requestedMaxDimension = action.maxDimension ?? action.max_dim;
10406
10326
  const maxDimension = Number.isFinite(requestedMaxDimension) ? Math.min(1600, Math.max(100, Math.floor(requestedMaxDimension))) : 1600;
@@ -10430,6 +10350,9 @@ var BrowserService = class {
10430
10350
  snapshotId: snapshot.snapshotId,
10431
10351
  domRevision: snapshot.domRevision,
10432
10352
  viewport: snapshot.viewport,
10353
+ attempts,
10354
+ maxAttempts,
10355
+ attemptsRemaining: Math.max(0, maxAttempts - attempts),
10433
10356
  refs: stableRefs,
10434
10357
  interactive: stableRefs,
10435
10358
  classification: detection,
@@ -12166,7 +12089,7 @@ var ServerRuntime = class _ServerRuntime {
12166
12089
  headedBrowser: true,
12167
12090
  pageEvaluation: true,
12168
12091
  stealth: true,
12169
- behavioralTiming: true
12092
+ behavioralTiming: false
12170
12093
  },
12171
12094
  features: {
12172
12095
  localBrowserTools: "available",
@@ -12196,7 +12119,9 @@ var ServerRuntime = class _ServerRuntime {
12196
12119
  classification: "bounded-evidence",
12197
12120
  connectedAiLoop: true,
12198
12121
  humanHandoff: true,
12199
- successRequiresAbsentClassification: true
12122
+ successRequiresAbsentClassification: true,
12123
+ defaultMaxAttempts: 32,
12124
+ maxAttempts: 100
12200
12125
  },
12201
12126
  persistence: {
12202
12127
  fileRootsConfigured: this.config.security.allowedFileRoots.length > 0,
@@ -12309,7 +12234,9 @@ async function reclaimStaleLock(lockPath) {
12309
12234
  if (after.ino !== before.ino || after.dev !== before.dev) {
12310
12235
  return false;
12311
12236
  }
12312
- await rename2(lockPath, `${lockPath}.stale-${randomUUID2()}`);
12237
+ const stalePath = `${lockPath}.stale-${randomUUID2()}`;
12238
+ await rename2(lockPath, stalePath);
12239
+ await unlink2(stalePath).catch(() => void 0);
12313
12240
  return true;
12314
12241
  } catch {
12315
12242
  return false;
@@ -12520,6 +12447,10 @@ var HTTP_NOT_FOUND_BODY = JSON.stringify({ error: "not_found" });
12520
12447
  var HTTP_SHUTTING_DOWN_BODY = JSON.stringify({ error: "server_shutting_down" });
12521
12448
  var HTTP_BUSY_BODY = JSON.stringify({ error: "server_busy" });
12522
12449
  var HTTP_UNAUTHORIZED_BODY = JSON.stringify({ error: "unauthorized" });
12450
+ var HTTP_UNSUPPORTED_MEDIA_BODY = JSON.stringify({
12451
+ jsonrpc: "2.0",
12452
+ error: { code: -32e3, message: "Unsupported Media Type: Content-Type must be application/json" }
12453
+ });
12523
12454
  async function main(args = process4.argv.slice(2)) {
12524
12455
  if (args[0] === "install") {
12525
12456
  const yes = args.includes("--yes") || args.includes("--no-interactive");
@@ -12867,6 +12798,14 @@ async function dispatchHttpRequest(request, response, nodeHandler, maxBodyBytes,
12867
12798
  if (request.aborted) {
12868
12799
  throw new AppError("HTTP_REQUEST_ABORTED", "The HTTP client disconnected before the request completed.", { status: 499, retryable: true });
12869
12800
  }
12801
+ const contentType = request.headers["content-type"];
12802
+ if (request.method?.toUpperCase() === "POST" && (typeof contentType !== "string" || !sdkIsJsonContentType(contentType))) {
12803
+ response.setHeader("connection", "close");
12804
+ closeIncompleteRequestAfterResponse(request, response);
12805
+ response.writeHead(415, { "content-type": "application/json" });
12806
+ response.end(HTTP_UNSUPPORTED_MEDIA_BODY);
12807
+ return;
12808
+ }
12870
12809
  const contentLength = Number(request.headers["content-length"] ?? 0);
12871
12810
  if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) {
12872
12811
  closeIncompleteRequestAfterResponse(request, response);